A complete Go implementation of RFC 9421 HTTP Message Signatures with support for signing, verification, and Content-Digest generation.
- Full RFC 9421 Implementation - Parse, sign, and verify HTTP message signatures
- 6 Signature Algorithms - RSA-PSS, RSA PKCS#1 v1.5, ECDSA (P-256, P-384), Ed25519, HMAC-SHA256
- 7 Digest Algorithms - SHA-2, SHA-3, BLAKE2b families
- Zero External Dependencies - Only
golang.org/x/cryptofor SHA-3/BLAKE2b - High-level Sign/Verify API -
pkg/httpsighelpers for common HTTP flows - Streaming Support - O(1) memory for large message bodies
- RFC 8941 Parser - Complete Structured Field Values implementation
go get github.com/forcebit/http-message-signatures-rfc9421-goRequires: Go 1.21+
package main
import (
"net/http"
"time"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/httpsig"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/parser"
)
func main() {
req, _ := http.NewRequest("POST", "https://example.com/api/resource", nil)
req.Header.Set("Content-Type", "application/json")
components := []parser.ComponentIdentifier{
{Name: "@method", Type: parser.ComponentDerived},
{Name: "@path", Type: parser.ComponentDerived},
{Name: "content-type", Type: parser.ComponentField},
}
key := []byte("0123456789abcdef0123456789abcdef")
signer, _ := httpsig.NewSigner(httpsig.SignerOptions{
Algorithm: "hmac-sha256",
Key: key,
KeyID: "my-key",
Components: components,
Created: time.Now(),
Expires: time.Now().Add(5 * time.Minute),
})
_, _ = signer.SignRequest(req)
verifier, _ := httpsig.NewVerifier(httpsig.VerifyOptions{
Key: key,
Algorithm: "hmac-sha256",
RequiredComponents: []parser.ComponentIdentifier{
{Name: "@method", Type: parser.ComponentDerived},
{Name: "@path", Type: parser.ComponentDerived},
},
ParamsValidation: parser.SignatureParamsValidationOptions{
RequireCreated: true,
CreatedNotOlderThan: 5 * time.Minute,
CreatedNotNewerThan: time.Minute,
RejectExpired: true,
ExpiresNotBeforeCreated: true,
},
})
_, _ = verifier.VerifyRequest(req)
}package main
import (
"net/http"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/httpsig"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/parser"
)
func main() {
key := []byte("0123456789abcdef0123456789abcdef")
signer, _ := httpsig.NewSigner(httpsig.SignerOptions{
Algorithm: "hmac-sha256",
Key: key,
Components: []parser.ComponentIdentifier{{Name: "@status", Type: parser.ComponentDerived}},
})
resp := &http.Response{StatusCode: 200, Header: http.Header{}}
resp.Header.Set("Content-Type", "application/json")
_, _ = signer.SignResponse(resp, nil)
verifier, _ := httpsig.NewVerifier(httpsig.VerifyOptions{
Key: key,
Algorithm: "hmac-sha256",
RequiredComponents: []parser.ComponentIdentifier{
{Name: "@status", Type: parser.ComponentDerived},
},
})
_, _ = verifier.VerifyResponse(resp, nil)
}SignerOptions:
Label: signature label (defaultsig1)Components: covered components (order matters)Algorithm,Key: required for signingKeyID,Nonce,Tag: optional metadataCreated,Expires: set explicit timestampsDisableCreated,DisableAlgorithm: omitcreated/algparamsNow: override clock ifCreatedis zero
VerifyOptions:
Label: specific signature label (required when multiple signatures are present)RequiredComponents: enforce coverageAllowedAlgorithms: allowlist of algorithmsKey,Algorithm: fixed verification key/algKeyResolver: dynamic key lookup (mutually exclusive withKey)ParamsValidation: created/expires policy and skew toleranceLimits: Structured Field parsing limits
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/httpsig"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/parser"
)
func main() {
resolver := httpsig.KeyResolverFunc(func(ctx context.Context, label string, params parser.SignatureParams) (interface{}, string, error) {
if params.KeyID == nil {
return nil, "", fmt.Errorf("missing keyid")
}
key := lookupKey(*params.KeyID)
return key, "hmac-sha256", nil
})
verifier, _ := httpsig.NewVerifier(httpsig.VerifyOptions{
KeyResolver: resolver,
ParamsValidation: parser.SignatureParamsValidationOptions{
RequireCreated: true,
CreatedNotOlderThan: 5 * time.Minute,
},
})
req, _ := http.NewRequest("GET", "https://example.com/api/resource", nil)
_, _ = verifier.VerifyRequest(req)
}
func lookupKey(keyID string) []byte {
return []byte("0123456789abcdef0123456789abcdef")
}Sign:
package main
import (
"net/http"
"time"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/base"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/parser"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/sfv"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/signing"
)
func main() {
req, _ := http.NewRequest("POST", "https://example.com/api/resource", nil)
req.Header.Set("Content-Type", "application/json")
components := []parser.ComponentIdentifier{
{Name: "@method", Type: parser.ComponentDerived},
{Name: "@path", Type: parser.ComponentDerived},
{Name: "content-type", Type: parser.ComponentField},
}
created := time.Now().Unix()
keyID := "my-key"
algID := "hmac-sha256"
params := parser.SignatureParams{
Created: &created,
KeyID: &keyID,
Algorithm: &algID,
}
msg := base.WrapRequest(req)
sigBase, _ := base.Build(msg, components, params)
key := []byte("0123456789abcdef0123456789abcdef")
alg, _ := signing.GetAlgorithm(algID)
sig, _ := alg.Sign(sigBase, key)
sigInputDict := &sfv.Dictionary{
Keys: []string{"sig1"},
Values: map[string]interface{}{
"sig1": sfv.InnerList{
Items: []sfv.Item{
{Value: "@method"},
{Value: "@path"},
{Value: "content-type"},
},
Parameters: []sfv.Parameter{
{Key: "created", Value: created},
{Key: "keyid", Value: keyID},
{Key: "alg", Value: algID},
},
},
},
}
sigDict := &sfv.Dictionary{
Keys: []string{"sig1"},
Values: map[string]interface{}{
"sig1": sfv.Item{Value: sig},
},
}
sigInput, _ := sfv.SerializeDictionary(sigInputDict)
sigHeader, _ := sfv.SerializeDictionary(sigDict)
req.Header.Set("Signature-Input", sigInput)
req.Header.Set("Signature", sigHeader)
}Verify:
package main
import (
"fmt"
"net/http"
"time"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/base"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/parser"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/sfv"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/signing"
)
func VerifyRequest(req *http.Request, key []byte) error {
parsed, err := parser.ParseSignatures(
req.Header.Get("Signature-Input"),
req.Header.Get("Signature"),
sfv.DefaultLimits(),
)
if err != nil {
return fmt.Errorf("parse error: %w", err)
}
sig, ok := parsed.Signatures["sig1"]
if !ok {
return fmt.Errorf("signature \"sig1\" not found")
}
if err := parser.ValidateSignatureParams(sig.SignatureParams, parser.SignatureParamsValidationOptions{
RequireCreated: true,
CreatedNotOlderThan: 5 * time.Minute,
CreatedNotNewerThan: time.Minute,
}); err != nil {
return fmt.Errorf("params error: %w", err)
}
msg := base.WrapRequest(req)
sigBase, err := base.Build(msg, sig.CoveredComponents, sig.SignatureParams)
if err != nil {
return fmt.Errorf("build error: %w", err)
}
algID := "hmac-sha256"
if sig.SignatureParams.Algorithm != nil {
algID = *sig.SignatureParams.Algorithm
}
alg, _ := signing.GetAlgorithm(algID)
return alg.Verify(sigBase, sig.SignatureValue, key)
}package main
import (
"fmt"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/digest"
)
func main() {
body := []byte(`{"hello": "world"}`)
// Compute digest
d, _ := digest.ComputeDigest(body, digest.AlgorithmSHA256)
// Format as header value
header, _ := digest.FormatContentDigest(map[string][]byte{
digest.AlgorithmSHA256: d,
})
fmt.Println(header)
// Output: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
}package main
import (
"io"
"os"
"github.com/forcebit/http-message-signatures-rfc9421-go/pkg/digest"
)
func main() {
file, _ := os.Open("large-file.bin")
defer file.Close()
// Create streaming hasher
h, _ := digest.NewDigester(digest.AlgorithmSHA512)
// Stream through hasher (constant memory)
io.Copy(h, file)
// Get digest
digestBytes := h.Sum(nil)
}| Package | Description |
|---|---|
pkg/httpsig |
High-level sign/verify helpers |
pkg/parser |
Parse Signature-Input and Signature headers |
pkg/base |
Build canonical signature base from HTTP messages |
pkg/signing |
Sign and verify with RFC 9421 algorithms |
pkg/digest |
Content-Digest generation and verification |
pkg/sfv |
RFC 8941 Structured Field Values parser |
| Algorithm ID | Type | Key Type |
|---|---|---|
rsa-pss-sha512 |
RSA-PSS | *rsa.PrivateKey / *rsa.PublicKey |
rsa-v1_5-sha256 |
RSA PKCS#1 v1.5 | *rsa.PrivateKey / *rsa.PublicKey |
ecdsa-p256-sha256 |
ECDSA | *ecdsa.PrivateKey / *ecdsa.PublicKey (P-256) |
ecdsa-p384-sha384 |
ECDSA | *ecdsa.PrivateKey / *ecdsa.PublicKey (P-384) |
ed25519 |
EdDSA | ed25519.PrivateKey / ed25519.PublicKey |
hmac-sha256 |
HMAC | []byte (min 16 bytes, recommended 32) |
| Algorithm ID | Family | Output Size |
|---|---|---|
sha-256 |
SHA-2 | 32 bytes |
sha-512 |
SHA-2 | 64 bytes |
sha-512/256 |
SHA-2 | 32 bytes |
sha3-256 |
SHA-3 | 32 bytes |
sha3-512 |
SHA-3 | 64 bytes |
blake2b-256 |
BLAKE2b | 32 bytes |
blake2b-512 |
BLAKE2b | 64 bytes |
Deprecated algorithms (MD5, SHA-1, etc.) are explicitly rejected.
All RFC 9421 derived components are supported:
| Component | Description |
|---|---|
@method |
HTTP request method (GET, POST, etc.) |
@target-uri |
Full target URI |
@authority |
Host and port |
@scheme |
URI scheme (http, https) |
@request-target |
Request target from request line |
@path |
Absolute path component |
@query |
Query string with leading ? |
@query-param |
Individual query parameter (requires name) |
@status |
Response status code |
func ParseSignatures(signatureInput, signature string, limits sfv.Limits) (*ParsedSignatures, error)Parses Signature-Input and Signature header values into structured data.
func Build(msg HTTPMessage, components []parser.ComponentIdentifier, params parser.SignatureParams) ([]byte, error)Constructs the canonical signature base per RFC 9421 Section 2.5.
func GetAlgorithm(id string) (Algorithm, error)Returns a signing algorithm by its RFC 9421 identifier.
func ComputeDigest(body []byte, algorithm string) ([]byte, error)Computes a cryptographic digest of the body.
func VerifyContentDigest(reader io.Reader, header string, requiredAlgorithms []string) errorVerifies Content-Digest header against streaming body (O(1) memory).
- Constant-time comparison for HMAC and digest verification
- RSA key validation - minimum 2048 bits required
- Algorithm rejection - deprecated algorithms explicitly rejected
- DoS prevention - configurable parser limits via
sfv.Limits
Compared against other Go RFC 9421 implementations (yaronf/httpsign, remitly-oss/httpsig-go, common-fate/httpsig) with consistent created-timestamp validation:
| Metric | Sign | Verify |
|---|---|---|
| RSA-PSS-SHA512 | 6-8% faster | 4-12% faster |
| ECDSA-P256 | 7-11% faster | 2-8% faster |
| HMAC-SHA256 | 1.3-1.8x faster | 1.4-2.3x faster |
| Memory | 7-50% less | 7-50% less |
| Allocations | 5-54% fewer | 5-54% fewer |
See benchmarks/README.md for detailed results and methodology.
# Run all tests
go test ./...
# Run with race detector
go test ./... -race
# Run with coverage
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out
# Run fuzz tests
go test ./pkg/sfv/... -fuzz=FuzzParseDictionary -fuzztime=30s
go test ./pkg/parser/... -fuzz=FuzzParseSignatures -fuzztime=30s- RFC 9421: HTTP Message Signatures
- RFC 8941: Structured Field Values for HTTP
- RFC 9530: Digest Fields
MIT License - see LICENSE for details.