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;

Reply via email to