Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci-aae-delegation.yml
Original file line number Diff line number Diff line change
@@ -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
106 changes: 106 additions & 0 deletions conformance/moltrust-aae-delegation-narrowing.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
150 changes: 150 additions & 0 deletions conformance/run_aae_delegation.py
Original file line number Diff line number Diff line change
@@ -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)