Paired SDK work for the AdCP RFC 9421 request-signing profile in adcp#2323 (3.0 optional, 4.0 required — tracked in adcp#2307).
Scope
Implement client signing and server verification for outgoing/incoming AdCP requests per the Signed Requests (Transport Layer) profile. Conforms against the test vectors shipped in the spec repo at static/test-vectors/request-signing/.
Deliverables
Client-side (signer)
adcp.Client accepts optional SigningKey (ed25519.PrivateKey or ECDSA *PrivateKey) + AgentURL string (identifies the signer's agents[] entry).
- On every outgoing request to operations in the seller's advertised
request_signing.required_for or supported_for, compute the canonical signature base per RFC 9421 §2.5 with the covered components mandated by the AdCP profile:
- Always:
@method, @target-uri, @authority
- With body:
content-type
- With body AND seller advertises
covers_content_digest: "required": content-digest (RFC 9530, sha-256)
- If seller advertises
covers_content_digest: "forbidden", signer MUST NOT cover content-digest.
- Attach
Signature, Signature-Input, and (when covered) Content-Digest headers.
- Sign with Ed25519 (default via
crypto/ed25519) or ES256 (via crypto/ecdsa with IEEE P1363 encoding — NOT DER).
- Honor the profile's sig-params:
created, expires (≤ created + 300), nonce (≥ 128 bits, base64url), keyid (matches a JWK kid in the signer's published JWKS), alg (ed25519 or ecdsa-p256-sha256), tag (exactly adcp/request-signing/v1).
- CLI helper:
adcp-go generate-signing-key [--alg ed25519|es256] → writes PEM + prints JWK with adcp_use: "request-signing" for publication at jwks_uri.
Server-side (verifier)
adcp/signing package: middleware for net/http, github.com/gorilla/mux, github.com/go-chi/chi.
- Signature:
func VerifyRequestSignature(r *http.Request, opts VerifyOptions) (*VerifiedSigner, error).
- Implement the 13-step verifier checklist + pre-check from the spec:
0. Pre-check: reject unsigned request to operation in required_for with request_signature_required. Reject if Signature present without Signature-Input (or vice versa) — the downgrade loophole vector 019 catches this.
- Parse
Signature-Input / Signature
- Params presence (all 6 required)
- Tag check (
adcp/request-signing/v1)
- Alg allowlist
- Window (including
expires > created, ±60s skew, ≤ 300s max)
- Covered components (considering
covers_content_digest policy: required / forbidden / either)
- Resolve
keyid → JWKS → agent entry (with SSRF validation per AdCP webhook SSRF rules and 30-second JWKS refetch cooldown BETWEEN refetches)
- Check
use: "sig", key_ops contains verify, adcp_use == "request-signing" (strip adcp_use before feeding JWK to crypto/ecdsa or similar strict consumers — retain for policy check)
- Revocation check (BEFORE crypto verify to avoid cheap amplification)
- Cryptographic verification of the signature base
- Recompute and compare
content-digest when covered
- Replay dedup on
(keyid, nonce)
- Insert into replay cache only after all prior steps pass
- Pluggable JWKS cache + SSRF-validated fetcher (
http.Client with DialContext hooks).
- Pluggable replay store (in-memory
sync.Map + expiry goroutine default; Redis adapter for multi-instance deployments).
- Return
*VerifiedSigner{KeyID, AgentURL, VerifiedAt} for downstream authorization.
- Emit typed errors matching the spec error taxonomy:
request_signature_required
request_signature_header_malformed
request_signature_params_incomplete
request_signature_tag_invalid
request_signature_alg_not_allowed
request_signature_window_invalid
request_signature_components_incomplete
request_signature_components_unexpected
request_signature_key_unknown
request_signature_key_purpose_invalid
request_signature_invalid
request_signature_digest_mismatch
request_signature_replayed
request_signature_key_revoked
request_signature_revocation_stale
request_signature_jwks_unavailable
request_signature_jwks_untrusted
- Return 401 with
WWW-Authenticate: Signature error="<code>" on rejection. Realm MUST be omitted per spec.
- Respect the "one Signature-Input label per request" rule — verify
sig1 and ignore additional labels.
Conformance tests
- Consume the test vectors shipped in the spec repo under
static/test-vectors/request-signing/:
- 19 negative vectors — the verifier MUST return
expected_outcome.error_code exactly. failed_step is informational.
- 3 positive vectors — the verifier MUST accept. Ed25519 signatures are byte-exact reproducible (determinism); ES256 is verify-only (random-k is conformant for verify).
- Run order per the README's "Recommended run order" section: positives first, then parse-level negatives, then semantic, then key-path, then crypto/stateful.
- Add end-to-end test: spin up an HTTP server with
VerifyRequestSignature, point a client at it with SigningKey, drive a create_media_buy. Exercise covers_content_digest: "either" and "required".
Docs
- README section on "Signing in 3.0" — how to opt in as a buyer or a seller.
- Go examples in the AdCP quickstart showing the 5-line integration.
Library recommendations
- RFC 9421 canonicalization + verification:
github.com/remitly-oss/httpsig-go is currently the most active Go implementation. Validate its output against the expected_signature_base in each positive vector before relying on it — Go RFC 9421 libraries are patchier than TS/Python and may need config to match the AdCP profile exactly (specific covered components, IEEE P1363 ES256 encoding).
- JWK / JWKS:
github.com/go-jose/go-jose/v4 — works with stdlib crypto/ed25519 and crypto/ecdsa keys; preserves unknown JWK members like adcp_use.
- Content-Digest (RFC 9530): may need a small custom impl; the header format is simple structured fields (
sha-256=:base64:).
- SSRF-validated fetch: use
http.Client with Transport.DialContext to pin the connection to a pre-validated IP per the Webhook URL validation rules.
Ordering
- Implement the verifier first — it consumes the conformance vectors directly and validates the spec.
- Implement the signer.
- End-to-end tests + docs.
Why this belongs in adcp-go
The spec is language-agnostic; the SDK is what Go implementers actually integrate. Shipping signing support in the SDK is what turns "the profile is optional in 3.0" from a theoretical statement into something Go-based counterparties can pilot. Without this, 4.0 required-signing lands on unimplemented libraries.
References
Paired SDK work for the AdCP RFC 9421 request-signing profile in adcp#2323 (3.0 optional, 4.0 required — tracked in adcp#2307).
Scope
Implement client signing and server verification for outgoing/incoming AdCP requests per the Signed Requests (Transport Layer) profile. Conforms against the test vectors shipped in the spec repo at
static/test-vectors/request-signing/.Deliverables
Client-side (signer)
adcp.Clientaccepts optionalSigningKey(ed25519.PrivateKey or ECDSA *PrivateKey) +AgentURLstring (identifies the signer'sagents[]entry).request_signing.required_fororsupported_for, compute the canonical signature base per RFC 9421 §2.5 with the covered components mandated by the AdCP profile:@method,@target-uri,@authoritycontent-typecovers_content_digest: "required":content-digest(RFC 9530, sha-256)covers_content_digest: "forbidden", signer MUST NOT covercontent-digest.Signature,Signature-Input, and (when covered)Content-Digestheaders.crypto/ed25519) or ES256 (viacrypto/ecdsawith IEEE P1363 encoding — NOT DER).created,expires(≤created + 300),nonce(≥ 128 bits, base64url),keyid(matches a JWKkidin the signer's published JWKS),alg(ed25519orecdsa-p256-sha256),tag(exactlyadcp/request-signing/v1).adcp-go generate-signing-key [--alg ed25519|es256]→ writes PEM + prints JWK withadcp_use: "request-signing"for publication atjwks_uri.Server-side (verifier)
adcp/signingpackage: middleware fornet/http,github.com/gorilla/mux,github.com/go-chi/chi.func VerifyRequestSignature(r *http.Request, opts VerifyOptions) (*VerifiedSigner, error).0. Pre-check: reject unsigned request to operation in
required_forwithrequest_signature_required. Reject ifSignaturepresent withoutSignature-Input(or vice versa) — the downgrade loophole vector 019 catches this.Signature-Input/Signatureadcp/request-signing/v1)expires > created, ±60s skew, ≤ 300s max)covers_content_digestpolicy:required/forbidden/either)keyid→ JWKS → agent entry (with SSRF validation per AdCP webhook SSRF rules and 30-second JWKS refetch cooldown BETWEEN refetches)use: "sig",key_opscontainsverify,adcp_use == "request-signing"(stripadcp_usebefore feeding JWK tocrypto/ecdsaor similar strict consumers — retain for policy check)content-digestwhen covered(keyid, nonce)http.ClientwithDialContexthooks).sync.Map+ expiry goroutine default; Redis adapter for multi-instance deployments).*VerifiedSigner{KeyID, AgentURL, VerifiedAt}for downstream authorization.request_signature_requiredrequest_signature_header_malformedrequest_signature_params_incompleterequest_signature_tag_invalidrequest_signature_alg_not_allowedrequest_signature_window_invalidrequest_signature_components_incompleterequest_signature_components_unexpectedrequest_signature_key_unknownrequest_signature_key_purpose_invalidrequest_signature_invalidrequest_signature_digest_mismatchrequest_signature_replayedrequest_signature_key_revokedrequest_signature_revocation_stalerequest_signature_jwks_unavailablerequest_signature_jwks_untrustedWWW-Authenticate: Signature error="<code>"on rejection. Realm MUST be omitted per spec.sig1and ignore additional labels.Conformance tests
static/test-vectors/request-signing/:expected_outcome.error_codeexactly.failed_stepis informational.VerifyRequestSignature, point a client at it withSigningKey, drive acreate_media_buy. Exercisecovers_content_digest: "either"and"required".Docs
Library recommendations
github.com/remitly-oss/httpsig-gois currently the most active Go implementation. Validate its output against theexpected_signature_basein each positive vector before relying on it — Go RFC 9421 libraries are patchier than TS/Python and may need config to match the AdCP profile exactly (specific covered components, IEEE P1363 ES256 encoding).github.laiyagushi.com/go-jose/go-jose/v4— works with stdlibcrypto/ed25519andcrypto/ecdsakeys; preserves unknown JWK members likeadcp_use.sha-256=:base64:).http.ClientwithTransport.DialContextto pin the connection to a pre-validated IP per the Webhook URL validation rules.Ordering
Why this belongs in adcp-go
The spec is language-agnostic; the SDK is what Go implementers actually integrate. Shipping signing support in the SDK is what turns "the profile is optional in 3.0" from a theoretical statement into something Go-based counterparties can pilot. Without this, 4.0 required-signing lands on unimplemented libraries.
References
warn_forcapability (spec), others at adcp#2324, #2325, #2327, #2328