net.http.signature: add HTTP Message Signatures (RFC 9421) module #17
This PR adds `vlib/net/http/signature` — a pure-V implementation of [RFC 9421][rfc9421] (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`. [rfc9421]: https://www.rfc-editor.org/rfc/rfc9421.html ## 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][wba] 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. [wba]: https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/ ## 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 ```v ignore 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.v` — `Key.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 signatures** — `rsa-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 supports** — `Content-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).