From 2bdd565c0e3e917d7b0db29b93c2a4ea2e06607a Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 3 Apr 2026 03:27:28 -0400 Subject: [PATCH 1/3] conformance: add MolTrust AAE delegation narrowing vectors --- .../moltrust-aae-delegation-narrowing.json | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 conformance/moltrust-aae-delegation-narrowing.json diff --git a/conformance/moltrust-aae-delegation-narrowing.json b/conformance/moltrust-aae-delegation-narrowing.json new file mode 100644 index 0000000..5326c03 --- /dev/null +++ b/conformance/moltrust-aae-delegation-narrowing.json @@ -0,0 +1,106 @@ +{ + "protocol": "MolTrust AAE v1.0", + "description": "Delegation narrowing test vectors — scope escalation, validity extension, self-issuance, ghost agent", + "canonicalization": "JCS RFC 8785", + "signature_scheme": "Ed25519", + "submitted_by": "did:moltrust:moltycell", + "reference": "https://moltrust.ch/MolTrust_Protocol_TechSpec_v0.6.pdf", + "vectors": [ + { + "id": "moltrust-tv-001", + "description": "Valid delegation with narrowed scope — baseline", + "expected_outcome": "VALID", + "parent": { + "subject": "did:moltrust:agent-a", + "issuer": "did:moltrust:issuer-root", + "mandate": { "scope": ["read", "write", "delegate"], "domains": ["github", "email"] }, + "constraints": { "spend_limit_usdc": 1000, "reversibility": "required", "reputation_minimum": 70 }, + "validity": { "not_before": "2026-04-01T00:00:00Z", "not_after": "2026-04-30T23:59:59Z" } + }, + "child": { + "subject": "did:moltrust:agent-b", + "issuer": "did:moltrust:agent-a", + "mandate": { "scope": ["read"], "domains": ["github"] }, + "constraints": { "spend_limit_usdc": 0, "reversibility": "required", "reputation_minimum": 70 }, + "validity": { "not_before": "2026-04-01T00:00:00Z", "not_after": "2026-04-07T23:59:59Z" } + }, + "rationale": "Child scope is strict subset of parent. Spend reduced to 0. Validity window narrowed. All constraints satisfied." + }, + { + "id": "moltrust-tv-002", + "description": "Scope escalation — child requests write access not in parent mandate", + "expected_outcome": "INVALID", + "failure_reason": "SCOPE_ESCALATION", + "parent": { + "subject": "did:moltrust:agent-b", + "issuer": "did:moltrust:agent-a", + "mandate": { "scope": ["read"], "domains": ["github"] }, + "constraints": { "spend_limit_usdc": 0, "reversibility": "required" }, + "validity": { "not_before": "2026-04-01T00:00:00Z", "not_after": "2026-04-07T23:59:59Z" } + }, + "child": { + "subject": "did:moltrust:agent-c", + "issuer": "did:moltrust:agent-b", + "mandate": { "scope": ["read", "write"], "domains": ["github"] }, + "constraints": { "spend_limit_usdc": 0, "reversibility": "required" }, + "validity": { "not_before": "2026-04-01T00:00:00Z", "not_after": "2026-04-03T23:59:59Z" } + }, + "rationale": "Child requests 'write' not present in parent mandate. Delegation chain verification must reject." + }, + { + "id": "moltrust-tv-003", + "description": "Validity window extension — child validity extends beyond parent", + "expected_outcome": "INVALID", + "failure_reason": "VALIDITY_ESCALATION", + "parent": { + "subject": "did:moltrust:agent-b", + "validity": { "not_before": "2026-04-01T00:00:00Z", "not_after": "2026-04-07T23:59:59Z" } + }, + "child": { + "subject": "did:moltrust:agent-c", + "issuer": "did:moltrust:agent-b", + "validity": { "not_before": "2026-04-01T00:00:00Z", "not_after": "2026-04-30T23:59:59Z" } + }, + "rationale": "An agent cannot grant more time than it has been granted. Child not_after exceeds parent not_after." + }, + { + "id": "moltrust-tv-004", + "description": "Self-issuance — agent attempts to re-issue its own AAE with relaxed constraints", + "expected_outcome": "INVALID", + "failure_reason": "SELF_ISSUANCE", + "original": { + "subject": "did:moltrust:agent-a", + "issuer": "did:moltrust:issuer-root", + "constraints": { "spend_limit_usdc": 100, "reversibility": "required" } + }, + "modified": { + "subject": "did:moltrust:agent-a", + "issuer": "did:moltrust:agent-a", + "constraints": { "spend_limit_usdc": 10000, "reversibility": "optional" } + }, + "rationale": "subject == issuer is always invalid. Closes RSAC Gap 1 (self-modification of security policy)." + }, + { + "id": "moltrust-tv-005", + "description": "Expired credentials — ghost agent presents valid but temporally expired AAE", + "expected_outcome": "INVALID", + "failure_reason": "EXPIRED", + "aae": { + "subject": "did:moltrust:agent-ghost", + "issuer": "did:moltrust:issuer-root", + "mandate": { "scope": ["read", "write"] }, + "validity": { "not_before": "2026-01-01T00:00:00Z", "not_after": "2026-03-01T23:59:59Z" } + }, + "evaluation_time": "2026-04-01T10:00:00Z", + "rationale": "AAE is cryptographically valid but expired. Closes RSAC Gap 3 (ghost agents with live credentials)." + } + ], + "dimension_mapping": { + "note": "AAE fields mapped to qntm ConstraintEvaluation facets", + "mandate.scope": "scope", + "constraints.spend_limit_usdc": "spend", + "validity.not_after": "time", + "constraints.reputation_minimum": "reputation", + "constraints.reversibility": "reversibility" + } +} From b3baf6be9a7972658dacaf4da52517445560a3b7 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 3 Apr 2026 03:27:29 -0400 Subject: [PATCH 2/3] conformance: add AAE delegation narrowing runner (5/5 pass, stdlib only) --- conformance/run_aae_delegation.py | 150 ++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 conformance/run_aae_delegation.py diff --git a/conformance/run_aae_delegation.py b/conformance/run_aae_delegation.py new file mode 100644 index 0000000..be18b8c --- /dev/null +++ b/conformance/run_aae_delegation.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +run_aae_delegation.py — Conformance runner for MolTrust AAE delegation narrowing vectors. + +Tests 5 delegation invariants from qntm Authority Constraints Interface spec (PR #11): + - TV-001: Valid narrowed delegation (VALID baseline) + - TV-002: Scope escalation rejection (SCOPE_ESCALATION) + - TV-003: Validity window extension rejection (VALIDITY_ESCALATION) + - TV-004: Self-issuance rejection (SELF_ISSUANCE) + - TV-005: Expired credential rejection (EXPIRED) + +No crypto dependencies — these are semantic invariants, not byte-level encoding checks. +(Contrast sv-sig-01 which requires cryptography/PyNaCl for round-trip verification.) + +Usage: + python3 run_aae_delegation.py [path/to/vectors.json] + +Exit 0: all assertions pass +Exit 1: one or more failures +""" + +import json +import sys +from datetime import datetime +from pathlib import Path + + +def _dt(s: str) -> datetime: + """Parse ISO 8601 UTC timestamp. Accepts trailing Z or +00:00.""" + return datetime.fromisoformat(s.replace("Z", "+00:00")) + + +# ── Invariant checkers ──────────────────────────────────────────────────────── + +def check_tv001(v) -> tuple[str, str | None]: + """TV-001: all delegation constraints must narrow or equal parent values.""" + p, c = v["parent"], v["child"] + + # Scope: child ⊆ parent + p_scope = set(p["mandate"]["scope"]) + c_scope = set(c["mandate"]["scope"]) + over = c_scope - p_scope + if over: + return "INVALID", f"SCOPE_ESCALATION: {sorted(over)} not in parent" + + # Spend: child ≤ parent + p_spend = p["constraints"]["spend_limit_usdc"] + c_spend = c["constraints"]["spend_limit_usdc"] + if c_spend > p_spend: + return "INVALID", f"SPEND_ESCALATION: {c_spend} > {p_spend}" + + # Validity: child not_after ≤ parent not_after + p_exp = _dt(p["validity"]["not_after"]) + c_exp = _dt(c["validity"]["not_after"]) + if c_exp > p_exp: + return "INVALID", f"VALIDITY_ESCALATION: child expires {c_exp} after parent {p_exp}" + + return "VALID", None + + +def check_tv002(v) -> tuple[str, str | None]: + """TV-002: scope escalation must be rejected.""" + p, c = v["parent"], v["child"] + p_scope = set(p["mandate"]["scope"]) + c_scope = set(c["mandate"]["scope"]) + over = c_scope - p_scope + if over: + return "INVALID", "SCOPE_ESCALATION" + return "VALID", None + + +def check_tv003(v) -> tuple[str, str | None]: + """TV-003: validity window extension must be rejected.""" + p_exp = _dt(v["parent"]["validity"]["not_after"]) + c_exp = _dt(v["child"]["validity"]["not_after"]) + if c_exp > p_exp: + return "INVALID", "VALIDITY_ESCALATION" + return "VALID", None + + +def check_tv004(v) -> tuple[str, str | None]: + """TV-004: self-issuance (subject == issuer) must be rejected.""" + modified = v["modified"] + if modified["subject"] == modified["issuer"]: + return "INVALID", "SELF_ISSUANCE" + return "VALID", None + + +def check_tv005(v) -> tuple[str, str | None]: + """TV-005: expired AAE must be rejected at evaluation_time.""" + not_after = _dt(v["aae"]["validity"]["not_after"]) + eval_time = _dt(v["evaluation_time"]) # pinned in vector; runner does NOT use now() + if eval_time > not_after: + return "INVALID", "EXPIRED" + return "VALID", None + + +CHECKERS = { + "moltrust-tv-001": check_tv001, + "moltrust-tv-002": check_tv002, + "moltrust-tv-003": check_tv003, + "moltrust-tv-004": check_tv004, + "moltrust-tv-005": check_tv005, +} + + +# ── Runner ──────────────────────────────────────────────────────────────────── + +def run(vectors_path: str) -> None: + data = json.loads(Path(vectors_path).read_text()) + vectors = data["vectors"] + + passed = 0 + total = len(vectors) + + for v in vectors: + vid = v["id"] + checker = CHECKERS.get(vid) + if not checker: + print(f"SKIP {vid:35s} (no checker)") + total -= 1 + continue + + outcome, reason = checker(v) + + expected_outcome = v.get("expected_outcome", "VALID") + expected_reason = v.get("failure_reason") # None for VALID cases + + outcome_ok = outcome == expected_outcome + reason_ok = expected_reason is None or reason == expected_reason + ok = outcome_ok and reason_ok + + if ok: + passed += 1 + print(f"PASS {vid:35s} {outcome} {reason or ''}") + else: + detail = [] + if not outcome_ok: + detail.append(f"outcome: expected {expected_outcome!r} got {outcome!r}") + if not reason_ok: + detail.append(f"reason: expected {expected_reason!r} got {reason!r}") + print(f"FAIL {vid:35s} {outcome} — {'; '.join(detail)}") + + print(f"\n{passed}/{total} assertions passed") + sys.exit(0 if passed == total else 1) + + +if __name__ == "__main__": + path = sys.argv[1] if len(sys.argv) > 1 else "moltrust-aae-delegation-narrowing.json" + run(path) From 47adfda6766f5ca245394e04db43d2663b79f02c Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 3 Apr 2026 03:27:29 -0400 Subject: [PATCH 3/3] ci: add AAE delegation conformance workflow --- .github/workflows/ci-aae-delegation.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/ci-aae-delegation.yml diff --git a/.github/workflows/ci-aae-delegation.yml b/.github/workflows/ci-aae-delegation.yml new file mode 100644 index 0000000..7063a94 --- /dev/null +++ b/.github/workflows/ci-aae-delegation.yml @@ -0,0 +1,22 @@ +name: AAE Delegation Narrowing Conformance + +on: + pull_request: + paths: + - 'conformance/moltrust-aae-delegation-narrowing.json' + - 'conformance/run_aae_delegation.py' + push: + branches: [main] + +jobs: + conformance: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Run delegation narrowing conformance suite + run: | + python3 conformance/run_aae_delegation.py \ + conformance/moltrust-aae-delegation-narrowing.json