Download raw body.
acme-client: add challenge hook to support dns-01
On Tue, Feb 20, 2024 at 10:32:11PM +0100, Christopher Zimmermann wrote:
> Hi,
>
> this diff adds a challenge hook to acme-client. This hook can be used to
> fulfill challenges. For example by putting the requested files onto a remote
> http server (http-01 challenge) or by modifying dns records (dns-01
> challenge). The latter are needed to obtain wildcard certificates.
> Is this diff ok? Is the design of the hook interface sane? Any feedback is
> welcome.
>
>
> Christopher
> Index: etc/examples/acme-client.conf
> ===================================================================
> RCS file: /cvs/src/etc/examples/acme-client.conf,v
> retrieving revision 1.5
> diff -u -p -r1.5 acme-client.conf
> --- etc/examples/acme-client.conf 10 May 2023 07:34:57 -0000 1.5
> +++ etc/examples/acme-client.conf 20 Feb 2024 21:20:26 -0000
> @@ -25,6 +25,12 @@ authority buypass-test {
>
> domain example.com {
> alternative names { secure.example.com }
> + # For wildcard certificates dns-01 challenges need
> + # to be handled by a hook script.
> + # An example script can be found in /etc/examples/acme-hook.sh
> + #alternative names { *.example.com }
> + #challengehook "/etc/acme/acme-hook.sh"
> + #delay 310
> domain key "/etc/ssl/private/example.com.key"
> domain full chain certificate "/etc/ssl/example.com.fullchain.pem"
> # Test with the staging server to avoid aggressive rate-limiting.
> Index: etc/examples/acme-hook.sh
> ===================================================================
> RCS file: etc/examples/acme-hook.sh
> diff -N etc/examples/acme-hook.sh
> --- /dev/null 1 Jan 1970 00:00:00 -0000
> +++ etc/examples/acme-hook.sh 20 Feb 2024 21:20:26 -0000
> @@ -0,0 +1,40 @@
> +#!/bin/ksh
> +#
> +# $OpenBSD: $
> +#
> +
> +password=XXXXXXXX
> +
> +update() {
> + doas -u nobody curl -K - <<- END
> + no-progress-meter
> + retry = 7
> + retry-connrefused
> + retry-delay = 20
> + max-time = 5
> + url = dyn.dns.he.net
> + user = $1:$password
> + data = txt=$2
> + END
> +}
> +
> +while read type domain token thumb _reserved
> +do
> + if [ "$type" = "dns-01" ]
> + then
> + txt=`echo -n "$token.$thumb" |sha256 -b |tr '+/' '-_' |tr -d '='`
> + domain="_acme-challenge.$domain"
> + result=`update "$domain" "$txt"`
> + echo "$0: Setting $domain to $txt: $result" >&2
> + reset[${#reset[@]}]="$domain"
> + echo "HANDLED"
> + else
> + echo "UNHANDLED"
> + fi
> +done
> +
> +for domain in "${reset[@]}"
> +do
> + result=`update "$domain" "X"`
> + echo "$0: Resetting $domain: $result" >&2
> +done
> Index: usr.sbin/acme-client/acme-client.conf.5
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/acme-client.conf.5,v
> retrieving revision 1.29
> diff -u -p -r1.29 acme-client.conf.5
> --- usr.sbin/acme-client/acme-client.conf.5 11 Jan 2021 07:23:42 -0000 1.29
> +++ usr.sbin/acme-client/acme-client.conf.5 20 Feb 2024 21:20:26 -0000
> @@ -193,10 +193,63 @@ The certificate authority (as declared a
> section) to use.
> If this setting is absent, the first authority specified is used.
> .It Ic challengedir Ar path
> -The directory in which the challenge file will be stored.
> +The directory in which the challenge file for
> +.Dv http-01
> +challenges will be stored if
> +.Ar challengehook
> +did not handle them.
> If it is not specified, a default of
> .Pa /var/www/acme
> will be used.
> +.It Ic challengehook Ar command
> +.Ar command
> +receives challenges, one per line, on its
> +.Va stdin
> +and responds on
> +.Va stdout
> +with
> +.Dv HANDLED
> +when the challenge was handled or
> +.Dv UNHANDLED
> +when the challenge was not accepted.
> +.Ic challengehook
> +is meant primarily for
> +.Dv dns-01
> +challeges, but can be used handle other types of challenges, too.
> +.Pp
> +The challenges are presented on stdin in this format:
> +
> +.Ar type
> +.Ar identifier
> +.Ar token
> +.Ar thumb
> +.Ar reserved
> +
> +The most interesting
> +.Ar type
> +is
> +.Dv dns-01.
> +.Ar identifier
> +is the domain name to which a _acme-challenge. TXT subdomain record
> +needs to be installed.
> +.Ar token
> +and
> +.Ar thumb
> +are the token and thumb which are needed to construct the TXT record.
> +.Ar reserved
> +is reserved for future use.
> +
> +An example hook script can be found in
> +.Pa /etc/examples/acme-hook.sh
> +
> +.It Ic delay Ar seconds
> +After challenges are handled, delay for
> +.Ar seconds
> +before asking the
> +.Ar authority
> +to check challenges. A generous delay may be needed to wait for changes
> +to DNS to propagate to all servers checked by the
> +.Ar authority.
> .El
> .Sh FILES
> .Bl -tag -width /etc/examples/acme-client.conf -compact
> Index: usr.sbin/acme-client/chngproc.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/chngproc.c,v
> retrieving revision 1.17
> diff -u -p -r1.17 chngproc.c
> --- usr.sbin/acme-client/chngproc.c 5 May 2022 19:51:35 -0000 1.17
> +++ usr.sbin/acme-client/chngproc.c 20 Feb 2024 21:20:26 -0000
> @@ -24,20 +24,63 @@
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> +#include <sys/socket.h>
>
> #include "extern.h"
>
> int
> -chngproc(int netsock, const char *root)
> +chngproc(int netsock, const char *root, const char *hook)
> {
> char *tok = NULL, *th = NULL, *fmt = NULL, **fs = NULL;
> + char *id = NULL, *type = NULL;
> size_t i, fsz = 0;
> int rc = 0, fd = -1, cc;
> + int hook_fds[2], hook_pid;
> + char buf[16];
> long lval;
> enum chngop op;
> void *pp;
>
>
> + if (hook != NULL) {
> + if (socketpair(AF_UNIX, SOCK_STREAM, 0, hook_fds) == -1) {
> + warn("socketpair");
> + goto out;
> + }
> +
> + if ((hook_pid = fork()) == -1) {
> + warn("fork");
> + goto out;
> + }
> +
> + if (hook_pid == 0) {
> + char *hook_buf;
> + char *argv[32];
> + const char *ifs = " \t\n";
> +
> + close(hook_fds[0]);
> + if (dup2(hook_fds[1], STDIN_FILENO) != STDIN_FILENO ||
> + dup2(hook_fds[1], STDOUT_FILENO) != STDOUT_FILENO) {
> + warn("dup");
> + goto out;
> + }
> +
> + hook_buf = strdup(hook);
> + i = 0;
> + argv[i] = strtok(hook_buf, ifs);
> + while (argv[i] != NULL &&
> + i + 1 < sizeof(argv) / sizeof(argv[0]))
> + argv[++i] = strtok(NULL, ifs);
> +
> + if (i == 0 || argv[i] != NULL)
> + errx(1, "Empty challengehook or too many arguments");
> +
> + execv(argv[0], argv);
> + err(1, "execv failed");
> + }
> + close(hook_fds[1]);
> + }
> +
> if (unveil(root, "wc") == -1) {
> warn("unveil %s", root);
> goto out;
> @@ -60,7 +103,7 @@ chngproc(int netsock, const char *root)
> else if (lval == CHNG_SYN)
> op = lval;
>
> - if (op == CHNG__MAX) {
> + if (op >= CHNG__MAX) {
> warnx("unknown operation from netproc");
> goto out;
> } else if (op == CHNG_STOP)
> @@ -74,11 +117,15 @@ chngproc(int netsock, const char *root)
> * of tokens that we'll later clean up.
> */
>
> + if ((id = readstr(netsock, COMM_ID)) == NULL)
> + goto out;
> + if ((type = readstr(netsock, COMM_TYPE)) == NULL)
> + goto out;
> if ((th = readstr(netsock, COMM_THUMB)) == NULL)
> goto out;
> - else if ((tok = readstr(netsock, COMM_TOK)) == NULL)
> + if ((tok = readstr(netsock, COMM_TOK)) == NULL)
> goto out;
> - else if (strlen(tok) < 1) {
> + if (strlen(tok) < 1) {
> warnx("token is too short");
> goto out;
> }
> @@ -91,67 +138,103 @@ chngproc(int netsock, const char *root)
> }
> }
>
> - if (asprintf(&fmt, "%s.%s", tok, th) == -1) {
> - warn("asprintf");
> - goto out;
> - }
> + if (hook != NULL) {
> + write(hook_fds[0], type, strlen(type));
> + write(hook_fds[0], "\t", 1);
> + write(hook_fds[0], id, strlen(id));
> + write(hook_fds[0], "\t", 1);
> + write(hook_fds[0], tok, strlen(tok));
> + write(hook_fds[0], "\t", 1);
> + write(hook_fds[0], th, strlen(th));
> + write(hook_fds[0], "\t", 1);
> + write(hook_fds[0], "reserved\n", 9);
> + cc = read(hook_fds[0], buf, sizeof(buf));
> + if (cc <= 0)
> + err(1, "reading from challengehook failed");
> + }
> + else
> + strcpy(buf, "UNHANDLED");
> +
> + if (strncmp(buf, "HANDLED", 7) == 0) {
> + op = CHNG_ACK;
> + }
> + else if (strncmp(buf, "UNHANDLED", 9) == 0
> + && strcmp(type, "http-01") == 0) {
> + /* Vector appending... */
> +
> + pp = reallocarray(fs, (fsz + 1), sizeof(char *));
> + if (pp == NULL) {
> + warn("realloc");
> + goto out;
> + }
> + fs = pp;
> + if (asprintf(&fs[fsz], "%s/%s", root, tok) == -1) {
> + warn("asprintf");
> + goto out;
> + }
> + fsz++;
>
> - /* Vector appending... */
> + /*
> + * Create and write to our challenge file.
> + * Note: we use file descriptors instead of FILE
> + * because we want to minimise our pledges.
> + */
> + fd = open(fs[fsz - 1], O_WRONLY|O_CREAT|O_TRUNC, 0444);
> + if (fd == -1) {
> + warn("%s", fs[fsz - 1]);
> + goto out;
> + }
> + if (asprintf(&fmt, "%s.%s", tok, th) == -1) {
> + warn("asprintf");
> + goto out;
> + }
> + if (write(fd, fmt, strlen(fmt)) == -1) {
> + warn("%s", fs[fsz - 1]);
> + goto out;
> + }
> + free(fmt);
> + if (close(fd) == -1) {
> + warn("%s", fs[fsz - 1]);
> + goto out;
> + }
> + fd = -1;
>
> - pp = reallocarray(fs, (fsz + 1), sizeof(char *));
> - if (pp == NULL) {
> - warn("realloc");
> - goto out;
> - }
> - fs = pp;
> - if (asprintf(&fs[fsz], "%s/%s", root, tok) == -1) {
> - warn("asprintf");
> - goto out;
> - }
> - fsz++;
> - free(tok);
> - tok = NULL;
> + dodbg("%s: created", fs[fsz - 1]);
> + op = CHNG_ACK;
>
> - /*
> - * Create and write to our challenge file.
> - * Note: we use file descriptors instead of FILE
> - * because we want to minimise our pledges.
> - */
> - fd = open(fs[fsz - 1], O_WRONLY|O_CREAT|O_TRUNC, 0444);
> - if (fd == -1) {
> - warn("%s", fs[fsz - 1]);
> - goto out;
> }
> - if (write(fd, fmt, strlen(fmt)) == -1) {
> - warn("%s", fs[fsz - 1]);
> - goto out;
> + else if (strncmp(buf, "UNHANDLED", 4) == 0) {
> + op = CHNG_FAIL;
> }
> - if (close(fd) == -1) {
> - warn("%s", fs[fsz - 1]);
> - goto out;
> + else {
> + warnx("got unknown reply from hook: <%.*s>\n", cc, buf);
> + op = CHNG_FAIL;
> }
> - fd = -1;
> -
> - free(th);
> - free(fmt);
> - th = fmt = NULL;
> -
> - dodbg("%s: created", fs[fsz - 1]);
>
> - /*
> - * Write our acknowledgement.
> - * Ignore reader failure.
> - */
> -
> - cc = writeop(netsock, COMM_CHNG_ACK, CHNG_ACK);
> - if (cc == 0)
> + if (writeop(netsock, COMM_CHNG_ACK, op) <= 0)
> break;
> - if (cc < 0)
> - goto out;
> +
> + free(type);
> + free(id);
> + free(th);
> + free(tok);
> + type = id = th = tok = fmt = NULL;
> }
>
> rc = 1;
> +
> out:
> + if (hook != NULL) {
> + if (shutdown(hook_fds[0], SHUT_WR))
> + err(1, "shutdown challengehook failed");
> + cc = read(hook_fds[0], buf, sizeof(buf));
> + if (cc == 0) ; /* EOF */
> + else if (cc < 0)
> + warn("reading from challengehook failed");
> + else if (cc > 0)
> + warn("unexpected read from challengehook");
> + close(hook_fds[0]);
> + }
> close(netsock);
> if (fd != -1)
> close(fd);
> @@ -160,9 +243,18 @@ out:
> warn("%s", fs[i]);
> free(fs[i]);
> }
> - free(fs);
> - free(fmt);
> - free(th);
> - free(tok);
> + if(type)
> + free(type);
> + if(id)
> + free(id);
> + if(fs)
> + free(fs);
> + if(fmt)
> + free(fmt);
> + if(th)
> + free(th);
> + if(tok)
> + free(tok);
> +
> return rc;
> }
> Index: usr.sbin/acme-client/extern.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/extern.h,v
> retrieving revision 1.20
> diff -u -p -r1.20 extern.h
> --- usr.sbin/acme-client/extern.h 14 Sep 2020 16:00:17 -0000 1.20
> +++ usr.sbin/acme-client/extern.h 20 Feb 2024 21:20:26 -0000
> @@ -44,6 +44,7 @@ enum chngop {
> CHNG_STOP = 0,
> CHNG_SYN,
> CHNG_ACK,
> + CHNG_FAIL,
> CHNG__MAX
> };
>
> @@ -116,6 +117,8 @@ enum comp {
> enum comm {
> COMM_REQ,
> COMM_THUMB,
> + COMM_ID,
> + COMM_TYPE,
> COMM_CERT,
> COMM_PAY,
> COMM_NONCE,
> @@ -157,6 +160,9 @@ enum chngstatus {
> };
>
> struct chng {
> + STAILQ_ENTRY(chng) next;
> + char *type; /* type of challenge */
> + char *identifier; /* domain to be authenticated */
> char *uri; /* uri on ACME server */
> char *token; /* token we must offer */
> char *error; /* "detail" field in case of error */
> @@ -164,6 +170,8 @@ struct chng {
> enum chngstatus status; /* challenge accepted? */
> };
>
> +STAILQ_HEAD(chng_queue, chng);
> +
> enum orderstatus {
> ORDER_INVALID = -1,
> ORDER_PENDING = 0,
> @@ -202,7 +210,7 @@ __BEGIN_DECLS
> */
> int acctproc(int, const char *, enum keytype);
> int certproc(int, int);
> -int chngproc(int, const char *);
> +int chngproc(int, const char *, const char *);
> int dnsproc(int);
> int revokeproc(int, const char *, int, int, const char *const *,
> size_t);
> @@ -211,7 +219,7 @@ int fileproc(int, const char *, const
> int keyproc(int, const char *, const char **, size_t,
> enum keytype);
> int netproc(int, int, int, int, int, int, int,
> - struct authority_c *, const char *const *,
> + struct authority_c *, int, const char *const *,
> size_t);
>
> /*
> @@ -253,7 +261,7 @@ struct jsmnn *json_parse(const char *, s
> void json_free(struct jsmnn *);
> int json_parse_response(struct jsmnn *);
> void json_free_challenge(struct chng *);
> -int json_parse_challenge(struct jsmnn *, struct chng *);
> +struct chng_queue json_parse_challenge(struct jsmnn *);
> void json_free_order(struct order *);
> int json_parse_order(struct jsmnn *, struct order *);
> int json_parse_upd_order(struct jsmnn *, struct order *);
> Index: usr.sbin/acme-client/json.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/json.c,v
> retrieving revision 1.21
> diff -u -p -r1.21 json.c
> --- usr.sbin/acme-client/json.c 14 Sep 2020 16:00:17 -0000 1.21
> +++ usr.sbin/acme-client/json.c 20 Feb 2024 21:20:26 -0000
> @@ -22,6 +22,7 @@
> #include <stdlib.h>
> #include <string.h>
> #include <unistd.h>
> +#include <sys/queue.h>
>
> #include "jsmn.h"
> #include "extern.h"
> @@ -254,9 +255,8 @@ json_getarray(struct jsmnn *n, const cha
> if (n->d.obj[i].lhs->type != JSMN_STRING &&
> n->d.obj[i].lhs->type != JSMN_PRIMITIVE)
> continue;
> - else if (strcmp(name, n->d.obj[i].lhs->d.str))
> - continue;
> - break;
> + if (strcmp(name, n->d.obj[i].lhs->d.str) == 0)
> + break;
> }
> if (i == n->fields)
> return NULL;
> @@ -331,10 +331,12 @@ json_getstr(struct jsmnn *n, const char
> void
> json_free_challenge(struct chng *p)
> {
> -
> + free(p->type);
> + free(p->identifier);
> free(p->uri);
> free(p->token);
> - p->uri = p->token = NULL;
> + free(p->error);
> + free(p);
> }
>
> /*
> @@ -370,43 +372,64 @@ json_parse_response(struct jsmnn *n)
> * information, into a structure.
> * We only care about the HTTP-01 response.
> */
> -int
> -json_parse_challenge(struct jsmnn *n, struct chng *p)
> +struct chng_queue
> +json_parse_challenge(struct jsmnn *n)
> {
> - struct jsmnn *array, *obj, *error;
> + struct jsmnn *array, *obj, *identifier, *error;
> + struct chng_queue chngs = STAILQ_HEAD_INITIALIZER(chngs);
> + struct chng *p;
> size_t i;
> - int rc;
> - char *type;
>
> if (n == NULL)
> - return 0;
> + return chngs;
> +
> + identifier = json_getobj(n, "identifier");
> + if (identifier == NULL)
> + return chngs;
>
> array = json_getarray(n, "challenges");
> if (array == NULL)
> - return 0;
> + return chngs;
>
> for (i = 0; i < array->fields; i++) {
> obj = json_getarrayobj(array->d.array[i]);
> if (obj == NULL)
> continue;
> - type = json_getstr(obj, "type");
> - if (type == NULL)
> - continue;
> - rc = strcmp(type, "http-01");
> - free(type);
> - if (rc)
> - continue;
> + p = malloc(sizeof(struct chng));
> + if (p == NULL) {
> + warn("malloc");
> + goto fail;
> + }
> + p->identifier = json_getstr(identifier, "value");
> + p->type = json_getstr(obj, "type");
> p->uri = json_getstr(obj, "url");
> p->token = json_getstr(obj, "token");
> + if (p->identifier == NULL || p->type == NULL || p->uri == NULL
> + || p->token == NULL) {
> + warnx("malformed challenge");
> + goto fail;
> + }
> p->status = json_parse_response(obj);
> + p->retry = 0;
> if (p->status == CHNG_INVALID) {
> error = json_getobj(obj, "error");
> p->error = json_getstr(error, "detail");
> }
> - return p->uri != NULL && p->token != NULL;
> + else
> + p->error = NULL;
> + STAILQ_INSERT_TAIL(&chngs, p, next);
> + }
> +
> + return chngs;
> +
> +fail:
> + while (!STAILQ_EMPTY(&chngs)) {
> + p = STAILQ_FIRST(&chngs);
> + STAILQ_REMOVE_HEAD(&chngs, next);
> + json_free_challenge(p);
> }
>
> - return 0;
> + return chngs;
> }
>
> static enum orderstatus
> Index: usr.sbin/acme-client/main.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/main.c,v
> retrieving revision 1.55
> diff -u -p -r1.55 main.c
> --- usr.sbin/acme-client/main.c 5 May 2022 19:51:35 -0000 1.55
> +++ usr.sbin/acme-client/main.c 20 Feb 2024 21:20:26 -0000
> @@ -220,7 +220,7 @@ main(int argc, char *argv[])
> c = netproc(key_fds[1], acct_fds[1],
> chng_fds[1], cert_fds[1],
> dns_fds[1], rvk_fds[1],
> - revocate, authority,
> + revocate, authority, domain->delay,
> (const char *const *)alts, altsz);
> exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
> }
> @@ -286,7 +286,7 @@ main(int argc, char *argv[])
> close(rvk_fds[0]);
> close(file_fds[0]);
> close(file_fds[1]);
> - c = chngproc(chng_fds[0], chngdir);
> + c = chngproc(chng_fds[0], chngdir, domain->challengehook);
> exit(c ? EXIT_SUCCESS : EXIT_FAILURE);
> }
>
> Index: usr.sbin/acme-client/netproc.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/netproc.c,v
> retrieving revision 1.33
> diff -u -p -r1.33 netproc.c
> --- usr.sbin/acme-client/netproc.c 14 Dec 2022 18:32:26 -0000 1.33
> +++ usr.sbin/acme-client/netproc.c 20 Feb 2024 21:20:26 -0000
> @@ -15,6 +15,7 @@
> * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
> */
>
> +#include <stdio.h>
> #include <assert.h>
> #include <ctype.h>
> #include <err.h>
> @@ -505,14 +506,13 @@ doupdorder(struct conn *c, struct order
> /*
> * Request a challenge for the given domain name.
> * This must be called for each name "alt".
> - * On non-zero exit, fills in "chng" with the challenge.
> */
> -static int
> -dochngreq(struct conn *c, const char *auth, struct chng *chng)
> +static struct chng_queue
> +dochngreq(struct conn *c, const char *auth)
> {
> - int rc = 0;
> long lc;
> struct jsmnn *j = NULL;
> + struct chng_queue chngs = STAILQ_HEAD_INITIALIZER(chngs);
>
> dodbg("%s: %s", __func__, auth);
>
> @@ -522,15 +522,13 @@ dochngreq(struct conn *c, const char *au
> warnx("%s: bad HTTP: %ld", auth, lc);
> else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL)
> warnx("%s: bad JSON object", auth);
> - else if (!json_parse_challenge(j, chng))
> - warnx("%s: bad challenge", auth);
> else
> - rc = 1;
> + chngs = json_parse_challenge(j);
>
> - if (rc == 0 || verbose > 1)
> + if (STAILQ_EMPTY(&chngs) || verbose > 1)
> buf_dump(&c->buf);
> json_free(j);
> - return rc;
> + return chngs;
> }
>
> /*
> @@ -673,7 +671,7 @@ dodirs(struct conn *c, const char *addr,
> */
> int
> netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd,
> - int revocate, struct authority_c *authority,
> + int revocate, struct authority_c *authority, int delay,
> const char *const *alts, size_t altsz)
> {
> int rc = 0;
> @@ -682,7 +680,8 @@ netproc(int kfd, int afd, int Cfd, int c
> struct conn c;
> struct capaths paths;
> struct order order;
> - struct chng *chngs = NULL;
> + struct chng_queue chngs = STAILQ_HEAD_INITIALIZER(chngs);
> + struct chng *chng;
> long lval;
>
> memset(&paths, 0, sizeof(struct capaths));
> @@ -782,12 +781,6 @@ netproc(int kfd, int afd, int Cfd, int c
> if (!doneworder(&c, alts, altsz, &order, &paths))
> goto out;
>
> - chngs = calloc(order.authsz, sizeof(struct chng));
> - if (chngs == NULL) {
> - warn("calloc");
> - goto out;
> - }
> -
> /*
> * Get thumbprint from acctproc. We will need it to construct
> * a response to the challenge
> @@ -812,42 +805,57 @@ netproc(int kfd, int afd, int Cfd, int c
> goto out;
> }
> for (i = 0; i < order.authsz; i++) {
> - if (!dochngreq(&c, order.auths[i], &chngs[i]))
> + struct chng_queue newchngs;
> +
> + newchngs = dochngreq(&c, order.auths[i]);
> + if (STAILQ_EMPTY(&newchngs))
> goto out;
>
> + STAILQ_CONCAT(&chngs, &newchngs);
> + }
> +
> + STAILQ_FOREACH(chng, &chngs, next) {
> dodbg("challenge, token: %s, uri: %s, status: "
> - "%d", chngs[i].token, chngs[i].uri,
> - chngs[i].status);
> + "%d", chng->token, chng->uri, chng->status);
>
> - if (chngs[i].status == CHNG_VALID ||
> - chngs[i].status == CHNG_INVALID)
> + if (chng->status == CHNG_VALID ||
> + chng->status == CHNG_INVALID)
> continue;
>
> - if (chngs[i].retry++ >= RETRY_MAX) {
> + if (chng->retry++ >= RETRY_MAX) {
> warnx("%s: too many tries",
> - chngs[i].uri);
> + chng->uri);
> goto out;
> }
>
> if (writeop(Cfd, COMM_CHNG_OP, CHNG_SYN) <= 0)
> goto out;
> - else if (writestr(Cfd, COMM_THUMB, thumb) <= 0)
> + if (writestr(Cfd, COMM_ID, chng->identifier) <= 0)
> goto out;
> - else if (writestr(Cfd, COMM_TOK,
> - chngs[i].token) <= 0)
> + if (writestr(Cfd, COMM_TYPE, chng->type) <= 0)
> + goto out;
> + if (writestr(Cfd, COMM_THUMB, thumb) <= 0)
> + goto out;
> + if (writestr(Cfd, COMM_TOK, chng->token) <= 0)
> goto out;
>
> /* Read that the challenge has been made. */
> if (readop(Cfd, COMM_CHNG_ACK) != CHNG_ACK)
> - goto out;
> + chng->status = CHNG_INVALID;
>
> }
> +
> + if (delay >= 0) {
> + dodbg("delay for %ds\n", delay);
> + sleep(delay);
> + }
> +
> /* Write to the CA that it's ready. */
> - for (i = 0; i < order.authsz; i++) {
> - if (chngs[i].status == CHNG_VALID ||
> - chngs[i].status == CHNG_INVALID)
> + STAILQ_FOREACH(chng, &chngs, next) {
> + if (chng->status == CHNG_VALID ||
> + chng->status == CHNG_INVALID)
> continue;
> - if (!dochngresp(&c, &chngs[i]))
> + if (!dochngresp(&c, chng))
> goto out;
> }
> break;
> @@ -880,15 +888,18 @@ netproc(int kfd, int afd, int Cfd, int c
>
> if (order.status != ORDER_VALID) {
> for (i = 0; i < order.authsz; i++) {
> - dochngreq(&c, order.auths[i], &chngs[i]);
> - if (chngs[i].error != NULL) {
> - if (stravis(&error, chngs[i].error, VIS_SAFE)
> - != -1) {
> + struct chng_queue newchngs;
> +
> + newchngs = dochngreq(&c, order.auths[i]);
> +
> + STAILQ_FOREACH(chng, &newchngs, next)
> + if (chng->error != NULL
> + && stravis(&error, chng->error,
> + VIS_SAFE) != -1) {
> warnx("%s", error);
> free(error);
> error = NULL;
> }
> - }
> }
> goto out;
> }
> @@ -917,10 +928,11 @@ out:
> free(thumb);
> free(c.kid);
> free(c.buf.buf);
> - if (chngs != NULL)
> - for (i = 0; i < order.authsz; i++)
> - json_free_challenge(&chngs[i]);
> - free(chngs);
> + while (!STAILQ_EMPTY(&chngs)) {
> + chng = STAILQ_FIRST(&chngs);
> + STAILQ_REMOVE_HEAD(&chngs, next);
> + json_free_challenge(chng);
> + }
> json_free_capaths(&paths);
> return rc;
> }
> Index: usr.sbin/acme-client/parse.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/parse.h,v
> retrieving revision 1.15
> diff -u -p -r1.15 parse.h
> --- usr.sbin/acme-client/parse.h 14 Sep 2020 16:00:17 -0000 1.15
> +++ usr.sbin/acme-client/parse.h 20 Feb 2024 21:20:26 -0000
> @@ -45,6 +45,7 @@ struct domain_c {
> TAILQ_ENTRY(domain_c) entry;
> TAILQ_HEAD(, altname_c) altname_list;
> int altname_count;
> + int delay;
> enum keytype keytype;
> char *handle;
> char *domain;
> @@ -53,6 +54,7 @@ struct domain_c {
> char *chain;
> char *fullchain;
> char *auth;
> + char *challengehook;
> char *challengedir;
> };
>
> Index: usr.sbin/acme-client/parse.y
> ===================================================================
> RCS file: /cvs/src/usr.sbin/acme-client/parse.y,v
> retrieving revision 1.45
> diff -u -p -r1.45 parse.y
> --- usr.sbin/acme-client/parse.y 15 Dec 2022 08:06:13 -0000 1.45
> +++ usr.sbin/acme-client/parse.y 20 Feb 2024 21:20:26 -0000
> @@ -102,6 +102,7 @@ typedef struct {
>
> %token AUTHORITY URL API ACCOUNT CONTACT
> %token DOMAIN ALTERNATIVE NAME NAMES CERT FULL CHAIN KEY SIGN WITH CHALLENGEDIR
> +%token CHALLENGEHOOK DELAY
> %token YES NO
> %token INCLUDE
> %token ERROR
> @@ -393,6 +394,28 @@ domainoptsl : ALTERNATIVE NAMES '{' optn
> err(EXIT_FAILURE, "strdup");
> domain->challengedir = s;
> }
> + | CHALLENGEHOOK STRING {
> + char *s;
> + if (domain->challengehook != NULL) {
> + yyerror("duplicate challengehook");
> + YYERROR;
> + }
> + if ((s = strdup($2)) == NULL)
> + err(EXIT_FAILURE, "strdup");
> + domain->challengehook = s;
> + }
> + | DELAY NUMBER {
> + if (domain->delay >= 0) {
> + yyerror("duplicate delay");
> + YYERROR;
> + }
> + if ($2 < 0) {
> + yyerror("invalid delay: %lld ", $2);
> + YYERROR;
> + }
> + domain->delay = $2;
> +
> + }
> ;
>
> altname_l : altname optcommanl altname_l
> @@ -462,7 +485,9 @@ lookup(char *s)
> {"certificate", CERT},
> {"chain", CHAIN},
> {"challengedir", CHALLENGEDIR},
> + {"challengehook", CHALLENGEHOOK},
> {"contact", CONTACT},
> + {"delay", DELAY},
> {"domain", DOMAIN},
> {"ecdsa", ECDSA},
> {"full", FULL},
> @@ -964,6 +989,7 @@ conf_new_domain(struct acme_conf *c, cha
> return (NULL);
> if ((d = calloc(1, sizeof(struct domain_c))) == NULL)
> err(EXIT_FAILURE, "%s", __func__);
> + d->delay = -1;
> TAILQ_INSERT_TAIL(&c->domain_list, d, entry);
>
> d->handle = s;
> @@ -1085,6 +1111,10 @@ print_config(struct acme_conf *xconf)
> printf("\tsign with \"%s\"\n", d->auth);
> if (d->challengedir != NULL)
> printf("\tchallengedir \"%s\"\n", d->challengedir);
> + if (d->challengehook != NULL)
> + printf("\tchallengehook \"%s\"\n", d->challengehook);
> + if (d->delay >= 0)
> + printf("\tdelay \"%d\"\n", d->delay);
> printf("}\n\n");
> }
> }
> @@ -1100,7 +1130,7 @@ domain_valid(const char *cp)
> {
>
> for ( ; *cp != '\0'; cp++)
> - if (!(*cp == '.' || *cp == '-' ||
> + if (!(*cp == '.' || *cp == '-' || *cp == '*' ||
> *cp == '_' || isalnum((unsigned char)*cp)))
> return 0;
> return 1;
Would love to use acme-client again with the new hook support you are
adding. Thank you!
I've been generating dns wildcard certs using acme.sh.
https://github.com/acmesh-official/acme.sh
There are a lot of shells scripts to handle various providers, including
on the local machine (nsd). Is it out of scope to support the dnsapi
they have for setup/teardown, or have an example hook able to call into a layer
to bridge it? They seem to support most major dns providers.
https://github.com/acmesh-official/acme.sh/tree/master/dnsapi
The nsd hook I use on OpenBSD (slightly modified)
https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_nsd.sh
Looking forward to using this either way.
--
Chaz
acme-client: add challenge hook to support dns-01