Skip to content

conformance_input.json has no signing-key slot, so the suite cannot test signature enforcement #105

Description

@BrocksiNet

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:

  1. publishing the JWK in the mock agent profile's keys[] (the profile server already exists, so this is where the key would go);
  2. 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;
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions