Help us shape the future of Go! Take the 2026 Go Developer Survey

samesame

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: AGPL-3.0 Imports: 36 Imported by: 0

README

samesame

The Web Bot Auth implementation for Anubis.

samesame lets HTTP clients sign requests and lets servers verify who sent them, following draft-ietf-webbotauth-httpsig-protocol-00 on top of HTTP Message Signatures (RFC 9421). It has three parts:

  • Verifier checks signatures on incoming requests. Anubis uses this.
  • Signer signs outgoing requests for a bot.
  • NewDirectoryHandler serves a bot's public keys at /.well-known/http-message-signatures-directory.

It requires Go 1.27 or later.

Verifying requests

v, err := samesame.NewVerifier(samesame.VerifierOptions{
	Resolver:   samesame.NewFetcher(samesame.FetcherOptions{}),
	NonceStore: samesame.NewMemoryNonceStore(0),
})
if err != nil {
	return err
}

res, err := v.Verify(r)
switch samesame.OutcomeOf(err) {
case samesame.OutcomeVerified:
	// res.Identifier is the agent's key directory URL, for example
	// https://bot.example/.well-known/http-message-signatures-directory.
	// It is nil when the key came from VerifierOptions.StaticKeys; then
	// res.KeyID is the only identity.
case samesame.OutcomeInvalid:
	// A signature is present and wrong: bad signature, expired, replayed,
	// or not following the web-bot-auth profile.
	http.Error(w, "invalid signature", samesame.StatusCode(err))
case samesame.OutcomeUnverified:
	// Unsigned, unknown key, or key discovery failed. This is not proof of
	// anything. Treat it as a signal, not as a verdict.
}

The verifier:

  • requires created, expires, keyid, and tag="web-bot-auth";
  • requires @authority or @target-uri to be covered;
  • limits expires - created to 24 hours by default;
  • finds the Signature-Agent member through the covered component's ;key= parameter, and accepts the legacy string form of the header;
  • refuses signatures sent over plain HTTP.

Behind a proxy that terminates TLS, set VerifierOptions.Scheme to read the original scheme from a header that you trust.

Fetcher downloads key directories named by Signature-Agent. That URL comes from the client, so the fetcher only uses https, does not follow redirects, and refuses to connect to loopback, private, link-local, and other non-public addresses. It also limits size, time, and concurrency. It caches directories using the response's caching headers. If a refresh fails, the fetcher keeps the last good directory.

Signing requests

s, err := samesame.NewSigner(privateKey, samesame.SignerOptions{
	AgentOrigin: "https://bot.example",
})
if err != nil {
	return err
}

client := &http.Client{Transport: s.Transport(nil)}

Supported keys are Ed25519, ECDSA P-256 and P-384, and RSA (RSA-PSS SHA-512). By default, each signature is valid for one hour and has a random nonce.

Serving the key directory

h, err := samesame.NewDirectoryHandler([]crypto.Signer{privateKey}, samesame.DirectoryHandlerOptions{
	// The hosts this directory is served for. Other hosts get 421.
	Authorities: []string{"bot.example"},
})
if err != nil {
	return err
}

mux.Handle(samesame.WellKnownPath, h)

The handler publishes only the public keys. Each response has one signature per key, which proves that the operator of that origin holds the key. The signatures are bound to the requested host, so the handler signs only for the hosts in Authorities. Otherwise anyone could get proofs that bind your keys to their own domain. Signatures are cached per host for up to an hour.

Managing keys with the CLI

cmd/samesame generates keys and the directory to publish for them.

go install github.com/TecharoHQ/samesame/cmd/samesame@latest

# Make a key. The key's keyid is printed; the file has mode 0600.
samesame keygen --out bot.key

# Print the directory JSON to serve at
# /.well-known/http-message-signatures-directory.
samesame directory bot.key

# To rotate, publish the new key next to the old one first.
samesame keygen --alg ecdsa-p256-sha256 --out next.key
samesame directory bot.key next.key

# Print the keyid of existing private or public PEM keys.
samesame keyid bot.key next.key

samesame directory prints the same JSON that NewDirectoryHandler serves.

Serving the directory as a static file

To serve the directory from nginx or Caddy instead of NewDirectoryHandler, use --sign-for:

samesame directory bot.key next.key \
	--sign-for bot.example \
	--out /srv/www/http-message-signatures-directory

This writes the directory to --out exactly as it must be served. It then prints an nginx location block and a Caddy handle block that serve that file with:

  • the media type application/http-message-signatures-directory+json;
  • Cache-Control (set with --max-age);
  • Content-Digest;
  • one directory response signature per key.

Use --format nginx or --format caddy to print only one of them.

These signatures prove that you hold the keys. Some verifiers require them, for example Cloudflare. They cover only the host and the body, so they can be static headers. Keep these points in mind:

  • The signatures expire after --lifetime (default 30 days). The printed config shows the exact time. Run the command again before then, and every time the keys change.
  • Give every host that verifiers use to reach the directory as its own --sign-for. A signature for one host does not work for another host.
  • Do not compress this file. Content-Digest covers the exact bytes that the server sends. The nginx block turns gzip off. If your Caddy site uses encode, limit it to other paths, as the printed comment shows.
  • In nginx, an add_header in the location block replaces the add_header directives that the server block would give it.
Serving a folder of keys

samesame serve serves the directory for every .pem private key in a folder. It watches the folder, so you can add or delete a key file to rotate keys without a restart:

samesame serve --keys ./var --authority bot.example --bind :8080
  • Responses carry directory signatures for each --authority. Other hosts get 421. Repeat --authority for each host verifiers use.
  • If a .pem file does not parse, for example while it is being written, the previous keys stay served until the next good reload.
  • With no keys in the folder, it answers 503.
  • It also reloads every --poll (default one minute), in case a change event is missed.
  • --keys, --authority, and --bind can also be set with SAMESAME_KEYS, SAMESAME_AUTHORITY, and SAMESAME_BIND.

In Go, the same operations are GenerateKey, MarshalPrivateKeyPEM, ParsePrivateKeyPEM, PublicJWK, MarshalDirectory, and SignStaticDirectory.

Docker

The image runs samesame serve. It is published to ghcr.io/techarohq/samesame.

docker run -d -p 8080:8080 \
	--user "$(id -u):$(id -g)" \
	-v "$PWD/var:/keys:ro" \
	-e SAMESAME_AUTHORITY=bot.example \
	ghcr.io/techarohq/samesame:latest
  • Mount your folder of .pem private keys at /keys. The container keeps watching it.
  • For several hosts, give SAMESAME_AUTHORITY as a comma-separated list, for example bot.example,www.bot.example.
  • The image runs as the non-root user 65532. Private keys usually have mode 0600, so the container cannot read them unless it runs as their owner. Use --user as in the example. In Kubernetes, use fsGroup with a Secret defaultMode of 0440. If the container cannot read the keys, it logs permission denied and answers 503.
  • samesame --version prints the version that the Go toolchain stamped from git.

To build the image:

docker buildx bake local                  # samesame:local, for this machine
VERSION=v1.2.3 docker buildx bake --push  # linux/amd64 and linux/arm64

The Docker workflow runs as follows:

  • Each pull request builds the image without pushing it.
  • Each push to main or develop pushes a branch tag and a sha- tag.
  • Each release pushes vX.Y.Z, vX.Y, and latest.
  • .dockerignore keeps var/ and all *.pem files out of the build context.

Not supported yet

  • jwks_uri and cimd members of Signature-Agent. The verifier ignores them.
  • Delegation and certificate chains.
  • The Signature Agent Card in draft-meunier-webbotauth-registry.

Development

npm ci        # commit hooks
go test ./...

The implementation plan is in docs/plans/base-implementation.md.

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

View Source
const (
	AgentTypeDirectory = "directory"
	AgentTypeJWKSURI   = "jwks_uri"
	AgentTypeCIMD      = "cimd"
)

Signature-Agent member types (protocol draft Section 5.5).

View Source
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.

View Source
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.

View Source
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
)
View Source
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"
)
View Source
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"
)
View Source
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
)
View Source
const DefaultMaxKeys = 32

DefaultMaxKeys is the default cap on the number of keys in a directory.

View Source
const DefaultNonceStoreSize = 1 << 16

DefaultNonceStoreSize is the default capacity of a MemoryNonceStore.

View Source
const MinRSAKeyBits = 2048

MinRSAKeyBits is the smallest RSA modulus accepted for signing or verifying.

View Source
const RSAKeyBits = 3072

RSAKeyBits is the modulus size GenerateKey uses for RSA keys.

Variables

View Source
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")
)
View Source
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")
)
View Source
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.

View Source
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")
)
View Source
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")
)

KeyAlgorithms lists the algorithms GenerateKey accepts.

Functions

func GenerateKey added in v1.0.0

func GenerateKey(alg string) (crypto.Signer, error)

GenerateKey creates a new signing key for alg, one of KeyAlgorithms.

func IsTestKey added in v1.0.0

func IsTestKey(thumbprint string) bool

IsTestKey reports whether the thumbprint belongs to a published RFC 9421 test key.

func MarshalDirectory added in v1.0.0

func MarshalDirectory(keys ...crypto.PublicKey) ([]byte, error)

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

func MarshalPrivateKeyPEM(key crypto.Signer) ([]byte, error)

MarshalPrivateKeyPEM encodes key as a PKCS #8 "PRIVATE KEY" PEM block.

func NewDirectoryHandler added in v1.0.0

func NewDirectoryHandler(keys []crypto.Signer, opts DirectoryHandlerOptions) (http.Handler, error)

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

func ParsePrivateKeyPEM(data []byte) (crypto.Signer, error)

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

func PublicJWK(pub crypto.PublicKey) (jwk.Key, error)

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

func StatusCode(err error) int

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

func Thumbprint(k jwk.Key) (string, error)

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.

func (*Directory) Key added in v1.0.0

func (d *Directory) Key(id string, now time.Time) (Key, bool)

Key returns the key with the given thumbprint if it is valid at now.

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

func (f *Fetcher) Resolve(ctx context.Context, identifier *url.URL) (*Directory, error)

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.

func NewKey added in v1.0.0

func NewKey(k jwk.Key) (Key, error)

NewKey validates a public JWK for Web Bot Auth and returns it as a Key with ID set to its thumbprint. It rejects private and symmetric keys and key types or sizes this package cannot verify. KeyResolver implementations should build Directory keys with it.

func (Key) ValidAt added in v1.0.0

func (k Key) ValidAt(t time.Time) bool

ValidAt reports whether the key's nbf/exp window contains t.

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 (s *MemoryNonceStore) CheckAndRecord(_ context.Context, scope, nonce string, until time.Time) (bool, error)

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
)

func OutcomeOf added in v1.0.0

func OutcomeOf(err error) Outcome

OutcomeOf returns the outcome carried by err: OutcomeVerified for nil, the VerifyError's outcome, or OutcomeUnverified for any other error.

func (Outcome) String added in v1.0.0

func (o Outcome) String() string

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) KeyID added in v1.0.0

func (s *Signer) KeyID() string

KeyID returns the thumbprint sent as the keyid parameter.

func (*Signer) Sign added in v1.0.0

func (s *Signer) Sign(r *http.Request) error

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.

func (*Verifier) Verify added in v1.0.0

func (v *Verifier) Verify(r *http.Request) (*Result, error)

Verify checks the web-bot-auth signatures on r. It returns the first signature that verifies. Otherwise it returns a *VerifyError: if any signature is invalid the error's outcome is OutcomeInvalid, else it is OutcomeUnverified.

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

Directories

Path Synopsis
cmd
samesame command
Command samesame generates and inspects Web Bot Auth signing keys.
Command samesame generates and inspects Web Bot Auth signing keys.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL