A Golang implementation of HTTP Message Signatures, as defined by RFC 9421 (the former draft-ietf-httpbis-message-signatures).
This is a nearly feature-complete implementation of the RFC, including all test vectors.
The library provides natural integration points with Go HTTP clients and servers, as well as direct usage of the sign and verify functions.
Below is what a basic client-side integration looks like. Additional examples are available in the API reference.
// Create a signer and a wrapped HTTP client
signer, _ := httpsign.NewRSAPSSSigner(*prvKey, httpsign.NewSignConfig(),
httpsign.Headers("@request-target", "content-digest")) // The Content-Digest header will be auto-generated
client := httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName("sig1").SetSigner(signer)) // sign requests, don't verify responses
// Send an HTTP POST, get response -- signing happens behind the scenes
body := `{"hello": "world"}`
res, _ := client.Post(ts.URL, "application/json", bufio.NewReader(strings.NewReader(body)))
// Read the response
serverText, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
v0.6.0 (shipped) raised the Go floor to 1.27+ and cut foreign JWS over to jwx v4 + ML-DSA. See internal-docs/RELEASE-v0.6.0.md.
v0.6.1 breaks foreign-JWS verify again: prefer NewJWSVerifier(allowed, key, …) (infer alg); use NewJWSVerifierWithAlg when needed; pass a JWSAlgAllowlist (nil skips policy). NewJWSSigner defaults to / requires SignAlg(false); use NewJWSSignerFromJWK when the private key is a JWK.
| Caller | Change in v0.6.1 |
|---|---|
| Native algorithms only | None. |
NewJWSVerifier(alg, key, …) (v0.6.0) |
Prefer NewJWSVerifier(allowed, key, …); else NewJWSVerifierWithAlg(allowed, alg, key, …). |
NewJWSSigner |
Nil config ⇒ SignAlg(false); SignAlg(true) errors. Prefer NewJWSSignerFromJWK for private JWKs. |
Pass a non-nil allowlist when keyid can select among keys. SetAllowedAlgs still only filters Signature-Input alg, not JWS jwa.
Full notes: internal-docs/RELEASE-v0.6.1.md.
Optional algorithms beyond the native set use lestrrat-go/jwx/v4 (≥ v4.5.0) via NewJWSSigner / NewJWSSignerFromJWK / NewJWSVerifier.
ML-DSA (FIPS 204) works through the same path with crypto/mldsa keys. Prefer inferring the alg from the public key:
priv, _ := mldsa.GenerateKey(mldsa.MLDSA65())
pub := priv.Public().(*mldsa.PublicKey)
signer, _ := httpsign.NewJWSSigner(jwa.MLDSA65(), priv, nil, fields) // SignAlg(false) by default
allowed, _ := httpsign.NewJWSAlgAllowlist(jwa.MLDSA65())
verifier, _ := httpsign.NewJWSVerifier(allowed, pub, httpsign.NewVerifyConfig(), fields)HMAC keys must be []byte (minimum length per RFC 7518). Raw RSA verify needs NewJWSVerifierWithAlg (or a JWK that carries alg).
- Requires Go 1.27+.
- The
Accept-Signatureheader is unimplemented. - In responses, when using the "wrapped handler" feature, the
Content-Typeheader is only signed if set explicitly by the server. This is different, but arguably more secure, than the normalnet.httpbehavior. - Behind a TLS-terminating reverse proxy: The
@schemederived component defaults toreq.TLS != nil. Behind nginx, Envoy, AWS ALB, etc.,req.TLSis nil, so@schemebecomes"http"even for HTTPS traffic. UseSetSchemeFromRequestonSignConfigandVerifyConfigto derive the scheme fromX-Forwarded-Protoor similar headers. - Nonce-based replay prevention: The signer can include a nonce via
SetNonce; the verifier does not track seen nonces by default. UseSetNonceValidatoronVerifyConfigto implement replay prevention—the callback must check uniqueness (e.g. via a cache or database) and return an error for duplicates. - Replay window: Without nonce validation,
SetNotOlderThan(default 10s) is the only replay defense. For sensitive operations, reduce this value or useSetNonceValidator. See the method docstrings for details.
Contributions to this project are welcome, both as issues and pull requests.