Skip to content

Add RFC 9421 HTTP Message Signatures example using wolfCrypt Ed25519#566

Open
sameehj wants to merge 2 commits intowolfSSL:masterfrom
sameehj:http-sign
Open

Add RFC 9421 HTTP Message Signatures example using wolfCrypt Ed25519#566
sameehj wants to merge 2 commits intowolfSSL:masterfrom
sameehj:http-sign

Conversation

@sameehj
Copy link
Copy Markdown
Contributor

@sameehj sameehj commented Mar 25, 2026

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

Copy link
Copy Markdown

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@sameehj
Copy link
Copy Markdown
Contributor Author

sameehj commented Mar 30, 2026

Addressed the issues reported by @wolfSSL-Fenrir-bot
regarding the last comment, the hardcoded private key follows convention.

Copy link
Copy Markdown
Contributor

@dgarske dgarske left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should any of this be a feature in wolfCrypt directly or does it belong as example for now?

@dgarske dgarske assigned sameehj and unassigned wolfSSL-Fenrir-bot Apr 8, 2026
@sameehj
Copy link
Copy Markdown
Contributor Author

sameehj commented Apr 9, 2026

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>
Copy link
Copy Markdown
Contributor

@dgarske dgarske left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐺 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_thttp-message-signatures/http_client_signed.c:201-212
  • [High] wc_ed25519_free called on potentially uninitialized key in cleanup pathhttp-message-signatures/test_vectors.c:188-222
  • [Medium] Resource leak: wc_ed25519_free not called on import_public failurehttp-message-signatures/http_server_verify.c:322-326
  • [Medium] Resource leak: wc_ed25519_free not called on import_private_only failurehttp-message-signatures/http_client_signed.c:245-249
  • [Medium] Large stack usage (~12KB+) in Sign and Verify functionshttp-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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [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:

Suggested change
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [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:

Suggested change
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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:

Suggested change
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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:

Suggested change
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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants