code: plan9front

Download patch

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)