From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on inbox.vuxu.org X-Spam-Level: X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=ham autolearn_force=no version=3.4.4 Received: (qmail 24024 invoked from network); 22 Sep 2021 23:47:12 -0000 Received: from 4ess.inri.net (216.126.196.42) by inbox.vuxu.org with ESMTPUTF8; 22 Sep 2021 23:47:12 -0000 Received: from mimir.eigenstate.org ([206.124.132.107]) by 4ess; Wed Sep 22 19:30:13 -0400 2021 Received: from abbatoir.myfiosgateway.com (pool-74-108-56-225.nycmny.fios.verizon.net [74.108.56.225]) by mimir.eigenstate.org (OpenSMTPD) with ESMTPSA id b506f852 (TLSv1.2:ECDHE-RSA-AES256-SHA:256:NO) for <9front@9front.org>; Wed, 22 Sep 2021 16:29:52 -0700 (PDT) Message-ID: <80092E59F7B79B6ABBB86C5DB6B6D4D1@eigenstate.org> To: 9front@9front.org Date: Wed, 22 Sep 2021 19:29:50 -0400 From: ori@eigenstate.org MIME-Version: 1.0 Content-Type: text/plain; charset="US-ASCII" Content-Transfer-Encoding: 7bit List-ID: <9front.9front.org> List-Help: X-Glyph: ➈ X-Bullshit: agile wrapper app hosting-oriented generator Subject: [9front] ip/acmed: proposal for merge Reply-To: 9front@9front.org Precedence: bulk Acmed appears to be working and useful. I think it should ship with the base system. Call for thoughts, review, and complaints. Here's the manpage: ACMED(8) ACMED(8) NAME ip/acmed - acme certificate client SYNOPSIS ip/acmed [ -a acctkey ] [ -o chalout ] [ -p provider ] [ -t type ] acctname csr [ domain ] DESCRIPTION Acmed fetches and renews TLS certificates using the acme (RFC8555) protocol. It requires a pregenerated account key and certificate signing key. There are a number of options. -a acctkey Specifies that acctkey is used to sign requests to the provider in place of the default /sys/lib/tls/acme/$acctname.pub. The key must be a jwk formatted RSA key. -o chalout specifies that the challenge material is placed in the location chalout. For HTTP challenges, chalout must be a directory that your choice of httpd will serve at http://domain.com/.well-known/acme-challenge. For DNS challenges, chalout is a file that should be included in your ndb database. If unspecified, http challenges will output to /usr/web/.well-known/acme-challenge, whle dns chal- lenges will output to /lib/ndb/dnschallenge. -p provider Specifies that provider is used as the pro- vider URL, in place of the default https://acme- v02.api.letsencrypt.org/directory. This must be the directory URL for the desired RFC8555 compliant pro- vider -t type Specifies that the challenge type. Supported chal- lenge types are currently http and dns. EXAMPLES Before acmed is run, the keys must be generated. auth/rsagen -t 'service=acme role=sign hash=sha256 acct=a@b.org'\ >acct.key auth/rsa2jwk acct.key >/sys/lib/tls/acmed/me@example.org.pub auth/rsagen -t 'service=tls owner=*' >cert.key auth/rsa2csr 'CN=mydomain.com' cert.key \ >/sys/lib/tls/acmed/mydomain.com.csr} \ This need only be run once. The certificate for the domain can now be fetched: acmed me@example.org /sys/lib/tls/acmed/mydomain.com.csr \ >/sys/lib/tls/acmed/mydomain.com.crt When using a DNS challenge, your DNS server must be config- ured, and must be configured to include the DNS challenge file: database= file=/net/ndb file=/lib/ndb/local file=/lib/ndb/common file=/lib/ndb/dnschallenge And acmed must be invoked with the domain: acmed me@example.org \ /sys/lib/tls/acmed/mydomain.com.csr \ mydomain.com \ >/sys/lib/tls/acmed/mydomain.com.crt SOURCE /sys/src/cmd/ip/acmed.c And the patch: diff 8f4842d3465e96d264f5c2f7fa2d61db871aae9f uncommitted --- /tmp/diff100000578912 +++ b//sys/man/8/acmed @@ -1,0 +1,137 @@ +.TH ACMED 8 +.SH NAME +ip/acmed \- acme certificate client +.SH SYNOPSIS +.B ip/acmed +[ +.B -a +.I acctkey +] +[ +.B -o +.I chalout +] +[ +.B -p +.I provider +] +[ +.B -t +.I type +] +.I acctname +.I csr +[ +.I domain +] +.SH DESCRIPTION +Acmed fetches and renews TLS certificates +using the +.I acme (RFC8555) +protocol. +It requires a pregenerated account key +and certificate signing key. +.PP +There are a number of options. +.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 -o +.I chalout +specifies that the challenge material is +placed in the location +.IR chalout . +.IP +For HTTP challenges, +.I chalout +must be a directory that your choice of +.I httpd +will serve at +.IR http://domain.com/.well-known/acme-challenge . +For DNS challenges, +.I chalout +is a file that should be included in your +.I ndb +database. +.IP +If unspecified, +.I http +challenges will output to +.IR /usr/web/.well-known/acme-challenge , +whle +.I dns +challenges will output to +.IR /lib/ndb/dnschallenge . +.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 -t +.I type +Specifies that the challenge type. Supported challenge +types are currently +.I http +and +.IR dns . +.SH EXAMPLES +Before +.B acmed +is run, the keys must be generated. +.IP +.EX +auth/rsagen -t 'service=acme role=sign hash=sha256 acct=a@b.org'\\ + >acct.key +auth/rsa2jwk acct.key >/sys/lib/tls/acmed/me@example.org.pub +auth/rsagen -t 'service=tls owner=*' >cert.key +auth/rsa2csr 'CN=mydomain.com' cert.key \\ + >/sys/lib/tls/acmed/mydomain.com.csr} \\ +.EE +.PP +This need only be run once. +.EE +.PP +The certificate for the domain can now be fetched: +.IP +.EX +acmed me@example.org /sys/lib/tls/acmed/mydomain.com.csr \\ + >/sys/lib/tls/acmed/mydomain.com.crt +.EE +.PP +When using a DNS challenge, your DNS server must be +configured, and must be configured to include the +DNS challenge file: +.EX +database= + file=/net/ndb + file=/lib/ndb/local + file=/lib/ndb/common + file=/lib/ndb/dnschallenge +.EE +And +.I acmed +must be invoked with the domain: +.EX +acmed me@example.org \\ + /sys/lib/tls/acmed/mydomain.com.csr \\ + mydomain.com \\ + >/sys/lib/tls/acmed/mydomain.com.crt +.EE +.SH SOURCE +.B /sys/src/cmd/ip/acmed.c + --- /tmp/diff100000578915 +++ b//sys/src/cmd/ip/acmed.c @@ -1,0 +1,830 @@ +#include +#include +#include +#include +#include +#include +#include + +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)(char*, char*, int*); +char *keyspec; +char *provider = "https://acme-v02.api.letsencrypt.org/directory"; /* test endpoint */ +char *challengeout; +char *challengedom; +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(char *ty, char *tok, int *matched) +{ + char path[1024]; + int fd, r; + + 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) + return -1; + r = fprint(fd, "%s.%s\n", tok, jwsthumb); + close(fd); + return r; +} + +static int +dnschallenge(char *ty, char *tok, int *matched) +{ + char *enc, auth[1024], hash[SHA2_256dlen]; + int fd, r; + + 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){ + werrstr("could not create challenge: %r"); + goto Error; + } + if(fprint(fd,"dom=_acme-challenge.%s soa=\n\ttxtrr=%s\n", challengedom, enc) == -1){ + werrstr("could not write challenge: %r"); + goto Error; + } + if((fd = open("/net/dns", OWRITE)) == -1){ + werrstr("could not open dns ctl: %r"); + goto Error; + } + 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; +} + +static int +challenge(JSON *j, char *authurl, int *matched) +{ + JSON *ty, *url, *tok, *poll, *state; + char *resp; + int i, 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; + + dprint("trying challenge %s\n", ty->s); + if(challengefn(ty->s, tok->s, matched) == -1){ + dprint("challengefn failed: %r\n"); + return -1; + } + + 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 +dochallenges(JSON *order) +{ + JSON *chals, *j, *cl; + JSONEl *ae, *ce; + int nresp, matched; + char *resp; + + if((j = jsonbyname(order, "authorizations")) == nil){ + werrstr("parse response: missing authorizations"); + return -1; + } + if(j->t != JSONArray){ + werrstr("parse response: authorizations must be array"); + return -1; + } + for(ae = j->first; ae != nil; ae = ae->next){ + if(ae->val->t != JSONString){ + werrstr("challenge: auth must be url"); + return -1; + } + if((resp = jwsrequest(ae->val->s, &nresp, nil, "")) == nil){ + werrstr("challenge: request %s: %r", ae->val->s); + return -1; + } + if((chals = jsonparse(resp)) == nil){ + werrstr("invalid challenge: %r"); + return -1; + } + if((cl = jsonbyname(chals, "challenges")) == nil){ + werrstr("missing challenge"); + jsonfree(chals); + return -1; + } + matched = 0; + for(ce = cl->first; ce != nil; ce = ce->next){ + if(challenge(ce->val, ae->val->s, &matched) == 0) + break; + if(matched) + werrstr("could not complete challenge: %r"); + } + if(!matched) + sysfatal("no matching auth type"); + jsonfree(chals); + free(resp); + } + return 0; +} + +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] [-o chalout] [-p provider] [-t type] acct csr [domain]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + char *acctkey, *ct, *co; + + JSONfmtinstall(); + fmtinstall('E', Econv); + + ct = "http"; + co = nil; + acctkey = nil; + ARGBEGIN{ + case 'd': + debug++; + break; + case 'a': + acctkey = EARGF(usage()); + break; + case 'o': + co = EARGF(usage()); + break; + case 'p': + provider = EARGF(usage()); + break; + case 't': + ct = EARGF(usage()); + break; + default: + usage(); + break; + }ARGEND; + + if(strcmp(ct, "http") == 0){ + challengeout = (co != nil) ? co : "/usr/web/.well-known/acme-challenge"; + challengefn = httpchallenge; + }else if(strcmp(ct, "dns") == 0){ + challengeout = (co != nil) ? co : "/lib/ndb/dnschallenge"; + challengefn = dnschallenge; + }else{ + sysfatal("unknown challenge type '%s'", ct); + } + + if(argc == 3) + challengedom = argv[2]; + else 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); +} --- a//sys/src/cmd/ip/httpd/httpd.c +++ b//sys/src/cmd/ip/httpd/httpd.c @@ -51,6 +51,7 @@ address = nil; hmydomain = nil; netdir = "/net"; + tmfmtinstall(); fmtinstall('D', hdatefmt); fmtinstall('H', httpfmt); fmtinstall('U', hurlfmt); --- a//sys/src/cmd/ip/mkfile +++ b//sys/src/cmd/ip/mkfile @@ -1,6 +1,7 @@