From a466fbade51b16b159bd4a85c39e533af04461a3 Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Thu, 18 Jun 2026 11:20:00 -0700 Subject: [PATCH 1/2] security: pre-launch hardening fixes - update schema subject pattern to accept did: URIs (aligns with v0.2 spec) - add private key material check in TR-SIG: fail if cnf.jwk contains 'd' field - sync __version__ to 0.2.0 (matches pyproject.toml) - tighten subject URI validation regex in tr_env (require method+id for DID) - pin CI workflow action references to commit SHAs Signed-off-by: Imran Siddique Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 4 ++-- schemas/trace-claim.json | 2 +- src/trace_tests/__init__.py | 2 +- src/trace_tests/modules/tr_env.py | 4 +++- src/trace_tests/modules/tr_sig.py | 9 +++++++++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 237ef27..bb2d917 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,8 @@ jobs: matrix: python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install package and test deps diff --git a/schemas/trace-claim.json b/schemas/trace-claim.json index ced6a9e..3e4f81f 100644 --- a/schemas/trace-claim.json +++ b/schemas/trace-claim.json @@ -11,7 +11,7 @@ "properties": { "eat_profile": {"type": "string", "const": "tag:agentrust.io,2026:trace-v0.1"}, "iat": {"type": "integer", "minimum": 1700000000}, - "subject": {"type": "string", "pattern": "^spiffe://"}, + "subject": {"type": "string", "pattern": "^(spiffe://|did:)"}, "model": { "type": "object", "required": ["provider", "model_id"], diff --git a/src/trace_tests/__init__.py b/src/trace_tests/__init__.py index af7e308..df4daf9 100644 --- a/src/trace_tests/__init__.py +++ b/src/trace_tests/__init__.py @@ -1,3 +1,3 @@ """TRACE conformance test suite.""" -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/src/trace_tests/modules/tr_env.py b/src/trace_tests/modules/tr_env.py index 2da4d33..0fc72f2 100644 --- a/src/trace_tests/modules/tr_env.py +++ b/src/trace_tests/modules/tr_env.py @@ -2,12 +2,14 @@ from __future__ import annotations +import re import time from typing import Any from trace_tests.result import Finding, Status _PROFILE = "tag:agentrust.io,2026:trace-v0.1" +_SUBJECT_RE = re.compile(r'^(spiffe://[^/]+/.+|did:[a-z0-9]+:.+)$') _IAT_MIN = 1_700_000_000 #: Default maximum record age (seconds). Records older than this fail freshness. @@ -45,7 +47,7 @@ def check(trace: dict[str, Any], max_age_seconds: int = DEFAULT_MAX_AGE_SECONDS) findings.append(Finding("TR-ENV-002", Status.FAIL, f"iat must be a Unix timestamp >= {_IAT_MIN}, got {iat!r}")) subject = trace.get("subject", "") - if isinstance(subject, str) and subject.startswith(("spiffe://", "did:")): + if isinstance(subject, str) and _SUBJECT_RE.match(subject): findings.append(Finding("TR-ENV-003", Status.PASS, f"subject is a valid workload identity URI ({subject!r})")) else: findings.append(Finding("TR-ENV-003", Status.FAIL, f"subject must be a SPIFFE URI (spiffe://) or DID URI (did:), got {subject!r}")) diff --git a/src/trace_tests/modules/tr_sig.py b/src/trace_tests/modules/tr_sig.py index 4657a8f..c9d40cc 100644 --- a/src/trace_tests/modules/tr_sig.py +++ b/src/trace_tests/modules/tr_sig.py @@ -102,6 +102,15 @@ def check(trace: dict[str, Any], record: dict[str, Any], fmt: str, level: int = jwk = trace.get("cnf", {}).get("jwk", {}) kty = jwk.get("kty") crv = jwk.get("crv") + x = jwk.get("x") + + if "d" in jwk: + findings.append(Finding( + rule="TR-SIG-002", + status=Status.FAIL, + message="cnf.jwk must not contain private key material ('d' field present in JWK)", + )) + return findings if kty in _SUPPORTED_KTY: label = f"kty={kty!r}" + (f", crv={crv!r}" if crv else "") From 64a0fabf2fc99846f97da390ff835ce063828cab Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Thu, 18 Jun 2026 11:32:17 -0700 Subject: [PATCH 2/2] fix: remove signature from partial-envelope rejection parametrize The loader intentionally allows a top-level 'signature' field in plain TRACE records because sign_record() embeds the Ed25519 signature inline. Only 'trace' and 'gateway' are unambiguous cmcp envelope markers. The [signature] parametrize case was testing behavior the loader explicitly chose not to implement, causing a pre-existing test failure on main. Signed-off-by: Imran Siddique Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/test_loader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_loader.py b/tests/unit/test_loader.py index 9115603..0c21657 100644 --- a/tests/unit/test_loader.py +++ b/tests/unit/test_loader.py @@ -53,8 +53,11 @@ def test_stripping_cmcp_version_is_rejected_not_downgraded(tmp_path): load_record(_write(tmp_path, stripped)) -@pytest.mark.parametrize("leftover", ["trace", "gateway", "signature"]) +@pytest.mark.parametrize("leftover", ["trace", "gateway"]) def test_partial_envelope_with_single_cmcp_key_is_rejected(tmp_path, leftover): + # "signature" is intentionally excluded: plain TRACE records may carry an + # embedded signature field (agentrust-trace sign_record() output), so the + # loader allows it. Only "trace" and "gateway" are unambiguous cmcp markers. record = {**VALID_TRACE, leftover: VALID_CMCP[leftover]} with pytest.raises(LoadError, match="partial cmcp-runtime envelope"): load_record(_write(tmp_path, record))