Skip to content

net.http.signature: add HTTP Message Signatures (RFC 9421) module#27113

Open
davlgd wants to merge 4 commits into
vlang:masterfrom
davlgd:davlgd-http-signature
Open

net.http.signature: add HTTP Message Signatures (RFC 9421) module#27113
davlgd wants to merge 4 commits into
vlang:masterfrom
davlgd:davlgd-http-signature

Conversation

@davlgd
Copy link
Copy Markdown
Contributor

@davlgd davlgd commented May 8, 2026

This PR adds vlib/net/http/signature — a pure-V implementation of RFC 9421 (HTTP Message Signatures). Sign and verify HTTP requests and responses with the four algorithms backed by vlib/crypto: hmac-sha256, ecdsa-p256-sha256, ecdsa-p384-sha384, ed25519.

No new C code, no new third-party crypto: everything sits on vlib/crypto (ecdsa, ed25519, hmac, sha256) and vlib/crypto/pem for Key.from_pem.

Why

RFC 9421 was published in February 2024. It supersedes the long-running Cavage drafts (draft-cavage-http-signatures-*) that ActivityPub / Mastodon-style federation has been using in non-interoperable variants for years, and gives the ecosystem a single normative spec with stable header names and a stable signature-base format. The signed-request model authenticates HTTP messages end-to-end across TLS terminators and lets multiple per-hop signatures (client → proxy → backend) coexist on the same request. The new IETF Web Bot Auth draft is built directly on top of it. With vlib/net/http already in the stdlib, V apps could not produce or verify RFC 9421 signatures until now.

What's covered

Algorithm name (IANA HTTP Signature Algorithms registry) Reference Status
hmac-sha256 RFC 9421 §3.3.3
ecdsa-p256-sha256 RFC 9421 §3.3.4
ecdsa-p384-sha384 RFC 9421 §3.3.5
ed25519 RFC 9421 §3.3.6

The RFC 9421 §2.2 derived components implemented are @method, @target-uri, @authority, @scheme, @request-target, @path, @query, @status — the @query-param selector (§2.2.8) is the only one missing and is called out below as deferred. Plain HTTP fields are matched by lowercased name, multi-value joined as ", ", OWS trimmed (RFC 9421 §2.1). Optional outer behaviours: multiple co-existing signatures merged into a single Structured Field per RFC 8941 §3.2; expires enforced when the caller passes now_unix > 0.

rsa-pss-sha512 and rsa-v1_5-sha256 are intentionally out of scope — vlib/crypto does not yet ship an RSA implementation. Adding them is mechanical once it does. @query-param, sf / key / bs parameter handling are deferred to a follow-up PR.

Module surface

import net.http
import net.http.signature
import time
// Sign an outbound request (Ed25519 + PEM-encoded private key).
priv := signature.Key.from_pem(alice_private_pem)!.with_keyid('alice')
signature.sign_request(mut req, priv,
    components: ['@method', '@target-uri', '@authority', 'date'])!
// Verify on the server side.
pub_key := signature.Key.from_pem(alice_public_pem)!
signature.verify_request(req, pub_key, now_unix: time.now().unix())!

Key.from_pem accepts the canonical PKCS#8 / SPKI / SEC1 PEM blocks openssl genpkey and friends produce; the raw-coordinate constructors (Key.ed25519_private(seed), Key.ecdsa_p256_public(x, y), …) remain for callers that have JWK-shaped material. created defaults to time.now().unix() when omitted, since RFC 9421 §7.2.1 RECOMMENDS it for replay protection.

A complete example program lives at examples/http_signature.v.

Conformance / test vectors

RFC 9421 Appendix B vectors are vendored under vlib/net/http/signature/tests/rfc9421/ and exercised by rfc9421_test.v:

Vector Algorithm Mode
§B.2.5 hmac-sha256 bytes-exact
§B.2.6 ed25519 bytes-exact
§B.2.4 ecdsa-p256-sha256 verify (ECDSA non-deterministic)

Both byte-exact tests reproduce the RFC reference signature down to the last base64 character; the ECDSA case verifies the reference signature and adds an independent sign-then-verify roundtrip.

In addition to the public corpus:

  • http_message_test.v — sign/verify roundtrips for HMAC, Ed25519, ECDSA P-256 (RFC key) and ECDSA P-384 (fresh keypair via ecdsa.generate_key); tampered-URL rejection; missing-header rejection; expires enforcement; two-signature coexistence; alg mismatch rejection; label grammar (Structured Field key form).
  • structured_field_test.v — Inner List + parameter serialisation pinned (("@method" "host");created=N;keyid="…" byte-for-byte), escape rules for quoted strings, multi-entry / single-entry parsing, raw signature_params_value preservation, and non-canonical wire-order verification — proves the verifier replays the wire param substring verbatim instead of re-serialising in a fixed canonical order, which is what makes interop with stacks that emit ;keyid=…;created=… (instead of the inverse) work.
  • key_test.vKey.from_pem round-trip with the RFC §B.1.3 P-256 PEM and §B.1.4 Ed25519 PEM, byte-exact RFC §B.2.6 signature reproduction via Key.from_pem, and rejection of unsupported PEM block types (RSA PRIVATE KEY etc).

I've also tested these modules against lib in other languages to check interop.

Out of scope (deliberate)

  • RSA signaturesrsa-pss-sha512 and rsa-v1_5-sha256 need an RSA-PSS implementation in vlib/crypto, which is a separate effort. Adding them is purely additive once it lands.
  • @query-param derived component (RFC 9421 §2.2.8) — defers per-parameter selection rules; rare in practice and easy to add later.
  • Structured-field re-serialisation parameters (sf, key, bs from §2.1.x) — used when signing structured-field values themselves; out of v1 to keep the parser narrow.
  • Body covered components beyond what Components.fields already supportsContent-Digest is signed as a regular header field; computing the digest itself stays the caller's concern (matches what every other RFC 9421 stack does).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b79a1623db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/net/http/signature/http_message.v Outdated
Comment on lines +187 to +190
mut c := Components{
method: req.method.str()
target_uri: req.url
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconstruct absolute @target-uri for parsed server requests

request_components copies req.url directly into target_uri, but stdlib request parsing stores origin-form request targets there (e.g. /foo), not absolute URIs (vlib/net/http/request.v sets url: target.str()). That makes verify_request compute "@target-uri": /foo for normal inbound HTTP/1.1 requests, so signatures from compliant peers that sign the full target URI (scheme + authority + path + query) will fail verification even with the correct key.

Useful? React with 👍 / 👎.

reason: 'ECDSA PEM parse failed: ${err.msg()}'
}
}
d := priv_obj.bytes()!
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pad PEM ECDSA private scalar to curve width

ecdsa.privkey_from_string(...).bytes() returns the private scalar in minimal-length form, so valid keys with leading zero bytes produce d shorter than the curve size; this unpadded d is passed into Key.ecdsa_*_private. Later, ecdsa_sign requires exactly coord_size * 3 bytes and rejects shorter keys, causing a subset of valid PEM ECDSA private keys to fail signing with MalformedMessage.

Useful? React with 👍 / 👎.

Comment thread vlib/net/http/signature/components.v
@medvednikov
Copy link
Copy Markdown
Member

vfmt

@davlgd davlgd force-pushed the davlgd-http-signature branch from f3e774d to f9dea48 Compare May 8, 2026 18:58
@davlgd
Copy link
Copy Markdown
Contributor Author

davlgd commented May 8, 2026

vfmt

Sorry I missed it before the last push. It's fixed.

@JalonSolov
Copy link
Copy Markdown
Collaborator

Run v git-fmt-hook install in your repo, and v fmt will be run on all your V files on every commit.

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.

3 participants