Skip to content

verify: verify_record leaves as TypeError when runtime.nonce is non-ASCII and expected_nonce is set #381

Closed

Description

@chernistry

What is wrong

verify_record documents its contract as InvalidSignature for a bad signature and ValueError for every other rejection. With expected_nonce supplied and a runtime.nonce outside ASCII, it leaves as a TypeError instead:

TypeError: comparing strings with non-ASCII characters is not supported

sign.py:712 compares the two nonces with hmac.compare_digest(actual_nonce, expected_nonce), which refuses str operands containing non-ASCII characters. The schema types runtime.nonce as a string with a description saying base64url, and no pattern, so the record is schema-valid.

Reproducer, on the base record from #379 (same BASE, KEY, NOW):

body = json.loads(json.dumps(BASE)); body["runtime"]["nonce"] = "nønce"
verify_record(sign_record(body, KEY), key_to_jwk(KEY), now=NOW, expected_nonce="a-nonce-value")
# TypeError: comparing strings with non-ASCII characters is not supported

The rule it tests: spec §3.2.2 "When a challenge nonce was issued, a record that omits or mismatches it MUST be rejected", and verify_record's own exception contract.

Spec section or file

  • src/agentrust_trace/sign.py:710-713
  • spec/trace-v0.2.md §3.2.2 (freshness, challenge-nonce binding) and §3.3 step 2

Expected behavior

ValueError("record runtime.nonce does not match expected_nonce"), reached by comparing the UTF-8 encodings (compare_digest(actual.encode(), expected.encode())) so the comparison stays constant-time and accepts any string. Alternatively the schema constrains nonce with a base64url pattern and the record is rejected earlier as schema-invalid; either way the outcome is one of the documented ones.

Impact

A caller that catches the documented exceptions and treats anything else as a verifier fault gets a fault from attacker-influenced input: the nonce is copied from the record. The record is rejected either way, so this is a contract escape rather than a wrong verdict.

Found by the differential run in #376 (mutation m41).

Activity

  1. chernistry commented on Sep 21, 2026

    @chernistry
    ContributorAuthor

    The line numbers in the body are this pull request's base (sign.py:710-713, 712). In the published 0.10.0 wheel the compare_digest call is sign.py:683; on main at ccbd5b9 it is 799. The quoted block is identical in all three, and the reproducer runs unchanged on the wheel. Noted by @lywinged on #376.

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