ref: 87eb9bc2b755ea8cfa0c4667e3da3f25bdda1cb1
parent: 4efd8575ebdccd2b915b39013799aaea9bc76719
author: cinap_lenrek <cinap_lenrek@felloff.net>
date: Mon Oct 25 12:59:29 EDT 2021
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
--- a/sys/man/8/acmed
+++ b/sys/man/8/acmed
@@ -8,6 +8,9 @@
.I acctkey
]
[
+.B -e
+.I cmd
+|
.B -o
.I chalout
]
@@ -21,9 +24,6 @@
]
.I acctname
.I csr
-[
-.I domain
-]
.SH DESCRIPTION
Acmed fetches and renews TLS certificates
using the
@@ -46,6 +46,18 @@
.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 @@
.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
--- a/sys/src/cmd/ip/acmed.c
+++ b/sys/src/cmd/ip/acmed.c
@@ -21,11 +21,11 @@
#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 @@
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 @@
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 @@
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 @@
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)
+ if((dfd = open(path, OREAD|OCEXEC)) == -1)
goto Error;
- if((r = slurp(dfd, nret)) == nil)
- 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 @@
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 @@
{
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 @@
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,9 +441,60 @@
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 @@
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,59 +511,65 @@
}
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");
- 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){
+ hashauthbuf(auth, sizeof(auth));
+
+ if((fd = create(challengeout, OWRITE|OCEXEC, 0666)) == -1){
werrstr("could not create challenge: %r");
- goto Error;
+ return -1;
}
- 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;
+ close(fd);
+ return -1;
}
- r = 0;
+ close(fd);
-Error:
- if(fd != -1)
- close(fd);
- free(enc);
- return r;
+ 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)
@@ -515,11 +576,12 @@
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 @@
}
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
case 'a':
acctkey = EARGF(usage());
break;
+ case 'e':
+ challengecmd = EARGF(usage());
+ break;
case 'o':
co = EARGF(usage());
break;
@@ -800,7 +871,12 @@
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 @@
sysfatal("unknown challenge type '%s'", ct);
}
- if(argc == 3)
- challengedom = argv[2];
- else if(argc != 2)
+ if(argc != 2)
usage();
if(acctkey == nil)