ref: b96a6c3ebfde70209c94154454b002d1a74bfb7f
parent: 8322a4dd98640ddf36741db9aaa4315b22045b18
author: Ori Bernstein <ori@eigenstate.org>
date: Sat Sep 11 15:36:05 EDT 2021
acmed: add dns challenge support ---
--- a/acmed.c
+++ b/acmed.c
@@ -21,11 +21,11 @@
#define Contenttype "contenttype application/jose+json"
#define between(x,min,max) (((min-1-x) & (x-max-1))>>8)
int debug;
-int (*challengefn)(JSON*, char*, int*);
+int (*challengefn)(char*, 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 *challengeout;
+char *challengedom;
char *keyid;
char *epnewnonce;
char *epnewacct;
@@ -439,11 +439,75 @@
}
static int
-httpchallenge(JSON *j, char *authurl, int *matched)
+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, path[1024];
- int i, fd, nresp;
+ char *resp;
+ int i, nresp;
if((ty = jsonbyname(j, "type")) == nil)
return -1;
@@ -453,16 +517,12 @@
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)
+ dprint("trying challenge %s\n", ty->s);
+ if(challengefn(ty->s, tok->s, matched) == -1){
+ dprint("challengefn failed: %r\n");
return -1;
- close(fd);
+ }
if((resp = jwsrequest(url->s, &nresp, nil, "{}")) == nil)
sysfatal("challenge: post %s: %r", url->s);
@@ -495,18 +555,6 @@
}
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;
@@ -514,27 +562,38 @@
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");
+ 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)
- 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(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);
- goto Error;
+ return -1;
}
matched = 0;
for(ce = cl->first; ce != nil; ce = ce->next){
- if(challengefn(ce->val, ae->val->s, &matched) == 0)
+ if(challenge(ce->val, ae->val->s, &matched) == 0)
break;
if(matched)
- sysfatal("could not complete challenge: %r");
+ werrstr("could not complete challenge: %r");
}
if(!matched)
sysfatal("no matching auth type");
@@ -542,9 +601,6 @@
free(resp);
}
return 0;
-Error:
- jsonfree(j);
- return -1;
}
static int
@@ -708,7 +764,7 @@
static void
usage(void)
{
- fprint(2, "usage: %s [-a acctkey] [-c cmd] [-p provider] [-t chal] [-w chaldir] acct csr\n", argv0);
+ fprint(2, "usage: %s [-a acctkey] [-o chalout] [-p provider] [-t type] acct csr [domain]\n", argv0);
exits("usage");
}
@@ -715,39 +771,29 @@
void
main(int argc, char **argv)
{
- char *acctkey, *t;
+ char *acctkey, *ct, *co;
JSONfmtinstall();
fmtinstall('E', Econv);
+ ct = "http";
+ co = nil;
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());
+ case 'o':
+ co = EARGF(usage());
break;
- case 'w':
- challengedir = EARGF(usage());
+ case 'p':
+ provider = 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);
+ ct = EARGF(usage());
break;
default:
usage();
@@ -754,7 +800,19 @@
break;
}ARGEND;
- if(argc != 2)
+ 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)
--- a/acmed.man
+++ b/acmed.man
@@ -4,55 +4,39 @@
.SH SYNOPSIS
.B acmed
[
-.B -o
-.I outdir
+.B -a
+.I acctkey
]
[
-.B -p
-.I provider
+.B -d
+.I domain
]
[
-.B -a
-.I acctkey
+.B -o
+.I chalout
]
[
-[
-.B e
-.I chalcmd
+.B -p
+.I provider
]
[
-.B w
-.I chaldir
+.B -t
+.I type
]
.I acctname
.I csr
+[
+.I domain
+]
.SH DESCRIPTION
Acmed fetches and renews TLS certificates
using the
-.I acme
+.I acme (RFC8555)
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
@@ -65,24 +49,56 @@
.I jwk
formatted RSA key.
.TP
-.B c
-.I csrkey
+.B -d
+specifies the domain name that will be used
+for
+.I DNS
+challenges.
+.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 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 .
+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 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/ .
+.B -t
+.I type
+Specifies that the challenge type. Supported challenge
+types are currently
+.I http
+and
+.IR dns .
.SH EXAMPLES
Before
.B acmed