From: Christopher Zimmermann Subject: acme-client: add challenge hook to support dns-01 To: Kristaps Dzonsons Cc: ports@openbsd.org Date: Tue, 20 Feb 2024 22:32:11 +0100 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 #include #include +#include #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 #include #include +#include #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 #include #include #include @@ -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;