How UCP Message Signatures Verify Webhooks: Complete Technical Guide
UCP message signatures secure webhook notifications by mandating HTTP Message Signatures with ECDSA over specific request components, requiring platforms to validate Content-Digest, resolve public keys from UCP profiles, and verify business ownership before accepting payloads.
The Universal Commerce Protocol (UCP) standardizes server-initiated webhook security through UCP message signatures implemented via the HTTP Message Signatures specification. As defined in the source code, every webhook payload must carry a valid cryptographic signature that platforms verify through a rigorous sequence of header validation and key resolution steps.
Required Headers for UCP Message Signatures
The webhook sender must include four specific headers defined in docs/specification/order.md (lines 46-49):
UCP-Agent: An RFC 8941 dictionary containing the business profile URL (/.well-known/ucp)Signature-Input: Lists signed components including@method,@authority,@path,content-digest, andcontent-typeSignature: The ECDSA signature value encoded in rawr||sformatContent-Digest: SHA-256 hash of the raw request body per RFC 9530
Step-by-Step UCP Message Signature Verification
Platforms must implement the verification algorithm described in docs/specification/order.md (lines 66-85) using the following sequence:
1. Parse Signature-Input
Extract the keyid and ordered component list from the Signature-Input header to determine which public key and request components participate in the signature.
2. Resolve the UCP Profile
Fetch the JSON Web Key Set from the URL specified in UCP-Agent. The platform retrieves the business profile from the /.well-known/ucp endpoint, which contains the signing_keys array.
3. Locate the Public JWK
Match the kid parameter from Signature-Input against the kid values in the signing_keys array to retrieve the correct public key for verification.
4. Validate Content-Digest
Recompute the SHA-256 hash over the raw request body and compare it to the Content-Digest header value to ensure payload integrity.
5. Reconstruct the Signature Base
Build the signature base string using the exact component order, HTTP method, authority, path, and headers specified in Signature-Input, following RFC 9421.
6. Verify the ECDSA Signature
Validate the raw r||s encoded signature from the Signature header against the reconstructed base and public key using ECDSA verification.
7. Authorize Business Ownership
Per docs/specification/order.md (lines 89-96), confirm the signing business owns the order referenced in the payload by matching the profile URL against the order's business record.
Security Controls for UCP Message Signatures
Mandatory Signature Enforcement
Every webhook payload must carry a valid signature according to docs/specification/signatures.md (lines 999-1000). Platforms must reject requests lacking signatures or with invalid cryptography.
Key Rotation Grace Period
Businesses rotate keys by publishing new JWKs in their profile. Per docs/specification/signatures.md (lines 44-48), platforms must accept old keys for a minimum grace period of 7 days to prevent delivery failures during rotation.
Replay Attack Prevention
The Idempotency-Key header is included in signed components, mitigating replay attacks as specified in docs/specification/signatures.md (lines 62-64).
Implementation Examples
Example HTTP webhook request:
POST /webhooks/ucp/orders HTTP/1.1
Host: platform.example.com
Content-Type: application/json
UCP-Agent: profile="https://merchant.example/.well-known/ucp"
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "content-type");keyid="merchant-2026"
Signature: sig1=:MEUCIQDTxNq8h7LGHpvVZQp1iHkFp9+3N8Mxk2zH1wK4YuVN8w...:
{"id":"order_abc123","event_id":"evt_123","created_time":"2026-01-15T12:00:00Z"}
Python verification pseudocode:
def verify_webhook(request):
# 1. Parse Signature-Input header
sig_input = parse_signature_input(request.headers["Signature-Input"])
kid = sig_input.keyid
components = sig_input.components
# 2. Resolve signer profile
profile_url = get_profile_url(request.headers["UCP-Agent"])
profile = fetch_json(profile_url)
public_key = find_jwk_by_kid(profile["signing_keys"], kid)
if not public_key:
raise VerificationError("key_not_found")
# 3. Verify Content-Digest (if body is signed)
if "content-digest" in components:
expected = "sha-256=:" + base64(sha256(request.body)) + ":"
if request.headers["Content-Digest"] != expected:
raise VerificationError("digest_mismatch")
# 4. Build signature base per RFC 9421
sig_base = build_signature_base(
components=components,
method=request.method,
authority=request.host,
path=request.path,
headers=request.headers,
keyid=kid,
)
# 5. Verify ECDSA signature (raw r||s encoding)
signature = parse_signature(request.headers["Signature"])
if not ecdsa_verify(sig_base, signature, public_key):
raise VerificationError("signature_invalid")
# 6. Authorization check (order ownership)
payload = json.loads(request.body)
if not business_owns_order(profile_url, payload["id"]):
raise VerificationError("unauthorized_sender")
return True
Summary
- Mandatory signing: All UCP webhooks must include valid HTTP Message Signatures per
docs/specification/signatures.md(lines 999-1000) - Four critical headers:
UCP-Agent,Signature-Input,Signature, andContent-Digestform the verification foundation - Seven-step verification: Parse headers, resolve profiles, locate JWKs, validate digests, rebuild signature bases, verify ECDSA, and authorize business ownership
- 7-day rotation grace: Old signing keys remain valid for one week after rotation to prevent delivery failures
- Replay protection: The
Idempotency-Keyheader binds signatures to specific requests, preventing replay attacks
Frequently Asked Questions
How does a platform locate the correct public key to verify a UCP message signature?
The platform extracts the profile URL from the UCP-Agent header, fetches the /.well-known/ucp endpoint, and matches the keyid from Signature-Input against the kid values in the signing_keys array returned in the profile.
What happens if a webhook arrives without a valid UCP message signature?
According to docs/specification/signatures.md (lines 999-1000), platforms must reject any webhook payload that lacks a valid signature or fails cryptographic verification.
How long are rotated signing keys valid in UCP message signatures?
When businesses publish new JWKs in their profile, platforms must continue accepting the previous key for a minimum grace period of 7 days, as specified in docs/specification/signatures.md (lines 44-48).
Which request components are included in UCP message signature calculations?
The Signature-Input header typically includes @method, @authority, @path, content-digest, content-type, and idempotency-key, ensuring the signature binds to the specific HTTP request and prevents replay attacks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →