From 525b2df2951212d145bf325a9061a0aed51da655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Fri, 9 Oct 2026 09:21:56 +0200 Subject: [PATCH] Share the chunked body parser with the test servers The EST test server carried its own copy of the chunk-size and framing parser, and #26 hardened only the client copy. The server copy rejected a zero-padded chunk size longer than eight digits and whitespace before a chunk-ext, both legal per RFC 9112 section 7.1.1, and accepted any trailer section without looking at it. Its framing scan also reported malformed input as complete and left the rejection to the decode pass. http.c now exports its framing scan and decoder, and the new wolfcert_server_read_chunked() receives and decodes a chunked request body for both servers. The EST server drops its own parser. The SCEP server, which only read Content-Length bodies, now accepts a chunked PKIOperation POST as well. The receive loop clamps its last read to the raw cap instead of refusing a body that still fits, and grows its buffer only when it is full. The framing scan now resumes where the previous call stopped instead of starting over at offset 0 after every read. A body sent in many small pieces used to cost time quadratic in its size, about 12 s for 240 KB fed one byte at a time; it is now linear, so a 1 MiB body takes a few milliseconds. The client's chunked response reader shares the scan and gets the same fix. Both servers now take a Transfer-Encoding of exactly "chunked" and answer any other coding, or a second Transfer-Encoding header, with 400; the EST server used to accept any value starting with "chunked". RFC 9112 section 6.3 requires the 400 when chunked is not the final coding, and section 6.1 would allow 501 for an unknown coding before it. A request framed by both Transfer-Encoding and Content-Length, or an HTTP/1.0 request carrying Transfer-Encoding, is decoded as chunked and the connection closes after the response (RFC 9112 section 6.1). The framing scan also carries the decoded-body cap, so a chunk size that would take the body past it is refused as soon as its size line arrives. The server used to buffer the whole raw cap before the decode pass noticed, about 1 MiB for a single FFFFFFFF size line. Any other HTTP/1.0 request now closes the connection too unless it sends Connection: keep-alive (RFC 9112 section 9.3). Both servers used to keep it open, leaving client and server waiting on each other. Connection is read as an option list (RFC 9110 section 7.6.1), so a close anywhere in it wins and keep-alive counts only as a whole token. The Content-Length, Transfer-Encoding and Connection handling and the close decision live in one server.c helper both servers call. test_est_chunked_robustness checks a table of framings and one of Transfer-Encoding values; against the old server parser the padded size, both chunk-ext whitespace forms and both bad trailers fail. test_scep_roundtrip posts a pkiMessage chunked, checks a chunked request on a kept-alive connection, bad framing, other codings and the conflicting-framing and HTTP/1.0 closes, and that neither a Content-Length-Hint field nor an HTTP/1.0 keep-alive request closes. test_est_chunked_robustness checks the same HTTP/1.0 behaviour on EST. test_http drives the reader's decoded and raw caps, rejects an oversized size line before any payload, and feeds the framing scan one byte at a time. --- src/est/est_server.c | 208 ++--------------- src/http.c | 127 ++++++----- src/internal.h | 46 ++++ src/scep/scep_server.c | 23 +- src/server.c | 152 +++++++++++++ .../integration/test_est_chunked_robustness.c | 159 +++++++++++++ tests/integration/test_scep_roundtrip.c | 214 ++++++++++++++++-- tests/unit/test_http.c | 77 +++++++ 8 files changed, 731 insertions(+), 275 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index e587411..2fc8ce0 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -109,7 +109,7 @@ typedef struct { size_t content_length; uint8_t* body; size_t body_len; - int connection_close; /* 1 when client sent Connection: close */ + int connection_close; /* 1 to close after the response */ void* heap; } EstRequest; @@ -139,94 +139,6 @@ 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..]. The 8 hex digit cap keeps csz within - * 32 bits. Returns 0 ok, 1 if the CRLF has not arrived, -1 if 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; - - 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; - - v = (v << 4) | (size_t)d; - parsed = 1; - } - if (parsed == 0) - return -1; - - *csz = v; - *next = he + 2; - - return 0; -} - -/* Walks the framing, since "0\r\n" also occurs in chunk sizes and data. - * Returns 1 once the last chunk and its trailers have arrived or the framing - * is malformed (the decode pass rejects it), 0 when more bytes are needed. */ -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; - if (r < 0) - return 1; - - ri = next; - if (csz == 0) { - /* RFC 7230 section 4.1: consume trailers through the blank - * line, or the leftover CRLF corrupts the next request. */ - 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; - if (ri == ls) - return 1; /* blank line terminates the trailers */ - - ri += 2; - } - - return 0; - } - - if (raw_len - ri < 2 || csz > raw_len - ri - 2) - return 0; - - ri += csz + 2; - } - - return 0; -} - static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, void* heap, char* buf, size_t buf_sz) { @@ -266,6 +178,9 @@ static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, if (sp2 == NULL) return WOLFCERT_ERR_PROTOCOL; + WolfCertReqFraming fr; + wolfcert_server_framing_init(&fr, sp2 + 1, (size_t)(line + llen - sp2 - 1)); + size_t plen = (size_t)(sp2 - sp1 - 1); if (plen >= sizeof(out->path)) return WOLFCERT_ERR_PROTOCOL; @@ -273,21 +188,18 @@ static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, memcpy(out->path, sp1 + 1, plen); out->path[plen] = '\0'; - int chunked = 0; while (read_line(&p, end, &line, &llen) == 0 && llen > 0) { const char* v; size_t vlen; + int rc; if (wolfcert_http_hdr_split(line, llen, &v, &vlen) != 0) return WOLFCERT_ERR_PROTOCOL; - if (wolfcert_http_hdr_is(line, llen, "Content-Length")) { - out->content_length = (size_t)strtoul(v, NULL, 10); - } - else if (wolfcert_http_hdr_is(line, llen, "Transfer-Encoding")) { - if (vlen >= 7 && wolfcert_ascii_ncasecmp(v, "chunked", 7) == 0) - chunked = 1; - } - else if (wolfcert_http_hdr_is(line, llen, "Authorization")) { + + rc = wolfcert_server_framing_hdr(&fr, line, llen, v, vlen); + if (rc < 0) + return rc; + if (rc == 0 && wolfcert_http_hdr_is(line, llen, "Authorization")) { if (out->auth_header != NULL) return WOLFCERT_ERR_PROTOCOL; @@ -297,103 +209,17 @@ static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, out->auth_header[vlen] = '\0'; } } - else if (wolfcert_http_hdr_is(line, llen, "Connection")) { - if (vlen >= 5 && wolfcert_ascii_ncasecmp(v, "close", 5) == 0) - out->connection_close = 1; - } } + out->content_length = fr.content_length; + out->connection_close = wolfcert_server_framing_close(&fr); + size_t body_have = (size_t)(end - p); - if (chunked) { + if (fr.chunked) { /* globalsign's estclient always sends chunked request bodies. */ - static const size_t BODY_CAP = 1 * 1024 * 1024; - uint8_t* raw = NULL; - size_t raw_len = 0, raw_cap = 0; - if (body_have > 0) { - raw = (uint8_t*)WOLFCERT_XMALLOC(body_have, heap); - if (raw == NULL) - return WOLFCERT_ERR_MEMORY; - - memcpy(raw, p, body_have); - raw_len = body_have; - raw_cap = body_have; - } - - 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); - return WOLFCERT_ERR_PROTOCOL; - } - - uint8_t* nb = (uint8_t*)WOLFCERT_XREALLOC(raw, raw_len + grow, heap); - if (nb == NULL) { - WOLFCERT_XFREE(raw, heap); - return WOLFCERT_ERR_MEMORY; - } - - raw = nb; - raw_cap = raw_len + grow; - ssize_t r = wolfcert_io_recv(s, fd, raw + raw_len, raw_cap - raw_len); - if (r <= 0) { - WOLFCERT_XFREE(raw, heap); - return WOLFCERT_ERR_IO; - } - - raw_len += (size_t)r; - } - - uint8_t* body = (uint8_t*)WOLFCERT_XMALLOC(raw_len + 1, heap); - if (body == NULL) { - WOLFCERT_XFREE(raw, heap); - return WOLFCERT_ERR_MEMORY; - } - - size_t body_sz = 0, ri = 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) - break; - if (r < 0) { - WOLFCERT_XFREE(body, heap); - WOLFCERT_XFREE(raw, heap); - return WOLFCERT_ERR_PROTOCOL; - } - - ri = next; - if (csz == 0) - break; - - if (csz > raw_len - ri || csz > BODY_CAP - body_sz) { - WOLFCERT_XFREE(body, heap); - WOLFCERT_XFREE(raw, heap); - return WOLFCERT_ERR_PROTOCOL; - } - - memcpy(body + body_sz, raw + ri, csz); - body_sz += csz; - ri += csz; - if (ri == raw_len) - break; - - if (ri + 2 > raw_len || - raw[ri] != '\r' || raw[ri + 1] != '\n') { - WOLFCERT_XFREE(body, heap); - WOLFCERT_XFREE(raw, heap); - return WOLFCERT_ERR_PROTOCOL; - } - - ri += 2; - } - - body[body_sz] = '\0'; - WOLFCERT_XFREE(raw, heap); - out->body = body; - out->body_len = body_sz; - - return WOLFCERT_OK; + return wolfcert_server_read_chunked(s, fd, p, body_have, + 1 * 1024 * 1024, &out->body, + &out->body_len, heap); } if (out->content_length > 0) { diff --git a/src/http.c b/src/http.c index 1df6214..b427340 100644 --- a/src/http.c +++ b/src/http.c @@ -859,21 +859,26 @@ static int read_headers(WolfCertConn* c, DynBuf* rx) } } -/* Parse the chunk-size line at raw[ri..]; *next lands past its CRLF. Returns 0 - * ok, 1 if the CRLF has not arrived, -1 if malformed or over 0xFFFFFFFF. */ -static int read_chunk_size(const uint8_t* raw, size_t raw_len, size_t ri, - size_t* csz, size_t* next) +/* Advance *at to the next CRLF; 1 if none yet, with *at where to resume. */ +static int find_crlf(const uint8_t* raw, size_t raw_len, size_t* at) { - size_t he = ri; - size_t v = 0; - int parsed = 0; + size_t i = *at; - while (he + 1 < raw_len && !(raw[he] == '\r' && raw[he+1] == '\n')) { - ++he; + while (i + 1 < raw_len && !(raw[i] == '\r' && raw[i + 1] == '\n')) { + ++i; } + *at = i; + + return i + 1 < raw_len ? 0 : 1; +} - if (he + 1 >= raw_len) - return 1; +/* Parse the chunk-size line raw[ri..he). Returns 0 ok, -1 if malformed or over + * 0xFFFFFFFF. */ +static int parse_chunk_size(const uint8_t* raw, size_t ri, size_t he, + size_t* csz) +{ + size_t v = 0; + int parsed = 0; for (size_t k = ri; k < he; ++k) { char c = (char)raw[k]; @@ -904,58 +909,54 @@ static int read_chunk_size(const uint8_t* raw, size_t raw_len, size_t ri, if (parsed == 0) return -1; /* empty chunk-size line */ - *csz = v; - *next = he + 2; + *csz = v; return 0; } -/* Walk chunk framing over the bytes received so far. Returns 1 at a - * zero-length chunk parsed on a chunk-header boundary whose trailer - * section is closed, 0 when more bytes are needed, -1 when malformed. */ -static int chunked_body_complete(const uint8_t* raw, size_t raw_len) +WOLFCERT_TEST_VIS int wolfcert_http_chunked_complete(const uint8_t* raw, + size_t raw_len, + WolfCertChunkScan* st) { - size_t ri = 0; - size_t ls = 0; + while (st->pos < raw_len) { + size_t ri = st->pos; + size_t csz = st->chunk; - 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; - if (r < 0) - return -1; + if (csz > 0) { + if (raw_len - ri < 2 || csz > raw_len - ri - 2) + return 0; /* payload and its CRLF not yet received */ - ri = next; - if (csz == 0) { - /* Consume the trailer section through its blank line; stopping - * at "0\r\n" desyncs keep-alive. */ - while (ri < raw_len) { - ls = ri; - while (ri + 1 < raw_len && - !(raw[ri] == '\r' && raw[ri + 1] == '\n')) { - ++ri; - } + if (raw[ri + csz] != '\r' || raw[ri + csz + 1] != '\n') + return -1; /* no CRLF closing the chunk payload */ - 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; - } - - return 0; + st->pos = ri + csz + 2; + st->chunk = 0; + continue; } - if (raw_len - ri < 2 || csz > raw_len - ri - 2) - return 0; /* chunk payload plus trailing CRLF not yet received */ + if (st->crlf < ri) + st->crlf = ri; + if (find_crlf(raw, raw_len, &st->crlf) != 0) + return 0; - if (raw[ri + csz] != '\r' || raw[ri + csz + 1] != '\n') - return -1; /* no CRLF closing the chunk payload */ + if (st->in_trailers) { + if (st->crlf == ri) + return 1; /* blank line terminates the trailers */ + } + else if (parse_chunk_size(raw, ri, st->crlf, &st->chunk) != 0 || + (st->max != 0 && st->chunk > st->max - st->total)) { + return -1; + } + else if (st->chunk == 0) { + /* Consume the trailer section through its blank line; stopping + * at "0\r\n" desyncs keep-alive. */ + st->in_trailers = 1; + } + else { + st->total += st->chunk; + } - ri += csz + 2; + st->pos = st->crlf + 2; } return 0; @@ -999,9 +1000,9 @@ static int check_trailers(const uint8_t* raw, size_t raw_len) return WOLFCERT_ERR_PROTOCOL; } -static int decode_chunked(const uint8_t* in, size_t in_len, - uint8_t** out, size_t* out_len, - size_t max_bytes, void* heap) +int wolfcert_http_chunked_decode(const uint8_t* in, size_t in_len, + uint8_t** out, size_t* out_len, + size_t max_bytes, void* heap) { DynBuf body = { .heap = heap, .max = max_bytes }; size_t p = 0; @@ -1009,14 +1010,15 @@ static int decode_chunked(const uint8_t* in, size_t in_len, while (p < in_len) { size_t clen = 0; - size_t next = 0; + size_t he = p; - if (read_chunk_size(in, in_len, p, &clen, &next) != 0) { + if (find_crlf(in, in_len, &he) != 0 || + parse_chunk_size(in, p, he, &clen) != 0) { WOLFCERT_XFREE(body.buf, heap); return WOLFCERT_ERR_PROTOCOL; } - p = next; + p = he + 2; if (clen == 0) { rc = check_trailers(in + p, in_len - p); if (rc != WOLFCERT_OK) { @@ -1074,11 +1076,13 @@ static int read_body(WolfCertConn* c, DynBuf* rx, size_t body_start, return WOLFCERT_ERR_PROTOCOL; if (chunked) { + WolfCertChunkScan scan = { .max = max_bytes }; int framed; for (;;) { - framed = chunked_body_complete(rx->buf + body_start, - rx->len - body_start); + framed = wolfcert_http_chunked_complete(rx->buf + body_start, + rx->len - body_start, + &scan); if (framed != 0) break; @@ -1095,8 +1099,9 @@ static int read_body(WolfCertConn* c, DynBuf* rx, size_t body_start, if (framed < 0) return WOLFCERT_ERR_PROTOCOL; - return decode_chunked(rx->buf + body_start, rx->len - body_start, - out, out_len, max_bytes, heap); + return wolfcert_http_chunked_decode(rx->buf + body_start, + rx->len - body_start, + out, out_len, max_bytes, heap); } if (length >= 0) { diff --git a/src/internal.h b/src/internal.h index 9a3d0d8..52a46c5 100644 --- a/src/internal.h +++ b/src/internal.h @@ -153,6 +153,32 @@ WOLFCERT_TEST_VIS void wolfcert_sock_nosigpipe(int fd); WOLFCERT_TEST_VIS int64_t wolfcert_mono_ms(void); +/* Message framing and persistence of one request to the test servers. */ +typedef struct { + size_t content_length; + int http10; + int chunked; + int cl_seen; + int keep_alive; + int close; +} WolfCertReqFraming; + +void wolfcert_server_framing_init(WolfCertReqFraming* f, const char* ver, + size_t ver_len); +/* 1 when the line is Content-Length, Transfer-Encoding or Connection, 0 for + * any other field, WOLFCERT_ERR_PROTOCOL for a coding other than chunked. */ +int wolfcert_server_framing_hdr(WolfCertReqFraming* f, const char* line, + size_t llen, const char* v, size_t vlen); +/* 1 when the connection closes after the response. */ +int wolfcert_server_framing_close(const WolfCertReqFraming* f); + +/* Receive the rest of a chunked request body that starts with have[0..have_len) + * and decode it, rejecting a body over max_bytes. */ +WOLFCERT_TEST_VIS int wolfcert_server_read_chunked(WolfCertServer* srv, int fd, + const void* have, size_t have_len, + size_t max_bytes, uint8_t** body, + size_t* body_len, void* heap); + WOLFCERT_API const WolfCertServerOps* wolfcert_est_server_ops(void); WOLFCERT_API const WolfCertServerOps* wolfcert_scep_server_ops(void); @@ -226,6 +252,26 @@ WOLFCERT_TEST_VIS void wolfcert_http_url_free (WolfCertUrl* u); WOLFCERT_TEST_VIS int wolfcert_http_url_origin(const WolfCertUrl* u, void* heap, char** out_origin); +/* Resume point for wolfcert_http_chunked_complete(); zero it before the first + * call, setting max to cap the decoded body (0 for no cap). */ +typedef struct { + size_t pos; + size_t crlf; + size_t chunk; + size_t max; + size_t total; + int in_trailers; +} WolfCertChunkScan; + +/* 1 once the last chunk and its trailer section have arrived, 0 when more + * bytes are needed, -1 when the framing is malformed. */ +WOLFCERT_TEST_VIS int wolfcert_http_chunked_complete(const uint8_t* raw, + size_t raw_len, + WolfCertChunkScan* st); +int wolfcert_http_chunked_decode(const uint8_t* in, size_t in_len, + uint8_t** out, size_t* out_len, + size_t max_bytes, void* heap); + /* strncasecmp() with ASCII-only case folding */ int wolfcert_ascii_ncasecmp(const char* a, const char* b, size_t n); /* 1 when the header line's field name is `name`, followed directly by ':'. */ diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 7fa50d1..cd7471e 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -198,6 +198,9 @@ static int read_request(WolfCertServer* s, int fd, ScepRequest* out, void* heap) if (sp2 == NULL) return WOLFCERT_ERR_PROTOCOL; + WolfCertReqFraming fr; + wolfcert_server_framing_init(&fr, sp2 + 1, (size_t)(line + llen - sp2 - 1)); + /* Split the request-target in place inside rawbuf. */ *sp2 = '\0'; out->path = sp1 + 1; @@ -214,18 +217,20 @@ static int read_request(WolfCertServer* s, int fd, ScepRequest* out, void* heap) const char* v; size_t vlen; - if (wolfcert_http_hdr_split(line, llen, &v, &vlen) != 0) + if (wolfcert_http_hdr_split(line, llen, &v, &vlen) != 0 || + wolfcert_server_framing_hdr(&fr, line, llen, v, vlen) < 0) return WOLFCERT_ERR_PROTOCOL; - if (wolfcert_http_hdr_is(line, llen, "Content-Length")) { - out->content_length = (size_t)strtoul(v, NULL, 10); - } - else if (wolfcert_http_hdr_is(line, llen, "Connection")) { - if (vlen >= 5 && wolfcert_ascii_ncasecmp(v, "close", 5) == 0) - out->connection_close = 1; - } } + out->content_length = fr.content_length; + out->connection_close = wolfcert_server_framing_close(&fr); + size_t have = (size_t)(end - p); + if (fr.chunked) { + return wolfcert_server_read_chunked(s, fd, p, have, 1 * 1024 * 1024, + &out->body, &out->body_len, heap); + } + if (out->content_length > 0) { if (out->content_length > 1 * 1024 * 1024) return WOLFCERT_ERR_PROTOCOL; @@ -1095,7 +1100,7 @@ static int handle_pki_op_get(WolfCertServer* s, int fd, ScepRequest* req) return rc; } - /* Free any body a bogus Content-Length made read_request allocate. */ + /* Free any body sent with a GET. */ WOLFCERT_XFREE(req->body, req->heap); req->body = der.data; req->body_len = der.len; diff --git a/src/server.c b/src/server.c index 9a3c6d1..c04c6f0 100644 --- a/src/server.c +++ b/src/server.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -156,6 +157,157 @@ ssize_t wolfcert_io_send(WolfCertServer* srv, int fd, const void* buf, size_t le return r; } +static void ows_trim(const char** v, size_t* vlen) +{ + while (*vlen > 0 && (**v == ' ' || **v == '\t')) { + (*v)++; + (*vlen)--; + } + while (*vlen > 0 && ((*v)[*vlen - 1] == ' ' || (*v)[*vlen - 1] == '\t')) + (*vlen)--; +} + +static int te_chunked(const char* v, size_t vlen) +{ + ows_trim(&v, &vlen); + return vlen == 7 && wolfcert_ascii_ncasecmp(v, "chunked", 7) == 0; +} + +/* Connection is a comma-separated option list (RFC 9110 section 7.6.1). */ +static void conn_options(WolfCertReqFraming* f, const char* v, size_t vlen) +{ + const char* end = v + vlen; + + while (v < end) { + const char* t = v; + size_t tlen; + + while (v < end && *v != ',') + v++; + tlen = (size_t)(v - t); + ows_trim(&t, &tlen); + + if (tlen == 5 && wolfcert_ascii_ncasecmp(t, "close", 5) == 0) + f->close = 1; + else if (tlen == 10 && + wolfcert_ascii_ncasecmp(t, "keep-alive", 10) == 0) + f->keep_alive = 1; + + if (v < end) + v++; + } +} + +void wolfcert_server_framing_init(WolfCertReqFraming* f, const char* ver, + size_t ver_len) +{ + memset(f, 0, sizeof(*f)); + f->http10 = ver_len == 8 && memcmp(ver, "HTTP/1.0", 8) == 0; +} + +int wolfcert_server_framing_hdr(WolfCertReqFraming* f, const char* line, + size_t llen, const char* v, size_t vlen) +{ + if (wolfcert_http_hdr_is(line, llen, "Content-Length")) { + f->content_length = (size_t)strtoul(v, NULL, 10); + f->cl_seen = 1; + } + else if (wolfcert_http_hdr_is(line, llen, "Transfer-Encoding")) { + /* Any coding but a lone chunked is refused with 400. */ + if (f->chunked || !te_chunked(v, vlen)) + return WOLFCERT_ERR_PROTOCOL; + + f->chunked = 1; + } + else if (wolfcert_http_hdr_is(line, llen, "Connection")) { + conn_options(f, v, vlen); + } + else { + return 0; + } + + return 1; +} + +int wolfcert_server_framing_close(const WolfCertReqFraming* f) +{ + /* RFC 9112 section 9.3: HTTP/1.0 persists only with keep-alive. Section + * 6.1: close after TE with Content-Length, or TE on HTTP/1.0. */ + return f->close || (f->http10 && !f->keep_alive) || + (f->chunked && (f->cl_seen || f->http10)); +} + +WOLFCERT_TEST_VIS int wolfcert_server_read_chunked(WolfCertServer* srv, int fd, + const void* have, size_t have_len, + size_t max_bytes, uint8_t** body, + size_t* body_len, void* heap) +{ + size_t raw_max = max_bytes + 64 * 1024; /* room for the framing */ + uint8_t* raw = NULL; + size_t raw_len = 0; + size_t raw_cap = 0; + WolfCertChunkScan scan = { .max = max_bytes }; + int framed; + int rc; + + if (have_len > raw_max) + return WOLFCERT_ERR_PROTOCOL; + + if (have_len > 0) { + raw = (uint8_t*)WOLFCERT_XMALLOC(have_len, heap); + if (raw == NULL) + return WOLFCERT_ERR_MEMORY; + + memcpy(raw, have, have_len); + raw_len = have_len; + raw_cap = have_len; + } + + while ((framed = wolfcert_http_chunked_complete(raw, raw_len, + &scan)) == 0) { + ssize_t r; + + if (raw_len == raw_cap) { + size_t grow = raw_cap < 2048 ? 2048 : raw_cap; + uint8_t* nb; + + if (grow > raw_max - raw_cap) + grow = raw_max - raw_cap; + if (grow == 0) { + WOLFCERT_XFREE(raw, heap); + return WOLFCERT_ERR_PROTOCOL; + } + + nb = (uint8_t*)WOLFCERT_XREALLOC(raw, raw_cap + grow, heap); + if (nb == NULL) { + WOLFCERT_XFREE(raw, heap); + return WOLFCERT_ERR_MEMORY; + } + + raw = nb; + raw_cap += grow; + } + + r = wolfcert_io_recv(srv, fd, raw + raw_len, raw_cap - raw_len); + if (r <= 0) { + WOLFCERT_XFREE(raw, heap); + return WOLFCERT_ERR_IO; + } + + raw_len += (size_t)r; + } + + if (framed < 0) + rc = WOLFCERT_ERR_PROTOCOL; + else + rc = wolfcert_http_chunked_decode(raw, raw_len, body, body_len, + max_bytes, heap); + + WOLFCERT_XFREE(raw, heap); + + return rc; +} + static int tls_setup(WolfCertServer* s, const WolfCertServerCfgSrv* cfg) { int rc = WOLFCERT_OK; diff --git a/tests/integration/test_est_chunked_robustness.c b/tests/integration/test_est_chunked_robustness.c index e48008d..12ffa35 100644 --- a/tests/integration/test_est_chunked_robustness.c +++ b/tests/integration/test_est_chunked_robustness.c @@ -185,6 +185,159 @@ static int accept_wellformed_chunks(uint16_t port) return 0; } +/* RFC 9112 section 7.1: legal framings reach the CSR check, malformed ones + * fail at the framing. */ +static int check_chunk_framing(uint16_t port) +{ + static const struct { + const char* framing; + const char* want; + } cases[] = { + { "000000004\r\nAAAA\r\n0\r\n\r\n", "Bad CSR" }, + { "4 ;n=v\r\nAAAA\r\n0\r\n\r\n", "Bad CSR" }, + { "4\t;n=v\r\nAAAA\r\n0\r\n\r\n", "Bad CSR" }, + { "4\r\nAAAA\r\n0\r\nX-T: 1\r\n\r\n", "Bad CSR" }, + { "4 5\r\nAAAA\r\n0\r\n\r\n", "Bad Request" }, + { "4\r\nAAAA\r\n0\r\ngarbage\r\n\r\n", "Bad Request" }, + { "4\r\nAAAA\r\n0\r\n: v\r\n\r\n", "Bad Request" }, + }; + static 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"; + char req[256]; + char status[128]; + int failed = 0; + size_t i; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int n = snprintf(req, sizeof(req), "%s%s", hdr, cases[i].framing); + REQUIRE(n > 0 && (size_t)n < sizeof(req)); + memset(status, 0, sizeof(status)); + send_and_read_status(port, req, (size_t)n, status, sizeof(status)); + if (strstr(status, cases[i].want) == NULL) { + fprintf(stderr, "FAIL framing case %zu: want '%s', got '%.40s'\n", + i, cases[i].want, status); + failed = 1; + } + } + return failed; +} + +/* Send req over a new TLS connection and read the response head into resp. */ +static int read_head(uint16_t port, const char* req, size_t len, char* resp, + size_t cap) +{ + TestTlsConn c; + size_t n = 0; + + resp[0] = '\0'; + REQUIRE(test_tls_connect(&c, port, g_tls_cert, g_tls_cert_len) == 0); + REQUIRE(test_tls_write(&c, req, len) == 0); + while (n + 1 < cap) { + int r = test_tls_read(&c, resp + n, cap - 1 - n); + if (r <= 0) + break; + n += (size_t)r; + resp[n] = '\0'; + if (strstr(resp, "\r\n\r\n") != NULL) + break; + } + resp[n] = '\0'; + test_tls_close(&c); + return 0; +} + +/* Only an exact "chunked" coding is decoded, and a request framed by both + * Transfer-Encoding and Content-Length, or sent as HTTP/1.0, closes. */ +static int check_transfer_encoding(uint16_t port) +{ + static const struct { + const char* headers; + const char* want; + const char* conn; + const char* version; + } cases[] = { + { "Transfer-Encoding: chunked \r\n", + "Bad CSR", "keep-alive", "1.1" }, + { "Transfer-Encoding: chunkedX\r\n", + "Bad Request", "close", "1.1" }, + { "Transfer-Encoding: gzip, chunked\r\n", + "Bad Request", "close", "1.1" }, + { "Transfer-Encoding: chunked\r\n" + "Transfer-Encoding: chunked\r\n", + "Bad Request", "close", "1.1" }, + { "Transfer-Encoding: chunked\r\n" + "Content-Length: 4\r\n", + "Bad CSR", "close", "1.1" }, + { "Transfer-Encoding: chunked\r\n", + "Bad CSR", "close", "1.0" }, + { "Connection: keep-alive\r\n" + "Transfer-Encoding: chunked\r\n", + "Bad CSR", "close", "1.0" }, + }; + char req[512]; + char resp[1024]; + char conn[32]; + size_t i; + int failed = 0; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int len = snprintf(req, sizeof(req), + "POST /.well-known/est/simpleenroll HTTP/%s\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "%s\r\n4\r\nAAAA\r\n0\r\n\r\n", + cases[i].version, cases[i].headers); + REQUIRE(len > 0 && (size_t)len < sizeof(req)); + REQUIRE(read_head(port, req, (size_t)len, resp, sizeof(resp)) == 0); + snprintf(conn, sizeof(conn), "Connection: %s\r\n", cases[i].conn); + if (strstr(resp, cases[i].want) == NULL || + strstr(resp, conn) == NULL) { + fprintf(stderr, "FAIL coding case %zu: want '%s' and '%s', " + "got '%.60s'\n", i, cases[i].want, cases[i].conn, + resp); + failed = 1; + } + } + return failed; +} + +/* An HTTP/1.0 request closes the connection unless it asks for keep-alive. */ +static int check_http10_persistence(uint16_t port) +{ + static const struct { + const char* headers; + const char* want; + } cases[] = { + { "", "Connection: close" }, + { "Connection: keep-alive\r\n", "Connection: keep-alive" }, + { "Connection: TE, keep-alive\r\n", "Connection: keep-alive" }, + { "Connection: keep-alive, close\r\n", "Connection: close" }, + { "Connection: keep-alivex\r\n", "Connection: close" }, + }; + char req[256]; + char resp[1024]; + size_t i; + int failed = 0; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int len = snprintf(req, sizeof(req), + "GET /.well-known/est/cacerts HTTP/1.0\r\n" + "Host: 127.0.0.1\r\n%s\r\n", cases[i].headers); + REQUIRE(len > 0 && (size_t)len < sizeof(req)); + REQUIRE(read_head(port, req, (size_t)len, resp, sizeof(resp)) == 0); + if (strstr(resp, cases[i].want) == NULL) { + fprintf(stderr, "FAIL HTTP/1.0 case %zu: want '%s', got '%.40s'\n", + i, cases[i].want, resp); + failed = 1; + } + } + return failed; +} + /* The chunk-size line "10\r\n" contains "0\r\n", so a framer that scans for * the terminator stops early and answers "Bad Request" instead of "Bad CSR". */ static int accept_multisegment_chunked_body(uint16_t port) @@ -481,6 +634,12 @@ int main(void) rc = reject_corrupt_chunk_trailer(port); if (rc == 0) rc = accept_wellformed_chunks(port); + if (rc == 0) + rc = check_chunk_framing(port); + if (rc == 0) + rc = check_transfer_encoding(port); + if (rc == 0) + rc = check_http10_persistence(port); if (rc == 0) rc = accept_multisegment_chunked_body(port); if (rc == 0) diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 890cda4..0c0c3d9 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -74,10 +74,40 @@ static int write_all_fd(int fd, const uint8_t* buf, size_t len) return 0; } +#define RAW_KEEP_ALIVE 0x1 +#define RAW_CHUNKED 0x2 + +/* Up to two chunks with a zero-padded size and a chunk-ext, then a trailer. */ +static int write_chunked_fd(int fd, const uint8_t* body, size_t body_len) +{ + static const char TRAILER[] = "0\r\nX-T: 1\r\n\r\n"; + size_t half = body_len >= 2 ? body_len / 2 : body_len; + char line[32]; + int n; + + if (half > 0) { + n = snprintf(line, sizeof(line), "%09zx ;x=1\r\n", half); + if (write_all_fd(fd, (const uint8_t*)line, (size_t)n) != 0 || + write_all_fd(fd, body, half) != 0 || + write_all_fd(fd, (const uint8_t*)"\r\n", 2) != 0) + return -1; + } + + if (body_len > half) { + n = snprintf(line, sizeof(line), "%zx\r\n", body_len - half); + if (write_all_fd(fd, (const uint8_t*)line, (size_t)n) != 0 || + write_all_fd(fd, body + half, body_len - half) != 0 || + write_all_fd(fd, (const uint8_t*)"\r\n", 2) != 0) + return -1; + } + + return write_all_fd(fd, (const uint8_t*)TRAILER, sizeof(TRAILER) - 1); +} + /* Sends hdr and body to the loopback server and reads the reply to EOF. * Returns the status code, or -1 on no reply. out_body is caller-freed. */ static int raw_exchange(uint16_t port, const char* hdr, size_t hdr_len, - const uint8_t* body, size_t body_len, + const uint8_t* body, size_t body_len, int chunked, uint8_t** out_body, size_t* out_body_len) { struct sockaddr_in addr; @@ -115,7 +145,9 @@ static int raw_exchange(uint16_t port, const char* hdr, size_t hdr_len, } if (write_all_fd(fd, (const uint8_t*)hdr, hdr_len) != 0 || - (body_len > 0 && write_all_fd(fd, body, body_len) != 0)) { + (chunked && write_chunked_fd(fd, body, body_len) != 0) || + (!chunked && body_len > 0 && + write_all_fd(fd, body, body_len) != 0)) { close(fd); return -1; } @@ -171,17 +203,27 @@ static int raw_exchange(uint16_t port, const char* hdr, size_t hdr_len, * branches the client API cannot produce. out_body, when set, is caller-freed. */ static int raw_http_req(uint16_t port, const char* method, const char* target, const char* content_type, - const uint8_t* body, size_t body_len, int persistent, + const uint8_t* body, size_t body_len, int flags, uint8_t** out_body, size_t* out_body_len) { + const char* conn = (flags & RAW_KEEP_ALIVE) ? "keep-alive" : "close"; + int chunked = body != NULL && (flags & RAW_CHUNKED); char hdr[512]; int n; - if (body != NULL) + if (chunked) + n = snprintf(hdr, sizeof(hdr), + "%s %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: %s\r\n" + "%s%s%sTransfer-Encoding: chunked\r\n\r\n", + method, target, conn, + content_type != NULL ? "Content-Type: " : "", + content_type != NULL ? content_type : "", + content_type != NULL ? "\r\n" : ""); + else if (body != NULL) n = snprintf(hdr, sizeof(hdr), "%s %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: %s\r\n" "%s%s%sContent-Length: %zu\r\n\r\n", - method, target, persistent ? "keep-alive" : "close", + method, target, conn, content_type != NULL ? "Content-Type: " : "", content_type != NULL ? content_type : "", content_type != NULL ? "\r\n" : "", @@ -189,7 +231,7 @@ static int raw_http_req(uint16_t port, const char* method, const char* target, else n = snprintf(hdr, sizeof(hdr), "%s %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: %s\r\n\r\n", - method, target, persistent ? "keep-alive" : "close"); + method, target, conn); if (n < 0 || (size_t)n >= sizeof(hdr)) { if (out_body != NULL && out_body_len != NULL) { *out_body = NULL; @@ -198,14 +240,14 @@ static int raw_http_req(uint16_t port, const char* method, const char* target, return -1; } - return raw_exchange(port, hdr, (size_t)n, body, body_len, + return raw_exchange(port, hdr, (size_t)n, body, body_len, chunked, out_body, out_body_len); } /* Status code of the reply to a hand-written request, or -1 on no reply. */ static int raw_request_status(uint16_t port, const char* req) { - return raw_exchange(port, req, strlen(req), NULL, 0, NULL, NULL); + return raw_exchange(port, req, strlen(req), NULL, 0, 0, NULL, NULL); } static int raw_http_status(uint16_t port, const char* target, const char* body) @@ -1165,7 +1207,8 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, if (raw_http_req(wolfcert_server_port(s), "POST", "/scep?operation=PKIOperation", "application/x-pki-message", - msg.data, msg.len, 1, NULL, NULL) != 400) { + msg.data, msg.len, RAW_KEEP_ALIVE, + NULL, NULL) != 400) { fprintf(stderr, "FAIL %s:%d required-attrs did not reject " "and close ahead of the messageType check\n", __FILE__, __LINE__); @@ -1187,7 +1230,7 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, /* POST a signed PKCSReq whose transactionID `tid` is tagged PrintableString * whatever its bytes, which wolfcert_scep_build_pki_message will not encode. */ static int post_raw_tid(uint16_t port, const uint8_t* signer, size_t signer_len, - const WolfCertBuffer* kder, const char* tid) + const WolfCertBuffer* kder, const char* tid, int flags) { static const byte oid_msg_type[] = { 0x06,0x0A,0x60,0x86,0x48,0x01,0x86,0xF8,0x45,0x01,0x09,0x02 }; @@ -1250,7 +1293,7 @@ static int post_raw_tid(uint16_t port, const uint8_t* signer, size_t signer_len, if (n > 0) st = raw_http_req(port, "POST", "/scep?operation=PKIOperation", - "application/x-pki-message", buf, (size_t)n, 0, + "application/x-pki-message", buf, (size_t)n, flags, &rsp, &rsp_len); free(rsp); @@ -1261,6 +1304,137 @@ static int post_raw_tid(uint16_t port, const uint8_t* signer, size_t signer_len, return st; } +static int connect_loopback(uint16_t port) +{ + struct sockaddr_in addr; + struct timeval tv = { .tv_sec = 5 }; + int fd = socket(AF_INET, SOCK_STREAM, 0); + + if (fd < 0) + return -1; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + close(fd); + return -1; + } + return fd; +} + +/* Send req on fd and read one response into buf; returns its status or -1. */ +static int exchange(int fd, const char* req, char* buf, size_t cap) +{ + const char* eoh = NULL; + const char* cl; + size_t need = 0; + size_t n = 0; + ssize_t r; + + if (write_all_fd(fd, (const uint8_t*)req, strlen(req)) != 0) + return -1; + + while (n + 1 < cap && (eoh == NULL || n < need)) { + r = read(fd, buf + n, cap - 1 - n); + if (r <= 0) + break; + n += (size_t)r; + buf[n] = '\0'; + if (eoh == NULL && (eoh = strstr(buf, "\r\n\r\n")) != NULL) { + cl = strstr(buf, "Content-Length:"); + need = (size_t)(eoh + 4 - buf) + + (cl != NULL && cl < eoh ? strtoul(cl + 15, NULL, 10) : 0); + } + } + buf[n] = '\0'; + + if (eoh == NULL || n < need || strncmp(buf, "HTTP/1.1 ", 9) != 0) + return -1; + return atoi(buf + 9); +} + +#define CAPS_REQ "GET /scep?operation=GetCACaps HTTP/1.1\r\nHost: 127.0.0.1\r\n" +#define CAPS_REQ_10 \ + "GET /scep?operation=GetCACaps HTTP/1.0\r\nHost: 127.0.0.1\r\n" + +/* Chunked framing is consumed exactly, bad framing gets 400, and TE with + * Content-Length or HTTP/1.0 without keep-alive closes the connection. */ +static int check_scep_chunked_framing(uint16_t port) +{ + static const char* const kept[] = { + CAPS_REQ "Transfer-Encoding: chunked\r\nContent-Length-Hint: 0\r\n" + "\r\n4 ;x=1\r\nAAAA\r\n0\r\nX-T: 1\r\n\r\n", + CAPS_REQ_10 "Connection: keep-alive\r\n\r\n", + CAPS_REQ_10 "Connection: TE, keep-alive\r\n\r\n", + }; + static const char plain[] = CAPS_REQ "\r\n"; + static const char* const closing[] = { + CAPS_REQ "Transfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n" + "4\r\nAAAA\r\n0\r\n\r\n", + CAPS_REQ_10 "Transfer-Encoding: chunked\r\n\r\n4\r\nAAAA\r\n0\r\n\r\n", + CAPS_REQ_10 "\r\n", + CAPS_REQ_10 "Connection: keep-alive, close\r\n\r\n", + CAPS_REQ_10 "Connection: keep-alive\r\nTransfer-Encoding: chunked\r\n" + "\r\n4\r\nAAAA\r\n0\r\n\r\n", + }; + static const char* const bad[] = { + CAPS_REQ "Transfer-Encoding: chunked\r\n\r\n4 5\r\nAAAA\r\n0\r\n\r\n", + CAPS_REQ "Transfer-Encoding: chunkedX\r\n\r\n4\r\nAAAA\r\n0\r\n\r\n", + CAPS_REQ "Transfer-Encoding: gzip, chunked\r\n\r\n" + "4\r\nAAAA\r\n0\r\n\r\n", + CAPS_REQ "Transfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n" + "\r\n4\r\nAAAA\r\n0\r\n\r\n", + CAPS_REQ "Transfer-Encoding : chunked\r\n\r\n4\r\nAAAA\r\n0\r\n\r\n", + }; + char buf[2048]; + size_t i; + int fails = 0; + int fd; + int st1, st2; + + for (i = 0; i < sizeof(kept) / sizeof(kept[0]); i++) { + fd = connect_loopback(port); + REQUIRE(fd >= 0); + st1 = exchange(fd, kept[i], buf, sizeof(buf)); + st2 = exchange(fd, plain, buf, sizeof(buf)); + close(fd); + if (st1 != 200 || st2 != 200) { + fprintf(stderr, "FAIL keep-alive case %zu: %d then %d\n", i, st1, + st2); + fails++; + } + } + + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + fd = connect_loopback(port); + REQUIRE(fd >= 0); + st1 = exchange(fd, bad[i], buf, sizeof(buf)); + close(fd); + if (st1 != 400) { + fprintf(stderr, "FAIL bad framing %zu got %d\n", i, st1); + fails++; + } + } + + for (i = 0; i < sizeof(closing) / sizeof(closing[0]); i++) { + fd = connect_loopback(port); + REQUIRE(fd >= 0); + st1 = exchange(fd, closing[i], buf, sizeof(buf)); + st2 = st1 == 200 && strstr(buf, "Connection: close") != NULL && + read(fd, buf, sizeof(buf)) == 0; + close(fd); + if (!st2) { + fprintf(stderr, "FAIL closing case %zu kept the connection " + "(status %d)\n", i, st1); + fails++; + } + } + + return fails; +} + /* A transactionID that is not a PrintableString gets 400. */ static int check_unprintable_tid(uint16_t port, const WolfCertKeyCfg* kcfg) { @@ -1283,7 +1457,7 @@ static int check_unprintable_tid(uint16_t port, const WolfCertKeyCfg* kcfg) csr.len, &signer, &signer_len, NULL); if (rc == WOLFCERT_OK) { - st = post_raw_tid(port, signer, signer_len, &kder, "tid@1"); + st = post_raw_tid(port, signer, signer_len, &kder, "tid@1", 0); if (st != 400) { fprintf(stderr, "FAIL %s:%d '@' transactionID got status %d\n", __FILE__, __LINE__, st); @@ -1294,7 +1468,7 @@ static int check_unprintable_tid(uint16_t port, const WolfCertKeyCfg* kcfg) /* The control reaches de-enveloping, which answers the junk content with * a signed FAILURE. */ if (rc == WOLFCERT_OK) { - st = post_raw_tid(port, signer, signer_len, &kder, "tid-1"); + st = post_raw_tid(port, signer, signer_len, &kder, "tid-1", 0); if (st != 200) { fprintf(stderr, "FAIL %s:%d control transactionID got status %d\n", __FILE__, __LINE__, st); @@ -1302,6 +1476,16 @@ static int check_unprintable_tid(uint16_t port, const WolfCertKeyCfg* kcfg) } } + if (rc == WOLFCERT_OK) { + st = post_raw_tid(port, signer, signer_len, &kder, "tid-2", + RAW_CHUNKED); + if (st != 200) { + fprintf(stderr, "FAIL %s:%d chunked control got status %d\n", + __FILE__, __LINE__, st); + rc = -1; + } + } + WOLFCERT_XFREE(signer, NULL); wolfcert_buffer_free(&kder); wolfcert_buffer_free(&csr); @@ -1455,7 +1639,8 @@ static int check_malformed_dispatch(uint16_t port, const WolfCertKeyCfg* kcfg, if (rc == WOLFCERT_OK) { if (raw_http_req(port, "POST", "/scep?operation=PKIOperation", "application/x-pki-message", - msg.data, msg.len, 1, NULL, NULL) != 200) { + msg.data, msg.len, RAW_KEEP_ALIVE, + NULL, NULL) != 200) { fprintf(stderr, "FAIL %s:%d dispatch failure did not answer " "and close\n", __FILE__, __LINE__); rc = -1; @@ -1705,6 +1890,7 @@ int main(void) REQUIRE(check_unprintable_tid(wolfcert_server_port(s), &kcfg) == WOLFCERT_OK); + REQUIRE(check_scep_chunked_framing(wolfcert_server_port(s)) == 0); REQUIRE(check_malformed_dispatch(wolfcert_server_port(s), &kcfg, ca_der->buffer, ca_der->length) diff --git a/tests/unit/test_http.c b/tests/unit/test_http.c index 798ef71..235264d 100644 --- a/tests/unit/test_http.c +++ b/tests/unit/test_http.c @@ -1180,6 +1180,77 @@ static int test_chunked_over_max_accumulated(void) return oversize_chunk_case(body); } +#ifdef WOLFCERT_HAVE_SERVER +/* The server's chunked reader decodes a buffered body and enforces its decoded + * cap at the size line, and its raw cap, before it would read the socket. */ +static int test_server_read_chunked_caps(void) +{ + static const char ok[] = "4\r\nabcd\r\n0\r\n\r\n"; + static const char big[] = "11\r\n"; + static const char sum[] = "8\r\n01234567\r\n9\r\n"; + const size_t raw_max = 16 + 64 * 1024; + uint8_t* body = NULL; + size_t body_len = 0; + uint8_t* raw; + int ok_rc, big_rc, sum_rc, full_rc, over_rc, ok_body; + + ok_rc = wolfcert_server_read_chunked(NULL, -1, ok, sizeof(ok) - 1, 16, + &body, &body_len, NULL); + ok_body = body != NULL && body_len == 4 && memcmp(body, "abcd", 4) == 0; + WOLFCERT_XFREE(body, NULL); + body = NULL; + + big_rc = wolfcert_server_read_chunked(NULL, -1, big, sizeof(big) - 1, 16, + &body, &body_len, NULL); + sum_rc = wolfcert_server_read_chunked(NULL, -1, sum, sizeof(sum) - 1, 16, + &body, &body_len, NULL); + + raw = (uint8_t*)malloc(raw_max + 1); + REQUIRE(raw != NULL); + memcpy(raw, "1;", 2); + memset(raw + 2, 'A', raw_max + 1 - 2); + full_rc = wolfcert_server_read_chunked(NULL, -1, raw, raw_max, 16, + &body, &body_len, NULL); + over_rc = wolfcert_server_read_chunked(NULL, -1, raw, raw_max + 1, 16, + &body, &body_len, NULL); + free(raw); + + REQUIRE(ok_rc == WOLFCERT_OK && ok_body); + REQUIRE(big_rc == WOLFCERT_ERR_PROTOCOL); + REQUIRE(sum_rc == WOLFCERT_ERR_PROTOCOL); + REQUIRE(full_rc == WOLFCERT_ERR_PROTOCOL); + REQUIRE(over_rc == WOLFCERT_ERR_PROTOCOL); + REQUIRE(body == NULL); + return 0; +} +#endif + +/* Feeding a chunked body a byte at a time gives the same verdict as whole. */ +static int test_chunked_complete_resume(void) +{ + static const char ok[] = "4;x=1\r\nabcd\r\n10\r\n0123456789abcdef\r\n" + "0\r\nX-A: 1\r\nX-B: 2\r\n\r\n"; + static const char bad[] = "4\r\nabcdX\r\n0\r\n\r\n"; + WolfCertChunkScan st = { 0 }; + size_t n; + int r = 0; + + for (n = 1; n < sizeof(ok) - 1; n++) { + r = wolfcert_http_chunked_complete((const uint8_t*)ok, n, &st); + REQUIRE(r == 0); + } + REQUIRE(wolfcert_http_chunked_complete((const uint8_t*)ok, n, &st) == 1); + + memset(&st, 0, sizeof(st)); + for (n = 1; n < sizeof(bad); n++) { + r = wolfcert_http_chunked_complete((const uint8_t*)bad, n, &st); + if (r != 0) + break; + } + REQUIRE(r == -1 && n == 9); + return 0; +} + int main(void) { /* make check has no per-test timeout, and a framing bug shows as a hang. */ @@ -1188,8 +1259,14 @@ int main(void) REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_url_parser()) return 1; +#ifdef WOLFCERT_HAVE_SERVER + if (test_server_read_chunked_caps()) + return 1; +#endif if (test_url_origin()) return 1; + if (test_chunked_complete_resume()) + return 1; if (test_loopback_http()) return 1; if (test_session_retry_after_reset())