ref: 4ab0e6e7237a631dc096603c18a154b6d7669947
dir: /aclient.c/
#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);
}