From 768f0775f171ac9424992c402b8cbecaa2c5d4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 1/8] Match HTTP header names without locale folding The SCEP server compared header names with a prefix strncasecmp, so a Content-Length-Foo field set the body length. It now uses the EST server's exact match, moved to http.c as wolfcert_http_hdr_is, which the client's find_header() also calls. A second helper, wolfcert_http_hdr_split, rejects whitespace before the colon (RFC 9112 section 5.1) and returns the value after leading whitespace, so both servers enforce that rule and parse header values from one place. strncasecmp also folds case by the C locale, which breaks on a Turkish locale where 'I' does not lower to 'i'. The SCEP server's Connection value, the SCEP client's GetCACaps keywords and the EST server's Basic scheme now use wolfcert_ascii_ncasecmp, which leaves no strncasecmp in src/ and lets the three files drop . The SCEP roundtrip test's two raw-request helpers now share one send-and-read function. --- src/est/est_server.c | 50 ++++------------ src/http.c | 53 +++++++++++----- src/internal.h | 6 ++ src/scep/scep_client.c | 3 +- src/scep/scep_server.c | 25 ++++---- tests/integration/test_scep_roundtrip.c | 80 +++++++++++++++++-------- 6 files changed, 121 insertions(+), 96 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index 0946de2e..d450d5a9 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -39,7 +39,6 @@ #include #include #include -#include #include #include #include @@ -228,15 +227,6 @@ 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_in(WolfCertServer* s, int fd, EstRequest* out, void* heap, char* buf, size_t buf_sz) { @@ -285,47 +275,29 @@ static int parse_request_in(WolfCertServer* s, int fd, EstRequest* out, int chunked = 0; while (read_line(&p, end, &line, &llen) == 0 && llen > 0) { - const char* hc = memchr(line, ':', llen); + const char* v; + size_t vlen; - /* RFC 9112 section 5.1: whitespace before the colon is a 400. */ - if (hc != NULL && hc > line && (hc[-1] == ' ' || hc[-1] == '\t')) + if (wolfcert_http_hdr_split(line, llen, &v, &vlen) != 0) return WOLFCERT_ERR_PROTOCOL; - if (hdr_is(line, llen, "Content-Length")) { - out->content_length = (size_t)strtoul(hc + 1, NULL, 10); + if (wolfcert_http_hdr_is(line, llen, "Content-Length")) { + out->content_length = (size_t)strtoul(v, 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); + else if (wolfcert_http_hdr_is(line, llen, "Transfer-Encoding")) { if (vlen >= 7 && wolfcert_ascii_ncasecmp(v, "chunked", 7) == 0) chunked = 1; } - else if (hdr_is(line, llen, "Authorization")) { + else if (wolfcert_http_hdr_is(line, llen, "Authorization")) { if (out->auth_header != NULL) return WOLFCERT_ERR_PROTOCOL; - const char* val = hc + 1; - while (val < line + llen && (*val == ' ' || *val == '\t')) { - ++val; - } - - 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); + memcpy(out->auth_header, v, 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); + else if (wolfcert_http_hdr_is(line, llen, "Connection")) { if (vlen >= 5 && wolfcert_ascii_ncasecmp(v, "close", 5) == 0) out->connection_close = 1; } @@ -542,8 +514,8 @@ static int check_basic_auth(const WolfCertServer* s, const char* auth_header) if (auth_header == NULL) return 0; - if (strncasecmp(auth_header, EST_BASIC_AUTH_SCHEME, - EST_BASIC_AUTH_SCHEME_LEN) != 0) + if (wolfcert_ascii_ncasecmp(auth_header, EST_BASIC_AUTH_SCHEME, + EST_BASIC_AUTH_SCHEME_LEN) != 0) return 0; size_t ul = strlen(s->cfg_basic_user); diff --git a/src/http.c b/src/http.c index db22f020..1df62142 100644 --- a/src/http.c +++ b/src/http.c @@ -79,6 +79,36 @@ int wolfcert_ascii_ncasecmp(const char* a, const char* b, size_t n) return 0; } +int wolfcert_http_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; +} + +int wolfcert_http_hdr_split(const char* line, size_t llen, const char** val, + size_t* vlen) +{ + const char* end = line + llen; + const char* v = (const char*)memchr(line, ':', llen); + + *val = NULL; + *vlen = 0; + if (v == NULL) + return 0; + if (v > line && (v[-1] == ' ' || v[-1] == '\t')) + return -1; + + ++v; + while (v < end && (*v == ' ' || *v == '\t')) + ++v; + + *val = v; + *vlen = (size_t)(end - v); + return 0; +} + WOLFCERT_TEST_VIS void wolfcert_http_url_free(WolfCertUrl* u) { if (u == NULL) @@ -566,21 +596,18 @@ static int dyn_append(DynBuf* d, const void* data, size_t len) static char* find_header(const char* headers, const char* name, void* heap) { - size_t nlen = strlen(name); const char* p = headers; while (*p) { - if (wolfcert_ascii_ncasecmp(p, name, nlen) == 0 && p[nlen] == ':') { - const char* v = p + nlen + 1; - while (*v == ' ' || *v == '\t') { - ++v; - } - - const char* e = strstr(v, "\r\n"); - if (e == NULL) - return NULL; + const char* nl = strstr(p, "\r\n"); + if (nl == NULL) + break; - size_t n = (size_t)(e - v); + size_t llen = (size_t)(nl - p); + const char* v; + size_t n; + if (wolfcert_http_hdr_is(p, llen, name) && + wolfcert_http_hdr_split(p, llen, &v, &n) == 0) { char* s = (char*)WOLFCERT_XMALLOC(n + 1, heap); if (s == NULL) return NULL; @@ -591,10 +618,6 @@ static char* find_header(const char* headers, const char* name, void* heap) return s; } - const char* nl = strstr(p, "\r\n"); - if (nl == NULL) - break; - p = nl + 2; } return NULL; diff --git a/src/internal.h b/src/internal.h index 018c1379..f3f1c1d8 100644 --- a/src/internal.h +++ b/src/internal.h @@ -228,6 +228,12 @@ WOLFCERT_TEST_VIS int wolfcert_http_url_origin(const WolfCertUrl* u, 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 ':'. */ +int wolfcert_http_hdr_is(const char* line, size_t llen, const char* name); +/* -1 on whitespace before the colon (RFC 9112 section 5.1), else 0 with the + * value after leading whitespace, or *val NULL when the line has no colon. */ +int wolfcert_http_hdr_split(const char* line, size_t llen, const char** val, + size_t* vlen); /* `_encode` emits one line; `_encode_mime` wraps at 64 columns, which libest's * BIO_f_base64 body parser requires. `_decode` accepts both. */ diff --git a/src/scep/scep_client.c b/src/scep/scep_client.c index 091d8bd6..906bc72a 100644 --- a/src/scep/scep_client.c +++ b/src/scep/scep_client.c @@ -41,7 +41,6 @@ #include #include -#include static char* append_query(const char* base, const char* op, void* heap) { @@ -95,7 +94,7 @@ static int has_cap(const char* body, size_t len, const char* needle) if (tlen > 0 && body[start + tlen - 1] == '\r') --tlen; - if (tlen == nl && strncasecmp(body + start, needle, nl) == 0) + if (tlen == nl && wolfcert_ascii_ncasecmp(body + start, needle, nl) == 0) return 1; if (i < len) diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 40ad6b55..53dcf80b 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -37,7 +37,6 @@ #include #include #include -#include #include #include @@ -212,21 +211,17 @@ static int read_request(WolfCertServer* s, int fd, ScepRequest* out, void* heap) } while (read_line(&p, end, &line, &llen) == 0 && llen > 0) { - if (llen > 14 && strncasecmp(line, "Content-Length", 14) == 0) { - char* c = memchr(line, ':', llen); - if (c) - out->content_length = (size_t)strtoul(c + 1, NULL, 10); + const char* v; + size_t vlen; + + 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 (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; - } + else if (wolfcert_http_hdr_is(line, llen, "Connection")) { + if (vlen >= 5 && wolfcert_ascii_ncasecmp(v, "close", 5) == 0) + out->connection_close = 1; } } diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 79abbc66..890cda4b 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -74,23 +74,20 @@ static int write_all_fd(int fd, const uint8_t* buf, size_t len) return 0; } -/* Raw HTTP/1.1 request to the loopback SCEP server, for malformed-request - * 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, +/* 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, uint8_t** out_body, size_t* out_body_len) { struct sockaddr_in addr; struct timeval tv; - char hdr[512]; uint8_t* resp = NULL; size_t resp_len = 0; size_t resp_cap = 0; const uint8_t* sep; int eof = 0; int fd; - int n; ssize_t r; int status = -1; @@ -117,24 +114,7 @@ static int raw_http_req(uint16_t port, const char* method, const char* target, return -1; } - 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", - content_type != NULL ? "Content-Type: " : "", - content_type != NULL ? content_type : "", - content_type != NULL ? "\r\n" : "", - body_len); - 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"); - if (n < 0 || (size_t)n >= sizeof(hdr)) { - close(fd); - return -1; - } - if (write_all_fd(fd, (const uint8_t*)hdr, (size_t)n) != 0 || + if (write_all_fd(fd, (const uint8_t*)hdr, hdr_len) != 0 || (body_len > 0 && write_all_fd(fd, body, body_len) != 0)) { close(fd); return -1; @@ -187,6 +167,47 @@ static int raw_http_req(uint16_t port, const char* method, const char* target, return status; } +/* Raw HTTP/1.1 request to the loopback SCEP server, for malformed-request + * 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, + uint8_t** out_body, size_t* out_body_len) +{ + char hdr[512]; + int n; + + 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", + content_type != NULL ? "Content-Type: " : "", + content_type != NULL ? content_type : "", + content_type != NULL ? "\r\n" : "", + body_len); + 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"); + if (n < 0 || (size_t)n >= sizeof(hdr)) { + if (out_body != NULL && out_body_len != NULL) { + *out_body = NULL; + *out_body_len = 0; + } + return -1; + } + + return raw_exchange(port, hdr, (size_t)n, body, body_len, + 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); +} + static int raw_http_status(uint16_t port, const char* target, const char* body) { return raw_http_req(port, "GET", target, NULL, @@ -1657,6 +1678,15 @@ int main(void) REQUIRE(wolfcert_scep_get_ca_caps(&tls_cli, &tls_caps) != WOLFCERT_ERR_TLS); } + /* A longer field name that starts with Content-Length is not one. */ + REQUIRE(raw_request_status(wolfcert_server_port(s), + "GET /scep?operation=GetCACaps HTTP/1.1\r\n" + "Host: 127.0.0.1\r\nContent-Length-Foo: 4\r\n" + "Connection: close\r\n\r\n") == 200); + REQUIRE(raw_request_status(wolfcert_server_port(s), + "GET /scep?operation=GetCACaps HTTP/1.1\r\n" + "Host : 127.0.0.1\r\nConnection: close\r\n\r\n") == 400); + /* Malformed GET PKIOperation requests get 400 (RFC 8894 section 4.1). */ REQUIRE(raw_http_status(wolfcert_server_port(s), "/scep?operation=PKIOperation", NULL) == 400); /* no message= */ From 13b158776d1b1facb08320fc213c7a8d9bbfaf2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 2/8] Reject non-PrintableString C and serialNumber in the CSR subject wolfSSL encodes the country and serialNumber RDNs as PrintableString, and wolfcert_csr_build copied any value into them, so "C=U_" or a serialNumber with '@' produced malformed DER. Both now fail with WOLFCERT_ERR_BAD_ARG. emailAddress, which wolfSSL encodes as IA5String, now rejects bytes above 0x7F the same way. The subject_dn contract in types.h lists both rejections. --- src/csr.c | 14 ++++++++++++++ tests/unit/test_csr.c | 34 ++++++++++++++++++++++++++++++++++ wolfcert/types.h | 5 ++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/csr.c b/src/csr.c index 5b95afa4..8b3b3eb1 100644 --- a/src/csr.c +++ b/src/csr.c @@ -69,6 +69,20 @@ static int assign_rdn(CertName* subject, const char* key, size_t klen, return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "csr", "subject %.*s is %zu bytes, limit %zu", (int)klen, key, vlen, rdn_fields[i].cap - 1); + /* wolfSSL encodes these two as PrintableString. */ + if ((rdn_fields[i].off == offsetof(CertName, country) || + rdn_fields[i].off == offsetof(CertName, serialDev)) && + !wolfcert_is_printable_string((const uint8_t*)val, vlen)) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "csr", + "subject %.*s is not a PrintableString", (int)klen, key); + /* And emailAddress as IA5String, which is 7-bit. */ + if (rdn_fields[i].off == offsetof(CertName, email)) { + for (size_t j = 0; j < vlen; ++j) { + if ((unsigned char)val[j] >= 0x80) + return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "csr", + "subject %.*s is not an IA5String", (int)klen, key); + } + } memcpy(dst, val, vlen); dst[vlen] = '\0'; return WOLFCERT_OK; diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index 02df008e..f3e33b9c 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -204,6 +204,38 @@ static int csr_build_rejects_oversized_rdn(void) #endif #ifdef WOLFCERT_HAVE_ECC +/* C and serialNumber outside the PrintableString set, and a non-ASCII + * emailAddress, are BAD_ARG. */ +static int csr_build_rejects_unprintable_rdn(void) +{ + static const char* const bad[] = { + "C=U_", "CN=dev,serialNumber=ab_c@1", + "CN=dev,emailAddress=j\xc3\xbcrgen@example.de" + }; + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertCertMeta meta = { 0 }; + WolfCertBuffer der = { 0 }; + size_t i; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + + for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { + meta.subject_dn = bad[i]; + REQUIRE(wolfcert_csr_build(key, &meta, &der) == WOLFCERT_ERR_BAD_ARG); + REQUIRE(der.data == NULL && der.len == 0); + } + + meta.subject_dn = "C=US,serialNumber=AB-12 (x),emailAddress=dev@example.org"; + REQUIRE(wolfcert_csr_build(key, &meta, &der) == WOLFCERT_OK); + REQUIRE(der.len > 0); + + wolfcert_buffer_free(&der); + wolfcert_key_free(key); + return 0; +} + /* Self-sign the subject and SAN set in c with the test's ECC key, then free * c; returns the DER length. */ static int make_self_cert(Cert* c, const WolfCertKey* key, byte* out, int cap) @@ -851,6 +883,8 @@ int main(void) return 1; #endif #ifdef WOLFCERT_HAVE_ECC + if (csr_build_rejects_unprintable_rdn()) + return 1; if (csr_build_rejects_oversized_rdn()) return 1; if (renewal_keeps_empty_subject(1) || renewal_keeps_empty_subject(0)) diff --git a/wolfcert/types.h b/wolfcert/types.h index 26e8c630..a2466986 100644 --- a/wolfcert/types.h +++ b/wolfcert/types.h @@ -100,7 +100,10 @@ typedef struct { * Values cannot hold ','; * customize can set one. A * CTC_NAME_SIZE-byte or longer - * value is WOLFCERT_ERR_BAD_ARG. */ + * value, a C or serialNumber + * that is not a PrintableString, + * or a non-ASCII emailAddress is + * WOLFCERT_ERR_BAD_ARG. */ const char* const* san_dns; /* array of length san_dns_len */ size_t san_dns_len; const char* const* san_ip; /* textual IPv4/IPv6 */ From abf5455ffed631b7ac207e878a0b4b843c7e61d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 3/8] Check the GetCACaps result in the SCEP CLI enroll and getcert ignored the return of wolfcert_scep_get_ca_caps. A non-200 reply still means a CA without capabilities and the command goes on, now with a warning that the content cipher falls back to 3DES unless --content-cipher is given. Any other failure, such as an I/O or TLS error, stops the command instead of enrolling with empty caps, and both commands name GetCACaps in the error. The scep.h prototype now documents the non-200 return, and the SCEP example checks the result the same way. --- cli/wolfcert_client.c | 27 +++++++++++++++++++++++++-- examples/enroll_scep.c | 8 ++++++-- wolfcert/scep.h | 2 ++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/cli/wolfcert_client.c b/cli/wolfcert_client.c index bcb9e2d3..71a48c08 100644 --- a/cli/wolfcert_client.c +++ b/cli/wolfcert_client.c @@ -1011,6 +1011,23 @@ static int scep_resolve_ca(const WolfCertServerCfg* srv, const CaPin* pin, return rc; } +/* Carry on without capabilities on a non-200 GetCACaps reply. */ +static int scep_fetch_caps(const WolfCertServerCfg* srv, WolfCertScepCaps* caps, + const char* cmd) +{ + int rc = wolfcert_scep_get_ca_caps(srv, caps); + + if (rc == WOLFCERT_ERR_HTTP) { + fprintf(stderr, "%s: GetCACaps failed, assuming a CA without " + "capabilities (3DES unless --content-cipher)\n", cmd); + rc = WOLFCERT_OK; + } + else if (rc != WOLFCERT_OK) { + fprintf(stderr, "%s: GetCACaps: %s\n", cmd, wolfcert_strerror(rc)); + } + return rc; +} + /* Fetch the CA, resolve any pin against it, then run PKCSReq and any polling. */ static int scep_enroll(const Opts* opts, const WolfCertServerCfg* srv, const CaPin* pin, const WolfCertKey* key, @@ -1029,8 +1046,9 @@ static int scep_enroll(const Opts* opts, const WolfCertServerCfg* srv, rc = scep_resolve_ca(srv, pin, "enroll", &ca_pem, &ca_bundle, &ca_der, &bundle, &bundle_len); + if (rc == WOLFCERT_OK) + rc = scep_fetch_caps(srv, &caps, "enroll"); if (rc == WOLFCERT_OK) { - wolfcert_scep_get_ca_caps(srv, &caps); rc = wolfcert_scep_pkcs_req_ex(srv, &caps, ca_der->buffer, ca_der->length, bundle, bundle_len, key, csr->data, csr->len, &scep_result); @@ -1699,7 +1717,12 @@ static int cmd_getcert(int argc, char** argv) } if (ret == 0) { - wolfcert_scep_get_ca_caps(&srv, &caps); + rc = scep_fetch_caps(&srv, &caps, "getcert"); + if (rc != WOLFCERT_OK) + ret = 2; + } + + if (ret == 0) { rc = wolfcert_scep_get_cert(&srv, &caps, ra_der->buffer, ra_der->length, bundle, bundle_len, signer_der->buffer, signer_der->length, diff --git a/examples/enroll_scep.c b/examples/enroll_scep.c index c0027610..def27121 100644 --- a/examples/enroll_scep.c +++ b/examples/enroll_scep.c @@ -101,9 +101,13 @@ int main(int argc, char** argv) return 2; } - /* 2) Optionally query capabilities. */ + /* 2) Query capabilities; carry on without them on a non-200. */ WolfCertScepCaps caps = { 0 }; - wolfcert_scep_get_ca_caps(&srv, &caps); + rc = wolfcert_scep_get_ca_caps(&srv, &caps); + if (rc != WOLFCERT_OK && rc != WOLFCERT_ERR_HTTP) { + fprintf(stderr, "GetCACaps failed: %s\n", wolfcert_strerror(rc)); + return 2; + } /* 3) Generate RSA key + CSR. */ WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_RSA, .param = 2048, diff --git a/wolfcert/scep.h b/wolfcert/scep.h index c0c8a27d..e996e5a7 100644 --- a/wolfcert/scep.h +++ b/wolfcert/scep.h @@ -41,6 +41,8 @@ typedef struct { int get_next_ca_cert; /* GetCACaps: GetNextCACert */ } WolfCertScepCaps; +/* GET ?operation=GetCACaps. A non-200 reply returns WOLFCERT_ERR_HTTP with + * out_caps all zero. */ WOLFCERT_API int wolfcert_scep_get_ca_caps(const WolfCertServerCfg* srv, WolfCertScepCaps* out_caps); From 40266d606507a7e74d5b33ae4f189c368fa2c063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 4/8] Tell truncated, empty and unallocated SCEP attributes apart wolfcert_scep_parse_pki_message reported an attribute value that overruns its SET as "multi-valued", and a failed copy of an attribute or the signer certificate returned WOLFCERT_OK with the output left NULL, which surfaced later as a misleading AUTH or "Bad Message" error. A truncated value now has its own message, an empty one is rejected before it reaches XMALLOC(0), and an allocation failure returns WOLFCERT_ERR_MEMORY, which the SCEP server answers with 500. A value whose tag or length was cut short (e.g. 13, or 13 81) was skipped as if the attribute were absent, so the parse could still return WOLFCERT_OK. The SET and value headers are now read with der_read_tlv(), which also bounds the value by its SET and handles every definite length form instead of only 81 and 82, and any header it rejects is reported as truncated. test_parse_reports_oom fails each allocation in turn and requires every output to stay NULL and the result to be WOLFCERT_ERR_MEMORY, unless the failure came back from wolfSSL's PKCS#7 verify, which can report a failed allocation as a parse error. test_text_attrib_truncated covers the cut tag, the cut 81 and 82 lengths, an indefinite length, a cut SET length and a value overrunning its SET. --- src/scep/scep_msg.c | 85 +++++++-------- src/scep/scep_server.c | 4 + tests/unit/test_scep_msg.c | 206 ++++++++++++++++++++++++++++++++----- 3 files changed, 226 insertions(+), 69 deletions(-) diff --git a/src/scep/scep_msg.c b/src/scep/scep_msg.c index acdca779..55b88e23 100644 --- a/src/scep/scep_msg.c +++ b/src/scep/scep_msg.c @@ -729,6 +729,8 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, char* s; uint8_t* b; size_t off, vlen, voff; + byte tag; + int hdr; int seen = 0; int bit; int rc; @@ -775,6 +777,9 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, *out_signer_cert = sc; *out_signer_cert_len = p7->verifyCertSz; } + else { + rc = WOLFCERT_ERR_MEMORY; + } } } @@ -802,7 +807,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, if (out_fail_info) *out_fail_info = NULL; - for (a = p7->decodedAttrib; a != NULL; a = a->next) { + for (a = p7->decodedAttrib; rc == WOLFCERT_OK && a != NULL; a = a->next) { bit = scep_attr_bit(a); if (bit == 0) continue; @@ -814,48 +819,32 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, } seen |= bit; - /* Depending on the wolfSSL version, value is either the outer SET OF - * AttributeValue or the bare first AttributeValue. */ - if (a->valueSz < 2 || a->value == NULL) - continue; - v = a->value; off = 0; - - if (v[0] == 0x31) { - off = 2; - if (v[1] == 0x81) { - if (a->valueSz < 3) - continue; - off = 3; - } - else if (v[1] == 0x82) { - if (a->valueSz < 4) - continue; - off = 4; + hdr = 0; + vlen = 0; + if (v != NULL && a->valueSz > 0) { + hdr = der_read_tlv(v, a->valueSz, &tag, &vlen); + /* Depending on the wolfSSL version, value is either the outer SET + * OF AttributeValue or the bare first AttributeValue. */ + if (hdr > 0 && tag == 0x31 && vlen > 0) { + off = (size_t)hdr; + hdr = der_read_tlv(v + off, vlen, &tag, &vlen); } } - - if (off + 2 > a->valueSz) - continue; - - vlen = v[off + 1]; - voff = off + 2; - if (v[off + 1] == 0x81) { - if (off + 3 > a->valueSz) - continue; - - vlen = v[off + 2]; - voff = off + 3; - } - else if (v[off + 1] == 0x82) { - if (off + 4 > a->valueSz) - continue; - vlen = ((size_t)v[off + 2] << 8) | v[off + 3]; - voff = off + 4; + if (hdr < 0) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", + "truncated signed attribute in pkiMessage"); + break; } + voff = off + (size_t)hdr; - if (voff + vlen != a->valueSz) { + if (vlen == 0) { + rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", + "empty signed attribute in pkiMessage"); + break; + } + if (voff + vlen < a->valueSz) { rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", "multi-valued signed attribute in pkiMessage"); break; @@ -865,7 +854,7 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, * which hold no NUL. */ if ((bit & (SCEP_ATTR_MSG_TYPE | SCEP_ATTR_PKI_STATUS | SCEP_ATTR_FAIL_INFO)) != 0 && - (v[off] != 0x13 || memchr(v + voff, 0, vlen) != NULL)) { + (tag != 0x13 || memchr(v + voff, 0, vlen) != NULL)) { rc = WOLFCERT_ERR(WOLFCERT_ERR_PROTOCOL, "scep", "signed attribute is not a PrintableString"); break; @@ -905,19 +894,23 @@ WOLFCERT_TEST_VIS int wolfcert_scep_parse_pki_message(const uint8_t* pki_der, if (out_str != NULL) { s = (char*)WOLFCERT_XMALLOC(vlen + 1, heap); - if (s != NULL) { - memcpy(s, v + off, vlen); - s[vlen] = '\0'; - *out_str = s; + if (s == NULL) { + rc = WOLFCERT_ERR_MEMORY; + break; } + memcpy(s, v + off, vlen); + s[vlen] = '\0'; + *out_str = s; } else if (out_bin != NULL) { b = (uint8_t*)WOLFCERT_XMALLOC(vlen, heap); - if (b != NULL) { - memcpy(b, v + off, vlen); - *out_bin = b; - *out_bin_len = vlen; + if (b == NULL) { + rc = WOLFCERT_ERR_MEMORY; + break; } + memcpy(b, v + off, vlen); + *out_bin = b; + *out_bin_len = vlen; } } diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index 53dcf80b..7fa50d13 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -943,6 +943,10 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) int rc = wolfcert_scep_parse_pki_message(req->body, req->body_len, &env, &tid, &tid_len, &snonce, &snonce_len, &rnonce, &rnonce_len, &mt, &ps, &signer_cert, &signer_cert_len, NULL, s->heap); + if (rc == WOLFCERT_ERR_MEMORY) { + send_text(s, fd, 500, "Server Error", "text/plain", ""); + goto out; + } if (rc != WOLFCERT_OK) { send_text(s, fd, 400, "Bad Request", "text/plain", ""); goto out; diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index d7b8c55c..f073bae9 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -764,11 +764,17 @@ static int check_result_defined(const char* what, int rc, const WolfCertScepResu #ifdef TEST_ALLOC_FAILURES /* Allocations left before failing_malloc() returns NULL; -1 never fails. */ static int g_allocs_left = -1; +static int g_alloc_failed; +static wolfSSL_Malloc_cb g_saved_malloc; +static wolfSSL_Free_cb g_saved_free; +static wolfSSL_Realloc_cb g_saved_realloc; static void* failing_malloc(size_t sz) { - if (g_allocs_left == 0) + if (g_allocs_left == 0) { + g_alloc_failed = 1; return NULL; + } if (g_allocs_left > 0) g_allocs_left--; return malloc(sz); @@ -781,35 +787,53 @@ static void failing_free(void* ptr) static void* failing_realloc(void* ptr, size_t sz) { - if (g_allocs_left == 0) + if (g_allocs_left == 0) { + g_alloc_failed = 1; return NULL; + } if (g_allocs_left > 0) g_allocs_left--; return realloc(ptr, sz); } +/* Lets `n` wolfSSL allocations succeed and fails the rest. */ +static int oom_arm(int n) +{ + if (wolfSSL_GetAllocators(&g_saved_malloc, &g_saved_free, + &g_saved_realloc) != 0) + return -1; + g_allocs_left = n; + g_alloc_failed = 0; + return wolfSSL_SetAllocators(failing_malloc, failing_free, + failing_realloc); +} + +/* Restores the allocators; 1 when an allocation was failed, 0 if not, or -1. */ +static int oom_disarm(void) +{ + g_allocs_left = -1; + if (wolfSSL_SetAllocators(g_saved_malloc, g_saved_free, + g_saved_realloc) != 0) + return -1; + return g_alloc_failed; +} + /* Failing each allocation in turn must give WOLFCERT_ERR_MEMORY or a match. */ 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 failed; 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); + REQUIRE(oom_arm(fails) == 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) { + REQUIRE((failed = oom_disarm()) >= 0); + if (rc != 1 && (rc != WOLFCERT_ERR_MEMORY || !failed)) { fprintf(stderr, "FAIL pem_has_cert with allocation %d failing: " "%d\n", fails, rc); return 1; @@ -822,22 +846,16 @@ static int pem_has_cert_reports_oom(const char* pem, size_t pem_len, 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 failed; 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); + REQUIRE(oom_arm(fails) == 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) { + REQUIRE((failed = oom_disarm()) >= 0); + if (rc != WOLFCERT_OK && (rc != WOLFCERT_ERR_MEMORY || !failed)) { fprintf(stderr, "FAIL verify_rep_signer with allocation %d " "failing: %d\n", fails, rc); return 1; @@ -2244,6 +2262,7 @@ static int check_text_attrib(const uint8_t* ca_der, size_t ca_len, REQUIRE(make_signed_with_attribs(ca_der, ca_len, ca_key, ca_key_len, &attrib, 1, &msg, &msg_len) == 0); + wolfcert_clear_error(); rc = wolfcert_scep_parse_pki_message(msg, msg_len, &env, NULL, NULL, NULL, NULL, NULL, NULL, &mt, &ps, NULL, NULL, &fi, NULL); @@ -2272,6 +2291,8 @@ static int test_text_attrib_printable(void) static const byte ps_nul[] = { 0x13, 0x02, '2', 0x00 }; static const byte fi_nul[] = { 0x13, 0x03, '0', 0x00, 'x' }; static const byte mt_utf8[] = { 0x0C, 0x01, '3' }; + static const byte ps_short[] = { 0x13, 0x05, '2' }; + static const byte ps_empty[] = { 0x13, 0x00 }; uint8_t* ca_der = NULL; size_t ca_len = 0; uint8_t* ca_key = NULL; @@ -2294,7 +2315,22 @@ static int test_text_attrib_printable(void) check_text_attrib(ca_der, ca_len, ca_key, ca_key_len, scep_oid_msg_type, sizeof(scep_oid_msg_type), mt_utf8, sizeof(mt_utf8), - WOLFCERT_ERR_PROTOCOL, NULL) != 0) { + WOLFCERT_ERR_PROTOCOL, NULL) != 0 || + check_text_attrib(ca_der, ca_len, ca_key, ca_key_len, + scep_oid_pki_status, sizeof(scep_oid_pki_status), + ps_short, sizeof(ps_short), + WOLFCERT_ERR_PROTOCOL, NULL) != 0 || + strstr(wolfcert_last_error_message(), "truncated") == NULL || + check_text_attrib(ca_der, ca_len, ca_key, ca_key_len, + scep_oid_pki_status, sizeof(scep_oid_pki_status), + ps_empty, sizeof(ps_empty), + WOLFCERT_ERR_PROTOCOL, NULL) != 0 || + strstr(wolfcert_last_error_message(), "empty") == NULL || + check_text_attrib(ca_der, ca_len, ca_key, ca_key_len, + scep_oid_pki_status, sizeof(scep_oid_pki_status), + ps_empty, 0, WOLFCERT_ERR_PROTOCOL, NULL) != 0 || + strstr(wolfcert_last_error_message(), "empty") == NULL) { + fprintf(stderr, "text attribute: %s\n", wolfcert_last_error_message()); ret = 1; } @@ -2303,6 +2339,124 @@ static int test_text_attrib_printable(void) return ret; } +/* A tag or length cut short, or a value overrunning its SET, is truncated. */ +static int test_text_attrib_truncated(void) +{ + static const byte cut_tag[] = { 0x13 }; + static const byte cut_len81[] = { 0x13, 0x81 }; + static const byte cut_len82[] = { 0x13, 0x82, 0x00 }; + static const byte indef[] = { 0x13, 0x80, '2', 0x00, 0x00 }; + static const byte cut_set[] = { 0x31, 0x81 }; + static const byte over_set[] = { 0x31, 0x03, 0x13, 0x05, '2' }; + static const struct { + const byte* v; + word32 len; + } cases[] = { + { cut_tag, sizeof(cut_tag) }, + { cut_len81, sizeof(cut_len81) }, + { cut_len82, sizeof(cut_len82) }, + { indef, sizeof(indef) }, + { cut_set, sizeof(cut_set) }, + { over_set, sizeof(over_set) }, + }; + uint8_t* ca_der = NULL; + size_t ca_len = 0; + uint8_t* ca_key = NULL; + size_t ca_key_len = 0; + size_t i; + int ret = 0; + + REQUIRE(make_ca(&ca_der, &ca_len, &ca_key, &ca_key_len) == 0); + + for (i = 0; ret == 0 && i < sizeof(cases) / sizeof(cases[0]); i++) { + if (check_text_attrib(ca_der, ca_len, ca_key, ca_key_len, + scep_oid_pki_status, sizeof(scep_oid_pki_status), + cases[i].v, cases[i].len, + WOLFCERT_ERR_PROTOCOL, NULL) != 0 || + strstr(wolfcert_last_error_message(), "truncated") == NULL) { + fprintf(stderr, "truncated case %zu: %s\n", i, + wolfcert_last_error_message()); + ret = 1; + } + } + + free(ca_der); + free(ca_key); + return ret; +} + +#ifdef TEST_ALLOC_FAILURES +/* Failing each allocation in turn must give an error or a parse with every + * attribute present. */ +static int test_parse_reports_oom(void) +{ + static const byte msg_type[] = { 0x13, 0x02, '1', '9' }; + static const byte tid[] = { 0x13, 0x01, 'A' }; + PKCS7Attrib attribs[2]; + uint8_t* ca_der = NULL; + size_t ca_len = 0; + uint8_t* ca_key = NULL; + size_t ca_key_len = 0; + uint8_t* msg = NULL; + size_t msg_len = 0; + int fails; + int failed; + int rc = WOLFCERT_ERR_MEMORY; + int ret = 0; + + attribs[0].oid = scep_oid_msg_type; + attribs[0].oidSz = sizeof(scep_oid_msg_type); + attribs[0].value = msg_type; + attribs[0].valueSz = sizeof(msg_type); + attribs[1].oid = scep_oid_trans_id; + attribs[1].oidSz = sizeof(scep_oid_trans_id); + attribs[1].value = tid; + attribs[1].valueSz = sizeof(tid); + REQUIRE(make_ca(&ca_der, &ca_len, &ca_key, &ca_key_len) == 0); + REQUIRE(make_signed_with_attribs(ca_der, ca_len, ca_key, ca_key_len, + attribs, 2, &msg, &msg_len) == 0); + + for (fails = 0; ret == 0 && rc != WOLFCERT_OK; fails++) { + WolfCertBuffer env = { 0 }; + uint8_t* t = NULL; + size_t t_len = 0; + uint8_t* signer = NULL; + size_t signer_len = 0; + char* mt = NULL; + + wolfcert_clear_error(); + REQUIRE(oom_arm(fails) == 0); + rc = wolfcert_scep_parse_pki_message(msg, msg_len, &env, &t, &t_len, + NULL, NULL, NULL, NULL, &mt, NULL, &signer, &signer_len, + NULL, NULL); + REQUIRE((failed = oom_disarm()) >= 0); + /* wolfSSL's PKCS#7 verify can report a failed allocation as a parse + * error, so only wolfCert's own allocations must give ERR_MEMORY. */ + if (rc == WOLFCERT_OK ? + (t == NULL || mt == NULL || signer == NULL) : + (!failed || + (rc != WOLFCERT_ERR_MEMORY && + wolfcert_last_wolfssl_err() == 0) || + t != NULL || mt != NULL || signer != NULL || + env.data != NULL)) { + fprintf(stderr, "FAIL parse_pki_message with allocation %d " + "failing: %d\n", fails, rc); + ret = 1; + } + WOLFCERT_XFREE(t, NULL); + WOLFCERT_XFREE(signer, NULL); + WOLFCERT_XFREE(mt, NULL); + wolfcert_buffer_free(&env); + } + + free(msg); + free(ca_der); + free(ca_key); + REQUIRE(fails > 1); + return ret; +} +#endif + int main(void) { REQUIRE(test_static_mem_init() == 0); @@ -2353,8 +2507,14 @@ int main(void) return 1; if (test_multi_value_signed_attrib()) return 1; +#ifdef TEST_ALLOC_FAILURES + if (test_parse_reports_oom()) + return 1; +#endif if (test_text_attrib_printable()) return 1; + if (test_text_attrib_truncated()) + return 1; #ifdef HAVE_ECC if (test_envelop_rejects_ecc_ra()) return 1; From 51422e5e551eb5606c28a31adff2cda0bdc7a608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 5/8] Handle SHA-384 and SHA-512 missing from wolfSSL in the CSR The CSR signature type picked SHA-384 for P-384 and SHA-512 for P-521, and honoured a preferred_hash of 384 or 512, without checking the wolfSSL build had those hashes, so signing failed on a build without them (est_csr_attrs_apply_roundtrip fails on main against one). A hash taken from the curve now steps down to the strongest one wolfSSL has, so P-521 takes SHA-384 when only SHA-512 is missing. A preferred_hash the build lacks fails with WOLFCERT_ERR_UNSUPPORTED instead: the application or the server's CSR attributes asked for that hash, and a CA enforcing it would reject a SHA-256 CSR with a generic 400 or 401. test_csr checks the hash of a P-256, P-384 and P-521 CSR, and of a P-256 CSR with each preferred_hash, against what the wolfSSL build provides. est_csr_attrs_apply_roundtrip expects WOLFCERT_ERR_UNSUPPORTED for its SHA-384 policy on a build without SHA-384. --- src/csr.c | 48 ++++++-- .../test_est_csr_attrs_apply_roundtrip.c | 14 +++ tests/unit/test_csr.c | 105 ++++++++++++++++++ wolfcert/types.h | 3 +- 4 files changed, 158 insertions(+), 12 deletions(-) diff --git a/src/csr.c b/src/csr.c index 8b3b3eb1..6e6a3762 100644 --- a/src/csr.c +++ b/src/csr.c @@ -181,20 +181,37 @@ static int build_san_seq(const WolfCertCertMeta* meta, Cert* cert, void* heap) return WOLFCERT_OK; } -/* RFC 5480 pairs P-256, P-384 and P-521 with SHA-256, SHA-384 and SHA-512. */ -#ifdef WOLFCERT_HAVE_ECC -static int ecdsa_sig_for_curve(int curve_id) +/* 1 when the wolfSSL build has SHA-. */ +static int sha2_in_build(int bits) { - switch (curve_id) { - case ECC_SECP384R1: - return CTC_SHA384wECDSA; - case ECC_SECP521R1: - return CTC_SHA512wECDSA; - case ECC_SECP256R1: + switch (bits) { + case 256: + return 1; +#ifdef WOLFSSL_SHA384 + case 384: + return 1; +#endif +#ifdef WOLFSSL_SHA512 + case 512: + return 1; +#endif default: - return CTC_SHA256wECDSA; + return 0; } } + +/* RFC 5480 pairs P-256, P-384 and P-521 with SHA-256, SHA-384 and SHA-512; + * a hash wolfSSL lacks steps down to the next one. */ +#ifdef WOLFCERT_HAVE_ECC +static int ecdsa_sig_for_curve(int curve_id) +{ + if (curve_id == ECC_SECP521R1 && sha2_in_build(512)) + return CTC_SHA512wECDSA; + if ((curve_id == ECC_SECP521R1 || curve_id == ECC_SECP384R1) && + sha2_in_build(384)) + return CTC_SHA384wECDSA; + return CTC_SHA256wECDSA; +} #endif /* CTC_* signature type for a hash size in bits, or 0 when the key's family @@ -239,6 +256,10 @@ static int choose_sig_type(const WolfCertKey* key, const WolfCertKeyAlg* alg, { if (meta != NULL && meta->preferred_hash != 0) { int sig = sig_type_for_hash(key->type, meta->preferred_hash); + if (sig != 0 && !sha2_in_build(meta->preferred_hash)) + return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "csr", + "preferred_hash %d: wolfSSL lacks SHA-%d", + meta->preferred_hash, meta->preferred_hash); if (sig != 0) return sig; } @@ -422,7 +443,12 @@ int wolfcert_csr_build_ex(const WolfCertKey* key, const WolfCertCertMeta* meta, wc_CertFree(cert); return WOLFCERT_ERR_UNSUPPORTED; } - cert->sigType = choose_sig_type(key, alg, meta); + int sig_type = choose_sig_type(key, alg, meta); + if (sig_type < 0) { + wc_CertFree(cert); + return sig_type; + } + cert->sigType = sig_type; size_t der_cap = alg->der_cap_hint + 1024; if (key->type == WOLFCERT_KEY_RSA) { diff --git a/tests/integration/test_est_csr_attrs_apply_roundtrip.c b/tests/integration/test_est_csr_attrs_apply_roundtrip.c index 6373b9fe..85360763 100644 --- a/tests/integration/test_est_csr_attrs_apply_roundtrip.c +++ b/tests/integration/test_est_csr_attrs_apply_roundtrip.c @@ -152,6 +152,13 @@ static int auto_apply_pins_everything(WolfCertServer* s) WolfCertBuffer cert_pem = { 0 }; int rc = wolfcert_client_enroll(cli, &srv, &key_cfg, &meta, &key, &cert_pem); +#ifndef WOLFSSL_SHA384 + REQUIRE(rc == WOLFCERT_ERR_UNSUPPORTED); + wolfcert_buffer_free(&cert_pem); + wolfcert_key_free(key); + wolfcert_client_free(cli); + return 0; +#endif REQUIRE(rc == WOLFCERT_OK); REQUIRE(key != NULL); REQUIRE(wolfcert_key_type(key) == WOLFCERT_KEY_ECC); @@ -197,6 +204,13 @@ static int explicit_caller_wins(WolfCertServer* s) WolfCertBuffer cert_pem = { 0 }; int rc = wolfcert_client_enroll(cli, &srv, &key_cfg, &meta, &key, &cert_pem); +#ifndef WOLFSSL_SHA384 + REQUIRE(rc == WOLFCERT_ERR_UNSUPPORTED); + wolfcert_buffer_free(&cert_pem); + wolfcert_key_free(key); + wolfcert_client_free(cli); + return 0; +#endif REQUIRE(rc == WOLFCERT_OK); REQUIRE(wolfcert_key_type(key) == WOLFCERT_KEY_RSA); diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index f3e33b9c..ea088f72 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -84,6 +84,107 @@ static int build_and_reparse(WolfCertKeyType kt, int param) return 0; } +#ifdef WOLFCERT_HAVE_ECC +/* An ECC CSR uses its curve's hash, or the strongest one wolfSSL has. */ +static int ecc_sig_follows_curve(void) +{ + static const struct { int param; int sig; } want[] = { + { 256, CTC_SHA256wECDSA }, +#if defined(HAVE_ECC384) || defined(HAVE_ALL_CURVES) +#ifdef WOLFSSL_SHA384 + { 384, CTC_SHA384wECDSA }, +#else + { 384, CTC_SHA256wECDSA }, +#endif +#endif +#if defined(HAVE_ECC521) || defined(HAVE_ALL_CURVES) +#if defined(WOLFSSL_SHA512) + { 521, CTC_SHA512wECDSA }, +#elif defined(WOLFSSL_SHA384) + { 521, CTC_SHA384wECDSA }, +#else + { 521, CTC_SHA256wECDSA }, +#endif +#endif + }; + WolfCertCertMeta meta = { .subject_dn = "CN=device-1" }; + + for (size_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) { + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, + .param = want[i].param, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + WolfCertBuffer der = { 0 }; + DecodedCert dc; + int sig; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(key, &meta, &der) == WOLFCERT_OK); + wc_InitDecodedCert(&dc, der.data, (word32)der.len, NULL); + REQUIRE(wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0); + sig = (int)dc.signatureOID; + wc_FreeDecodedCert(&dc); + wolfcert_buffer_free(&der); + wolfcert_key_free(key); + if (sig != want[i].sig) { + fprintf(stderr, "FAIL P-%d signed with %d, want %d\n", + want[i].param, sig, want[i].sig); + return 1; + } + } + return 0; +} + +/* An ECC CSR signs with preferred_hash, or fails when wolfSSL lacks it. */ +static int ecc_sig_follows_preferred_hash(void) +{ + static const struct { int hash; int sig; } want[] = { + { 256, CTC_SHA256wECDSA }, +#ifdef WOLFSSL_SHA384 + { 384, CTC_SHA384wECDSA }, +#else + { 384, 0 }, +#endif +#ifdef WOLFSSL_SHA512 + { 512, CTC_SHA512wECDSA }, +#else + { 512, 0 }, +#endif + }; + WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertKey* key = NULL; + int ret = 0; + + REQUIRE(wolfcert_key_generate(&cfg, &key) == WOLFCERT_OK); + for (size_t i = 0; i < sizeof(want) / sizeof(want[0]) && ret == 0; i++) { + WolfCertCertMeta meta = { .subject_dn = "CN=device-1", + .preferred_hash = want[i].hash }; + WolfCertBuffer der = { 0 }; + DecodedCert dc; + int sig = 0; + int rc = wolfcert_csr_build(key, &meta, &der); + + if (rc == WOLFCERT_OK) { + wc_InitDecodedCert(&dc, der.data, (word32)der.len, NULL); + if (wc_ParseCert(&dc, CERTREQ_TYPE, NO_VERIFY, NULL) == 0) + sig = (int)dc.signatureOID; + wc_FreeDecodedCert(&dc); + } + if (want[i].sig == 0) + ret = rc != WOLFCERT_ERR_UNSUPPORTED || der.data != NULL; + else + ret = sig != want[i].sig; + if (ret != 0) + fprintf(stderr, "FAIL SHA-%d gave rc %d sig %d, want sig %d\n", + want[i].hash, rc, sig, want[i].sig); + wolfcert_buffer_free(&der); + } + wolfcert_key_free(key); + return ret; +} +#endif + /* UID, rfc822Name and iPAddress meta fields reach the built CSR. */ static int build_with_extras(void) { @@ -907,6 +1008,10 @@ int main(void) return 1; if (build_and_reparse(WOLFCERT_KEY_ECC, 256)) return 1; + if (ecc_sig_follows_curve()) + return 1; + if (ecc_sig_follows_preferred_hash()) + return 1; #endif #ifdef WOLFCERT_HAVE_RSA if (build_and_reparse(WOLFCERT_KEY_RSA, 2048)) diff --git a/wolfcert/types.h b/wolfcert/types.h index a2466986..b3483473 100644 --- a/wolfcert/types.h +++ b/wolfcert/types.h @@ -120,7 +120,8 @@ typedef struct { const char* challenge_password; /* Optional signature hash size in bits (256 / 384 / 512); 0 uses the key - * type's default. Ed25519, Ed448 and ML-DSA ignore it. */ + * type's default. A hash the wolfSSL build lacks is + * WOLFCERT_ERR_UNSUPPORTED. Ed25519, Ed448 and ML-DSA ignore it. */ int preferred_hash; /* Optional: invoked after standard fields are set, before signing. */ From 2321f45188319cb9d3331facc3501a5435a4e977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 6/8] Declare nanosleep in the CA store test test_server_ca_store.c includes tls_test_util.h, which calls nanosleep, but did not define _POSIX_C_SOURCE like the other tests that include it. Under -std=c11 glibc then hides the prototype, and GCC 14 and newer reject the implicit declaration. --- tests/unit/test_server_ca_store.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_server_ca_store.c b/tests/unit/test_server_ca_store.c index 27a13123..3a6bd8bf 100644 --- a/tests/unit/test_server_ca_store.c +++ b/tests/unit/test_server_ca_store.c @@ -17,6 +17,8 @@ * along with wolfCert. If not, see . */ +#define _POSIX_C_SOURCE 200809L + #include #include #include "internal.h" From 6a0290219de0dfa07b28707ceff54c7e08af250c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 7/8] Keep the monotonic clock and deadlines in 64 bits wolfcert_mono_ms and the deadlines built on it were long, which on a 32-bit POSIX host wraps after about 24.8 days of uptime and then breaks the server's request deadline, the PHA wait and the client connect timeout. --- src/est/est_server.c | 2 +- src/internal.h | 4 ++-- src/net_posix.c | 6 +++--- tests/unit/test_net.c | 14 +++----------- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/est/est_server.c b/src/est/est_server.c index d450d5a9..e587411f 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -611,7 +611,7 @@ static int ensure_post_handshake_auth(WolfCertServer* s) size_t fin_len; size_t cur_len; char probe; - long deadline; + int64_t deadline; int r; int err; diff --git a/src/internal.h b/src/internal.h index f3f1c1d8..9a3d0d8d 100644 --- a/src/internal.h +++ b/src/internal.h @@ -141,7 +141,7 @@ struct WolfCertServer { * retries in wolfcert_io_{recv,send} are allowed only then. */ int poll_timeouts_armed; void* heap; - long deadline_ms; /* 0 when none is armed */ + int64_t deadline_ms; /* 0 when none is armed */ }; /* wolfSSL_read/write on a TLS connection, else recv()/send(). */ @@ -151,7 +151,7 @@ ssize_t wolfcert_io_send(WolfCertServer* srv, int fd, const void* buf, size_t le /* Best-effort SO_NOSIGPIPE; a no-op where unsupported or fd is not a socket. */ WOLFCERT_TEST_VIS void wolfcert_sock_nosigpipe(int fd); -long wolfcert_mono_ms(void); +WOLFCERT_TEST_VIS int64_t wolfcert_mono_ms(void); WOLFCERT_API const WolfCertServerOps* wolfcert_est_server_ops(void); WOLFCERT_API const WolfCertServerOps* wolfcert_scep_server_ops(void); diff --git a/src/net_posix.c b/src/net_posix.c index f128dca9..16fb6c77 100644 --- a/src/net_posix.c +++ b/src/net_posix.c @@ -56,11 +56,11 @@ void wolfcert_sock_nosigpipe(int fd) #endif } -long wolfcert_mono_ms(void) +int64_t wolfcert_mono_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); - return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } /* timeout_ms <= 0 blocks. Returns 0 on success, -1 on error or timeout. */ @@ -133,7 +133,7 @@ int wolfcert_posix_connect(const char* host, int port, int timeout_ms, void* ctx return -1; /* timeout_ms bounds the whole connect across all candidate addresses. */ - long deadline = (timeout_ms > 0) ? wolfcert_mono_ms() + timeout_ms : 0; + int64_t deadline = (timeout_ms > 0) ? wolfcert_mono_ms() + timeout_ms : 0; int fd = -1; for (rp = res; rp != NULL; rp = rp->ai_next) { diff --git a/tests/unit/test_net.c b/tests/unit/test_net.c index af4fba79..ebc9a583 100644 --- a/tests/unit/test_net.c +++ b/tests/unit/test_net.c @@ -34,7 +34,6 @@ #include #include #include -#include #include #define REQUIRE(cond) \ @@ -45,13 +44,6 @@ } \ } while (0) -static long mono_ms(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; -} - static volatile sig_atomic_t g_sigpipe_raised; static void note_sigpipe(int sig) @@ -151,9 +143,9 @@ int main(void) close(ls); /* An unroutable address must fail well under the OS default (~75s). */ - long t0 = mono_ms(); + int64_t t0 = wolfcert_mono_ms(); int fd2 = wolfcert_posix_connect("10.255.255.1", 9, 250, NULL); - long elapsed = mono_ms() - t0; + int64_t elapsed = wolfcert_mono_ms() - t0; REQUIRE(fd2 < 0); REQUIRE(elapsed < 3000); @@ -170,6 +162,6 @@ int main(void) return 1; #endif - printf("OK (unreachable connect returned in %ldms)\n", elapsed); + printf("OK (unreachable connect returned in %ldms)\n", (long)elapsed); return 0; } From c517bf96fb54ec643a3ec507aef61abd412e23f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 8 Oct 2026 13:21:02 +0200 Subject: [PATCH 8/8] Pass the Zephyr action inputs through env The zephyr-workspace action expanded ${{ inputs.* }} straight into run scripts; they now reach the shell as environment variables. The SDK download also retries transient server errors, which have failed the qemu_x86 job before. --- .github/actions/zephyr-workspace/action.yml | 25 +++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/.github/actions/zephyr-workspace/action.yml b/.github/actions/zephyr-workspace/action.yml index 39d5776a..8663e674 100644 --- a/.github/actions/zephyr-workspace/action.yml +++ b/.github/actions/zephyr-workspace/action.yml @@ -40,9 +40,12 @@ runs: - name: Workspace cache key id: wskey shell: bash + env: + ZEPHYR_REF: ${{ inputs.zephyr-ref }} + WOLFSSL_REF: ${{ inputs.wolfssl-ref }} + SCRIPT_HASH: ${{ hashFiles('scripts/ci/zephyr-add-wolfssl.py') }} run: | - h="${{ hashFiles('scripts/ci/zephyr-add-wolfssl.py') }}" - echo "key=zephyr-ws-${RUNNER_OS}-${{ inputs.zephyr-ref }}-${{ inputs.wolfssl-ref }}-${h:0:12}-$(python3 -V | tr -d ' ')" \ + echo "key=zephyr-ws-${RUNNER_OS}-${ZEPHYR_REF}-${WOLFSSL_REF}-${SCRIPT_HASH:0:12}-$(python3 -V | tr -d ' ')" \ >> "$GITHUB_OUTPUT" - name: Restore the Zephyr workspace @@ -56,6 +59,8 @@ runs: - name: Initialise the west workspace shell: bash + env: + ZEPHYR_REF: ${{ inputs.zephyr-ref }} run: | set -euo pipefail if [ ! -x "$HOME/zephyr-venv/bin/west" ]; then @@ -64,14 +69,16 @@ runs: fi if [ ! -d "$HOME/zephyrproject/.west" ]; then "$HOME/zephyr-venv/bin/west" init -m https://github.com/zephyrproject-rtos/zephyr \ - --mr "${{ inputs.zephyr-ref }}" "$HOME/zephyrproject" + --mr "$ZEPHYR_REF" "$HOME/zephyrproject" fi - name: Add wolfSSL to the manifest shell: bash + env: + WOLFSSL_REF: ${{ inputs.wolfssl-ref }} run: | ./scripts/ci/zephyr-add-wolfssl.py \ - "$HOME/zephyrproject/zephyr/west.yml" "${{ inputs.wolfssl-ref }}" + "$HOME/zephyrproject/zephyr/west.yml" "$WOLFSSL_REF" - name: Update west modules shell: bash @@ -91,12 +98,16 @@ runs: - name: Install the Zephyr SDK shell: bash + env: + SDK_VERSION: ${{ inputs.sdk-version }} + TOOLCHAIN: ${{ inputs.toolchain }} run: | set -euo pipefail - v="${{ inputs.sdk-version }}" + v="$SDK_VERSION" cd "$HOME" - wget -q "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${v}/zephyr-sdk-${v}_linux-x86_64_minimal.tar.xz" + wget -q --tries=3 --retry-on-http-error=429,500,502,503,504 \ + "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${v}/zephyr-sdk-${v}_linux-x86_64_minimal.tar.xz" tar xf "zephyr-sdk-${v}_linux-x86_64_minimal.tar.xz" cd "zephyr-sdk-${v}" - ./setup.sh -h -c -t "${{ inputs.toolchain }}" + ./setup.sh -h -c -t "$TOOLCHAIN" echo "ZEPHYR_SDK_INSTALL_DIR=$HOME/zephyr-sdk-${v}" >> "$GITHUB_ENV"