Add RFC 9421 HTTP Message Signatures example using wolfCrypt Ed25519#566
Add RFC 9421 HTTP Message Signatures example using wolfCrypt Ed25519#566sameehj wants to merge 2 commits intowolfSSL:masterfrom
Conversation
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #566
Scan targets checked: wolfssl-examples-bugs, wolfssl-examples-src
Findings: 5
Medium (2)
Silent truncation in parse_sf_string returns success on buffer overflow
File: http-message-signatures/common/wc_sf.c:81-96
Function: parse_sf_string
Category: Buffer overflows
When a quoted sf-string exceeds maxLen - 1 characters, the while loop exits due to the i < maxLen - 1 guard, but the function still returns a non-NULL pointer (indicating success) even though *p does not point at the closing ". The output buffer contains a silently truncated value, and the returned parse pointer is left mid-string rather than past the closing quote. Callers in parse_params and wc_SfParseSigInput treat any non-NULL return as success and continue parsing from this incorrect position.
In the verification path (wc_HttpSig_Verify), the Signature-Input header is attacker-controlled. An attacker could craft a component identifier exceeding 255 bytes (the WC_SF_MAX_STRING limit) that, when truncated, collides with or resolves to a different component name, potentially causing the signature base to be constructed over different components than intended. Additionally, the mispositioned parse pointer causes subsequent token parsing to operate on arbitrary data within the string, leading to unpredictable parser behavior.
while (*p && *p != '"' && i < maxLen - 1) {
if (*p == '\\') {
p++;
if (*p != '\\' && *p != '"')
return NULL;
}
out[i++] = *p++;
}
out[i] = '\0';
if (*p == '"')
p++;
return p;Recommendation: After the loop, check whether the loop exited due to buffer exhaustion rather than finding the closing quote. If *p != '"' && *p != '\0', return NULL to signal a parse error:
out[i] = '\0';
if (*p != '"')
return NULL;
p++;
return p;Silent truncation of integers exceeding 15 digits leaves trailing digits in parse stream
File: http-message-signatures/common/wc_sf.c:101-118
Function: parse_sf_integer
Category: Buffer overflows
The parse_sf_integer function limits parsing to 15 digits via the digits < 15 loop guard. When an integer has more than 15 digits, the loop exits after consuming only the first 15, and the function returns success with the parse pointer pointing at the 16th digit. The remaining digits are left in the parse stream and will be interpreted as subsequent tokens by the caller.
In parse_params, this causes the remaining digits to be treated as the next parameter name (via parse_token), which will fail since digits are not valid key-start characters. However, depending on the exact input structure, an attacker crafting a Signature-Input header with a >15-digit integer parameter could cause the parser to enter an inconsistent state where the created timestamp is a truncated value while the remaining digits corrupt subsequent parameter parsing.
while (*p >= '0' && *p <= '9' && digits < 15) {
d = *p - '0';
if (v > (LONG_MAX - d) / 10)
return NULL;
v = v * 10 + d;
p++;
digits++;
}
if (digits == 0)
return NULL;
*val = neg ? -v : v;
return p;Recommendation: After the loop, verify that no more digits remain. If the input contains more than 15 digits, return NULL to reject the value per RFC 8941 Section 3.3.1 (integers must be at most 15 digits):
if (digits == 0)
return NULL;
if (*p >= '0' && *p <= '9')
return NULL; /* exceeds 15-digit sf-integer limit */
*val = neg ? -v : v;
return p;Low (2)
Decoded signature size not validated before Ed25519 verify
File: http-message-signatures/common/wc_http_sig.c:403-407
Function: wc_HttpSig_Verify
Category: Missing error handling
After wc_SfParseSigValue decodes the base64 signature into rawSig, the code does not verify that rawSigSz equals ED25519_SIG_SIZE (64 bytes) before passing it to wc_ed25519_verify_msg. A malformed Signature header could contain a base64 value that decodes to fewer than 64 bytes. While wc_ed25519_verify_msg is expected to reject incorrectly-sized signatures internally, explicitly validating the size provides defense-in-depth and produces a clearer error for debugging.
rawSigSz = ED25519_SIG_SIZE;
ret = wc_SfParseSigValue(signature, usedLabel, rawSig, &rawSigSz);
if (ret != 0)
return ret;
/* Missing: if (rawSigSz != ED25519_SIG_SIZE) return BAD_FUNC_ARG; */Recommendation: Add an explicit size check after decoding the signature:
ret = wc_SfParseSigValue(signature, usedLabel, rawSig, &rawSigSz);
if (ret != 0)
return ret;
if (rawSigSz != ED25519_SIG_SIZE)
return BAD_FUNC_ARG;Stack-allocated signature base uses int for size tracking of word32 buffer capacity
File: http-message-signatures/common/wc_http_sig.c:149-150
Function: build_signature_base
Category: Integer overflow
build_signature_base casts the word32 *baseOutSz to int maxSz. While this is safe with the current WC_HTTPSIG_MAX_SIG_BASE = 4096 constant, if the buffer size constant were ever increased beyond INT_MAX (or if the function is reused with a caller-specified size), the cast would produce a negative maxSz, causing all bounds checks (pos + len >= maxSz) to pass and enabling writes past the end of the buffer. The same pattern exists in sf_append_escaped and wc_SfGenSigParams.
int maxSz = (int)*baseOutSz;Recommendation: Use word32 consistently for size tracking, or add an explicit guard at function entry:
if (*baseOutSz > (word32)INT_MAX)
return BAD_FUNC_ARG;Info (1)
Hardcoded Ed25519 private key in demo client may be copied to production
File: http-message-signatures/http_client_signed.c:68-73
Function: N/A (file scope)
Category: Hardcoded credentials
The client example embeds the RFC 9421 Appendix B.1.4 Ed25519 private key seed as kDemoPrivKey. While this is appropriately labeled "demo only" in a comment, developers using this file as a template may not replace the key before deploying. The corresponding public key is in the server example and is published in the RFC, meaning any system using these keys provides zero authentication security. The sign_request.c example correctly generates a random key each run, demonstrating the better pattern.
/* RFC 9421 Appendix B.1.4 — Ed25519 private key seed (demo only) */
static const byte kDemoPrivKey[ED25519_KEY_SIZE] = {
0x9f, 0x83, 0x62, 0xf8, 0x7a, 0x48, 0x4a, 0x95,
...Recommendation: Add a runtime warning at startup when the demo key is in use, e.g.:
printf("WARNING: Using RFC 9421 demo key — NOT FOR PRODUCTION\n");Alternatively, add a #warning or comment block at the key definition site directing users to generate their own key.
This review was generated automatically by Fenrir. Findings are non-blocking.
Initial implementation of RFC 9421 HTTP Message Signatures as a wolfssl-examples project. Covers a minimal interoperable subset: derived components (@method, @authority, @path, @query), arbitrary HTTP header fields, Ed25519 signing/verification, single signature (sig1), and timestamp-based replay protection. Files: - common/wc_sf.{c,h}: Minimal RFC 8941 structured fields subset (dictionary lookup, inner lists, parameters, byte sequences) - common/wc_http_sig.{c,h}: RFC 9421 Sign/Verify/GetKeyId API - sign_request.c: Standalone signing example - http_server_verify.c: Demo HTTP server with signature verification - http_client_signed.c: Demo HTTP client sending signed requests - test_vectors.c: 11 tests including RFC 9421 Appendix B.2.6 Design decisions: - Ed25519-only (alg enforced on verify path) - sigOut/inputOut are char* (NUL-terminated strings) - Header names lowercased per RFC 9421 Section 2.1 - Portable case-insensitive comparison (no POSIX strcasecmp) - SO_RCVTIMEO on server to prevent blocking on slow clients - Signature base written directly to caller buffer (no double-buffer) Known limitations: - 32-bit long: parse_sf_integer caps at 9 digits, breaking current UNIX timestamps (needs fix, see below) - No content-digest, multi-signature, or full RFC 8941 support - Duplicate headers: first match wins, no folding Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
|
Addressed the issues reported by @wolfSSL-Fenrir-bot |
dgarske
left a comment
There was a problem hiding this comment.
Should any of this be a feature in wolfCrypt directly or does it belong as example for now?
RFC 9421 is HTTP + structured fields + a canonical signing string; crypto is plain Ed25519 / HMAC-SHA256 from wolfCrypt. So examples + curl are the right home for now - same idea as AWS Sigv4 living in curl and using the SSL backend for hash/HMAC. We could add a small optional sign/verify helper in wolfSSL later if multiple products need it; no strong need yet. |
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
dgarske
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 8 total — 5 posted, 3 skipped
Posted findings
- [High] Missing httpLen validation before send() — negative size passed as size_t —
http-message-signatures/http_client_signed.c:201-212 - [High] wc_ed25519_free called on potentially uninitialized key in cleanup path —
http-message-signatures/test_vectors.c:188-222 - [Medium] Resource leak: wc_ed25519_free not called on import_public failure —
http-message-signatures/http_server_verify.c:322-326 - [Medium] Resource leak: wc_ed25519_free not called on import_private_only failure —
http-message-signatures/http_client_signed.c:245-249 - [Medium] Large stack usage (~12KB+) in Sign and Verify functions —
http-message-signatures/common/wc_http_sig.c:212-330
Skipped findings
- [Low] read_response missing newline when server response has no body
- [Low] Makefile missing header dependency rules
- [Info] CFLAGS missing -Icommon for include path clarity
Review generated by Skoll via openclaw
| printf("[Client] Tampering: changing %s header after signing\n", | ||
| tamperHdr); | ||
|
|
||
| httpLen = build_http_request( |
There was a problem hiding this comment.
🟠 [High] Missing httpLen validation before send() — negative size passed as size_t
🚫 BLOCK bug
build_http_request() returns -1 on buffer overflow. The return value is stored in httpLen (an int) but is never checked before being passed to send(fd, httpReq, httpLen, 0). When httpLen is -1, it gets implicitly converted to size_t (an unsigned type), producing a very large value (~4 billion on 64-bit). This causes send() to attempt reading far beyond the httpReq buffer, which is undefined behavior.
Suggestion:
| httpLen = build_http_request( | |
| httpLen = build_http_request( | |
| method, path, query, authority, | |
| headers, hdrCount, | |
| sigBuf, sigBufSz, | |
| inputBuf, inputBufSz, | |
| tamperHdr, tamperVal, | |
| httpReq, sizeof(httpReq)); | |
| if (httpLen < 0) { | |
| printf("[Client] Failed to build HTTP request\n"); | |
| return -1; | |
| } | |
| fd = do_connect(); | |
| if (fd < 0) return -1; | |
| send(fd, httpReq, httpLen, 0); |
— Skoll automated review via openclaw
| ret = wc_InitRng(&rng); | ||
| if (ret != 0) return ret; | ||
|
|
||
| ret = wc_ed25519_init(&key); |
There was a problem hiding this comment.
🟠 [High] wc_ed25519_free called on potentially uninitialized key in cleanup path
🚫 BLOCK bug
If wc_ed25519_init(&key) fails at line 188, execution jumps to done: (line 220) which calls wc_ed25519_free(&verifyKey) on a never-initialized local variable. Since verifyKey is an uninitialized stack variable, its fields contain indeterminate values. wc_ed25519_free() may attempt to zero or free garbage pointer values within the struct, resulting in undefined behavior. The same issue applies if wc_ed25519_init(&key) succeeds but wc_ed25519_init(&verifyKey) fails — wc_ed25519_free(&key) is called on a key whose init failed. Contrast this with the individual test functions (e.g., test_query_component at line 460-462) which correctly handle each failure separately.
Suggestion:
| ret = wc_ed25519_init(&key); | |
| XMEMSET(&key, 0, sizeof(key)); | |
| XMEMSET(&verifyKey, 0, sizeof(verifyKey)); | |
| ret = wc_InitRng(&rng); | |
| if (ret != 0) return ret; | |
| ret = wc_ed25519_init(&key); | |
| if (ret != 0) goto done; | |
| ret = wc_ed25519_init(&verifyKey); | |
| if (ret != 0) goto done; |
— Skoll automated review via openclaw
| printf("Failed to init public key: %d\n", ret); | ||
| return 1; | ||
| } | ||
| ret = wc_ed25519_import_public(kDemoPubKey, ED25519_PUB_KEY_SIZE, &pubKey); |
There was a problem hiding this comment.
🟡 [Medium] Resource leak: wc_ed25519_free not called on import_public failure
💡 SUGGEST bug
After wc_ed25519_init(&pubKey) succeeds at line 317, if wc_ed25519_import_public() fails at line 322, the function returns without calling wc_ed25519_free(&pubKey). This leaks the initialized key resources. While the process exits immediately so the OS reclaims memory, wolfSSL examples should demonstrate proper cleanup patterns as users copy these patterns.
Suggestion:
| ret = wc_ed25519_import_public(kDemoPubKey, ED25519_PUB_KEY_SIZE, &pubKey); | |
| ret = wc_ed25519_import_public(kDemoPubKey, ED25519_PUB_KEY_SIZE, &pubKey); | |
| if (ret != 0) { | |
| printf("Failed to import public key: %d\n", ret); | |
| wc_ed25519_free(&pubKey); | |
| return 1; | |
| } |
— Skoll automated review via openclaw
| printf("Failed to init key: %d\n", ret); | ||
| return 1; | ||
| } | ||
| ret = wc_ed25519_import_private_only(kDemoPrivKey, ED25519_KEY_SIZE, &key); |
There was a problem hiding this comment.
🟡 [Medium] Resource leak: wc_ed25519_free not called on import_private_only failure
💡 SUGGEST bug
After wc_ed25519_init(&key) succeeds at line 240, if wc_ed25519_import_private_only() fails at line 245, the function returns without calling wc_ed25519_free(&key). The subsequent error paths at lines 254-257 and 261-264 correctly call wc_ed25519_free(&key), but this first failure path does not.
Suggestion:
| ret = wc_ed25519_import_private_only(kDemoPrivKey, ED25519_KEY_SIZE, &key); | |
| ret = wc_ed25519_import_private_only(kDemoPrivKey, ED25519_KEY_SIZE, &key); | |
| if (ret != 0) { | |
| printf("Failed to import private key: %d\n", ret); | |
| wc_ed25519_free(&key); | |
| return 1; | |
| } |
— Skoll automated review via openclaw
| /* --- Public API: Sign --- */ | ||
|
|
||
| #ifdef HAVE_ED25519_SIGN | ||
| int wc_HttpSig_Sign( |
There was a problem hiding this comment.
🟡 [Medium] Large stack usage (~12KB+) in Sign and Verify functions
💡 SUGGEST api
wc_HttpSig_Sign and wc_HttpSig_Verify allocate substantial stack buffers: wc_SfSigInput sfIn (~6.8KB due to items[16][256] + params[8] with 256-byte strVal), sigParams[1024], sigBase[4096], and componentNames[16]. Total per-call stack is ~12KB+. The header documents this (Reduce WC_HTTPSIG_MAX_SIG_BASE on stack-constrained embedded targets) but WC_SF_MAX_STRING=256 and WC_SF_MAX_ITEMS=16 in wc_SfSigInput are the dominant contributors (~6.8KB alone) and are not mentioned in the tuning guidance.
Recommendation: Document in the header comment that WC_SF_MAX_STRING and WC_SF_MAX_ITEMS also contribute significantly to stack usage and should be tuned for embedded targets. Alternatively, consider reducing WC_SF_MAX_STRING (256 is generous for component identifiers) or heap-allocating wc_SfSigInput internally.
— Skoll automated review via openclaw
Initial implementation of RFC 9421 HTTP Message Signatures as a wolfssl-examples project. Covers a minimal interoperable subset: derived components (@method, @authority, @path, @query), arbitrary HTTP header fields, Ed25519 signing/verification, single signature (sig1), and timestamp-based replay protection.
Files:
Design decisions:
Known limitations: