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