summaryrefslogtreecommitdiff
path: root/modules/md
diff options
context:
space:
mode:
authorStefan Eissing <icing@apache.org>2022-04-27 11:48:36 +0000
committerStefan Eissing <icing@apache.org>2022-04-27 11:48:36 +0000
commit6b5e7d4588b98810788ed4a5ff73f2cd14ed7835 (patch)
tree9f80b49223aafcbf61913cc58d89b38fa00d4d15 /modules/md
parent33a168cc58c8315f6cb4ffc688b8a06ed0300afa (diff)
downloadhttpd-6b5e7d4588b98810788ed4a5ff73f2cd14ed7835.tar.gz
*) mod_md: added support for managing certificates via a
local tailscale demon for users of that secure networking. This gives trusted certificates for tailscale assigned domain names in the *.ts.net space. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1900313 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'modules/md')
-rw-r--r--modules/md/config2.m41
-rw-r--r--modules/md/md_acme_drive.c12
-rw-r--r--modules/md/md_core.c2
-rw-r--r--modules/md/md_crypt.c52
-rw-r--r--modules/md/md_crypt.h11
-rw-r--r--modules/md/md_curl.c3
-rw-r--r--modules/md/md_http.c7
-rw-r--r--modules/md/md_http.h7
-rw-r--r--modules/md/md_json.c4
-rw-r--r--modules/md/md_reg.c22
-rw-r--r--modules/md/md_reg.h2
-rw-r--r--modules/md/md_store_fs.c4
-rw-r--r--modules/md/md_tailscale.c381
-rw-r--r--modules/md/md_tailscale.h25
-rw-r--r--modules/md/md_util.c10
-rw-r--r--modules/md/md_util.h1
-rw-r--r--modules/md/md_version.h7
-rw-r--r--modules/md/mod_md.dsp4
-rw-r--r--modules/md/mod_md_config.c3
-rw-r--r--modules/md/mod_md_status.c26
20 files changed, 565 insertions, 19 deletions
diff --git a/modules/md/config2.m4 b/modules/md/config2.m4
index b20ab3b45e..e416736a8c 100644
--- a/modules/md/config2.m4
+++ b/modules/md/config2.m4
@@ -156,6 +156,7 @@ md_reg.lo dnl
md_status.lo dnl
md_store.lo dnl
md_store_fs.lo dnl
+md_tailscale.lo dnl
md_time.lo dnl
md_util.lo dnl
mod_md.lo dnl
diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c
index 6c461dc9af..bc0f17f271 100644
--- a/modules/md/md_acme_drive.c
+++ b/modules/md/md_acme_drive.c
@@ -1036,9 +1036,19 @@ static apr_status_t acme_driver_preload(md_proto_driver_t *d,
return rv;
}
+static apr_status_t acme_complete_md(md_t *md, apr_pool_t *p)
+{
+ (void)p;
+ if (!md->ca_url) {
+ md->ca_url = MD_ACME_DEF_URL;
+ }
+ return APR_SUCCESS;
+}
+
static md_proto_t ACME_PROTO = {
MD_PROTO_ACME, acme_driver_init, acme_driver_renew,
- acme_driver_preload_init, acme_driver_preload
+ acme_driver_preload_init, acme_driver_preload,
+ acme_complete_md,
};
apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p)
diff --git a/modules/md/md_core.c b/modules/md/md_core.c
index 620b809984..f82f950503 100644
--- a/modules/md/md_core.c
+++ b/modules/md/md_core.c
@@ -427,7 +427,7 @@ apr_status_t md_get_ca_url_from_name(const char **purl, apr_pool_t *p, const cha
}
}
*purl = name;
- rv = md_util_abs_http_uri_check(p, name, &err);
+ rv = md_util_abs_uri_check(p, name, &err);
if (APR_SUCCESS != rv) {
apr_array_header_t *names;
diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c
index 73e8900977..8baab51c8b 100644
--- a/modules/md/md_crypt.c
+++ b/modules/md/md_crypt.c
@@ -709,6 +709,53 @@ apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p,
return rv;
}
+apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool,
+ const struct md_http_response_t *res)
+{
+ apr_status_t rv;
+ apr_off_t data_len;
+ char *pem_data;
+ apr_size_t pem_len;
+ md_pkey_t *pkey;
+ BIO *bf;
+ passwd_ctx ctx;
+
+ rv = apr_brigade_length(res->body, 1, &data_len);
+ if (APR_SUCCESS != rv) goto leave;
+ if (data_len > 1024*1024) { /* certs usually are <2k each */
+ rv = APR_EINVAL;
+ goto leave;
+ }
+ rv = apr_brigade_pflatten(res->body, &pem_data, &pem_len, res->req->pool);
+ if (APR_SUCCESS != rv) goto leave;
+
+ if (NULL == (bf = BIO_new_mem_buf(pem_data, (int)pem_len))) {
+ rv = APR_ENOMEM;
+ goto leave;
+ }
+ pkey = make_pkey(pool);
+ ctx.pass_phrase = NULL;
+ ctx.pass_len = 0;
+ ERR_clear_error();
+ pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, NULL, &ctx);
+ BIO_free(bf);
+
+ if (pkey->pkey == NULL) {
+ unsigned long err = ERR_get_error();
+ rv = APR_EINVAL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, pool,
+ "error loading pkey from http response: %s",
+ ERR_error_string(err, NULL));
+ goto leave;
+ }
+ rv = APR_SUCCESS;
+ apr_pool_cleanup_register(pool, pkey, pkey_cleanup, apr_pool_cleanup_null);
+
+leave:
+ *ppkey = (APR_SUCCESS == rv)? pkey : NULL;
+ return rv;
+}
+
/* Determine the message digest used for signing with the given private key.
*/
static const EVP_MD *pkey_get_MD(md_pkey_t *pkey)
@@ -1137,6 +1184,11 @@ const char *md_cert_get_serial_number(const md_cert_t *cert, apr_pool_t *p)
return s;
}
+int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b)
+{
+ return X509_cmp(a->x509, b->x509) == 0;
+}
+
int md_cert_is_valid_now(const md_cert_t *cert)
{
return ((X509_cmp_current_time(X509_get_notBefore(cert->x509)) < 0)
diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h
index 203dc40f40..a892e00f1e 100644
--- a/modules/md/md_crypt.h
+++ b/modules/md/md_crypt.h
@@ -117,6 +117,12 @@ void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey);
apr_status_t md_crypt_hmac64(const char **pmac64, const struct md_data_t *hmac_key,
apr_pool_t *p, const char *d, size_t dlen);
+/**
+ * Read a private key from a http response.
+ */
+apr_status_t md_pkey_read_http(md_pkey_t **ppkey, apr_pool_t *pool,
+ const struct md_http_response_t *res);
+
/**************************************************************************************************/
/* X509 certificates */
@@ -179,6 +185,11 @@ apr_time_t md_cert_get_not_after(const md_cert_t *cert);
apr_time_t md_cert_get_not_before(const md_cert_t *cert);
struct md_timeperiod_t md_cert_get_valid(const md_cert_t *cert);
+/**
+ * Return != 0 iff the hash values of the certificates are equal.
+ */
+int md_certs_are_equal(const md_cert_t *a, const md_cert_t *b);
+
apr_status_t md_cert_get_issuers_uri(const char **puri, const md_cert_t *cert, apr_pool_t *p);
apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, const md_cert_t *cert, apr_pool_t *p);
diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c
index e05f37b899..e3f32d3548 100644
--- a/modules/md/md_curl.c
+++ b/modules/md/md_curl.c
@@ -301,6 +301,9 @@ static apr_status_t internals_setup(md_http_request_t *req)
if (req->ca_file) {
curl_easy_setopt(curl, CURLOPT_CAINFO, req->ca_file);
}
+ if (req->unix_socket_path) {
+ curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, req->unix_socket_path);
+ }
if (req->body_len >= 0) {
/* set the Content-Length */
diff --git a/modules/md/md_http.c b/modules/md/md_http.c
index 53e4f89d99..74db961301 100644
--- a/modules/md/md_http.c
+++ b/modules/md/md_http.c
@@ -33,6 +33,7 @@ struct md_http_t {
void *impl_data; /* to be used by the implementation */
const char *user_agent;
const char *proxy_url;
+ const char *unix_socket_path;
md_http_timeouts_t timeout;
const char *ca_file;
};
@@ -143,6 +144,11 @@ void md_http_set_ca_file(md_http_t *http, const char *ca_file)
http->ca_file = ca_file;
}
+void md_http_set_unix_socket_path(md_http_t *http, const char *path)
+{
+ http->unix_socket_path = path;
+}
+
static apr_status_t req_set_body(md_http_request_t *req, const char *content_type,
apr_bucket_brigade *body, apr_off_t body_len,
int detect_len)
@@ -211,6 +217,7 @@ static apr_status_t req_create(md_http_request_t **preq, md_http_t *http,
req->proxy_url = http->proxy_url;
req->timeout = http->timeout;
req->ca_file = http->ca_file;
+ req->unix_socket_path = http->unix_socket_path;
*preq = req;
return rv;
}
diff --git a/modules/md/md_http.h b/modules/md/md_http.h
index e24de03113..c210aa9913 100644
--- a/modules/md/md_http.h
+++ b/modules/md/md_http.h
@@ -65,6 +65,7 @@ struct md_http_request_t {
const char *user_agent;
const char *proxy_url;
const char *ca_file;
+ const char *unix_socket_path;
apr_table_t *headers;
struct apr_bucket_brigade *body;
apr_off_t body_len;
@@ -118,6 +119,12 @@ void md_http_set_stalling(md_http_request_t *req, long bytes_per_sec, apr_time_t
void md_http_set_ca_file(md_http_t *http, const char *ca_file);
/**
+ * Set the path of a unix domain socket for use instead of TCP
+ * in a connection. Disable by providing NULL as path.
+ */
+void md_http_set_unix_socket_path(md_http_t *http, const char *path);
+
+/**
* Perform the request. Then this function returns, the request and
* all its memory has been freed and must no longer be used.
*/
diff --git a/modules/md/md_json.c b/modules/md/md_json.c
index 161b2c7910..e0f977ea56 100644
--- a/modules/md/md_json.c
+++ b/modules/md/md_json.c
@@ -176,7 +176,7 @@ static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap)
aj = json_object_get(j, key);
if (!aj) {
aj = json_array();
- json_object_set(j, key, aj);
+ json_object_set_new(j, key, aj);
}
if (!json_is_array(aj)) {
@@ -202,7 +202,7 @@ static apr_status_t jselect_insert(json_t *val, size_t index, md_json_t *json, v
aj = json_object_get(j, key);
if (!aj) {
aj = json_array();
- json_object_set(j, key, aj);
+ json_object_set_new(j, key, aj);
}
if (!json_is_array(aj)) {
diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c
index 4fba9c7ae4..0c59aeb737 100644
--- a/modules/md/md_reg.c
+++ b/modules/md/md_reg.c
@@ -33,6 +33,7 @@
#include "md_reg.h"
#include "md_store.h"
#include "md_status.h"
+#include "md_tailscale.h"
#include "md_util.h"
#include "md_acme.h"
@@ -98,7 +99,8 @@ apr_status_t md_reg_create(md_reg_t **preg, apr_pool_t *p, struct md_store_t *st
md_timeslice_create(&reg->renew_window, p, MD_TIME_LIFE_NORM, MD_TIME_RENEW_WINDOW_DEF);
md_timeslice_create(&reg->warn_window, p, MD_TIME_LIFE_NORM, MD_TIME_WARN_WINDOW_DEF);
- if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))) {
+ if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))
+ && APR_SUCCESS == (rv = md_tailscale_protos_add(reg->protos, p))) {
rv = load_props(reg, p);
}
@@ -901,12 +903,22 @@ apr_status_t md_reg_sync_finish(md_reg_t *reg, md_t *md, apr_pool_t *p, apr_pool
md_t *old;
apr_status_t rv;
int changed = 1;
+ md_proto_t *proto;
- if (!md->ca_url) {
- md->ca_url = MD_ACME_DEF_URL;
- md->ca_proto = MD_PROTO_ACME;
+ if (!md->ca_proto) {
+ md->ca_proto = MD_PROTO_ACME;
+ }
+ proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto));
+ if (!proto) {
+ rv = APR_ENOTIMPL;
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp,
+ "[%s] uses unknown CA protocol '%s'",
+ md->name, md->ca_proto);
+ goto leave;
}
-
+ rv = proto->complete_md(md, p);
+ if (APR_SUCCESS != rv) goto leave;
+
rv = state_init(reg, p, md);
if (APR_SUCCESS != rv) goto leave;
diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h
index 46034b0fe6..aa626c9276 100644
--- a/modules/md/md_reg.h
+++ b/modules/md/md_reg.h
@@ -220,6 +220,7 @@ typedef apr_status_t md_proto_renew_cb(md_proto_driver_t *driver, struct md_resu
typedef apr_status_t md_proto_init_preload_cb(md_proto_driver_t *driver, struct md_result_t *result);
typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver,
md_store_group_t group, struct md_result_t *result);
+typedef apr_status_t md_proto_complete_md_cb(md_t *md, apr_pool_t *p);
struct md_proto_t {
const char *protocol;
@@ -227,6 +228,7 @@ struct md_proto_t {
md_proto_renew_cb *renew;
md_proto_init_preload_cb *init_preload;
md_proto_preload_cb *preload;
+ md_proto_complete_md_cb *complete_md;
};
/**
diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c
index c2a4a4e493..ab43054ce5 100644
--- a/modules/md/md_store_fs.c
+++ b/modules/md/md_store_fs.c
@@ -503,7 +503,6 @@ static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs,
perms = gperms(s_fs, group);
- *pdir = NULL;
rv = fs_get_dname(pdir, &s_fs->s, group, name, p);
if ((APR_SUCCESS != rv) || (MD_SG_NONE == group)) goto cleanup;
@@ -522,8 +521,7 @@ static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs,
}
cleanup:
if (APR_SUCCESS != rv) {
- md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "mk_group_dir %d %s",
- group, (*pdir? *pdir : (name? name : "(null)")));
+ md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "mk_group_dir %d %s", group, name);
}
return rv;
}
diff --git a/modules/md/md_tailscale.c b/modules/md/md_tailscale.c
new file mode 100644
index 0000000000..dd3b1458ad
--- /dev/null
+++ b/modules/md/md_tailscale.c
@@ -0,0 +1,381 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_result.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_tailscale.h"
+
+typedef struct {
+ apr_pool_t *pool;
+ md_proto_driver_t *driver;
+ const char *unix_socket_path;
+ md_t *md;
+ apr_array_header_t *chain;
+ md_pkey_t *pkey;
+} ts_ctx_t;
+
+static apr_status_t ts_init(md_proto_driver_t *d, md_result_t *result)
+{
+ ts_ctx_t *ts_ctx;
+ apr_uri_t uri;
+ const char *ca_url;
+ apr_status_t rv = APR_SUCCESS;
+
+ md_result_set(result, APR_SUCCESS, NULL);
+ ts_ctx = apr_pcalloc(d->p, sizeof(*ts_ctx));
+ ts_ctx->pool = d->p;
+ ts_ctx->driver = d;
+ ts_ctx->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *));
+
+ ca_url = d->md->ca_url;
+ if (!ca_url) {
+ ca_url = MD_TAILSCALE_DEF_URL;
+ }
+ rv = apr_uri_parse(d->p, ca_url, &uri);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "error parsing CA URL `%s`", ca_url);
+ goto leave;
+ }
+ if (uri.scheme && uri.scheme[0] && strcmp("file", uri.scheme)) {
+ rv = APR_ENOTIMPL;
+ md_result_printf(result, rv, "non `file` URLs not supported, CA URL is `%s`",
+ ca_url);
+ goto leave;
+ }
+ if (uri.hostname && uri.hostname[0] && strcmp("localhost", uri.hostname)) {
+ rv = APR_ENOTIMPL;
+ md_result_printf(result, rv, "non `localhost` URLs not supported, CA URL is `%s`",
+ ca_url);
+ goto leave;
+ }
+ ts_ctx->unix_socket_path = uri.path;
+ d->baton = ts_ctx;
+
+leave:
+ return rv;
+}
+
+static apr_status_t ts_preload_init(md_proto_driver_t *d, md_result_t *result)
+{
+ return ts_init(d, result);
+}
+
+static apr_status_t ts_preload(md_proto_driver_t *d,
+ md_store_group_t load_group, md_result_t *result)
+{
+ apr_status_t rv;
+ md_t *md;
+ md_credentials_t *creds;
+ md_pkey_spec_t *pkspec;
+ apr_array_header_t *all_creds;
+ const char *name;
+ int i;
+
+ name = d->md->name;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: preload start", name);
+ /* Load data from MD_SG_STAGING and save it into "load_group".
+ */
+ if (APR_SUCCESS != (rv = md_load(d->store, MD_SG_STAGING, name, &md, d->p))) {
+ md_result_set(result, rv, "loading staged md.json");
+ goto leave;
+ }
+
+ /* tailscale generates one cert+key with key specification being whatever
+ * it chooses. Use the NULL spec here.
+ */
+ all_creds = apr_array_make(d->p, 5, sizeof(md_credentials_t*));
+ pkspec = NULL;
+ if (APR_SUCCESS != (rv = md_creds_load(d->store, MD_SG_STAGING, name, pkspec, &creds, d->p))) {
+ md_result_printf(result, rv, "loading staged credentials");
+ goto leave;
+ }
+ if (!creds->chain) {
+ rv = APR_ENOENT;
+ md_result_printf(result, rv, "no certificate in staged credentials");
+ goto leave;
+ }
+ if (APR_SUCCESS != (rv = md_check_cert_and_pkey(creds->chain, creds->pkey))) {
+ md_result_printf(result, rv, "certificate and private key do not match in staged credentials");
+ goto leave;
+ }
+ APR_ARRAY_PUSH(all_creds, md_credentials_t*) = creds;
+
+ md_result_activity_setn(result, "purging store tmp space");
+ rv = md_store_purge(d->store, d->p, load_group, name);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, NULL);
+ goto leave;
+ }
+
+ md_result_activity_setn(result, "saving staged md/privkey/pubcert");
+ if (APR_SUCCESS != (rv = md_save(d->store, d->p, load_group, md, 1))) {
+ md_result_set(result, rv, "writing md.json");
+ goto leave;
+ }
+
+ for (i = 0; i < all_creds->nelts; ++i) {
+ creds = APR_ARRAY_IDX(all_creds, i, md_credentials_t*);
+ if (APR_SUCCESS != (rv = md_creds_save(d->store, d->p, load_group, name, creds, 1))) {
+ md_result_printf(result, rv, "writing credentials #%d", i);
+ goto leave;
+ }
+ }
+
+ md_result_set(result, APR_SUCCESS, "saved staged data successfully");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t rv_of_response(const md_http_response_t *res)
+{
+ switch (res->status) {
+ case 200:
+ return APR_SUCCESS;
+ case 400:
+ return APR_EINVAL;
+ case 401: /* sectigo returns this instead of 403 */
+ case 403:
+ return APR_EACCES;
+ case 404:
+ return APR_ENOENT;
+ default:
+ return APR_EGENERAL;
+ }
+ return APR_SUCCESS;
+}
+
+static apr_status_t on_get_cert(const md_http_response_t *res, void *baton)
+{
+ ts_ctx_t *ts_ctx = baton;
+ apr_status_t rv;
+
+ rv = rv_of_response(res);
+ if (APR_SUCCESS != rv) goto leave;
+ apr_array_clear(ts_ctx->chain);
+ rv = md_cert_chain_read_http(ts_ctx->chain, ts_ctx->pool, res);
+ if (APR_SUCCESS != rv) goto leave;
+
+leave:
+ return rv;
+}
+
+static apr_status_t on_get_key(const md_http_response_t *res, void *baton)
+{
+ ts_ctx_t *ts_ctx = baton;
+ apr_status_t rv;
+
+ rv = rv_of_response(res);
+ if (APR_SUCCESS != rv) goto leave;
+ rv = md_pkey_read_http(&ts_ctx->pkey, ts_ctx->pool, res);
+ if (APR_SUCCESS != rv) goto leave;
+
+leave:
+ return rv;
+}
+
+static apr_status_t ts_renew(md_proto_driver_t *d, md_result_t *result)
+{
+ const char *name, *domain, *url;
+ apr_status_t rv = APR_ENOENT;
+ ts_ctx_t *ts_ctx = d->baton;
+ md_http_t *http;
+ const md_pubcert_t *pubcert;
+ md_cert_t *old_cert, *new_cert;
+ int reset_staging = d->reset;
+
+ /* "renewing" the certificate from tailscale. Since tailscale has its
+ * own ideas on when to do this, we can only inspect the certificate
+ * it gives us and see if it is different from the current one we have.
+ * (if we have any. first time, lacking a cert, any it gives us is
+ * considered as 'renewed'.)
+ */
+ name = d->md->name;
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: renewing cert", name);
+
+ /* When not explicitly told to reset, we check the existing data. If
+ * it is incomplete or old, we trigger the reset for a clean start. */
+ if (!reset_staging) {
+ md_result_activity_setn(result, "Checking staging area");
+ rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ts_ctx->md, d->p);
+ if (APR_SUCCESS == rv) {
+ /* So, we have a copy in staging, but is it a recent or an old one? */
+ if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) {
+ reset_staging = 1;
+ }
+ }
+ else if (APR_STATUS_IS_ENOENT(rv)) {
+ reset_staging = 1;
+ rv = APR_SUCCESS;
+ }
+ }
+
+ if (reset_staging) {
+ md_result_activity_setn(result, "Resetting staging area");
+ /* reset the staging area for this domain */
+ rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p,
+ "%s: reset staging area", d->md->name);
+ if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) {
+ md_result_printf(result, rv, "resetting staging area");
+ goto leave;
+ }
+ rv = APR_SUCCESS;
+ ts_ctx->md = NULL;
+ }
+
+ if (!ts_ctx->md || strcmp(ts_ctx->md->ca_url, d->md->ca_url)) {
+ md_result_activity_printf(result, "Resetting staging for %s", d->md->name);
+ /* re-initialize staging */
+ md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: setup staging", d->md->name);
+ md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+ ts_ctx->md = md_copy(d->p, d->md);
+ rv = md_save(d->store, d->p, MD_SG_STAGING, ts_ctx->md, 0);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "Saving MD information in staging area.");
+ md_result_log(result, MD_LOG_ERR);
+ goto leave;
+ }
+ }
+
+ if (!ts_ctx->unix_socket_path) {
+ rv = APR_ENOTIMPL;
+ md_result_set(result, rv, "only unix sockets are supported for tailscale connections");
+ goto leave;
+ }
+
+ rv = md_util_is_unix_socket(ts_ctx->unix_socket_path, d->p);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "tailscale socket not available, may not be up: %s",
+ ts_ctx->unix_socket_path);
+ goto leave;
+ }
+
+ rv = md_http_create(&http, d->p,
+ apr_psprintf(d->p, "Apache mod_md/%s", MOD_MD_VERSION),
+ NULL);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "creating http context");
+ goto leave;
+ }
+ md_http_set_unix_socket_path(http, ts_ctx->unix_socket_path);
+
+ domain = (d->md->domains->nelts > 0)?
+ APR_ARRAY_IDX(d->md->domains, 0, const char*) : NULL;
+ if (!domain) {
+ rv = APR_EINVAL;
+ md_result_set(result, rv, "no domain names available");
+ }
+
+ url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=crt",
+ domain);
+ rv = md_http_GET_perform(http, url, NULL, on_get_cert, ts_ctx);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "retrieving certificate from tailscale");
+ goto leave;
+ }
+ if (ts_ctx->chain->nelts <= 0) {
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "tailscale returned no certificates");
+ goto leave;
+ }
+
+ /* Got the key and the chain, is it new? */
+ rv = md_reg_get_pubcert(&pubcert, d->reg,d->md, 0, d->p);
+ if (APR_SUCCESS == rv) {
+ old_cert = APR_ARRAY_IDX(pubcert->certs, 0, md_cert_t*);
+ new_cert = APR_ARRAY_IDX(ts_ctx->chain, 0, md_cert_t*);
+ if (md_certs_are_equal(old_cert, new_cert)) {
+ /* tailscale has not renewed the certificate, yet */
+ rv = APR_ENOENT;
+ md_result_set(result, rv, "tailscale has not renewed the certificate yet");
+ /* let's check this daily */
+ md_result_delay_set(result, apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY));
+ goto leave;
+ }
+ }
+
+ /* We have a new certificate (or had none before).
+ * Get the key and store both in STAGING.
+ */
+ url = apr_psprintf(d->p, "http://localhost/localapi/v0/cert/%s?type=key",
+ domain);
+ rv = md_http_GET_perform(http, url, NULL, on_get_key, ts_ctx);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "retrieving key from tailscale");
+ goto leave;
+ }
+
+ rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, name, NULL, ts_ctx->pkey, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_set(result, rv, "saving private key");
+ goto leave;
+ }
+
+ rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, name,
+ NULL, ts_ctx->chain, 1);
+ if (APR_SUCCESS != rv) {
+ md_result_printf(result, rv, "saving new certificate chain.");
+ goto leave;
+ }
+
+ md_result_set(result, APR_SUCCESS,
+ "A new tailscale certificate has been retrieved successfully and can "
+ "be used. A graceful server restart is recommended.");
+
+leave:
+ md_result_log(result, MD_LOG_DEBUG);
+ return rv;
+}
+
+static apr_status_t ts_complete_md(md_t *md, apr_pool_t *p)
+{
+ (void)p;
+ if (!md->ca_url) {
+ md->ca_url = MD_TAILSCALE_DEF_URL;
+ }
+ return APR_SUCCESS;
+}
+
+
+static md_proto_t TAILSCALE_PROTO = {
+ MD_PROTO_TAILSCALE, ts_init, ts_renew,
+ ts_preload_init, ts_preload, ts_complete_md,
+};
+
+apr_status_t md_tailscale_protos_add(apr_hash_t *protos, apr_pool_t *p)
+{
+ (void)p;
+ apr_hash_set(protos, MD_PROTO_TAILSCALE, sizeof(MD_PROTO_TAILSCALE)-1, &TAILSCALE_PROTO);
+ return APR_SUCCESS;
+}
diff --git a/modules/md/md_tailscale.h b/modules/md/md_tailscale.h
new file mode 100644
index 0000000000..67a874dc28
--- /dev/null
+++ b/modules/md/md_tailscale.h
@@ -0,0 +1,25 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_tailscale_h
+#define mod_md_md_tailscale_h
+
+#define MD_PROTO_TAILSCALE "tailscale"
+
+apr_status_t md_tailscale_protos_add(struct apr_hash_t *protos, apr_pool_t *p);
+
+#endif /* mod_md_md_tailscale_h */
+
diff --git a/modules/md/md_util.c b/modules/md/md_util.c
index 23923c8b2e..884c0bb91e 100644
--- a/modules/md/md_util.c
+++ b/modules/md/md_util.c
@@ -398,6 +398,16 @@ apr_status_t md_util_is_file(const char *path, apr_pool_t *pool)
return rv;
}
+apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool)
+{
+ apr_finfo_t info;
+ apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool);
+ if (rv == APR_SUCCESS) {
+ rv = (info.filetype == APR_SOCK)? APR_SUCCESS : APR_EINVAL;
+ }
+ return rv;
+}
+
int md_file_exists(const char *fname, apr_pool_t *p)
{
return (fname && *fname && APR_SUCCESS == md_util_is_file(fname, p));
diff --git a/modules/md/md_util.h b/modules/md/md_util.h
index 71e66eb500..e430655fca 100644
--- a/modules/md/md_util.h
+++ b/modules/md/md_util.h
@@ -189,6 +189,7 @@ apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...);
apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool);
apr_status_t md_util_is_file(const char *path, apr_pool_t *pool);
+apr_status_t md_util_is_unix_socket(const char *path, apr_pool_t *pool);
int md_file_exists(const char *fname, apr_pool_t *p);
typedef apr_status_t md_util_file_cb(void *baton, struct apr_file_t *f, apr_pool_t *p);
diff --git a/modules/md/md_version.h b/modules/md/md_version.h
index 3ab7ee3335..92cad1b1cd 100644
--- a/modules/md/md_version.h
+++ b/modules/md/md_version.h
@@ -27,7 +27,7 @@
* @macro
* Version number of the md module as c string
*/
-#define MOD_MD_VERSION "2.4.13"
+#define MOD_MD_VERSION "2.4.14"
/**
* @macro
@@ -35,8 +35,9 @@
* release. This is a 24 bit number with 8 bits for major number, 8 bits
* for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
*/
-#define MOD_MD_VERSION_NUM 0x02040d
+#define MOD_MD_VERSION_NUM 0x02040e
-#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory"
+#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory"
+#define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock"
#endif /* mod_md_md_version_h */
diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp
index 7b247a5bc0..d0365ed853 100644
--- a/modules/md/mod_md.dsp
+++ b/modules/md/mod_md.dsp
@@ -205,6 +205,10 @@ SOURCE=./md_store_fs.c
# End Source File
# Begin Source File
+SOURCE=./md_tailscale.c
+# End Source File
+# Begin Source File
+
SOURCE=./md_time.c
# End Source File
# Begin Source File
diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c
index 8d3260634e..82c7191768 100644
--- a/modules/md/mod_md_config.c
+++ b/modules/md/mod_md_config.c
@@ -26,6 +26,7 @@
#include <http_vhost.h>
#include "md.h"
+#include "md_acme.h"
#include "md_crypt.h"
#include "md_log.h"
#include "md_json.h"
@@ -108,7 +109,7 @@ static md_srv_conf_t defconf = {
&def_warn_window, /* warn window */
NULL, /* ca url */
NULL, /* ca contact (email) */
- "ACME", /* ca protocol */
+ MD_PROTO_ACME, /* ca protocol */
NULL, /* ca agreemnent */
NULL, /* ca challenges array */
NULL, /* ca eab kid */
diff --git a/modules/md/mod_md_status.c b/modules/md/mod_md_status.c
index 59f4e0ffd7..6891ef832e 100644
--- a/modules/md/mod_md_status.c
+++ b/modules/md/mod_md_status.c
@@ -352,11 +352,31 @@ static void si_val_cert_valid_time(status_ctx *ctx, md_json_t *mdj, const status
static void si_val_ca_url(status_ctx *ctx, md_json_t *mdj, const status_info *info)
{
md_json_t *jcert;
- status_info sub = *info;
- sub.key = MD_KEY_URL;
jcert = md_json_getj(mdj, info->key, NULL);
- if (jcert) si_val_url(ctx, jcert, &sub);
+ if (jcert) {
+ const char *proto, *s, *url;
+
+ proto = md_json_gets(jcert, MD_KEY_PROTO, NULL);
+ s = url = md_json_gets(jcert, MD_KEY_URL, NULL);
+ if (proto && !strcmp(proto, "tailscale")) {
+ s = "tailscale";
+ }
+ else if (url) {
+ s = md_get_ca_name_from_url(ctx->p, url);
+ }
+ if (HTML_STATUS(ctx)) {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "<a href='%s'>%s</a>",
+ ap_escape_html2(ctx->p, url, 1),
+ ap_escape_html2(ctx->p, s, 1));
+ }
+ else {
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sName: %s\n",
+ ctx->prefix, info->label, s);
+ apr_brigade_printf(ctx->bb, NULL, NULL, "%s%sURL: %s\n",
+ ctx->prefix, info->label, url);
+ }
+ }
}
static int count_certs(void *baton, const char *key, md_json_t *json)