Skip to content

Python reference verifier cannot discover the signing keys of a schema correct peer at its own declared version #227

Description

@vishkaty

What this is

This is the Python twin of #225, which documents the identical defect in
the Node reference server (reached there via #162 and #179) and already
traces this Python instance to our own merged #122 and #169.
That issue already names this companion; this is it.

What I found

The Python sample server declares UCP_VERSION 2026-04-08
(routes/discovery_profile.json). At that pin,
source/discovery/profile_schema.json $defs/base requires ucp and
separately declares signing_keys as a top level sibling of ucp. That
schema defines no keys field anywhere, nested or otherwise.

discovery.py publishes signing_keys[] correctly at the top level, but
also mirrors the same key into a nested ucp.keys[] that has no basis in
the schema at any pin (rest/python/server/routes/discovery.py:73). Its own
verifier, _extract_keys() in ucp_signing.py, reads only that nested
field (rest/python/server/ucp_signing.py:821):

ucp = document.get("ucp", document)
value = ucp.get("keys") if isinstance(ucp, dict) else None

Every real profile document carries ucp (the schema requires it), so
this reader always takes that branch and looks inside ucp for keys.
It can never see a top level sibling field on any real document. A peer
that publishes only the schema correct top level signing_keys[], with
nothing extra, fails key discovery against this very server.

Observed against main (00333a8)

Fresh clone at main (00333a8), ucp-sdk pinned to 0.4.6 locally (a
fresh uv sync currently resolves 0.5.0 and the server fails to import,
tracked separately in #221/#222). A direct call against _extract_keys
confirms the read side:

_extract_keys({"ucp": {"version": "2026-04-08"}, "signing_keys": [{"kid": "k"}]})
# => [] -- the schema correct shape returns nothing

_extract_keys({"ucp": {"keys": [{"kid": "k"}]}})
# => [{"kid": "k"}] -- only the nonstandard nested shape is read

The booted server confirms the write side matches (curl
/.well-known/ucp): signing_keys[] at the top level, keys[] mirrored
under ucp, both carrying the same webhook JWK.

Expected

signing_keys[] (the 2026-04-08 pin this server declares) published at
the top level, and read from the top level.

Where it comes from

#122 (request signature verification) introduced _extract_keys reading
only a nested keys[], with a comment pre-adopting the ucp#566 rename
that only applies to 2026-08-25 and later, and getting the location wrong
too (ucp#566 places keys as a top level sibling of ucp, never nested).
#169 (webhook signing) then added the top level signing_keys[]
publication and, to satisfy the #122 reader, the nested ucp.keys[]
mirror.

Why CI did not catch it

Every test that exercises key discovery constructed its own profile
document rather than reading the real served output through both the
write and read paths at once. The shared "good" fixture in
signature_integration_test.py was shaped {"ucp": {"keys": [...]}},
matching the actual (wrong) expectation of _extract_keys rather than
the schema. This fixture is a bare dict literal, not an attribute access,
so it was missed by an initial grep for attribute-style .keys usage.

A fix is attached as a companion PR.

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