From 267dbb142b062eb83ae8423a9f8fb9519c2f202e Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Mon, 21 Sep 2026 08:28:59 -0700 Subject: [PATCH 1/2] fix: compare challenge nonces as UTF-8 bytes Signed-off-by: Imran Siddique --- CHANGELOG.md | 3 +++ src/agentrust_trace/sign.py | 4 +++- tests/test_sign.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b83532d1..6ec69283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR. ## [Unreleased] +- Compare challenge nonces as UTF-8 bytes so non-ASCII mismatches raise the documented ValueError instead of TypeError (#381). Matching strings remain accepted without Unicode normalization. Reported by @chernistry. + + - Regenerate the v0.3 draft after canonical reproducibility additions and make the schema inventory test use portable repository paths on Windows. diff --git a/src/agentrust_trace/sign.py b/src/agentrust_trace/sign.py index 61e8926a..0f1d5436 100644 --- a/src/agentrust_trace/sign.py +++ b/src/agentrust_trace/sign.py @@ -796,7 +796,9 @@ def verify_record( # Freshness: bind to a caller-supplied nonce when provided. if expected_nonce is not None: actual_nonce = record.get("runtime", {}).get("nonce") - if not isinstance(actual_nonce, str) or not compare_digest(actual_nonce, expected_nonce): + if not isinstance(actual_nonce, str) or not compare_digest( + actual_nonce.encode("utf-8"), expected_nonce.encode("utf-8") + ): raise ValueError("record runtime.nonce does not match expected_nonce") # Canonical bytes: record without "signature" key diff --git a/tests/test_sign.py b/tests/test_sign.py index 1bad8baf..8eb31fb2 100644 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -1186,3 +1186,31 @@ def test_the_guards_do_not_refuse_what_they_should_accept(): jwk_thumbprint(key_to_jwk(key)) verify_record(record, key_to_jwk(key)) + + +@pytest.mark.parametrize("nonce", ["abc123", "n\u00f8nce", "\U0001f512"]) +def test_verify_record_nonce_unicode_match(nonce): + key = generate_key() + record = _fresh_record() + record["runtime"]["nonce"] = nonce + signed = sign_record(record, key) + verify_record(signed, key_to_jwk(key), expected_nonce=nonce) + + +@pytest.mark.parametrize( + ("actual", "expected"), + [ + ("wrong", "abc123"), + ("n\u00f8nce", "a-nonce-value"), + ("a-nonce-value", "n\u00f8nce"), + ("\U0001f512", "\U0001f513"), + ("\u00e9", "e\u0301"), + ], +) +def test_verify_record_nonce_unicode_mismatch(actual, expected): + key = generate_key() + record = _fresh_record() + record["runtime"]["nonce"] = actual + signed = sign_record(record, key) + with pytest.raises(ValueError, match="record runtime.nonce does not match expected_nonce"): + verify_record(signed, key_to_jwk(key), expected_nonce=expected) From 7897d82ba14e4a718e8378e034f2bad4791388f9 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Mon, 21 Sep 2026 13:15:53 -0700 Subject: [PATCH 2/2] fix: validate nonce configuration and test comparison use Signed-off-by: Imran Siddique --- CHANGELOG.md | 2 +- src/agentrust_trace/sign.py | 2 ++ tests/test_sign.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec69283..88f8acb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR. ## [Unreleased] -- Compare challenge nonces as UTF-8 bytes so non-ASCII mismatches raise the documented ValueError instead of TypeError (#381). Matching strings remain accepted without Unicode normalization. Reported by @chernistry. +- Compare challenge nonces as UTF-8 bytes so non-ASCII mismatches raise the documented ValueError instead of TypeError (#381). Matching strings remain accepted without Unicode normalization. Invalid non-string expected_nonce settings raise a clear ValueError. Reported by @chernistry; follow-up review by @lywinged. - Regenerate the v0.3 draft after canonical reproducibility additions and make diff --git a/src/agentrust_trace/sign.py b/src/agentrust_trace/sign.py index 0f1d5436..8dc097f0 100644 --- a/src/agentrust_trace/sign.py +++ b/src/agentrust_trace/sign.py @@ -795,6 +795,8 @@ def verify_record( # Freshness: bind to a caller-supplied nonce when provided. if expected_nonce is not None: + if not isinstance(expected_nonce, str): + raise ValueError("expected_nonce must be a string") actual_nonce = record.get("runtime", {}).get("nonce") if not isinstance(actual_nonce, str) or not compare_digest( actual_nonce.encode("utf-8"), expected_nonce.encode("utf-8") diff --git a/tests/test_sign.py b/tests/test_sign.py index 8eb31fb2..4d741cf7 100644 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -1,6 +1,7 @@ """Tests for agentrust_trace.sign.""" import base64 +import hmac import json import time @@ -1214,3 +1215,33 @@ def test_verify_record_nonce_unicode_mismatch(actual, expected): signed = sign_record(record, key) with pytest.raises(ValueError, match="record runtime.nonce does not match expected_nonce"): verify_record(signed, key_to_jwk(key), expected_nonce=expected) + + +@pytest.mark.parametrize("expected", [b"a-nonce-value", 123, [], True, {}]) +def test_verify_record_rejects_invalid_expected_nonce_configuration(expected): + key = generate_key() + record = _fresh_record() + record["runtime"]["nonce"] = "a-nonce-value" + signed = sign_record(record, key) + with pytest.raises(ValueError, match="^expected_nonce must be a string$"): + verify_record(signed, key_to_jwk(key), expected_nonce=expected) + + +def test_nonce_verdict_uses_compare_digest_result(monkeypatch): + key = generate_key() + record = _fresh_record() + record["runtime"]["nonce"] = "n\u00f8nce" + signed = sign_record(record, key) + real_compare = hmac.compare_digest + seen = [] + + def refuse_nonce(a, b): + if isinstance(a, bytes) and isinstance(b, bytes): + seen.append((a, b)) + return False + return real_compare(a, b) + + monkeypatch.setattr(hmac, "compare_digest", refuse_nonce) + with pytest.raises(ValueError, match="record runtime.nonce does not match expected_nonce"): + verify_record(signed, key_to_jwk(key), expected_nonce="n\u00f8nce") + assert seen == [("n\u00f8nce".encode("utf-8"), "n\u00f8nce".encode("utf-8"))]