shithub: acmed

Download patch

ref: aa8713e13b9f8de595b6c34015a9d068425fc9c4
parent: ac9970d30b5153298b96d551379f99923b4d1a57
author: Ori Bernstein <[email protected]>
date: Mon Jul 5 19:22:15 EDT 2021

renamed, update manpages

--- a/aclient.c
+++ /dev/null
@@ -1,794 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <json.h>
-#include <libsec.h>
-#include <auth.h>
-#include <authsrv.h>
-
-typedef struct Hdr Hdr;
-
-#pragma varargck	type	"E"	char*
-
-struct Hdr {
-	char	*name;
-	char	*val;
-	int	nval;
-};
-
-#define Keyspec		"proto=rsa service=acme role=sign hash=sha256 acct=%s"
-#define Useragent	"useragent aclient-plan9"
-#define Contenttype	"contenttype application/jose+json"
-#define between(x,min,max)	(((min-1-x) & (x-max-1))>>8)
-int	debug;
-int	(*challengefn)(JSON*, char*, int*);
-char	*keyspec;
-char	*provider = "https://acme-v02.api.letsencrypt.org/directory"; /* test endpoint */
-char	*challengedir = "/usr/web/.well-known/acme-challenge";
-char	*outdir;
-char	*keyid;
-char	*epnewnonce;
-char	*epnewacct;
-char	*epneworder;
-char	*eprevokecert;
-char	*epkeychange;
-char	*csrkey;
-char	*jwsthumb;
-JSON	*jwskey;
-
-#define dprint(...) if(debug)fprint(2, __VA_ARGS__);
-
-char*
-evsmprint(char *fmt, va_list ap)
-{
-	char *r;
-
-	if((r = vsmprint(fmt, ap)) == nil)
-		abort();
-	return r;
-}
-
-char*
-esmprint(char *fmt, ...)
-{
-	va_list ap;
-	char *r;
-
-	va_start(ap, fmt);
-	r = evsmprint(fmt, ap);
-	va_end(ap);
-	return r;
-}
-
-int
-encurl64chr(int o)
-{
-	int c;
-
-	c  = between(o,  0, 25) & ('A'+o);
-	c |= between(o, 26, 51) & ('a'+(o-26));
-	c |= between(o, 52, 61) & ('0'+(o-52));
-	c |= between(o, 62, 62) & ('-');
-	c |= between(o, 63, 63) & ('_');
-	return c;
-}
-char*
-encurl64(void *in, int n)
-{
-	int lim;
-	char *out, *p;
-
-	lim = 4*n/3 + 5;
-	if((out = malloc(lim)) == nil)
-		abort();
-	enc64x(out, lim, in, n, encurl64chr);
-	if((p = strchr(out, '=')) != nil)
-		*p = 0;
-	return out;
-}
-
-char*
-signRS256(char *hdr, char *prot)
-{
-	uchar hash[SHA2_256dlen];
-	DigestState *s;
-	AuthRpc *rpc;
-	int afd;
-	char *r;
-
-	if((afd = open("/mnt/factotum/rpc", ORDWR)) < 0)
-		return nil;
-	if((rpc = auth_allocrpc(afd)) == nil){
-		close(afd);
-		return nil;
-	}
-	if(auth_rpc(rpc, "start", keyspec, strlen(keyspec)) != ARok){
-		auth_freerpc(rpc);
-		close(afd);
-		return nil;
-	}
-
-	s = sha2_256((uchar*)hdr, strlen(hdr), nil, nil);
-	s = sha2_256((uchar*)".", strlen("."), nil, s);
-	sha2_256((uchar*)prot, strlen(prot), hash, s);
-
-	if(auth_rpc(rpc, "write", hash, sizeof(hash)) != ARok)
-		sysfatal("sign: write hash: %r");
-	if(auth_rpc(rpc, "read", nil, 0) != ARok)
-		sysfatal("sign: read sig: %r");
-	r = encurl64(rpc->arg, rpc->narg);
-	auth_freerpc(rpc);
-	close(afd);
-	return r;	
-}
-
-/*
- * Reads all available data from an fd.
- * guarantees returned value is terminated.
- */
-static char*
-slurp(int fd, int *n)
-{
-	char *b;
-	int r, sz;
-
-	*n = 0;
-	sz = 32;
-	if((b = malloc(sz)) == nil)
-		abort();
-	while(1){
-		if(*n + 1 == sz){
-			sz *= 2;
-			if((b = realloc(b, sz)) == nil)
-				abort();
-		}
-		r = read(fd, b + *n, sz - *n - 1);
-		if(r == 0)
-			break;
-		if(r == -1){
-			free(b);
-			return nil;
-		}
-		*n += r;
-	}
-	b[*n] = 0;
-	return b;
-}
-		
-static int
-webopen(char *url, char *dir, int ndir)
-{
-	char buf[16];
-	int n, cfd, conn;
-
-	if((cfd = open("/mnt/web/clone", ORDWR)) == -1)
-		return -1;
-	if((n = read(cfd, buf, sizeof(buf)-1)) == -1)
-		return -1;
-	buf[n] = 0;
-	conn = atoi(buf);
-
-	if(fprint(cfd, "url %s", url) == -1)
-		goto Error;
-	snprint(dir, ndir, "/mnt/web/%d", conn);
-	return cfd;
-Error:
-	close(cfd);
-	return -1;
-}
-
-static char*
-get(char *url, int *n)
-{
-	char *r, dir[64], path[80];
-	int cfd, dfd;
-
-	r = nil;
-	dfd = -1;
-	if((cfd = webopen(url, dir, sizeof(dir))) == -1)
-		goto Error;
-	snprint(path, sizeof(path), "%s/%s", dir, "body");
-	if((dfd = open(path, OREAD)) == -1)
-		goto Error;
-	r = slurp(dfd, n);
-Error:
-	if(dfd != -1) close(dfd);
-	if(cfd != -1) close(cfd);
-	return r;
-}
-
-static char*
-post(char *url, char *buf, int nbuf, int *nret, Hdr *h)
-{
-	char *r, dir[64], path[80];
-	int cfd, dfd, hfd, ok;
-
-	r = nil;
-	ok = 0;
-	dfd = -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)
-		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)
-		goto Error;
-	if(h != nil){
-		snprint(path, sizeof(path), "%s/%s", dir, h->name);
-		if((hfd = open(path, OREAD)) == -1)
-			goto Error;
-		if((h->val = slurp(hfd, &h->nval)) == nil)
-			goto Error;
-		close(hfd);
-	}
-	ok = 1;
-Error:
-	if(dfd != -1) close(dfd);
-	if(cfd != -1) close(cfd);
-	if(!ok && h != nil)
-		free(h->val);
-	return r;
-}
-
-static int
-endpoints(void)
-{
-	JSON *j;
-	JSONEl *e;
-	char *s;
-	int n;
-
-	if((s = get(provider, &n)) == nil)
-		sysfatal("get %s: %r", provider);
-	if((j = jsonparse(s)) == nil)
-		sysfatal("parse endpoints: %r");
-	if(j->t != JSONObject)
-		sysfatal("expected object");
-	for(e = j->first; e != nil; e = e->next){
-		if(e->val->t != JSONString)
-			continue;
-		if(strcmp(e->name, "keyChange") == 0)
-			epkeychange = strdup(e->val->s);
-		else if(strcmp(e->name, "newAccount") == 0)
-			epnewacct = strdup(e->val->s);
-		else if(strcmp(e->name, "newNonce") == 0)
-			epnewnonce = strdup(e->val->s);
-		else if(strcmp(e->name, "newOrder") == 0)
-			epneworder = strdup(e->val->s);
-		else if(strcmp(e->name, "revokeCert") == 0)
-			eprevokecert = strdup(e->val->s);
-	}
-	jsonfree(j);
-	free(s);
-	if(epnewnonce==nil|| epnewacct==nil || epneworder==nil
-	|| eprevokecert==nil || epkeychange==nil){
-		sysfatal("missing directory entries");
-		return -1;
-	}
-	return 0;
-}
-
-static char*
-getnonce(void)
-{
-	char *r, dir[64], path[80];
-	int n, cfd, dfd, hfd;
-
-	r = nil;
-	dfd = -1;
-	hfd = -1;
-	if((cfd = webopen(epnewnonce, dir, sizeof(dir))) == -1)
-		goto Error;
-	fprint(cfd, "request HEAD");
-
-	snprint(path, sizeof(path), "%s/%s", dir, "body");
-	if((dfd = open(path, OREAD)) == -1)
-		goto Error;
-	snprint(path, sizeof(path), "%s/%s", dir, "replaynonce");
-	if((hfd = open(path, OREAD)) == -1)
-		goto Error;
-	r = slurp(hfd, &n);
-Error:
-	if(hfd != -1)
-		close(hfd);
-	if(dfd != -1)
-		close(dfd);
-	close(cfd);
-	return r;
-}
-
-char*
-jwsenc(char *hdr, char *msg, int *nbuf)
-{
-	char *h, *m, *s, *r;
-
-	h = encurl64(hdr, strlen(hdr));
-	m = encurl64(msg, strlen(msg));
-	s = signRS256(h, m);
-	if(s == nil)
-		return nil;
-
-	r = esmprint(
-		"{\n"
-		"\"protected\": \"%s\",\n"
-		"\"payload\": \"%s\",\n"
-		"\"signature\": \"%s\"\n"
-		"}\n",
-		h, m, s);
-	*nbuf = strlen(r);
-	free(h);
-	free(m);
-	free(s);
-
-	return r;
-}
-
-char*
-jwsheader(char *url)
-{
-	char *nonce;
-
-	if((nonce = getnonce()) == nil)
-		sysfatal("get nonce: %r");
-	return esmprint(
-		"{"
-		"\"alg\": \"RS256\","
-		"\"nonce\": \"%E\","
-		"\"kid\": \"%E\","
-		"\"url\": \"%E\""
-		"}",
-		nonce, keyid, url);
-}
-
-char*
-jwsrequest(char *url, int *nresp, Hdr *h, char *fmt, ...)
-{
-	char *hdr, *msg, *req, *resp;
-	int nreq;
-	va_list ap;
-
-	va_start(ap, fmt);
-	hdr = jwsheader(url);
-	msg = evsmprint(fmt, ap);
-	req = jwsenc(hdr, msg, &nreq);
-	dprint("req=\"%s\"\n", req);
-	resp = post(url, req, nreq, nresp, h);
-	free(hdr);
-	free(req);
-	free(msg);
-	va_end(ap);
-	dprint("resp=%s\n", resp);
-	return resp;
-}
-
-static void
-mkaccount(char *addr)
-{
-	char *nonce, *hdr, *msg, *req, *resp;
-	int nreq, nresp;
-	Hdr loc;
-
-	if((nonce = getnonce()) == nil)
-		sysfatal("get nonce: %r");
-	hdr = esmprint(
-		"{"
-		"\"alg\": \"RS256\","
-		"\"jwk\": %J,"
-		"\"nonce\": \"%E\","
-		"\"url\": \"%E\""
-		"}",
-		jwskey, nonce, epnewacct);
-	msg = esmprint(
-		"{"
-		"\"termsOfServiceAgreed\": true,"
-		"\"contact\": [\"mailto:%E\"]"
-		"}",
-		addr);
-	free(nonce);
-	if((req = jwsenc(hdr, msg, &nreq)) == nil)
-		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);
-	keyid = loc.val;
-}
-
-static JSON*
-submitorder(char *dom, Hdr *hdr)
-{
-	char *resp;
-	int nresp;
-	JSON *r;
-
-	resp = jwsrequest(epneworder, &nresp, hdr,
-		"{"
-		"  \"identifiers\": [{"
-		"    \"type\": \"dns\","
-		"    \"value\": \"%E\""
-		"  }],"
-		"  \"wildcard\": false"
-		"}",
-		dom);
-	if(resp == nil)
-		sysfatal("submit order: %r");
-	if((r = jsonparse(resp)) == nil)
-		sysfatal("parse order: %r");
-	free(resp);
-	return r;
-}
-
-static int
-httpchallenge(JSON *j, char *authurl, int *matched)
-{
-	JSON *ty, *url, *tok, *poll, *state;
-	char *resp, path[1024];
-	int i, fd, nresp;
-
-	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;
-	if(strcmp(ty->s, "http-01") != 0)
-		return -1;
-	*matched = 1;
-
-	snprint(path, sizeof(path), "%s/%s", challengedir, tok->s);
-	if((fd = create(path, OWRITE, 0666)) == -1)
-		sysfatal("create: %r"); //return -1;
-	if(fprint(fd, "%s.%s\n", tok->s, jwsthumb) == -1)
-		return -1;
-	close(fd);
-
-	if((resp = jwsrequest(url->s, &nresp, nil, "{}")) == nil)
-		sysfatal("challenge: post %s: %r", url->s);
-	free(resp);
-
-	for(i = 0; i < 60; i++){
-		sleep(1000);
-		if((resp = jwsrequest(authurl, &nresp, nil, "")) == nil)
-			sysfatal("challenge: post %s: %r", url->s);
-		if((poll = jsonparse(resp)) == nil){
-			free(resp);
-			return -1;
-		}
-		if((state = jsonbyname(poll, "status")) != nil && state->t == JSONString){
-			if(strcmp(state->s, "valid") == 0){
-				jsonfree(poll);
-				return 0;
-			}
-			else if(strcmp(state->s, "pending") != 0){
-				fprint(2, "error: %J", poll);
-				werrstr("status '%s'", state->s);
-				jsonfree(poll);
-				return -1;
-			}
-		}
-		jsonfree(poll);	
-	}
-	werrstr("timeout");
-	return -1;
-}
-
-static int
-dnschallenge(JSON*, char*, int*)
-{
-	return -1;
-}
-
-static int
-dochallenges(JSON *order)
-{
-	JSON *chals, *j, *cl;
-	JSONEl *ae, *ce;
-	int nresp, matched;
-	char *resp;
-
-	if((j = jsonbyname(order, "authorizations")) == nil)
-		sysfatal("parse response: missing authorizations");
-	if(j->t != JSONArray)
-		sysfatal("parse response: authorizations must be array");
-	for(ae = j->first; ae != nil; ae = ae->next){
-		if(ae->val->t != JSONString)
-			sysfatal("challenge: auth must be url");
-		if((resp = jwsrequest(ae->val->s, &nresp, nil, "")) == nil)
-			sysfatal("challenge: request %s: %r", ae->val->s);
-		if((chals = jsonparse(resp)) == nil)
-			goto Error;
-		if((cl = jsonbyname(chals, "challenges")) == nil){
-			jsonfree(chals);
-			goto Error;
-		}
-		matched = 0;
-		for(ce = cl->first; ce != nil; ce = ce->next){
-			if(challengefn(ce->val, ae->val->s, &matched) == 0)
-				break;
-			if(matched)
-				sysfatal("could not complete challenge: %r");
-		}
-		if(!matched)
-			sysfatal("no matching auth type");
-		jsonfree(chals);
-		free(resp);
-	}
-	return 0;
-Error:
-	jsonfree(j);
-	return -1;
-}
-
-static char*
-gencsr(char *dom)
-{
-	char cn[512];
-	char *der, *b64;
-	int nder, pid, pfd[2];
-	Waitmsg *w;
-
-	snprint(cn, sizeof(cn), "CN=%s,%s", dom,dom);
-
-	if(pipe(pfd) == -1)
-		return nil;
-	if((pid = fork()) == -1)
-		return nil;
-	if(pid == 0){
-		close(pfd[1]);
-		dup(pfd[0], 0);
-		dup(pfd[0], 1);
-		execl("/bin/auth/rsa2csr", "rsa2csr", cn, csrkey, nil);
-		sysfatal("exec: %r");
-	}
-	close(pfd[0]);
-	if((der = slurp(pfd[1], &nder)) == nil)
-		return nil;
-	if((w = wait()) == nil){
-		free(der);
-		return nil;
-	}
-	if(w->msg == nil && strlen(w->msg) != 0){
-		werrstr(w->msg);
-		free(der);
-		free(w);
-		return nil;
-	}
-	free(w);
-	b64 = encurl64(der, nder);
-	free(der);
-	return b64;
-}
-
-static int
-submitcsr(JSON *order, char *dom)
-{
-	char *csr, *resp;
-	int nresp;
-	JSON *j;
-
-	if((j = jsonbyname(order, "finalize")) == nil)
-		sysfatal("parse response: missing authorizations");
-	if(j->t != JSONString)
-		sysfatal("parse response: finalizer must be string");
-	if((csr = gencsr(dom)) == nil)
-		sysfatal("gencsr: %r");
-	if((resp = jwsrequest(j->s, &nresp, nil, "{\"csr\":\"%E\"}", csr)) == nil)
-		sysfatal("submit csr: %r");
-	free(csr);
-	free(resp);
-	return 0;
-}
-
-static int
-fetchcert(char *url, char *dom)
-{
-	JSON *cert, *poll, *state;
-	char *resp, path[1024];
-	int fd, nresp, i;
-
-	poll = nil;
-	for(i = 0; i < 60; i++){
-		sleep(1000);
-		if((resp = jwsrequest(url, &nresp, nil, "")) == nil)
-			return -1;
-		if((poll = jsonparse(resp)) == nil){
-			free(resp);
-			return -1;
-		}
-		free(resp);
-		if((state = jsonbyname(poll, "status")) != nil && state->t == JSONString){
-			if(strcmp(state->s, "valid") == 0)
-				break;
-			else if(strcmp(state->s, "pending") != 0 && strcmp(state->s, "processing") != 0){
-				fprint(2, "error: %J", poll);
-				werrstr("invalid request: %s", state->s);
-				jsonfree(poll);
-				return -1;
-				
-			}
-		}
-		jsonfree(poll);
-	}
-	if(poll == nil){
-		werrstr("timed out");
-		return -1;
-	}
-	if((cert = jsonbyname(poll, "certificate")) == nil || cert->t != JSONString){
-		werrstr("missing cert url in response");
-		jsonfree(poll);
-		return -1;
-	}
-	if((resp = jwsrequest(cert->s, &nresp, nil, "")) == nil){
-		jsonfree(poll);
-		return -1;
-	}
-	jsonfree(poll);
-	snprint(path, sizeof(path), "%s/%s.crt", outdir, dom);
-	if((fd = create(path, OWRITE, 0600)) == -1){
-		free(resp);
-		return -1;
-	}
-	if(write(fd, resp, nresp) != nresp){
-		free(resp);
-		close(fd);
-		return -1;
-	}
-	close(fd);
-	return 0;
-}
-
-static void
-getcert(char *addr, char *dom)
-{
-	Hdr loc;
-	JSON *o;
-
-	USED(addr);
-	loc.name = "location";
-	if((o = submitorder(dom, &loc)) == nil)
-		sysfatal("order: %r");
-	if(dochallenges(o) == -1)
-		sysfatal("challenge: %r");
-	if(submitcsr(o, dom) == -1)
-		sysfatal("signing cert: %r");
-	if(fetchcert(loc.val, dom) == -1)
-		sysfatal("saving cert: %r");
-}
-
-static int
-Econv(Fmt *f)
-{
-	char *s;
-	Rune r;
-	int w;
-
-	w = 0;
-	s = va_arg(f->args, char*);
-	while(*s){
-		s += chartorune(&r, s);
-		if(r == '\\' || r == '\"')
-			w += fmtrune(f, '\\');
-		w += fmtrune(f, r);
-	}
-	return w;
-}
-
-static int
-loadkey(char *path)
-{
-	uchar h[SHA2_256dlen];
-	char key[8192];
-	JSON *j, *e, *kty, *n;
-	DigestState *ds;
-	int fd, nr;
-
-	if((fd = open(path, OREAD)) == -1)
-		return -1;
-	if((nr = readn(fd, key, sizeof(key))) == -1)
-		return -1;
-	key[nr] = 0;
-
-	if((j = jsonparse(key)) == nil)
-		return -1;
-	if((e = jsonbyname(j, "e")) == nil || e->t != JSONString)
-		return -1;
-	if((kty = jsonbyname(j, "kty")) == nil || kty->t != JSONString)
-		return -1;
-	if((n = jsonbyname(j, "n")) == nil || n->t != JSONString)
-		return -1;
-
-	ds = sha2_256((uchar*)"{\"e\":\"", 6, nil, nil);
-	ds = sha2_256((uchar*)e->s, strlen(e->s), nil, ds);
-	ds = sha2_256((uchar*)"\",\"kty\":\"", 9, nil, ds);
-	ds = sha2_256((uchar*)kty->s, strlen(kty->s), nil, ds);
-	ds = sha2_256((uchar*)"\",\"n\":\"", 7, nil, ds);
-	ds = sha2_256((uchar*)n->s, strlen(n->s), nil, ds);
-	sha2_256((uchar*)"\"}", 2, h, ds);
-	jwskey = j;
-	jwsthumb = encurl64(h, sizeof(h));
-	return 0;
-}
-
-static void
-usage(void)
-{
-	fprint(2, "usage: %s [-a acctkey] [-c csrkey] [-p chalpath] [-o crtdir] acct cert\n", argv0);
-	exits("usage");
-}
-
-void
-main(int argc, char **argv)
-{
-	char *acctkey, *t;
-
-	JSONfmtinstall();
-	fmtinstall('E', Econv);
-
-	acctkey = nil;
-	outdir = nil;
-	challengefn = httpchallenge;
-	ARGBEGIN{
-	case 'd':
-		debug++;
-		break;
-	case 'o':
-		outdir = EARGF(usage());
-		break;
-	case 'p':
-		provider = EARGF(usage());
-		break;
-	case 'a':
-		acctkey = EARGF(usage());
-		break;
-	case 'c':
-		csrkey = EARGF(usage());
-		break;
-	case 'w':
-		challengedir = EARGF(usage());
-		break;
-	case 't':
-		t = EARGF(usage());
-		if(strcmp(t, "http") == 0)
-			challengefn = httpchallenge;
-		else if(strcmp(t, "dns") != 0)
-			challengefn = dnschallenge;
-		else
-			sysfatal("unknown challenge type '%s' (need http or dns)", t);
-		break;
-	default:
-		usage();
-		break;
-	}ARGEND;
-
-	if(argc != 2)
-		usage();
-
-	if(acctkey == nil)
-		acctkey = esmprint("/sys/lib/tls/acme/%s.pub", argv[0]);
-	if(outdir == nil)
-		outdir = "/sys/lib/tls/acme";
-	if(csrkey == nil)
-		csrkey = esmprint("/sys/lib/tls/acme/%s.key", argv[1]);
-
-	if((keyspec = smprint(Keyspec, argv[0])) == nil)
-		sysfatal("smprint: %r");
-	if(loadkey(acctkey) == -1)
-		sysfatal("load key: %r");
-
-	if(endpoints() == -1)
-		sysfatal("endpoints: %r");
-	mkaccount(argv[0]);
-	getcert(argv[0], argv[1]);
-	exits(nil);
-}
--- a/aclient.man
+++ /dev/null
@@ -1,118 +1,0 @@
-.TH ACLIENT 1
-.SH NAME
-aclient \- acme certificate client
-.SH SYNOPSIS
-.B aclient
-[
-.B -o
-.I outdir
-]
-[
-.B -p
-.I provider
-]
-[
-.B -a
-.I acctkey
-]
-[
-.B c
-.I csrkey
-]
-[
-.B w
-.I chaldir
-]
-.I acctname
-.I domain
-.SH DESCRIPTION
-Aclient fetches and renews TLS certificates
-using the
-.I acme
-protocol.
-It requires a pregenerated account key
-and certificate signing key.
-.PP
-There are a number of options.
-.TP
-.B -o
-.I outdir
-Specifies that the signed certificate is placed in
-.I outdir
-in place of the default
-.IR /sys/lib/tls/acme/ .
-.TP
-.B -p
-.I provider
-Specifies that
-.I provider
-is used as the provider URL, in place of the default
-.IR https://acme-v02.api.letsencrypt.org/directory .
-This must be the directory URL for the desired
-.I RFC8555
-compliant provider
-.TP
-.B -a
-.I acctkey
-Specifies that
-.I acctkey
-is used to sign requests to the
-.I provider
-in place of the default
-.IR /sys/lib/tls/acme/$acctname.pub .
-The key must be a
-.I jwk
-formatted RSA key.
-.TP
-.B c
-.I csrkey
-Specifies that
-.I csrkey
-is used to produce the CSR sent to
-.I provider
-in place of the default
-.IR /sys/lib/tls/acme/$domain.key .
-The key must be a plan 9 formatted
-RSA key suitable for
-.IR aux/rsa2csr .
-.TP
-.B w
-.I chaldir
-Specifies that the challenge is written out to
-.IR chaldir .
-For HTTP challenges, this defaults to
-.IR /usr/web/.well-known/acme-challenge/ .
-.SH EXAMPLES
-Before
-.B aclient
-is run, the keys must be generated.
-.IP
-.EX
-auth/rsagen -t 'service=acme role=sign hash=sha256 [email protected]' |
-	tee /sys/lib/acme/[email protected] |
-	auth/rsa2jwk > /sys/lib/acme/[email protected]
-auth/rsagen -t 'service=tls owner=*'
-	>/sys/lib/acme/mydomain.com.key
-.EE
-.PP
-This need only be run once.
-Once the keys are generated, they should be loaded into factotum:
-.IP
-.EX
-cat /sys/lib/acme/mydomain.com.key >/mnt/factotum/ctl
-cat /sys/lib/acme/[email protected] >/mnt/factotum/ctl
-.EE
-.PP
-The certificate for the domain can now be fetched:
-.IP
-.EX
-aclient [email protected] mydomain.com
-.EE
-.SH SOURCE
-.B /sys/src/cmd/$somewhere/aclient.c
-.SH BUGS
-.I Aclient only supports HTTP challenges that are
-placed in static directories.
-It should add support for DNS challenges, and provide
-a way for a script to be invoked while handling them.
-
--- /dev/null
+++ b/acmed.c
@@ -1,0 +1,772 @@
+#include <u.h>
+#include <libc.h>
+#include <json.h>
+#include <mp.h>
+#include <libsec.h>
+#include <auth.h>
+#include <authsrv.h>
+
+typedef struct Hdr Hdr;
+
+#pragma varargck	type	"E"	char*
+
+struct Hdr {
+	char	*name;
+	char	*val;
+	int	nval;
+};
+
+#define Keyspec		"proto=rsa service=acme role=sign hash=sha256 acct=%s"
+#define Useragent	"useragent aclient-plan9"
+#define Contenttype	"contenttype application/jose+json"
+#define between(x,min,max)	(((min-1-x) & (x-max-1))>>8)
+int	debug;
+int	(*challengefn)(JSON*, char*, int*);
+char	*keyspec;
+char	*provider = "https://acme-v02.api.letsencrypt.org/directory"; /* test endpoint */
+char	*challengedir = "/usr/web/.well-known/acme-challenge";
+char	*challengecmd;
+char	*keyid;
+char	*epnewnonce;
+char	*epnewacct;
+char	*epneworder;
+char	*eprevokecert;
+char	*epkeychange;
+char	*jwsthumb;
+JSON	*jwskey;
+
+#define dprint(...) if(debug)fprint(2, __VA_ARGS__);
+
+char*
+evsmprint(char *fmt, va_list ap)
+{
+	char *r;
+
+	if((r = vsmprint(fmt, ap)) == nil)
+		abort();
+	return r;
+}
+
+char*
+esmprint(char *fmt, ...)
+{
+	va_list ap;
+	char *r;
+
+	va_start(ap, fmt);
+	r = evsmprint(fmt, ap);
+	va_end(ap);
+	return r;
+}
+
+int
+encurl64chr(int o)
+{
+	int c;
+
+	c  = between(o,  0, 25) & ('A'+o);
+	c |= between(o, 26, 51) & ('a'+(o-26));
+	c |= between(o, 52, 61) & ('0'+(o-52));
+	c |= between(o, 62, 62) & ('-');
+	c |= between(o, 63, 63) & ('_');
+	return c;
+}
+char*
+encurl64(void *in, int n)
+{
+	int lim;
+	char *out, *p;
+
+	lim = 4*n/3 + 5;
+	if((out = malloc(lim)) == nil)
+		abort();
+	enc64x(out, lim, in, n, encurl64chr);
+	if((p = strchr(out, '=')) != nil)
+		*p = 0;
+	return out;
+}
+
+char*
+signRS256(char *hdr, char *prot)
+{
+	uchar hash[SHA2_256dlen];
+	DigestState *s;
+	AuthRpc *rpc;
+	int afd;
+	char *r;
+
+	if((afd = open("/mnt/factotum/rpc", ORDWR)) < 0)
+		return nil;
+	if((rpc = auth_allocrpc(afd)) == nil){
+		close(afd);
+		return nil;
+	}
+	if(auth_rpc(rpc, "start", keyspec, strlen(keyspec)) != ARok){
+		auth_freerpc(rpc);
+		close(afd);
+		return nil;
+	}
+
+	s = sha2_256((uchar*)hdr, strlen(hdr), nil, nil);
+	s = sha2_256((uchar*)".", strlen("."), nil, s);
+	sha2_256((uchar*)prot, strlen(prot), hash, s);
+
+	if(auth_rpc(rpc, "write", hash, sizeof(hash)) != ARok)
+		sysfatal("sign: write hash: %r");
+	if(auth_rpc(rpc, "read", nil, 0) != ARok)
+		sysfatal("sign: read sig: %r");
+	r = encurl64(rpc->arg, rpc->narg);
+	auth_freerpc(rpc);
+	close(afd);
+	return r;	
+}
+
+/*
+ * Reads all available data from an fd.
+ * guarantees returned value is terminated.
+ */
+static void*
+slurp(int fd, int *n)
+{
+	char *b;
+	int r, sz;
+
+	*n = 0;
+	sz = 32;
+	if((b = malloc(sz)) == nil)
+		abort();
+	while(1){
+		if(*n + 1 == sz){
+			sz *= 2;
+			if((b = realloc(b, sz)) == nil)
+				abort();
+		}
+		r = read(fd, b + *n, sz - *n - 1);
+		if(r == 0)
+			break;
+		if(r == -1){
+			free(b);
+			return nil;
+		}
+		*n += r;
+	}
+	b[*n] = 0;
+	return b;
+}
+		
+static int
+webopen(char *url, char *dir, int ndir)
+{
+	char buf[16];
+	int n, cfd, conn;
+
+	if((cfd = open("/mnt/web/clone", ORDWR)) == -1)
+		return -1;
+	if((n = read(cfd, buf, sizeof(buf)-1)) == -1)
+		return -1;
+	buf[n] = 0;
+	conn = atoi(buf);
+
+	if(fprint(cfd, "url %s", url) == -1)
+		goto Error;
+	snprint(dir, ndir, "/mnt/web/%d", conn);
+	return cfd;
+Error:
+	close(cfd);
+	return -1;
+}
+
+static char*
+get(char *url, int *n)
+{
+	char *r, dir[64], path[80];
+	int cfd, dfd;
+
+	r = nil;
+	dfd = -1;
+	if((cfd = webopen(url, dir, sizeof(dir))) == -1)
+		goto Error;
+	snprint(path, sizeof(path), "%s/%s", dir, "body");
+	if((dfd = open(path, OREAD)) == -1)
+		goto Error;
+	r = slurp(dfd, n);
+Error:
+	if(dfd != -1) close(dfd);
+	if(cfd != -1) close(cfd);
+	return r;
+}
+
+static char*
+post(char *url, char *buf, int nbuf, int *nret, Hdr *h)
+{
+	char *r, dir[64], path[80];
+	int cfd, dfd, hfd, ok;
+
+	r = nil;
+	ok = 0;
+	dfd = -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)
+		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)
+		goto Error;
+	if(h != nil){
+		snprint(path, sizeof(path), "%s/%s", dir, h->name);
+		if((hfd = open(path, OREAD)) == -1)
+			goto Error;
+		if((h->val = slurp(hfd, &h->nval)) == nil)
+			goto Error;
+		close(hfd);
+	}
+	ok = 1;
+Error:
+	if(dfd != -1) close(dfd);
+	if(cfd != -1) close(cfd);
+	if(!ok && h != nil)
+		free(h->val);
+	return r;
+}
+
+static int
+endpoints(void)
+{
+	JSON *j;
+	JSONEl *e;
+	char *s;
+	int n;
+
+	if((s = get(provider, &n)) == nil)
+		sysfatal("get %s: %r", provider);
+	if((j = jsonparse(s)) == nil)
+		sysfatal("parse endpoints: %r");
+	if(j->t != JSONObject)
+		sysfatal("expected object");
+	for(e = j->first; e != nil; e = e->next){
+		if(e->val->t != JSONString)
+			continue;
+		if(strcmp(e->name, "keyChange") == 0)
+			epkeychange = strdup(e->val->s);
+		else if(strcmp(e->name, "newAccount") == 0)
+			epnewacct = strdup(e->val->s);
+		else if(strcmp(e->name, "newNonce") == 0)
+			epnewnonce = strdup(e->val->s);
+		else if(strcmp(e->name, "newOrder") == 0)
+			epneworder = strdup(e->val->s);
+		else if(strcmp(e->name, "revokeCert") == 0)
+			eprevokecert = strdup(e->val->s);
+	}
+	jsonfree(j);
+	free(s);
+	if(epnewnonce==nil|| epnewacct==nil || epneworder==nil
+	|| eprevokecert==nil || epkeychange==nil){
+		sysfatal("missing directory entries");
+		return -1;
+	}
+	return 0;
+}
+
+static char*
+getnonce(void)
+{
+	char *r, dir[64], path[80];
+	int n, cfd, dfd, hfd;
+
+	r = nil;
+	dfd = -1;
+	hfd = -1;
+	if((cfd = webopen(epnewnonce, dir, sizeof(dir))) == -1)
+		goto Error;
+	fprint(cfd, "request HEAD");
+
+	snprint(path, sizeof(path), "%s/%s", dir, "body");
+	if((dfd = open(path, OREAD)) == -1)
+		goto Error;
+	snprint(path, sizeof(path), "%s/%s", dir, "replaynonce");
+	if((hfd = open(path, OREAD)) == -1)
+		goto Error;
+	r = slurp(hfd, &n);
+Error:
+	if(hfd != -1)
+		close(hfd);
+	if(dfd != -1)
+		close(dfd);
+	close(cfd);
+	return r;
+}
+
+char*
+jwsenc(char *hdr, char *msg, int *nbuf)
+{
+	char *h, *m, *s, *r;
+
+	h = encurl64(hdr, strlen(hdr));
+	m = encurl64(msg, strlen(msg));
+	s = signRS256(h, m);
+	if(s == nil)
+		return nil;
+
+	r = esmprint(
+		"{\n"
+		"\"protected\": \"%s\",\n"
+		"\"payload\": \"%s\",\n"
+		"\"signature\": \"%s\"\n"
+		"}\n",
+		h, m, s);
+	*nbuf = strlen(r);
+	free(h);
+	free(m);
+	free(s);
+
+	return r;
+}
+
+char*
+jwsheader(char *url)
+{
+	char *nonce;
+
+	if((nonce = getnonce()) == nil)
+		sysfatal("get nonce: %r");
+	return esmprint(
+		"{"
+		"\"alg\": \"RS256\","
+		"\"nonce\": \"%E\","
+		"\"kid\": \"%E\","
+		"\"url\": \"%E\""
+		"}",
+		nonce, keyid, url);
+}
+
+char*
+jwsrequest(char *url, int *nresp, Hdr *h, char *fmt, ...)
+{
+	char *hdr, *msg, *req, *resp;
+	int nreq;
+	va_list ap;
+
+	va_start(ap, fmt);
+	hdr = jwsheader(url);
+	msg = evsmprint(fmt, ap);
+	req = jwsenc(hdr, msg, &nreq);
+	dprint("req=\"%s\"\n", req);
+	resp = post(url, req, nreq, nresp, h);
+	free(hdr);
+	free(req);
+	free(msg);
+	va_end(ap);
+	dprint("resp=%s\n", resp);
+	return resp;
+}
+
+static void
+mkaccount(char *addr)
+{
+	char *nonce, *hdr, *msg, *req, *resp;
+	int nreq, nresp;
+	Hdr loc;
+
+	if((nonce = getnonce()) == nil)
+		sysfatal("get nonce: %r");
+	hdr = esmprint(
+		"{"
+		"\"alg\": \"RS256\","
+		"\"jwk\": %J,"
+		"\"nonce\": \"%E\","
+		"\"url\": \"%E\""
+		"}",
+		jwskey, nonce, epnewacct);
+	msg = esmprint(
+		"{"
+		"\"termsOfServiceAgreed\": true,"
+		"\"contact\": [\"mailto:%E\"]"
+		"}",
+		addr);
+	free(nonce);
+	if((req = jwsenc(hdr, msg, &nreq)) == nil)
+		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);
+	keyid = loc.val;
+}
+
+static JSON*
+submitorder(char **dom, int ndom, Hdr *hdr)
+{
+	char *req, *resp, *sep, rbuf[8192];
+	int nresp, i;
+	JSON *r;
+
+	sep = "";
+	req = seprint(rbuf, rbuf+sizeof(rbuf),
+		"{"
+		"  \"identifiers\": [");
+	for(i = 0; i < ndom; i++){
+		req = seprint(req, rbuf+sizeof(rbuf),
+			"%s{"
+			"  \"type\": \"dns\","
+			"  \"value\": \"%E\""
+			"}",
+			sep, dom[i]);
+		sep = ",";
+	}
+	req = seprint(req, rbuf+sizeof(rbuf),
+		"  ],"
+		"  \"wildcard\": false"
+		"}");
+	if(req - rbuf < 2)
+		sysfatal("truncated order");
+	resp = jwsrequest(epneworder, &nresp, hdr, "%s", rbuf);
+	if(resp == nil)
+		sysfatal("submit order: %r");
+	if((r = jsonparse(resp)) == nil)
+		sysfatal("parse order: %r");
+	free(resp);
+	return r;
+}
+
+static int
+httpchallenge(JSON *j, char *authurl, int *matched)
+{
+	JSON *ty, *url, *tok, *poll, *state;
+	char *resp, path[1024];
+	int i, fd, nresp;
+
+	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;
+	if(strcmp(ty->s, "http-01") != 0)
+		return -1;
+	*matched = 1;
+
+	snprint(path, sizeof(path), "%s/%s", challengedir, tok->s);
+	if((fd = create(path, OWRITE, 0666)) == -1)
+		sysfatal("create: %r"); //return -1;
+	if(fprint(fd, "%s.%s\n", tok->s, jwsthumb) == -1)
+		return -1;
+	close(fd);
+
+	if((resp = jwsrequest(url->s, &nresp, nil, "{}")) == nil)
+		sysfatal("challenge: post %s: %r", url->s);
+	free(resp);
+
+	for(i = 0; i < 60; i++){
+		sleep(1000);
+		if((resp = jwsrequest(authurl, &nresp, nil, "")) == nil)
+			sysfatal("challenge: post %s: %r", url->s);
+		if((poll = jsonparse(resp)) == nil){
+			free(resp);
+			return -1;
+		}
+		if((state = jsonbyname(poll, "status")) != nil && state->t == JSONString){
+			if(strcmp(state->s, "valid") == 0){
+				jsonfree(poll);
+				return 0;
+			}
+			else if(strcmp(state->s, "pending") != 0){
+				fprint(2, "error: %J", poll);
+				werrstr("status '%s'", state->s);
+				jsonfree(poll);
+				return -1;
+			}
+		}
+		jsonfree(poll);	
+	}
+	werrstr("timeout");
+	return -1;
+}
+
+static int
+dnschallenge(JSON*, char*, int*)
+{
+	return -1;
+}
+
+static int
+cmdchallenge(JSON*, char*, int*)
+{
+	return -1;
+}
+
+static int
+dochallenges(JSON *order)
+{
+	JSON *chals, *j, *cl;
+	JSONEl *ae, *ce;
+	int nresp, matched;
+	char *resp;
+
+	if((j = jsonbyname(order, "authorizations")) == nil)
+		sysfatal("parse response: missing authorizations");
+	if(j->t != JSONArray)
+		sysfatal("parse response: authorizations must be array");
+	for(ae = j->first; ae != nil; ae = ae->next){
+		if(ae->val->t != JSONString)
+			sysfatal("challenge: auth must be url");
+		if((resp = jwsrequest(ae->val->s, &nresp, nil, "")) == nil)
+			sysfatal("challenge: request %s: %r", ae->val->s);
+		if((chals = jsonparse(resp)) == nil)
+			goto Error;
+		if((cl = jsonbyname(chals, "challenges")) == nil){
+			jsonfree(chals);
+			goto Error;
+		}
+		matched = 0;
+		for(ce = cl->first; ce != nil; ce = ce->next){
+			if(challengefn(ce->val, ae->val->s, &matched) == 0)
+				break;
+			if(matched)
+				sysfatal("could not complete challenge: %r");
+		}
+		if(!matched)
+			sysfatal("no matching auth type");
+		jsonfree(chals);
+		free(resp);
+	}
+	return 0;
+Error:
+	jsonfree(j);
+	return -1;
+}
+
+static int
+submitcsr(JSON *order, char *b64csr)
+{
+	char *resp;
+	int nresp;
+	JSON *j;
+
+	if((j = jsonbyname(order, "finalize")) == nil)
+		sysfatal("parse response: missing authorizations");
+	if(j->t != JSONString)
+		werrstr("parse response: finalizer must be string");
+	if((resp = jwsrequest(j->s, &nresp, nil, "{\"csr\":\"%E\"}", b64csr)) == nil)
+		sysfatal("submit csr: %r");
+	free(resp);
+	return 0;
+}
+
+static int
+fetchcert(char *url)
+{
+	JSON *cert, *poll, *state;
+	int i, r, nresp;
+	char *resp;
+
+	poll = nil;
+	for(i = 0; i < 60; i++){
+		sleep(1000);
+		if((resp = jwsrequest(url, &nresp, nil, "")) == nil)
+			return -1;
+		if((poll = jsonparse(resp)) == nil){
+			free(resp);
+			return -1;
+		}
+		free(resp);
+		if((state = jsonbyname(poll, "status")) != nil && state->t == JSONString){
+			if(strcmp(state->s, "valid") == 0)
+				break;
+			else if(strcmp(state->s, "pending") != 0 && strcmp(state->s, "processing") != 0){
+				fprint(2, "error: %J", poll);
+				werrstr("invalid request: %s", state->s);
+				jsonfree(poll);
+				return -1;
+				
+			}
+		}
+		jsonfree(poll);
+	}
+	if(poll == nil){
+		werrstr("timed out");
+		return -1;
+	}
+	if((cert = jsonbyname(poll, "certificate")) == nil || cert->t != JSONString){
+		werrstr("missing cert url in response");
+		jsonfree(poll);
+		return -1;
+	}
+	if((resp = jwsrequest(cert->s, &nresp, nil, "")) == nil){
+		jsonfree(poll);
+		return -1;
+	}
+	jsonfree(poll);
+	r = write(1, resp, nresp);
+	free(resp);
+	if(r != nresp)
+		return -1;
+	return 0;
+}
+
+static void
+getcert(char *csrpath)
+{
+	char *csr, *dom[64], name[2048];
+	uchar *der;
+	int nder, ndom, fd;
+	RSApub *rsa;
+	Hdr loc;
+	JSON *o;
+
+	if((fd = open(csrpath, OREAD)) == -1)
+		sysfatal("open %s: %r", csrpath);
+	if((der = slurp(fd, &nder)) == nil)
+		sysfatal("read %s: %r", csrpath);
+	if((rsa = X509reqtoRSApub(der, nder, name, sizeof(name))) == nil)
+		sysfatal("decode csr: %r");
+	if((csr = encurl64(der, nder)) == nil)
+		sysfatal("encode %s: %r", csrpath);
+	if((ndom = getfields(name, dom, nelem(dom), 1, ",")) == nelem(dom))
+		sysfatal("too man domains");
+	rsapubfree(rsa);
+	close(fd);
+	free(der);
+
+	loc.name = "location";
+	if((o = submitorder(dom, ndom, &loc)) == nil)
+		sysfatal("order: %r");
+	if(dochallenges(o) == -1)
+		sysfatal("challenge: %r");
+	if(submitcsr(o, csr) == -1)
+		sysfatal("signing cert: %r");
+	if(fetchcert(loc.val) == -1)
+		sysfatal("saving cert: %r");
+	free(csr);
+}
+
+static int
+Econv(Fmt *f)
+{
+	char *s;
+	Rune r;
+	int w;
+
+	w = 0;
+	s = va_arg(f->args, char*);
+	while(*s){
+		s += chartorune(&r, s);
+		if(r == '\\' || r == '\"')
+			w += fmtrune(f, '\\');
+		w += fmtrune(f, r);
+	}
+	return w;
+}
+
+static int
+loadkey(char *path)
+{
+	uchar h[SHA2_256dlen];
+	char key[8192];
+	JSON *j, *e, *kty, *n;
+	DigestState *ds;
+	int fd, nr;
+
+	if((fd = open(path, OREAD)) == -1)
+		return -1;
+	if((nr = readn(fd, key, sizeof(key))) == -1)
+		return -1;
+	key[nr] = 0;
+
+	if((j = jsonparse(key)) == nil)
+		return -1;
+	if((e = jsonbyname(j, "e")) == nil || e->t != JSONString)
+		return -1;
+	if((kty = jsonbyname(j, "kty")) == nil || kty->t != JSONString)
+		return -1;
+	if((n = jsonbyname(j, "n")) == nil || n->t != JSONString)
+		return -1;
+
+	ds = sha2_256((uchar*)"{\"e\":\"", 6, nil, nil);
+	ds = sha2_256((uchar*)e->s, strlen(e->s), nil, ds);
+	ds = sha2_256((uchar*)"\",\"kty\":\"", 9, nil, ds);
+	ds = sha2_256((uchar*)kty->s, strlen(kty->s), nil, ds);
+	ds = sha2_256((uchar*)"\",\"n\":\"", 7, nil, ds);
+	ds = sha2_256((uchar*)n->s, strlen(n->s), nil, ds);
+	sha2_256((uchar*)"\"}", 2, h, ds);
+	jwskey = j;
+	jwsthumb = encurl64(h, sizeof(h));
+	return 0;
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-a acctkey] [-c cmd] [-p provider] [-t chal] [-w chaldir]  acct csr\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *acctkey, *t;
+
+	JSONfmtinstall();
+	fmtinstall('E', Econv);
+
+	acctkey = nil;
+	challengefn = httpchallenge;
+	ARGBEGIN{
+	case 'd':
+		debug++;
+		break;
+	case 'p':
+		provider = EARGF(usage());
+		break;
+	case 'a':
+		acctkey = EARGF(usage());
+		break;
+	case 'c':
+		challengecmd = EARGF(usage());
+		break;
+	case 'w':
+		challengedir = EARGF(usage());
+		break;
+	case 't':
+		t = EARGF(usage());
+		if(strcmp(t, "http") == 0)
+			challengefn = httpchallenge;
+		else if(strcmp(t, "dns") != 0)
+			challengefn = dnschallenge;
+		else if(strcmp(t, "cmd"))
+			challengefn = cmdchallenge;
+		else
+			sysfatal("unknown challenge type '%s' (need http or dns)", t);
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	if(argc != 2)
+		usage();
+
+	if(acctkey == nil)
+		acctkey = esmprint("/sys/lib/tls/acmed/%s.pub", argv[0]);
+	if((keyspec = smprint(Keyspec, argv[0])) == nil)
+		sysfatal("smprint: %r");
+	if(loadkey(acctkey) == -1)
+		sysfatal("load key: %r");
+
+	if(endpoints() == -1)
+		sysfatal("endpoints: %r");
+	mkaccount(argv[0]);
+	getcert(argv[1]);
+	exits(nil);
+}
--- /dev/null
+++ b/acmed.man
@@ -1,0 +1,118 @@
+.TH ACLIENT 8
+.SH NAME
+acmed \- acme certificate client
+.SH SYNOPSIS
+.B acmed
+[
+.B -o
+.I outdir
+]
+[
+.B -p
+.I provider
+]
+[
+.B -a
+.I acctkey
+]
+[
+[
+.B e
+.I chalcmd
+]
+[
+.B w
+.I chaldir
+]
+.I acctname
+.I csr
+.SH DESCRIPTION
+Acmed fetches and renews TLS certificates
+using the
+.I acme
+protocol.
+It requires a pregenerated account key
+and certificate signing key.
+.PP
+There are a number of options.
+.TP
+.B -o
+.I outdir
+Specifies that the signed certificate is placed in
+.I outdir
+in place of the default
+.IR /sys/lib/tls/acme/ .
+.TP
+.B -p
+.I provider
+Specifies that
+.I provider
+is used as the provider URL, in place of the default
+.IR https://acme-v02.api.letsencrypt.org/directory .
+This must be the directory URL for the desired
+.I RFC8555
+compliant provider
+.TP
+.B -a
+.I acctkey
+Specifies that
+.I acctkey
+is used to sign requests to the
+.I provider
+in place of the default
+.IR /sys/lib/tls/acme/$acctname.pub .
+The key must be a
+.I jwk
+formatted RSA key.
+.TP
+.B c
+.I csrkey
+Specifies that
+.I csrkey
+is used to produce the CSR sent to
+.I provider
+in place of the default
+.IR /sys/lib/tls/acme/$domain.key .
+The key must be a plan 9 formatted
+RSA key suitable for
+.IR aux/rsa2csr .
+.TP
+.B w
+.I chaldir
+Specifies that the challenge is written out to
+.IR chaldir .
+For HTTP challenges, this defaults to
+.IR /usr/web/.well-known/acme-challenge/ .
+.SH EXAMPLES
+Before
+.B acmed
+is run, the keys must be generated.
+.IP
+.EX
+auth/rsagen -t 'service=acme role=sign hash=sha256 [email protected]' \\
+	| tee >{auth/rsa2jwk \\
+		>/sys/lib/tls/acmed/[email protected]}
+	| ipso -r -l factotum
+auth/rsagen -t 'service=tls owner=*' \\
+	| tee >{auth/rsa2csr 'CN=mydomain.com' \\
+		>/sys/lib/tls/acmed/mydomain.com.csr} \\
+	| ipso -r -l factotum
+.EE
+.PP
+This need only be run once.
+.EE
+.PP
+The certificate for the domain can now be fetched:
+.IP
+.EX
+acmed [email protected] mydomain.com \\
+	>/sys/lib/tls/acmed/mydomain.com.crt
+.EE
+.SH SOURCE
+.B /sys/src/cmd/$somewhere/acmed.c
+.SH BUGS
+.I Acmed only supports HTTP challenges that are
+placed in static directories.
+It should add support for DNS challenges, and provide
+a way for a script to be invoked while handling them.
+
--- a/mkfile
+++ b/mkfile
@@ -2,7 +2,7 @@
 
 BIN=/$objtype/bin
 MAN=/sys/man/8
-TARG=aclient
-OFILES=aclient.$O
+TARG=acmed
+OFILES=acmed.$O
 
 </sys/src/cmd/mkone