Native HTTP Message Signatures in curl, Powered by wolfSSL – Part 3

In Part 1, we argued agents need cryptographic request authenticity. In Part 2 we surveyed the ecosystem and the adjacent tools. This post is about what we’re contributing: native RFC 9421 support at the plumbing layer — curl and libcurl, with wolfCrypt providing the Ed25519 math.
Two open PRs:

why curl

A large share of CLI, CI, shell-out, and embedded HTTP traffic ends at libcurl. Every language has its own nice HTTP client, but the moment someone subprocess.run(“curl …”)s — and in agent/MCP code that happens constantly — the signing story breaks unless it’s built in.
Putting RFC 9421 in curl means:

  • Any agent framework that shells out to curl inherits signing without a new dependency.
  • MCP tool servers that embed libcurl get verifiable outbound requests with one curl_easy_setopt call.
  • CI jobs, cron tasks, health checks, and embedded firmware that already use curl get request-level identity tomorrow without changing language.

curl already has multi-backend crypto — OpenSSL, wolfSSL, GnuTLS, mbedTLS — so we don’t have to pick a winner.

The CLI, end-to-end

Four new flags, matching four new CURLOPT_* options:

CLI flag libcurl option Purpose
–httpsig CURLOPT_HTTPSIG Algorithm: ed25519 or hmac-sha256
–httpsig-key CURLOPT_HTTPSIG_KEY Path to a hex-encoded key file
–httpsig-keyid CURLOPT_HTTPSIG_KEYID Value placed in Signature-Input’s keyid=
–httpsig-headers CURLOPT_HTTPSIG_HEADERS Space-separated components to cover

Default covered components: @method @authority @path (plus @query when present). Use –httpsig-headers to add things like content-digest, content-type, or authorization.

curl --httpsig ed25519 \
    --httpsig-key agent-42.hex \
    --httpsig-keyid agent-42 \
    --httpsig-headers '@method @authority @path content-digest' \
    -H 'Content-Digest: sha-256=:X48E9q...:' \
    --data-binary @tool_call.json \
    https://api.example.com/v1/tools/exec

On the wire:

POST /v1/tools/exec HTTP/1.1
Host: api.example.com
Content-Digest: sha-256=:X48E9q...:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest");\
                created=1713657600;keyid="agent-42";alg="ed25519"
Signature: sig1=:dHA0...base64...==:

HMAC-SHA256 works the same way, swapping ed25519 for hmac-sha256 and a symmetric key file. That mode exists mostly for legacy-interop and testing.

The Content-Digest sharp edge (this one matters)

Notice that in the example above, we manually computed Content-Digest and passed it in with -H. This is a real usability problem, not a footnote.

If you sign content-digest as a component but forget to emit the header, the signature references a nonexistent component and the request is rejected — annoying but safe.

The actually dangerous failure is the inverse: you emit a body, you don’t include content-digest in the signed components, and an attacker can swap the body while leaving your signature valid. The signature would cover the method and URL, but not what you actually said. For an agent API where the body is the instruction (“transfer $10,000”), this is a catastrophic class of bug waiting to happen.

Two responses we’re working on:

  1. Automatic Content-Digest emission when curl has a request body — on the roadmap and explicitly called out in the PR as a follow-up.
  2. Refusing to sign a body-carrying request unless content-digest is in the signed components, or printing a loud warning.

Until that lands, the honest advice is: if you send a body, sign its digest. Every time. Consider wrapping curl invocations in a small helper that computes and injects Content-Digest so it can’t be forgotten.

Key handling — what the PR does and doesn’t do

This is the other area where a maintainer will (rightly) push back. What PR #21239 ships today:

  • Hex-encoded key files on disk. Simple, portable, scriptable, and the right level of abstraction for a first cut. The key never appears on the command line.
  • Symmetric HMAC keys and raw Ed25519 seeds use the same on-disk format.
  • Key length validation at load time.

What the PR does not do, and should not pretend to:

  • No HSM / PKCS#11 / TPM / secure element integration. This matters a lot for production agents on edge devices; it’s tracked as a follow-up. wolfCrypt has the plumbing for several of these — the integration work is on our side.
  • No rotation story. keyid is metadata; curl will sign with whatever key you hand it. Rotation — overlapping keys, gradual rollover, revocation — is a job for the layer above curl (the registry, JWKS endpoint, DID document). Part 2’s “not an identity system” caveat applies here.
  • No key generation. Generate with openssl genpkey -algorithm ed25519 or wolfssl’s tooling; curl only consumes.
  • No encrypted-at-rest key format. If your threat model includes disk-capture, store the key somewhere curl can cat it out of (an OS keyring, a tmpfs fed by your secrets manager) rather than putting it on disk in plaintext.

Rough guidance we’re giving early adopters:

  • Dev / CI: hex file on disk, short-lived, rotated aggressively. Fine.
  • Server-side agents: hex file mounted from a secrets manager (AWS Secrets Manager, HashiCorp Vault, sealed secrets) with agent process restarts on rotation.
  • Embedded / edge: wait for the PKCS#11 or secure-element path; don’t ship plaintext Ed25519 seeds to a device you can’t physically trust.

How the two PRs fit together

The two PRs are sibling contributions, not a stack.

┌────────────────────────────────┐   ┌────────────────────────────────┐
│  curl/curl#21239               │   │  wolfSSL/wolfssl-examples#566  │
│  libcurl + CLI integration     │   │  reference C implementation    │
│ ────────────────────────────── │   │ ────────────────────────────── │
│ lib/http_httpsig.c    (~615)   │   │ common/wc_sf.{c,h}             │
│ lib/httpsig_crypto.c  (~185)   │   │   RFC 8941 structured fields   │
│ 19 test-data files             │   │ common/wc_http_sig.{c,h}       │
│ (test5000–test5018)            │   │   RFC 9421 sign/verify API     │
│ docs + manpages + bindings     │   │ 3 demos + 11 test vectors      │
│                                │   │ (incl. RFC 9421 Appendix B.2.6)│
│  Backends:                     │   │                                │
│  • OpenSSL  → EVP_DigestSign   │   │  Crypto:                       │
│  • wolfSSL  → wc_ed25519_*     │   │  • wolfCrypt Ed25519 only      │
│  • HMAC via Curl_hmacit()      │   │                                │
│    (all backends)              │   │                                │
└─────────────┬──────────────────┘   └─────────────────┬──────────────┘
              │                                        │
              └─────────── both call ──────────────────┘
                        wolfCrypt directly

wolfssl-examples#566 is a standalone reference implementation of RFC 9421 on top of wolfCrypt — useful for embedded projects, interop testing, and reading the spec in isolation without curl’s build system. curl#21239 is the production integration, using wolfCrypt (via the library) and OpenSSL as the two Ed25519 backends, with HMAC-SHA256 going through curl’s existing HMAC infrastructure so every TLS backend gets HMAC for free.

wolfSSL-examples (#566) — what the reference does

  • Intentionally minimal interoperable subset of RFC 9421:
  • Derived components @method, @authority, @path, @query
  • Arbitrary header fields (lowercased per §2.1, portable case-insensitive compare)
  • Ed25519 only, and the verifier ignores the self-declared alg parameter and enforces Ed25519 — a standard hardening pattern against algorithm-substitution attacks
  • Single signature label (sig1)
  • Timestamp-based replay protection
  • RFC 8941 Structured Fields subset — just enough for 9421

Three runnable demos (sign_request, http_server_verify, http_client_signed), 11 test vectors including RFC 9421 Appendix B.2.6 — the canonical interop test — and ~3,400 lines of C in a standalone http-message-signatures/ directory.
One known limitation honestly flagged: on 32-bit long platforms, parse_sf_integer truncates current UNIX timestamps at 9 digits. Fix queued.

curl (#21239) — what production integration looks like
lib/httpsig_crypto.c follows curl’s existing sha256.c multi-backend pattern: OpenSSL via EVP_DigestSign, wolfSSL via wc_ed25519_sign_msg/wc_ed25519_verify_msg, HMAC-SHA256 via Curl_hmacit(). lib/http_httpsig.c (~615 LOC) handles the RFC 9421 protocol work.
19 test-data files (test5000–test5018) cover both algorithms and both CLI and libcurl entry points.
./configure –with-wolfssl –enable-httpsig
CURL_DISABLE_HTTPSIG is there for builds that don’t want it.

The harder half: verification

Signing is the easy part. A production RFC 9421 verifier has to solve things the curl PR deliberately stays out of:

  1. Key lookup. How does keyid=”agent-42″ turn into a public key? JWKS URL? DID resolution? DNS TXT record (the ApertoID draft)? Internal registry? Each has latency, caching, and failure modes — and a wrong answer is a silent auth bypass.
  2. Replay protection. created is mandatory but insufficient. A naïve 5-minute window lets the same request replay 300 times within the window. You need either nonce with persistent server-side storage of seen values, or a signed monotonic counter. Both have operational cost.
  3. Clock skew. created and expires are absolute timestamps. Agents on embedded devices have bad clocks. You either accept skew (widening the replay window) or you require NTP (creating a dependency agents may not have).
  4. alg enforcement. Verifiers must not trust the self-declared alg parameter — they must pre-bind keyid to an algorithm, or risk algorithm-substitution attacks. This is exactly what the wolfssl-examples reference does.
  5. Chain-of-custody verification. When multiple signatures ride the same request (see below), verifiers need a policy for which signatures are required, in what order, and with what trust relationships between their keys.

Verification-side support for curl — incoming request verification from libcurl — is a planned follow-up PR. It will almost certainly be where the most interesting work happens.

The chain of custody

This is the thing that makes 9421 different from “just sign the request.”
Picture a user prompt that flows: user → planner agent → executor agent → vendor API.
With multi-signature, each hop adds its own labeled signature without removing the previous one:

Signature-Input: sig-user=(...);keyid="user-alice";alg="ed25519";created=...,
                sig-planner=(...);keyid="agent-planner-17";alg="ed25519";created=...,
                sig-exec=(...);keyid="agent-exec-3";alg="ed25519";created=...
Signature: sig-user=:...:, sig-planner=:...:, sig-exec=:...:

The vendor API can now answer questions no other protocol answers cleanly:

  • Did this ultimately come from Alice? → verify sig-user.
  • Which executor agent actually made the call? → verify sig-exec.
  • Did the planner authorize this executor? → policy on the pair of keyids.
  • Is this a legal chain under our rules? → a policy engine can refuse requests where the chain doesn’t match an allowed delegation pattern.

This — agent-to-agent verifiable execution chains over HTTP — is the real capability the primitive unlocks. It’s the reason getting 9421 into every HTTP client matters. Chain of custody only works if every hop can sign.

What’s next

Both PRs are open and in review. If you want to try it today:

On our near-term list:

  • Incoming-request verification inside libcurl
  • Automatic Content-Digest emission when –data is present
  • nonce and expires support beyond created
  • PKCS#11 / HSM key sources
  • Interop tests against Vestauth, OpenBotAuth, and Cloudflare Web Bot Auth
  • Benchmarks on the curl+wolfCrypt path — a dedicated post

RFC 9421 is a primitive, not a complete identity system — the authorization, revocation, reputation, and policy layers still have to be built on top. But primitives matter, and they have to live where the requests live. That’s curl. That’s libcurl. That’s every agent that has ever shelled out.
We’re getting it there.

 

Series:
Part 1 — Signing the Agentic Web
Part 2 — The Identity Gap
Part 3 — Native HTTP Message Signatures in curl, Powered by wolfSSL ← you are here

If you have questions about any of the above, please contact us at facts@wolfssl.com or call us at +1 425 245 8247.

Download wolfSSL Now