--- /sys/src/cmd/auth/acmed.c Sun Jan 28 03:36:41 2024 +++ /sys/src/cmd/auth/acmed.c Sun Jun 28 00:00:00 2026 @@ -5,6 +5,7 @@ #include #include #include +#include typedef struct Hdr Hdr; @@ -17,8 +18,7 @@ struct Hdr { }; #define Keyspec "proto=rsa service=acme role=sign hash=sha256 acct=%s" -#define Contenttype "contenttype application/jose+json" -#define between(x,min,max) (((min-1-x) & (x-max-1))>>8) +#define Contenttype "application/jose+json" int debug; int (*challengefn)(char*, char*, char*, int*); char *keyspec; @@ -58,30 +58,26 @@ esmprint(char *fmt, ...) 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; + lim = 4*((n+2)/3) + 1; if((out = malloc(lim)) == nil) abort(); - enc64x(out, lim, in, n, encurl64chr); - if((p = strchr(out, '=')) != nil) - *p = 0; + enc64(out, lim, in, n); + for(p = out; *p != 0; p++){ + if(*p == '+') + *p = '-'; + else if(*p == '/') + *p = '_'; + else if(*p == '='){ + *p = 0; + break; + } + } return out; } @@ -152,92 +148,165 @@ slurp(int fd, int *n) b[*n] = 0; return b; } - -static int -webopen(char *url, char *dir, int ndir) + +/* set the error string from the acme problem document, else the raw body */ +static void +acmeerror(char *body) { - char buf[16]; - int n, cfd, conn; + JSON *j; + JSONEl *e; + char *m; - if((cfd = open("/mnt/web/clone", ORDWR|OCEXEC)) == -1) - return -1; - if((n = read(cfd, buf, sizeof(buf)-1)) == -1) - goto Error; - 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; + m = body; + if((j = jsonparse(body)) != nil && j->t == JSONObject){ + for(e = j->first; e != nil; e = e->next){ + if(e->val->t == JSONString && strcmp(e->name, "detail") == 0){ + m = e->val->s; + break; + } + } + } + werrstr("%s", m); + if(j != nil) + jsonfree(j); } static char* -get(char *url, int *n) +httpreq(char *method, char *url, char *ctype, char *body, int nbody, + int *nresp, char *wanthdr, char **hdrval) { - char *r, dir[64], path[80]; - int cfd, dfd; + char *p, *q, *line, *resp, *port, auth[256], host[256], path[1024], req[2048]; + int fd, tfd, tls, n, cap, status; + TLSconn conn; + Biobuf b; + + if(hdrval != nil) + *hdrval = nil; + *nresp = 0; + + tls = 0; + if(cistrncmp(url, "https://", 8) == 0){ + tls = 1; + p = url+8; + }else if(cistrncmp(url, "http://", 7) == 0) + p = url+7; + else{ + werrstr("bad url: %s", url); + return nil; + } + q = strchr(p, '/'); + n = q!=nil ? q-p : strlen(p); + if(n >= (int)sizeof(auth)) + n = sizeof(auth)-1; + memmove(auth, p, n); + auth[n] = 0; + snprint(path, sizeof(path), "%s", q!=nil ? q : "/"); + + snprint(host, sizeof(host), "%s", auth); + if((port = strchr(host, ':')) != nil) + *port++ = 0; + else + port = tls ? "443" : "80"; + if((fd = dial(netmkaddr(host, "tcp", port), nil, nil, nil)) < 0) + return nil; + if(tls){ + memset(&conn, 0, sizeof(conn)); + conn.serverName = host; + tfd = tlsClient(fd, &conn); + free(conn.cert); + free(conn.sessionID); + if(tfd < 0) + return nil; + fd = tfd; + } - 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|OCEXEC)) == -1) - goto Error; - r = slurp(dfd, n); -Error: - if(dfd != -1) close(dfd); - if(cfd != -1) close(cfd); - return r; + p = seprint(req, req+sizeof(req), "%s %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: acmed\r\n", + method, path, auth); + if(ctype != nil) + p = seprint(p, req+sizeof(req), "Content-Type: %s\r\n", ctype); + if(body != nil) + p = seprint(p, req+sizeof(req), "Content-Length: %d\r\n", nbody); + p = seprint(p, req+sizeof(req), "\r\n"); + if(write(fd, req, p-req) != p-req + || (body!=nil && write(fd, body, nbody) != nbody)){ + close(fd); + return nil; + } + + Binit(&b, fd, OREAD); + if((line = Brdline(&b, '\n')) == nil){ + werrstr("no http response"); + Bterm(&b); + close(fd); + return nil; + } + if((q = strchr(line, ' ')) == nil){ + werrstr("malformed http response"); + Bterm(&b); + close(fd); + return nil; + } + status = atoi(q+1); + while((line = Brdline(&b, '\n')) != nil){ + n = Blinelen(&b); + while(n > 0 && (line[n-1]=='\n' || line[n-1]=='\r')) + line[--n] = 0; + if(n == 0) + break; + if(wanthdr!=nil && *hdrval==nil && (q = strchr(line, ':')) != nil){ + *q++ = 0; + if(cistrcmp(line, wanthdr) == 0){ + while(*q==' ' || *q=='\t') + q++; + *hdrval = strdup(q); + } + } + } + resp = nil; + cap = 0; + for(;;){ + if(*nresp+1 >= cap){ + cap = cap!=0 ? cap*2 : 8192; + if((resp = realloc(resp, cap)) == nil) + abort(); + } + n = Bread(&b, resp+*nresp, cap-*nresp-1); + if(n <= 0) + break; + *nresp += n; + } + resp[*nresp] = 0; + Bterm(&b); + close(fd); + if(status >= 400){ + acmeerror(resp); + free(resp); + if(hdrval != nil){ + free(*hdrval); + *hdrval = nil; + } + return nil; + } + return resp; +} + +static char* +get(char *url, int *n) +{ + return httpreq("GET", url, nil, nil, 0, n, nil, nil); } static char* post(char *url, char *buf, int nbuf, int *nret, Hdr *h) { - char *r, dir[64], path[80]; - int cfd, dfd, hfd, ok; + char *r; - r = nil; - ok = 0; - dfd = -1; - hfd = -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|OCEXEC)) == -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|OCEXEC)) == -1) - goto Error; if(h != nil){ - snprint(path, sizeof(path), "%s/%s", dir, h->name); - if((hfd = open(path, OREAD|OCEXEC)) == -1) - goto Error; - if((h->val = slurp(hfd, &h->nval)) == nil) - goto Error; - } - if((r = slurp(dfd, nret)) == nil) - goto Error; - ok = 1; -Error: - if(hfd != -1) close(hfd); - if(dfd != -1) close(dfd); - if(cfd != -1) close(cfd); - if(!ok && h != nil){ - free(h->val); - h->val = nil; - h->nval = 0; + r = httpreq("POST", url, Contenttype, buf, nbuf, nret, h->name, &h->val); + h->nval = h->val!=nil ? strlen(h->val) : 0; + return r; } - return r; + return httpreq("POST", url, Contenttype, buf, nbuf, nret, nil, nil); } static int @@ -279,30 +348,14 @@ endpoints(void) static char* getnonce(void) { - char *r, dir[64], path[80]; - int n, cfd, dfd, hfd; + char *r, *nonce; + int n; - 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|OCEXEC)) == -1) - goto Error; - snprint(path, sizeof(path), "%s/%s", dir, "replaynonce"); - if((hfd = open(path, OREAD|OCEXEC)) == -1) - goto Error; - r = slurp(hfd, &n); -Error: - if(hfd != -1) - close(hfd); - if(dfd != -1) - close(dfd); - close(cfd); - return r; + nonce = nil; + if((r = httpreq("GET", epnewnonce, nil, nil, 0, &n, "replay-nonce", &nonce)) == nil) + return nil; + free(r); + return nonce; } char* @@ -406,9 +459,6 @@ mkaccount(char *addr) static char* idn(char *dom) { - static char buf[256]; - if(utf2idn(dom, buf, sizeof(buf)) >= 0) - return buf; return dom; } --- /sys/src/cmd/auth/rsa2jwk.c Sun Jun 4 02:08:39 2023 +++ /sys/src/cmd/auth/rsa2jwk.c Sun Jun 28 00:00:00 2026 @@ -5,33 +5,26 @@ #include #include "rsa2any.h" -#define between(x,min,max) (((min-1-x) & (x-max-1))>>8) - -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; + lim = 4*((n+2)/3) + 1; if((out = malloc(lim)) == nil) sysfatal("malloc: %r"); - enc64x(out, lim, in, n, encurl64chr); - if((p = strchr(out, '=')) != nil) - *p = 0; + enc64(out, lim, in, n); + for(p = out; *p != 0; p++){ + if(*p == '+') + *p = '-'; + else if(*p == '/') + *p = '_'; + else if(*p == '='){ + *p = 0; + break; + } + } return out; } @@ -61,7 +54,7 @@ main(int argc, char **argv) if(argc > 1) usage(); - if((k = getrsakey(argc, argv, 0, nil)) == nil) + if((k = getkey(argc, argv, 0, nil)) == nil) sysfatal("%r"); nlen = (mpsignif(k->pub.n)+7)/8; --- /sys/man/8/acmed Thu Jan 25 07:18:34 2024 +++ /sys/man/8/acmed Sun Jun 28 00:00:00 2026 @@ -47,6 +47,10 @@ outputs the new certificate in PEM format to standard output. .PP .I Acmed +talks to the provider over HTTP itself, +so no separate web file server is needed. +.PP +.I Acmed accepts the following options: .TP .B -a @@ -160,7 +164,7 @@ and certificate signing request file lik .IP .EX auth/rsagen -t 'service=tls role=client owner=*' > cert.key -auth/rsa2csr 'CN=mydomain.com' cert.key \\ +auth/rsa2csr 'CN=mydomain.com, mydomain.com' cert.key \\ > /sys/lib/tls/acmed/mydomain.com.csr .EE .PP @@ -171,11 +175,7 @@ and for more examples on how to use RSA keys. .IP .PP -The certificate for the domain can now be fetched. -This requires -.IR webfs (4) -to be mounted as the ACME protocol uses HTTP -to talk to the provider. +The certificate for the domain can now be fetched: .IP .EX auth/acmed me@example.com /sys/lib/tls/acmed/mydomain.com.csr \\ @@ -231,8 +231,7 @@ Account public keys. .IR ndb (8), .IR rsa (8), .IR secstore (1), -.IR tlssrv (8), -.IR webfs (4). +.IR tlssrv (8). .SH BUGS .PP When using DNS challenge, @@ -262,4 +261,4 @@ can be used to work around this. .SH HISTORY .PP .I Auth/acmed -first appeared in 9front (Oct 2021) +first appeared in 9front (Oct 2021). --- /n/sources/plan9/sys/src/cmd/auth/mkfile Sat Mar 19 10:00:08 2011 +++ /sys/src/cmd/auth/mkfile Thu Jun 18 00:00:00 2026 @@ -3,6 +3,7 @@ # programs # TARG=\ + acmed\ as\ asn12dsa\ asn12rsa\ @@ -27,6 +28,7 @@ TARG=\ printnetkey\ readnvram\ rsa2csr\ + rsa2jwk\ rsa2pub\ rsa2ssh\ rsa2x509\ @@ -117,10 +119,10 @@ nuke:V: $O.%: lib.$O.a $O.dsa2ssh $O.dsafill $O.dsa2x509 $O.dsa2pub $O.dsa2csr: rsa2any.$O -$O.rsa2ssh $O.rsafill $O.rsa2x509 $O.rsa2pub $O.rsa2csr: rsa2any.$O +$O.rsa2ssh $O.rsafill $O.rsa2x509 $O.rsa2pub $O.rsa2csr $O.rsa2jwk: rsa2any.$O $O.authsrv $O.guard.srv: secureidcheck.$O -rsa2ssh.$O rsafill.$O rsa2x509.$O rsa2pub.$O rsa2csr.$O: rsa2any.h +rsa2ssh.$O rsafill.$O rsa2x509.$O rsa2pub.$O rsa2csr.$O rsa2jwk.$O: rsa2any.h $BIN/netkey:V: $O.netkey cp $O.netkey /$objtype/bin/netkey --- /n/sources/plan9/sys/src/cmd/auth/rsagen.c Wed Mar 26 06:00:01 2003 +++ /sys/src/cmd/auth/rsagen.c Thu Jun 18 00:00:00 2026 @@ -18,7 +18,7 @@ main(int argc, char **argv) char *tag; RSApriv *key; - bits = 1024; + bits = 2048; tag = nil; key = nil; fmtinstall('B', mpfmt); --- /n/sources/plan9/sys/man/8/rsa Wed Apr 4 10:50:09 2012 +++ /sys/man/8/rsa Thu Jun 18 00:00:00 2026 @@ -1,6 +1,6 @@ .TH RSA 8 .SH NAME -rsagen, rsafill, asn12rsa, rsa2pub, rsa2ssh, rsa2x509 \- generate and format rsa keys +rsagen, rsafill, asn12rsa, rsa2pub, rsa2ssh, rsa2x509, rsa2csr, rsa2jwk \- generate and format rsa keys .SH SYNOPSIS .B auth/rsagen [ @@ -45,6 +45,17 @@ rsagen, rsafill, asn12rsa, rsa2pub, rsa2ssh, rsa2x509 \- generate and format rsa [ .I file ] +.PP +.B auth/rsa2jwk +[ +.I file +] +.PP +.B auth/rsa2csr +.I subject +[ +.I file +] .SH DESCRIPTION Plan 9 represents an RSA key as an attribute-value pair list prefixed with the string @@ -111,7 +122,7 @@ whose .B n has exactly .I nbits -(default 1024) +(default 2048) significant bits. If .I tag @@ -212,6 +223,25 @@ Applications that serve TLS-encrypted sessions (for example, and .IR tlssrv (8)) expect certificates in ASN.1/DER/PEM format. +.PP +.I Rsa2jwk +reads a Plan 9 RSA public or private key and prints the public portion +as a RFC7517 formatted JSON Web Key. +This is the account key format used by +.IR acmed (8). +.PP +.I Rsa2csr +takes the +.I subject +and a RSA private key and writes a PKCS #10 certificate signing request +in binary ASN.1/DER format to standard output. +The +.I subject +is a Distinguished Name as for +.I rsa2x509 +above; any names after the first comma become subjectAltName +DNS entries in the request, as required by ACME providers (see +.IR acmed (8)). .SH EXAMPLES Generate a fresh key and use it to start a TLS-enabled web server: .IP @@ -232,10 +262,20 @@ auth/rsa2ssh key | ssh unix 'cat >>.ssh/authorized_keys' cat key >/mnt/factotum/ctl ssh unix .EE +.PP +Generate a key and a certificate signing request for use with +.IR acmed (8): +.IP +.EX +auth/rsagen -t 'service=tls role=client owner=*' >key +cat key >/mnt/factotum/ctl +auth/rsa2csr 'CN=example.com, example.com' key >example.com.csr +.EE .SH SOURCE .B /sys/src/cmd/auth .SH "SEE ALSO .IR ssh (1), +.IR acmed (8), .IR factotum (4), .IR dsa (8), .IR pem (8)