diff --git a/src/est/csr_attrs.c b/src/est/csr_attrs.c index 49f6bd9..5f7c2ee 100644 --- a/src/est/csr_attrs.c +++ b/src/est/csr_attrs.c @@ -73,7 +73,7 @@ static int der_take_tl(const uint8_t* p, size_t avail, hdr = 2 + n; } - if (hdr + len > avail) + if (len > avail - hdr) return WOLFCERT_ERR_PARSE; *out_len = len; diff --git a/src/est/est_client.c b/src/est/est_client.c index fc1b36f..193fa5b 100644 --- a/src/est/est_client.c +++ b/src/est/est_client.c @@ -592,12 +592,15 @@ int wolfcert_est_session_simple_enroll_nb(WolfCertEstSession* s, } s->in_req = (WolfCertHttpRequest){ - .method = "POST", .url = s->in_url, - .content_type = "application/pkcs10", - .accept = "application/pkcs7-mime", - .body = s->in_body.data, .body_len = s->in_body.len, - .max_response_bytes = s->max_body, - .heap = s->heap, + .method = "POST", + .url = s->in_url, + .content_type = "application/pkcs10", + .content_transfer_encoding = "base64", + .accept = "application/pkcs7-mime", + .body = s->in_body.data, + .body_len = s->in_body.len, + .max_response_bytes = s->max_body, + .heap = s->heap, }; s->in_out = out_cert_pem; s->in_op = EST_OP_SIMPLE_ENROLL; @@ -696,12 +699,15 @@ int wolfcert_est_session_simple_enroll(WolfCertEstSession* s, } WolfCertHttpRequest req = { - .method = "POST", .url = url, - .content_type = "application/pkcs10", - .accept = "application/pkcs7-mime", - .body = b64.data, .body_len = b64.len, - .max_response_bytes = s->max_body, - .heap = s->heap, + .method = "POST", + .url = url, + .content_type = "application/pkcs10", + .content_transfer_encoding = "base64", + .accept = "application/pkcs7-mime", + .body = b64.data, + .body_len = b64.len, + .max_response_bytes = s->max_body, + .heap = s->heap, }; WolfCertHttpResponse resp = { 0 }; rc = wolfcert_http_session_request(s->http, &req, &resp); diff --git a/src/est/est_server.c b/src/est/est_server.c index dbda31d..90dd88b 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -148,6 +148,113 @@ static int read_line(const char** p, const char* end, char** ls, size_t* ll) return 0; } +/* Parse the chunk-size line at raw[ri..]. On success store the chunk + * length in *csz and the offset just past its terminating CRLF in + * *next. The hex-digit count is capped at 8 so a crafted long size line + * cannot wrap the value past a later bounds check; 8 digits cover every + * chunk we accept (BODY_CAP is 1 MiB). Shared by the completeness scan + * and the decode pass so the two cannot drift apart. + * + * returns 0 on success + * returns 1 when the CRLF ending the size line has not arrived yet + * (need more bytes) + * returns -1 when the line is malformed */ +static int read_chunk_size(const uint8_t* raw, size_t raw_len, size_t ri, + size_t* csz, size_t* next) +{ + size_t he = ri; + size_t hex_digits = 0; + size_t v = 0; + int parsed = 0; + + while (he + 1 < raw_len && !(raw[he] == '\r' && raw[he+1] == '\n')) { + ++he; + } + + if (he + 1 >= raw_len) + return 1; /* size line not fully received yet */ + + for (size_t k = ri; k < he; ++k) { + char c = (char)raw[k]; + if (c == ';') + break; /* chunk-ext */ + + int d = (c >= '0' && c <= '9') ? c - '0' + : (c >= 'a' && c <= 'f') ? c - 'a' + 10 + : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; + if (d < 0 || ++hex_digits > 8) + return -1; /* bad hex digit or oversized length */ + + v = (v << 4) | (size_t)d; + parsed = 1; + } + if (parsed == 0) + return -1; /* empty chunk-size line */ + + *csz = v; + *next = he + 2; + + return 0; +} + +/* Walk chunk framing over the bytes received so far and report whether + * the message is complete, meaning a genuine zero-length chunk has been + * parsed at a chunk-header boundary. Returns 1 when reading can stop + * (complete, or the framing is malformed and the decode pass below will + * reject it) and 0 when more bytes are still needed. A substring scan + * for "0\r\n" cannot decide this: a chunk-size line that is a multiple + * of 16 and chunk data both contain those bytes. */ +static int chunked_body_complete(const uint8_t* raw, size_t raw_len) +{ + size_t ri = 0; + size_t ls = 0; + + while (ri < raw_len) { + size_t csz = 0; + size_t next = 0; + int r = read_chunk_size(raw, raw_len, ri, &csz, &next); + if (r > 0) + return 0; /* chunk-size line not fully received yet */ + if (r < 0) + return 1; /* malformed line - the decode pass rejects it */ + + /* read_chunk_size guarantees next <= raw_len, so ri stays + * <= raw_len and the raw_len - ri math below cannot underflow. */ + ri = next; + if (csz == 0) { + /* Last-chunk marker. RFC 7230: the message ends only after + * the CRLF terminating the trailer section (minimum + * "0\r\n\r\n"). Stopping at "0\r\n" would leave that CRLF + * unread on the socket and corrupt the next request on a + * keep-alive connection. Consume any trailer field lines up + * to the terminating blank line before declaring complete. */ + while (ri < raw_len) { + ls = ri; + while (ri + 1 < raw_len && + !(raw[ri] == '\r' && raw[ri + 1] == '\n')) { + ++ri; + } + + if (ri + 1 >= raw_len) + return 0; /* trailer line CRLF not fully received */ + if (ri == ls) + return 1; /* blank line terminates the trailers */ + + ri += 2; /* skip this trailer field line, scan the next */ + } + + return 0; /* trailer terminator not yet received */ + } + + if (raw_len - ri < 2 || csz > raw_len - ri - 2) + return 0; /* chunk payload plus trailing CRLF not yet received */ + + ri += csz + 2; + } + + return 0; +} + static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) { memset(out, 0, sizeof(*out)); @@ -266,20 +373,7 @@ static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) raw_cap = body_have; } - int saw_end = 0; - while (!saw_end) { - /* Any "0\r\n" at a chunk-header position marks the end. */ - for (size_t i = 0; i + 2 < raw_len; ++i) { - if (raw[i] == '0' && raw[i+1] == '\r' && raw[i+2] == '\n') { - /* Cheap heuristic: treat the last occurrence as the - * terminator. The decode pass below revalidates. */ - saw_end = 1; - } - } - - if (saw_end) - break; - + while (!chunked_body_complete(raw, raw_len)) { size_t grow = raw_len < 2048 ? 2048 : raw_len; if (raw_len + grow > BODY_CAP + 64 * 1024) { WOLFCERT_XFREE(raw, heap); @@ -312,51 +406,18 @@ static int parse_request(WolfCertServer* s, int fd, EstRequest* out, void* heap) size_t body_sz = 0, ri = 0; while (ri < raw_len) { - /* Locate the end of the chunk-size line. */ - size_t he = ri; - while (he + 1 < raw_len && !(raw[he] == '\r' && raw[he+1] == '\n')) { - ++he; - } - - if (he + 1 >= raw_len) - break; - - unsigned long csz = 0; - int parsed = 0; - size_t hex_digits = 0; - for (size_t k = ri; k < he; ++k) { - char c = (char)raw[k]; - if (c == ';') - break; /* chunk-ext */ - - int d = (c >= '0' && c <= '9') ? c - '0' - : (c >= 'a' && c <= 'f') ? c - 'a' + 10 - : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; - if (d < 0) { - parsed = -1; - break; - } - - /* Cap the hex-digit count so a crafted long chunk-size - * line can't silently wrap `csz` to a small value that - * then sneaks past the `ri + csz > raw_len` check. 8 - * digits cover every legitimate chunk we'd ever accept - * (BODY_CAP is 1 MiB). */ - if (++hex_digits > 8) { - parsed = -1; - break; - } - - csz = (csz << 4) | (unsigned long)d; - parsed = 1; - } - if (parsed <= 0) { + size_t csz = 0; + size_t next = 0; + int r = read_chunk_size(raw, raw_len, ri, &csz, &next); + if (r > 0) + break; /* chunk-size line not fully received */ + if (r < 0) { WOLFCERT_XFREE(body, heap); WOLFCERT_XFREE(raw, heap); return WOLFCERT_ERR_PROTOCOL; } - ri = he + 2; + ri = next; if (csz == 0) break; @@ -663,7 +724,7 @@ static int csr__take_tl(const uint8_t* p, size_t avail, h = 2 + n; } - if (h + l > avail) + if (l > avail - h) return -1; *len = l; diff --git a/src/internal.h b/src/internal.h index 71ed19d..2e48c0f 100644 --- a/src/internal.h +++ b/src/internal.h @@ -213,11 +213,12 @@ WOLFCERT_API const WolfCertServerOps* wolfcert_scep_server_ops(void); #if defined(WOLFCERT_BUILD_TESTING) /* Test-only fault injection for a started SCEP server: make it emit a CertRep - * without a recipientNonce, and/or sign the CertRep with a throwaway key - * instead of the CA key. Used to drive the client-side rejection paths. Call - * after wolfcert_server_start and before the client request. */ + * without a recipientNonce, sign the CertRep with a throwaway key instead of + * the CA key, and/or force the CertRep senderNonce RNG draw to fail. Used to + * drive the client-side rejection and server error paths. Call after + * wolfcert_server_start and before the client request. */ WOLFCERT_TEST_VIS void wolfcert_scep_server_set_faults(WolfCertServer* s, - int omit_recipient_nonce, int sign_with_wrong_key); + int omit_recipient_nonce, int sign_with_wrong_key, int rng_fail); #endif /* ---- error reporting --------------------------------------------------- */ diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index 2be0e9d..9ddf25c 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -81,13 +81,40 @@ static void fill_common(const WolfCertServerCfg* srv, WolfCertHttpRequest* req) /* ---- GetCACaps ---------------------------------------------------------- */ +/* RFC 8894 section 3.5.2: GetCACaps is a newline-delimited list of exact + * capability tokens. Match a whole line, not a substring, so an unknown + * token that merely contains a known one is not mistaken for it. */ static int has_cap(const char* body, size_t len, const char* needle) { - size_t nl = strlen(needle); - for (size_t i = 0; i + nl <= len; ++i) { - if (strncasecmp(body + i, needle, nl) == 0) { + size_t nl; + size_t i = 0; + + /* Guard both pointers before dereferencing either: strlen(needle) + * below and body[] indexing in the scan. `len` needs no separate + * bound - every body[] access is gated by `i < len`, and the + * strncasecmp only fires when the matched token length equals nl, + * so it never reads past body + len. */ + if (body == NULL || needle == NULL) + return 0; + + nl = strlen(needle); + + while (i < len) { + size_t start = i; + size_t tlen; + + while (i < len && body[i] != '\n') + ++i; + + tlen = i - start; + if (tlen > 0 && body[start + tlen - 1] == '\r') + --tlen; + + if (tlen == nl && strncasecmp(body + start, needle, nl) == 0) return 1; - } + + if (i < len) + ++i; } return 0; @@ -487,11 +514,20 @@ static int do_scep_round_trip(const WolfCertServerCfg* srv, return rc; WC_RNG rng; - wc_InitRng_ex(&rng, heap, WOLFCERT_DEVID_SOFTWARE); + if (wc_InitRng_ex(&rng, heap, WOLFCERT_DEVID_SOFTWARE) != 0) { + wolfcert_buffer_free(&env); + return WOLFCERT_ERR(WOLFCERT_ERR_CRYPTO, "scep", + "RNG init failed for transactionID/nonce"); + } uint8_t txid_gen[16], nonce[16]; - wc_RNG_GenerateBlock(&rng, txid_gen, sizeof(txid_gen)); - wc_RNG_GenerateBlock(&rng, nonce, sizeof(nonce)); + if (wc_RNG_GenerateBlock(&rng, txid_gen, sizeof(txid_gen)) != 0 || + wc_RNG_GenerateBlock(&rng, nonce, sizeof(nonce)) != 0) { + wc_FreeRng(&rng); + wolfcert_buffer_free(&env); + return WOLFCERT_ERR(WOLFCERT_ERR_CRYPTO, "scep", + "RNG failed generating transactionID/nonce"); + } wc_FreeRng(&rng); diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index d9477a1..f754270 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -89,6 +89,7 @@ typedef struct { * production build. wrong_ca is a throwaway signer generated on first use. */ int fault_omit_recipient_nonce; int fault_sign_with_wrong_key; + int fault_rng_fail; WolfCertCa wrong_ca; int wrong_ca_ready; #endif @@ -96,12 +97,13 @@ typedef struct { #if defined(WOLFCERT_BUILD_TESTING) WOLFCERT_TEST_VIS void wolfcert_scep_server_set_faults(WolfCertServer* s, - int omit_recipient_nonce, int sign_with_wrong_key) + int omit_recipient_nonce, int sign_with_wrong_key, int rng_fail) { ScepPriv* p = (ScepPriv*)s->priv; p->fault_omit_recipient_nonce = omit_recipient_nonce; p->fault_sign_with_wrong_key = sign_with_wrong_key; + p->fault_rng_fail = rng_fail; } #endif @@ -515,10 +517,25 @@ static int send_cert_rep(WolfCertServer* s, int fd, * so the signed pkiMessage is built with an absent pkcsPKIEnvelope. */ WC_RNG rng; - wc_InitRng_ex(&rng, s->heap, WOLFCERT_DEVID_SOFTWARE); + if (wc_InitRng_ex(&rng, s->heap, WOLFCERT_DEVID_SOFTWARE) != 0) { + wolfcert_buffer_free(&resp_env); + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return WOLFCERT_ERR_CRYPTO; + } uint8_t my_nonce[16]; - wc_RNG_GenerateBlock(&rng, my_nonce, sizeof(my_nonce)); + int nonce_rc = wc_RNG_GenerateBlock(&rng, my_nonce, sizeof(my_nonce)); +#if defined(WOLFCERT_BUILD_TESTING) + if (((ScepPriv*)s->priv)->fault_rng_fail) + nonce_rc = -1; +#endif + if (nonce_rc != 0) { + wc_FreeRng(&rng); + wolfcert_buffer_free(&resp_env); + send_text(s, fd, 500, "Server Error", "text/plain", ""); + return WOLFCERT_ERR_CRYPTO; + } + wc_FreeRng(&rng); /* RFC 8894 section 3.1: CertRep MUST carry messageType, pkiStatus, @@ -637,14 +654,20 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, if (signer_cert != NULL && signer_matches_csr(signer_cert, signer_cert_len, csr->data, csr->len, s->heap) != WOLFCERT_OK) { - send_text(s, fd, 400, "Signer/CSR Key Mismatch", "text/plain", ""); - return WOLFCERT_ERR_AUTH; + /* Report the failure as a CertRep, then close the connection. */ + s->keep_alive = 0; + return send_cert_rep(s, fd, NULL, 0, env_target, env_target_len, + tid, tid_len, snonce, snonce_len, + "2", "2" /* badRequest */); } if (check_challenge(csr->data, csr->len, s->cfg_challenge, s->heap) != WOLFCERT_OK) { - send_text(s, fd, 403, "Invalid Challenge", "text/plain", ""); - return WOLFCERT_ERR_AUTH; + /* Report the failure as a CertRep, then close the connection. */ + s->keep_alive = 0; + return send_cert_rep(s, fd, NULL, 0, env_target, env_target_len, + tid, tid_len, snonce, snonce_len, + "2", "2" /* badRequest */); } if (s->cfg.scep_require_approval) { diff --git a/src/store.c b/src/store.c index ce1974a..60f0bff 100644 --- a/src/store.c +++ b/src/store.c @@ -440,6 +440,7 @@ int wolfcert_store_read_key(WolfCertStoreOps* store, const char* key_name, return rc; rc = wolfcert_key_from_pem(pem.data, pem.len, store->heap, out_key); + wc_ForceZero(pem.data, (word32)pem.len); wolfcert_buffer_free(&pem); return rc; diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index bb6f223..7c2702d 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -29,6 +29,10 @@ * 2. Corrupt inter-chunk trailer (bytes where CRLF should be). * 3. Well-formed sanity baseline so the test fails loudly if the * server stops accepting chunked altogether. + * 4. Well-formed body split across three TCP segments with a + * chunk-size line ending in the bytes '0' '\r' '\n'. + * 5. Keep-alive correctness when the last-chunk trailer CRLF arrives + * in its own segment, so a following request is not corrupted. * * The target is `src/est/est_server.c`'s parse_request chunked path; * no TLS is involved so we can script the byte-exact request here. @@ -42,13 +46,19 @@ #include #include +#include "tls_test_util.h" /* TEST_ENROLL_KEY_TYPE / _PARAM */ + +#include /* Base64_Encode_NoNl */ + #include #include #include +#include #include #include #include #include +#include #include #define REQUIRE(cond) \ @@ -108,6 +118,18 @@ static int send_and_read_status(uint16_t port, return (int)n; } +/* Sleep helper that nudges the request segments below toward landing in + * distinct recv() calls rather than being coalesced into one buffer. + * Best-effort only: TCP guarantees no recv() boundaries, so this just + * makes the intended segmentation likely, not certain. */ +static void nap_ms(long ms) +{ + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +} + /* Shape #1: a chunk-size line longer than 8 hex digits. The parser * must reject this rather than letting the shift-accumulate silently * wrap. */ @@ -176,8 +198,208 @@ static int accept_wellformed_chunks(uint16_t port) return 0; } +/* Shape #4: a well-formed chunked body delivered across three TCP + * segments, with a chunk-size line ("10" = 16 bytes) that ends in the + * bytes '0' '\r' '\n'. A completion check that scans for "0\r\n" + * anywhere in the accumulated buffer trips on that size line the moment + * the first partial segment lands, stops reading before the rest of the + * body arrives, and the request is truncated. The framer must instead + * track chunk framing and keep reading until a genuine zero-length + * chunk, so the full body reaches the CSR layer and comes back + * "400 Bad CSR" rather than the "400 Bad Request" a truncated request + * produces. */ +static int accept_multisegment_chunked_body(uint16_t port) +{ + const char* hdr = + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + const char* seg2 = "10\r\nAAAA"; /* size line + 4/16 bytes */ + const char* seg3 = "AAAAAAAAAAAA\r\n0\r\n\r\n"; /* last 12 bytes + terminator */ + struct sockaddr_in sa = { .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + char status[128] = { 0 }; + size_t n = 0; + int cs = socket(AF_INET, SOCK_STREAM, 0); + + REQUIRE(cs >= 0); + REQUIRE(connect(cs, (struct sockaddr*)&sa, sizeof(sa)) == 0); + + REQUIRE(send(cs, hdr, strlen(hdr), 0) == (ssize_t)strlen(hdr)); + nap_ms(80); + (void)send(cs, seg2, strlen(seg2), 0); + nap_ms(80); + (void)send(cs, seg3, strlen(seg3), 0); + + while (n + 1 < sizeof(status)) { + ssize_t r = recv(cs, status + n, sizeof(status) - 1 - n, 0); + if (r <= 0) + break; + n += (size_t)r; + status[n] = '\0'; + if (memchr(status, '\n', n) != NULL) + break; + } + close(cs); + + REQUIRE(strstr(status, "Bad CSR") != 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. + * *head gets "\r\n\r\n\r\n0\r\n" and *tail gets the + * lone "\r\n". Both are heap-allocated; the caller frees them. */ +static int build_split_enroll(char** head, size_t* head_len, char** tail) +{ + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, + .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertCertMeta meta = { .subject_dn = "CN=keepalive-test" }; + WolfCertKey* key = NULL; + WolfCertBuffer csr = { 0 }; + byte* b64 = NULL; + word32 b64_len = 0; + char* buf = NULL; + char* trl = NULL; + size_t hdr_len = 0; + int rc; + + rc = wolfcert_key_generate(&kcfg, &key); + if (rc == WOLFCERT_OK) + rc = wolfcert_csr_build(key, &meta, &csr); + + if (rc == WOLFCERT_OK) { + b64_len = (word32)(((csr.len + 2) / 3) * 4 + 4); + b64 = (byte*)malloc(b64_len); + if (b64 == NULL || + Base64_Encode_NoNl(csr.data, (word32)csr.len, b64, &b64_len) != 0) + rc = WOLFCERT_ERR_CRYPTO; + } + + if (rc == WOLFCERT_OK) { + /* headers + chunk-size line + base64 body + CRLF + "0\r\n" */ + buf = (char*)malloc(512 + b64_len); + trl = strdup("\r\n"); /* trailer terminator, sent as its own segment */ + if (buf == NULL || trl == NULL) + rc = WOLFCERT_ERR_MEMORY; + } + + if (rc == WOLFCERT_OK) { + hdr_len = (size_t)snprintf(buf, 256, + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Content-Transfer-Encoding: base64\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "%x\r\n", (unsigned)b64_len); + memcpy(buf + hdr_len, b64, b64_len); + hdr_len += b64_len; + memcpy(buf + hdr_len, "\r\n0\r\n", 5); /* end chunk, last-chunk line */ + hdr_len += 5; + + *head = buf; + *head_len = hdr_len; + *tail = trl; + buf = NULL; /* ownership handed to caller */ + trl = NULL; + } + + free(b64); + free(buf); /* NULL on success; frees the partial build on failure */ + free(trl); + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + return (rc == WOLFCERT_OK) ? 0 : 1; +} + +/* Shape #5: keep-alive correctness when a chunked request's terminating + * trailer CRLF arrives in its own TCP segment. A framer that treats + * "0\r\n" as complete before that final CRLF stops one byte-pair short + * and leaves "\r\n" on the socket; the next request on the same + * keep-alive connection then parses the stray CRLF as an empty request + * line and comes back "400 Bad Request". The corruption is only + * observable once request #1 succeeds and keeps the connection alive, so + * request #1 enrolls a real CSR (the in-tree server issues against its + * generated CA -> "200"). Request #2 is a body-less GET so a mis-parse + * closes cleanly with the "400 Bad Request" visible (no reset). With the + * framer consuming the trailer terminator, request #2 reaches the + * cacerts handler and no "Bad Request" appears. */ +static int keepalive_after_split_trailer(uint16_t port) +{ + const char* req2 = + "GET /.well-known/est/cacerts HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Connection: close\r\n" + "\r\n"; + struct sockaddr_in sa = { .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + char* req1_head = NULL; + char* req1_tail = NULL; + size_t req1_head_len = 0; + char resp[4096] = { 0 }; + size_t n = 0; + int cs; + + REQUIRE(build_split_enroll(&req1_head, &req1_head_len, &req1_tail) == 0); + + cs = socket(AF_INET, SOCK_STREAM, 0); + REQUIRE(cs >= 0); + REQUIRE(connect(cs, (struct sockaddr*)&sa, sizeof(sa)) == 0); + + /* Request #1: last-chunk line first, trailer CRLF withheld into its + * own segment so a premature "0\r\n" completion leaves it unread. */ + REQUIRE(send(cs, req1_head, req1_head_len, 0) == (ssize_t)req1_head_len); + nap_ms(80); + REQUIRE(send(cs, req1_tail, strlen(req1_tail), 0) + == (ssize_t)strlen(req1_tail)); + + /* Wait for request #1's response head before sending request #2. */ + while (n + 1 < sizeof(resp)) { + ssize_t r = recv(cs, resp + n, sizeof(resp) - 1 - n, 0); + if (r <= 0) + break; + n += (size_t)r; + resp[n] = '\0'; + if (strstr(resp, "\r\n\r\n") != NULL) + break; + } + REQUIRE(strstr(resp, "200") != NULL); /* enrollment issued a cert */ + + REQUIRE(send(cs, req2, strlen(req2), 0) == (ssize_t)strlen(req2)); + + /* Drain until the server closes (request #2 asked for Connection: + * close), appending onto the same buffer. */ + while (n + 1 < sizeof(resp)) { + ssize_t r = recv(cs, resp + n, sizeof(resp) - 1 - n, 0); + if (r <= 0) + break; + n += (size_t)r; + resp[n] = '\0'; + } + close(cs); + free(req1_head); + free(req1_tail); + + /* Request #2 must have reached the cacerts handler, not the framer: + * a "Bad Request" means the stray trailer CRLF corrupted it. */ + REQUIRE(strstr(resp, "Bad Request") == NULL); + return 0; +} + int main(void) { + /* A truncated request makes the server respond and close while the + * client is still writing later segments; ignore the resulting + * SIGPIPE and read the response back instead. */ + signal(SIGPIPE, SIG_IGN); + REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); WolfCertServerCfgSrv cfg = { @@ -196,6 +418,10 @@ int main(void) rc = reject_corrupt_chunk_trailer(port); if (rc == 0) rc = accept_wellformed_chunks(port); + if (rc == 0) + rc = accept_multisegment_chunked_body(port); + if (rc == 0) + rc = keepalive_after_split_trailer(port); wolfcert_server_stop(srv); pthread_join(tid, NULL); diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 11c1dda..ac5fea1 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -35,14 +35,15 @@ #include "internal.h" /* whitebox SCEP pkiMessage helpers */ +#include +#include #include #include #include #include #include #include -#include -#include +#include #include #define REQUIRE(cond) \ @@ -185,9 +186,103 @@ static int check_get_fallback(const WolfCertServerCfg* cli, return rc; } +/* Minimal single-shot HTTP responder that answers any request with a + * caller-supplied GetCACaps body, so a test can drive capability parsing + * with a body the real server would never emit. */ +struct caps_ctx { int port; const char* body; }; + +static void* caps_srv_thread(void* arg) +{ + struct caps_ctx* cc = (struct caps_ctx*)arg; + int ls = socket(AF_INET, SOCK_STREAM, 0); + if (ls < 0) + return NULL; + int yes = 1; + setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(0), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + if (bind(ls, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + close(ls); + return NULL; + } + if (listen(ls, 1) < 0) { + close(ls); + return NULL; + } + socklen_t slen = sizeof(sa); + getsockname(ls, (struct sockaddr*)&sa, &slen); + cc->port = ntohs(sa.sin_port); + + int cs = accept(ls, NULL, NULL); + close(ls); + if (cs < 0) + return NULL; + + char buf[1024]; + size_t n = 0; + while (n < sizeof(buf) - 1) { + ssize_t r = recv(cs, buf + n, sizeof(buf) - 1 - n, 0); + if (r <= 0) + break; + n += (size_t)r; + buf[n] = '\0'; + if (strstr(buf, "\r\n\r\n") != NULL) + break; + } + + char resp[512]; + int rn = snprintf(resp, sizeof(resp), + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n\r\n%s", + strlen(cc->body), cc->body); + if (rn > 0) + send(cs, resp, (size_t)rn, 0); + shutdown(cs, SHUT_WR); + close(cs); + return NULL; +} + +/* RFC 8894 section 3.5.2: GetCACaps is a newline-delimited list of exact + * tokens. A future token that merely contains a known one as a substring + * (Renewal-Extra, AESGCM) must not be read as advertising that capability. */ +static int test_caps_token_matching(void) +{ + const char* caps_body = + "POSTPKIOperation\r\n" + "Renewal-Extra\r\n" + "AESGCM\r\n"; + struct caps_ctx cc = { .port = 0, .body = caps_body }; + pthread_t tid; + REQUIRE(pthread_create(&tid, NULL, caps_srv_thread, &cc) == 0); + for (int i = 0; i < 200 && cc.port == 0; ++i) { + const struct timespec ts = { 0, 5 * 1000 * 1000 }; + nanosleep(&ts, NULL); + } + REQUIRE(cc.port != 0); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/scep", cc.port); + WolfCertServerCfg cli = { .protocol = WOLFCERT_PROTO_SCEP, .server_url = url }; + WolfCertScepCaps caps = { 0 }; + REQUIRE(wolfcert_scep_get_ca_caps(&cli, &caps) == WOLFCERT_OK); + pthread_join(tid, NULL); + + /* Exact token still matches; substring-only lines do not. */ + REQUIRE(caps.post_pki_operation == 1); + REQUIRE(caps.renewal == 0); + REQUIRE(caps.aes == 0); + return 0; +} + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); + + if (test_caps_token_matching()) + return 1; + WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_SCEP, .bind_host = "127.0.0.1", .bind_port = 0 }; WolfCertServer* s = NULL; @@ -309,17 +404,19 @@ int main(void) WolfCertKey* dk2 = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk2) == WOLFCERT_OK); - /* 1) no challenge in CSR -> server 403 -> HTTP layer returns ERR_HTTP */ + /* 1) no challenge in CSR -> server answers with a signed CertRep FAILURE + * (RFC 8894 pkiStatus=2), which the client surfaces as ERR_PROTOCOL. + * A plain HTTP 4xx here would instead read back as ERR_HTTP. */ WolfCertCertMeta meta_none = { .subject_dn = "CN=chal-none" }; WolfCertBuffer csr_none = { 0 }; REQUIRE(wolfcert_csr_build(dk2, &meta_none, &csr_none) == WOLFCERT_OK); WolfCertBuffer out_none = { 0 }; REQUIRE(wolfcert_scep_pkcs_req(&cli2, &caps2, ca2_der->buffer, ca2_der->length, dk2, csr_none.data, csr_none.len, &out_none) - != WOLFCERT_OK); + == WOLFCERT_ERR_PROTOCOL); wolfcert_buffer_free(&csr_none); - /* 2) wrong challenge -> same rejection */ + /* 2) wrong challenge -> same CertRep FAILURE path */ WolfCertCertMeta meta_bad = { .subject_dn = "CN=chal-bad", .challenge_password = "battery-staple" }; WolfCertBuffer csr_bad = { 0 }; @@ -327,7 +424,7 @@ int main(void) WolfCertBuffer out_bad = { 0 }; REQUIRE(wolfcert_scep_pkcs_req(&cli2, &caps2, ca2_der->buffer, ca2_der->length, dk2, csr_bad.data, csr_bad.len, &out_bad) - != WOLFCERT_OK); + == WOLFCERT_ERR_PROTOCOL); wolfcert_buffer_free(&csr_bad); /* 3) correct challenge -> issuance succeeds */ @@ -421,7 +518,7 @@ int main(void) .bind_host = "127.0.0.1", .bind_port = 0 }; WolfCertServer* s3 = NULL; REQUIRE(wolfcert_server_start(&cfg3, &s3) == WOLFCERT_OK); - wolfcert_scep_server_set_faults(s3, 1 /* omit recipientNonce */, 0); + wolfcert_scep_server_set_faults(s3, 1 /* omit recipientNonce */, 0, 0); pthread_t tid3; REQUIRE(pthread_create(&tid3, NULL, server_thread, s3) == 0); @@ -466,7 +563,7 @@ int main(void) .bind_host = "127.0.0.1", .bind_port = 0 }; WolfCertServer* s4 = NULL; REQUIRE(wolfcert_server_start(&cfg4, &s4) == WOLFCERT_OK); - wolfcert_scep_server_set_faults(s4, 0, 1 /* sign with wrong key */); + wolfcert_scep_server_set_faults(s4, 0, 1 /* sign with wrong key */, 0); pthread_t tid4; REQUIRE(pthread_create(&tid4, NULL, server_thread, s4) == 0); @@ -501,6 +598,52 @@ int main(void) wolfcert_buffer_free(&out4); wolfcert_key_free(dk4); + /* ---- senderNonce RNG failure must abort, not leak stack ------------- + * If the RNG draw for the CertRep senderNonce fails, the server must not + * build a CertRep over an uninitialized buffer. It frees its scratch, + * answers HTTP 500, and returns an error, which the client sees as a + * transport failure. Drive a server whose nonce draw is forced to fail + * and confirm the enrollment does not succeed. This also exercises the + * error-path cleanup under the sanitizer builds. */ + WolfCertServerCfgSrv cfg5 = { .protocol = WOLFCERT_PROTO_SCEP, + .bind_host = "127.0.0.1", .bind_port = 0 }; + WolfCertServer* s5 = NULL; + REQUIRE(wolfcert_server_start(&cfg5, &s5) == WOLFCERT_OK); + wolfcert_scep_server_set_faults(s5, 0, 0, 1 /* RNG draw fails */); + pthread_t tid5; + REQUIRE(pthread_create(&tid5, NULL, server_thread, s5) == 0); + + char url5[128]; + snprintf(url5, sizeof(url5), "http://127.0.0.1:%u/scep", wolfcert_server_port(s5)); + WolfCertServerCfg cli5 = { .protocol = WOLFCERT_PROTO_SCEP, .server_url = url5 }; + + WolfCertScepCaps caps5 = { 0 }; + REQUIRE(wolfcert_scep_get_ca_caps(&cli5, &caps5) == WOLFCERT_OK); + WolfCertBuffer ca5_pem = { 0 }; + REQUIRE(wolfcert_scep_get_ca_cert(&cli5, &ca5_pem) == WOLFCERT_OK); + DerBuffer* ca5_der = NULL; + REQUIRE(wc_PemToDer(ca5_pem.data, (long)ca5_pem.len, CERT_TYPE, + &ca5_der, NULL, NULL, NULL) == 0); + + WolfCertKey* dk5 = NULL; + REQUIRE(wolfcert_key_generate(&kcfg, &dk5) == WOLFCERT_OK); + WolfCertCertMeta meta5 = { .subject_dn = "CN=scep-rng-fail" }; + WolfCertBuffer csr5 = { 0 }; + REQUIRE(wolfcert_csr_build(dk5, &meta5, &csr5) == WOLFCERT_OK); + WolfCertBuffer out5 = { 0 }; + REQUIRE(wolfcert_scep_pkcs_req(&cli5, &caps5, ca5_der->buffer, ca5_der->length, + dk5, csr5.data, csr5.len, &out5) + == WOLFCERT_ERR_HTTP); + + wolfcert_server_stop(s5); + pthread_join(tid5, NULL); + wolfcert_server_free(s5); + wc_FreeDer(&ca5_der); + wolfcert_buffer_free(&ca5_pem); + wolfcert_buffer_free(&csr5); + wolfcert_buffer_free(&out5); + wolfcert_key_free(dk5); + wc_FreeDer(&ca_der); wc_FreeDer(&issued_der); wolfcert_buffer_free(&ca_pem); diff --git a/tests/unit/test_est.c b/tests/unit/test_est.c index b5b26b9..a537971 100644 --- a/tests/unit/test_est.c +++ b/tests/unit/test_est.c @@ -40,6 +40,7 @@ #include "../integration/tls_test_util.h" #include +#include #include #include #include @@ -93,7 +94,9 @@ static int make_test_ca(uint8_t* out, size_t cap, size_t* out_len) /* EST is TLS-only (RFC 7030), so the mock responder terminates TLS using a * self-signed identity the client pins as its trust anchor. */ -struct srv_ctx { int port; uint8_t* body; size_t len; WOLFSSL_CTX* ctx; }; +struct srv_ctx { int port; uint8_t* body; size_t len; WOLFSSL_CTX* ctx; + char request[8192]; size_t request_len; + int post_missing_ctenc; }; static void tls_write_all(WOLFSSL* ssl, const void* buf, int len) { @@ -107,7 +110,7 @@ static void tls_write_all(WOLFSSL* ssl, const void* buf, int len) } } -static void handle_conn(int cs, const struct srv_ctx* sc) +static void handle_conn(int cs, struct srv_ctx* sc) { WOLFSSL* ssl = wolfSSL_new(sc->ctx); if (ssl == NULL) { @@ -156,6 +159,23 @@ static void handle_conn(int cs, const struct srv_ctx* sc) } } + /* Record the raw request so a test can inspect which headers the + * client emitted on the wire. The last connection wins. */ + if (n > 0) { + size_t cap = sizeof(sc->request) - 1; + size_t cpy = (size_t)n < cap ? (size_t)n : cap; + memcpy(sc->request, buf, cpy); + sc->request[cpy] = '\0'; + sc->request_len = cpy; + + /* Every base64 enrollment POST must advertise the encoding. Count + * any POST that does not, so a regression in either session enroll + * variant is caught, not only the last request captured above. */ + if (strncmp(sc->request, "POST", 4) == 0 && + strstr(sc->request, "Content-Transfer-Encoding: base64") == NULL) + ++sc->post_missing_ctenc; + } + char hdr[256]; int hn = snprintf(hdr, sizeof(hdr), "HTTP/1.1 200 OK\r\n" @@ -186,7 +206,7 @@ static void* srv_thread(void* arg) getsockname(ls, (struct sockaddr*)&sa, &slen); sc->port = ntohs(sa.sin_port); - for (int i = 0; i < 3; ++i) { + for (int i = 0; i < 5; ++i) { int cs = accept(ls, NULL, NULL); if (cs < 0) break; @@ -282,6 +302,30 @@ static int test_est_require_server_auth(void) return 0; } +/* Drive a non-blocking session enroll to completion, poll()ing on the + * session fd between WANT_READ / WANT_WRITE returns. */ +static int pump_simple_enroll(WolfCertEstSession* s, + const uint8_t* csr, size_t csr_len, + WolfCertBuffer* out) +{ + int fd = wolfcert_est_session_fd(s); + for (;;) { + int rc = wolfcert_est_session_simple_enroll_nb(s, csr, csr_len, out); + if (rc == WOLFCERT_OK) + return 0; + if (rc == WOLFCERT_ERR_WANT_READ || rc == WOLFCERT_ERR_WANT_WRITE) { + struct pollfd p = { + .fd = fd, + .events = (rc == WOLFCERT_ERR_WANT_WRITE) ? POLLOUT : POLLIN, + }; + if (poll(&p, 1, 5000) <= 0) + return -1; + continue; + } + return -1; + } +} + int main(void) { /* The mock TLS responder may wolfSSL_write() after the client has read its @@ -363,10 +407,42 @@ int main(void) csr.data, csr.len, &reenrolled) == WOLFCERT_OK); wolfcert_buffer_free(&reenrolled); + /* The session-based enroll base64-encodes the CSR too, so it must + * emit Content-Transfer-Encoding: base64 like the one-shot path + * (RFC 7030 section 4.2.1). */ + WolfCertEstSession* sess = NULL; + REQUIRE(wolfcert_est_session_open(&srv, &sess) == WOLFCERT_OK); + WolfCertBuffer sess_enrolled = { 0 }; + REQUIRE(wolfcert_est_session_simple_enroll(sess, csr.data, csr.len, + &sess_enrolled) == WOLFCERT_OK); + REQUIRE(memmem(sess_enrolled.data, sess_enrolled.len, + "BEGIN CERTIFICATE", 17) != NULL); + wolfcert_buffer_free(&sess_enrolled); + wolfcert_est_session_close(sess); + + /* Same requirement for the non-blocking session enroll, which shares the + * base64 body path: drive it through a poll loop and confirm its request + * carries the header too. */ + WolfCertEstSession* sess_nb = NULL; + REQUIRE(wolfcert_est_session_open_async(&srv, &sess_nb) == WOLFCERT_OK); + WolfCertBuffer sess_nb_enrolled = { 0 }; + REQUIRE(pump_simple_enroll(sess_nb, csr.data, csr.len, + &sess_nb_enrolled) == 0); + REQUIRE(memmem(sess_nb_enrolled.data, sess_nb_enrolled.len, + "BEGIN CERTIFICATE", 17) != NULL); + wolfcert_buffer_free(&sess_nb_enrolled); + wolfcert_est_session_close(sess_nb); + wolfcert_buffer_free(&csr); wolfcert_key_free(dk); wolfcert_buffer_free(&b64); pthread_join(tid, NULL); + + /* sc.request holds the last connection, the non-blocking enroll; + * post_missing_ctenc catches any POST enrollment, the blocking session + * variant included, that dropped the header. */ + REQUIRE(strstr(sc.request, "Content-Transfer-Encoding: base64") != NULL); + REQUIRE(sc.post_missing_ctenc == 0); wolfSSL_CTX_free(ctx); free(tls_cert); free(tls_key);