Documentation
¶
Overview ¶
Package webhook provides protocol-independent webhook signing, verification, replay protection, and delivery primitives.
Index ¶
- Constants
- Variables
- func Canonicalize(message Message, keyID string, algorithm Algorithm) ([]byte, error)
- func CaptureBody(request *http.Request, maxBytes int64) ([]byte, error)
- func MarshalDeliveryRequest(delivery DeliveryRequest, maxBytes int) ([]byte, error)
- func NewSecureHTTPClient(policy *SSRFPolicy, timeout time.Duration) (*http.Client, error)
- func SetSignatureHeaders(header http.Header, signatures []Signature) error
- func VerifiedBodyFromContext(ctx context.Context) ([]byte, bool)
- type Algorithm
- type DeadLetterFunc
- type Deliverer
- func (d *Deliverer) Deliver(ctx context.Context, delivery DeliveryRequest) (DeliveryResult, error)
- func (d *Deliverer) DeliverOnce(ctx context.Context, delivery DeliveryRequest) (DeliveryResult, error)
- func (d *Deliverer) FanOut(ctx context.Context, deliveries []DeliveryRequest, workers int) ([]FanOutResult, error)
- func (d *Deliverer) Replay(ctx context.Context, originalDeliveryID string, delivery DeliveryRequest) (DeliveryResult, error)
- type DeliveryAttempt
- type DeliveryConfig
- type DeliveryRequest
- type DeliveryResult
- type EndpointPolicy
- type EndpointPolicyFunc
- type Envelope
- type ErrorHook
- type EventIDExtractor
- type FailureClassification
- type FanOutResult
- type HTTPDoer
- type HeaderLimits
- type Message
- type MiddlewareConfig
- type NetIPResolver
- type NonceGenerator
- type Observation
- type Observer
- type ObserverFunc
- type Operation
- type Outcome
- type Reason
- type ReplayHook
- type ReplayStore
- type RequestOptions
- type RetryPolicy
- type SSRFPolicy
- type SSRFPolicyConfig
- type Signature
- type Signer
- type SignerConfig
- type SigningKey
- type SleepFunc
- type Verification
- type VerificationError
- type VerificationKey
- type Verifier
- func (v *Verifier) Middleware(config MiddlewareConfig, next http.Handler) (http.Handler, error)
- func (v *Verifier) Verify(message Message, signatures []Signature) (Verification, error)
- func (v *Verifier) VerifyAndRecord(ctx context.Context, message Message, signatures []Signature, eventID string) (Verification, error)
- func (v *Verifier) VerifyRequest(ctx context.Context, request *http.Request, options RequestOptions) (verification Verification, body []byte, err error)
- type VerifierConfig
Examples ¶
Constants ¶
const IdempotencyHeader = "Idempotency-Key"
const (
// SignatureHeader carries one strict structured value per signing key.
SignatureHeader = "Webhook-Signature"
)
Variables ¶
var ( // ErrBodyTooLarge means a request body exceeded its configured hard limit. ErrBodyTooLarge = errors.New("webhook body too large") // ErrBodyRead means the exact request body could not be captured. ErrBodyRead = errors.New("webhook body read failed") )
var ( ErrDeliveryFailed = errors.New("webhook delivery failed") ErrEndpointRejected = errors.New("webhook endpoint rejected") ErrResponseTooLarge = errors.New("webhook response too large") ErrFanOutLimit = errors.New("webhook fan-out limit exceeded") )
var ( // ErrMalformedSignatureHeader means signature header syntax was invalid or // ambiguous. One malformed value rejects the entire header set. ErrMalformedSignatureHeader = errors.New("malformed webhook signature header") // ErrSignatureHeadersTooLarge means header count or bytes exceeded limits. ErrSignatureHeadersTooLarge = errors.New("webhook signature headers too large") // ErrMalformedSignedHeader means a fixed behavior-changing header was // duplicated, oversized, or otherwise unsafe to canonicalize. ErrMalformedSignedHeader = errors.New("malformed webhook signed header") )
var ( // ErrInvalidSignature means that no supplied signature authenticated. ErrInvalidSignature = errors.New("invalid webhook signature") // ErrInvalidTimestamp means that a signature timestamp is missing, invalid, // or outside the configured tolerance. ErrInvalidTimestamp = errors.New("invalid webhook timestamp") // ErrInvalidConfiguration means a signer or verifier configuration is not // safe to use. ErrInvalidConfiguration = errors.New("invalid webhook configuration") // ErrNoActiveKey means no configured signing key is active. ErrNoActiveKey = errors.New("no active webhook signing key") // ErrReplay means an authenticated event ID was already atomically stored. ErrReplay = errors.New("webhook replay detected") // ErrMissingEventID means replay protection was requested without an ID. ErrMissingEventID = errors.New("webhook event ID is required") // ErrReplayStore means replay state could not be checked and the request was // rejected closed. ErrReplayStore = errors.New("webhook replay store unavailable") // ErrNonceGeneration means a signer could not produce a safe nonce. ErrNonceGeneration = errors.New("webhook nonce generation failed") )
var ErrDeliveryEncoding = errors.New("invalid webhook delivery encoding")
var ErrInvalidEnvelope = errors.New("invalid webhook envelope")
Functions ¶
func Canonicalize ¶
Canonicalize constructs the versioned, unambiguous bytes signed by v1.
func CaptureBody ¶
CaptureBody reads at most maxBytes+1 bytes, closes the original body, and restores an independent reader only after a complete successful capture. A declared oversized body is rejected before the first read.
func MarshalDeliveryRequest ¶
func MarshalDeliveryRequest(delivery DeliveryRequest, maxBytes int) ([]byte, error)
MarshalDeliveryRequest emits deterministic v1 bytes for queue and outbox adapters and rejects output beyond maxBytes.
func NewSecureHTTPClient ¶
NewSecureHTTPClient creates a direct, no-proxy client whose transport re-resolves and revalidates addresses at dial time. Redirects are returned to the caller without being followed.
func SetSignatureHeaders ¶
SetSignatureHeaders replaces all signature headers with strict v1 values.
Types ¶
type DeadLetterFunc ¶
type DeadLetterFunc func(ctx context.Context, result DeliveryResult) error
DeadLetterFunc receives a terminal bounded result for durable handling by the application. The core does not implement storage or a queue.
type Deliverer ¶
type Deliverer struct {
// contains filtered or unexported fields
}
Deliverer signs and sends bounded webhook attempts.
func NewDeliverer ¶
func NewDeliverer(config DeliveryConfig) (*Deliverer, error)
NewDeliverer validates every mandatory safety bound and injectable.
func (*Deliverer) Deliver ¶
func (d *Deliverer) Deliver(ctx context.Context, delivery DeliveryRequest) (DeliveryResult, error)
Deliver performs bounded attempts. Retries are disabled for requests that do not carry an explicit idempotency key.
Example ¶
package main
import (
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"time"
webhook "github.com/faustbrian/go-webhook"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
endpoint, _ := url.Parse(server.URL)
prefix := netip.MustParsePrefix("127.0.0.0/8")
policy, _ := webhook.NewSSRFPolicy(webhook.SSRFPolicyConfig{
Resolver: net.DefaultResolver,
AllowHTTP: true, AllowedPrefixes: []netip.Prefix{prefix}, MaxAddresses: 8,
})
client, _ := webhook.NewSecureHTTPClient(policy, time.Second)
signer, _ := webhook.NewSigner(webhook.SignerConfig{
Algorithm: webhook.SHA256,
Keys: []webhook.SigningKey{{ID: "current", Secret: []byte("example-secret")}},
})
identifier := 0
deliverer, _ := webhook.NewDeliverer(webhook.DeliveryConfig{
Client: client, Signer: signer, EndpointPolicy: policy,
Retry: webhook.RetryPolicy{MaxAttempts: 1},
IDGenerator: func() (string, error) {
identifier++
return fmt.Sprintf("id-%d", identifier), nil
},
MaxRequestBytes: 64, MaxResponseBytes: 64, MaxFanOut: 1,
HeaderLimits: webhook.HeaderLimits{MaxSignatures: 1, MaxBytes: 512},
})
result, _ := deliverer.Deliver(context.Background(), webhook.DeliveryRequest{
Endpoint: endpoint, Body: []byte("hello"), EventID: "event-1",
})
fmt.Println(result.Attempts[0].StatusCode, len(result.Attempts))
}
Output: 204 1
func (*Deliverer) DeliverOnce ¶
func (d *Deliverer) DeliverOnce(ctx context.Context, delivery DeliveryRequest) (DeliveryResult, error)
DeliverOnce performs exactly one HTTP attempt even when the Deliverer retry policy allows more. Queue and outbox consumers use this to avoid nested retry multiplication.
func (*Deliverer) FanOut ¶
func (d *Deliverer) FanOut(ctx context.Context, deliveries []DeliveryRequest, workers int) ([]FanOutResult, error)
FanOut runs deliveries with a bounded worker pool and no durable queue.
func (*Deliverer) Replay ¶
func (d *Deliverer) Replay( ctx context.Context, originalDeliveryID string, delivery DeliveryRequest, ) (DeliveryResult, error)
Replay audits operator intent before starting a new independently identified delivery. It does not mutate or reuse the original attempt record.
type DeliveryAttempt ¶
type DeliveryAttempt struct {
ID string
Number int
StartedAt time.Time
CompletedAt time.Time
StatusCode int
Classification FailureClassification
RetryAfter time.Duration
Diagnostic string
}
DeliveryAttempt records one actual HTTP attempt without payloads, secrets, signatures, endpoint query strings, or sensitive response headers.
type DeliveryConfig ¶
type DeliveryConfig struct {
Client HTTPDoer
Signer *Signer
EndpointPolicy EndpointPolicy
Retry RetryPolicy
Clock func() time.Time
Sleep SleepFunc
IDGenerator func() (string, error)
MaxRequestBytes int64
MaxResponseBytes int64
MaxFanOut int
HeaderLimits HeaderLimits
DeadLetter DeadLetterFunc
ReplayHook ReplayHook
Observer Observer
}
DeliveryConfig configures a bounded deliverer.
type DeliveryRequest ¶
type DeliveryRequest struct {
Endpoint *url.URL
Body []byte
EventID string
IdempotencyKey string
Headers http.Header
Metadata map[string]string
}
DeliveryRequest describes one endpoint delivery.
func UnmarshalDeliveryRequest ¶
func UnmarshalDeliveryRequest(encoded []byte, maxBytes int) (DeliveryRequest, error)
UnmarshalDeliveryRequest bounds input before strict decoding and rejects unknown fields, trailing data, unsafe URLs, and incomplete identities.
type DeliveryResult ¶
type DeliveryResult struct {
ID string
EventID string
Attempts []DeliveryAttempt
ResponseBody []byte
}
DeliveryResult contains bounded delivery evidence.
type EndpointPolicy ¶
EndpointPolicy validates an endpoint immediately before every attempt.
type EndpointPolicyFunc ¶
EndpointPolicyFunc adapts a function into an endpoint policy.
type Envelope ¶
type Envelope struct {
ID string
Type string
Source string
Subject string
Time time.Time
Data json.RawMessage
Metadata map[string]string
}
Envelope is a small protocol-independent event envelope. Its v1 JSON field order and UTC timestamp representation are compatibility-sensitive.
func (Envelope) MarshalJSON ¶
MarshalJSON validates required fields and emits deterministic JSON.
type ErrorHook ¶
ErrorHook receives internal verification failures. Implementations must not log request bodies, signatures, secrets, or unredacted sensitive headers.
type EventIDExtractor ¶
EventIDExtractor extracts a replay identifier after authentication.
func HeaderEventID ¶
func HeaderEventID(name string, maxBytes int) EventIDExtractor
HeaderEventID returns a strict single-header extractor with a byte limit.
type FailureClassification ¶
type FailureClassification string
FailureClassification is a stable delivery outcome category.
const ( FailureNone FailureClassification = "none" FailureRetryable FailureClassification = "retryable" FailureTerminal FailureClassification = "terminal" FailureExhausted FailureClassification = "exhausted" )
type FanOutResult ¶
type FanOutResult struct {
Result DeliveryResult
Err error
}
FanOutResult preserves the input order of a bounded fan-out operation.
type HeaderLimits ¶
HeaderLimits bounds parsing before any base64 decoding or allocation based on decoded values.
type Message ¶
type Message struct {
Timestamp time.Time
Nonce string
Method string
Path string
RawQuery string
Host string
ContentType string
IdempotencyKey string
Body []byte
Metadata map[string]string
}
Message contains the exact request components covered by a signature. Body is never transformed before hashing.
type MiddlewareConfig ¶
type MiddlewareConfig struct {
Request RequestOptions
FailureStatus int
OnError ErrorHook
}
MiddlewareConfig configures inbound verification middleware.
type NetIPResolver ¶
type NetIPResolver interface {
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
}
NetIPResolver is implemented by net.Resolver and controlled test resolvers.
type NonceGenerator ¶
NonceGenerator returns a fresh public nonce for one signing operation. Implementations must be concurrency-safe.
type Observation ¶
type Observation struct {
Operation Operation
Outcome Outcome
Reason Reason
Duration time.Duration
Algorithm Algorithm
StatusCode int
Attempt int
Classification FailureClassification
}
Observation intentionally has no payload, signature, key, event ID, endpoint, header, query, or arbitrary error field.
type Observer ¶
type Observer interface {
Observe(ctx context.Context, observation Observation)
}
Observer receives secret-safe observations.
Example ¶
package main
import (
"context"
"fmt"
webhook "github.com/faustbrian/go-webhook"
)
func main() {
observer := webhook.ObserverFunc(func(_ context.Context, event webhook.Observation) {
fmt.Println(event.Operation, event.Outcome)
})
observer.Observe(context.Background(), webhook.Observation{
Operation: webhook.OperationVerification, Outcome: webhook.OutcomeSuccess,
})
}
Output: verification success
type ObserverFunc ¶
type ObserverFunc func(ctx context.Context, observation Observation)
ObserverFunc adapts a function into an Observer.
func (ObserverFunc) Observe ¶
func (f ObserverFunc) Observe(ctx context.Context, observation Observation)
Observe implements Observer.
type Reason ¶
type Reason string
Reason is a stable failure category that cannot contain caller data.
const ( ReasonNone Reason = "none" ReasonSignature Reason = "signature" ReasonTimestamp Reason = "timestamp" ReasonReplay Reason = "replay" ReasonStore Reason = "store" ReasonLimit Reason = "limit" ReasonCanceled Reason = "canceled" ReasonTransport Reason = "transport" ReasonStatus Reason = "status" ReasonPolicy Reason = "policy" ReasonInternal Reason = "internal" )
type ReplayHook ¶
ReplayHook records an operator-requested replay before a new delivery starts.
type ReplayStore ¶
type ReplayStore interface {
CheckAndRecord(ctx context.Context, key string, expiresAt time.Time) (recorded bool, err error)
}
ReplayStore atomically checks and records replay keys. Implementations MUST return true only when the key was absent and was recorded with expiresAt in the same atomic operation. false, nil means the key already existed.
type RequestOptions ¶
type RequestOptions struct {
MaxBodyBytes int64
HeaderLimits HeaderLimits
Metadata map[string]string
EventID EventIDExtractor
}
RequestOptions controls bounded HTTP signing and verification.
type RetryPolicy ¶
RetryPolicy bounds attempts and exponential backoff.
type SSRFPolicy ¶
type SSRFPolicy struct {
// contains filtered or unexported fields
}
SSRFPolicy validates URL syntax and every resolved address.
func NewSSRFPolicy ¶
func NewSSRFPolicy(config SSRFPolicyConfig) (*SSRFPolicy, error)
NewSSRFPolicy constructs a policy with explicit resolver and DNS bounds.
type SSRFPolicyConfig ¶
type SSRFPolicyConfig struct {
Resolver NetIPResolver
AllowHTTP bool
AllowedPrefixes []netip.Prefix
DeniedPrefixes []netip.Prefix
MaxAddresses int
}
SSRFPolicyConfig configures default-deny endpoint validation. AllowedPrefixes are explicit exceptions for private test or trusted network endpoints.
type Signature ¶
type Signature struct {
Version string
Algorithm Algorithm
KeyID string
Timestamp time.Time
Nonce string
Value string
}
Signature is a transport-neutral signature value.
func ParseSignatureHeaders ¶
func ParseSignatureHeaders(header http.Header, limits HeaderLimits) ([]Signature, error)
ParseSignatureHeaders parses all values strictly. Comma-combined values, duplicate key IDs, noncanonical timestamps, and unknown fields are rejected.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer creates signatures for every active key, newest first.
Example (Rotation) ¶
package main
import (
"fmt"
"net/http"
"time"
webhook "github.com/faustbrian/go-webhook"
)
func main() {
now := time.Unix(1_700_000_000, 0)
signer, _ := webhook.NewSigner(webhook.SignerConfig{
Algorithm: webhook.SHA512,
Keys: []webhook.SigningKey{
{ID: "new", Secret: []byte("new-secret"), NotBefore: now.Add(-time.Hour)},
{ID: "old", Secret: []byte("old-secret"), NotAfter: now.Add(time.Hour)},
},
Clock: func() time.Time { return now },
})
signatures, _ := signer.Sign(webhook.Message{Timestamp: now, Method: http.MethodPost, Path: "/hooks", Host: "receiver.example"})
fmt.Println(len(signatures))
}
Output: 2
func NewSigner ¶
func NewSigner(config SignerConfig) (*Signer, error)
NewSigner validates and copies signer configuration.
func (*Signer) SignRequest ¶
func (s *Signer) SignRequest(request *http.Request, options RequestOptions) ([]Signature, []byte, error)
SignRequest captures and restores the exact body, signs the effective request target, and replaces any preexisting signature headers.
type SignerConfig ¶
type SignerConfig struct {
Algorithm Algorithm
Keys []SigningKey
Clock func() time.Time
NonceGenerator NonceGenerator
}
SignerConfig configures outbound signatures.
type SigningKey ¶
type SigningKey struct {
ID string
Secret []byte
NotBefore time.Time
NotAfter time.Time
Revoked bool
}
SigningKey is a versioned outbound secret. Empty validity bounds are open.
type Verification ¶
Verification describes the key and algorithm that authenticated a message.
func VerificationFromContext ¶
func VerificationFromContext(ctx context.Context) (Verification, bool)
VerificationFromContext returns the authenticated verification result.
type VerificationError ¶
VerificationError separates a stable caller-facing message from internal diagnostics. Diagnostic must only be sent to a secret-safe internal sink.
func (*VerificationError) Error ¶
func (e *VerificationError) Error() string
Error implements error without exposing internal diagnostic details.
func (*VerificationError) SafeMessage ¶
func (e *VerificationError) SafeMessage() string
SafeMessage returns a stable response suitable for an untrusted caller.
func (*VerificationError) Unwrap ¶
func (e *VerificationError) Unwrap() error
Unwrap enables errors.Is checks against the failure category.
type VerificationKey ¶
type VerificationKey struct {
ID string
Secret []byte
NotBefore time.Time
NotAfter time.Time
Revoked bool
}
VerificationKey is a versioned inbound secret. Empty validity bounds are open.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier authenticates signatures with bounded clock skew.
func NewVerifier ¶
func NewVerifier(config VerifierConfig) (*Verifier, error)
NewVerifier validates and copies verifier configuration.
func (*Verifier) Middleware ¶
Middleware authenticates and replay-checks a request before invoking next. Failure responses always contain only the stable safe message.
Example ¶
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"time"
webhook "github.com/faustbrian/go-webhook"
)
func main() {
now := time.Unix(1_700_000_000, 0)
secret := []byte("example-secret-at-least-rotate-in-production")
signer, _ := webhook.NewSigner(webhook.SignerConfig{
Algorithm: webhook.SHA256,
Keys: []webhook.SigningKey{{ID: "current", Secret: secret}},
Clock: func() time.Time { return now },
})
verifier, _ := webhook.NewVerifier(webhook.VerifierConfig{
Algorithm: webhook.SHA256,
Keys: []webhook.VerificationKey{{ID: "current", Secret: secret}},
Clock: func() time.Time { return now }, Tolerance: time.Minute,
})
next := http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
body, _ := webhook.VerifiedBodyFromContext(request.Context())
fmt.Println(string(body))
})
handler, _ := verifier.Middleware(webhook.MiddlewareConfig{Request: webhook.RequestOptions{
MaxBodyBytes: 64, HeaderLimits: webhook.HeaderLimits{MaxSignatures: 1, MaxBytes: 512},
}}, next)
request := httptest.NewRequest(http.MethodPost, "https://receiver.example/hooks", bytes.NewBufferString("hello"))
_, _, _ = signer.SignRequest(request, webhook.RequestOptions{MaxBodyBytes: 64, HeaderLimits: webhook.HeaderLimits{MaxSignatures: 1, MaxBytes: 512}})
handler.ServeHTTP(httptest.NewRecorder(), request)
}
Output: hello
func (*Verifier) Verify ¶
func (v *Verifier) Verify(message Message, signatures []Signature) (Verification, error)
Verify accepts the first valid signature without exposing which checks failed. All signature comparisons use hmac.Equal.
func (*Verifier) VerifyAndRecord ¶
func (v *Verifier) VerifyAndRecord( ctx context.Context, message Message, signatures []Signature, eventID string, ) (Verification, error)
VerifyAndRecord authenticates a message and then uses the replay store's atomic check-and-record operation. It fails closed on every storage error.
func (*Verifier) VerifyRequest ¶
func (v *Verifier) VerifyRequest( ctx context.Context, request *http.Request, options RequestOptions, ) (verification Verification, body []byte, err error)
VerifyRequest bounds and parses headers before capturing the exact body. It authenticates before invoking the event-ID extractor or replay store.
type VerifierConfig ¶
type VerifierConfig struct {
Algorithm Algorithm
Keys []VerificationKey
Clock func() time.Time
Tolerance time.Duration
ReplayStore ReplayStore
ReplayTTL time.Duration
ReplayNamespace string
Observer Observer
}
VerifierConfig configures inbound signature verification.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
adapters
|
|
|
idempotency
Package webhookidempotency adapts idempotency leases to webhook replay checks.
|
Package webhookidempotency adapts idempotency leases to webhook replay checks. |
|
otel
Package webhookotel adapts secret-safe webhook observations to the telemetry runtime.
|
Package webhookotel adapts secret-safe webhook observations to the telemetry runtime. |
|
outbox
Package webhookoutbox adapts webhook deliveries to outbox envelopes and relay publishers.
|
Package webhookoutbox adapts webhook deliveries to outbox envelopes and relay publishers. |
|
queue
Package webhookqueue adapts bounded delivery requests to queue messages.
|
Package webhookqueue adapts bounded delivery requests to queue messages. |
|
slog
Package webhookslog adapts secret-safe webhook observations to log's standard slog surface.
|
Package webhookslog adapts secret-safe webhook observations to log's standard slog surface. |
|
Package webhooktest provides deterministic clocks, nonces, identifiers, signers, and verifiers for consumer tests.
|
Package webhooktest provides deterministic clocks, nonces, identifiers, signers, and verifiers for consumer tests. |