Index | Thread | Search

From:
Chaz Kettleson <chaz@pyr3x.com>
Subject:
Re: acme-client: add challenge hook to support dns-01
To:
Kristaps Dzonsons <kristaps@bsd.lv>, ports@openbsd.org
Date:
Mon, 26 Feb 2024 17:14:47 -0500

Download raw body.

Thread
  • Chaz Kettleson:

    acme-client: add challenge hook to support dns-01

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

    acme-client: add challenge hook to support dns-01