From 87eb9bc2b755ea8cfa0c4667e3da3f25bdda1cb1 Mon Sep 17 00:00:00 2001 From: cinap_lenrek Date: Mon, 25 Oct 2021 16:59:29 +0000 Subject: [PATCH] acmed: add external command flag -e, improvements, bugs - allow for external command to be run to install a challenge using -e flag - remove the challengedom argument, it is given by the subject in the csr - fix some filedescriptor leaks in error paths --- sys/man/8/acmed | 22 ++++- sys/src/cmd/ip/acmed.c | 206 ++++++++++++++++++++++++++++------------- 2 files changed, 157 insertions(+), 71 deletions(-) diff --git a/sys/man/8/acmed b/sys/man/8/acmed index 6e7dbf26f..5159bb2a6 100644 --- a/sys/man/8/acmed +++ b/sys/man/8/acmed @@ -8,6 +8,9 @@ ip/acmed \- acme certificate client .I acctkey ] [ +.B -e +.I cmd +| .B -o .I chalout ] @@ -21,9 +24,6 @@ ip/acmed \- acme certificate client ] .I acctname .I csr -[ -.I domain -] .SH DESCRIPTION Acmed fetches and renews TLS certificates using the @@ -46,6 +46,18 @@ The key must be a .I jwk formatted RSA key. .TP +.B -e +.I cmd +Specifies that an external command sholud be run to +install the challenge response. +The +.I cmd +is run with the following four arguments: +The challenge method, +the identifier (domain), +the token, +and last the challenge response. +.TP .B -o .I chalout Specifies that the challenge material is @@ -133,10 +145,10 @@ database= .PP And .I acmed -must be invoked with the domain: +must be invoked like: .IP .EX -ip/acmed -t dns me@example.com mydomain.com.csr mydomain.com \\ +ip/acmed -t dns me@example.com mydomain.com.csr \\ >mydomain.com.crt .EE .SH SEE ALSO diff --git a/sys/src/cmd/ip/acmed.c b/sys/src/cmd/ip/acmed.c index 55724871c..15e147b28 100644 --- a/sys/src/cmd/ip/acmed.c +++ b/sys/src/cmd/ip/acmed.c @@ -21,11 +21,11 @@ struct Hdr { #define Contenttype "contenttype application/jose+json" #define between(x,min,max) (((min-1-x) & (x-max-1))>>8) int debug; -int (*challengefn)(char*, char*, int*); +int (*challengefn)(char*, char*, char*, int*); char *keyspec; char *provider = "https://acme-v02.api.letsencrypt.org/directory"; /* test endpoint */ +char *challengecmd; char *challengeout; -char *challengedom; char *keyid; char *epnewnonce; char *epnewacct; @@ -95,7 +95,7 @@ signRS256(char *hdr, char *prot) int afd; char *r; - if((afd = open("/mnt/factotum/rpc", ORDWR)) < 0) + if((afd = open("/mnt/factotum/rpc", ORDWR|OCEXEC)) < 0) return nil; if((rpc = auth_allocrpc(afd)) == nil){ close(afd); @@ -160,10 +160,10 @@ webopen(char *url, char *dir, int ndir) char buf[16]; int n, cfd, conn; - if((cfd = open("/mnt/web/clone", ORDWR)) == -1) + if((cfd = open("/mnt/web/clone", ORDWR|OCEXEC)) == -1) return -1; if((n = read(cfd, buf, sizeof(buf)-1)) == -1) - return -1; + goto Error; buf[n] = 0; conn = atoi(buf); @@ -187,7 +187,7 @@ get(char *url, int *n) if((cfd = webopen(url, dir, sizeof(dir))) == -1) goto Error; snprint(path, sizeof(path), "%s/%s", dir, "body"); - if((dfd = open(path, OREAD)) == -1) + if((dfd = open(path, OREAD|OCEXEC)) == -1) goto Error; r = slurp(dfd, n); Error: @@ -205,35 +205,39 @@ post(char *url, char *buf, int nbuf, int *nret, Hdr *h) r = nil; ok = 0; dfd = -1; + hfd = -1; if((cfd = webopen(url, dir, sizeof(dir))) == -1) goto Error; if(write(cfd, Contenttype, strlen(Contenttype)) == -1) goto Error; snprint(path, sizeof(path), "%s/%s", dir, "postbody"); - if((dfd = open(path, OWRITE)) == -1) + if((dfd = open(path, OWRITE|OCEXEC)) == -1) goto Error; if(write(dfd, buf, nbuf) != nbuf) goto Error; close(dfd); snprint(path, sizeof(path), "%s/%s", dir, "body"); - if((dfd = open(path, OREAD)) == -1) - goto Error; - if((r = slurp(dfd, nret)) == nil) + if((dfd = open(path, OREAD|OCEXEC)) == -1) goto Error; if(h != nil){ snprint(path, sizeof(path), "%s/%s", dir, h->name); - if((hfd = open(path, OREAD)) == -1) + if((hfd = open(path, OREAD|OCEXEC)) == -1) goto Error; if((h->val = slurp(hfd, &h->nval)) == nil) goto Error; - close(hfd); } + if((r = slurp(dfd, nret)) == nil) + goto Error; ok = 1; Error: + if(hfd != -1) close(hfd); if(dfd != -1) close(dfd); if(cfd != -1) close(cfd); - if(!ok && h != nil) + if(!ok && h != nil){ free(h->val); + h->val = nil; + h->nval = 0; + } return r; } @@ -289,10 +293,10 @@ getnonce(void) fprint(cfd, "request HEAD"); snprint(path, sizeof(path), "%s/%s", dir, "body"); - if((dfd = open(path, OREAD)) == -1) + if((dfd = open(path, OREAD|OCEXEC)) == -1) goto Error; snprint(path, sizeof(path), "%s/%s", dir, "replaynonce"); - if((hfd = open(path, OREAD)) == -1) + if((hfd = open(path, OREAD|OCEXEC)) == -1) goto Error; r = slurp(hfd, &n); Error: @@ -373,7 +377,7 @@ mkaccount(char *addr) { char *nonce, *hdr, *msg, *req, *resp; int nreq, nresp; - Hdr loc; + Hdr loc = { "location" }; if((nonce = getnonce()) == nil) sysfatal("get nonce: %r"); @@ -396,7 +400,6 @@ mkaccount(char *addr) sysfatal("failed to sign: %r"); dprint("req=\"%s\"\n", req); - loc.name = "location"; if((resp = post(epnewacct, req, nreq, &nresp, &loc)) == nil) sysfatal("failed req: %r"); dprint("resp=%s, loc=%s\n", resp, loc.val); @@ -438,8 +441,59 @@ submitorder(char **dom, int ndom, Hdr *hdr) return r; } +static void +hashauthbuf(char *buf, int nbuf) +{ + uchar hash[SHA2_256dlen]; + char *enc; + + sha2_256((uchar*)buf, strlen(buf), hash, nil); + if((enc = encurl64(hash, sizeof(hash))) == nil) + sysfatal("hashbuf: %r"); + if(snprint(buf, nbuf, "%s", enc) != strlen(enc)) + sysfatal("hashbuf: buffer too small, truncated"); + free(enc); +} + static int -httpchallenge(char *ty, char *tok, int *matched) +runchallenge(char *ty, char *dom, char *tok, int *matched) +{ + char auth[1024]; + Waitmsg *w; + int pid; + + snprint(auth, sizeof(auth), "%s.%s", tok, jwsthumb); + if(strcmp(ty, "dns-01") == 0) + hashauthbuf(auth, sizeof(auth)); + + pid = fork(); + switch(pid){ + case -1: + return -1; + case 0: + execl(challengecmd, challengecmd, ty, dom, tok, auth, nil); + exits("exec"); + } + + while((w = wait()) != nil){ + if(w->pid != pid){ + free(w); + continue; + } + if(w->msg[0] == '\0'){ + free(w); + *matched = 1; + return 0; + } + werrstr("%s", w->msg); + free(w); + return -1; + } + return -1; +} + +static int +httpchallenge(char *ty, char *, char *tok, int *matched) { char path[1024]; int fd, r; @@ -447,8 +501,9 @@ httpchallenge(char *ty, char *tok, int *matched) if(strcmp(ty, "http-01") != 0) return -1; *matched = 1; + snprint(path, sizeof(path), "%s/%s", challengeout, tok); - if((fd = create(path, OWRITE, 0666)) == -1) + if((fd = create(path, OWRITE|OCEXEC, 0666)) == -1) return -1; r = fprint(fd, "%s.%s\n", tok, jwsthumb); close(fd); @@ -456,70 +511,77 @@ httpchallenge(char *ty, char *tok, int *matched) } static int -dnschallenge(char *ty, char *tok, int *matched) +dnschallenge(char *ty, char *dom, char *tok, int *matched) { - char *enc, auth[1024], hash[SHA2_256dlen]; - int fd, r; + char auth[1024]; + int fd; if(strcmp(ty, "dns-01") != 0) return -1; *matched = 1; - if(challengedom == nil){ - werrstr("dns challenge requires domain"); + + snprint(auth, sizeof(auth), "%s.%s", tok, jwsthumb); + hashauthbuf(auth, sizeof(auth)); + + if((fd = create(challengeout, OWRITE|OCEXEC, 0666)) == -1){ + werrstr("could not create challenge: %r"); return -1; } - - r = -1; - fd = -1; - snprint(auth, sizeof(auth), "%s.%s", tok, jwsthumb); - sha2_256((uchar*)auth, strlen(auth), (uchar*)hash, nil); - if((enc = encurl64(hash, sizeof(hash))) == nil){ - werrstr("encoding failed: %r"); - goto Error; - } - if((fd = create(challengeout, OWRITE, 0666)) == -1){ - werrstr("could not create challenge: %r"); - goto Error; - } - if(fprint(fd,"dom=_acme-challenge.%s soa=\n\ttxtrr=%s\n", challengedom, enc) == -1){ + if(fprint(fd,"dom=_acme-challenge.%s soa=\n\ttxt=\"%s\"\n", dom, auth) == -1){ werrstr("could not write challenge: %r"); - goto Error; + close(fd); + return -1; } - if((fd = open("/net/dns", OWRITE)) == -1){ + close(fd); + + if((fd = open("/net/dns", OWRITE|OCEXEC)) == -1){ werrstr("could not open dns ctl: %r"); - goto Error; + return -1; } if(fprint(fd, "refresh") == -1){ werrstr("could not write dns refresh: %r"); - goto Error; - } - r = 0; - -Error: - if(fd != -1) close(fd); - free(enc); - return r; + return -1; + } + close(fd); + + return 0; } static int -challenge(JSON *j, char *authurl, int *matched) +challenge(JSON *j, char *authurl, JSON *id, char *dom[], int ndom, int *matched) { - JSON *ty, *url, *tok, *poll, *state; + JSON *dn, *ty, *url, *tok, *poll, *state; char *resp; int i, nresp; + if((dn = jsonbyname(id, "value")) == nil) + return -1; + if(dn->t != JSONString) + return -1; + + /* make sure the identifier matches the csr */ + for(i = 0; i < ndom; i++){ + if(cistrcmp(dom[i], dn->s) == 0) + break; + } + if(i >= ndom){ + werrstr("unknown challenge identifier '%s'", dn->s); + return -1; + } + if((ty = jsonbyname(j, "type")) == nil) return -1; if((url = jsonbyname(j, "url")) == nil) return -1; if((tok = jsonbyname(j, "token")) == nil) return -1; + if(ty->t != JSONString || url->t != JSONString || tok->t != JSONString) return -1; dprint("trying challenge %s\n", ty->s); - if(challengefn(ty->s, tok->s, matched) == -1){ + if(challengefn(ty->s, dn->s, tok->s, matched) == -1){ dprint("challengefn failed: %r\n"); return -1; } @@ -555,9 +617,9 @@ challenge(JSON *j, char *authurl, int *matched) } static int -dochallenges(JSON *order) +dochallenges(char *dom[], int ndom, JSON *order) { - JSON *chals, *j, *cl; + JSON *chals, *j, *cl, *id; JSONEl *ae, *ce; int nresp, matched; char *resp; @@ -583,6 +645,11 @@ dochallenges(JSON *order) werrstr("invalid challenge: %r"); return -1; } + if((id = jsonbyname(chals, "identifier")) == nil){ + werrstr("missing identifier"); + jsonfree(chals); + return -1; + } if((cl = jsonbyname(chals, "challenges")) == nil){ werrstr("missing challenge"); jsonfree(chals); @@ -590,7 +657,7 @@ dochallenges(JSON *order) } matched = 0; for(ce = cl->first; ce != nil; ce = ce->next){ - if(challenge(ce->val, ae->val->s, &matched) == 0) + if(challenge(ce->val, ae->val->s, id, dom, ndom, &matched) == 0) break; if(matched) werrstr("could not complete challenge: %r"); @@ -678,10 +745,10 @@ getcert(char *csrpath) uchar *der; int nder, ndom, fd; RSApub *rsa; - Hdr loc; + Hdr loc = { "location" }; JSON *o; - if((fd = open(csrpath, OREAD)) == -1) + if((fd = open(csrpath, OREAD|OCEXEC)) == -1) sysfatal("open %s: %r", csrpath); if((der = slurp(fd, &nder)) == nil) sysfatal("read %s: %r", csrpath); @@ -695,10 +762,9 @@ getcert(char *csrpath) close(fd); free(der); - loc.name = "location"; if((o = submitorder(dom, ndom, &loc)) == nil) sysfatal("order: %r"); - if(dochallenges(o) == -1) + if(dochallenges(dom, ndom, o) == -1) sysfatal("challenge: %r"); if(submitcsr(o, csr) == -1) sysfatal("signing cert: %r"); @@ -734,9 +800,11 @@ loadkey(char *path) DigestState *ds; int fd, nr; - if((fd = open(path, OREAD)) == -1) + if((fd = open(path, OREAD|OCEXEC)) == -1) return -1; - if((nr = readn(fd, key, sizeof(key))) == -1) + nr = readn(fd, key, sizeof(key)); + close(fd); + if(nr == -1) return -1; key[nr] = 0; @@ -764,7 +832,7 @@ loadkey(char *path) static void usage(void) { - fprint(2, "usage: %s [-a acctkey] [-o chalout] [-p provider] [-t type] acct csr [domain]\n", argv0); + fprint(2, "usage: %s [-a acctkey] [-e cmd | -o chalout] [-p provider] [-t type] acct csr\n", argv0); exits("usage"); } @@ -786,6 +854,9 @@ main(int argc, char **argv) case 'a': acctkey = EARGF(usage()); break; + case 'e': + challengecmd = EARGF(usage()); + break; case 'o': co = EARGF(usage()); break; @@ -800,7 +871,12 @@ main(int argc, char **argv) break; }ARGEND; - if(strcmp(ct, "http") == 0){ + if(challengecmd){ + if(co != nil) + usage(); + challengeout = "/dev/null"; + challengefn = runchallenge; + }else if(strcmp(ct, "http") == 0){ challengeout = (co != nil) ? co : "/usr/web/.well-known/acme-challenge"; challengefn = httpchallenge; }else if(strcmp(ct, "dns") == 0){ @@ -810,9 +886,7 @@ main(int argc, char **argv) sysfatal("unknown challenge type '%s'", ct); } - if(argc == 3) - challengedom = argv[2]; - else if(argc != 2) + if(argc != 2) usage(); if(acctkey == nil)