'tis the season to be jolly... I think it's time to kick the tires on this one.
I don't like the "exec" keyword, we should find something better. Also, should the user be optional? Oh, and it's not enforcing that exec is present in the config. sthen pointed me in the direction of dehydrated https://github.com/dehydrated-io/dehydrated/blob/master/docs/dns-verification.md and uacme https://github.com/ndilieto/uacme as examples for acme clients that implement hooks for (dns) challenges I implemented the uacme api since I find that less ugly. It should be trivial to transmogrify it with a shell one-liner to support dehydrated. Comments, tests? diff --git acme-client.conf.5 acme-client.conf.5 index 3c5fd1c2362..e580a365c51 100644 --- acme-client.conf.5 +++ acme-client.conf.5 @@ -170,11 +170,55 @@ in one file, and is required by most browsers. This is optional if .Ar domain certificate is specified. -.It Ic sign with Ar authority +.It Ic sign with Ar authority Op Ic challenge Ar type The certificate authority (as declared above in the .Sx AUTHORITIES section) to use. If this setting is absent, the first authority specified is used. +.Ar type +can be +.Cm http +or +.Cm dns . +It defaults to +.Cm http . +.It Ic exec Ar script Ic as Ar user +Run +.Ar script +as user +.Ar user +for each +.Cm dns +challenge. +This is required when using the +.Cm dns +challenge type. +The script is called with five arguments: +.Bl -tag -width Ds +.It Ar method +.Cm begin , +.Cm done , +or +.Cm failed . +.Cm begin +indicates that a DNS record should be created and +.Cm done +or +.Cm failed +indicate that a DNS record should be removed. +.It Ar type +.Cm dns-01 . +.It Ar ident +The domain. +.It Ar token +Unused, for compatibility with existing hook scripts. +.It Ar auth +The challenge response. +.El +.Pp +The script needs to create a DNS record of the form +.Dl _acme-challenge.ident 30 IN TXT auth +and exit once it has propagated to all name servers. .It Ic challengedir Ar path The directory in which the challenge file will be stored. If it is not specified, a default of diff --git chngproc.c chngproc.c index 476daed3416..deef61fccc6 100644 --- chngproc.c +++ chngproc.c @@ -15,10 +15,13 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <sys/wait.h> + #include <assert.h> #include <err.h> #include <errno.h> #include <fcntl.h> +#include <pwd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -26,8 +29,38 @@ #include "extern.h" -int -chngproc(int netsock, const char *root) +static int +do_exec(const char *exec, const char *method, const char *type, + const char *ident, const char *token, const char *auth) +{ + pid_t pid; + int status; + + switch (pid = fork()) { + case -1: + warn("fork"); + return -1; + case 0: /* child */ + /* XXX close netproc fd */ + if (pledge("stdio rpath exec", NULL) == -1) { + warn("pledge"); + return -1; + } + execl(exec, exec, method, type, ident, token, auth, NULL); + warn("execl %s", exec); + _exit(1); + } + if (waitpid(pid, &status, 0) == -1) { + warn("waitpid"); + return -1; + } + if (WEXITSTATUS(status) == 0) + return 0; + return -1; +} + +static int +chngproc_http(int netsock, const char *root) { char *tok = NULL, *th = NULL, *fmt = NULL, **fs = NULL; size_t i, fsz = 0; @@ -153,3 +186,114 @@ out: free(tok); return rc; } + +static int +chngproc_dns(int netsock, const char* exec, const char *exec_as) +{ + struct passwd *pw; + size_t i, identsz = 0; + long lval; + int rc = 0, cc; + enum chngop op; + char *ident = NULL, *tok = NULL, *auth = NULL; + char **idents = NULL; + void *pp; + + if ((pw = getpwnam(exec_as)) == NULL) { + warn("getpwnam"); + goto out; + } + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) { + warnx("can't drop privileges"); + goto out; + } + + if (unveil(exec, "x") == -1) { + warn("unveil"); + goto out; + } + + if (pledge("stdio rpath proc exec", NULL) == -1) { + warn("pledge"); + goto out; + } + + for (;;) { + op = CHNG__MAX; + 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 out; + } else if (op == CHNG_STOP) + break; + + assert(op == CHNG_SYN); + + if ((ident = readstr(netsock, COMM_IDENT)) == NULL) + goto out; + if ((tok = readstr(netsock, COMM_TOK)) == NULL) + goto out; + else if ((auth = readstr(netsock, COMM_AUTH)) == NULL) + goto out; + + pp = reallocarray(idents, (identsz + 1), sizeof(char *)); + if (pp == NULL) { + warn("realloc"); + goto out; + } + idents = pp; + if ((idents[identsz] = strdup(ident)) == NULL) { + warn("strdup"); + goto out; + } + identsz++; + if (do_exec(exec, "begin", "dns-01", ident, tok, auth) == -1) { + warnx("exec failed"); + goto out; + } + free(ident); + ident = NULL; + free(tok); + tok = NULL; + free(auth); + auth = NULL; + + cc = writeop(netsock, COMM_CHNG_ACK, CHNG_ACK); + if (cc == 0) + break; + if (cc < 0) + goto out; + } + + rc = 1; +out: + close(netsock); + for (i = 0; i < identsz; i++) { + if (do_exec(exec, rc == 1 ? "done" : "failed", "dns-01", + idents[i], "", "") == -1) + warnx("exec failed"); + } + free(ident); + free(tok); + free(auth); + return rc; +} + +int +chngproc(int netsock, enum chngtype chngtype, const char *root, const char + *exec, const char *exec_as) +{ + switch (chngtype) { + case CHNG_HTTP: + return chngproc_http(netsock, root); + case CHNG_DNS: + return chngproc_dns(netsock, exec, exec_as); + } + return 0; +} diff --git extern.h extern.h index 4b43b6ef4ac..ad78b636bf7 100644 --- extern.h +++ extern.h @@ -122,6 +122,8 @@ enum comm { COMM_KID, COMM_URL, COMM_TOK, + COMM_IDENT, + COMM_AUTH, COMM_CHNG_OP, COMM_CHNG_ACK, COMM_ACCT, @@ -161,6 +163,7 @@ struct chng { char *token; /* token we must offer */ char *error; /* "detail" field in case of error */ size_t retry; /* how many times have we tried */ + char *identifier; /* the domain */ enum chngstatus status; /* challenge accepted? */ }; @@ -202,7 +205,8 @@ __BEGIN_DECLS */ int acctproc(int, const char *, enum keytype); int certproc(int, int); -int chngproc(int, const char *); +int chngproc(int, enum chngtype, const char *, const char*, + const char*); int dnsproc(int); int revokeproc(int, const char *, int, int, const char *const *, size_t); @@ -212,7 +216,7 @@ 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 *, - size_t); + size_t, enum chngtype); /* * Debugging functions. @@ -253,7 +257,8 @@ struct jsmnn *json_parse(const char *, size_t); 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 *); +int json_parse_challenge(struct jsmnn *, struct chng *, + enum chngtype); void json_free_order(struct order *); int json_parse_order(struct jsmnn *, struct order *); int json_parse_upd_order(struct jsmnn *, struct order *); diff --git json.c json.c index 92e087b2ec7..1e382ba26c7 100644 --- json.c +++ json.c @@ -371,7 +371,7 @@ json_parse_response(struct jsmnn *n) * We only care about the HTTP-01 response. */ int -json_parse_challenge(struct jsmnn *n, struct chng *p) +json_parse_challenge(struct jsmnn *n, struct chng *p, enum chngtype chngtype) { struct jsmnn *array, *obj, *error; size_t i; @@ -381,6 +381,12 @@ json_parse_challenge(struct jsmnn *n, struct chng *p) if (n == NULL) return 0; + obj = json_getobj(n, "identifier"); + if (obj == NULL) + return 0; + if ((p->identifier = json_getstr(obj, "value")) == NULL) + return 0; + array = json_getarray(n, "challenges"); if (array == NULL) return 0; @@ -392,7 +398,16 @@ json_parse_challenge(struct jsmnn *n, struct chng *p) type = json_getstr(obj, "type"); if (type == NULL) continue; - rc = strcmp(type, "http-01"); + + switch (chngtype) { + case CHNG_HTTP: + rc = strcmp(type, "http-01"); + break; + case CHNG_DNS: + rc = strcmp(type, "dns-01"); + break; + } + free(type); if (rc) continue; diff --git main.c main.c index 65ea2cf3ac3..0f05de9bce6 100644 --- main.c +++ main.c @@ -213,11 +213,9 @@ main(int argc, char *argv[]) close(file_fds[1]); close(dns_fds[0]); close(rvk_fds[0]); - c = netproc(key_fds[1], acct_fds[1], - chng_fds[1], cert_fds[1], - dns_fds[1], rvk_fds[1], - revocate, authority, - (const char *const *)alts, altsz); + c = netproc(key_fds[1], acct_fds[1], chng_fds[1], cert_fds[1], + dns_fds[1], rvk_fds[1], revocate, authority, + (const char *const *)alts, altsz, domain->chng); exit(c ? EXIT_SUCCESS : EXIT_FAILURE); } @@ -282,7 +280,8 @@ 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], domain->chng, chngdir, domain->exec, + domain->exec_as); exit(c ? EXIT_SUCCESS : EXIT_FAILURE); } diff --git netproc.c netproc.c index 7c502643acc..61a8c2e414d 100644 --- netproc.c +++ netproc.c @@ -25,6 +25,8 @@ #include <tls.h> #include <vis.h> +#include <openssl/evp.h> + #include "http.h" #include "extern.h" #include "parse.h" @@ -507,7 +509,8 @@ doupdorder(struct conn *c, struct order *order) * On non-zero exit, fills in "chng" with the challenge. */ static int -dochngreq(struct conn *c, const char *auth, struct chng *chng) +dochngreq(struct conn *c, const char *auth, struct chng *chng, enum chngtype + chngtype) { int rc = 0; long lc; @@ -521,7 +524,7 @@ dochngreq(struct conn *c, const char *auth, struct chng *chng) 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)) + else if (!json_parse_challenge(j, chng, chngtype)) warnx("%s: bad challenge", auth); else rc = 1; @@ -666,14 +669,52 @@ dodirs(struct conn *c, const char *addr, struct capaths *paths) return rc; } +static char * +fmt_dns_chng(const char *token, const char *thumb) +{ + EVP_MD_CTX *ctx = NULL; + unsigned int digsz; + unsigned char *dig = NULL; + char *resp = NULL, *dig64 = NULL; + + if (asprintf(&resp, "%s.%s", token, thumb) == -1) { + resp = NULL; + goto out; + } else if ((dig = malloc(EVP_MAX_MD_SIZE)) == NULL) { + warn("malloc"); + goto out; + } else if ((ctx = EVP_MD_CTX_create()) == NULL) { + warnx("EVP_MD_CTX_create"); + goto out; + } else if (!EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) { + warnx("EVP_SignInit_ex"); + goto out; + } else if (!EVP_DigestUpdate(ctx, resp, strlen(resp))) { + warnx("EVP_SignUpdate"); + goto out; + } else if (!EVP_DigestFinal_ex(ctx, dig, &digsz)) { + warnx("EVP_SignFinal"); + goto out; + } else if ((dig64 = base64buf_url((char *)dig, digsz)) == NULL) + warnx("base64buf_url"); +out: + if (ctx != NULL) + EVP_MD_CTX_destroy(ctx); + free(dig); + free(resp); + return dig64; +} + + + /* * Communicate with the ACME server. * We need the certificate we want to upload and our account key information. */ int netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, - int revocate, struct authority_c *authority, - const char *const *alts, size_t altsz) + int revocate, struct authority_c *authority, const char *const *alts, + size_t altsz, enum chngtype chngtype) { int rc = 0; size_t i; @@ -811,7 +852,8 @@ netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, goto out; } for (i = 0; i < order.authsz; i++) { - if (!dochngreq(&c, order.auths[i], &chngs[i])) + if (!dochngreq(&c, order.auths[i], &chngs[i], + chngtype)) goto out; dodbg("challenge, token: %s, uri: %s, status: " @@ -822,19 +864,41 @@ netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, chngs[i].status == CHNG_INVALID) continue; + /* XXX not correct for dns-01? */ if (chngs[i].retry++ >= RETRY_MAX) { warnx("%s: too many tries", chngs[i].uri); goto out; } - if (writeop(Cfd, COMM_CHNG_OP, CHNG_SYN) <= 0) - goto out; - else if (writestr(Cfd, COMM_THUMB, thumb) <= 0) - goto out; - else if (writestr(Cfd, COMM_TOK, - chngs[i].token) <= 0) - goto out; + switch (chngtype) { + case CHNG_HTTP: + if (writeop(Cfd, COMM_CHNG_OP, + CHNG_SYN) <= 0) + goto out; + else if (writestr(Cfd, COMM_THUMB, + thumb) <= 0) + goto out; + else if (writestr(Cfd, COMM_TOK, + chngs[i].token) <= 0) + goto out; + break; + case CHNG_DNS: + if (writeop(Cfd, COMM_CHNG_OP, + CHNG_SYN) <= 0) + goto out; + else if (writestr(Cfd, COMM_IDENT, + chngs[i].identifier) <= 0) + goto out; + else if (writestr(Cfd, COMM_TOK, + chngs[i].token) <= 0) + goto out; + else if (writestr(Cfd, COMM_AUTH, + fmt_dns_chng(chngs[i].token, + thumb)) <= 0) + goto out; + break; + } /* Read that the challenge has been made. */ if (readop(Cfd, COMM_CHNG_ACK) != CHNG_ACK) @@ -879,7 +943,7 @@ netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, if (order.status != ORDER_VALID) { for (i = 0; i < order.authsz; i++) { - dochngreq(&c, order.auths[i], &chngs[i]); + dochngreq(&c, order.auths[i], &chngs[i], chngtype); if (chngs[i].error != NULL) { if (stravis(&error, chngs[i].error, VIS_SAFE) != -1) { diff --git parse.h parse.h index 3954f62a0d0..435bd22102e 100644 --- parse.h +++ parse.h @@ -22,6 +22,14 @@ #define AUTH_MAXLEN 120 /* max length of an authority_c name */ #define DOMAIN_MAXLEN 255 /* max len of a domain name (rfc2181) */ +/* + * Supported challenge types. + */ +enum chngtype { + CHNG_HTTP, + CHNG_DNS +}; + /* * XXX other size limits needed? * limit all paths to PATH_MAX @@ -54,6 +62,9 @@ struct domain_c { char *fullchain; char *auth; char *challengedir; + enum chngtype chng; + char *exec; + char *exec_as; }; struct altname_c { diff --git parse.y parse.y index 1febcb10a3a..22faef7e457 100644 --- parse.y +++ parse.y @@ -102,7 +102,7 @@ typedef struct { %token AUTHORITY URL API ACCOUNT CONTACT %token DOMAIN ALTERNATIVE NAME NAMES CERT FULL CHAIN KEY SIGN WITH CHALLENGEDIR -%token YES NO +%token YES NO CHALLENGE HTTP DNS EXEC AS %token INCLUDE %token ERROR %token RSA ECDSA @@ -368,7 +368,7 @@ domainoptsl : ALTERNATIVE NAMES '{' altname_l '}' } domain->fullchain = s; } - | SIGN WITH STRING { + | SIGN WITH STRING optchallenge { char *s; if (domain->auth != NULL) { yyerror("duplicate sign with"); @@ -383,6 +383,19 @@ domainoptsl : ALTERNATIVE NAMES '{' altname_l '}' } domain->auth = s; } + | EXEC STRING AS STRING { + char *s; + if (domain->exec != NULL || domain->exec_as != NULL) { + yyerror("duplicate exec"); + YYERROR; + } + if ((s = strdup($2)) == NULL) + err(EXIT_FAILURE, "strdup"); + domain->exec = s; + if ((s = strdup($4)) == NULL) + err(EXIT_FAILURE, "strdup"); + domain->exec_as = s; + } | CHALLENGEDIR STRING { char *s; if (domain->challengedir != NULL) { @@ -395,6 +408,17 @@ domainoptsl : ALTERNATIVE NAMES '{' altname_l '}' } ; +optchallenge : CHALLENGE HTTP { + domain->chng = CHNG_HTTP; + } + | CHALLENGE DNS { + domain->chng = CHNG_DNS; + } + | { + domain->chng = CHNG_HTTP; + } + ; + altname_l : altname comma altname_l | altname ; @@ -458,14 +482,19 @@ lookup(char *s) {"account", ACCOUNT}, {"alternative", ALTERNATIVE}, {"api", API}, + {"as", AS}, {"authority", AUTHORITY}, {"certificate", CERT}, {"chain", CHAIN}, + {"challenge", CHALLENGE}, {"challengedir", CHALLENGEDIR}, {"contact", CONTACT}, + {"dns", DNS}, {"domain", DOMAIN}, {"ecdsa", ECDSA}, + {"exec", EXEC}, {"full", FULL}, + {"http", HTTP}, {"include", INCLUDE}, {"key", KEY}, {"name", NAME}, @@ -1082,7 +1111,12 @@ print_config(struct acme_conf *xconf) printf("\tdomain full chain certificate \"%s\"\n", d->fullchain); if (d->auth != NULL) - printf("\tsign with \"%s\"\n", d->auth); + printf("\tsign with \"%s\" challenge %s\n", d->auth, + d->chng == CHNG_HTTP ? "http" : "dns"); + if (d->chng == CHNG_DNS && d->exec != NULL && d->exec_as + != NULL) + printf("\texec \"%s\" as \"%s\"\n", d->exec, + d->exec_as); if (d->challengedir != NULL) printf("\tchallengedir \"%s\"\n", d->challengedir); printf("}\n\n"); @@ -1099,6 +1133,8 @@ int domain_valid(const char *cp) { + if (*cp == '*' && *(cp + 1) == '.') + cp += 2; for ( ; *cp != '\0'; cp++) if (!(*cp == '.' || *cp == '-' || *cp == '_' || isalnum((int)*cp))) diff --git util.c util.c index 4da5b294163..2c980c7bac5 100644 --- util.c +++ util.c @@ -51,6 +51,8 @@ static const char *const comms[COMM__MAX] = { "payload", /* COMM_PAY */ "nonce", /* COMM_NONCE */ "token", /* COMM_TOK */ + "domain", /* COMM_IDENT */ + "auth", /* COMM_AUTH */ "challenge-op", /* COMM_CHNG_OP */ "challenge-ack", /* COMM_CHNG_ACK */ "account", /* COMM_ACCT */ -- I'm not entirely sure you are real.