/* $Id: netproc.c,v 1.37 2024/10/10 09:39:35 florian Exp $ */ /* * Copyright (c) 2016 Kristaps Dzonsons * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include "http.h" #include "extern.h" #include "parse.h" #define RETRY_DELAY 5 #define RETRY_MAX 10 /* * Buffer used when collecting the results of an http transfer. */ struct buf { char *buf; /* binary buffer */ size_t sz; /* length of buffer */ }; /* * Used for communication with other processes. */ struct conn { const char *newnonce; /* nonce authority */ char *kid; /* kid when account exists */ int fd; /* acctproc handle */ int dfd; /* dnsproc handle */ struct buf buf; /* http body buffer */ }; /* * If something goes wrong (or we're tracing output), we dump the * current transfer's data as a debug message. */ static void buf_dump(const struct buf *buf) { char *nbuf; if (buf->sz == 0) return; /* must be at least 4 * srclen + 1 long */ if ((nbuf = calloc(buf->sz + 1, 4)) == NULL) err(EXIT_FAILURE, "calloc"); strvisx(nbuf, buf->buf, buf->sz, VIS_SAFE); dodbg("transfer buffer: [%s] (%zu bytes)", nbuf, buf->sz); free(nbuf); } /* * Extract the domain and port from a URL. * The url must be formatted as schema://address[/stuff]. * This returns NULL on failure. */ static char * url2host(const char *host, short *port, char **path) { char *url, *ep; /* We only understand HTTP and HTTPS. */ if (strncmp(host, "https://", 8) == 0) { *port = 443; if ((url = strdup(host + 8)) == NULL) { warn("strdup"); return NULL; } } else if (strncmp(host, "http://", 7) == 0) { *port = 80; if ((url = strdup(host + 7)) == NULL) { warn("strdup"); return NULL; } } else { warnx("%s: unknown schema", host); return NULL; } /* Terminate path part. */ if ((ep = strchr(url, '/')) != NULL) { *path = strdup(ep); *ep = '\0'; } else *path = strdup(""); if (*path == NULL) { warn("strdup"); free(url); return NULL; } return url; } /* * Contact dnsproc and resolve a host. * Place the answers in "v" and return the number of answers, which can * be at most MAX_SERVERS_DNS. * Return <0 on failure. */ static ssize_t urlresolve(int fd, const char *host, struct source *v) { char *addr; size_t i, sz; long lval; if (writeop(fd, COMM_DNS, DNS_LOOKUP) <= 0) return -1; else if (writestr(fd, COMM_DNSQ, host) <= 0) return -1; else if ((lval = readop(fd, COMM_DNSLEN)) < 0) return -1; sz = lval; assert(sz <= MAX_SERVERS_DNS); for (i = 0; i < sz; i++) { memset(&v[i], 0, sizeof(struct source)); if ((lval = readop(fd, COMM_DNSF)) < 0) goto err; else if (lval != 4 && lval != 6) goto err; else if ((addr = readstr(fd, COMM_DNSA)) == NULL) goto err; v[i].family = lval; v[i].ip = addr; } return sz; err: for (i = 0; i < sz; i++) free(v[i].ip); return -1; } /* * Send a "regular" HTTP GET message to "addr" and stuff the response * into the connection buffer. * Return the HTTP error code or <0 on failure. */ static long nreq(struct conn *c, const char *addr) { struct httpget *g; struct source src[MAX_SERVERS_DNS]; struct httphead *st; char *host, *path; short port; size_t srcsz; ssize_t ssz; long code; int redirects = 0; if ((host = url2host(addr, &port, &path)) == NULL) return -1; again: if ((ssz = urlresolve(c->dfd, host, src)) < 0) { free(host); free(path); return -1; } srcsz = ssz; g = http_get(src, srcsz, host, port, path, 0, NULL, 0); free(host); free(path); if (g == NULL) return -1; switch (g->code) { case 301: case 302: case 303: case 307: case 308: redirects++; if (redirects > 3) { warnx("too many redirects"); http_get_free(g); return -1; } if ((st = http_head_get("Location", g->head, g->headsz)) == NULL) { warnx("redirect without location header"); http_get_free(g); return -1; } host = url2host(st->val, &port, &path); http_get_free(g); if (host == NULL) return -1; goto again; break; default: code = g->code; break; } /* Copy the body part into our buffer. */ free(c->buf.buf); c->buf.sz = g->bodypartsz; c->buf.buf = malloc(c->buf.sz); if (c->buf.buf == NULL) { warn("malloc"); code = -1; } else memcpy(c->buf.buf, g->bodypart, c->buf.sz); http_get_free(g); return code; } /* * Create and send a signed communication to the ACME server. * Stuff the response into the communication buffer. * Return <0 on failure on the HTTP error code otherwise. */ static long sreq(struct conn *c, const char *addr, int kid, const char *req, char **loc) { struct httpget *g; struct source src[MAX_SERVERS_DNS]; char *host, *path, *nonce, *reqsn; short port; struct httphead *h; ssize_t ssz; long code; if ((host = url2host(c->newnonce, &port, &path)) == NULL) return -1; if ((ssz = urlresolve(c->dfd, host, src)) < 0) { free(host); free(path); return -1; } g = http_get(src, (size_t)ssz, host, port, path, 1, NULL, 0); free(host); free(path); if (g == NULL) return -1; h = http_head_get("Replay-Nonce", g->head, g->headsz); if (h == NULL) { warnx("%s: no replay nonce", c->newnonce); http_get_free(g); return -1; } else if ((nonce = strdup(h->val)) == NULL) { warn("strdup"); http_get_free(g); return -1; } http_get_free(g); /* * Send the url, nonce and request payload to the acctproc. * This will create the proper JSON object we need. */ if (writeop(c->fd, COMM_ACCT, kid ? ACCT_KID_SIGN : ACCT_SIGN) <= 0) { free(nonce); return -1; } else if (writestr(c->fd, COMM_PAY, req) <= 0) { free(nonce); return -1; } else if (writestr(c->fd, COMM_NONCE, nonce) <= 0) { free(nonce); return -1; } else if (writestr(c->fd, COMM_URL, addr) <= 0) { free(nonce); return -1; } free(nonce); if (kid && writestr(c->fd, COMM_KID, c->kid) <= 0) return -1; /* Now read back the signed payload. */ if ((reqsn = readstr(c->fd, COMM_REQ)) == NULL) return -1; /* Now send the signed payload to the CA. */ if ((host = url2host(addr, &port, &path)) == NULL) { free(reqsn); return -1; } else if ((ssz = urlresolve(c->dfd, host, src)) < 0) { free(host); free(path); free(reqsn); return -1; } g = http_get(src, (size_t)ssz, host, port, path, 0, reqsn, strlen(reqsn)); free(host); free(path); free(reqsn); if (g == NULL) return -1; /* Stuff response into parse buffer. */ code = g->code; free(c->buf.buf); c->buf.sz = g->bodypartsz; c->buf.buf = malloc(c->buf.sz); if (c->buf.buf == NULL) { warn("malloc"); code = -1; } else memcpy(c->buf.buf, g->bodypart, c->buf.sz); if (loc != NULL) { free(*loc); *loc = NULL; h = http_head_get("Location", g->head, g->headsz); /* error checking done by caller */ if (h != NULL) *loc = strdup(h->val); } http_get_free(g); return code; } /* * Send to the CA that we want to authorise a new account. * This only happens once for a new account key. * Returns non-zero on success. */ static int donewacc(struct conn *c, const struct capaths *p, const char *contact) { struct jsmnn *j = NULL; int rc = 0; char *req, *detail, *error = NULL, *accturi = NULL; long lc; if ((req = json_fmt_newacc(contact)) == NULL) warnx("json_fmt_newacc"); else if ((lc = sreq(c, p->newaccount, 0, req, &c->kid)) < 0) warnx("%s: bad comm", p->newaccount); else if (lc == 400) { if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL) warnx("%s: bad JSON object", p->newaccount); else { detail = json_getstr(j, "detail"); if (detail != NULL && stravis(&error, detail, VIS_SAFE) != -1) { warnx("%s", error); free(error); } } } else if (lc != 200 && lc != 201) warnx("%s: bad HTTP: %ld", p->newaccount, lc); else if (c->buf.buf == NULL || c->buf.sz == 0) warnx("%s: empty response", p->newaccount); else rc = 1; if (c->kid != NULL) { if (stravis(&accturi, c->kid, VIS_SAFE) != -1) printf("account key: %s\n", accturi); free(accturi); } if (rc == 0 || verbose > 1) buf_dump(&c->buf); free(req); return rc; } /* * Check if our account already exists, if not create it. * Populates conn->kid. * Returns non-zero on success. */ static int dochkacc(struct conn *c, const struct capaths *p, const char *contact) { int rc = 0; char *req, *accturi = NULL; long lc; if ((req = json_fmt_chkacc()) == NULL) warnx("json_fmt_chkacc"); else if ((lc = sreq(c, p->newaccount, 0, req, &c->kid)) < 0) warnx("%s: bad comm", p->newaccount); else if (lc != 200 && lc != 400) warnx("%s: bad HTTP: %ld", p->newaccount, lc); else if (c->buf.buf == NULL || c->buf.sz == 0) warnx("%s: empty response", p->newaccount); else if (lc == 400) rc = donewacc(c, p, contact); else rc = 1; if (c->kid == NULL) rc = 0; else { if (stravis(&accturi, c->kid, VIS_SAFE) != -1) dodbg("account key: %s", accturi); free(accturi); } if (rc == 0 || verbose > 1) buf_dump(&c->buf); free(req); return rc; } /* * Submit a new order for a certificate. */ static int doneworder(struct conn *c, const char *const *alts, size_t altsz, struct order *order, const struct capaths *p) { struct jsmnn *j = NULL; int rc = 0; char *req; long lc; if ((req = json_fmt_neworder(alts, altsz)) == NULL) warnx("json_fmt_neworder"); else if ((lc = sreq(c, p->neworder, 1, req, &order->uri)) < 0) warnx("%s: bad comm", p->neworder); else if (lc != 201) warnx("%s: bad HTTP: %ld", p->neworder, lc); else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL) warnx("%s: bad JSON object", p->neworder); else if (!json_parse_order(j, order)) warnx("%s: bad order", p->neworder); else if (order->status == ORDER_INVALID) warnx("%s: order invalid", p->neworder); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); free(req); json_free(j); return rc; } /* * Update order status */ static int doupdorder(struct conn *c, struct order *order) { struct jsmnn *j = NULL; int rc = 0; long lc; if ((lc = sreq(c, order->uri, 1, "", NULL)) < 0) warnx("%s: bad comm", order->uri); else if (lc != 200) warnx("%s: bad HTTP: %ld", order->uri, lc); else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL) warnx("%s: bad JSON object", order->uri); else if (!json_parse_upd_order(j, order)) warnx("%s: bad order", order->uri); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); json_free(j); return rc; } /* * Request a challenge for the given domain name. * This must be called for each name "alt". * On non-zero exit, fills in "chng" with the challenge. */ static int dochngreq(struct conn *c, const char *auth, struct chng *chng) { int rc = 0; long lc; struct jsmnn *j = NULL; dodbg("%s: %s", __func__, auth); if ((lc = sreq(c, auth, 1, "", NULL)) < 0) warnx("%s: bad comm", auth); else if (lc != 200) warnx("%s: bad HTTP: %ld", auth, lc); else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL) warnx("%s: bad JSON object", auth); else if (!json_parse_challenge(j, chng)) warnx("%s: bad challenge", auth); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); json_free(j); return rc; } /* * Tell the CA that a challenge response is in place. */ static int dochngresp(struct conn *c, const struct chng *chng) { int rc = 0; long lc; dodbg("%s: challenge", chng->uri); if ((lc = sreq(c, chng->uri, 1, "{}", NULL)) < 0) warnx("%s: bad comm", chng->uri); else if (lc != 200 && lc != 201 && lc != 202) warnx("%s: bad HTTP: %ld", chng->uri, lc); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); return rc; } /* * Submit our csr to the CA. */ static int docert(struct conn *c, const char *uri, const char *csr) { char *req; int rc = 0; long lc; dodbg("%s: certificate", uri); if ((req = json_fmt_newcert(csr)) == NULL) warnx("json_fmt_newcert"); else if ((lc = sreq(c, uri, 1, req, NULL)) < 0) warnx("%s: bad comm", uri); else if (lc != 200) warnx("%s: bad HTTP: %ld", uri, lc); else if (c->buf.sz == 0 || c->buf.buf == NULL) warnx("%s: empty response", uri); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); free(req); return rc; } /* * Get certificate from CA */ static int dogetcert(struct conn *c, const char *uri) { int rc = 0; long lc; dodbg("%s: certificate", uri); if ((lc = sreq(c, uri, 1, "", NULL)) < 0) warnx("%s: bad comm", uri); else if (lc != 200) warnx("%s: bad HTTP: %ld", uri, lc); else if (c->buf.sz == 0 || c->buf.buf == NULL) warnx("%s: empty response", uri); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); return rc; } static int dorevoke(struct conn *c, const char *addr, const char *cert) { char *req; int rc = 0; long lc = 0; dodbg("%s: revocation", addr); if ((req = json_fmt_revokecert(cert)) == NULL) warnx("json_fmt_revokecert"); else if ((lc = sreq(c, addr, 1, req, NULL)) < 0) warnx("%s: bad comm", addr); else if (lc != 200 && lc != 201 && lc != 409) warnx("%s: bad HTTP: %ld", addr, lc); else rc = 1; if (lc == 409) warnx("%s: already revoked", addr); if (rc == 0 || verbose > 1) buf_dump(&c->buf); free(req); return rc; } /* * Look up directories from the certificate authority. */ static int dodirs(struct conn *c, const char *addr, struct capaths *paths) { struct jsmnn *j = NULL; long lc; int rc = 0; dodbg("%s: directories", addr); if ((lc = nreq(c, addr)) < 0) warnx("%s: bad comm", addr); else if (lc != 200 && lc != 201) warnx("%s: bad HTTP: %ld", addr, lc); else if ((j = json_parse(c->buf.buf, c->buf.sz)) == NULL) warnx("json_parse"); else if (!json_parse_capaths(j, paths)) warnx("%s: bad CA paths", addr); else rc = 1; if (rc == 0 || verbose > 1) buf_dump(&c->buf); json_free(j); return rc; } /* * Communicate with the ACME server. * We need the certificate we want to upload and our account key information. */ int netproc(int kfd, int afd, int Cfd, int cfd, int dfd, int rfd, int revocate, struct authority_c *authority, const char *const *alts, size_t altsz) { int rc = 0; size_t i; char *cert = NULL, *thumb = NULL, *error = NULL; struct conn c; struct capaths paths; struct order order; struct chng *chngs = NULL; long lval; memset(&paths, 0, sizeof(struct capaths)); memset(&c, 0, sizeof(struct conn)); if (unveil(tls_default_ca_cert_file(), "r") == -1) { warn("unveil %s", tls_default_ca_cert_file()); goto out; } if (pledge("stdio inet rpath", NULL) == -1) { warn("pledge"); goto out; } if (http_init() == -1) { warn("http_init"); goto out; } if (pledge("stdio inet", NULL) == -1) { warn("pledge"); goto out; } /* * Wait until the acctproc, keyproc, and revokeproc have started up and * are ready to serve us data. * Then check whether revokeproc indicates that the certificate on file * (if any) can be updated. */ if ((lval = readop(afd, COMM_ACCT_STAT)) == 0) { rc = 1; goto out; } else if (lval != ACCT_READY) { warnx("unknown operation from acctproc"); goto out; } if ((lval = readop(kfd, COMM_KEY_STAT)) == 0) { rc = 1; goto out; } else if (lval != KEY_READY) { warnx("unknown operation from keyproc"); goto out; } if ((lval = readop(rfd, COMM_REVOKE_RESP)) == 0) { rc = 1; goto out; } else if (lval != REVOKE_EXP && lval != REVOKE_OK) { warnx("unknown operation from revokeproc"); goto out; } /* If our certificate is up-to-date, return now. */ if (lval == REVOKE_OK) { rc = 1; goto out; } c.dfd = dfd; c.fd = afd; /* * Look up the API urls of the ACME server. */ if (!dodirs(&c, authority->api, &paths)) goto out; c.newnonce = paths.newnonce; /* Check if our account already exists or create it. */ if (!dochkacc(&c, &paths, authority->contact)) goto out; /* * If we're meant to revoke, then wait for revokeproc to send us * the certificate (if it's found at all). * Following that, submit the request to the CA then notify the * certproc, which will in turn notify the fileproc. * XXX currently we can only sign with the account key, the RFC * also mentions signing with the privat key of the cert itself. */ if (revocate) { if ((cert = readstr(rfd, COMM_CSR)) == NULL) goto out; if (!dorevoke(&c, paths.revokecert, cert)) goto out; else if (writeop(cfd, COMM_CSR_OP, CERT_REVOKE) > 0) rc = 1; goto out; } memset(&order, 0, sizeof(order)); if (!doneworder(&c, alts, altsz, &order, &paths)) goto out; chngs = calloc(order.authsz, sizeof(struct chng)); if (chngs == NULL) { warn("calloc"); goto out; } /* * Get thumbprint from acctproc. We will need it to construct * a response to the challenge */ if (writeop(afd, COMM_ACCT, ACCT_THUMBPRINT) <= 0) goto out; else if ((thumb = readstr(afd, COMM_THUMB)) == NULL) goto out; while(order.status != ORDER_VALID && order.status != ORDER_INVALID) { switch (order.status) { case ORDER_INVALID: warnx("order invalid"); goto out; case ORDER_VALID: rc = 1; continue; case ORDER_PENDING: if (order.authsz < 1) { warnx("order is in state pending but no " "authorizations know"); goto out; } for (i = 0; i < order.authsz; i++) { if (!dochngreq(&c, order.auths[i], &chngs[i])) goto out; dodbg("challenge, token: %s, uri: %s, status: " "%d", chngs[i].token, chngs[i].uri, chngs[i].status); if (chngs[i].status == CHNG_VALID || chngs[i].status == CHNG_INVALID) continue; if (chngs[i].retry++ >= RETRY_MAX) { warnx("%s: too many tries", chngs[i].uri); goto out; } if (writeop(Cfd, COMM_CHNG_OP, CHNG_SYN) <= 0) goto out; else if (writestr(Cfd, COMM_THUMB, thumb) <= 0) goto out; else if (writestr(Cfd, COMM_TOK, chngs[i].token) <= 0) goto out; /* Read that the challenge has been made. */ if (readop(Cfd, COMM_CHNG_ACK) != CHNG_ACK) goto out; } /* Write to the CA that it's ready. */ for (i = 0; i < order.authsz; i++) { if (chngs[i].status == CHNG_VALID || chngs[i].status == CHNG_INVALID) continue; if (!dochngresp(&c, &chngs[i])) goto out; } break; case ORDER_READY: /* * Write our acknowledgement that the challenges are * over. * The challenge process will remove all of the files. */ if (writeop(Cfd, COMM_CHNG_OP, CHNG_STOP) <= 0) goto out; /* Wait to receive the certificate itself. */ if ((cert = readstr(kfd, COMM_CERT)) == NULL) goto out; if (!docert(&c, order.finalize, cert)) goto out; break; default: warnx("unhandled status: %d", order.status); goto out; } if (!doupdorder(&c, &order)) goto out; dodbg("order.status %d", order.status); if (order.status == ORDER_PENDING) sleep(RETRY_DELAY); } if (order.status != ORDER_VALID) { for (i = 0; i < order.authsz; i++) { dochngreq(&c, order.auths[i], &chngs[i]); if (chngs[i].error != NULL) { if (stravis(&error, chngs[i].error, VIS_SAFE) != -1) { warnx("%s", error); free(error); error = NULL; } } } goto out; } if (order.certificate == NULL) { warnx("no certificate url received"); goto out; } if (!dogetcert(&c, order.certificate)) goto out; else if (writeop(cfd, COMM_CSR_OP, CERT_UPDATE) <= 0) goto out; else if (writebuf(cfd, COMM_CSR, c.buf.buf, c.buf.sz) <= 0) goto out; rc = 1; out: close(cfd); close(kfd); close(afd); close(Cfd); close(dfd); close(rfd); free(cert); free(thumb); free(c.kid); free(c.buf.buf); if (chngs != NULL) for (i = 0; i < order.authsz; i++) json_free_challenge(&chngs[i]); free(chngs); json_free_capaths(&paths); return rc; }