net.http.signature: add HTTP Message Signatures (RFC 9421) module#27113
net.http.signature: add HTTP Message Signatures (RFC 9421) module#27113davlgd wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
💡 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".
| mut c := Components{ | ||
| method: req.method.str() | ||
| target_uri: req.url | ||
| } |
There was a problem hiding this comment.
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()! |
There was a problem hiding this comment.
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 👍 / 👎.
|
vfmt |
f3e774d to
f9dea48
Compare
Sorry I missed it before the last push. It's fixed. |
|
Run |
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 byvlib/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) andvlib/crypto/pemforKey.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. Withvlib/net/httpalready in the stdlib, V apps could not produce or verify RFC 9421 signatures until now.What's covered
hmac-sha256ecdsa-p256-sha256ecdsa-p384-sha384ed25519The RFC 9421 §2.2 derived components implemented are
@method,@target-uri,@authority,@scheme,@request-target,@path,@query,@status— the@query-paramselector (§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;expiresenforced when the caller passesnow_unix > 0.rsa-pss-sha512andrsa-v1_5-sha256are intentionally out of scope —vlib/cryptodoes not yet ship an RSA implementation. Adding them is mechanical once it does.@query-param,sf/key/bsparameter handling are deferred to a follow-up PR.Module surface
Key.from_pemaccepts the canonical PKCS#8 / SPKI / SEC1 PEM blocksopenssl genpkeyand friends produce; the raw-coordinate constructors (Key.ed25519_private(seed),Key.ecdsa_p256_public(x, y), …) remain for callers that have JWK-shaped material.createddefaults totime.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 byrfc9421_test.v:hmac-sha256ed25519ecdsa-p256-sha256Both 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 viaecdsa.generate_key); tampered-URL rejection; missing-header rejection;expiresenforcement; two-signature coexistence;algmismatch 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, rawsignature_params_valuepreservation, 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.v—Key.from_pemround-trip with the RFC §B.1.3 P-256 PEM and §B.1.4 Ed25519 PEM, byte-exact RFC §B.2.6 signature reproduction viaKey.from_pem, and rejection of unsupported PEM block types (RSA PRIVATE KEYetc).I've also tested these modules against lib in other languages to check interop.
Out of scope (deliberate)
rsa-pss-sha512andrsa-v1_5-sha256need an RSA-PSS implementation invlib/crypto, which is a separate effort. Adding them is purely additive once it lands.@query-paramderived component (RFC 9421 §2.2.8) — defers per-parameter selection rules; rare in practice and easy to add later.sf,key,bsfrom §2.1.x) — used when signing structured-field values themselves; out of v1 to keep the parser narrow.Components.fieldsalready supports —Content-Digestis signed as a regular header field; computing the digest itself stays the caller's concern (matches what every other RFC 9421 stack does).