Skip to content

trace-verify verifies TRACE signatures with json.dumps(sort_keys=True), which spec section 3.2.2 names as insufficient #138

Description

@lywinged

Raised first on #134, which merged before anyone had a chance to look at it. Moving it here rather than leaving it on a closed PR, because it is not a defect in that document: spec/registry-anchor-v1.md §7 is what makes the package a reference implementation, and §5.2 explicitly hands signature verification to v0.2 §3.3, which the package implements.

Filed here because there is nowhere else public to file it. trace-verify's PyPI metadata points Bug Tracker at github.com/agentrust-io/trace-registry, which returns 404 to an anonymous reader.

What it does

trace_verify._signature.canonical_body_bytes builds the signature pre-image as:

body = {k: v for k, v in claim.items() if k != "signature"}
json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("ascii")

spec/trace-v0.2.md §3.2.2 names that exact construction:

Implementations MUST use an RFC 8785-conformant library. Using json.dumps(sort_keys=True) (Python) or equivalent ad-hoc sorting is insufficient: it diverges from RFC 8785 for non-ASCII strings and for IEEE 754 number serialization.

agentrust_trace.sign_record signs the same object -- the claim with signature removed -- through rfc8785.dumps. Same scheme, different canonicalizer, so the two agree on ASCII and disagree everywhere else.

Reproduction

agentrust-trace 0.6.0 and trace-verify 0.2.0, both from PyPI. Escapes throughout, so nothing here depends on how a terminal renders it.

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from agentrust_trace.sign import sign_record, key_to_jwk
import trace_verify._signature as tvs

key = Ed25519PrivateKey.generate()
record = {
    "eat_profile": "https://trace.agentrust-io.com/profiles/v0.2",
    "sub": "spiffe://trust.example.org/agent/payments",
    "policy_label": "Zahlungsverkehr f\u00fcr M\u00fcnchen",  # escapes; any non-ASCII does it
}
signed = sign_record(record, key)
print(tvs.verify_claim_signature(signed, key_to_jwk(key)))   # False
record contents                                agentrust-trace   trace-verify
ASCII only                                     valid             valid
any string field containing U+00FC             valid             INVALID
any string field containing a CJK character    valid             INVALID

The CLI verifies signatures by default and exits 1 when the check fails, so an external verifier following §7, on a valid record carrying a German policy label or a non-Latin data class, is told the signature is invalid. verify_claim_against_registry's docstring describes it as "the fail-closed check used by the anchoring paths"; if that is accurate, such a record also cannot be anchored, but that path is not visible from outside.

Two readings

The public material does not distinguish them, so I am asking rather than asserting:

  1. the registry deliberately signs over a different pre-image than v0.2 §3.2.2, in which case that belongs in spec/registry-anchor-v1.md §0 as a third row -- and it is the most consequential one, since §0 currently states that the signing layer is JCS; or
  2. it is a defect in the package.

Either way I would not have §7 point an external verifier at it before that is settled, since §7 exists precisely so that people run it.

Severity, honestly

Nobody is hurt today. This is a Developer Preview, ADOPTERS.md lists one organisation, and a record containing a non-ASCII character may not exist in the wild yet. Nothing is failing right now.

What makes it worth fixing while it is cheap is the failure mode. A valid, correctly signed record is reported as having an invalid signature. For evidence whose whole purpose is that a third party can check it without trusting the operator, a false rejection is the worst direction to fail in, and the trigger is not exotic: one accented character in a policy label.

It is also the mistake spec/registry-anchor-v1.md §0 warns about, in the opposite direction. §0 says do not reuse the signing canonicalizer at the leaf. What is in the shipped code is the leaf canonicalizer reused at the signing layer. The warning is right and one-directional, and the missing direction is the one that has already happened.

Two smaller things in the same package

  • PyPI metadata points Homepage, Documentation and Bug Tracker at github.com/agentrust-io/trace-registry. All three return 404 to an anonymous reader, which is the audience spec/registry-anchor-v1.md exists to serve.
  • The installed distribution is 0.2.0 while trace_verify.__version__ is 0.1.0, and --version prints the latter. Same drift chore(release): 0.6.0, and stop __version__ drifting #131 fixed here.

Happy to open a PR against whichever repository ends up owning the fix, if that is useful and if it becomes reachable.

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