requestsigning

package
v12.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package requestsigning proves that an HTTP request body was produced by someone holding a shared key, and that it was produced recently.

It is one implementation of the timestamped-HMAC scheme, for every place the platform needs one: outbound webhooks, inbound webhooks from a third party, and first-party service-to-service calls. Those three used to be three reimplementations, and the failure mode of a reimplemented signing scheme is that it looks fine until somebody times a comparison.

X-Platform-Signature: v1,t=1753900000,s=<hex(HMAC-SHA256(key, "v1." + t + "." + body))>
X-Platform-Timestamp: 1753900000

The scheme

Both the version and the timestamp are inside the signed material, and each is load-bearing.

The timestamp is what makes a captured request expire. A signature over the body alone is valid forever, so anyone who observes one request can replay it indefinitely. Verify rejects anything outside DefaultTolerance, and does so before computing any HMAC, so a replay flood costs the receiver nothing.

The v1 prefix is what makes the construction replaceable. Binding the scheme into the signed bytes means a v1 signature can only ever verify as v1, so a later scheme can be introduced alongside it rather than by flag-day.

Rotation is the other half. Keyring carries Current and Previous, and a request is signed under both while Previous is set — several s= components in one header. Either side rolls its key by accepting both for as long as it needs, and the operator clears Previous afterwards. A single shared secret cannot be rolled at all without breaking every counterparty simultaneously; in practice that means it never gets rolled.

The three seams

Sign and Verify are the functions, for code that already holds the bytes — webhooks signs its deliveries through Sign.

Signer and Verifier are the interfaces, for code that should not have to. Both resolve their keyring per operation through a KeySource, which is what turns a rotation into a change in the secret store rather than a deploy.

Both take an *http.Request, and that is load-bearing rather than convenient. The signer reads its bytes out of the same request its caller is about to send, so signing one payload and transmitting another is not a mistake the shape can make; the verifier locates its own proof, so which header carries it stays the scheme's business instead of something every wiring site restates. Both read the body through RequestBody, which prefers GetBody — the callers in this module set it, so the read rewinds rather than consumes and the handler downstream still gets every byte.

What the interfaces do not decide is how much of a body is worth reading. That bound is a serving concern and it lives in requestsigning/http, which caps the read before the verifier ever sees the request.

keys, err := requestsigning.NewSecretKeySource(secretSource, "SIGNING_KEY", "SIGNING_KEY_PREVIOUS")
if err != nil {
	return err
}

signer, err := requestsigning.NewSigner(keys)
if err != nil {
	return err
}

client, err := httpclient.NewHTTPClient(
	httpclient.WithRequestSigning(signer),
	httpclient.WithRetryPolicy(policy),
)

The signing transport sits *under* the retry loop, so every attempt is signed afresh. A retry that fires after thirty seconds of backoff carrying the original attempt's timestamp arrives stale, and the receiver is right to reject it — which is a failure that only shows up under load, in the requests that were already having a bad time.

The inbound half is a routing.Middleware in requestsigning/http, over the same Verifier.

Keys

Read them through secrets, not config. NewSecretKeySource resolves both names on every operation, which is affordable because secrets.NewCachingSource answers from memory and consults the backend once per TTL; pair it with secrets.WithRefresh so a rotation is noticed on a timer rather than on the next request.

The secret's value is used as key material verbatim. A store holding it base64- or hex-encoded should be wrapped in a KeySourceFunc that decodes it, so what the store holds and what the HMAC consumes cannot drift.

Other schemes

v1 is what this package mints, and it is not the only thing it can check. Verifier is an interface so that an inbound scheme somebody else designed is an implementation of it rather than a second verification stack — the receiving service runs one middleware over one seam either way. An implementation owes two things: a Scheme name for its spans and log lines, and a VerifyRequest that finds its own proof on the request and checks it against the body, read through RequestBody so that what it verifies and what the handler sees are the same bytes.

Code that holds bytes rather than a request — a queue consumer reading a message payload, a test — wants the Verify function instead. The interface is deliberately HTTP-shaped; the function is not.

There is no registry of schemes by name, and that is deliberate. Which scheme guards an endpoint is something the wiring already knows — it is choosing the route and the key source in the same breath — so a name-to-constructor map would buy nothing except package-level mutable state and an ordering dependency between init functions. A service that genuinely needs to pick from a config string does what the rest of this module does: a config subpackage with a switch over the providers it imports, returning errors.ErrUnknownProvider for a name it does not carry.

Whatever the selection mechanism, it belongs to startup. Reading the scheme off an incoming request would let the caller choose which verifier judges it, and a caller who can choose picks the weakest one on offer.

What the failures mean

ErrInvalidSignature is deliberately undifferentiated — missing header, malformed header, unknown scheme, wrong key, tampered body are one error, because telling a caller which one applied tells an attacker how close a forgery came. ErrStaleSignature is separate, because clock skew is the one benign cause and an operator can act on it. Both map to 401 through errors/http, and to codes.Unauthenticated through errors/grpc.

ErrNoVerificationKey is neither: a verifier holding no keys rejects everything, which looks identical to a fleet of callers that all got their signing wrong. It is unmapped, so it surfaces as a 500 — the server's fault, reported as the server's fault.

Index

Examples

Constants

View Source
const (
	// SignatureHeader carries the signature(s) over the request body.
	SignatureHeader = "X-Platform-Signature"

	// TimestampHeader carries the signing timestamp, as Unix seconds. It is the
	// same value that appears inside the signature; it is exposed separately so
	// a receiver can reject a stale request before doing any HMAC work.
	TimestampHeader = "X-Platform-Timestamp"

	// SchemeV1 is the only scheme this package mints. It is the literal prefix
	// bound into the signed bytes, so changing it is a wire break, not a rename.
	SchemeV1 = "v1"

	// DefaultTolerance is how far a signature's timestamp may sit from the
	// verifier's clock before verification rejects it.
	//
	// Five minutes is the customary figure, and it is a compromise between two
	// real failures: too tight and ordinary clock skew between sender and
	// receiver rejects good requests, too loose and a captured request stays
	// replayable for as long as the window lasts.
	DefaultTolerance = 5 * time.Minute
)

Variables

View Source
var (
	// ErrInvalidSignature indicates a signature header that is missing,
	// malformed, carries no recognized scheme, or does not match the body under
	// any key the verifier holds. The cases are deliberately one error: telling
	// a caller which of them applied tells an attacker how close a forgery came.
	ErrInvalidSignature = platformerrors.New("invalid request signature")

	// ErrStaleSignature indicates a signature whose timestamp is outside the
	// tolerance. It is distinct from ErrInvalidSignature because it is the one
	// verification failure with a benign cause an operator can act on — clock
	// skew — and it says nothing about the key.
	ErrStaleSignature = platformerrors.New("request signature timestamp outside tolerance")

	// ErrNoSigningKey indicates a keyring with no current key. Unsigned
	// requests are not something this package will mint: a receiver that cannot
	// authenticate a payload cannot safely act on it.
	ErrNoSigningKey = platformerrors.New("no current signing key")

	// ErrNoVerificationKey indicates a verification attempted against a keyring
	// holding no keys at all.
	//
	// It is deliberately not ErrInvalidSignature. A verifier with no keys
	// rejects everything, which looks from the outside exactly like a fleet of
	// callers that all got their signing wrong; naming it separately is what
	// lets the server report its own misconfiguration as a fault of its own
	// rather than as a verdict about the caller.
	ErrNoVerificationKey = platformerrors.New("no verification key")

	// ErrNilKeySource indicates a constructor called without a KeySource. It
	// wraps errors.ErrNilInputParameter, so a caller may check either.
	ErrNilKeySource = platformerrors.Wrap(platformerrors.ErrNilInputParameter, "nil signing key source")
)

Functions

func RequestBody

func RequestBody(req *http.Request) ([]byte, error)

RequestBody reads a request's body without consuming it, when it can.

GetBody is preferred and Body is the fallback, which is net/http's own way of saying whether a body is replayable. A signer or verifier calling this on a request that carries GetBody — which is what both callers in this module hand it — leaves that request as readable as it found it. On one that does not, the body is consumed, and whoever built it that way owns the consequence.

It is exported because a scheme this package did not write needs to read the body exactly the way the built-in one does. A verifier that read it some other way would be checking a signature over bytes the handler never sees.

func Sign

func Sign(keyring Keyring, body []byte, at time.Time) (string, error)

Sign renders the SignatureHeader value for body at the given time, under every active key in keyring.

The result looks like:

v1,t=1753900000,s=<hex>,s=<hex>

A second s= appears only during a rotation window, when keyring.Previous is set. Emitting both is what lets a receiver roll its key without coordinating an instant of downtime with whoever operates the sender: it accepts either signature while it switches, and the operator drops Previous once every receiver has.

Verify accepts a header with any number of s= components, so widening this to a longer key list later is not a wire change.

Example

Rotation is why Keyring is a pair. A request is signed under both keys while Previous is set, so either side can switch without coordinating an instant of downtime with the other.

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/primandproper/platform-go/v12/cryptography/requestsigning"
)

func main() {
	rotating := requestsigning.Keyring{
		Current:  []byte("the new key"),
		Previous: []byte("the outgoing key"),
	}

	payload := []byte(`{"id":"order-7"}`)
	signedAt := time.Unix(1753900000, 0)

	signature, err := requestsigning.Sign(rotating, payload, signedAt)
	if err != nil {
		panic(err)
	}

	// Two s= components: a receiver that has moved to the new key and one that
	// has not both find a signature they can verify.
	fmt.Println(strings.Count(signature, ",s="))

	fmt.Println(requestsigning.Verify(
		requestsigning.Keyring{Current: []byte("the new key")},
		payload, signature, requestsigning.WithVerificationTime(signedAt),
	))
	fmt.Println(requestsigning.Verify(
		requestsigning.Keyring{Current: []byte("the outgoing key")},
		payload, signature, requestsigning.WithVerificationTime(signedAt),
	))

}
Output:
2
<nil>
<nil>

func Verify

func Verify(keyring Keyring, body []byte, signature string, opts ...Option) error

Verify checks a SignatureHeader value against body under keyring, and is what a receiver calls on receipt.

It ships with the signer on purpose. Verification is where these schemes are actually got wrong: receivers compare with ==, forget the timestamp check, or verify a re-serialized body rather than the received bytes. Handing out the sender and leaving the receiver to reimplement it from prose is how that keeps happening.

body must be the exact bytes received, read before any decoding. Decoding and re-encoding changes key order and whitespace, and the signature covers bytes, not meaning.

A signature verifies if it matches under any key in keyring, so a receiver holding both an old and a new key accepts requests from either side of a rotation.

Example

The receiving end. Verification is where these schemes are actually got wrong, so it ships with the sender rather than being described in prose for each receiver to reimplement.

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"time"

	"github.com/primandproper/platform-go/v12/cryptography/requestsigning"
)

func main() {
	keyring := requestsigning.Keyring{Current: []byte("the shared signing key")}

	receiver := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
		// The exact bytes received, read before any decoding. Decoding and
		// re-encoding changes key order and whitespace, and the signature
		// covers bytes rather than meaning.
		body, err := io.ReadAll(req.Body)
		if err != nil {
			res.WriteHeader(http.StatusBadRequest)

			return
		}

		signature := req.Header.Get(requestsigning.SignatureHeader)
		if err = requestsigning.Verify(keyring, body, signature); err != nil {
			res.WriteHeader(http.StatusUnauthorized)

			return
		}

		res.WriteHeader(http.StatusNoContent)
	}))
	defer receiver.Close()

	payload := []byte(`{"id":"order-7"}`)

	signature, err := requestsigning.Sign(keyring, payload, time.Now())
	if err != nil {
		panic(err)
	}

	req, err := http.NewRequestWithContext(context.Background(),
		http.MethodPost, receiver.URL, strings.NewReader(string(payload)))
	if err != nil {
		panic(err)
	}

	req.Header.Set(requestsigning.SignatureHeader, signature)

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer func() { _ = res.Body.Close() }()

	fmt.Println(res.StatusCode)

	// A tampered body no longer verifies.
	fmt.Println(requestsigning.Verify(keyring, []byte(`{"id":"order-8"}`), signature))

}
Output:
204
invalid request signature

Types

type Freshness

type Freshness struct {
	// Clock is the source of time. Absent means the wall clock.
	Clock clock.Clock

	// At pins the instant verification compares against, winning over Clock. It
	// exists for tests and for replaying a captured request against a known
	// instant. The zero time means "not pinned" rather than the Unix epoch, so
	// an unset field cannot silently reject everything.
	At time.Time

	// Tolerance is how far a signed timestamp may sit from Now, in either
	// direction.
	//
	// There is deliberately no value meaning "do not check". A signature with
	// no freshness bound is replayable forever, which is the property a signed
	// timestamp exists to remove; a caller that wants a long window names a long
	// duration and can be seen to have done so.
	Tolerance time.Duration
}

Freshness is how a verifier decides whether a signed timestamp is recent enough: what "now" is, and how far from it a timestamp may sit.

It is exported because it is not this scheme's. Every signature scheme that binds a timestamp into the signed material — this package's v1, Stripe's t=…,v1=…, the next vendor's — needs the same three-source resolution of "now" and the same symmetric window around it, and the copies of that in this module had already begun to differ in whether a skew was rounded and which direction the comparison read. There is one of each here so a verifier for somebody else's scheme gets the resolution and the sentinel rather than a paragraph of prose telling it to reimplement them.

The zero value resolves to the wall clock with no tolerance at all, which rejects everything; a constructor building one is expected to start from DefaultTolerance.

func (Freshness) Check

func (f Freshness) Check(signedAt time.Time) error

Check reports whether signedAt sits within Tolerance of Now, returning a wrapped ErrStaleSignature carrying the drift when it does not.

The window is symmetric. A timestamp from the future is as suspect as one from the past — it is either clock skew, which is the benign case this error exists to name, or a sender minting signatures that stay valid longer than the window allows.

Callers run this before computing any MAC. A stale request is then rejected without spending work proportional to its body, which is what keeps a replay flood from costing a receiver anything. The timestamp is unauthenticated at that point, and that is fine: forging it either moves the request out of the window or leaves it signed under a payload whose MAC will not match, so nothing is decided on an unverified value.

func (Freshness) Now

func (f Freshness) Now() time.Time

Now resolves the instant a verification compares a signed timestamp against: the pinned one if a caller named it, the injected clock's otherwise, and the wall clock when neither was supplied.

type KeySource

type KeySource interface {
	// Keyring returns the keys in force right now.
	Keyring(ctx context.Context) (Keyring, error)
}

KeySource resolves the keyring a signature is minted or checked under, at the moment it is needed.

It is resolved per operation rather than captured at construction, and that is the whole point of the indirection: a key read once at boot and held for the life of the process is a key that cannot be rotated without a restart, which is why signing material tends to sit in config files and never change.

func StaticKeyring

func StaticKeyring(keyring Keyring) KeySource

StaticKeyring returns a KeySource that always answers with keyring.

It is for tests and for a keyring a caller already holds in memory. It cannot rotate: nothing re-reads it, so a process using it keeps signing under the same key until it restarts. Reach for NewSecretKeySource in anything long-lived.

type KeySourceFunc

type KeySourceFunc func(ctx context.Context) (Keyring, error)

KeySourceFunc adapts a function to KeySource. It is the seam for a keyring assembled some way this package does not ship — a per-tenant lookup, a base64-encoded secret, a keyring derived from a database row.

func (KeySourceFunc) Keyring

func (f KeySourceFunc) Keyring(ctx context.Context) (Keyring, error)

Keyring calls f.

type Keyring

type Keyring struct {
	// Current is the key new signatures are minted under. Required to sign.
	Current []byte `json:"-"`
	// Previous is an outgoing key still emitted alongside Current during a
	// rotation window. Empty outside one.
	Previous []byte `json:"-"`
}

Keyring carries the HMAC keys a signature is minted and checked under.

It is a pair rather than a single value so that rotation is not an outage. A request is signed under Current and, while Previous is set, again under Previous; both signatures travel in the same header. A receiver therefore accepts requests throughout the window in which either side is switching keys, and the operator clears Previous once everyone has moved.

A single shared secret makes that impossible: rolling it breaks every counterparty at the same instant, so in practice it never gets rolled.

func (Keyring) Keys

func (k Keyring) Keys() [][]byte

Keys reports the keyring's non-empty keys, in the order a signature emits them.

type Option

type Option func(*config)

Option configures signing and verification. One type serves Sign's counterpart Verify, NewSigner, and NewVerifier, because the three share a notion of what time it is; each option's doc says which of them read it.

func WithClock

func WithClock(c clock.Clock) Option

WithClock swaps the source of time a Signer stamps with and a Verifier compares against. Read by Verify, NewSigner, and NewVerifier.

Inside a testing/synctest bubble clock.NewClock already reads the bubble's fake time, so this is for the cases a bubble cannot express — a deliberately skewed peer, a clock driven by a test harness. A nil clock is ignored.

func WithTolerance

func WithTolerance(d time.Duration) Option

WithTolerance overrides DefaultTolerance — how far the signature's timestamp may sit from the verifier's clock. A non-positive duration leaves the default in place. Read by Verify and NewVerifier; a Signer has no use for it.

There is deliberately no way to disable the check. A signature with no freshness bound is replayable forever, which is the property this scheme exists to remove.

func WithVerificationTime

func WithVerificationTime(t time.Time) Option

WithVerificationTime pins the instant a verification compares the signature's timestamp against, instead of reading a clock. It exists for tests and for replaying a captured request against a known instant, and it wins over WithClock. Read by Verify and NewVerifier.

A zero time is ignored, so this cannot accidentally pin verification to the Unix epoch and reject everything.

type SecretKeySource

type SecretKeySource struct {
	// contains filtered or unexported fields
}

SecretKeySource reads a keyring out of a secrets.SecretSource by name. It is exported, and returned by NewSecretKeySource, so a caller can depend on the source it built rather than on the KeySource seam.

func NewSecretKeySource

func NewSecretKeySource(source secrets.SecretSource, currentName, previousName string) (*SecretKeySource, error)

NewSecretKeySource reads the keyring from source on every operation, so signing material lives where secrets live rather than in a config file.

currentName is required. previousName names the outgoing key of a rotation window and may be empty, which is the steady state; a previousName that resolves to secrets.ErrSecretNotFound, or to an empty value, is treated the same way — as a window that is not open. A missing *current* key is an error, because a signer with no key is a signer that cannot sign and a verifier with no key would accept nothing.

Rotation

Reading per operation is what makes rotation a secret-store change rather than a deploy, and it is only affordable because secrets.NewCachingSource exists: wrap the provider in one and these reads are answered from memory, with the backend consulted once per TTL. Pair it with secrets.WithRefresh so a rotation is noticed on a timer instead of on the next request, and with OnChange if something else in the process must be told.

The values are used as key material verbatim, with no decoding. A secret stored base64- or hex-encoded should be wrapped in a KeySourceFunc that decodes it, so that what the store holds and what the HMAC consumes cannot drift apart silently.

func (*SecretKeySource) Keyring

func (s *SecretKeySource) Keyring(ctx context.Context) (Keyring, error)

Keyring resolves both names.

type Signer

type Signer interface {
	// Scheme names the wire format this signer mints. It is a label, for
	// spans and log lines; nothing dispatches on it.
	Scheme() string

	// SignRequest reads req's body through RequestBody and writes the proof
	// into req.Header. It writes headers rather than returning one value
	// because a scheme may carry more than one — v1 sets a timestamp beside
	// its signature, so a receiver can shed a stale request before hashing
	// anything.
	//
	// req must be a request the caller is willing to have read, and should
	// carry a GetBody so the read is repeatable — see RequestBody. It is
	// called once per attempt rather than once per logical request, so the
	// timestamp it stamps is always fresh: a retry that fires after a long
	// backoff must not arrive already stale.
	SignRequest(ctx context.Context, req *http.Request) error
}

Signer stamps a request with whatever proves its body was produced by someone holding the key.

It takes the request rather than a header bag and a []byte, and that is what keeps it honest: the bytes it signs are read from the same request its caller is about to send, so the two cannot be different bytes. A seam that accepted the body separately would let a caller sign one payload and transmit another, and nothing in the type would notice.

type V1Signer

type V1Signer struct {
	// contains filtered or unexported fields
}

V1Signer mints v1 signatures over a keyring it re-reads per request. It is exported, and returned by NewSigner, so a caller can depend on the scheme it built rather than on the Signer seam.

func NewSigner

func NewSigner(keys KeySource, opts ...Option) (*V1Signer, error)

NewSigner builds the v1 Signer: it stamps SignatureHeader and TimestampHeader over the request body, under every key the source holds.

The keyring is resolved per call rather than captured, so a rotation in the secret store reaches the wire without a restart — see NewSecretKeySource for what makes that affordable.

Reads WithClock; WithTolerance and WithVerificationTime belong to the verifying side and are ignored here.

Example

A Signer and a Verifier built from one key source are the two halves a service wires: the client stamps, the server checks, and neither holds key material of its own.

package main

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/primandproper/platform-go/v12/cryptography/requestsigning"
)

func main() {
	keys := requestsigning.StaticKeyring(requestsigning.Keyring{Current: []byte("the shared key")})

	// In a real service these come from secrets, via NewSecretKeySource, so a
	// rotation is a change in the store rather than a deploy.
	signer, err := requestsigning.NewSigner(keys)
	if err != nil {
		panic(err)
	}

	verifier, err := requestsigning.NewVerifier(keys)
	if err != nil {
		panic(err)
	}

	req, err := http.NewRequestWithContext(context.Background(),
		http.MethodPost, "https://internal.example.com/charge", strings.NewReader(`{"amount":4200}`))
	if err != nil {
		panic(err)
	}

	// Neither side is told which header carries the proof, and neither is handed
	// the body apart from the request that holds it. The signer reads through
	// GetBody, so the request is as sendable afterwards as it was before.
	if err = signer.SignRequest(context.Background(), req); err != nil {
		panic(err)
	}

	fmt.Println(verifier.VerifyRequest(context.Background(), req))

	// An unsigned request is refused the same way a badly signed one is.
	req.Header.Del(requestsigning.SignatureHeader)
	fmt.Println(verifier.VerifyRequest(context.Background(), req))

}
Output:
<nil>
no X-Platform-Signature header: invalid request signature

func (*V1Signer) Scheme

func (s *V1Signer) Scheme() string

Scheme returns SchemeV1.

func (*V1Signer) SignRequest

func (s *V1Signer) SignRequest(ctx context.Context, req *http.Request) error

SignRequest stamps the signature and timestamp headers over req's body.

The timestamp header carries the same value that is inside the signature. It is set separately so a receiver can reject a stale request before spending an HMAC on it; a receiver must still treat the signature as authoritative, since only the copy inside the signed material is covered by the MAC.

type V1Verifier

type V1Verifier struct {
	// contains filtered or unexported fields
}

V1Verifier checks v1 signatures against a keyring it re-reads per request. It is exported, and returned by NewVerifier, so a caller can depend on the scheme it built rather than on the Verifier seam.

func NewVerifier

func NewVerifier(keys KeySource, opts ...Option) (*V1Verifier, error)

NewVerifier builds the v1 Verifier: it checks the SignatureHeader value against the body under every key the source holds.

The keyring is resolved per call, so a receiver picks up the far side's key rotation without a restart — and, more usefully, can carry both keys through a window of its own.

Reads WithClock, WithTolerance, and WithVerificationTime.

func (*V1Verifier) Scheme

func (v *V1Verifier) Scheme() string

Scheme returns SchemeV1.

func (*V1Verifier) VerifyRequest

func (v *V1Verifier) VerifyRequest(ctx context.Context, req *http.Request) error

VerifyRequest checks req's SignatureHeader against its body.

TimestampHeader is not consulted. It is a courtesy copy for a receiver that wants to shed a stale request before reading a body at all; the value this checks is the one inside the signed material, which is the only one an attacker cannot edit.

type Verifier

type Verifier interface {
	// Scheme names the wire format this verifier reads. It is a label, for
	// spans and log lines; nothing dispatches on it.
	Scheme() string

	// VerifyRequest checks req and returns nil only if its body was signed
	// under a key this verifier holds. A request carrying no proof at all is
	// ErrInvalidSignature: unsigned and badly signed are both "did not prove
	// it holds the key".
	//
	// The body is read through RequestBody, so it must be the exact bytes
	// received and the read must not have been bounded somewhere the
	// signature was not. requestsigning/http's middleware caps it before
	// handing the request over, which is where that bound belongs.
	VerifyRequest(ctx context.Context, req *http.Request) error
}

Verifier checks that a request was signed by a holder of a key it trusts, and is the inbound half of Signer.

NewVerifier is only its v1 implementation. A scheme this package did not design — a proof in another header, in another format — satisfies these same two methods, so a service checking somebody else's signature runs the same middleware over the same seam rather than a second verification stack beside it. Locating the proof is the scheme's own business, which is why this takes the whole request and not a header value somebody else picked out of it.

Code that holds bytes rather than a request — a queue consumer reading a message payload, a test — wants the Verify function instead. That is the transport-agnostic seam; this one is deliberately HTTP-shaped.

Directories

Path Synopsis
Package http adapts requestsigning to inbound HTTP.
Package http adapts requestsigning to inbound HTTP.

Jump to

Keyboard shortcuts

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