acmed: import acme (RFC8555) client
This commit is contained in:
parent
5a807265a8
commit
4c7745b202
2 changed files with 981 additions and 0 deletions
151
sys/man/8/acmed
Normal file
151
sys/man/8/acmed
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
.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 pre-generated account key
|
||||||
|
and certificate signing key.
|
||||||
|
.PP
|
||||||
|
Acmed accepts the following options:
|
||||||
|
.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 -o
|
||||||
|
.I chalout
|
||||||
|
Specifies that the challenge material is
|
||||||
|
placed in the location
|
||||||
|
.IR chalout .
|
||||||
|
Its behavior depends on the challenge type,
|
||||||
|
as specified with the
|
||||||
|
.B -t
|
||||||
|
flag.
|
||||||
|
.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:
|
||||||
|
.IP
|
||||||
|
.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:
|
||||||
|
.IP
|
||||||
|
.EX
|
||||||
|
acmed me@example.org \\
|
||||||
|
/sys/lib/tls/acmed/mydomain.com.csr \\
|
||||||
|
mydomain.com \\
|
||||||
|
>/sys/lib/tls/acmed/mydomain.com.crt
|
||||||
|
.EE
|
||||||
|
.SH SEE ALSO
|
||||||
|
.IR rsa (8),
|
||||||
|
.IR srvtls (4).
|
||||||
|
.SH SOURCE
|
||||||
|
.B /sys/src/cmd/ip/acmed.c
|
||||||
|
.SH HISTORY
|
||||||
|
.PP
|
||||||
|
Ip/acmed first appeared in 9front (Oct 2021)
|
830
sys/src/cmd/ip/acmed.c
Normal file
830
sys/src/cmd/ip/acmed.c
Normal file
|
@ -0,0 +1,830 @@
|
||||||
|
#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)(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);
|
||||||
|
}
|
Loading…
Reference in a new issue