Documentation
¶
Overview ¶
Package samesame implements Web Bot Auth: HTTP Message Signatures (RFC 9421) as profiled by draft-meunier-web-bot-auth-architecture, plus the key directory and Signature-Agent header from draft-meunier-http-message-signatures-directory.
Index ¶
- Constants
- Variables
- func GenerateKey(alg string) (crypto.Signer, error)
- func IsTestKey(thumbprint string) bool
- func MarshalDirectory(keys ...crypto.PublicKey) ([]byte, error)
- func MarshalPrivateKeyPEM(key crypto.Signer) ([]byte, error)
- func NewDirectoryHandler(keys []crypto.Signer, opts DirectoryHandlerOptions) (http.Handler, error)
- func ParsePrivateKeyPEM(data []byte) (crypto.Signer, error)
- func PublicJWK(pub crypto.PublicKey) (jwk.Key, error)
- func StatusCode(err error) int
- func Thumbprint(k jwk.Key) (string, error)
- func WriteChallenge(w http.ResponseWriter, opts ChallengeOptions) error
- type AgentMember
- type ChallengeOptions
- type Directory
- type DirectoryHandlerOptions
- type DirectoryOptions
- type Fetcher
- type FetcherOptions
- type Key
- type KeyResolver
- type MemoryNonceStore
- type NonceStore
- type Outcome
- type Result
- type SignatureAgent
- type Signer
- type SignerOptions
- type StaticDirectory
- type Verifier
- type VerifierOptions
- type VerifyError
Constants ¶
const ( AgentTypeDirectory = "directory" AgentTypeJWKSURI = "jwks_uri" AgentTypeCIMD = "cimd" )
Signature-Agent member types (protocol draft Section 5.5).
const ( AlgEd25519 = "ed25519" AlgECDSAP256SHA256 = "ecdsa-p256-sha256" AlgECDSAP384SHA384 = "ecdsa-p384-sha384" AlgRSAPSSSHA512 = "rsa-pss-sha512" )
Algorithms this package signs and verifies with, named as in the HTTP Signature Algorithms registry. HMAC is deliberately absent: the protocol draft (Section 6.4) forbids shared secrets.
const ( DefaultFetchTimeout = 5 * time.Second DefaultMaxDirectorySize = 64 << 10 DefaultMinTTL = time.Minute DefaultMaxTTL = 24 * time.Hour DefaultTTL = time.Hour DefaultNegativeTTL = 30 * time.Second DefaultMaxCacheEntries = 10_000 DefaultMaxConcurrentFetches = 64 // MaxNegativeTTL bounds how long a failed fetch is remembered (protocol // draft Appendix C.5). MaxNegativeTTL = 5 * time.Minute )
Fetcher defaults.
const ( // DefaultDirectoryMaxAge is the default Cache-Control max-age of a served // directory. DefaultDirectoryMaxAge = time.Hour // DefaultDirectorySignatureLifetime is how long directory response // signatures stay valid. Appendix C.8 of the protocol draft recommends // lifetimes well beyond any republication interval. DefaultDirectorySignatureLifetime = 30 * 24 * time.Hour )
const ( // TagWebBotAuth is the signature tag agents use to sign requests. TagWebBotAuth = "web-bot-auth" // TagDirectory is the signature tag directory servers use to sign // directory responses. TagDirectory = "http-message-signatures-directory" // MediaTypeDirectory is the media type of a key directory. MediaTypeDirectory = "application/http-message-signatures-directory+json" // WellKnownPath is where a key directory is served on an origin. WellKnownPath = "/.well-known/http-message-signatures-directory" // HeaderSignatureAgent names the header that points to an agent's key // directory. HeaderSignatureAgent = "Signature-Agent" )
const ( // DefaultExpiry is how long a signature is valid when SignerOptions.Expiry // is zero. DefaultExpiry = time.Hour // MaxExpiry is the longest signature lifetime the signer allows. The // protocol draft (Section 5.2) recommends no more than 24 hours. MaxExpiry = 24 * time.Hour // DefaultLabel is the default Signature and Signature-Agent member key. DefaultLabel = "sig1" )
const ( // DefaultMaxValidity is the default longest expires - created window the // verifier accepts. The protocol draft recommends signers stay within 24 // hours (Section 5.2) and leaves the verifier's limit to policy // (Appendix C.6). DefaultMaxValidity = 24 * time.Hour // DefaultClockSkew is the default tolerance for created and expires. DefaultClockSkew = time.Minute // DefaultMaxSignatures is the default number of web-bot-auth signatures // the verifier will look at in one request. DefaultMaxSignatures = 4 )
const DefaultMaxKeys = 32
DefaultMaxKeys is the default cap on the number of keys in a directory.
const DefaultNonceStoreSize = 1 << 16
DefaultNonceStoreSize is the default capacity of a MemoryNonceStore.
const MinRSAKeyBits = 2048
MinRSAKeyBits is the smallest RSA modulus accepted for signing or verifying.
const RSAKeyBits = 3072
RSAKeyBits is the modulus size GenerateKey uses for RSA keys.
Variables ¶
var ( ErrMalformedSignatureAgent = errors.New("samesame: malformed Signature-Agent header") ErrUnsupportedAgentType = errors.New("samesame: unsupported Signature-Agent member type") ErrInvalidAgentMember = errors.New("samesame: invalid Signature-Agent member") )
var ( ErrMalformedDirectory = errors.New("samesame: malformed key directory") ErrTooManyKeys = errors.New("samesame: key directory has too many keys") ErrInvalidKey = errors.New("samesame: invalid key in directory") ErrTestKey = errors.New("samesame: key is a published test key") )
var ( ErrNoSignature = errors.New("samesame: no web-bot-auth signature") ErrMalformedSignature = errors.New("samesame: malformed signature headers") ErrInsecureTransport = errors.New("samesame: signature sent over plaintext HTTP") ErrProfile = errors.New("samesame: signature does not follow the web-bot-auth profile") ErrNotYetValid = errors.New("samesame: signature created in the future") ErrExpired = errors.New("samesame: signature expired") ErrValidityTooLong = errors.New("samesame: signature validity window too long") ErrBadSignature = errors.New("samesame: signature does not verify") ErrKeyUnknown = errors.New("samesame: signing key unknown") ErrDiscovery = errors.New("samesame: key discovery failed") ErrNonceRequired = errors.New("samesame: signature has no nonce") ErrReplay = errors.New("samesame: nonce already used") ErrNonceStore = errors.New("samesame: nonce store unavailable") ErrTooManySignatures = errors.New("samesame: too many signatures") )
Errors wrapped by VerifyError. Use errors.Is to check for them.
var ( ErrFetchRefused = errors.New("samesame: directory fetch refused") ErrFetchFailed = errors.New("samesame: directory fetch failed") ErrNotADirectory = errors.New("samesame: response is not a key directory") ErrDirectoryTooBig = errors.New("samesame: key directory too large") ErrBlockedAddress = errors.New("samesame: address is not publicly routable") ErrDirectoryUnbound = errors.New("samesame: directory key has no valid response signature") )
var ( ErrUnsupportedKey = errors.New("samesame: unsupported signing key") ErrSignerConfig = errors.New("samesame: invalid signer configuration") ErrLabelInUse = errors.New("samesame: signature label already present on request") )
var KeyAlgorithms = []string{AlgEd25519, AlgECDSAP256SHA256, AlgECDSAP384SHA384, AlgRSAPSSSHA512}
KeyAlgorithms lists the algorithms GenerateKey accepts.
Functions ¶
func GenerateKey ¶ added in v1.0.0
GenerateKey creates a new signing key for alg, one of KeyAlgorithms.
func IsTestKey ¶ added in v1.0.0
IsTestKey reports whether the thumbprint belongs to a published RFC 9421 test key.
func MarshalDirectory ¶ added in v1.0.0
MarshalDirectory serializes the public halves of keys as an HTTP Message Signatures Directory, the JSON served at WellKnownPath.
func MarshalPrivateKeyPEM ¶ added in v1.0.0
MarshalPrivateKeyPEM encodes key as a PKCS #8 "PRIVATE KEY" PEM block.
func NewDirectoryHandler ¶ added in v1.0.0
NewDirectoryHandler returns an http.Handler that serves the public halves of keys as an HTTP Message Signatures Directory, for mounting at WellKnownPath. Each response carries one signature per key over ("@authority";req "content-digest") with tag http-message-signatures-directory, proving possession of the keys for the requested authority (protocol draft Appendix B.1).
func ParsePrivateKeyPEM ¶ added in v1.0.0
ParsePrivateKeyPEM decodes a PEM private key usable by NewSigner and NewDirectoryHandler. It accepts PKCS #8 ("PRIVATE KEY"), SEC 1 ("EC PRIVATE KEY"), and PKCS #1 ("RSA PRIVATE KEY") blocks.
func PublicJWK ¶ added in v1.0.0
PublicJWK returns pub as a JWK ready for a key directory: kid is the key thumbprint (protocol draft Section 5.5) and use is "sig". Passing a private key is safe; only its public half is exported.
func StatusCode ¶ added in v1.0.0
StatusCode suggests an HTTP status code for rejecting a request that failed with err (protocol draft Sections 5.3 and 5.4): 400 for unparseable signature headers, 429 for a replayed nonce, and 403 otherwise. Whether to reject an unverified request at all is the caller's policy.
func Thumbprint ¶ added in v1.0.0
Thumbprint returns the base64url JWK SHA-256 thumbprint of k. Web Bot Auth uses this as the keyid signature parameter.
func WriteChallenge ¶ added in v1.0.0
func WriteChallenge(w http.ResponseWriter, opts ChallengeOptions) error
WriteChallenge sets an Accept-Signature header asking for a web-bot-auth signature (protocol draft Section 5.3). It does not write a status code; use StatusCode.
Types ¶
type AgentMember ¶ added in v1.0.0
type AgentMember struct {
// Key is the dictionary member key. It is empty for the legacy string
// form of the header.
Key string
// Type is the member's type parameter, AgentTypeDirectory when absent.
Type string
// Value is the URI string exactly as sent.
Value string
// Identifier is the URL a verifier resolves for this member. It is nil
// when the member must be ignored; Err then says why.
Identifier *url.URL
Err error
}
AgentMember is one member of a Signature-Agent header.
type ChallengeOptions ¶ added in v1.0.0
type ChallengeOptions struct {
// Label is the requested signature label and Signature-Agent member key.
// Defaults to DefaultLabel.
Label string
// Nonce, when set, asks the agent to sign with this exact nonce. The
// caller is responsible for remembering and checking it.
Nonce string
}
ChallengeOptions controls the Accept-Signature header WriteChallenge sends.
type Directory ¶ added in v1.0.0
type Directory struct {
Keys []Key
// Invalid holds one error per key that was dropped while parsing. The
// protocol draft says verifiers should reject malformed entries, so one
// bad key does not reject the whole directory.
Invalid []error
}
Directory is a parsed HTTP Message Signatures Directory.
func ParseDirectory ¶ added in v1.0.0
func ParseDirectory(data []byte, opts DirectoryOptions) (*Directory, error)
ParseDirectory parses a JWKS key directory. It fails only when the document as a whole is malformed or too large; bad individual keys are dropped and recorded in Directory.Invalid.
type DirectoryHandlerOptions ¶ added in v1.0.0
type DirectoryHandlerOptions struct {
// Authorities lists the hosts, as sent in the Host header and with any
// non-default port, that this directory is served for, such as
// "bot.example". Required. Directory response signatures bind the keys
// to the request's authority (Appendix B.1), so signing for any Host a
// client sends would hand out proofs for other domains. Requests for
// other hosts get 421 Misdirected Request.
Authorities []string
// MaxAge is sent as Cache-Control max-age. Defaults to
// DefaultDirectoryMaxAge.
MaxAge time.Duration
// SignatureLifetime is the expires - created of each directory response
// signature. Defaults to DefaultDirectorySignatureLifetime.
SignatureLifetime time.Duration
}
DirectoryHandlerOptions configures NewDirectoryHandler.
type DirectoryOptions ¶ added in v1.0.0
type DirectoryOptions struct {
// MaxKeys caps the number of keys a directory may list (protocol draft
// Section 6.7). Zero means DefaultMaxKeys.
MaxKeys int
// AllowTestKeys accepts the published RFC 9421 test keys. Only tests
// should set this (protocol draft Section 6.8).
AllowTestKeys bool
}
DirectoryOptions controls how a key directory is parsed.
type Fetcher ¶ added in v1.0.0
type Fetcher struct {
// contains filtered or unexported fields
}
Fetcher resolves Signature-Agent identifiers to key directories over HTTPS, with caching. It implements KeyResolver.
func NewFetcher ¶ added in v1.0.0
func NewFetcher(opts FetcherOptions) *Fetcher
NewFetcher creates a Fetcher.
func (*Fetcher) Resolve ¶ added in v1.0.0
Resolve returns the key directory at identifier, from cache when fresh.
A failed fetch never evicts a cached directory (protocol draft Section 6.10): the last good directory keeps being returned until a fetch succeeds. A successful fetch always replaces it, even if keys disappear.
type FetcherOptions ¶ added in v1.0.0
type FetcherOptions struct {
// Directory controls how fetched directories are parsed.
Directory DirectoryOptions
// Timeout bounds each fetch, including reading the body. Defaults to
// DefaultFetchTimeout.
Timeout time.Duration
// MaxBodySize caps the directory size after content decoding. Defaults
// to DefaultMaxDirectorySize.
MaxBodySize int64
// MinTTL and MaxTTL clamp how long a fetched directory is cached. TTL
// is used when the response has no caching headers.
MinTTL, MaxTTL, TTL time.Duration
// NegativeTTL is the base backoff after a failed fetch. It grows
// exponentially with consecutive failures, is raised to any
// Retry-After, and never exceeds MaxNegativeTTL.
NegativeTTL time.Duration
// MaxCacheEntries caps the number of cached directories. Defaults to
// DefaultMaxCacheEntries.
MaxCacheEntries int
// MaxConcurrentFetches caps fetches in flight across all origins.
// Fetches of the same identifier are coalesced, and a directory
// identifier is one per origin, so each origin already has at most one
// fetch in flight (protocol draft Appendix C.3). Defaults to
// DefaultMaxConcurrentFetches.
MaxConcurrentFetches int
// ClockSkew is the tolerance for directory response signature times.
// Defaults to DefaultClockSkew.
ClockSkew time.Duration
// AllowedHosts, when set, is the only set of hosts (as in URL.Host)
// that may be fetched.
AllowedHosts []string
// AllowPrivateAddresses permits fetching from loopback, private,
// link-local, and other non-public addresses. Only tests should set it.
AllowPrivateAddresses bool
// TLSConfig is used for fetches, for example to trust a test CA.
TLSConfig *tls.Config
// VerifyDirectorySignatures keeps only keys that carry a valid directory
// response signature (protocol draft Appendix B.1). The draft lets
// verifiers use directly resolved keys without this proof, so it is off
// by default.
VerifyDirectorySignatures bool
// Now returns the current time. Defaults to time.Now.
Now func() time.Time
}
FetcherOptions configures a Fetcher.
type Key ¶ added in v1.0.0
type Key struct {
// ID is the base64url JWK SHA-256 thumbprint (RFC 7638, RFC 8037). A
// signature's keyid parameter must match it.
ID string
// JWK is the public key.
JWK jwk.Key
// NotBefore and Expires come from the nbf and exp members, and are zero
// when absent. The protocol draft does not define these members, so
// honoring them is local policy.
NotBefore time.Time
Expires time.Time
// contains filtered or unexported fields
}
Key is a public key from a key directory.
type KeyResolver ¶ added in v1.0.0
type KeyResolver interface {
Resolve(ctx context.Context, identifier *url.URL) (*Directory, error)
}
KeyResolver resolves a Signature-Agent identifier to its key directory. Fetcher implements it.
type MemoryNonceStore ¶ added in v1.0.0
type MemoryNonceStore struct {
// contains filtered or unexported fields
}
MemoryNonceStore is an in-process NonceStore with a fixed capacity. Entries are forgotten once their signature expires.
When the store is full, it evicts the soonest-expiring nonce of the scope holding the most entries instead of refusing new ones. A flood of signed requests, which anyone with their own key directory can produce, then weakens replay protection for the heaviest scope, usually the flooder, rather than making every agent unverifiable. Evicted reports how often this happens.
func NewMemoryNonceStore ¶ added in v1.0.0
func NewMemoryNonceStore(size int) *MemoryNonceStore
NewMemoryNonceStore creates a MemoryNonceStore holding at most size nonces. A size of zero or less means DefaultNonceStoreSize.
func (*MemoryNonceStore) CheckAndRecord ¶ added in v1.0.0
func (*MemoryNonceStore) Evicted ¶ added in v1.0.0
func (s *MemoryNonceStore) Evicted() uint64
Evicted returns how many unexpired nonces were dropped to make room.
func (*MemoryNonceStore) Len ¶ added in v1.0.0
func (s *MemoryNonceStore) Len() int
Len returns the number of nonces currently remembered.
type NonceStore ¶ added in v1.0.0
type NonceStore interface {
CheckAndRecord(ctx context.Context, scope, nonce string, until time.Time) (fresh bool, err error)
}
NonceStore records signature nonces to detect replays.
CheckAndRecord must atomically check whether nonce has been seen in scope and, if not, record it until the given time. It returns true when the nonce is fresh. The verifier passes the agent's identifier, or its keyid for static keys, as scope, so one agent's nonces cannot collide with or crowd out another's.
If CheckAndRecord returns an error, the verifier treats the signature as unverified: without nonce state it cannot say whether the request is a replay (protocol draft Appendix C.6).
type Outcome ¶ added in v1.0.0
type Outcome int
Outcome is the result of verifying a request (protocol draft Appendix C.1).
const ( // OutcomeUnverified means the verifier could not get enough information // to decide: the request is unsigned, discovery failed, or the key is // unknown. It is neither proof of identity nor proof of forgery. OutcomeUnverified Outcome = iota // OutcomeInvalid means a signature, its covered components, its key, or // its freshness failed to verify. OutcomeInvalid // OutcomeVerified means a signature verified. OutcomeVerified )
type Result ¶ added in v1.0.0
type Result struct {
Outcome Outcome
// Label is the signature's label in Signature and Signature-Input.
Label string
// KeyID is the key thumbprint. When Identifier is nil, it is the only
// identity the request has.
KeyID string
// Identifier is the resolved key directory URL the request is attributed
// to, or nil when the key came from StaticKeys.
Identifier *url.URL
// Key is the public key the signature verified with.
Key jwk.Key
Created time.Time
Expires time.Time
Nonce string
// NonceChecked is true when a NonceStore saw this nonce for the first
// time. It is false when there was no nonce or no NonceStore.
NonceChecked bool
}
Result describes a verified signature.
type SignatureAgent ¶ added in v1.0.0
type SignatureAgent struct {
// Legacy is true when the header used the deprecated single String Item
// form. Its one member then has an empty Key.
Legacy bool
Members map[string]AgentMember
}
SignatureAgent is a parsed Signature-Agent header.
func ParseSignatureAgent ¶ added in v1.0.0
func ParseSignatureAgent(h http.Header) (*SignatureAgent, error)
ParseSignatureAgent parses the Signature-Agent header in h. It returns nil and no error when the header is absent. Members that are well-formed structured fields but unusable (wrong scheme, not an origin, unsupported type) are kept with Identifier nil and Err set, so callers can report why a signature could not be attributed.
func (*SignatureAgent) Member ¶ added in v1.0.0
func (sa *SignatureAgent) Member(key string) (AgentMember, bool)
Member returns the member a signature covers. For the dictionary form, key is the ;key= parameter of the covered signature-agent component. For the legacy form, the component has no key and the single member is returned.
type Signer ¶ added in v1.0.0
type Signer struct {
// contains filtered or unexported fields
}
Signer signs outgoing requests with Web Bot Auth signatures.
func NewSigner ¶ added in v1.0.0
func NewSigner(key crypto.Signer, opts SignerOptions) (*Signer, error)
NewSigner creates a Signer for key, which must be an ed25519.PrivateKey, an *ecdsa.PrivateKey on P-256 or P-384, or an *rsa.PrivateKey (signed with RSA-PSS SHA-512).
func (*Signer) Sign ¶ added in v1.0.0
Sign adds Signature-Agent, Signature-Input, and Signature headers to r. It covers @authority and the signer's Signature-Agent member. Existing signatures and Signature-Agent members on r are kept. On error, r's headers are left as they were.
func (*Signer) Transport ¶ added in v1.0.0
func (s *Signer) Transport(next http.RoundTripper) http.RoundTripper
Transport returns an http.RoundTripper that signs every request before passing it to next. If next is nil, http.DefaultTransport is used.
type SignerOptions ¶ added in v1.0.0
type SignerOptions struct {
// AgentOrigin is the https origin that serves this agent's key directory,
// such as "https://bot.example". It is sent in Signature-Agent. Required:
// the protocol draft (Section 4.3) says a signed request MUST carry
// Signature-Agent.
AgentOrigin string
// Label is the Signature / Signature-Input dictionary key. Defaults to
// DefaultLabel.
Label string
// AgentKey is the Signature-Agent dictionary key. Defaults to Label.
AgentKey string
// Expiry is how long each signature is valid. Defaults to DefaultExpiry
// and may not exceed MaxExpiry.
Expiry time.Duration
// DisableNonce omits the nonce parameter. By default each signature gets
// a random 64-byte nonce so verifiers can detect replays.
DisableNonce bool
}
SignerOptions configures a Signer.
type StaticDirectory ¶ added in v1.0.0
type StaticDirectory struct {
// Body is the directory JSON, byte for byte what NewDirectoryHandler
// serves.
Body []byte
// Header holds the Content-Type, Cache-Control, Content-Digest,
// Signature-Input, and Signature fields to send with Body.
Header http.Header
// Expires is when the signatures stop being valid. Generate new ones
// before then.
Expires time.Time
}
StaticDirectory is a key directory with precomputed response signatures, for serving from a static file server.
func SignStaticDirectory ¶ added in v1.0.0
func SignStaticDirectory(keys []crypto.Signer, authority string, opts DirectoryHandlerOptions) (*StaticDirectory, error)
SignStaticDirectory signs a key directory for one authority, the host verifiers fetch it from. The signatures cover only that authority and the body's Content-Digest, so they are the same for every request and can be configured as static response headers. opts.Authorities is ignored.
The file server must send Body unmodified: compressing it changes the bytes Content-Digest covers.
type Verifier ¶ added in v1.0.0
type Verifier struct {
// contains filtered or unexported fields
}
Verifier checks web-bot-auth signatures on incoming requests.
func NewVerifier ¶ added in v1.0.0
func NewVerifier(opts VerifierOptions) (*Verifier, error)
NewVerifier creates a Verifier.
type VerifierOptions ¶ added in v1.0.0
type VerifierOptions struct {
// Resolver fetches key directories named by Signature-Agent. When nil,
// only StaticKeys are used.
Resolver KeyResolver
// StaticKeys are keys known out of band. A request verified with one of
// them is identified only by its key thumbprint, never by a URL
// (protocol draft Section 4.3).
StaticKeys []crypto.PublicKey
// AllowTestKeys accepts the published RFC 9421 test keys in StaticKeys.
// Only tests should set this.
AllowTestKeys bool
// MaxValidity caps expires - created. Zero means DefaultMaxValidity.
MaxValidity time.Duration
// ClockSkew is the tolerance applied to created and expires. Zero means
// DefaultClockSkew.
ClockSkew time.Duration
// MaxSignatures caps how many web-bot-auth signatures in one request are
// examined. Zero means DefaultMaxSignatures.
MaxSignatures int
// NonceStore, when set, rejects signatures whose nonce was already seen.
NonceStore NonceStore
// RequireNonce rejects signatures without a nonce. It only makes sense
// with a NonceStore.
RequireNonce bool
// Scheme returns the scheme the client used. Behind a reverse proxy that
// terminates TLS, set this to read a trusted forwarded header. The
// default is "https" when r.TLS is set and "http" otherwise.
Scheme func(r *http.Request) string
// AllowInsecure accepts signatures on plaintext HTTP requests. The
// protocol draft (Section 6.1) says origins SHOULD refuse them.
AllowInsecure bool
// Now returns the current time. Defaults to time.Now.
Now func() time.Time
}
VerifierOptions configures a Verifier.
type VerifyError ¶ added in v1.0.0
type VerifyError struct {
Outcome Outcome
Label string // signature label, empty when not specific to one
Err error
}
VerifyError describes why a request did not verify.
func (*VerifyError) Error ¶ added in v1.0.0
func (e *VerifyError) Error() string
func (*VerifyError) LogValue ¶ added in v1.0.0
func (e *VerifyError) LogValue() slog.Value
func (*VerifyError) Unwrap ¶ added in v1.0.0
func (e *VerifyError) Unwrap() error