Summary
conformance_input.json has no field for a signing key, and integration_test_utils.get_headers() sends a placeholder:
return {
"UCP-Agent": f'profile="{profile_url}"',
"request-signature": "test",
...
}
So the suite cannot produce an RFC 9421 signature, and a merchant configured to enforce signatures refuses every one of the 77 tests. That means the suite can only be run against a merchant that does not check signatures — and signature verification is the one part of UCP where a plausible-looking implementation is most likely to be wrong in a way nothing else notices.
Why this matters concretely
We hit exactly that failure mode in ucp-php-sdk. Our SDK emitted ECDSA signatures as DER from openssl_sign where RFC 9421 §3.3.2 requires fixed-width r || s, and advertised alg="ES256" (a JWS name) rather than the registry id ecdsa-p256-sha256. Every signature it produced was unverifiable by any conformant peer, and every signature a conformant peer sent was rejected.
Our whole test suite was green throughout, because it verified our signer against our own verifier. The conformance suite is the external oracle that should have caught it — and could not, because the default policy it can exercise is "do not check".
We have now written a separate pass outside the suite to cover this, which works but is the wrong place for it: the check belongs where every implementation runs it, not in one SDK's repository.
What we think is needed
An optional signing identity in conformance_input.json, e.g.:
{
"ucp_version": "2026-08-25",
"signing_key": {
"kid": "conformance-agent-key",
"alg": "ecdsa-p256-sha256",
"private_key_pem": "-----BEGIN PRIVATE KEY-----\n...",
"jwk": { "kty": "EC", "crv": "P-256", "kid": "...", "x": "...", "y": "..." }
}
}
with the suite:
- publishing the JWK in the mock agent profile's
keys[] (the profile server already exists, so this is where the key would go);
- signing each request per RFC 9421 when the key is present, and keeping today's placeholder behaviour when it is absent, so existing runs are unaffected;
- ideally, generating an ephemeral key per run when none is configured, so this needs no configuration at all to be useful.
If it is preferable not to put a private key in the fixture, generating one per run and publishing only the public half in the mock profile is strictly better and needs no new fixture field.
Tests this would unlock
Beyond "a signed request is accepted", the interesting cases are the refusals, which are where implementations diverge:
- an unsigned request is refused, and refused with a UCP error descriptor rather than a stack trace or HTML — an agent that cannot parse the refusal cannot distinguish a signature problem from an outage
- a request whose covered components omit
content-digest while carrying a body is refused; otherwise stripping it from the covered list is a body-swap primitive, since the signature still verifies over method and target while nothing attests to the bytes
- a signature covering a component set other than the implementer's own default still verifies — RFC 9421 leaves the covered set entirely to the signer, and rebuilding the base from a hardcoded list is a very easy mistake to make (we made it)
@signature-params is reproduced byte-for-byte from what was received, including parameters the verifier does not itself read, so an unknown parameter such as tag="web-bot-auth" does not break an otherwise valid signature
- a signature with no
expires, or one whose lifetime is unbounded, is handled per whatever the spec intends here — worth pinning either way
Offer
Happy to open the PR rather than leave this as a report — we have the client-side implementation already and can adapt it, including the ephemeral-key variant. Say which shape you would accept and we will send it.
Filed from ucp-php-sdk, which runs this suite in CI. Related reports: #101, #102, #103, #104.
Summary
conformance_input.jsonhas no field for a signing key, andintegration_test_utils.get_headers()sends a placeholder:So the suite cannot produce an RFC 9421 signature, and a merchant configured to enforce signatures refuses every one of the 77 tests. That means the suite can only be run against a merchant that does not check signatures — and signature verification is the one part of UCP where a plausible-looking implementation is most likely to be wrong in a way nothing else notices.
Why this matters concretely
We hit exactly that failure mode in
ucp-php-sdk. Our SDK emitted ECDSA signatures as DER fromopenssl_signwhere RFC 9421 §3.3.2 requires fixed-widthr || s, and advertisedalg="ES256"(a JWS name) rather than the registry idecdsa-p256-sha256. Every signature it produced was unverifiable by any conformant peer, and every signature a conformant peer sent was rejected.Our whole test suite was green throughout, because it verified our signer against our own verifier. The conformance suite is the external oracle that should have caught it — and could not, because the default policy it can exercise is "do not check".
We have now written a separate pass outside the suite to cover this, which works but is the wrong place for it: the check belongs where every implementation runs it, not in one SDK's repository.
What we think is needed
An optional signing identity in
conformance_input.json, e.g.:{ "ucp_version": "2026-08-25", "signing_key": { "kid": "conformance-agent-key", "alg": "ecdsa-p256-sha256", "private_key_pem": "-----BEGIN PRIVATE KEY-----\n...", "jwk": { "kty": "EC", "crv": "P-256", "kid": "...", "x": "...", "y": "..." } } }with the suite:
keys[](the profile server already exists, so this is where the key would go);If it is preferable not to put a private key in the fixture, generating one per run and publishing only the public half in the mock profile is strictly better and needs no new fixture field.
Tests this would unlock
Beyond "a signed request is accepted", the interesting cases are the refusals, which are where implementations diverge:
content-digestwhile carrying a body is refused; otherwise stripping it from the covered list is a body-swap primitive, since the signature still verifies over method and target while nothing attests to the bytes@signature-paramsis reproduced byte-for-byte from what was received, including parameters the verifier does not itself read, so an unknown parameter such astag="web-bot-auth"does not break an otherwise valid signatureexpires, or one whose lifetime is unbounded, is handled per whatever the spec intends here — worth pinning either wayOffer
Happy to open the PR rather than leave this as a report — we have the client-side implementation already and can adapt it, including the ephemeral-key variant. Say which shape you would accept and we will send it.
Filed from
ucp-php-sdk, which runs this suite in CI. Related reports: #101, #102, #103, #104.