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

Reply via email to