diff --git a/CLAUDE.md b/CLAUDE.md index ef33260..5fac90d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,9 +35,7 @@ configure time if the installed wolfSSL lacks any of `HAVE_PKCS7`, It also link-probes the `WOLFSSL_ASN_API` helpers it calls, which a shared libwolfssl exports only under one of `WOLFSSL_PUBLIC_ASN` (the lean choice), `OPENSSL_EXTRA`, `OPENSSL_EXTRA_X509_SMALL` or `WOLFSSL_TEST_CERT`; a static -one links them regardless. The test server's post-handshake-auth mode -additionally needs `KEEP_PEER_CERT` and `WOLFSSL_HAVE_TLS_UNIQUE`; without -them it returns `WOLFCERT_ERR_UNSUPPORTED`. The OpenSSL compatibility layer +one links them regardless. The OpenSSL compatibility layer itself is not required. With ML-DSA enabled it additionally needs `WOLFSSL_MLDSA_CHECK_KEY` (`wc_MlDsaKey_CheckKey()`), which reloading an ML-DSA CA from a store calls -- checked when `src/key_algs.c` compiles, @@ -45,6 +43,8 @@ since only `dilithium.h` resolves that macro. `--enable-mldsa` gives it by default; it is lost only if wolfSSL is built with `WOLFSSL_DILITHIUM_NO_CHECK_KEY` or `WOLFSSL_MLDSA_VERIFY_ONLY`. +The in-tree test server needs `KEEP_PEER_CERT` for post-handshake auth and `/simplereenroll`, and `WOLFSSL_HAVE_TLS_UNIQUE` for post-handshake auth; `README.md` has the details below its configure line. + **Key algorithms are gated** by `WOLFCERT_HAVE_` (RSA, ECC, ED25519, ED448, MLDSA). RSA, ECC, Ed25519, Ed448 and ML-DSA are each *optional* (absent => warning, that key type returns @@ -107,7 +107,7 @@ After a build with `-DWOLFCERT_ENABLE_CLI=ON` (the default): ```sh build/wolfcert-server --proto est --listen 127.0.0.1:8443 \ - --tls-cert server.crt --tls-key server.key + --tls-cert server.crt --tls-key server.key --est-allow-anonymous build/wolfcert-client enroll --proto est \ --url https://127.0.0.1:8443/.well-known/est --trust server.crt \ --key-type ecc:256 --subject "CN=dev" \ diff --git a/Makefile.am b/Makefile.am index 7db25c1..295d8de 100644 --- a/Makefile.am +++ b/Makefile.am @@ -130,6 +130,7 @@ test_csr_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_store_SOURCES = tests/unit/test_store.c test_store_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_transport_SOURCES = tests/unit/test_transport.c +test_transport_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src test_transport_LDADD = libwolfcert.la $(WOLFSSL_LIBS) test_net_SOURCES = tests/unit/test_net.c diff --git a/README.md b/README.md index 6377bd3..23ba912 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,7 @@ optional key types wolfCert picks up when available: -DWOLFSSL_PUBLIC_ASN -DWOLFSSL_HAVE_TLS_UNIQUE" ``` -`-DKEEP_PEER_CERT` and `-DWOLFSSL_HAVE_TLS_UNIQUE` are needed only by the test -server's post-handshake-auth mode (`wolfcert-server --tls-post-handshake-auth`). -Without them that mode returns `WOLFCERT_ERR_UNSUPPORTED`, and -`est_pha_roundtrip` / `est_async_roundtrip` skip. `--enable-opensslextra` -gives `KEEP_PEER_CERT` but not `WOLFSSL_HAVE_TLS_UNIQUE`. +`-DKEEP_PEER_CERT` and `-DWOLFSSL_HAVE_TLS_UNIQUE` are needed only by the test server. Its post-handshake-auth mode (`wolfcert-server --tls-post-handshake-auth`) needs both, and its `/simplereenroll` needs `KEEP_PEER_CERT` to compare the CSR with the certificate being renewed. Without them that mode returns `WOLFCERT_ERR_UNSUPPORTED`, an mTLS server answers `/simplereenroll` with 500, `est_pha_roundtrip` / `est_async_roundtrip` skip, and `est_mtls_roundtrip` skips its reenroll cases. `--enable-opensslextra` gives `KEEP_PEER_CERT` but not `WOLFSSL_HAVE_TLS_UNIQUE`. ### Header-based configuration (no build system) @@ -93,8 +89,11 @@ Start the bundled test server (issues from an auto-generated local CA): ```sh # EST is TLS-only (RFC 7030), so it needs a server identity for the # listen address; SCEP authenticates at the pkiMessage layer instead. +# EST also needs --basic USER:PASS, --tls-client-ca PEM, or +# --est-allow-anonymous to issue to any client; /simplereenroll +# also needs --tls-client-ca and the KEEP_PEER_CERT build above. ./wolfcert-server --proto est --listen 127.0.0.1:8443 \ - --tls-cert server.crt --tls-key server.key + --tls-cert server.crt --tls-key server.key --est-allow-anonymous ./wolfcert-server --proto scep --listen 127.0.0.1:8088 ``` diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index 96ba0b5..a6055e7 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -62,17 +62,21 @@ static void print_usage(FILE* out) " [--basic USER:PASS] [--challenge PASS]\n" " [--tls-cert PEM --tls-key PEM [--tls-client-ca PEM]]\n" " [--scep-require-approval] [--scep-enable-next-ca]\n" - " [--scep-enable-get-cert]\n" + " [--scep-enable-get-cert] [--est-allow-anonymous]\n" "\n" "Options:\n" " --proto est|scep Protocol to serve (required)\n" " --listen HOST:PORT Bind address (default 0.0.0.0:8080)\n" - " --basic USER:PASS Require HTTP Basic auth (EST enroll)\n" + " --basic USER:PASS Require HTTP Basic auth (EST enroll); both non-empty\n" " --challenge PASS Require this SCEP challengePassword in the CSR\n" " --tls-cert PEMFILE Terminate TLS with this server certificate (PEM);\n" " required for --proto est (RFC 7030)\n" " --tls-key PEMFILE Private key for --tls-cert (PEM)\n" " --tls-client-ca PEMFILE Require mutual TLS; verify clients against this CA\n" + " --est-allow-anonymous Issue EST certificates to any client; --proto est\n" + " needs this, --basic or --tls-client-ca.\n" + " /simplereenroll also needs --tls-client-ca and a\n" + " KEEP_PEER_CERT wolfSSL\n" " --scep-require-approval Defer SCEP PKCSReq/RenewalReq (pkiStatus=PENDING); issue\n" " on first GetCertInitial with the same transactionID\n" " --scep-enable-next-ca Advertise + answer GetNextCACert (RFC 8894 section 4.7),\n" @@ -123,7 +127,7 @@ static int parse_listen(const char* arg, char** host, uint16_t* port) static int parse_basic(const char* arg, char** user, char** pass) { const char* colon = strchr(arg, ':'); - if (colon == NULL) + if (colon == NULL || colon == arg || colon[1] == '\0') return -1; *user = strndup(arg, (size_t)(colon - arg)); @@ -177,6 +181,7 @@ int main(int argc, char** argv) { "tls-post-handshake-auth", no_argument, NULL, 'H' }, { "csrattrs-file", required_argument, NULL, 'F' }, { "est-require-csrattrs", no_argument, NULL, 'Q' }, + { "est-allow-anonymous", no_argument, NULL, 'Y' }, { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, 'V' }, { 0 } @@ -203,6 +208,7 @@ int main(int argc, char** argv) uint8_t* csr_attrs_blob = NULL; size_t csr_attrs_blob_len = 0; int est_require_csr_attrs = 0; + int est_allow_anonymous = 0; int c; while ((c = getopt_long(argc, argv, "", opts, NULL)) != -1) { @@ -218,7 +224,7 @@ int main(int argc, char** argv) break; case 'b': if (parse_basic(optarg, &user, &pass) != 0) { - fprintf(stderr, "invalid --basic (expected USER:PASS)\n"); + fprintf(stderr, "invalid --basic (expected non-empty USER:PASS)\n"); return 1; } break; @@ -274,6 +280,9 @@ int main(int argc, char** argv) case 'Q': est_require_csr_attrs = 1; break; + case 'Y': + est_allow_anonymous = 1; + break; case 'V': printf("wolfcert-server %s\n", wolfcert_version_string()); return 0; @@ -310,6 +319,13 @@ int main(int argc, char** argv) return 1; } + if (sel == WOLFCERT_PROTO_EST && user == NULL && tls_ca == NULL && + !est_allow_anonymous) { + fprintf(stderr, "wolfcert-server: --proto est requires --basic, " + "--tls-client-ca or --est-allow-anonymous\n"); + return 1; + } + if (host == NULL) host = strdup("0.0.0.0"); @@ -372,6 +388,7 @@ int main(int argc, char** argv) .csr_attributes_der = csr_attrs_blob, .csr_attributes_len = csr_attrs_blob_len, .est_require_csr_attributes = est_require_csr_attrs, + .est_allow_anonymous_enroll = est_allow_anonymous, }; int rc = wolfcert_server_start(&cfg, &g_server); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5e69c02..05e4bb4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -138,7 +138,7 @@ four endpoints a typical device needs: | `GET /cacerts` | `wolfcert_est_get_cacerts` | CA chain as degenerate PKCS#7; decoded to PEM for you. | | `GET /csrattrs` | `wolfcert_est_get_csr_attrs` | Raw body (empty on HTTP 204); decode with `wolfcert_est_parse_csr_attrs`. | | `POST /simpleenroll` | `wolfcert_est_simple_enroll` | Body is base64-wrapped CSR DER; 200 returns the issued cert as PKCS#7. | -| `POST /simplereenroll` | `wolfcert_est_simple_reenroll` | Same, with the cert being renewed used as the implicit client identity. | +| `POST /simplereenroll` | `wolfcert_est_simple_reenroll` | Same, with the cert being renewed used as the implicit client identity. The test server's requirements for it are at `WolfCertServerCfgSrv.tls_client_ca_pem` in `wolfcert/server.h`. | A typical flow is: `wolfcert_key_generate` → `wolfcert_csr_build` → `wolfcert_est_simple_enroll` → persist the returned PEM. The keep-alive @@ -172,7 +172,7 @@ during the handshake, since TLS 1.2 has no PHA. On the first `/simpleenroll` or `/simplereenroll` without a peer cert, the server calls `wolfSSL_request_certificate()` and waits (5 s, capped at the per-request deadline) for the client's post-handshake Finished. Any TLS error, app data -first, or an empty Certificate gets a 401; if the request deadline ends the +first, or an empty Certificate gets a 403; if the request deadline ends the wait first, the connection is dropped like any request that misses it. The accept loop keeps the connection open across requests, so the anonymous `/cacerts` and the authenticated enroll land on one connection. The mode diff --git a/examples/certs/README.md b/examples/certs/README.md index eab9636..9b6411b 100644 --- a/examples/certs/README.md +++ b/examples/certs/README.md @@ -20,7 +20,8 @@ Two algorithm families, each a self-signed CA plus a server and a client leaf: ## Use with the CLIs -Server-side TLS (optionally mutual TLS with `--tls-client-ca`): +Server-side TLS with mutual TLS via `--tls-client-ca` (swap it for +`--est-allow-anonymous` to issue to any client): ```sh build/wolfcert-server --proto est --listen 127.0.0.1:8443 \ diff --git a/examples/certs/gen-certs.sh b/examples/certs/gen-certs.sh index ad4f49d..be9c2d9 100755 --- a/examples/certs/gen-certs.sh +++ b/examples/certs/gen-certs.sh @@ -16,7 +16,7 @@ # # Mapping onto the CLIs: # wolfcert-server --tls-cert /server-cert.pem --tls-key /server-key.pem \ -# [--tls-client-ca /ca-cert.pem] +# --tls-client-ca /ca-cert.pem | --est-allow-anonymous # wolfcert-client ... --trust /ca-cert.pem \ # [--client-cert /client-cert.pem --client-key /client-key.pem] # diff --git a/src/ca_issue.c b/src/ca_issue.c index 8894d29..22c3027 100644 --- a/src/ca_issue.c +++ b/src/ca_issue.c @@ -638,52 +638,59 @@ static void free_subject_pubkey(word32 keyOID, void* impl, void* heap) } } -/* Carry the subjectAltName from the parsed CSR into the issued cert. wolfSSL - * only transcribes the subject DN, and it also splits parsed alt names by type - * (rfc822Name lands in altEmailNames, not altNames), so we recombine the - * carriable lists -- DNS / URI / IP / registeredID in altNames and rfc822Name - * in altEmailNames -- into one list and flatten it into the GeneralNames - * SEQUENCE that Cert.altNames expects. - * - * directoryName and otherName cannot be faithfully re-encoded from a parsed - * cert via wc_FlattenAltNames (the parser strips the directoryName SEQUENCE - * wrapper, and wolfSSL's cert generator has no path to restore it). Rather - * than silently issue a cert missing a SAN entry the requester asked for, we - * reject such a CSR. The altDirNames / altOtherNamesRaw lists (and the - * rfc822Name split) only exist when wolfSSL keeps name-constraint state. */ -static int flatten_csr_san(DecodedCert* dc, Cert* nc, void* heap) +/* 1 when every GeneralName in san is an rfc822Name, dNSName, URI, iPAddress or + * registeredID, the forms the test CA issues. */ +static int san_types_issuable(const byte* san, word32 san_len) { - DNS_entry* merged = NULL; - int rc = 0; - -#ifndef IGNORE_NAME_CONSTRAINTS - if (dc->altDirNames != NULL || dc->altOtherNamesRaw != NULL) - return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca", - "CSR carries a directoryName/otherName SAN that cannot be issued"); -#endif + word32 idx = 0; + word32 end; + int len = 0; + byte tag = 0; + + if (GetASNTag(san, &idx, &tag, san_len) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(san, &idx, &len, san_len) < 0) + return 0; + + end = idx + (word32)len; + while (idx < end) { + if (GetASNTag(san, &idx, &tag, end) < 0 || + GetLength(san, &idx, &len, end) < 0) + return 0; + if (tag != (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE) && + tag != (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE)) + return 0; + idx += (word32)len; + } - const DNS_entry* srcs[] = { - dc->altNames, -#ifndef IGNORE_NAME_CONSTRAINTS - dc->altEmailNames, -#endif - }; + return 1; +} - for (size_t i = 0; i < sizeof(srcs) / sizeof(srcs[0]) && rc == 0; ++i) { - for (const DNS_entry* e = srcs[i]; e != NULL && rc == 0; e = e->next) - rc = wc_SetDNSEntry(heap, e->name, e->len, e->type, &merged); - } - if (rc != 0) { - FreeAltNames(merged, heap); - return WOLFCERT_ERR_WC(rc, "ca", "SetDNSEntry(issue SAN)"); - } +/* Copy the CSR's subjectAltName into the issued cert byte for byte. */ +static int copy_csr_san(const DecodedCert* dc, Cert* nc) +{ + const byte* san = NULL; + word32 san_len = 0; - /* Encode straight into nc->altNames / altNamesSz (0 when no SAN). */ - rc = wc_SetAltNamesFromList(nc, merged); + if (wolfcert_find_san(dc, &san, &san_len) != WOLFCERT_OK) + return WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "ca", + "CSR extensions do not parse"); + if (san_len > sizeof(nc->altNames)) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca", + "CSR SAN is %u bytes, limit %d", (unsigned)san_len, + (int)sizeof(nc->altNames)); + if (san != NULL && !san_types_issuable(san, san_len)) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca", + "CSR carries an otherName, x400Address, directoryName or " + "ediPartyName SAN"); - FreeAltNames(merged, heap); - if (rc != 0) - return WOLFCERT_ERR_WC(rc, "ca", "SetAltNamesFromList(issue)"); + if (san != NULL) + memcpy(nc->altNames, san, san_len); + nc->altNamesSz = (int)san_len; + nc->altNamesCrit = dc->extSubjAltNameCrit; return WOLFCERT_OK; } @@ -791,9 +798,7 @@ int wolfcert_ca_issue(WolfCertCa* ca, nc->daysValid = 365; nc->isCA = 0; - /* Carry the requested subjectAltName from the CSR into the issued - * cert. */ - rc = flatten_csr_san(&dc, nc, heap); + rc = copy_csr_san(&dc, nc); } if (rc == 0) { diff --git a/src/client.c b/src/client.c index a358a87..f898d8d 100644 --- a/src/client.c +++ b/src/client.c @@ -232,6 +232,9 @@ int wolfcert_client_reenroll(WolfCertClient* client, const WolfCertServerCfg* sr if (srv == NULL || current_cert == NULL || current_key == NULL || meta == NULL || out_key == NULL || out_cert_pem == NULL) return WOLFCERT_ERR_BAD_ARG; + if (wolfcert_csr_meta_sets_identity(meta)) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "client", + "a renewal keeps the certificate's subject and SAN"); WolfCertKey* nk = NULL; int rc; diff --git a/src/csr.c b/src/csr.c index c16cd95..fb3816e 100644 --- a/src/csr.c +++ b/src/csr.c @@ -271,72 +271,6 @@ static int choose_sig_type(const WolfCertKey* key, const WolfCertKeyAlg* alg, return alg->ctc_sig_default; } -/* Find the subjectAltName extension in a certificate's [3] Extensions and - * return its GeneralNames bytes; *san stays NULL when there is none. */ -static int find_san(const byte* ext, int ext_sz, const byte** san, - word32* san_len) -{ - static const byte san_oid[] = { ASN_OBJECT_ID, 0x03, 0x55, 0x1D, 0x11 }; - word32 idx = 0; - word32 end = 0; - word32 ext_end = 0; - int len = 0; - int rc = WOLFCERT_OK; - byte tag = 0; - - *san = NULL; - *san_len = 0; - if (ext == NULL || ext_sz <= 0) - return WOLFCERT_OK; - - if (GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || - tag != (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3) || - GetLength(ext, &idx, &len, (word32)ext_sz) < 0 || - GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || - tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || - GetLength(ext, &idx, &len, (word32)ext_sz) < 0) - rc = WOLFCERT_ERR_PARSE; - else - end = idx + (word32)len; - - /* Extension ::= SEQUENCE { OID, critical BOOLEAN OPTIONAL, OCTET STRING } */ - while (rc == WOLFCERT_OK && *san == NULL && idx < end) { - if (GetASNTag(ext, &idx, &tag, end) < 0 || - tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || - GetLength(ext, &idx, &len, end) < 0) { - rc = WOLFCERT_ERR_PARSE; - } - else { - ext_end = idx + (word32)len; - if (ext_end - idx < sizeof(san_oid) || - memcmp(ext + idx, san_oid, sizeof(san_oid)) != 0) { - idx = ext_end; - } - else { - idx += (word32)sizeof(san_oid); - if (idx < ext_end && ext[idx] == ASN_BOOLEAN) { - if (GetASNTag(ext, &idx, &tag, ext_end) < 0 || - GetLength(ext, &idx, &len, ext_end) < 0) - rc = WOLFCERT_ERR_PARSE; - else - idx += (word32)len; - } - if (rc == WOLFCERT_OK && - (GetASNTag(ext, &idx, &tag, ext_end) < 0 || - tag != ASN_OCTET_STRING || - GetLength(ext, &idx, &len, ext_end) < 0)) - rc = WOLFCERT_ERR_PARSE; - if (rc == WOLFCERT_OK) { - *san = ext + idx; - *san_len = (word32)len; - } - } - } - } - - return rc; -} - /* Copy renew_cert's Subject and SAN into cert */ static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, size_t renew_cert_len, void* heap) @@ -394,8 +328,7 @@ static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, } if (rc == WOLFCERT_OK && - find_san(dc->extensions, dc->extensionsSz, &san, &san_len) != - WOLFCERT_OK) + wolfcert_find_san(dc, &san, &san_len) != WOLFCERT_OK) rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "csr", "certificate extensions do not parse"); if (rc == WOLFCERT_OK && san_len > sizeof(cert->altNames)) @@ -417,6 +350,13 @@ static int copy_cert_identity(Cert* cert, const uint8_t* renew_cert, return rc; } +int wolfcert_csr_meta_sets_identity(const WolfCertCertMeta* meta) +{ + return meta->subject_dn != NULL || meta->san_dns_len != 0 || + meta->san_ip_len != 0 || meta->san_uri_len != 0 || + meta->san_email_len != 0; +} + int wolfcert_csr_build(const WolfCertKey* key, const WolfCertCertMeta* meta, WolfCertBuffer* out_der) { @@ -430,10 +370,7 @@ int wolfcert_csr_build_ex(const WolfCertKey* key, const WolfCertCertMeta* meta, if (key == NULL || meta == NULL || out_der == NULL) return WOLFCERT_ERR_BAD_ARG; - if (renew_cert != NULL && - (meta->subject_dn != NULL || meta->san_dns_len != 0 || - meta->san_ip_len != 0 || meta->san_uri_len != 0 || - meta->san_email_len != 0)) + if (renew_cert != NULL && wolfcert_csr_meta_sets_identity(meta)) return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "csr", "a renewal keeps the certificate's subject and SAN"); diff --git a/src/est/est_server.c b/src/est/est_server.c index 2fcb6db..efe4a61 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -33,6 +33,7 @@ #include "../internal.h" #include +#include #include #include #include @@ -52,10 +53,10 @@ * * The EST RFC has no explicit transaction identifier for async * enrollment - the client is expected to re-POST the identical CSR. We - * key pending entries off the SHA-256 of the base64-encoded request - * body so the second POST produces the same digest as the first, even - * across reconnects. The queue is capped and intentionally shallow; - * real deployments use a proper approval workflow. */ + * key pending entries off the SHA-256 of the decoded, signature-checked + * CSR DER so the second POST produces the same digest as the first, even + * across reconnects and base64 re-wrapping. The queue is capped and + * intentionally shallow; real deployments use a proper approval workflow. */ #define EST_PENDING_CAP 8 /* How long a client gets to answer a post-handshake CertificateRequest. */ @@ -258,6 +259,15 @@ static int chunked_body_complete(const uint8_t* raw, size_t raw_len) return 0; } +/* 1 when the line's field name is `name`, followed directly by ':'. */ +static int hdr_is(const char* line, size_t llen, const char* name) +{ + size_t n = strlen(name); + + return llen > n && line[n] == ':' && + wolfcert_ascii_ncasecmp(line, name, n) == 0; +} + static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) { memset(out, 0, sizeof(*out)); @@ -306,53 +316,50 @@ static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) int chunked = 0; while (read_line(&p, end, &line, &llen) == 0 && llen > 0) { - if (llen > 14 && strncasecmp(line, "Content-Length", 14) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) - out->content_length = (size_t)strtoul(colon + 1, NULL, 10); - } - else if (llen > 17 && strncasecmp(line, "Transfer-Encoding", 17) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) { - const char* v = colon + 1; - while (v < line + llen && (*v == ' ' || *v == '\t')) { - ++v; - } + const char* hc = memchr(line, ':', llen); - size_t vlen = (size_t)(line + llen - v); - if (vlen >= 7 && strncasecmp(v, "chunked", 7) == 0) - chunked = 1; + /* RFC 9112 section 5.1: whitespace before the colon is a 400. */ + if (hc != NULL && hc > line && (hc[-1] == ' ' || hc[-1] == '\t')) + return WOLFCERT_ERR_PROTOCOL; + if (hdr_is(line, llen, "Content-Length")) { + out->content_length = (size_t)strtoul(hc + 1, NULL, 10); + } + else if (hdr_is(line, llen, "Transfer-Encoding")) { + const char* v = hc + 1; + while (v < line + llen && (*v == ' ' || *v == '\t')) { + ++v; } + + size_t vlen = (size_t)(line + llen - v); + if (vlen >= 7 && wolfcert_ascii_ncasecmp(v, "chunked", 7) == 0) + chunked = 1; } - else if (llen > 13 && strncasecmp(line, "Authorization", 13) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) { - char* val = colon + 1; - while (*val == ' ' || *val == '\t') { - ++val; - } + else if (hdr_is(line, llen, "Authorization")) { + if (out->auth_header != NULL) + return WOLFCERT_ERR_PROTOCOL; - size_t vlen = llen - (size_t)(val - line); - out->auth_header = (char*)WOLFCERT_XMALLOC(vlen + 1, heap); - if (out->auth_header) { - memcpy(out->auth_header, val, vlen); - out->auth_header[vlen] = '\0'; - } + const char* val = hc + 1; + while (val < line + llen && (*val == ' ' || *val == '\t')) { + ++val; } - } - else if (llen > 10 && strncasecmp(line, "Connection", 10) == 0) { - char* colon = memchr(line, ':', llen); - if (colon != NULL) { - const char* v = colon + 1; - while (v < line + llen && (*v == ' ' || *v == '\t')) { - ++v; - } - size_t vlen = (size_t)(line + llen - v); - if (vlen >= 5 && strncasecmp(v, "close", 5) == 0) - out->connection_close = 1; + size_t vlen = (size_t)(line + llen - val); + out->auth_header = (char*)WOLFCERT_XMALLOC(vlen + 1, heap); + if (out->auth_header) { + memcpy(out->auth_header, val, vlen); + out->auth_header[vlen] = '\0'; } } + else if (hdr_is(line, llen, "Connection")) { + const char* v = hc + 1; + while (v < line + llen && (*v == ' ' || *v == '\t')) { + ++v; + } + + size_t vlen = (size_t)(line + llen - v); + if (vlen >= 5 && wolfcert_ascii_ncasecmp(v, "close", 5) == 0) + out->connection_close = 1; + } } size_t body_have = (size_t)(end - p); @@ -509,44 +516,52 @@ static const char* conn_hdr(const WolfCertServer* s) return s->keep_alive ? "keep-alive" : "close"; } -static void send_status(WolfCertServer* s, int fd, int status, const char* phrase) +#define EST_TEXT_PLAIN "Content-Type: text/plain\r\n" +#define EST_PKCS7_CERTS \ + "Content-Type: application/pkcs7-mime; smime-type=certs-only\r\n" \ + "Content-Transfer-Encoding: base64\r\n" + +/* Send a response with body_len bytes of body; a NULL body sends only the + * headers, as for HEAD. `headers` holds CRLF-terminated header lines. */ +static void send_response(WolfCertServer* s, int fd, int status, + const char* phrase, const char* headers, + const void* body, size_t body_len) { - char line[128]; - int n = snprintf(line, sizeof(line), - "HTTP/1.1 %d %s\r\nContent-Length: 0\r\nConnection: %s\r\n\r\n", - status, phrase, conn_hdr(s)); + char hdr[320]; + int n; + + /* RFC 9110 section 8.6: a 204 carries no Content-Length. */ + if (status == 204) + n = snprintf(hdr, sizeof(hdr), "HTTP/1.1 204 %s\r\n%s" + "Connection: %s\r\n\r\n", phrase, headers, conn_hdr(s)); + else + n = snprintf(hdr, sizeof(hdr), "HTTP/1.1 %d %s\r\n%s" + "Content-Length: %zu\r\nConnection: %s\r\n\r\n", + status, phrase, headers, body_len, conn_hdr(s)); + if (n < 0 || (size_t)n >= sizeof(hdr)) + return; - send_all(s, fd, line, (size_t)n); + send_all(s, fd, hdr, (size_t)n); + if (body != NULL && body_len > 0) + send_all(s, fd, body, body_len); } -static void send_pkcs7_b64(WolfCertServer* s, int fd, const uint8_t* b64, size_t b64_len) +/* RFC 7030 section 4.2.3: an error body is a plaintext reason. */ +static void send_error(WolfCertServer* s, int fd, int status, + const char* phrase, const char* why) { - char hdr[256]; - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 200 OK\r\n" - "Content-Type: application/pkcs7-mime; smime-type=certs-only\r\n" - "Content-Transfer-Encoding: base64\r\n" - "Content-Length: %zu\r\n" - "Connection: %s\r\n\r\n", - b64_len, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)n); - send_all(s, fd, b64, b64_len); + send_response(s, fd, status, phrase, EST_TEXT_PLAIN, why, strlen(why)); } /* RFC 7030 section 4.2.3: 202 Accepted with `Retry-After` tells the client the * enrolment is pending manual approval and can be retried. */ static void send_accepted_retry_after(WolfCertServer* s, int fd, int retry_after_sec) { - char hdr[192]; - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 202 Accepted\r\n" - "Retry-After: %d\r\n" - "Content-Length: 0\r\n" - "Connection: %s\r\n\r\n", - retry_after_sec > 0 ? retry_after_sec : 1, conn_hdr(s)); + char ra[40]; - send_all(s, fd, hdr, (size_t)n); + snprintf(ra, sizeof(ra), "Retry-After: %d\r\n", + retry_after_sec > 0 ? retry_after_sec : 1); + send_response(s, fd, 202, "Accepted", ra, NULL, 0); } /* HTTP Basic authentication scheme token, including its trailing space. */ @@ -599,10 +614,12 @@ static int check_basic_auth(const WolfCertServer* s, const char* auth_header) * - 204 No Content -> server has no attributes to advertise. * - 200 OK -> CsrAttrs DER, base64-encoded, content-type * application/csrattrs. */ -static int handler_csr_attrs(WolfCertServer* s, int fd) +static int handler_csr_attrs(WolfCertServer* s, int fd, int head) { + static const char why[] = "cannot encode the CSR attributes\n"; + if (s->cfg_csr_attrs == NULL || s->cfg_csr_attrs_len == 0) { - send_status(s, fd, 204, "No Content"); + send_response(s, fd, 204, "No Content", "", NULL, 0); return WOLFCERT_OK; } @@ -610,48 +627,40 @@ static int handler_csr_attrs(WolfCertServer* s, int fd) int rc = wolfcert_base64_encode_mime(s->cfg_csr_attrs, s->cfg_csr_attrs_len, &b64, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd, 500, "Server Error"); + send_response(s, fd, 500, "Server Error", EST_TEXT_PLAIN, + head ? NULL : why, sizeof(why) - 1); return rc; } - char hdr[256]; - int n = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 200 OK\r\n" - "Content-Type: application/csrattrs\r\n" - "Content-Transfer-Encoding: base64\r\n" - "Content-Length: %zu\r\n" - "Connection: %s\r\n\r\n", - b64.len, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)n); - send_all(s, fd, b64.data, b64.len); + send_response(s, fd, 200, "OK", + "Content-Type: application/csrattrs\r\n" + "Content-Transfer-Encoding: base64\r\n", + head ? NULL : b64.data, b64.len); wolfcert_buffer_free(&b64); return WOLFCERT_OK; } -static int handler_cacerts(WolfCertServer* s, int fd) +static int handler_cacerts(WolfCertServer* s, int fd, int head) { + static const char why[] = "cannot build the CA certificates response\n"; const uint8_t* certs[1] = { s->ca.cert_der }; size_t clen[1] = { s->ca.cert_der_len }; WolfCertBuffer p7 = { 0 }; - - int rc = wolfcert_pkcs7_build_certs_only(certs, clen, 1, &p7, s->heap); - if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); - return rc; - } - WolfCertBuffer b64 = { 0 }; - rc = wolfcert_base64_encode_mime(p7.data, p7.len, &b64, s->heap); + int rc = wolfcert_pkcs7_build_certs_only(certs, clen, 1, &p7, s->heap); + if (rc == WOLFCERT_OK) + rc = wolfcert_base64_encode_mime(p7.data, p7.len, &b64, s->heap); wolfcert_buffer_free(&p7); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_response(s, fd, 500, "Server Error", EST_TEXT_PLAIN, + head ? NULL : why, sizeof(why) - 1); return rc; } - send_pkcs7_b64(s, fd,b64.data, b64.len); + send_response(s, fd, 200, "OK", EST_PKCS7_CERTS, + head ? NULL : b64.data, b64.len); wolfcert_buffer_free(&b64); return WOLFCERT_OK; @@ -920,57 +929,164 @@ static void send_missing_oid(WolfCertServer* s, int fd, wolfcert_oid_to_dotted(oid, oid_len, txt, sizeof(txt)); char body[192]; - int bl = snprintf(body, sizeof(body), - "CSR missing required attribute OID %s\n", txt); - char hdr[192]; - int hl = snprintf(hdr, sizeof(hdr), - "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n" - "Content-Length: %d\r\nConnection: %s\r\n\r\n", - bl, conn_hdr(s)); - - send_all(s, fd, hdr, (size_t)hl); - send_all(s, fd, body, (size_t)bl); + snprintf(body, sizeof(body), + "CSR missing required attribute OID %s\n", txt); + send_error(s, fd, 400, "Bad Request", body); +} + +/* RFC 7030 section 4.2.2: a reenroll CSR must carry the Subject and SAN of + * the TLS client certificate it renews. */ +static int reenroll_identity_check(WolfCertServer* s, + const uint8_t* csr_der, size_t csr_len) +{ +#ifdef KEEP_PEER_CERT + WOLFSSL_X509* peer = NULL; + const unsigned char* peer_der = NULL; + int peer_len = 0; + DecodedCert pc; + DecodedCert cc; + const byte* psan = NULL; + const byte* csan = NULL; + word32 psan_len = 0; + word32 csan_len = 0; + int rc = WOLFCERT_OK; + int wrc; + + if (s->tls_current != NULL) + peer = wolfSSL_get_peer_certificate(s->tls_current); + if (peer != NULL) + peer_der = wolfSSL_X509_get_der(peer, &peer_len); + if (peer_der == NULL || peer_len <= 0) { + if (peer != NULL) + wolfSSL_FreeX509(peer); + /* With a client CA set, a missing cert is not the client's fault. */ + if (s->tls_current != NULL && s->cfg.tls_client_ca_pem != NULL && + s->cfg.tls_client_ca_pem_len > 0) + return WOLFCERT_ERR(WOLFCERT_ERR_TLS, "est", + "reenroll: cannot read the client certificate"); + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "est", + "reenroll: no client certificate to renew"); + } + + wc_InitDecodedCert(&pc, peer_der, (word32)peer_len, s->heap); + wc_InitDecodedCert(&cc, (byte*)csr_der, (word32)csr_len, s->heap); + wrc = wc_ParseCert(&pc, CERT_TYPE, NO_VERIFY, NULL); + if (wrc != 0) { + rc = WOLFCERT_ERR(wrc == MEMORY_E ? WOLFCERT_ERR_MEMORY : + WOLFCERT_ERR_CRYPTO, "est", + "reenroll: cannot parse the client certificate (%d)", wrc); + } + else if ((wrc = wc_ParseCert(&cc, CERTREQ_TYPE, VERIFY, NULL)) != 0) { + if (wrc == MEMORY_E) + rc = WOLFCERT_ERR_WC(wrc, "est", "reenroll: ParseCert(CSR)"); + else + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "est", + "reenroll: CSR does not parse or verify (%d)", wrc); + } + else if (wolfcert_find_san(&pc, &psan, &psan_len) != WOLFCERT_OK || + wolfcert_find_san(&cc, &csan, &csan_len) != WOLFCERT_OK) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "est", + "reenroll: extensions do not parse"); + } + else if (pc.subjectRawLen != cc.subjectRawLen || + (pc.subjectRawLen > 0 && + memcmp(pc.subjectRaw, cc.subjectRaw, + (size_t)pc.subjectRawLen) != 0) || + pc.extSubjAltNameCrit != cc.extSubjAltNameCrit || + (psan == NULL) != (csan == NULL) || psan_len != csan_len || + (psan_len > 0 && memcmp(psan, csan, psan_len) != 0)) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "est", + "reenroll: CSR Subject/SAN differs from the renewed certificate"); + } + + wc_FreeDecodedCert(&cc); + wc_FreeDecodedCert(&pc); + wolfSSL_FreeX509(peer); + return rc; +#else + (void)csr_der; + (void)csr_len; + if (s->tls_current == NULL || s->cfg.tls_client_ca_pem == NULL || + s->cfg.tls_client_ca_pem_len == 0) + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "est", + "reenroll: no client certificate to renew"); + /* The handshake demanded a client cert, which this build cannot read. */ + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "est", + "reenroll: this wolfSSL build keeps no peer certificate"); +#endif +} + +/* Parse the PKCS#10 request and check its self-signature. */ +static int csr_verify(const uint8_t* csr_der, size_t csr_len, void* heap) +{ + DecodedCert dc; + int rc; + + wc_InitDecodedCert(&dc, (byte*)csr_der, (word32)csr_len, heap); + rc = wc_ParseCert(&dc, CERTREQ_TYPE, VERIFY, NULL); + wc_FreeDecodedCert(&dc); + if (rc == MEMORY_E) + return WOLFCERT_ERR_WC(rc, "est", "ParseCert(CSR)"); + if (rc != 0) + return WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "est", + "CSR does not parse or verify (%d)", rc); + + return WOLFCERT_OK; } -static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) +static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req, + int reenroll) { int pha = ensure_post_handshake_auth(s); if (pha != WOLFCERT_OK) { - send_status(s, fd, 401, "Unauthorized"); + send_error(s, fd, 403, "Forbidden", + "client certificate authentication failed\n"); return pha; } - if (req->body == NULL || req->body_len == 0) { - send_status(s, fd,400, "Bad Request"); - return WOLFCERT_ERR_HTTP; + /* No TLS on this connection: only Basic or the anonymous opt-in admits + * an enroll. */ + if (s->cfg_basic_user == NULL && !s->cfg.est_allow_anonymous_enroll && + s->tls_current == NULL) { + send_error(s, fd, 403, "Forbidden", + "client authentication required\n"); + return WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "est", + "enroll: no Basic credentials or TLS client on this connection"); } - /* Manual-approval gate. First POST for a given CSR: park it and - * return 202 + Retry-After. Second (matching) POST: drop the - * pending entry and fall through to issuance. */ - if (s->cfg.est_require_approval && s->priv != NULL) { - EstPriv* p = (EstPriv*)s->priv; - uint8_t h[32]; - sha256_bytes(req->body, req->body_len, h); - int idx = pending_find(p, h); - if (idx < 0) { - if (!pending_add(p, h)) { - send_status(s, fd, 503, "Service Unavailable"); - return WOLFCERT_ERR_PROTOCOL; - } - send_accepted_retry_after(s, fd, s->cfg.est_retry_after_sec); - return WOLFCERT_OK; - } - pending_remove(p, idx); + if (req->body == NULL || req->body_len == 0) { + send_error(s, fd, 400, "Bad Request", "request body is empty\n"); + return WOLFCERT_ERR_HTTP; } WolfCertBuffer csr = { 0 }; int rc = wolfcert_base64_decode(req->body, req->body_len, &csr, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd,400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", "CSR is not valid base64\n"); return rc; } + if (reenroll) { + rc = reenroll_identity_check(s, csr.data, csr.len); + if (rc != WOLFCERT_OK) { + if (rc == WOLFCERT_ERR_AUTH) + send_error(s, fd, 403, "Forbidden", + "no client certificate to renew\n"); + else if (rc == WOLFCERT_ERR_PARSE) + send_error(s, fd, 400, "Bad Request", + "CSR is not a valid PKCS#10 request\n"); + else if (rc == WOLFCERT_ERR_PROTOCOL) + send_error(s, fd, 400, "Bad Request", + "CSR does not match the certificate being renewed\n"); + else + send_error(s, fd, 500, "Server Error", + "cannot check the certificate being renewed\n"); + + wolfcert_buffer_free(&csr); + return rc; + } + } + /* RFC 7030 section 3.5 lets an EST client bind the proof-of-possession to * the TLS session by carrying the tls-unique channel binding (RFC 5929) * inside the CSR (typically the PKCS#9 challengePassword), in which case @@ -999,20 +1115,54 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) if (erc == WOLFCERT_ERR_PROTOCOL && missing_len > 0) send_missing_oid(s, fd, missing_oid, missing_len); else - send_status(s, fd, 400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", + "CSR does not satisfy the CSR attributes policy\n"); wolfcert_buffer_free(&csr); return erc; } } + /* Manual-approval gate. First POST for a given CSR: park it and + * return 202 + Retry-After. Second (matching) POST: drop the + * pending entry and fall through to issuance. */ + if (s->cfg.est_require_approval && s->priv != NULL) { + EstPriv* p = (EstPriv*)s->priv; + uint8_t h[32]; + /* The reenroll identity check has already verified the CSR. */ + rc = reenroll ? WOLFCERT_OK : csr_verify(csr.data, csr.len, s->heap); + if (rc == WOLFCERT_ERR_MEMORY) { + send_error(s, fd, 500, "Server Error", "cannot check the CSR\n"); + wolfcert_buffer_free(&csr); + return rc; + } + if (rc != WOLFCERT_OK) { + send_error(s, fd, 400, "Bad CSR", "CSR rejected by the CA\n"); + wolfcert_buffer_free(&csr); + return rc; + } + sha256_bytes(csr.data, csr.len, h); + int idx = pending_find(p, h); + if (idx < 0) { + wolfcert_buffer_free(&csr); + if (!pending_add(p, h)) { + send_error(s, fd, 503, "Service Unavailable", + "pending enrollment queue is full\n"); + return WOLFCERT_ERR_PROTOCOL; + } + send_accepted_retry_after(s, fd, s->cfg.est_retry_after_sec); + return WOLFCERT_OK; + } + pending_remove(p, idx); + } + uint8_t* issued = NULL; size_t issued_len = 0; rc = wolfcert_ca_issue(&s->ca, csr.data, csr.len, &issued, &issued_len); wolfcert_buffer_free(&csr); if (rc != WOLFCERT_OK) { - send_status(s, fd,400, "Bad CSR"); + send_error(s, fd, 400, "Bad CSR", "CSR rejected by the CA\n"); return rc; } @@ -1023,7 +1173,8 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) WOLFCERT_XFREE(issued, s->heap); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot build the certificate response\n"); return rc; } @@ -1032,11 +1183,12 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) wolfcert_buffer_free(&p7); if (rc != WOLFCERT_OK) { - send_status(s, fd,500, "Server Error"); + send_error(s, fd, 500, "Server Error", + "cannot build the certificate response\n"); return rc; } - send_pkcs7_b64(s, fd,b64.data, b64.len); + send_response(s, fd, 200, "OK", EST_PKCS7_CERTS, b64.data, b64.len); wolfcert_buffer_free(&b64); return WOLFCERT_OK; @@ -1044,6 +1196,7 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) static int handle_request(WolfCertServer* s, int fd) { + static const char unknown_op[] = "unknown EST operation\n"; EstRequest req = { 0 }; int rc = parse_request(s, fd, &req, s->heap); @@ -1051,7 +1204,7 @@ static int handle_request(WolfCertServer* s, int fd) /* No parseable request means the client hung up or sent * garbage; either way we're done with this connection. */ s->keep_alive = 0; - send_status(s, fd, 400, "Bad Request"); + send_error(s, fd, 400, "Bad Request", "malformed HTTP request\n"); free_req(&req); return rc; } @@ -1067,25 +1220,39 @@ static int handle_request(WolfCertServer* s, int fd) suffix = last ? last + 1 : req.path; } - if (strcmp(req.method, "GET") == 0 && strcmp(suffix, "cacerts") == 0) { - rc = handler_cacerts(s, fd); + /* RFC 9110 section 9.3.2: HEAD gets GET's headers without the content. */ + int head = strcmp(req.method, "HEAD") == 0; + if ((head || strcmp(req.method, "GET") == 0) && + strcmp(suffix, "cacerts") == 0) { + rc = handler_cacerts(s, fd, head); } - else if (strcmp(req.method, "GET") == 0 && strcmp(suffix, "csrattrs") == 0) { - rc = handler_csr_attrs(s, fd); + else if ((head || strcmp(req.method, "GET") == 0) && + strcmp(suffix, "csrattrs") == 0) { + rc = handler_csr_attrs(s, fd, head); } else if (strcmp(req.method, "POST") == 0 && (strcmp(suffix, "simpleenroll") == 0 || strcmp(suffix, "simplereenroll") == 0)) { if (!check_basic_auth(s, req.auth_header)) { - send_status(s, fd,401, "Unauthorized"); + static const char why[] = "HTTP Basic authentication failed\n"; + + send_response(s, fd, 401, "Unauthorized", + "WWW-Authenticate: Basic realm=\"estrealm\"\r\n" + EST_TEXT_PLAIN, why, sizeof(why) - 1); rc = WOLFCERT_ERR_AUTH; } else { - rc = handler_enroll(s, fd, &req); + rc = handler_enroll(s, fd, &req, + strcmp(suffix, "simplereenroll") == 0); } } + else if (head) { + send_response(s, fd, 404, "Not Found", EST_TEXT_PLAIN, NULL, + sizeof(unknown_op) - 1); + rc = WOLFCERT_ERR_NOT_FOUND; + } else { - send_status(s, fd,404, "Not Found"); + send_error(s, fd, 404, "Not Found", unknown_op); rc = WOLFCERT_ERR_NOT_FOUND; } diff --git a/src/http.c b/src/http.c index 5c13eaf..9431c5b 100644 --- a/src/http.c +++ b/src/http.c @@ -446,7 +446,7 @@ struct WolfCertHttpSession { long sm_content_length; /* -1 if unknown */ char* sm_content_type; /* taken from headers */ int sm_status; - int sm_retry_after_sec; /* delta-seconds; 0 if absent */ + int sm_retry_after_sec; /* seconds; 0 if absent */ int sm_head_request; int sm_interim; /* interim 1xx blocks dropped so far */ WolfCertHttpResponse* sm_resp; /* caller's resp; written to on DONE */ @@ -641,6 +641,186 @@ static char* find_header(const char* headers, const char* name, void* heap) return NULL; } +#define WOLFCERT_HTTP_MAX_RETRY_AFTER 86400 +/* 2026-01-01 00:00:00 UTC; an earlier wc_Time() is a clock not yet set. */ +#define WOLFCERT_HTTP_CLOCK_FLOOR 1767225600 + +static int only_ows(const char* p) +{ + while (*p == ' ' || *p == '\t') + ++p; + + return *p == '\0'; +} + +#ifndef NO_ASN_TIME +static int take_digits(const char** p, int min, int max, int* out) +{ + int n = 0; + int v = 0; + + while (n < max && **p >= '0' && **p <= '9') { + v = v * 10 + (**p - '0'); + ++*p; + ++n; + } + if (n < min) + return -1; + + *out = v; + return 0; +} + +static int take_month(const char** p) +{ + static const char months[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + int i; + + for (i = 0; i < 12; i++) { + if (strncmp(*p, months + i * 3, 3) == 0) { + *p += 3; + return i + 1; + } + } + + return 0; +} + +static int take_time_of_day(const char** p, int* out) +{ + int h; + int m; + int sec; + + if (take_digits(p, 2, 2, &h) != 0 || *(*p)++ != ':' || + take_digits(p, 2, 2, &m) != 0 || *(*p)++ != ':' || + take_digits(p, 2, 2, &sec) != 0 || h > 23 || m > 59 || sec > 60) + return -1; + + *out = h * 3600 + m * 60 + sec; + return 0; +} + +/* Seconds since 1970-01-01 00:00:00 UTC for a UTC date and time of day. */ +static int64_t date_to_unix_time(int year, int mon, int day, int tod) +{ + int64_t y = year - (mon <= 2); + int64_t era = (y >= 0 ? y : y - 399) / 400; + int64_t yoe = y - era * 400; + int64_t doy = (153 * (mon + (mon > 2 ? -3 : 9)) + 2) / 5 + day - 1; + + return (era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468) + * 86400 + tod; +} + +/* Seconds since 1970-01-01 00:00:00 UTC for an HTTP-date (RFC 9110 + * section 5.6.7), or -1. */ +static int64_t http_date_to_unix_time(const char* s, int64_t now) +{ + static const unsigned char mdays[12] = { + 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 + }; + const char* p = s; + int day = 0; + int mon = 0; + int year = 0; + int tod = 0; + int two_digit = 0; + + while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) + ++p; + + if (p[0] == ',' && p[1] == ' ') { + p += 2; + if (take_digits(&p, 2, 2, &day) != 0) + return -1; + + if (*p == ' ') { + ++p; + mon = take_month(&p); + if (mon == 0 || *p++ != ' ' || take_digits(&p, 4, 4, &year) != 0) + return -1; + } + else if (*p == '-') { + ++p; + mon = take_month(&p); + if (mon == 0 || *p++ != '-' || take_digits(&p, 2, 2, &year) != 0) + return -1; + two_digit = 1; + } + else { + return -1; + } + + if (*p++ != ' ' || take_time_of_day(&p, &tod) != 0 || + strncmp(p, " GMT", 4) != 0 || !only_ows(p + 4)) + return -1; + } + else if (p[0] == ' ') { + ++p; + mon = take_month(&p); + if (mon == 0 || *p++ != ' ') + return -1; + if (*p == ' ') + ++p; + if (take_digits(&p, 1, 2, &day) != 0 || *p++ != ' ' || + take_time_of_day(&p, &tod) != 0 || *p++ != ' ' || + take_digits(&p, 4, 4, &year) != 0 || !only_ows(p)) + return -1; + } + else { + return -1; + } + + /* A two-digit year more than 50 years ahead is the most recent past one. */ + if (two_digit) { + year += 1900; + while (date_to_unix_time(year + 50, mon, day, tod) <= now) + year += 100; + } + + if (day < 1 || day > mdays[mon - 1] || + (mon == 2 && day == 29 && + !((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))) + return -1; + + return date_to_unix_time(year, mon, day, tod); +} +#endif + +/* Retry-After as seconds to wait; see WolfCertHttpResponse.retry_after_sec. */ +static int parse_retry_after(const char* v) +{ + int64_t delay = 0; + + if (*v >= '0' && *v <= '9') { + while (*v >= '0' && *v <= '9') { + if (delay <= WOLFCERT_HTTP_MAX_RETRY_AFTER) + delay = delay * 10 + (*v - '0'); + ++v; + } + if (!only_ows(v)) + delay = 0; + } +#ifndef NO_ASN_TIME + else { + int64_t now = (int64_t)wc_Time(NULL); + int64_t when = -1; + + if (now >= WOLFCERT_HTTP_CLOCK_FLOOR) + when = http_date_to_unix_time(v, now); + if (when >= 0) + delay = when - now; + } +#endif + + if (delay <= 0) + return 0; + + return (delay > WOLFCERT_HTTP_MAX_RETRY_AFTER) + ? WOLFCERT_HTTP_MAX_RETRY_AFTER : (int)delay; +} + static int parse_status_line(const char* line, int* out_status) { if (strncmp(line, "HTTP/", 5) != 0) @@ -1342,19 +1522,9 @@ static int http_read_response(WolfCertConn* c, if (rc == WOLFCERT_OK) { ct = find_header(headers_nt, "Content-Type", heap); - /* RFC 7231 section 7.1.3: `Retry-After` carries either delta-seconds - * or an HTTP-date. wolfCert parses delta-seconds only. */ ra = find_header(headers_nt, "Retry-After", heap); if (ra != NULL) { - const char* p = ra; - while (*p == ' ' || *p == '\t') - ++p; - - if (*p >= '0' && *p <= '9') { - long v = strtol(p, NULL, 10); - if (v > 0 && v <= 86400) - retry_after = (int)v; - } + retry_after = parse_retry_after(ra); WOLFCERT_XFREE(ra, heap); } @@ -1932,16 +2102,7 @@ static int inspect_headers(WolfCertHttpSession* s) s->sm_retry_after_sec = 0; char* ra = find_header(hdrs, "Retry-After", s->heap); if (ra != NULL) { - const char* p = ra; - while (*p == ' ' || *p == '\t') - ++p; - - if (*p >= '0' && *p <= '9') { - long v = strtol(p, NULL, 10); - if (v > 0 && v <= 86400) - s->sm_retry_after_sec = (int)v; - } - + s->sm_retry_after_sec = parse_retry_after(ra); WOLFCERT_XFREE(ra, s->heap); } diff --git a/src/internal.c b/src/internal.c index 25c9b1f..d3ae8b3 100644 --- a/src/internal.c +++ b/src/internal.c @@ -508,7 +508,7 @@ int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, if (rc != 0 || der == NULL) { if (der != NULL) wc_FreeDer(&der); - return WOLFCERT_ERR_PARSE; + return rc == MEMORY_E ? WOLFCERT_ERR_MEMORY : WOLFCERT_ERR_PARSE; } uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(der->length, heap); @@ -525,3 +525,70 @@ int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, wc_FreeDer(&der); return WOLFCERT_OK; } + +WOLFCERT_TEST_VIS int wolfcert_find_san(const DecodedCert* dc, + const byte** san, word32* san_len) +{ + static const byte san_oid[] = { ASN_OBJECT_ID, 0x03, 0x55, 0x1D, 0x11 }; + const byte* ext = dc->extensions; + int ext_sz = dc->extensionsSz; + word32 idx = 0; + word32 end = 0; + word32 ext_end = 0; + int len = 0; + int rc = WOLFCERT_OK; + byte tag = 0; + + *san = NULL; + *san_len = 0; + if (ext == NULL || ext_sz <= 0) + return WOLFCERT_OK; + + if ((!dc->isCSR && + (GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || + tag != (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3) || + GetLength(ext, &idx, &len, (word32)ext_sz) < 0)) || + GetASNTag(ext, &idx, &tag, (word32)ext_sz) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(ext, &idx, &len, (word32)ext_sz) < 0) + rc = WOLFCERT_ERR_PARSE; + else + end = idx + (word32)len; + + /* Extension ::= SEQUENCE { OID, critical BOOLEAN OPTIONAL, OCTET STRING } */ + while (rc == WOLFCERT_OK && *san == NULL && idx < end) { + if (GetASNTag(ext, &idx, &tag, end) < 0 || + tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) || + GetLength(ext, &idx, &len, end) < 0) { + rc = WOLFCERT_ERR_PARSE; + } + else { + ext_end = idx + (word32)len; + if (ext_end - idx < sizeof(san_oid) || + memcmp(ext + idx, san_oid, sizeof(san_oid)) != 0) { + idx = ext_end; + } + else { + idx += (word32)sizeof(san_oid); + if (idx < ext_end && ext[idx] == ASN_BOOLEAN) { + if (GetASNTag(ext, &idx, &tag, ext_end) < 0 || + GetLength(ext, &idx, &len, ext_end) < 0) + rc = WOLFCERT_ERR_PARSE; + else + idx += (word32)len; + } + if (rc == WOLFCERT_OK && + (GetASNTag(ext, &idx, &tag, ext_end) < 0 || + tag != ASN_OCTET_STRING || + GetLength(ext, &idx, &len, ext_end) < 0)) + rc = WOLFCERT_ERR_PARSE; + if (rc == WOLFCERT_OK) { + *san = ext + idx; + *san_len = (word32)len; + } + } + } + } + + return rc; +} diff --git a/src/internal.h b/src/internal.h index e11fcbb..17098af 100644 --- a/src/internal.h +++ b/src/internal.h @@ -359,6 +359,14 @@ int wolfcert_transport_fd(const WolfCertTransport* t, void* conn); int wolfcert_pem_cert_to_der(const uint8_t* pem, size_t pem_len, WolfCertBuffer* out_der, void* heap); +/* 1 when meta sets a Subject or SAN. */ +int wolfcert_csr_meta_sets_identity(const WolfCertCertMeta* meta); + +/* GeneralNames of the subjectAltName in dc, or *san NULL when there is + * none. Returns WOLFCERT_OK or WOLFCERT_ERR_PARSE. */ +WOLFCERT_TEST_VIS int wolfcert_find_san(const DecodedCert* dc, + const byte** san, word32* san_len); + /* wolfcert_csr_build() for a renewal: with renew_cert (PEM or DER) set, the * CSR carries that certificate's Subject and SAN, and meta may not set them. */ WOLFCERT_TEST_VIS int wolfcert_csr_build_ex(const WolfCertKey* key, @@ -443,7 +451,8 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_issuer_and_serial( /* Does any certificate in `pem` carry `issuer` and `serial`? A CertRep is only * checked for signer and nonce, so without this a CA could answer a GetCert with * some other certificate and the caller would write it out as the one it named. - * An entry that will not parse is skipped, not treated as the end of the list. */ + * An entry that will not parse is skipped, not treated as the end of the list. + * Returns 1 if found, 0 if not, or WOLFCERT_ERR_MEMORY. */ WOLFCERT_TEST_VIS int wolfcert_scep_pem_has_cert(const uint8_t* pem, size_t pem_len, const uint8_t* issuer, size_t issuer_len, const uint8_t* serial, size_t serial_len, @@ -510,7 +519,7 @@ int wolfcert_extract_spki(const uint8_t* der, size_t len, int is_csr, /* RFC 8894: a CertRep must be signed by the CA or its RA. Confirm the response * signer certificate shares a public key with some certificate in the trusted * GetCACert bundle (one or more concatenated DER certs). Returns WOLFCERT_OK on - * match, WOLFCERT_ERR_AUTH otherwise. */ + * match, WOLFCERT_ERR_MEMORY on OOM, WOLFCERT_ERR_AUTH otherwise. */ WOLFCERT_TEST_VIS int wolfcert_scep_verify_rep_signer( const uint8_t* signer_cert, size_t signer_cert_len, const uint8_t* ca_bundle, size_t ca_bundle_len, void* heap); diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index db625b4..b28c238 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -893,7 +893,9 @@ static int scep_finish(void* heap, if (rc == WOLFCERT_OK) { rc = wolfcert_scep_verify_rep_signer(rx_signer, rx_signer_len, ca_bundle, ca_bundle_len, heap); - if (rc != WOLFCERT_OK) + if (rc == WOLFCERT_ERR_MEMORY) + rc = WOLFCERT_ERR(rc, "scep", "out of memory checking the CertRep signer"); + else if (rc != WOLFCERT_OK) rc = WOLFCERT_ERR(WOLFCERT_ERR_AUTH, "scep", "CertRep is not signed by the CA/RA certificate"); } @@ -1323,20 +1325,26 @@ WOLFCERT_TEST_VIS int wolfcert_scep_pem_has_cert(const uint8_t* pem, size_t pem_ { static const char BEGIN[] = "-----BEGIN CERTIFICATE-----"; const size_t blen = sizeof(BEGIN) - 1; + DecodedCert* dc; const char* p; const char* end; + int match = 0; if (pem == NULL || issuer == NULL || serial == NULL || pem_len < blen) return 0; + dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + if (dc == NULL) + return WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "scep", + "GetCert: cannot allocate a DecodedCert"); + p = (const char*)pem; end = p + pem_len; /* Compare the remaining length: p + blen would run past the buffer. */ - while ((size_t)(end - p) >= blen) { + while (match == 0 && (size_t)(end - p) >= blen) { WolfCertBuffer der = { 0 }; - DecodedCert dc; - int match; + int rc; if (memcmp(p, BEGIN, blen) != 0) { p++; @@ -1344,36 +1352,36 @@ WOLFCERT_TEST_VIS int wolfcert_scep_pem_has_cert(const uint8_t* pem, size_t pem_ } /* A bad entry must not end the search: the target may sit behind it. */ - if (wolfcert_pem_cert_to_der((const uint8_t*)p, (size_t)(end - p), - &der, heap) != WOLFCERT_OK) { - p += blen; - continue; + rc = wolfcert_pem_cert_to_der((const uint8_t*)p, (size_t)(end - p), + &der, heap); + if (rc == WOLFCERT_ERR_MEMORY) { + match = WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "scep", + "GetCert: out of memory decoding a certificate"); } - - wc_InitDecodedCert(&dc, der.data, (word32)der.len, heap); - if (wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL) != 0) { - wc_FreeDecodedCert(&dc); - wolfcert_buffer_free(&der); - p += blen; - continue; + else if (rc == WOLFCERT_OK) { + wc_InitDecodedCert(dc, der.data, (word32)der.len, heap); + rc = wc_ParseCert(dc, CERT_TYPE, NO_VERIFY, NULL); + if (rc == MEMORY_E) { + match = WOLFCERT_ERR(WOLFCERT_ERR_MEMORY, "scep", + "GetCert: out of memory parsing a certificate"); + } + else if (rc == 0) { + match = dc->serialSz > 0 && + (size_t)dc->serialSz == serial_len && + memcmp(dc->serial, serial, serial_len) == 0 && + dc->issuerRaw != NULL && dc->issuerRawLen > 0 && + (size_t)dc->issuerRawLen == issuer_len && + memcmp(dc->issuerRaw, issuer, issuer_len) == 0; + } + wc_FreeDecodedCert(dc); } - match = dc.serialSz > 0 && (size_t)dc.serialSz == serial_len && - memcmp(dc.serial, serial, serial_len) == 0 && - dc.issuerRaw != NULL && dc.issuerRawLen > 0 && - (size_t)dc.issuerRawLen == issuer_len && - memcmp(dc.issuerRaw, issuer, issuer_len) == 0; - - wc_FreeDecodedCert(&dc); wolfcert_buffer_free(&der); - - if (match) - return 1; - p += blen; } - return 0; + WOLFCERT_XFREE(dc, heap); + return match; } int wolfcert_scep_get_cert(const WolfCertServerCfg* srv, @@ -1435,14 +1443,17 @@ int wolfcert_scep_get_cert(const WolfCertServerCfg* srv, key_der, key_der_len, "21", ias.data, ias.len, NULL, 0, out); - if (rc == WOLFCERT_OK && out->status == WOLFCERT_SCEP_STATUS_SUCCESS && - !wolfcert_scep_pem_has_cert(out->cert_pem.data, out->cert_pem.len, - want_issuer, want_issuer_len, - want_serial, want_serial_len, heap)) { - wolfcert_buffer_free(&out->cert_pem); - out->status = WOLFCERT_SCEP_STATUS_UNSET; - rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", - "GetCert returned no certificate with the requested issuer and serial"); + if (rc == WOLFCERT_OK && out->status == WOLFCERT_SCEP_STATUS_SUCCESS) { + int has = wolfcert_scep_pem_has_cert(out->cert_pem.data, + out->cert_pem.len, + want_issuer, want_issuer_len, + want_serial, want_serial_len, heap); + if (has != 1) { + wolfcert_buffer_free(&out->cert_pem); + out->status = WOLFCERT_SCEP_STATUS_UNSET; + rc = (has < 0) ? has : WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", + "GetCert returned no certificate with the requested issuer and serial"); + } } wolfcert_buffer_free(&ias); diff --git a/src/scep/scep_msg.c b/src/scep/scep_msg.c index 6752993..fb464b9 100644 --- a/src/scep/scep_msg.c +++ b/src/scep/scep_msg.c @@ -462,7 +462,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_self_signed_rsa(RsaKey* key, const uint8_t* csr_der, size_t csr_len, uint8_t** out_der, size_t* out_len, void* heap) { - DecodedCert dc; + DecodedCert* dc; Cert* cert; WC_RNG rng; uint8_t* der; @@ -486,6 +486,12 @@ WOLFCERT_TEST_VIS int wolfcert_scep_self_signed_rsa(RsaKey* key, wc_InitCert_ex(cert, heap, WOLFCERT_DEVID_SOFTWARE); + dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + if (dc == NULL) { + wc_CertFree(cert); + return WOLFCERT_ERR_MEMORY; + } + /* RFC 8894 section 2.3: the signer certificate SHOULD carry the same * subject name as the enclosed PKCS#10 request. The certificate is * self-signed, so its issuer name is the same DN. @@ -496,33 +502,35 @@ WOLFCERT_TEST_VIS int wolfcert_scep_self_signed_rsa(RsaKey* key, * for a NUL-free DN that leaves room for a terminator, and fall back to the * request's common name otherwise. The signer subject is not security * relevant: issuance binds on the public key, not this name. */ - wc_InitDecodedCert(&dc, (const byte*)csr_der, (word32)csr_len, heap); - rc = wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL); + wc_InitDecodedCert(dc, (const byte*)csr_der, (word32)csr_len, heap); + rc = wc_ParseCert(dc, CERTREQ_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); wc_CertFree(cert); return WOLFCERT_ERR_PARSE; } - if (dc.subjectRaw != NULL && dc.subjectRawLen > 0 && - dc.subjectRawLen < (int)sizeof(cert->sbjRaw) && - memchr(dc.subjectRaw, 0x00, (size_t)dc.subjectRawLen) == NULL) { - memcpy(cert->sbjRaw, dc.subjectRaw, (size_t)dc.subjectRawLen); - cert->sbjRaw[dc.subjectRawLen] = '\0'; - memcpy(cert->issRaw, dc.subjectRaw, (size_t)dc.subjectRawLen); - cert->issRaw[dc.subjectRawLen] = '\0'; + if (dc->subjectRaw != NULL && dc->subjectRawLen > 0 && + dc->subjectRawLen < (int)sizeof(cert->sbjRaw) && + memchr(dc->subjectRaw, 0x00, (size_t)dc->subjectRawLen) == NULL) { + memcpy(cert->sbjRaw, dc->subjectRaw, (size_t)dc->subjectRawLen); + cert->sbjRaw[dc->subjectRawLen] = '\0'; + memcpy(cert->issRaw, dc->subjectRaw, (size_t)dc->subjectRawLen); + cert->issRaw[dc->subjectRawLen] = '\0'; } - else if (dc.subjectCN != NULL && dc.subjectCNLen > 0) { - int cn = dc.subjectCNLen < CTC_NAME_SIZE - 1 - ? dc.subjectCNLen : CTC_NAME_SIZE - 1; - memcpy(cert->subject.commonName, dc.subjectCN, (size_t)cn); + else if (dc->subjectCN != NULL && dc->subjectCNLen > 0) { + int cn = dc->subjectCNLen < CTC_NAME_SIZE - 1 + ? dc->subjectCNLen : CTC_NAME_SIZE - 1; + memcpy(cert->subject.commonName, dc->subjectCN, (size_t)cn); cert->subject.commonName[cn] = '\0'; } else { strncpy(cert->subject.commonName, "SCEP Enrollee", CTC_NAME_SIZE - 1); cert->subject.commonName[CTC_NAME_SIZE - 1] = '\0'; } - wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); cert->selfSigned = 1; cert->sigType = CTC_SHA256wRSA; @@ -1037,57 +1045,52 @@ static void issuing_ca_name(const DecodedCert* dc, const uint8_t** out_name, } } -WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( - const uint8_t* ra_cert_der, size_t ra_cert_len, - const uint8_t* csr_der, size_t csr_len, - WolfCertBuffer* out_der, void* heap) +static int issuer_and_subject_der(DecodedCert* ic, DecodedCert* sc, + const uint8_t* ra_cert_der, size_t ra_cert_len, + const uint8_t* csr_der, size_t csr_len, + WolfCertBuffer* out_der, void* heap) { const uint8_t* issuer_name; int issuer_name_len; - if (ra_cert_der == NULL || csr_der == NULL || out_der == NULL) - return WOLFCERT_ERR_BAD_ARG; - - DecodedCert ic; - wc_InitDecodedCert(&ic, (byte*)ra_cert_der, + wc_InitDecodedCert(ic, (byte*)ra_cert_der, (word32)ra_cert_len, heap); - int rc = wc_ParseCert(&ic, CERT_TYPE, NO_VERIFY, NULL); + int rc = wc_ParseCert(ic, CERT_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_PARSE; } - DecodedCert sc; - wc_InitDecodedCert(&sc, (byte*)csr_der, (word32)csr_len, heap); + wc_InitDecodedCert(sc, (byte*)csr_der, (word32)csr_len, heap); - rc = wc_ParseCert(&sc, CERTREQ_TYPE, NO_VERIFY, NULL); + rc = wc_ParseCert(sc, CERTREQ_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_PARSE; } - issuing_ca_name(&ic, &issuer_name, &issuer_name_len); + issuing_ca_name(ic, &issuer_name, &issuer_name_len); if (issuer_name == NULL || issuer_name_len <= 0 || - sc.subjectRaw == NULL || sc.subjectRawLen <= 0) { - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + sc->subjectRaw == NULL || sc->subjectRawLen <= 0) { + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_PARSE; } /* Give each Name its own SEQUENCE, so the result decodes as * IssuerAndSubject ::= SEQUENCE { issuer Name, subject Name }. */ size_t issuer_tlv = enc_tlv_len((size_t)issuer_name_len); - size_t subject_tlv = enc_tlv_len((size_t)sc.subjectRawLen); + size_t subject_tlv = enc_tlv_len((size_t)sc->subjectRawLen); size_t inner = issuer_tlv + subject_tlv; size_t cap = inner + 8; uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(cap, heap); if (buf == NULL) { - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_MEMORY; } @@ -1095,8 +1098,8 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( int ll = der_put_len(buf + 1, cap - 1, inner); if (ll < 0) { WOLFCERT_XFREE(buf, heap); - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_MEMORY; } @@ -1105,21 +1108,21 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( buf + off, cap - off); if (n > 0) { off += (size_t)n; - n = enc_seq(sc.subjectRaw, (size_t)sc.subjectRawLen, + n = enc_seq(sc->subjectRaw, (size_t)sc->subjectRawLen, buf + off, cap - off); } if (n < 0) { WOLFCERT_XFREE(buf, heap); - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); return WOLFCERT_ERR_MEMORY; } off += (size_t)n; - wc_FreeDecodedCert(&ic); - wc_FreeDecodedCert(&sc); + wc_FreeDecodedCert(ic); + wc_FreeDecodedCert(sc); out_der->data = buf; out_der->len = off; out_der->heap = heap; @@ -1127,31 +1130,47 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( return WOLFCERT_OK; } -WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( +WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_subject( const uint8_t* ra_cert_der, size_t ra_cert_len, - const uint8_t* serial, size_t serial_len, + const uint8_t* csr_der, size_t csr_len, WolfCertBuffer* out_der, void* heap) { - const uint8_t* issuer_name; - int issuer_name_len; + DecodedCert* dc; + int rc; - if (ra_cert_der == NULL || serial == NULL || serial_len == 0 || - out_der == NULL) + if (ra_cert_der == NULL || csr_der == NULL || out_der == NULL) return WOLFCERT_ERR_BAD_ARG; - DecodedCert ic; - wc_InitDecodedCert(&ic, (byte*)ra_cert_der, (word32)ra_cert_len, heap); + dc = (DecodedCert*)WOLFCERT_XMALLOC(2 * sizeof(*dc), heap); + if (dc == NULL) + return WOLFCERT_ERR_MEMORY; + + rc = issuer_and_subject_der(&dc[0], &dc[1], ra_cert_der, ra_cert_len, + csr_der, csr_len, out_der, heap); + WOLFCERT_XFREE(dc, heap); + return rc; +} + +static int issuer_and_serial_der(DecodedCert* ic, + const uint8_t* ra_cert_der, size_t ra_cert_len, + const uint8_t* serial, size_t serial_len, + WolfCertBuffer* out_der, void* heap) +{ + const uint8_t* issuer_name; + int issuer_name_len; - int rc = wc_ParseCert(&ic, CERT_TYPE, NO_VERIFY, NULL); + wc_InitDecodedCert(ic, (byte*)ra_cert_der, (word32)ra_cert_len, heap); + + int rc = wc_ParseCert(ic, CERT_TYPE, NO_VERIFY, NULL); if (rc != 0) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_PARSE; } - issuing_ca_name(&ic, &issuer_name, &issuer_name_len); + issuing_ca_name(ic, &issuer_name, &issuer_name_len); if (issuer_name == NULL || issuer_name_len <= 0) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_PARSE; } @@ -1164,7 +1183,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(cap, heap); if (buf == NULL) { - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_MEMORY; } @@ -1172,7 +1191,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( int ll = der_put_len(buf + 1, cap - 1, inner); if (ll < 0) { WOLFCERT_XFREE(buf, heap); - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); return WOLFCERT_ERR_MEMORY; } @@ -1184,7 +1203,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( n = enc_integer(serial, serial_len, buf + off, cap - off); } - wc_FreeDecodedCert(&ic); + wc_FreeDecodedCert(ic); if (n < 0) { WOLFCERT_XFREE(buf, heap); @@ -1199,6 +1218,28 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( return WOLFCERT_OK; } +WOLFCERT_TEST_VIS int wolfcert_scep_issuer_and_serial( + const uint8_t* ra_cert_der, size_t ra_cert_len, + const uint8_t* serial, size_t serial_len, + WolfCertBuffer* out_der, void* heap) +{ + DecodedCert* ic; + int rc; + + if (ra_cert_der == NULL || serial == NULL || serial_len == 0 || + out_der == NULL) + return WOLFCERT_ERR_BAD_ARG; + + ic = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*ic), heap); + if (ic == NULL) + return WOLFCERT_ERR_MEMORY; + + rc = issuer_and_serial_der(ic, ra_cert_der, ra_cert_len, serial, serial_len, + out_der, heap); + WOLFCERT_XFREE(ic, heap); + return rc; +} + WOLFCERT_TEST_VIS int wolfcert_scep_parse_issuer_and_serial( const uint8_t* der, size_t der_len, const uint8_t** out_issuer, @@ -1279,32 +1320,36 @@ WOLFCERT_TEST_VIS int wolfcert_scep_issuer_name_matches( int wolfcert_extract_spki(const uint8_t* der, size_t len, int is_csr, uint8_t** out_spki, size_t* out_len, void* heap) { - DecodedCert dc; - wc_InitDecodedCert(&dc, (byte*)der, (word32)len, heap); + DecodedCert* dc = (DecodedCert*)WOLFCERT_XMALLOC(sizeof(*dc), heap); + uint8_t* buf = NULL; + int rc = WOLFCERT_OK; + int wrc; - int rc = wc_ParseCert(&dc, is_csr ? CERTREQ_TYPE : CERT_TYPE, NO_VERIFY, NULL); - if (rc != 0) { - wc_FreeDecodedCert(&dc); - return WOLFCERT_ERR_PARSE; - } + if (dc == NULL) + return WOLFCERT_ERR_MEMORY; - if (dc.publicKey == NULL || dc.pubKeySize == 0) { - wc_FreeDecodedCert(&dc); - return WOLFCERT_ERR_PARSE; - } + wc_InitDecodedCert(dc, (byte*)der, (word32)len, heap); + wrc = wc_ParseCert(dc, is_csr ? CERTREQ_TYPE : CERT_TYPE, NO_VERIFY, NULL); + if (wrc == MEMORY_E) + rc = WOLFCERT_ERR_MEMORY; + else if (wrc != 0 || dc->publicKey == NULL || dc->pubKeySize == 0) + rc = WOLFCERT_ERR_PARSE; - uint8_t* buf = (uint8_t*)WOLFCERT_XMALLOC(dc.pubKeySize, heap); - if (buf == NULL) { - wc_FreeDecodedCert(&dc); - return WOLFCERT_ERR_MEMORY; + if (rc == WOLFCERT_OK) { + buf = (uint8_t*)WOLFCERT_XMALLOC(dc->pubKeySize, heap); + if (buf == NULL) + rc = WOLFCERT_ERR_MEMORY; } - memcpy(buf, dc.publicKey, dc.pubKeySize); - *out_spki = buf; - *out_len = dc.pubKeySize; - wc_FreeDecodedCert(&dc); + if (rc == WOLFCERT_OK) { + memcpy(buf, dc->publicKey, dc->pubKeySize); + *out_spki = buf; + *out_len = dc->pubKeySize; + } - return WOLFCERT_OK; + wc_FreeDecodedCert(dc); + WOLFCERT_XFREE(dc, heap); + return rc; } /* Total length (tag + length octets + value) of the DER SEQUENCE at `p`, or 0 @@ -1333,28 +1378,35 @@ WOLFCERT_TEST_VIS int wolfcert_scep_verify_rep_signer(const uint8_t* signer_cert size_t ca_spki_len = 0; size_t off = 0; size_t clen; - int matched = 0; + int erc; + int rc; if (signer_cert == NULL || ca_bundle == NULL) return WOLFCERT_ERR_AUTH; - if (wolfcert_extract_spki(signer_cert, signer_cert_len, 0, - &signer_spki, &signer_spki_len, heap) != WOLFCERT_OK) - return WOLFCERT_ERR_AUTH; + rc = wolfcert_extract_spki(signer_cert, signer_cert_len, 0, + &signer_spki, &signer_spki_len, heap); + if (rc != WOLFCERT_OK) + return rc == WOLFCERT_ERR_MEMORY ? rc : WOLFCERT_ERR_AUTH; /* RFC 8894: the CertRep is signed by the CA or its RA. Accept the signer * if it shares a public key with any certificate in the trusted GetCACert * bundle (one or more concatenated DER certs). */ - while (off < ca_bundle_len && !matched) { + rc = WOLFCERT_ERR_AUTH; + while (off < ca_bundle_len && rc == WOLFCERT_ERR_AUTH) { clen = der_seq_len(ca_bundle + off, ca_bundle_len - off); if (clen == 0) break; - if (wolfcert_extract_spki(ca_bundle + off, clen, 0, - &ca_spki, &ca_spki_len, heap) == WOLFCERT_OK) { + erc = wolfcert_extract_spki(ca_bundle + off, clen, 0, + &ca_spki, &ca_spki_len, heap); + if (erc == WOLFCERT_ERR_MEMORY) { + rc = erc; + } + else if (erc == WOLFCERT_OK) { if (ca_spki_len == signer_spki_len && memcmp(ca_spki, signer_spki, signer_spki_len) == 0) - matched = 1; + rc = WOLFCERT_OK; WOLFCERT_XFREE(ca_spki, heap); ca_spki = NULL; } @@ -1363,7 +1415,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_verify_rep_signer(const uint8_t* signer_cert } WOLFCERT_XFREE(signer_spki, heap); - return matched ? WOLFCERT_OK : WOLFCERT_ERR_AUTH; + return rc; } WOLFCERT_TEST_VIS int wolfcert_scep_check_cert_rep(const char* msg_type, diff --git a/src/server.c b/src/server.c index 4be66b1..497432e 100644 --- a/src/server.c +++ b/src/server.c @@ -287,6 +287,20 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) return WOLFCERT_ERR(WOLFCERT_ERR_TLS, "server", "EST requires TLS: set tls_cert_pem and tls_key_pem (RFC 7030)"); + if (cfg->http_basic_user != NULL && + (cfg->http_basic_user[0] == '\0' || + cfg->http_basic_pass == NULL || cfg->http_basic_pass[0] == '\0')) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "server", + "http_basic_user and http_basic_pass must both be non-empty"); + + if (cfg->protocol == WOLFCERT_PROTO_EST && cfg->http_basic_user == NULL && + (cfg->tls_client_ca_pem == NULL || + cfg->tls_client_ca_pem_len == 0) && + !cfg->est_allow_anonymous_enroll) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "server", + "EST enrollment needs http_basic_user or tls_client_ca_pem, " + "or est_allow_anonymous_enroll"); + void* heap = cfg->heap ? cfg->heap : wolfcert_default_heap(); WolfCertServer* s = (WolfCertServer*)WOLFCERT_XMALLOC(sizeof(*s), heap); if (s == NULL) @@ -314,6 +328,14 @@ int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out) if (cfg->http_basic_pass) s->cfg_basic_pass = wolfcert_strdup(cfg->http_basic_pass, heap); + if (s->cfg_bind_host == NULL || + (cfg->challenge_password && s->cfg_challenge == NULL) || + (cfg->http_basic_user && s->cfg_basic_user == NULL) || + (cfg->http_basic_pass && s->cfg_basic_pass == NULL)) { + wolfcert_server_free(s); + return WOLFCERT_ERR_MEMORY; + } + if (cfg->csr_attributes_der != NULL && cfg->csr_attributes_len > 0) { s->cfg_csr_attrs = (uint8_t*)WOLFCERT_XMALLOC(cfg->csr_attributes_len, heap); if (s->cfg_csr_attrs == NULL) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9a55a0a..bf823ce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,7 @@ endif() find_package(Threads REQUIRED) add_executable(test_transport unit/test_transport.c) +target_include_directories(test_transport PRIVATE ${CMAKE_SOURCE_DIR}/src) target_link_libraries(test_transport PRIVATE wolfcert) add_test(NAME transport COMMAND test_transport) # A lost never-0 guard spins the read-until-close loop, so bound the run. diff --git a/tests/integration/cli_proto_scoping.sh b/tests/integration/cli_proto_scoping.sh index f3ac0c6..5620499 100755 --- a/tests/integration/cli_proto_scoping.sh +++ b/tests/integration/cli_proto_scoping.sh @@ -217,6 +217,35 @@ fi # is built alongside wolfcert-client whenever the server is enabled; without it # there is nothing to enroll against, so skip just this group. SERVER="$(dirname "$CLI")/wolfcert-server" + +# wolfcert-server refuses an EST listener that could not authenticate anyone. +# Any readable file passes as --tls-cert: these checks run before it is parsed. +expect_server_reject() { + local saved="$CLI" + CLI="$SERVER" + expect_reject "$@" + CLI="$saved" +} +if [ -x "$SERVER" ]; then + expect_server_reject "est server with no client auth" "--est-allow-anonymous" \ + --proto est --tls-cert "$0" --tls-key "$0" + for b in ":pw" "alice:" "alice"; do + expect_server_reject "--basic $b" "non-empty USER:PASS" \ + --proto est --tls-cert "$0" --tls-key "$0" --basic "$b" + done + # $0 is no PEM, so start fails in TLS setup, before anything binds. + out="$("$SERVER" --proto est --listen 127.0.0.1:18099 --tls-cert "$0" \ + --tls-key "$0" --est-allow-anonymous 2>&1)" + case "$out" in + *"start failed"*) + echo "ok --est-allow-anonymous passes the auth check" ;; + *) + echo "FAIL: --est-allow-anonymous did not reach server start" + echo " got: $out" + fails=$((fails + 1)) + ;; + esac +fi if [ ! -x "$SERVER" ]; then echo "skip --ca-fingerprint pinning (wolfcert-server not built)" else diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index 11a1410..20e8a17 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -113,6 +113,48 @@ static int send_and_read_status(uint16_t port, return (int)n; } +static int fetch_whole(uint16_t port, const char* method, const char* op, + char* resp, size_t cap) +{ + TestTlsConn c; + char req[160]; + size_t n = 0; + int len; + int r; + + len = snprintf(req, sizeof(req), "%s /.well-known/est/%s HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nConnection: close\r\n\r\n", + method, op); + REQUIRE(len > 0 && (size_t)len < sizeof(req)); + REQUIRE(test_tls_connect(&c, port, g_tls_cert, g_tls_cert_len) == 0); + REQUIRE(test_tls_write(&c, req, (size_t)len) == 0); + while (n + 1 < cap && (r = test_tls_read(&c, resp + n, cap - 1 - n)) > 0) + n += (size_t)r; + test_tls_close(&c); + resp[n] = '\0'; + return 0; +} + +/* HEAD must answer with GET's headers and end there, or its body would be read + * as the next response on a kept-alive connection. */ +static int head_matches_get(uint16_t port, const char* op, const char* status) +{ + char get[4096]; + char head[4096]; + const char* eoh; + + REQUIRE(fetch_whole(port, "GET", op, get, sizeof(get)) == 0); + REQUIRE(fetch_whole(port, "HEAD", op, head, sizeof(head)) == 0); + if (strncmp(head, status, strlen(status)) != 0) + fprintf(stderr, "HEAD %s: %.40s\n", op, head); + REQUIRE(strncmp(head, status, strlen(status)) == 0); + eoh = strstr(head, "\r\n\r\n"); + REQUIRE(eoh != NULL); + REQUIRE(eoh[4] == '\0'); + REQUIRE(strncmp(get, head, (size_t)(eoh + 4 - head)) == 0); + return 0; +} + /* Shape #1: a chunk-size line longer than 8 hex digits. The parser * must reject this rather than letting the shift-accumulate silently * wrap. */ @@ -230,6 +272,43 @@ static int accept_multisegment_chunked_body(uint16_t port) return 0; } +/* Authorization is a singleton field, so a second one is a malformed request. */ +static int reject_duplicate_authorization(uint16_t port) +{ + const char* req = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Authorization: Basic AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" + "Authorization: Basic AA==\r\n" + "Connection: close\r\n" + "\r\n"; + /* A field that only starts with "Authorization" is a different field. */ + const char* other = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Authorization-Foo: x\r\n" + "Authorization: Basic AA==\r\n" + "Connection: close\r\n" + "\r\n"; + /* RFC 9112 section 5.1: whitespace before a colon is a 400. */ + const char* spaced = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Length : 0\r\n" + "Connection: close\r\n" + "\r\n"; + char status[128] = { 0 }; + send_and_read_status(port, req, strlen(req), status, sizeof(status)); + REQUIRE(strstr(status, "400") != NULL); + memset(status, 0, sizeof(status)); + send_and_read_status(port, other, strlen(other), status, sizeof(status)); + REQUIRE(strstr(status, "200") != NULL); + memset(status, 0, sizeof(status)); + send_and_read_status(port, spaced, strlen(spaced), status, sizeof(status)); + REQUIRE(strstr(status, "400") != NULL); + return 0; +} + /* Build a chunked simpleenroll request whose body carries a real, * base64-encoded CSR in a single chunk, but split so the last-chunk line * ("0\r\n") is delivered separately from its terminating trailer CRLF. @@ -386,6 +465,7 @@ static int no_sigpipe_on_response(void) WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_EST, .bind_host = "127.0.0.1", .bind_port = 0, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; struct sigaction sa, old; @@ -451,6 +531,7 @@ int main(void) .bind_host = "127.0.0.1", .bind_port = 0, .tls_cert_pem = g_tls_cert, .tls_cert_pem_len = g_tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); @@ -459,7 +540,13 @@ int main(void) uint16_t port = wolfcert_server_port(srv); - int rc = reject_oversized_chunk_size(port); + int rc = head_matches_get(port, "nope", "HTTP/1.1 404"); + if (rc == 0) + rc = head_matches_get(port, "cacerts", "HTTP/1.1 200"); + if (rc == 0) + rc = head_matches_get(port, "csrattrs", "HTTP/1.1 204"); + if (rc == 0) + rc = reject_oversized_chunk_size(port); if (rc == 0) rc = reject_corrupt_chunk_trailer(port); if (rc == 0) @@ -468,6 +555,8 @@ int main(void) rc = accept_multisegment_chunked_body(port); if (rc == 0) rc = keepalive_after_split_trailer(port); + if (rc == 0) + rc = reject_duplicate_authorization(port); if (rc == 0) rc = no_sigpipe_on_response(); diff --git a/tests/integration/test_est_csr_attrs_apply_roundtrip.c b/tests/integration/test_est_csr_attrs_apply_roundtrip.c index 131b2a3..fa23563 100644 --- a/tests/integration/test_est_csr_attrs_apply_roundtrip.c +++ b/tests/integration/test_est_csr_attrs_apply_roundtrip.c @@ -278,6 +278,7 @@ int main(void) .csr_attributes_len = policy.len, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); diff --git a/tests/integration/test_est_csr_attrs_enforce.c b/tests/integration/test_est_csr_attrs_enforce.c index d6659eb..0120ac6 100644 --- a/tests/integration/test_est_csr_attrs_enforce.c +++ b/tests/integration/test_est_csr_attrs_enforce.c @@ -270,6 +270,83 @@ static int reject_body_names_missing_oid(uint16_t port) return 0; } +/* RFC 7030 section 4.2.3: an error response without a media type must carry a + * plaintext explanation, so neither a 404 nor an empty-body 400 may be empty. */ +static int reject_bodies_are_plaintext(uint16_t port) +{ + static const char* const reqs[] = { + "GET /.well-known/est/nosuchop HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n", + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n" + }; + char resp[1024]; + size_t i; + + for (i = 0; i < sizeof(reqs) / sizeof(reqs[0]); ++i) { + const char* body; + int n = send_and_read_all(port, reqs[i], strlen(reqs[i]), + resp, sizeof(resp)); + REQUIRE(n > 0); + REQUIRE(strstr(resp, i == 0 ? " 404 " : " 400 ") != NULL); + REQUIRE(strstr(resp, "Content-Type: text/plain\r\n") != NULL); + body = strstr(resp, "\r\n\r\n"); + REQUIRE(body != NULL && body[4] != '\0'); + } + return 0; +} + +/* RFC 9110: a 204 carries no Content-Length, a 401 carries a challenge, and a + * missing client certificate is a 403 since HTTP auth cannot supply one. */ +static int bare_status_headers(uint16_t port) +{ + static const char get_attrs[] = + "GET /.well-known/est/csrattrs HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; + static const char enroll_noauth[] = + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "AAAA"; + static const char reenroll_nocert[] = + "POST /.well-known/est/simplereenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Authorization: Basic YWxpY2U6c2VjcmV0\r\n" /* alice:secret */ + "Content-Type: application/pkcs10\r\n" + "Content-Length: 4\r\n" + "Connection: close\r\n" + "\r\n" + "AAAA"; + char resp[1024]; + + REQUIRE(send_and_read_all(port, get_attrs, sizeof(get_attrs) - 1, + resp, sizeof(resp)) > 0); + REQUIRE(strstr(resp, " 204 ") != NULL); + REQUIRE(strstr(resp, "Content-Length") == NULL); + + REQUIRE(send_and_read_all(port, enroll_noauth, sizeof(enroll_noauth) - 1, + resp, sizeof(resp)) > 0); + REQUIRE(strstr(resp, " 401 ") != NULL); + REQUIRE(strstr(resp, "WWW-Authenticate: Basic realm=") != NULL); + + REQUIRE(send_and_read_all(port, reenroll_nocert, + sizeof(reenroll_nocert) - 1, + resp, sizeof(resp)) > 0); + REQUIRE(strstr(resp, " 403 ") != NULL); + return 0; +} + /* Client C - server advertises ONLY an Attribute-with-values item * (no bare OIDs). The CSR doesn't carry anything matching it. * Enforcement is presence-only on bare OIDs, so this must still @@ -329,6 +406,7 @@ int main(void) .est_require_csr_attributes = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); @@ -357,6 +435,7 @@ int main(void) .est_require_csr_attributes = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv2 = NULL; REQUIRE(wolfcert_server_start(&cfg2, &srv2) == WOLFCERT_OK); @@ -387,6 +466,7 @@ int main(void) .est_require_csr_attributes = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv_raw = NULL; REQUIRE(wolfcert_server_start(&cfg_raw, &srv_raw) == WOLFCERT_OK); @@ -394,11 +474,34 @@ int main(void) REQUIRE(pthread_create(&tid_raw, NULL, server_thread, srv_raw) == 0); rc = reject_body_names_missing_oid(wolfcert_server_port(srv_raw)); + if (rc == 0) + rc = reject_bodies_are_plaintext(wolfcert_server_port(srv_raw)); wolfcert_server_stop(srv_raw); pthread_join(tid_raw, NULL); wolfcert_server_free(srv_raw); wolfcert_buffer_free(&policy_raw); + if (rc != 0) + return rc; + + /* Fourth server with no policy and Basic auth on, for bodiless replies. */ + WolfCertServerCfgSrv cfg_bare = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", .bind_port = 0, + .http_basic_user = "alice", .http_basic_pass = "secret", + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + }; + WolfCertServer* srv_bare = NULL; + REQUIRE(wolfcert_server_start(&cfg_bare, &srv_bare) == WOLFCERT_OK); + pthread_t tid_bare; + REQUIRE(pthread_create(&tid_bare, NULL, server_thread, srv_bare) == 0); + + rc = bare_status_headers(wolfcert_server_port(srv_bare)); + + wolfcert_server_stop(srv_bare); + pthread_join(tid_bare, NULL); + wolfcert_server_free(srv_bare); free(tls_cert); free(tls_key); if (rc != 0) diff --git a/tests/integration/test_est_csr_attrs_roundtrip.c b/tests/integration/test_est_csr_attrs_roundtrip.c index ce8ed30..70601dd 100644 --- a/tests/integration/test_est_csr_attrs_roundtrip.c +++ b/tests/integration/test_est_csr_attrs_roundtrip.c @@ -114,6 +114,7 @@ int main(void) .csr_attributes_len = blob.len, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); @@ -174,6 +175,7 @@ int main(void) .bind_host = "127.0.0.1", .bind_port = 0, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv2 = NULL; REQUIRE(wolfcert_server_start(&cfg2, &srv2) == WOLFCERT_OK); diff --git a/tests/integration/test_est_mtls_roundtrip.c b/tests/integration/test_est_mtls_roundtrip.c index 89221a9..157ee9c 100644 --- a/tests/integration/test_est_mtls_roundtrip.c +++ b/tests/integration/test_est_mtls_roundtrip.c @@ -24,13 +24,20 @@ * then runs wolfcert-client through wolfcert_est_simple_enroll with * client_cert / client_key set on WolfCertServerCfg. * - * Three assertions: + * Seven assertions: * 1. mTLS works: a client that does NOT present a certificate is * rejected by the TLS handshake. * 2. mTLS works: a client that DOES present a cert signed by the * configured trust anchor enrolls successfully. * 3. /simplereenroll presents the cert being renewed even when * client_cert names a different, untrusted identity. + * 4. /simplereenroll refuses a CSR whose Subject or SAN differs from + * the cert being renewed. + * 5. /simplereenroll accepts a renewal of a multi-SAN cert the server's + * own CA issued, but not one that changes a SAN entry or marks it critical. + * 6. /simplereenroll answers a body that is not a PKCS#10 request with 400. + * 7. With manual approval on, a mismatched reenroll is refused at once + * instead of being parked. * * Exercises the TLS 1.3 negotiation path (wolfTLS_client_method / * wolfTLS_server_method) and the new client_cert plumbing on @@ -71,6 +78,7 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +#ifdef KEEP_PEER_CERT /* Re-enroll the trusted cli_cert while cfg carries an identity the server * does not trust; the handshake must present the cert being renewed. */ static int test_reenroll_ignores_cfg_identity(const char* url, @@ -131,6 +139,393 @@ static int test_reenroll_ignores_cfg_identity(const char* url, return 0; } +/* Re-enroll cli_cert with a CSR whose Subject or SAN differs from it; the + * server must refuse to issue. */ +static int test_reenroll_rejects_identity_change(const char* url, + const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + static const char* const dns[] = { "other.example" }; + const WolfCertCertMeta metas[2] = { + { .subject_dn = "CN=someone-else" }, + { .subject_dn = "CN=factory-bootstrap", .san_dns = dns, .san_dns_len = 1 }, + }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKey* cur_key = NULL; + WolfCertKey* dk = NULL; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + + for (size_t i = 0; i < sizeof(metas) / sizeof(metas[0]); ++i) { + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + int rc; + + REQUIRE(wolfcert_csr_build(dk, &metas[i], &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + csr.data, csr.len, &issued); + wolfcert_buffer_free(&csr); + if (rc == WOLFCERT_OK) + fprintf(stderr, "reenroll with changed identity %zu was issued\n", i); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(issued.data == NULL); + } + + wolfcert_key_free(dk); + wolfcert_key_free(cur_key); + return 0; +} + +/* A reenroll body that is DER but not PKCS#10 is the client's fault: 400. */ +static int test_reenroll_rejects_non_csr(const char* url, + const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + static const uint8_t not_csr[] = { 0x30, 0x03, 0x02, 0x01, 0x00 }; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKey* cur_key = NULL; + WolfCertBuffer issued = { 0 }; + int rc; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + not_csr, sizeof(not_csr), &issued); + wolfcert_key_free(cur_key); + if (rc != WOLFCERT_ERR_HTTP) + fprintf(stderr, "non-CSR reenroll rc=%d (%s)\n", rc, + wolfcert_last_error_message()); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(issued.data == NULL); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + return 0; +} + +#ifdef HAVE_ECC +/* The GeneralNames of a non-critical SAN extension in der, or NULL. */ +static const uint8_t* raw_san(const uint8_t* der, size_t der_len, size_t* len) +{ + static const uint8_t oid[] = { 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04 }; + const uint8_t* p = memmem(der, der_len, oid, sizeof(oid)); + size_t n; + + if (p == NULL || (size_t)(der + der_len - p) < sizeof(oid) + 3) + return NULL; + p += sizeof(oid); + if (p[0] < 0x80) { + *len = p[0]; + n = 1; + } + else if (p[0] == 0x81) { + *len = p[1]; + n = 2; + } + else if (p[0] == 0x82) { + *len = ((size_t)p[1] << 8) | p[2]; + n = 3; + } + else { + return NULL; + } + return (size_t)(der + der_len - p) >= n + *len ? p + n : NULL; +} + +/* A CSR carrying cert's raw Subject and SAN, with the SAN marked critical. */ +static int csr_with_critical_san(const WolfCertBuffer* cert_pem, + uint8_t* out, size_t out_cap) +{ + WC_RNG rng; + ecc_key key; + Cert req; + uint8_t der[4096]; + const uint8_t* san; + size_t san_len = 0; + int der_len; + int len = -1; + + der_len = wc_CertPemToDer(cert_pem->data, (int)cert_pem->len, der, + (int)sizeof(der), CERT_TYPE); + if (der_len <= 0) + return -1; + san = raw_san(der, (size_t)der_len, &san_len); + if (san == NULL || san_len > sizeof(req.altNames) || wc_InitRng(&rng) != 0) + return -1; + if (wc_ecc_init(&key) == 0) { + if (wc_ecc_make_key(&rng, 32, &key) == 0 && wc_InitCert(&req) == 0 && + wc_SetSubjectRaw(&req, der, der_len) == 0) { + memcpy(req.altNames, san, san_len); + req.altNamesSz = (int)san_len; + req.altNamesCrit = 1; + req.version = 0; + req.sigType = CTC_SHA256wECDSA; + len = wc_MakeCertReq_ex(&req, out, (word32)out_cap, ECC_TYPE, &key); + if (len > 0) + len = wc_SignCert_ex(req.bodySz, req.sigType, out, + (word32)out_cap, ECC_TYPE, &key, &rng); + } + wc_ecc_free(&key); + } + wc_FreeRng(&rng); + return len; +} +#endif + +/* Enroll a multi-RDN, multi-SAN identity through the server's own CA, then + * renew it: the CA's re-encoding must pass the reenroll identity check. */ +static int test_reenroll_server_issued(const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* tls_key, size_t tls_key_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + static const char* const dns[] = { "a.example", "b.example", "c.example" }; + static const char* const dns2[] = { "a.example", "b.example", "d.example" }; + static const char* const ip[] = { "127.0.0.1", "10.0.0.7" }; + static const char* const email[] = { "dev@example.com" }; + const WolfCertCertMeta meta = { + .subject_dn = "CN=multi-san,O=wolfSSL,OU=EST", + .san_dns = dns, .san_dns_len = 3, + .san_ip = ip, .san_ip_len = 2, + .san_email = email, .san_email_len = 1, + }; + const WolfCertCertMeta meta2 = { + .subject_dn = "CN=multi-san,O=wolfSSL,OU=EST", + .san_dns = dns2, .san_dns_len = 3, + .san_ip = ip, .san_ip_len = 2, + .san_email = email, .san_email_len = 1, + }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertServerCfgSrv cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .tls_client_ca_pem = cli_cert, .tls_client_ca_pem_len = cli_cert_len, + }; + WolfCertStoreOps* store = wolfcert_store_memory_open(NULL); + WolfCertServer* srv = NULL; + WolfCertBuffer ca_der = { 0 }; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + WolfCertBuffer renewed = { 0 }; + WolfCertBuffer san_changed = { 0 }; + WolfCertKey* k1 = NULL; + WolfCertKey* k2 = NULL; + uint8_t* bundle = NULL; + size_t bundle_cap; + pthread_t tid; + char url[128]; + int pem_len; + int rc; +#ifdef HAVE_ECC + uint8_t crit_csr[4096]; + WolfCertBuffer refused = { 0 }; + int crit_len; +#endif + + /* The first start mints the CA into the store; the second adopts it. */ + REQUIRE(store != NULL); + cfg.ca_store = store; + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + wolfcert_server_free(srv); + srv = NULL; + REQUIRE(store->read(store->ctx, "ca.cert.der", &ca_der) == WOLFCERT_OK); + + bundle_cap = cli_cert_len + ca_der.len * 2 + 256; + bundle = (uint8_t*)malloc(bundle_cap); + REQUIRE(bundle != NULL); + memcpy(bundle, cli_cert, cli_cert_len); + pem_len = wc_DerToPem(ca_der.data, (word32)ca_der.len, bundle + cli_cert_len, + (word32)(bundle_cap - cli_cert_len), CERT_TYPE); + REQUIRE(pem_len > 0); + cfg.tls_client_ca_pem = bundle; + cfg.tls_client_ca_pem_len = cli_cert_len + (size_t)pem_len; + + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); + snprintf(url, sizeof(url), "https://127.0.0.1:%u/.well-known/est", + wolfcert_server_port(srv)); + + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + .client_cert = cli_cert, + .client_cert_len = cli_cert_len, + .client_key = cli_key, + .client_key_len = cli_key_len, + }; + + REQUIRE(wolfcert_key_generate(&kcfg, &k1) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(k1, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_enroll(&cli, csr.data, csr.len, &issued); + wolfcert_buffer_free(&csr); + REQUIRE(rc == WOLFCERT_OK); + + REQUIRE(wolfcert_key_generate(&kcfg, &k2) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(k2, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, issued.data, issued.len, k1, + csr.data, csr.len, &renewed); + if (rc != WOLFCERT_OK) + fprintf(stderr, "reenroll of a server-issued cert rc=%d (%s)\n", rc, + wolfcert_strerror(rc)); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(memmem(renewed.data, renewed.len, "BEGIN CERTIFICATE", 17) != NULL); + + /* Same Subject and SAN shape, one dNSName changed. */ + wolfcert_buffer_free(&csr); + REQUIRE(wolfcert_csr_build(k2, &meta2, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, issued.data, issued.len, k1, + csr.data, csr.len, &san_changed); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + REQUIRE(san_changed.data == NULL); + +#ifdef HAVE_ECC + crit_len = csr_with_critical_san(&issued, crit_csr, sizeof(crit_csr)); + REQUIRE(crit_len > 0); + rc = wolfcert_est_simple_reenroll(&cli, issued.data, issued.len, k1, + crit_csr, (size_t)crit_len, &refused); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + REQUIRE(refused.data == NULL); +#endif + + wolfcert_server_stop(srv); + pthread_join(tid, NULL); + wolfcert_server_free(srv); + wolfcert_store_memory_close(store); + wolfcert_buffer_free(&renewed); + wolfcert_buffer_free(&issued); + wolfcert_buffer_free(&csr); + wolfcert_buffer_free(&ca_der); + wolfcert_key_free(k2); + wolfcert_key_free(k1); + free(bundle); + return 0; +} + +/* The approval gate runs after the identity check, so a mismatched reenroll + * gets its 400 on the first POST rather than a 202. */ +static int test_reenroll_mismatch_not_parked(const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* tls_key, size_t tls_key_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + const WolfCertCertMeta meta = { .subject_dn = "CN=someone-else" }; + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertServerCfgSrv cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .tls_client_ca_pem = cli_cert, .tls_client_ca_pem_len = cli_cert_len, + .est_require_approval = 1, + }; + WolfCertServer* srv = NULL; + WolfCertKey* cur_key = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + pthread_t tid; + char url[128]; + int rc; + + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); + snprintf(url, sizeof(url), "https://127.0.0.1:%u/.well-known/est", + wolfcert_server_port(srv)); + + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(dk, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + csr.data, csr.len, &issued); + if (rc != WOLFCERT_ERR_HTTP) + fprintf(stderr, "mismatched reenroll under approval rc=%d (%s)\n", rc, + wolfcert_last_error_message()); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(issued.data == NULL); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 400") != NULL); + + wolfcert_server_stop(srv); + pthread_join(tid, NULL); + wolfcert_server_free(srv); + wolfcert_buffer_free(&csr); + wolfcert_key_free(dk); + wolfcert_key_free(cur_key); + return 0; +} + +#endif /* KEEP_PEER_CERT */ + +#ifndef KEEP_PEER_CERT +/* The mTLS handshake demanded a client cert this build cannot read: 500. */ +static int test_reenroll_needs_peer_cert(const char* url, + const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertCertMeta meta = { .subject_dn = "CN=factory-bootstrap" }; + WolfCertServerCfg cli = { + .protocol = WOLFCERT_PROTO_EST, + .server_url = url, + .trust_anchors = tls_cert, + .trust_anchors_len = tls_cert_len, + .verify_server = 1, + }; + WolfCertKey* cur_key = NULL; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer issued = { 0 }; + int rc; + + REQUIRE(wolfcert_key_from_pem(cli_key, cli_key_len, NULL, &cur_key) == WOLFCERT_OK); + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(dk, &meta, &csr) == WOLFCERT_OK); + rc = wolfcert_est_simple_reenroll(&cli, cli_cert, cli_cert_len, cur_key, + csr.data, csr.len, &issued); + wolfcert_buffer_free(&csr); + wolfcert_key_free(dk); + wolfcert_key_free(cur_key); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(strstr(wolfcert_last_error_message(), "HTTP 500") != NULL); + REQUIRE(issued.data == NULL); + return 0; +} +#endif + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -225,15 +620,47 @@ int main(void) wolfcert_key_free(dk); } +#ifdef KEEP_PEER_CERT /* --- Case 3: reenroll presents the cert being renewed, not cfg's. */ REQUIRE(test_reenroll_ignores_cfg_identity(url, tls_cert, tls_cert_len, cli_cert, cli_cert_len, cli_key, cli_key_len) == 0); + /* --- Case 4: reenroll may not change the Subject or SAN. */ + REQUIRE(test_reenroll_rejects_identity_change(url, tls_cert, tls_cert_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); + + /* --- Case 6: a body that is not a CSR is a 400, not a server error. */ + REQUIRE(test_reenroll_rejects_non_csr(url, tls_cert, tls_cert_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); +#else + /* --- Without KEEP_PEER_CERT an mTLS reenroll is a 500, not an issue. */ + REQUIRE(test_reenroll_needs_peer_cert(url, tls_cert, tls_cert_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); + printf("skip reenroll cases: wolfSSL built without KEEP_PEER_CERT\n"); +#endif + wolfcert_server_stop(srv); pthread_join(tid, NULL); wolfcert_server_free(srv); +#ifdef KEEP_PEER_CERT + /* --- Case 5: renew a server-issued cert carrying several SANs. */ + REQUIRE(test_reenroll_server_issued(tls_cert, tls_cert_len, + tls_key, tls_key_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); + + /* --- Case 7: under manual approval a mismatch is refused, not parked. */ + REQUIRE(test_reenroll_mismatch_not_parked(tls_cert, tls_cert_len, + tls_key, tls_key_len, + cli_cert, cli_cert_len, + cli_key, cli_key_len) == 0); +#endif + free(tls_cert); free(tls_key); free(cli_cert); diff --git a/tests/integration/test_est_pending_roundtrip.c b/tests/integration/test_est_pending_roundtrip.c index 691780a..f1bc050 100644 --- a/tests/integration/test_est_pending_roundtrip.c +++ b/tests/integration/test_est_pending_roundtrip.c @@ -48,6 +48,7 @@ #include #include +#include #include "tls_test_util.h" @@ -136,6 +137,32 @@ static int make_csr(const char* subject, WolfCertKey** out_key, return wolfcert_csr_build(*out_key, &meta, out_csr) == WOLFCERT_OK ? 0 : 1; } +/* POST a base64 CSR body as given and return the response status code. */ +static int post_enroll_raw(uint16_t port, const byte* b64, word32 b64_len) +{ + TestTlsConn c; + char hdr[256]; + char resp[64] = { 0 }; + int n; + + n = snprintf(hdr, sizeof(hdr), + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Type: application/pkcs10\r\n" + "Content-Length: %u\r\nConnection: close\r\n\r\n", + (unsigned)b64_len); + if (n <= 0 || (size_t)n >= sizeof(hdr) || + test_tls_connect(&c, port, g_ca, g_ca_len) != 0) + return -1; + if (test_tls_write(&c, hdr, (size_t)n) != 0 || + test_tls_write(&c, b64, b64_len) != 0 || + test_tls_read(&c, resp, sizeof(resp) - 1) < 12) { + test_tls_close(&c); + return -1; + } + test_tls_close(&c); + return atoi(resp + 9); +} + static int pending_path(WolfCertServer* s) { char url[128]; @@ -188,6 +215,39 @@ static int pending_path(WolfCertServer* s) wolfSSL_CertManagerFree(cm); wolfcert_est_result_free(&r2); + + /* ---- A malformed or forged CSR is rejected, never parked ---- */ + static const uint8_t not_a_csr[] = "this is not a PKCS#10 request"; + WolfCertEstResult bad = { 0 }; + rc = wolfcert_est_simple_enroll_ex(&cli, not_a_csr, sizeof(not_a_csr), + &bad); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(bad.status == WOLFCERT_EST_STATUS_FAILURE); + wolfcert_est_result_free(&bad); + + csr_ex.data[csr_ex.len - 1] ^= 0x01; + rc = wolfcert_est_simple_enroll_ex(&cli, csr_ex.data, csr_ex.len, &bad); + REQUIRE(rc == WOLFCERT_ERR_HTTP); + REQUIRE(bad.status == WOLFCERT_EST_STATUS_FAILURE); + wolfcert_est_result_free(&bad); + + /* The queue matches the decoded CSR, whatever the base64 line wrapping. */ + WolfCertKey* dk_wrap = NULL; + WolfCertBuffer csr_wrap = { 0 }; + byte b64[4096]; + word32 b64_len = sizeof(b64); + REQUIRE(make_csr("CN=device-est-pending-rewrap", &dk_wrap, &csr_wrap) == 0); + REQUIRE(Base64_Encode(csr_wrap.data, (word32)csr_wrap.len, b64, + &b64_len) == 0); + REQUIRE(memchr(b64, '\n', b64_len - 1) != NULL); + REQUIRE(post_enroll_raw(wolfcert_server_port(s), b64, b64_len) == 202); + b64_len = sizeof(b64); + REQUIRE(Base64_Encode_NoNl(csr_wrap.data, (word32)csr_wrap.len, b64, + &b64_len) == 0); + REQUIRE(post_enroll_raw(wolfcert_server_port(s), b64, b64_len) == 200); + wolfcert_buffer_free(&csr_wrap); + wolfcert_key_free(dk_wrap); + wolfcert_buffer_free(&csr_ex); wolfcert_key_free(dk_ex); @@ -483,6 +543,7 @@ int main(void) .est_retry_after_sec = 1, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* s = NULL; REQUIRE(wolfcert_server_start(&cfg, &s) == WOLFCERT_OK); diff --git a/tests/integration/test_est_pha_roundtrip.c b/tests/integration/test_est_pha_roundtrip.c index 37fc07c..c7679a9 100644 --- a/tests/integration/test_est_pha_roundtrip.c +++ b/tests/integration/test_est_pha_roundtrip.c @@ -28,11 +28,13 @@ * 2. Call /simpleenroll - the server must trigger a CertificateRequest * via wolfSSL_request_certificate(); the client answers from the * pre-loaded identity and the CSR is issued. + * A raw TLS client then renews its cert with /simplereenroll over PHA, which + * the client API cannot do: only sessions offer PHA, and they have no reenroll. * * Negative controls: a session without a client identity, and one with an * identity but no PHA opt-in, must both fail /simpleenroll while /cacerts * still succeeds, as it must for an anonymous TLS 1.2 client. A client that - * never answers the CertificateRequest gets a 401 once the wait runs out. + * never answers the CertificateRequest gets a 403 once the wait runs out. */ #define _POSIX_C_SOURCE 200809L @@ -45,6 +47,7 @@ #include #include +#include #include #include "tls_test_util.h" @@ -67,6 +70,57 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +#ifdef WOLFSSL_POST_HANDSHAKE_AUTH +/* POST a renewal CSR for cli_cert over a PHA connection and expect a 200. */ +static int pha_reenroll(uint16_t port, const uint8_t* tls_cert, size_t tls_cert_len, + const uint8_t* cli_cert, size_t cli_cert_len, + const uint8_t* cli_key, size_t cli_key_len) +{ + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertCertMeta meta = { .subject_dn = "CN=factory-bootstrap" }; + WolfCertKey* dk = NULL; + WolfCertBuffer csr = { 0 }; + TestTlsConn c; + char req[4096]; + byte b64[3072]; + word32 b64_len = sizeof(b64); + char resp[64] = { 0 }; + int n; + + REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(dk, &meta, &csr) == WOLFCERT_OK); + wolfcert_key_free(dk); + REQUIRE(Base64_Encode_NoNl(csr.data, (word32)csr.len, b64, &b64_len) == 0); + wolfcert_buffer_free(&csr); + n = snprintf(req, sizeof(req), + "POST /.well-known/est/simplereenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Type: application/pkcs10\r\n" + "Content-Length: %u\r\nConnection: close\r\n\r\n%.*s", + (unsigned)b64_len, (int)b64_len, (const char*)b64); + REQUIRE(n > 0 && (size_t)n < sizeof(req)); + + /* On the CTX: wolfSSL unloads an SSL-level identity after the handshake. */ + REQUIRE(test_tls_setup(&c, port, tls_cert, tls_cert_len) == 0); + REQUIRE(wolfSSL_CTX_use_certificate_buffer(c.ctx, cli_cert, (long)cli_cert_len, + WOLFSSL_FILETYPE_PEM) == WOLFSSL_SUCCESS); + REQUIRE(wolfSSL_CTX_use_PrivateKey_buffer(c.ctx, cli_key, (long)cli_key_len, + WOLFSSL_FILETYPE_PEM) == WOLFSSL_SUCCESS); + wolfSSL_free(c.ssl); + c.ssl = wolfSSL_new(c.ctx); + REQUIRE(c.ssl != NULL && wolfSSL_set_fd(c.ssl, c.fd) == WOLFSSL_SUCCESS); + REQUIRE(wolfSSL_allow_post_handshake_auth(c.ssl) == 0); + REQUIRE(wolfSSL_connect(c.ssl) == WOLFSSL_SUCCESS); + REQUIRE(test_tls_write(&c, req, (size_t)n) == 0); + REQUIRE(test_tls_read(&c, resp, sizeof(resp) - 1) > 0); + test_tls_close(&c); + if (strncmp(resp, "HTTP/1.1 200", 12) != 0) + fprintf(stderr, "pha reenroll: %s\n", resp); + REQUIRE(strncmp(resp, "HTTP/1.1 200", 12) == 0); + return 0; +} +#endif + #ifdef WOLFSSL_POST_HANDSHAKE_AUTH static int discard_send(WOLFSSL* ssl, char* buf, int sz, void* ctx) { @@ -143,6 +197,7 @@ int main(void) WolfCertServerCfgSrv bad = cfg; bad.tls_client_ca_pem = NULL; bad.tls_client_ca_pem_len = 0; + bad.est_allow_anonymous_enroll = 1; REQUIRE(wolfcert_server_start(&bad, &srv) == WOLFCERT_ERR_BAD_ARG); REQUIRE(srv == NULL); @@ -248,7 +303,11 @@ int main(void) } #ifdef WOLFSSL_POST_HANDSHAKE_AUTH - /* --- A PHA client that never answers the CertificateRequest gets a 401 + /* --- /simplereenroll checks the CSR against the cert sent over PHA. */ + REQUIRE(pha_reenroll(wolfcert_server_port(srv), tls_cert, tls_cert_len, + cli_cert, cli_cert_len, cli_key, cli_key_len) == 0); + + /* --- A PHA client that never answers the CertificateRequest gets a 403 * once the server stops waiting, well before the request deadline. */ { static const char req[] = @@ -273,7 +332,7 @@ int main(void) /* Drop the reply wolfSSL_read() sends to the CertificateRequest. */ wolfSSL_SSLSetIOSend(c.ssl, discard_send); REQUIRE(test_tls_read(&c, resp, sizeof(resp) - 1) > 0); - REQUIRE(strncmp(resp, "HTTP/1.1 401", 12) == 0); + REQUIRE(strncmp(resp, "HTTP/1.1 403", 12) == 0); test_tls_close(&c); } #endif diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index 5771cb1..5434175 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -49,6 +49,8 @@ #include #include #include +#include +#include #define REQUIRE(cond) \ do { \ @@ -61,6 +63,7 @@ static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } +#ifdef KEEP_PEER_CERT static int impostor_customize(void* wolfssl_cert, void* ctx) { Cert* c = (Cert*)wolfssl_cert; @@ -124,12 +127,14 @@ static int same_public_key(const uint8_t* a, size_t a_len, const uint8_t* b, wc_FreeDecodedCert(&dc[1]); return ret; } +#endif /* Reenroll with a mismatched meta, with a renaming customize callback, then - * with a fresh key. */ -static int test_client_reenroll_keeps_identity(const char* url, - const uint8_t* tls_cert, - size_t tls_cert_len) + * with a fresh key, against a server that trusts the cert being renewed. */ +static int test_client_reenroll_keeps_identity(const uint8_t* tls_cert, + size_t tls_cert_len, + const uint8_t* tls_key, + size_t tls_key_len) { static const char* const impostor_dns[] = { "impostor.example" }; uint8_t* cur_cert = NULL; @@ -140,6 +145,17 @@ static int test_client_reenroll_keeps_identity(const char* url, WolfCertKey* out_key = NULL; WolfCertBuffer issued = { 0 }; WolfCertCertMeta meta; + WolfCertServerCfgSrv scfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + }; + WolfCertKeyCfg no_kcfg = { .type = (WolfCertKeyType)0x7f }; + WolfCertServer* srv = NULL; + pthread_t tid; + char url[128]; WolfCertServerCfg cli = { .protocol = WOLFCERT_PROTO_EST, .server_url = url, @@ -147,16 +163,25 @@ static int test_client_reenroll_keeps_identity(const char* url, .trust_anchors_len = tls_cert_len, .verify_server = 1, }; +#ifdef KEEP_PEER_CERT WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; int called = 0; +#endif REQUIRE(mint_self_id("reenroll-device", 0, &cur_cert, &cur_cert_len, &cur_key_pem, &cur_key_len) == 0); REQUIRE(wolfcert_key_from_pem(cur_key_pem, cur_key_len, NULL, &cur_key) == WOLFCERT_OK); + scfg.tls_client_ca_pem = cur_cert; + scfg.tls_client_ca_pem_len = cur_cert_len; + REQUIRE(wolfcert_server_start(&scfg, &srv) == WOLFCERT_OK); + REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); + snprintf(url, sizeof(url), "https://127.0.0.1:%u/.well-known/est", + wolfcert_server_port(srv)); + memset(&meta, 0, sizeof(meta)); meta.subject_dn = "CN=impostor"; REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, @@ -170,6 +195,14 @@ static int test_client_reenroll_keeps_identity(const char* url, cur_key, NULL, &meta, &out_key, &issued) == WOLFCERT_ERR_BAD_ARG); + /* Refused before key generation, which would fail UNSUPPORTED here. */ + memset(&meta, 0, sizeof(meta)); + meta.subject_dn = "CN=impostor"; + REQUIRE(wolfcert_client_reenroll(NULL, &cli, cur_cert, cur_cert_len, + cur_key, &no_kcfg, &meta, &out_key, + &issued) == WOLFCERT_ERR_BAD_ARG); + +#ifdef KEEP_PEER_CERT /* The caller's callback still runs but cannot rename the cert. */ memset(&meta, 0, sizeof(meta)); meta.customize = impostor_customize; @@ -190,7 +223,11 @@ static int test_client_reenroll_keeps_identity(const char* url, REQUIRE(check_renewed_identity(&issued) == 0); REQUIRE(same_public_key(issued.data, issued.len, cur_cert, cur_cert_len) == 0); +#endif + wolfcert_server_stop(srv); + pthread_join(tid, NULL); + wolfcert_server_free(srv); wolfcert_key_free(out_key); wolfcert_buffer_free(&issued); wolfcert_key_free(cur_key); @@ -199,6 +236,43 @@ static int test_client_reenroll_keeps_identity(const char* url, return 0; } +/* POST a junk CSR over serve_fd(), which carries no TLS, and expect `want`: + * " 403 " where the guard refuses it, " 400 " where the CA does. */ +static int serve_fd_enroll(const WolfCertServerCfgSrv* cfg, const char* auth, + const char* want) +{ + WolfCertServer* srv = NULL; + char req[512]; + char resp[512]; + ssize_t n; + int len; + int sv[2]; + int rc; + + len = snprintf(req, sizeof(req), + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Type: application/pkcs10\r\n%s" + "Content-Length: 4\r\nConnection: close\r\n\r\nAAAA", auth); + REQUIRE(len > 0 && (size_t)len < sizeof(req)); + REQUIRE(wolfcert_server_start(cfg, &srv) == WOLFCERT_OK); + REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + REQUIRE(write(sv[1], req, (size_t)len) == (ssize_t)len); + + rc = wolfcert_server_serve_fd(srv, sv[0]); + n = read(sv[1], resp, sizeof(resp) - 1); + close(sv[0]); + close(sv[1]); + wolfcert_server_free(srv); + + REQUIRE(n > 0); + resp[n] = '\0'; + if (strstr(resp, want) == NULL) + fprintf(stderr, "serve_fd enroll: wanted%s, got %.40s\n", want, resp); + REQUIRE(strstr(resp, want) != NULL); + REQUIRE(strcmp(want, " 403 ") != 0 || rc == WOLFCERT_ERR_AUTH); + return 0; +} + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -231,12 +305,51 @@ int main(void) == WOLFCERT_ERR_NOT_FOUND); wolfcert_store_memory_close(plain_store); + /* Nor may it start with no way to authenticate an enrolling client. */ + WolfCertServerCfgSrv open_cfg = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", + .bind_port = 0, + .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, + .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + }; + WolfCertServer* open_srv = NULL; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(open_srv == NULL); + + /* Empty Basic credentials would admit the public header "Basic Og==". */ + open_cfg.http_basic_user = ""; + open_cfg.http_basic_pass = ""; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + open_cfg.http_basic_user = "alice"; + open_cfg.http_basic_pass = NULL; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + open_cfg.http_basic_pass = ""; + REQUIRE(wolfcert_server_start(&open_cfg, &open_srv) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(open_srv == NULL); + + /* serve_fd() has no TLS: a client CA alone admits nobody, Basic or the + * anonymous opt-in let the request through to the CA. */ + open_cfg.http_basic_pass = "secret"; + REQUIRE(serve_fd_enroll(&open_cfg, + "Authorization: Basic YWxpY2U6c2VjcmV0\r\n", + " 400 ") == 0); + open_cfg.http_basic_user = NULL; + open_cfg.http_basic_pass = NULL; + open_cfg.est_allow_anonymous_enroll = 1; + REQUIRE(serve_fd_enroll(&open_cfg, "", " 400 ") == 0); + open_cfg.est_allow_anonymous_enroll = 0; + open_cfg.tls_client_ca_pem = tls_cert; + open_cfg.tls_client_ca_pem_len = tls_cert_len; + REQUIRE(serve_fd_enroll(&open_cfg, "", " 403 ") == 0); + WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_EST, .bind_host = "127.0.0.1", .bind_port = 0, .tls_cert_pem = tls_cert, .tls_cert_pem_len = tls_cert_len, .tls_key_pem = tls_key, .tls_key_pem_len = tls_key_len, + .est_allow_anonymous_enroll = 1, }; WolfCertServer* srv = NULL; REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); @@ -296,8 +409,8 @@ int main(void) wolfcert_buffer_free(&ca_pem_der); wc_FreeDer(&ta_der); - REQUIRE(test_client_reenroll_keeps_identity(url, tls_cert, tls_cert_len) - == 0); + REQUIRE(test_client_reenroll_keeps_identity(tls_cert, tls_cert_len, + tls_key, tls_key_len) == 0); wolfcert_server_stop(srv); pthread_join(tid, NULL); diff --git a/tests/integration/test_server_stop_idle.c b/tests/integration/test_server_stop_idle.c index 0a466bd..6a8fc6c 100644 --- a/tests/integration/test_server_stop_idle.c +++ b/tests/integration/test_server_stop_idle.c @@ -294,6 +294,7 @@ int main(void) cfg.tls_cert_pem_len = tls_cert_len; cfg.tls_key_pem = tls_key; cfg.tls_key_pem_len = tls_key_len; + cfg.est_allow_anonymous_enroll = 1; /* 1. Parked in wolfSSL_accept(): TCP is up, no ClientHello follows. */ memset(&ctx, 0, sizeof(ctx)); @@ -414,6 +415,7 @@ int main(void) cfg.tls_cert_pem_len = tls_cert_len; cfg.tls_key_pem = tls_key; cfg.tls_key_pem_len = tls_key_len; + cfg.est_allow_anonymous_enroll = 1; memset(&ctx, 0, sizeof(ctx)); REQUIRE(wolfcert_server_start(&cfg, &ctx.srv) == WOLFCERT_OK); diff --git a/tests/interop/est_globalsign.sh b/tests/interop/est_globalsign.sh index 4875378..93a916e 100755 --- a/tests/interop/est_globalsign.sh +++ b/tests/interop/est_globalsign.sh @@ -129,7 +129,7 @@ openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ -batch >/dev/null 2>&1 "$WC_SERVER" --proto est --listen "127.0.0.1:$WC_PORT" \ - --tls-cert srv/wc.crt --tls-key srv/wc.key \ + --tls-cert srv/wc.crt --tls-key srv/wc.key --est-allow-anonymous \ >wc-server.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT diff --git a/tests/interop/est_libest.sh b/tests/interop/est_libest.sh index eed8dbd..539fdb2 100755 --- a/tests/interop/est_libest.sh +++ b/tests/interop/est_libest.sh @@ -124,7 +124,7 @@ openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ -addext "subjectAltName=IP:127.0.0.1" >/dev/null 2>&1 "$WC_SERVER" --proto est --listen "127.0.0.1:$EST_PORT" \ - --tls-cert wc-srv.crt --tls-key wc-srv.key \ + --tls-cert wc-srv.crt --tls-key wc-srv.key --est-allow-anonymous \ >wc-server.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT diff --git a/tests/interop/openssl_pkcs7_xcheck.sh b/tests/interop/openssl_pkcs7_xcheck.sh index 8b0675c..6feaa59 100755 --- a/tests/interop/openssl_pkcs7_xcheck.sh +++ b/tests/interop/openssl_pkcs7_xcheck.sh @@ -49,7 +49,7 @@ echo "[1] EST /cacerts -> OpenSSL pkcs7 parse" PORT=$(free_port) "$WC_SERVER" --proto est --listen "127.0.0.1:$PORT" \ --tls-cert tls-server.crt --tls-key tls-server.key \ - >wc-server.log 2>&1 & + --est-allow-anonymous >wc-server.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT wait_port 127.0.0.1 "$PORT" @@ -122,7 +122,7 @@ echo "[3] OpenSSL-generated CSR -> wolfcert-server EST enroll" PORT=$(free_port) "$WC_SERVER" --proto est --listen "127.0.0.1:$PORT" \ --tls-cert tls-server.crt --tls-key tls-server.key \ - >wc-server3.log 2>&1 & + --est-allow-anonymous >wc-server3.log 2>&1 & WC_PID=$! trap 'kill_if "$WC_PID"' EXIT wait_port 127.0.0.1 "$PORT" diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index 01007a1..c771e6c 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -30,6 +30,7 @@ #include #include +#include #include #define REQUIRE(cond) \ @@ -632,6 +633,224 @@ static int csr_carries_large_san(void) wolfcert_key_free(key); return 0; } + +/* Write a DER tag and length; returns the header size. */ +static word32 der_hdr(byte* out, byte tag, word32 len) +{ + word32 n = 0; + + out[n++] = tag; + if (len >= 0x100) { + out[n++] = 0x82; + out[n++] = (byte)(len >> 8); + } + else if (len >= 0x80) { + out[n++] = 0x81; + } + out[n++] = (byte)len; + return n; +} + +/* Wrap the len bytes at buf in a tag and length; returns the new length. */ +static word32 der_wrap(byte* buf, word32 len, byte tag) +{ + byte hdr[4]; + word32 n = der_hdr(hdr, tag, len); + + memmove(buf + n, buf, len); + memcpy(buf, hdr, n); + return n + len; +} + +#define OU_VALUE_MAX 250 + +/* Encoded size of one OU RDN holding v value bytes. */ +static word32 ou_rdn_len(word32 v) +{ + byte hdr[4]; + word32 atv = 5 + der_hdr(hdr, ASN_UTF8STRING, v) + v; + word32 seq = der_hdr(hdr, ASN_SEQUENCE | ASN_CONSTRUCTED, atv) + atv; + + return der_hdr(hdr, ASN_SET | ASN_CONSTRUCTED, seq) + seq; +} + +/* The value size of the one OU RDN that encodes to exactly len bytes, or 0. */ +static word32 ou_value_for(word32 len) +{ + word32 v; + + for (v = 1; v <= OU_VALUE_MAX; v++) { + if (ou_rdn_len(v) == len) + return v; + } + return 0; +} + +/* OU RDNs whose encoding is exactly len (>= 12) bytes, in few enough RDNs for + * the 16 an OPENSSL_EXTRA wolfSSL parses. */ +static word32 put_ou_rdns(byte* out, word32 len) +{ + static const byte ou_oid[] = { 0x06, 0x03, 0x55, 0x04, 0x0b }; + word32 p = 0; + word32 start; + word32 rest; + word32 v; + word32 w; + + while (p < len) { + v = ou_value_for(len - p); + for (w = OU_VALUE_MAX; v == 0 && w > 0; w--) { + if (ou_rdn_len(w) + 12 <= len - p) { + rest = len - p - ou_rdn_len(w); + if (rest > ou_rdn_len(OU_VALUE_MAX) || ou_value_for(rest) != 0) + v = w; + } + } + start = p; + memcpy(out + p, ou_oid, sizeof(ou_oid)); + p += (word32)sizeof(ou_oid); + p += der_hdr(out + p, ASN_UTF8STRING, v); + memset(out + p, 'a', v); + p += v; + p = start + der_wrap(out + start, p - start, + ASN_SEQUENCE | ASN_CONSTRUCTED); + p = start + der_wrap(out + start, p - start, ASN_SET | ASN_CONSTRUCTED); + } + return p; +} + +/* A GeneralNames of dNSNames that is exactly len (>= 260) bytes; returns the + * number of names. */ +static int put_dns_names(byte* out, word32 len) +{ + word32 p = der_hdr(out, ASN_SEQUENCE | ASN_CONSTRUCTED, len - 4); + word32 v; + int n = 0; + + while (p < len) { + v = (len - p >= 105) ? 100 : len - p - 2; + out[p++] = ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE; + out[p++] = (byte)v; + memset(out + p, 'a', v); + p += v; + n++; + } + return n; +} + +/* An unsigned cert whose Subject contents are subj_len bytes and whose SAN, + * if san_len is not 0, is san_len bytes; *names gets the SAN's name count. */ +static int make_sized_cert(const WolfCertKey* key, word32 subj_len, + word32 san_len, byte* out, int* names) +{ + static const byte tbs_head[] = { + 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x01, 0x01, + 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, + 0x30, 0x0c, 0x31, 0x0a, 0x30, 0x08, 0x06, 0x03, 0x55, 0x04, 0x03, + 0x0c, 0x01, 'i', + 0x30, 0x1e, + 0x17, 0x0d, '2', '6', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', + 'Z', + 0x17, 0x0d, '3', '6', '0', '1', '0', '1', '0', '0', '0', '0', '0', '0', + 'Z' + }; + static const byte sig_tail[] = { + 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, + 0x03, 0x09, 0x00, 0x30, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01 + }; + static const byte san_oid[] = { 0x06, 0x03, 0x55, 0x1d, 0x11 }; + word32 p = (word32)sizeof(tbs_head); + word32 start; + word32 ext; + int spki; + + memcpy(out, tbs_head, sizeof(tbs_head)); + start = p; + p += put_ou_rdns(out + p, subj_len); + p = start + der_wrap(out + start, p - start, + ASN_SEQUENCE | ASN_CONSTRUCTED); + spki = wc_EccPublicKeyToDer((ecc_key*)key->impl, out + p, 512, 1); + if (spki <= 0) + return -1; + p += (word32)spki; + + if (san_len != 0) { + ext = p; + memcpy(out + p, san_oid, sizeof(san_oid)); + p += (word32)sizeof(san_oid); + *names = put_dns_names(out + p, san_len); + p += der_wrap(out + p, san_len, ASN_OCTET_STRING); + p = ext + der_wrap(out + ext, p - ext, ASN_SEQUENCE | ASN_CONSTRUCTED); + p = ext + der_wrap(out + ext, p - ext, ASN_SEQUENCE | ASN_CONSTRUCTED); + p = ext + der_wrap(out + ext, p - ext, + ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | 3); + } + + p = der_wrap(out, p, ASN_SEQUENCE | ASN_CONSTRUCTED); + memcpy(out + p, sig_tail, sizeof(sig_tail)); + p += (word32)sizeof(sig_tail); + return (int)der_wrap(out, p, ASN_SEQUENCE | ASN_CONSTRUCTED); +} + +/* The largest Subject and SAN a renewal carries, and one byte past each. */ +static int renewal_size_limits(void) +{ + const word32 name_max = (word32)sizeof(((Cert*)NULL)->sbjRaw) - 1; + const word32 san_max = (word32)sizeof(((Cert*)NULL)->altNames); + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer csr = { 0 }; + DecodedCert cc; + DecodedCert dc; + byte* cert; + int cert_len; + int names = 0; + int same; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE((cert = (byte*)malloc(name_max + san_max + 1024)) != NULL); + + cert_len = make_sized_cert(key, name_max, 0, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + wc_InitDecodedCert(&cc, cert, (word32)cert_len, NULL); + wc_InitDecodedCert(&dc, csr.data, (word32)csr.len, NULL); + same = wc_ParseCert(&cc, CERT_TYPE, NO_VERIFY, NULL) == 0 && + wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0 && + cc.subjectRawLen == (int)name_max && + dc.subjectRawLen == (int)name_max && + memcmp(cc.subjectRaw, dc.subjectRaw, name_max) == 0; + wc_FreeDecodedCert(&dc); + wc_FreeDecodedCert(&cc); + REQUIRE(same); + wolfcert_buffer_free(&csr); + + cert_len = make_sized_cert(key, name_max + 1, 0, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(csr.data == NULL); + + cert_len = make_sized_cert(key, 12, san_max, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_OK); + REQUIRE(count_csr_sans(&csr) == names); + wolfcert_buffer_free(&csr); + + cert_len = make_sized_cert(key, 12, san_max + 1, cert, &names); + REQUIRE(cert_len > 0); + REQUIRE(wolfcert_csr_build_ex(key, &meta, cert, (size_t)cert_len, &csr) + == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(csr.data == NULL); + + free(cert); + wolfcert_key_free(key); + return 0; +} #endif int main(void) @@ -662,6 +881,8 @@ int main(void) return 1; if (csr_carries_large_san()) return 1; + if (renewal_size_limits()) + return 1; if (build_and_reparse(WOLFCERT_KEY_ECC, 256)) return 1; #endif diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index b7a16da..6b870ce 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -789,6 +789,103 @@ static int check_result_defined(const char* what, int rc, const WolfCertScepResu return 0; } +/* OPENSSL_EXTRA's GetCertName reports a failed X509_NAME allocation as + * ASN_PARSE_E, indistinguishable from a bad certificate. */ +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) && !defined(OPENSSL_EXTRA) && \ + !defined(OPENSSL_EXTRA_X509_SMALL) +#define TEST_ALLOC_FAILURES +#endif + +#ifdef TEST_ALLOC_FAILURES +/* Allocations left before failing_malloc() returns NULL; -1 never fails. */ +static int g_allocs_left = -1; + +static void* failing_malloc(size_t sz) +{ + if (g_allocs_left == 0) + return NULL; + if (g_allocs_left > 0) + g_allocs_left--; + return malloc(sz); +} + +static void failing_free(void* ptr) +{ + free(ptr); +} + +static void* failing_realloc(void* ptr, size_t sz) +{ + if (g_allocs_left == 0) + return NULL; + if (g_allocs_left > 0) + g_allocs_left--; + return realloc(ptr, sz); +} + +/* Failing each allocation in turn must give WOLFCERT_ERR_MEMORY, never a + * "not found" that GetCert would blame on the CA. */ +static int pem_has_cert_reports_oom(const char* pem, size_t pem_len, + const DecodedCert* lc) +{ + wolfSSL_Malloc_cb mf; + wolfSSL_Free_cb ff; + wolfSSL_Realloc_cb rf; + int fails; + int rc = 0; + + REQUIRE(wolfSSL_GetAllocators(&mf, &ff, &rf) == 0); + for (fails = 0; rc != 1; fails++) { + g_allocs_left = fails; + REQUIRE(wolfSSL_SetAllocators(failing_malloc, failing_free, + failing_realloc) == 0); + rc = wolfcert_scep_pem_has_cert((const uint8_t*)pem, pem_len, + lc->issuerRaw, + (size_t)lc->issuerRawLen, lc->serial, + (size_t)lc->serialSz, NULL); + REQUIRE(wolfSSL_SetAllocators(mf, ff, rf) == 0); + g_allocs_left = -1; + if (rc != 1 && rc != WOLFCERT_ERR_MEMORY) { + fprintf(stderr, "FAIL pem_has_cert with allocation %d failing: " + "%d\n", fails, rc); + return 1; + } + } + REQUIRE(fails > 1); + return 0; +} + +/* Running out of memory while checking a CertRep signer is not a forgery. */ +static int rep_signer_reports_oom(const uint8_t* signer, size_t signer_len, + const uint8_t* bundle, size_t bundle_len) +{ + wolfSSL_Malloc_cb mf; + wolfSSL_Free_cb ff; + wolfSSL_Realloc_cb rf; + int fails; + int rc = WOLFCERT_ERR_MEMORY; + + REQUIRE(wolfSSL_GetAllocators(&mf, &ff, &rf) == 0); + for (fails = 0; rc != WOLFCERT_OK; fails++) { + g_allocs_left = fails; + REQUIRE(wolfSSL_SetAllocators(failing_malloc, failing_free, + failing_realloc) == 0); + rc = wolfcert_scep_verify_rep_signer(signer, signer_len, bundle, + bundle_len, NULL); + REQUIRE(wolfSSL_SetAllocators(mf, ff, rf) == 0); + g_allocs_left = -1; + if (rc != WOLFCERT_OK && rc != WOLFCERT_ERR_MEMORY) { + fprintf(stderr, "FAIL verify_rep_signer with allocation %d " + "failing: %d\n", fails, rc); + return 1; + } + } + REQUIRE(fails > 1); + return 0; +} +#endif + /* The GetCert response check walks a PEM bundle: a certificate that will not * parse is skipped, so one ahead of the target cannot hide it. */ static int test_pem_has_cert(void) @@ -821,6 +918,9 @@ static int test_pem_has_cert(void) REQUIRE(wolfcert_scep_pem_has_cert((const uint8_t*)pem, (size_t)n, lc.issuerRaw, (size_t)lc.issuerRawLen, lc.serial, (size_t)lc.serialSz, NULL) == 1); +#ifdef TEST_ALLOC_FAILURES + REQUIRE(pem_has_cert_reports_oom(pem, (size_t)n, &lc) == 0); +#endif /* Behind an unparseable entry it must still be found. */ REQUIRE((size_t)n + sizeof(JUNK) < sizeof(bundle)); @@ -1592,6 +1692,9 @@ static int test_signer_matches_any_bundle_cert(void) REQUIRE(wolfcert_scep_verify_rep_signer(u_der, u_len, bundle, bundle_len, NULL) != WOLFCERT_OK); +#ifdef TEST_ALLOC_FAILURES + REQUIRE(rep_signer_reports_oom(b_der, b_len, bundle, bundle_len) == 0); +#endif free(bundle); free(a_der); diff --git a/tests/unit/test_server_ca_store.c b/tests/unit/test_server_ca_store.c index 7fecb9d..9f2d72d 100644 --- a/tests/unit/test_server_ca_store.c +++ b/tests/unit/test_server_ca_store.c @@ -72,6 +72,7 @@ static void ca_store_cfg(WolfCertServerCfgSrv* cfg, WolfCertStoreOps* store) cfg->tls_cert_pem_len = srv_cert_pem_len; cfg->tls_key_pem = srv_key_pem; cfg->tls_key_pem_len = srv_key_pem_len; + cfg->est_allow_anonymous_enroll = 1; #endif cfg->bind_host = "127.0.0.1"; cfg->ca_store = store; @@ -415,6 +416,78 @@ static int test_ca_persists_across_starts(void) return 0; } +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) +/* Index of the one allocation fail_one_malloc() refuses; -1 refuses none. */ +static int g_fail_at = -1; +static int g_allocs; + +static void* fail_one_malloc(size_t sz) +{ + return g_allocs++ == g_fail_at ? NULL : malloc(sz); +} + +static void fail_one_free(void* ptr) +{ + free(ptr); +} + +static void* fail_one_realloc(void* ptr, size_t sz) +{ + return g_allocs++ == g_fail_at ? NULL : realloc(ptr, sz); +} + +/* A start that survives any single failed allocation keeps every credential. */ +static int test_start_oom_keeps_credentials(void) +{ + WolfCertStoreOps* store = wolfcert_store_memory_open(NULL); + WolfCertServerCfgSrv cfg; + WolfCertServer* srv = NULL; + wolfSSL_Malloc_cb mf; + wolfSSL_Free_cb ff; + wolfSSL_Realloc_cb rf; + int fail_at; + int rc; + + REQUIRE(store != NULL); + ca_store_cfg(&cfg, store); + cfg.http_basic_user = "alice"; + cfg.http_basic_pass = "secret"; + cfg.challenge_password = "otp"; + REQUIRE(wolfcert_server_start(&cfg, &srv) == WOLFCERT_OK); + wolfcert_server_free(srv); + + REQUIRE(wolfSSL_GetAllocators(&mf, &ff, &rf) == 0); + for (fail_at = 0; ; fail_at++) { + srv = NULL; + g_fail_at = fail_at; + g_allocs = 0; + REQUIRE(wolfSSL_SetAllocators(fail_one_malloc, fail_one_free, + fail_one_realloc) == 0); + rc = wolfcert_server_start(&cfg, &srv); + REQUIRE(wolfSSL_SetAllocators(mf, ff, rf) == 0); + if (rc != WOLFCERT_OK) { + REQUIRE(srv == NULL); + continue; + } + REQUIRE(srv->cfg_basic_user != NULL && + strcmp(srv->cfg_basic_user, "alice") == 0); + REQUIRE(srv->cfg_basic_pass != NULL && + strcmp(srv->cfg_basic_pass, "secret") == 0); + REQUIRE(srv->cfg_challenge != NULL && + strcmp(srv->cfg_challenge, "otp") == 0); + wolfcert_server_free(srv); + if (g_allocs <= fail_at) + break; + } + g_fail_at = -1; + REQUIRE(fail_at > 4); + + wolfcert_store_memory_close(store); + return 0; +} +#endif + /* Fill `store` with a freshly generated CA of `type` by letting a server start * against it, then hand back copies of the stored pair. */ /* Every compiled key type, so each algorithm's certificate-to-key check is @@ -543,6 +616,88 @@ static int generate_ca_into(WolfCertStoreOps* store, WolfCertKeyType type, return 0; } +/* Have the CA in store issue a CSR carrying san; *rc_out is the issue result. */ +static int issue_with_san(WolfCertStoreOps* store, const uint8_t* san, + size_t san_len, int* rc_out, uint8_t** issued, + size_t* issued_len) +{ + WolfCertCa ca; + WC_RNG rng; + test_signkey key; + Cert req; + uint8_t csr[4096]; + int csr_len; + + REQUIRE(wc_InitRng(&rng) == 0); + REQUIRE(test_signkey_make(&key, &rng) == 0); + REQUIRE(wc_InitCert(&req) == 0); + strncpy(req.subject.commonName, "san-leaf", CTC_NAME_SIZE - 1); + memcpy(req.altNames, san, san_len); + req.altNamesSz = (int)san_len; + req.sigType = TEST_CERT_SIGTYPE; + csr_len = test_sign_certreq(&req, csr, (int)sizeof(csr), &key, &rng); + test_signkey_free(&key); + wc_FreeRng(&rng); + REQUIRE(csr_len > 0); + + REQUIRE(wolfcert_ca_load(&ca, store, NULL) == WOLFCERT_OK); + *rc_out = wolfcert_ca_issue(&ca, csr, (size_t)csr_len, issued, issued_len); + wolfcert_ca_free(&ca); + return 0; +} + +/* The CA issues a CSR's SAN unchanged, entries in their order across types, + * and refuses the GeneralName forms it does not issue. */ +static int test_issued_san_verbatim(void) +{ + static const uint8_t san[] = { + 0x30, 0x20, + 0x82, 0x09, 'b', '.', 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x81, 0x0d, 'a', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', + 'm', + 0x87, 0x04, 127, 0, 0, 1 + }; + static const uint8_t dir_san[] = { + 0x30, 0x12, + 0xa4, 0x10, 0x30, 0x0e, 0x31, 0x0c, 0x30, 0x0a, 0x06, 0x03, 0x55, 0x04, + 0x03, 0x0c, 0x03, 'd', 'i', 'r' + }; + WolfCertStoreOps* store = wolfcert_store_memory_open(NULL); + WolfCertBuffer ca_cert = { 0 }; + WolfCertBuffer ca_key = { 0 }; + uint8_t* issued = NULL; + size_t issued_len = 0; + DecodedCert dc; + const byte* got = NULL; + word32 got_len = 0; + int rc = -1; + + REQUIRE(store != NULL); + REQUIRE(generate_ca_into(store, CA_KEY_TYPES[0], &ca_cert, &ca_key) == 0); + wolfcert_buffer_free(&ca_cert); + wolfcert_buffer_free(&ca_key); + + REQUIRE(issue_with_san(store, dir_san, sizeof(dir_san), &rc, &issued, + &issued_len) == 0); + REQUIRE(rc == WOLFCERT_ERR_UNSUPPORTED); + REQUIRE(issue_with_san(store, san, sizeof(san), &rc, &issued, + &issued_len) == 0); + wolfcert_store_memory_close(store); + REQUIRE(rc == WOLFCERT_OK); + + wc_InitDecodedCert(&dc, issued, (word32)issued_len, NULL); + rc = wc_ParseCert(&dc, CERT_TYPE, NO_VERIFY, NULL); + if (rc == 0) + rc = wolfcert_find_san(&dc, &got, &got_len); + if (rc == 0 && (got_len != sizeof(san) || + memcmp(got, san, sizeof(san)) != 0)) + rc = -1; + wc_FreeDecodedCert(&dc); + WOLFCERT_XFREE(issued, NULL); + REQUIRE(rc == 0); + return 0; +} + static int mismatched_ca_rejected(WolfCertKeyType type) { WolfCertStoreOps* src_a = wolfcert_store_memory_open(NULL); @@ -784,12 +939,19 @@ int main(void) return 1; if (test_ca_persists_across_starts()) return 1; +#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_STATIC_MEMORY) && \ + !defined(WOLFSSL_DEBUG_MEMORY) + if (test_start_oom_keeps_credentials()) + return 1; +#endif if (test_every_alg_reloads()) return 1; if (test_mismatched_ca_rejected()) return 1; if (test_corrupt_ca_cert_rejected()) return 1; + if (test_issued_san_verbatim()) + return 1; #ifdef WOLFCERT_HAVE_ECC if (test_leaf_ca_rejected()) return 1; diff --git a/tests/unit/test_transport.c b/tests/unit/test_transport.c index 612cc55..f190e2a 100644 --- a/tests/unit/test_transport.c +++ b/tests/unit/test_transport.c @@ -31,9 +31,11 @@ #include #include #include "../test_static_mem.h" +#include "internal.h" #include #include +#include #define REQUIRE(cond) \ do { \ @@ -662,6 +664,196 @@ static int test_nb_204_keeps_retry_after(void) return 0; } +static int retry_after_of(const char* value, int nb, int* out) +{ + WolfCertHttpResponse resp = { 0 }; + Peer p = { 0 }; + char raw[256]; + int rc; + + snprintf(raw, sizeof(raw), + "HTTP/1.1 503 Service Unavailable\r\nRetry-After: %s\r\n" + "Content-Length: 0\r\nConnection: keep-alive\r\n\r\n", value); + p.open_rc = WOLFCERT_ERR_IO; + rc = nb ? nb_fetch(&p, raw, &resp) : fetch(&p, raw, sizeof(raw), &resp); + *out = resp.retry_after_sec; + wolfcert_http_response_free(&resp); + + return rc; +} + +/* delay-seconds is digits alone, apart from trailing whitespace. */ +static int test_retry_after_delay_seconds(void) +{ + static const char* const bad[] = { "120junk", "12 0", "120s", "1.5" }; + size_t i; + int nb; + int sec; + + for (nb = 0; nb <= 1; nb++) { + REQUIRE(retry_after_of("120 \t", nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 120); + REQUIRE(retry_after_of("99999999999999999999999", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 86400); + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + REQUIRE(retry_after_of(bad[i], nb, &sec) == WOLFCERT_OK); + if (sec != 0) + fprintf(stderr, "accepted malformed delay \"%s\"\n", bad[i]); + REQUIRE(sec == 0); + } + } + + return 0; +} + +#ifndef NO_ASN_TIME +/* RFC 9110 section 5.6.7: IMF-fixdate, obsolete RFC 850 and asctime. */ +static void retry_after_date(char* out, size_t sz, time_t t, int form) +{ + static const char* const wd[] = { + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + }; + static const char* const wdl[] = { + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", + "Saturday" + }; + static const char* const mon[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + struct tm* g = gmtime(&t); + + if (form == 0) { + snprintf(out, sz, "%s, %02d %s %04d %02d:%02d:%02d GMT", + wd[g->tm_wday], g->tm_mday, mon[g->tm_mon], + g->tm_year + 1900, g->tm_hour, g->tm_min, g->tm_sec); + } + else if (form == 1) { + snprintf(out, sz, "%s, %02d-%s-%02d %02d:%02d:%02d GMT", + wdl[g->tm_wday], g->tm_mday, mon[g->tm_mon], + g->tm_year % 100, g->tm_hour, g->tm_min, g->tm_sec); + } + else { + snprintf(out, sz, "%s %s %2d %02d:%02d:%02d %04d", + wd[g->tm_wday], mon[g->tm_mon], g->tm_mday, + g->tm_hour, g->tm_min, g->tm_sec, g->tm_year + 1900); + } +} + +static int test_retry_after_http_date(void) +{ + static const char* const bad[] = { + "Sun, 06 Nov 2095 08:49", + "Sun, 06 Nov 2095 08:49:37", + "Sun, 06 Nov 2095 08:49:37 UTC", + "Sun, 06 Nov 2095 08:49:37 GMTx", + "Sun, 06 Nov 2095 08:49:37 GMT extra", + "Sun, 06 Nov 2095 24:00:00 GMT", + "Sun, 06 Foo 2095 08:49:37 GMT", + "Sun, 6 Nov 2095 08:49:37 GMT", + "Tue, 29 Feb 2095 08:49:37 GMT", + "Mon, 29 Feb 2100 08:49:37 GMT", + "Sunday, 06-Nov-95 08:49", + "Sun Nov 6 08:49:37", + "Sunday", + "", + }; + char date[64]; + size_t i; + time_t t = time(NULL); + int cy = gmtime(&t)->tm_year + 1900; + int form; + int nb; + int sec; + + for (nb = 0; nb <= 1; nb++) { + for (form = 0; form <= 2; form++) { + retry_after_date(date, sizeof(date), time(NULL) + 120, form); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec >= 110 && sec <= 120); + + retry_after_date(date, sizeof(date), time(NULL) - 120, form); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 0); + } + + /* RFC 850 years resolve against now, not a fixed 1970 pivot. */ + snprintf(date, sizeof(date), "Monday, 01-Jan-%02d 00:00:00 GMT", + (cy + 45) % 100); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 86400); + snprintf(date, sizeof(date), "Monday, 01-Jan-%02d 00:00:00 GMT", + (cy + 55) % 100); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 0); + + /* Exactly 50 calendar years ahead is still the future. */ + for (i = 0; i < 2; i++) { + time_t at = time(NULL) + (i == 0 ? -60 : 120); + struct tm g = *gmtime(&at); + + if (g.tm_mon == 1 && g.tm_mday == 29) + break; + strftime(date, sizeof(date), "Monday, %d-%b-", &g); + snprintf(date + strlen(date), sizeof(date) - strlen(date), + "%02d %02d:%02d:%02d GMT", (g.tm_year + 1900 + 50) % 100, + g.tm_hour, g.tm_min, g.tm_sec); + REQUIRE(retry_after_of(date, nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == (i == 0 ? 86400 : 0)); + } + + /* Future years throughout, so 0 can only mean the value was rejected. */ + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + REQUIRE(retry_after_of(bad[i], nb, &sec) == WOLFCERT_OK); + if (sec != 0) + fprintf(stderr, "accepted malformed date \"%s\"\n", bad[i]); + REQUIRE(sec == 0); + } + REQUIRE(retry_after_of("Thu, 29 Feb 2096 08:49:37 GMT", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 86400); + + REQUIRE(retry_after_of("Sun, 06 Nov 2094 08:49:37 GMT", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 86400); + REQUIRE(retry_after_of("172800", nb, &sec) == WOLFCERT_OK); + REQUIRE(sec == 86400); + REQUIRE(retry_after_of("Sun, 31 Nov 2094 08:49:37 GMT", nb, &sec) + == WOLFCERT_OK); + REQUIRE(sec == 0); + } + + return 0; +} + +static time_t unset_clock(time_t* t) +{ + if (t != NULL) + *t = 1000; + return 1000; +} + +/* A device clock still near 1970 must not turn every date into a day's wait. */ +static int test_retry_after_unset_clock(void) +{ + int sec = -1; + int rc; + + REQUIRE(wc_SetTimeCb(unset_clock) == 0); + rc = retry_after_of("Sun, 06 Nov 2094 08:49:37 GMT", 0, &sec); + wc_SetTimeCb(NULL); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(sec == 0); + REQUIRE(wc_SetTimeCb(unset_clock) == 0); + rc = retry_after_of("120", 1, &sec); + wc_SetTimeCb(NULL); + REQUIRE(rc == WOLFCERT_OK); + REQUIRE(sec == 120); + return 0; +} +#endif + static int test_nb_interim_then_final(void) { WolfCertHttpResponse resp = { 0 }; @@ -965,6 +1157,14 @@ int main(void) return 1; if (test_nb_204_keeps_retry_after()) return 1; + if (test_retry_after_delay_seconds()) + return 1; +#ifndef NO_ASN_TIME + if (test_retry_after_http_date()) + return 1; + if (test_retry_after_unset_clock()) + return 1; +#endif if (test_nb_interim_then_final()) return 1; if (test_eof_empty_body_is_null()) diff --git a/wolfcert/est.h b/wolfcert/est.h index c130fa2..90df444 100644 --- a/wolfcert/est.h +++ b/wolfcert/est.h @@ -145,7 +145,7 @@ WOLFCERT_API int wolfcert_csr_attrs_build(const WolfCertCsrAttrItem* items, /* ---- EST enrollment result (RFC 7030 section 4.2) ----------------------------- * * RFC 7030 section 4.2.3 lets a server respond to /simpleenroll (or - * /simplereenroll) with `202 Accepted` + `Retry-After: ` when the + * /simplereenroll) with `202 Accepted` + `Retry-After` when the * request has been accepted but the certificate is not yet ready - * typically because the deployment requires manual approval. This is the * EST analogue of SCEP's `pkiStatus=PENDING`. The client is expected to @@ -158,9 +158,9 @@ WOLFCERT_API int wolfcert_csr_attrs_build(const WolfCertCsrAttrItem* items, * the issued cert (PEM). * status == PENDING -> the call returns `WOLFCERT_OK`; `cert_pem` is * empty; `retry_after_sec` carries - * the server's hint (0 when the server did not - * send Retry-After, or sent it in the HTTP-date - * form which wolfCert does not yet parse). + * the server's hint in seconds (0 when the + * server did not send a usable Retry-After; see + * `WolfCertHttpResponse.retry_after_sec`). * status == FAILURE -> any other HTTP status came back; the call returns * `WOLFCERT_ERR_AUTH` for 401/403, else * `WOLFCERT_ERR_HTTP`. @@ -187,9 +187,9 @@ typedef struct { /* Populated and owned iff status == SUCCESS. */ WolfCertBuffer cert_pem; /* Server's suggested wait before the client should re-POST. Only - * meaningful when status == PENDING. 0 when the server did not send - * `Retry-After`; callers may apply their own backoff policy in that - * case. */ + * meaningful when status == PENDING. 0 when the server sent no usable + * `Retry-After` (see `WolfCertHttpResponse.retry_after_sec`); callers may + * apply their own backoff policy in that case. */ int retry_after_sec; void* heap; } WolfCertEstResult; diff --git a/wolfcert/http.h b/wolfcert/http.h index b926551..c5731f7 100644 --- a/wolfcert/http.h +++ b/wolfcert/http.h @@ -76,10 +76,8 @@ typedef struct { /* May be NULL when body_len is 0; check body_len, not body. */ uint8_t* body; size_t body_len; - /* `Retry-After` header parsed as delta-seconds (RFC 7231 section 7.1.3). - * Populated for any response that carries the header; 0 means the - * header was absent or wasn't in the delta-seconds form. The HTTP - * date form is not supported. */ + /* `Retry-After` in seconds, capped at 86400 (RFC 9110 10.2.3); 0 if absent, + * malformed, past, or a date under NO_ASN_TIME or an unset clock. */ int retry_after_sec; void* heap; } WolfCertHttpResponse; diff --git a/wolfcert/scep.h b/wolfcert/scep.h index 231fbf5..cb7a23c 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -237,7 +237,8 @@ WOLFCERT_API int wolfcert_scep_get_cert_initial(const WolfCertServerCfg* srv, * trip that verifies but answers with no certificate carrying the requested * issuer and serial is the one outcome that is neither: it returns * WOLFCERT_ERR_PROTOCOL and leaves out->status UNSET, so a substituted bundle - * is never mistaken for the certificate that was asked for. + * is never mistaken for the certificate that was asked for. Running out of + * memory while searching the reply returns WOLFCERT_ERR_MEMORY instead. * * Despite the name, wolfcert_scep_get_cert_initial is not an "initial" variant * of this call: it is messageType 20, polling a pending enrollment. There is no diff --git a/wolfcert/server.h b/wolfcert/server.h index 7fee4d4..1a2e6e2 100644 --- a/wolfcert/server.h +++ b/wolfcert/server.h @@ -46,7 +46,7 @@ typedef struct { to an ephemeral CA. */ const char* challenge_password; /* SCEP challengePassword to accept; NULL disables */ const char* http_basic_user; /* EST HTTP Basic credentials to accept; NULL disables */ - const char* http_basic_pass; + const char* http_basic_pass; /* must be non-empty when http_basic_user is set */ /* CA configuration. NULL/zero values fall back to library defaults. */ WolfCertKeyType ca_key_type; /* WOLFCERT_KEY_RSA default */ @@ -56,6 +56,8 @@ typedef struct { * terminates TLS on every accepted connection before dispatching to * the protocol handler. tls_client_ca_pem, when set, enables mutual * TLS (WOLFSSL_VERIFY_PEER) against the supplied client-CA bundle. + * EST /simplereenroll needs it (else 403) and a wolfSSL built with + * KEEP_PEER_CERT (else 500). * * Mandatory for WOLFCERT_PROTO_EST, which RFC 7030 section 3.1 defines * over TLS only: wolfcert_server_start() returns WOLFCERT_ERR_TLS @@ -100,11 +102,11 @@ typedef struct { /* EST manual-approval mode (RFC 7030 section 4.2.3). When set, the first * /simpleenroll or /simplereenroll POST for a given CSR returns * `202 Accepted` with a `Retry-After: ` header; - * the next POST with the same CSR body issues the certificate - * normally. Server-side state is keyed on the SHA-256 of the CSR - * body so the client must re-POST an identical request - which is - * what `wolfcert_est_simple_enroll_ex` does when a caller loops on - * the PENDING status. + * the next POST with the same CSR issues the certificate normally, so + * the client must re-POST the same CSR - which is what + * `wolfcert_est_simple_enroll_ex` does when a caller loops on the + * PENDING status. A CSR that does not decode or whose signature does + * not verify is answered with 400 and never parked. * * `est_retry_after_sec` is the value emitted in the `Retry-After` * header; defaults to 1 when zero. This is a test-server @@ -145,6 +147,9 @@ typedef struct { /* Heap hint for server-internal allocations. */ void* heap; + + /* Lets EST start with neither Basic nor tls_client_ca_pem. */ + int est_allow_anonymous_enroll; } WolfCertServerCfgSrv; WOLFCERT_API int wolfcert_server_start(const WolfCertServerCfgSrv* cfg, WolfCertServer** out); @@ -160,7 +165,8 @@ WOLFCERT_API uint16_t wolfcert_server_port(const WolfCertServer* srv); /* Embed wolfCert's protocol handling in an existing event loop: hand the * library an already-accepted connection; it services exactly one request * and returns, leaving the caller to close the fd. - * WOLFCERT_SERVER_REQUEST_TIMEOUT_MS does not apply. */ + * WOLFCERT_SERVER_REQUEST_TIMEOUT_MS does not apply. The fd carries no TLS, so + * EST enrollment on it needs http_basic_user or est_allow_anonymous_enroll. */ WOLFCERT_API int wolfcert_server_serve_fd(WolfCertServer* srv, int fd); #ifdef __cplusplus