|
| 1 | +# `net.http.signature` — HTTP Message Signatures (RFC 9421) |
| 2 | + |
| 3 | +Sign and verify HTTP requests and responses per [RFC 9421][rfc9421] — |
| 4 | +the standard that replaces the long-running `Signature` / |
| 5 | +`Signature-Input` drafts and underpins production deployments at major |
| 6 | +CDNs, mTLS proxies, mutual API authentication, and the upcoming |
| 7 | +[Web Bot Auth][web-bot-auth] work. |
| 8 | + |
| 9 | +[rfc9421]: https://www.rfc-editor.org/rfc/rfc9421.html |
| 10 | +[web-bot-auth]: https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/ |
| 11 | + |
| 12 | +## Quick start |
| 13 | + |
| 14 | +```v ignore |
| 15 | +import net.http |
| 16 | +import net.http.signature |
| 17 | +
|
| 18 | +// Sign an outbound request. `created` defaults to time.now().unix(). |
| 19 | +mut req := http.new_request(.post, 'https://example.com/items', '{}')! |
| 20 | +req.header.add_custom('Date', 'Tue, 20 Apr 2021 02:07:55 GMT')! |
| 21 | +req.header.add_custom('Content-Type', 'application/json')! |
| 22 | +
|
| 23 | +priv := signature.Key.from_pem(alice_private_pem)!.with_keyid('alice') |
| 24 | +signature.sign_request(mut req, priv, |
| 25 | + components: ['@method', '@target-uri', '@authority', 'date', 'content-type'] |
| 26 | +)! |
| 27 | +// req now carries Signature-Input and Signature header fields. |
| 28 | +
|
| 29 | +// On the receiving side, verify with the public key resolved from `keyid`: |
| 30 | +pub_key := signature.Key.from_pem(alice_public_pem)! |
| 31 | +signature.verify_request(req, pub_key, now_unix: time.now().unix())! |
| 32 | +``` |
| 33 | + |
| 34 | +`Key.from_pem` accepts the canonical PKCS#8 / SPKI / SEC1 PEM blocks |
| 35 | +that `openssl genpkey` and friends produce. The raw-coordinate |
| 36 | +constructors (`Key.ed25519_private(seed)`, `Key.ecdsa_p256_public(x, |
| 37 | +y)`, …) are still available when you have JWK-style key material. |
| 38 | + |
| 39 | +The `now_unix` option enforces the optional `expires` parameter; pass |
| 40 | +`0` (the default) to skip the expiry check. |
| 41 | + |
| 42 | +## Algorithms |
| 43 | + |
| 44 | +| IANA name | Status | Backed by | |
| 45 | +| ------------------- | ------ | --------- | |
| 46 | +| `hmac-sha256` | ✅ | `crypto.hmac` + `crypto.sha256` | |
| 47 | +| `ecdsa-p256-sha256` | ✅ | `crypto.ecdsa` (P-256) | |
| 48 | +| `ecdsa-p384-sha384` | ✅ | `crypto.ecdsa` (P-384) | |
| 49 | +| `ed25519` | ✅ | `crypto.ed25519` | |
| 50 | + |
| 51 | +`rsa-pss-sha512` and `rsa-v1_5-sha256` are intentionally out of scope — |
| 52 | +`vlib/crypto` does not yet ship an RSA implementation. Adding them is |
| 53 | +mechanical once it does. |
| 54 | + |
| 55 | +## Covered components |
| 56 | + |
| 57 | +All derived components from RFC 9421 §2.2 are implemented: |
| 58 | + |
| 59 | +`@method`, `@target-uri`, `@authority`, `@scheme`, `@request-target`, |
| 60 | +`@path`, `@query`, `@status`. |
| 61 | + |
| 62 | +Plain HTTP fields are matched by *lowercased* field name, with |
| 63 | +multi-value fields joined by `", "` and OWS trimmed at the boundaries |
| 64 | +(RFC 9421 §2.1). |
| 65 | + |
| 66 | +`@query-param` (RFC 9421 §2.2.8), structured-field re-serialisation |
| 67 | +(`sf`, `key`, `bs` parameters from §2.1.x), and binary-wrapped fields |
| 68 | +are deferred to a follow-up PR. |
| 69 | + |
| 70 | +## Two API layers |
| 71 | + |
| 72 | +```v ignore |
| 73 | +// Components-level - works on any HTTP-shaped data, no http.Request |
| 74 | +// dependency. Use this offline (signing fixtures, building tests). |
| 75 | +base := signature.signature_base_string(components, params)! |
| 76 | +out := signature.sign(components, params, key, 'sig1')! |
| 77 | +signature.verify(components, sig_input, sig_value, 'sig1', key)! |
| 78 | +
|
| 79 | +// http.Request / http.Response wrappers - sugar over the above. |
| 80 | +signature.sign_request(mut req, key, components: [...], created: now)! |
| 81 | +signature.verify_request(req, key, now_unix: now)! |
| 82 | +``` |
| 83 | + |
| 84 | +## Design notes |
| 85 | + |
| 86 | +* **No silent algorithm fallbacks.** If you set the `alg` parameter |
| 87 | + and it doesn't match the key's algorithm, `sign` errors out with |
| 88 | + `MalformedMessage`. `verify` does the same on the inbound side. |
| 89 | + RFC 9421 §3.1 step 3 makes this a correctness requirement. |
| 90 | + |
| 91 | +* **Empty `keyid` is allowed.** RFC 9421 doesn't make `keyid` |
| 92 | + mandatory; some out-of-band channel (mTLS cert, JWT bearer) |
| 93 | + identifies the signer instead. `sign` still emits a usable |
| 94 | + signature; the verifier picks the key by other means. |
| 95 | + |
| 96 | +* **Multiple signatures coexist.** Calling `sign_request` twice with |
| 97 | + different labels merges the labelled entries into a single |
| 98 | + `Signature-Input` / `Signature` field per RFC 8941 §3.2 (comma- |
| 99 | + separated dictionary) — TLS-terminating proxies and federated |
| 100 | + signing scenarios both rely on this layout. |
| 101 | + |
| 102 | +* **No clock dependency.** Both `created` and the expiry check are |
| 103 | + driven by the caller (`opts.created`, `opts.now_unix`). Signing in |
| 104 | + bulk over historical data, deterministic test runs, and replay |
| 105 | + protection are all the caller's concern. |
| 106 | + |
| 107 | +## Test vectors |
| 108 | + |
| 109 | +RFC 9421 Appendix B vectors are vendored under |
| 110 | +`tests/rfc9421/` and exercised by `rfc9421_test.v`: |
| 111 | + |
| 112 | +| Section | Algorithm | Mode | |
| 113 | +| --- | --- | --- | |
| 114 | +| B.2.5 | `hmac-sha256` | **bytes-exact** | |
| 115 | +| B.2.6 | `ed25519` | **bytes-exact** | |
| 116 | +| B.2.4 | `ecdsa-p256-sha256` | verify (ECDSA non-deterministic) | |
| 117 | + |
| 118 | +`http_message_test.v` covers sign/verify roundtrips across all four |
| 119 | +supported algorithms (including a freshly-generated P-384 key), |
| 120 | +tampered URL rejection, missing-header rejection, expiry enforcement, |
| 121 | +multi-signature coexistence, and `alg` / label validation. |
| 122 | +`structured_field_test.v` pins the Inner List + parameter |
| 123 | +serialisation, multi-value field joining, OWS trimming, and the |
| 124 | +`@query` empty-vs-present semantics. |
0 commit comments