http_message_signatures
A minimal, framework-agnostic implementation of RFC 9421 (HTTP Message Signatures), targeting both the Erlang and JavaScript backends. It does not yet cover 100% of the RFC, see below for unsupported areas.
gleam add generic_http_message_signatures
import gleam/option.{None, Some}
import generic_http_message_signatures/component.{Authority, Derived, Field, Method}
import generic_http_message_signatures/keys
import generic_http_message_signatures/message.{Request}
import generic_http_message_signatures/params.{SignatureParams}
import generic_http_message_signatures/signer
import generic_http_message_signatures/verifier
pub fn main() {
let #(private_key, public_key) = keys.generate()
let request =
Request("POST", "https://example.com/foo", [
#("Date", "Tue, 20 Apr 2021 02:07:55 GMT"),
#("Content-Type", "application/json"),
])
let signature_params =
SignatureParams(
components: [Derived(Method), Derived(Authority), Field("date")],
key_id: "example-key",
algorithm: "ed25519",
created: Some(1_618_884_473),
expires: None,
)
let assert Ok(signed) =
signer.sign(request, private_key, "sig1", signature_params)
// Attach `signed.signature_input` and `signed.signature` as the
// `Signature-Input` and `Signature` headers on the outgoing request.
let assert Ok(Nil) =
verifier.verify(
message: request,
public_key: public_key,
signature_params: signature_params,
signature_header: signed.signature,
label: "sig1",
policy: verifier.VerifyPolicy(
required_components: [Derived(Method), Derived(Authority)],
now: 1_618_884_473,
max_age_seconds: None,
),
)
}
This library only computes and checks header values — it has no dependency
on any HTTP framework, so it works with mist, wisp, httpc, or anything
else. verify’s now is always caller-supplied rather than read from the
system clock — this keeps verification deterministic and identical across
the Erlang and JavaScript targets, and trivially testable with fixed
timestamps.
Multiple signatures on one message
RFC 9421 models Signature-Input/Signature as dictionaries, so several
independently-produced signatures (e.g. one from the client, one from a
forwarding proxy — each potentially with a different key, algorithm, or
covered-component set) can share one header pair under different labels.
Use signer.sign_one to produce each signature and signer.combine to pack
them together:
let assert Ok(client_signed) =
signer.sign_one(request, client_key, "client", client_params)
let assert Ok(proxy_signed) =
signer.sign_one(request, proxy_key, "proxy", proxy_params)
let assert Ok(signed) = signer.combine([client_signed, proxy_signed])
verifier.verify already handles this on the way in — call it once per
label, passing the shared signature_header/signature_input value each
time; it extracts just the entry for the given label out of the
dictionary.
Not yet supported
PRs are welcome for any of the following:
- Any signature algorithm other than Ed25519.
SignatureParams.algorithmis a free-form string (not a closed enum), so it’s already possible to construct params for other RFC 9421 §6.3-registered algorithms —signandverifywill just returnUnsupportedAlgorithmfor anything other than"ed25519". - Structured-field component parameters:
sf,key,bs(RFC §2.1.1–2.1.3). - Trailer fields / the
trparameter (RFC §2.1.4). - Request-response binding / the
reqparameter (RFC §2.4). - Signature negotiation (
Accept-Signature, RFC §5). nonceandtagsignature parameters (RFC §2.3) — accepted as “recognized but unused.”- Non-origin-form
@request-target(absolute-form, authority-form, asterisk-form — RFC §2.2.5). Onlymethod + path + queryrequests are supported.
Development
gleam run -m example_message
gleam test
gleam test --target javascript