Skip to content

tools(acta_receipt_verifier): reject private JWKs, non-canonical base64url keys, and non-canonical signature.sig hex #605

Closed

Description

@drk-ram

What is wrong

At main (4c83b1d), tools/acta_receipt_verifier.py (the reference verifier for draft-farley-acta-signed-receipts-03 used by tests/test_acta_fixtures.py and tests/test_acta_receipt_verifier.py) has three verification and input-hardening gaps:

  1. issuer_key accepts JWKs containing private key material and non-canonical base64url x values:

    • In tools/acta_receipt_verifier.py:165-186, issuer_key checks jwk.get("kty") != "OKP" or jwk.get("crv") != "Ed25519" or not isinstance(x, str) and then calls base64.urlsafe_b64decode(x + "=" * (-len(x) % 4)).
    • It does not reject RFC 7517 / RFC 7518 / RFC 8037 private key parameters (d, p, q, dp, dq, qi, oth, k), and it accepts padded (=), non-url-safe (+//), ignored-character, or non-zero-trailing-bit base64url strings for x.
  2. signature_verifies accepts uppercase hex and arbitrary whitespace in signature.sig:

    • In tools/acta_receipt_verifier.py:86-92, signature_verifies calls bytes.fromhex(envelope["signature"]["sig"]) directly.
    • Python's bytes.fromhex accepts uppercase hex characters (A-F) and strips arbitrary ASCII whitespace (" " + sig.upper() + "\n"), whereas _link_is_well_formed (lines 114-119) holds hex fields to lowercase hex ([0-9a-f]) without whitespace.
  3. _read_json accepts duplicate JSON keys and does not catch RecursionError:

    • _read_json (lines 189-193) calls json.loads(...) without an object_pairs_hook to reject duplicate keys (even though signature_verifies and envelope_hash canonicalize via RFC 8785 JCS, which is defined over I-JSON RFC 7493 with no duplicate keys) and only catches (OSError, UnicodeDecodeError, ValueError), allowing RecursionError on deeply nested JSON to crash the verifier instead of returning ("undecidable", "malformed").

Reproduction on 4c83b1d:

from tools import acta_receipt_verifier as acta
from agentrust_trace import sign

k = sign.generate_key()
jwk = sign.key_to_jwk(k)
jwk["kid"] = "k1"
jwk["d"] = "private-key-material"

# 1. Accepts JWK with private key 'd':
ik = acta.issuer_key({"keys": [jwk]}, "k1")

# 2. Accepts uppercase hex with surrounding whitespace in signature.sig:
payload = {"issued_at": "2026-10-20T00:00:00Z", "action": "test"}
sig_hex = k.sign(acta.jcs(payload)).hex()
env = {"payload": payload, "signature": {"alg": "EdDSA", "kid": "k1", "sig": "  " + sig_hex.upper() + "\n"}}
assert acta.verify_envelope(env, ik) == ("valid", None)

Spec section or file

  • tools/acta_receipt_verifier.py (issuer_key, signature_verifies, _read_json, _instant)
  • tests/test_acta_receipt_verifier.py

Expected behavior

  1. issuer_key rejects any JWK carrying private key parameters (d, p, q, dp, dq, qi, oth, k) or non-canonical unpadded base64url x with Undecidable("malformed_key_set").
  2. signature_verifies requires envelope["signature"]["sig"] to match ^[0-9a-f]{128}\Z before verifying the Ed25519 signature, returning False (("invalid", "signature_invalid")) on uppercase hex or whitespace.
  3. _read_json rejects duplicate JSON object keys and catches RecursionError as Undecidable("malformed").

Impact

acta_receipt_verifier.py accepts unredacted private JWKs, non-canonical key encodings, duplicate JSON members, and non-canonical signature hex strings with whitespace.

Activity

  1. added 2 commits that reference this issue on Oct 22, 2026
    a36e946
    60bc7f2
  2. added a commit that references this issue on Oct 28, 2026
    2bbff1d
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