Thanks for your feedback guys. I tried to improve the interface by
calling the hook for each challenge challenge individually and send
information from acme-client via environment variables, which are
checked against a restrictive alphabet. This makes dropping privileges
easier and passing random crap from the internet harder.
Privileges can now be dropped with this idiom:
[ `/usr/bin/who -m |cut -d ' ' -f 1` == 'nobody' ] ||
exec /usr/bin/su -s /bin/sh nobody -s "$@" <"$0"
On Wed, Feb 21, 2024 at 05:38:43PM +0100, Florian Obser wrote:
btw. a few years back I came up with this:
https://marc.info/?l=openbsd-tech&m=160883000402270&w=2
I still have the diff lying around somewhere.
I have no recollection if it's actually better.
You added actual support for dns-01 challenges into acme-client. Digest
calculation is performed in acme-client. The hooks can only accept
dns-01 challenges.
I add a generic hook interface that will pass any challenge to the hook,
which can then decide, which challenges to accept and must perform
digest computation by itself.
However, one stated requirement at the time was that the dns-01
challenge must work with base tools out of the box, i.e. nsd(8). I have
no idea how one would do that.
I have not tried it, but it should work by adding a zonefile for the
_acme-challenge zone, which is empty most of the time, but will get
populated by the acme-client hook and cleared after authorization.
nsd can reload zonefiles on SIGHUP.
So I dropped the diff and figured out how to avoid wildcard certs.
That's what I did for quite some time.
But what I need this for now is for getting certificates on different
machines for the same top level domain.
I can do this either via dns-01, which seems to be most straight-forward
or by sending the challenge files to the one machine which hosts http
for the domain.
Both can be accomplished by the generic hook mechanism.
On Wed, Feb 21, 2024 at 09:02:33AM -0700, Theo de Raadt wrote:
I also think this is a mistake. The proposed hook mechanism seems too
powerful and overly general.
To avoid that, maybe consider the minimum it could do, and still work.
I tried to move some complexity from the hook to acme-client.
The minimum the hook needs to do is:
for dns-01: add a dns txt record and tear down a dns txt record
for http-01: temporarily serve a http resource. Maybe by uploading a
file somewhere and deleting it afterwards.
The minimum information the hook needs from acme-client for dns-01
challenges is:
- the domain name of the dns record (_acme-challenge.example.com)
- what to put there (the sha256/base64 digest of token.thumb)
- authentication like username / password of dynamic dns service.
username / password need not be passed in from acme-client, but still
need to be stored somewhere accissible by the hook script.
The minimum information for http-01 challenges is:
- the key authorization (token.thumb)
- where to serve the files (challengedir or domain)
- authentication.
I kept the digest calculation in the hook, since it is only needed for
dns-01 and I prefer to keep the hook mechanism in acme-client
indifferent to the hook type. This means this complexity stays in the
example hook script:
txt=`echo -n "$ACME_TOKEN.$ACME_THUMB" |sha256 -b |tr '+/' '-_' |tr -d '='`
ACME_TOKEN and ACME_THUMB are checked to only contain characters from
the base64_url alphabet. So this seems quite safe to me.
I think it should perform an action based only upon pre-configured
information.
The only decision making that is going on is deciding whether to accept
a challenge based on its type.
And I actually want to be able to authorize some subdomains via dns-01,
but others via http-01. So this cannot be decided in acme-client.conf.
The domain cannot be pre-configured / hardcoded because of the
alternative names which will also need authentication.
Then the server can do it's job, and the hook mechanism
watches for the job to be finished. Then the hook mechanism should
communicate what it sees back (without any dangerous inspection
or interpretation) to the main client engine for decision making.
Now the only thing that is passed back to acme-client is an exit status.
Success or failure/rejection of the challenge.
The diff is hard to read for some reason
I believe the readability is fixed in this new diff.
Christopher
--
http://gmerlin.de
OpenPGP: http://gmerlin.de/christopher.pub
CB07 DA40 B0B6 571D 35E2 0DEF 87E2 92A7 13E5 DEE1
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 24 Feb 2024 20:33:31 -0000
@@ -0,0 +1,41 @@
+#!/bin/sh
+# $OpenBSD: $
+
+[ `/usr/bin/who -m |cut -d ' ' -f 1` == 'nobody' ] ||
+ exec /usr/bin/su -s /bin/sh nobody -s "$@" <"$0"
+
+update() {
+ echo -n "acme-hook.sh: Setting $1 to $2: "
+ curl -K - <<- END
+ no-progress-meter
+ retry = 7
+ retry-connrefused
+ retry-delay = 20
+ max-time = 5
+ url = dyn.dns.he.net
+ user = $1:XXXX_PASSWORD_XXXX
+ data = txt=$2
+ END
+ ret=$?
+ echo
+ return "$ret"
+}
+
+case "$ACME_TYPE" in
+ dns-01)
+ record="_acme-challenge.$ACME_IDENTIFIER"
+ txt=`echo -n "$ACME_TOKEN.$ACME_THUMB" |sha256 -b |tr '+/' '-_' |tr -d '='`
+
+ case "$ACME_TASK" in
+ handle)
+ update "$record" "$txt" >&2
+ ;;
+ cleanup)
+ update "$record" "X" >&2
+ ;;
+ esac
+ ;;
+ *)
+ exit 1
+ ;;
+esac
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 24 Feb 2024 20:33:31 -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: 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 24 Feb 2024 20:33:31 -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 24 Feb 2024 20:33:31 -0000
@@ -24,20 +24,207 @@
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
#include "extern.h"
+static int
+is_alnum(char *s, char *extra)
+{
+ int c;
+ while ((c = (unsigned char)*s++) != 0) {
+ if (!isalnum(c) && strchr(extra, c) == NULL) {
+ warnx("token is not a valid base64url");
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
int
-chngproc(int netsock, const char *root)
+chngproc(int netsock, const char *root, const char *hook)
{
+ struct chng {
+ SLIST_ENTRY(chng) next;
+ char *env[4];
+ char buf[0];
+ } *chng;
+ SLIST_HEAD(queue_t, chng) queue = SLIST_HEAD_INITIALIZER(queue);
char *tok = NULL, *th = NULL, *fmt = NULL, **fs = NULL;
- size_t i, fsz = 0;
+ char *id = NULL, *type = NULL;
+ size_t i, j, fsz = 0;
int rc = 0, fd = -1, cc;
long lval;
enum chngop op;
void *pp;
+ if (hook != NULL) {
+ const char **argv;
+ extern char **environ;
+ char **env, **env_ext;
+ int hook_pid, hook_status;
+ char *p;
+ const char *ifs = " \t\n";
+
+ if (pledge("stdio exec proc", NULL) == -1) {
+ warn("pledge");
+ return 0;
+ }
+
+ /* parse hook path and parameters */
+ i = strlen(hook) + 1;
+ p = alloca(i);
+ strlcpy(p, hook, i);
+
+ if (strtok(p, ifs) == NULL)
+ errx(1, "Empty challengehook");
+ for (j = 2; strtok(NULL, ifs); j++) ;
+ argv = alloca(j * sizeof(char *));
+ strlcpy(p, hook, i);
+ argv[0] = strtok(p, ifs);
+ for (i = 1; i < j; i++)
+ argv[i] = strtok(NULL, ifs);
+ assert(argv[j - 1] == NULL);
+
+ /* prepare for extended environment */
+ for (i = 0; environ[i] != NULL; i++) ;
+
+ env = alloca((i + 7) * sizeof(char *));
+ memcpy(env, environ, i * sizeof(char *));
+
+ env_ext = env + i;
+
+ env_ext[0] = alloca(17);
+ snprintf(env_ext[0], 17, "ACME_TASK=handle");
+ i = 11 + strlen(root);
+ env_ext[1] = alloca(i);
+ snprintf(env_ext[1], i, "ACME_ROOT=%s", root);
+ env_ext[6] = NULL;
+
+ for (;;) {
+ if ((lval = readop(netsock, COMM_CHNG_OP)) == 0)
+ op = CHNG_STOP;
+ else if (lval == CHNG_SYN)
+ op = lval;
+
+ if (op >= CHNG__MAX) {
+ warnx("unknown operation from netproc");
+ goto clean;
+ } else if (op == CHNG_STOP)
+ break;
+
+ assert(op == CHNG_SYN);
+
+ if ((id = readstr(netsock, COMM_ID)) == NULL)
+ goto clean;
+ if ((type = readstr(netsock, COMM_TYPE)) == NULL)
+ goto clean;
+ if ((th = readstr(netsock, COMM_THUMB)) == NULL)
+ goto clean;
+ if ((tok = readstr(netsock, COMM_TOK)) == NULL)
+ goto clean;
+ if (strlen(tok) < 1) {
+ warnx("token is too short");
+ goto clean;
+ }
+
+ if (!is_alnum(id, "-.")) {
+ warnx("domain is not a valid
base64url");
+ goto clean;
+ }
+ if (!is_alnum(type, "-")) {
+ warnx("challenge type malformed");
+ goto clean;
+ }
+ if (!is_alnum(th, "-_")) {
+ warnx("thumbprint is not a valid
base64url");
+ goto clean;
+ }
+ if (!is_alnum(tok, "-_")) {
+ warnx("token is not a valid base64url");
+ goto clean;
+ }
+
+ i = strlen(id) + strlen(type) +
+ strlen(th) + strlen(tok) + 52;
+ chng = calloc(1, sizeof(struct chng) + i);
+ if (chng == NULL)
+ goto clean;
+ chng->env[0] = chng->buf;
+ j = snprintf(chng->env[0], i, "ACME_IDENTIFIER=%s", id);
+ i -= ++j;
+ chng->env[1] = chng->env[0] + j;
+ j = snprintf(chng->env[1], i, "ACME_TYPE=%s", type);
+ i -= ++j;
+ chng->env[2] = chng->env[1] + j;
+ j = snprintf(chng->env[2], i, "ACME_THUMB=%s", th);
+ i -= ++j;
+ chng->env[3] = chng->env[2] + j;
+ j = snprintf(chng->env[3], i, "ACME_TOKEN=%s", tok);
+ i -= ++j;
+ assert(i == 0);
+
+ free(id); free(type); free(th); free(tok);
+ id = type = th = tok = NULL;
+
+ SLIST_INSERT_HEAD(&queue, chng, next);
+
+ memcpy(env_ext + 2, chng->env, sizeof(chng->env));
+
+ if ((hook_pid = fork()) == -1) {
+ warn("fork");
+ goto clean;
+ }
+ if (hook_pid == 0) {
+ execve(argv[0], (char *const *)argv, env);
+ err(1, "execv failed");
+ }
+
+ waitpid(hook_pid, &hook_status, 0);
+
+ dodbg("hook exited with status %i",
WEXITSTATUS(hook_status));
+ if (WIFEXITED(hook_status) &&
+ WEXITSTATUS(hook_status) == 0) {
+ op = CHNG_ACK;
+ }
+ else
+ op = CHNG_FAIL;
+
+ if (writeop(netsock, COMM_CHNG_ACK, op) <= 0)
+ goto clean;
+
+ }
+ rc = 1;
+
+clean:
+ snprintf(env_ext[0], 17, "ACME_TASK=clean");
+
+ while (!SLIST_EMPTY(&queue)) {
+ chng = SLIST_FIRST(&queue);
+ SLIST_REMOVE_HEAD(&queue, next);
+
+ memcpy(env_ext + 2, chng->env, sizeof(chng->env));
+
+ if ((hook_pid = fork()) == -1) {
+ warn("fork");
+ goto out;
+ }
+ if (hook_pid == 0) {
+ execve(argv[0], (char *const *)argv, env);
+ err(1, "execv failed");
+ }
+
+ waitpid(hook_pid, &hook_status, 0);
+
+ //free(chng);
+ }
+
+ goto out;
+ }
+
if (unveil(root, "wc") == -1) {
warn("unveil %s", root);
goto out;
@@ -60,7 +247,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,6 +261,10 @@ 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)
@@ -82,13 +273,9 @@ chngproc(int netsock, const char *root)
warnx("token is too short");
goto out;
}
-
- for (i = 0; tok[i]; ++i) {
- int ch = (unsigned char)tok[i];
- if (!isalnum(ch) && ch != '-' && ch != '_') {
- warnx("token is not a valid base64url");
- goto out;
- }
+ else if (!is_alnum(tok, "-_")) {
+ warnx("token is not a valid base64url");
+ goto out;
}
if (asprintf(&fmt, "%s.%s", tok, th) == -1) {
@@ -160,9 +347,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 24 Feb 2024 20:33:31 -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 24 Feb 2024 20:33:31 -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 24 Feb 2024 20:33:31 -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 24 Feb 2024 20:33:31 -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 24 Feb 2024 20:33:31 -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 24 Feb 2024 20:33:31 -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;