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, and content-type
  • Signature: The ECDSA signature value encoded in raw r||s format
  • Content-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, and Content-Digest form 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-Key header 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →