diff --git a/integrations/comply54/.gitignore b/integrations/comply54/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/integrations/comply54/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/integrations/comply54/README.md b/integrations/comply54/README.md new file mode 100644 index 0000000..01e2806 --- /dev/null +++ b/integrations/comply54/README.md @@ -0,0 +1,144 @@ +# comply54 → TRACE Adapter + +Converts a [comply54](https://github.com/comply54/comply54) `ComplianceResult` JSON into a **signed TRACE v0.1 JWT** (Ed25519). + +comply54 evaluates AI agent actions against African regulatory frameworks (NDPA 2023, CBN Transaction Controls, KDPA 2019, POPIA, and 9 other jurisdictions). This adapter maps the compliance decision into the TRACE attestation format so the policy outcome becomes a cryptographically verifiable evidence record. + +## Conformance Level + +**Level 0 (software-only)** — No hardware TEE attestation. The JWT is signed with Ed25519 and carries all required TRACE envelope fields. Runtime fields use software-simulated placeholders; hardware fields are marked `not-attested`. + +| Check | Status | +|-------|--------| +| `eat_profile` = `tag:agentrust.io,2026:trace-v0.1` | ✅ | +| `iat` (integer Unix timestamp) | ✅ | +| `subject` (SPIFFE URI) | ✅ | +| `cnf.jwk` with Ed25519 public key | ✅ | +| `policy.bundle_hash` (SHA-256 of sorted pack IDs) | ✅ | +| `appraisal.status` mapped from comply54 decision | ✅ | +| Ed25519 signature binding | ✅ | +| Hardware TEE measurement | ❌ Level 0 — placeholder | + +## Decision → Appraisal Mapping + +| comply54 `overall` | TRACE `appraisal.status` | +|--------------------|--------------------------| +| `allow` | `affirming` | +| `audit` | `advisory` | +| `escalate` | `warning` | +| `deny` | `contraindicated` | + +## Usage + +**1. Install dependencies** + +```bash +pip install comply54 PyJWT cryptography +``` + +**2. Generate a comply54 ComplianceResult and save as JSON** + +```python +from comply54 import NigeriaFintechCompliance +import json + +compliance = NigeriaFintechCompliance() +result = compliance.check( + "transfer_funds", + {"amount": 15_000_000, "currency": "NGN"}, + context={"kyc_tier": 3}, +) +with open("result.json", "w") as f: + json.dump(result.model_dump(mode="json"), f, default=str) +``` + +**3. Convert to TRACE JWT** + +```bash +python src/comply54_to_trace.py result.json \ + --agent-id payments-agent \ + --model anthropic/claude-sonnet-4-6 +``` + +Output: `claim.jwt` (signed JWT, compact format) + printed to stdout. + +**4. Inspect the JWT payload** + +```bash +python -c " +import jwt +payload = jwt.decode(open('claim.jwt').read(), options={'verify_signature': False}) +print('eat_profile:', payload['eat_profile']) +print('appraisal: ', payload['appraisal']) +print('comply54: ', payload['comply54']) +" +``` + +**5. Run tests** + +```bash +pip install pytest +python -m pytest integrations/comply54/tests/ -v +``` + +All 20 tests should pass. + +## Key Management + +By default a fresh Ed25519 key is generated per run (suitable for testing and CI). + +For persistent keys, set `TRACE_PRIVATE_KEY_PEM`: + +```bash +# Generate a persistent key +python -c " +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption +key = Ed25519PrivateKey.generate() +print(key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()).decode()) +" > signing.pem + +export TRACE_PRIVATE_KEY_PEM="$(cat signing.pem)" +python src/comply54_to_trace.py result.json +``` + +## What is verified + +- `eat_profile` is exactly `tag:agentrust.io,2026:trace-v0.1` +- `policy.bundle_hash` is `sha256:` + hex(SHA-256(JSON-sorted pack IDs)) — reproducible from the same comply54 result +- `appraisal.status` matches the comply54 decision using the mapping table above +- `comply54.audit_id` matches the `audit_id` from the source ComplianceResult +- JWT is signed with Ed25519; public key is embedded in `cnf.jwk` + +## Limitations + +- Hardware attestation fields (`runtime.measurement`, `build_provenance.digest`, `model.weights_digest`) are software-simulated placeholders. Level 1/2 requires running comply54 inside a TEE (AMD SEV-SNP, Intel TDX, or equivalent). +- `transparency` is empty — no SCITT log anchor at Level 0. +- Model identity fields reflect what the caller passes via `--model`. comply54 evaluates policy against the agent's action; it does not independently verify which model ran. + +## comply54 extension claims + +The JWT carries a non-standard `comply54` object with: + +```json +{ + "comply54": { + "audit_id": "uuid", + "overall": "deny", + "jurisdictions": ["NG", "KE", "ZA"], + "packs_evaluated": ["nigeria/cbn", "nigeria/ndpa", "universal/pii-leakage"], + "violations": [ + { + "pack": "nigeria/cbn", + "regulation": "CBN Transaction Controls", + "action": "deny", + "messages": ["CBN NIP cap exceeded: ₦15,000,000 > ₦10,000,000 limit"] + } + ] + } +} +``` + +## Repository + +[comply54/comply54](https://github.com/comply54/comply54) diff --git a/integrations/comply54/integration.yaml b/integrations/comply54/integration.yaml new file mode 100644 index 0000000..c6ca846 --- /dev/null +++ b/integrations/comply54/integration.yaml @@ -0,0 +1,17 @@ +name: comply54 +vendor: comply54 +integrates_with: + - trace +description: >- + Converts a comply54 ComplianceResult into a signed TRACE v0.1 JWT, + mapping African regulatory policy decisions to verifiable attestation claims. +maintainer: + github: kingztech2019 + email: oluwajuwon.falore@sagegreytech.com +repository: https://github.com/comply54/comply54 +homepage: https://comply54.io +license: Apache-2.0 +tier: community +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.2.0" diff --git a/integrations/comply54/requirements.txt b/integrations/comply54/requirements.txt new file mode 100644 index 0000000..8a1b154 --- /dev/null +++ b/integrations/comply54/requirements.txt @@ -0,0 +1,2 @@ +PyJWT>=2.8.0 +cryptography>=42.0.0 diff --git a/integrations/comply54/src/__init__.py b/integrations/comply54/src/__init__.py new file mode 100644 index 0000000..10a2305 --- /dev/null +++ b/integrations/comply54/src/__init__.py @@ -0,0 +1,3 @@ +from .comply54_to_trace import comply54_to_trace_payload, load_or_generate_key, private_key_to_jwk + +__all__ = ["comply54_to_trace_payload", "load_or_generate_key", "private_key_to_jwk"] diff --git a/integrations/comply54/src/comply54_to_trace.py b/integrations/comply54/src/comply54_to_trace.py new file mode 100644 index 0000000..eab8e15 --- /dev/null +++ b/integrations/comply54/src/comply54_to_trace.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +comply54 -> TRACE v0.1 Adapter +Reads a comply54 ComplianceResult JSON and emits a signed TRACE v0.1 JWT (Ed25519). +Conforms to TRACE spec at Level 0 (software-only; no hardware TEE attestation). + +Usage: + python comply54_to_trace.py result.json + python comply54_to_trace.py result.json --agent-id payments-agent --model anthropic/claude-sonnet-4-6 + +The JWT is written to claim.jwt and printed to stdout. +Set TRACE_PRIVATE_KEY_PEM to supply a persistent Ed25519 key; otherwise a +fresh key is generated per run (suitable for testing). +""" + +import argparse +import base64 +import hashlib +import json +import os +import sys +import time +from pathlib import Path + +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + +# ── comply54 appraisal → TRACE appraisal status ─────────────────────────────── + +_APPRAISAL_MAP = { + "allow": "affirming", + "audit": "advisory", + "escalate": "warning", + "deny": "contraindicated", +} + + +# ── Key helpers ─────────────────────────────────────────────────────────────── + +def load_or_generate_key() -> Ed25519PrivateKey: + pem = os.environ.get("TRACE_PRIVATE_KEY_PEM") + if pem: + return serialization.load_pem_private_key(pem.encode(), password=None) + return Ed25519PrivateKey.generate() + + +def private_key_to_jwk(key: Ed25519PrivateKey) -> dict: + raw = key.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + return { + "kty": "OKP", + "crv": "Ed25519", + "x": base64.urlsafe_b64encode(raw).decode().rstrip("="), + } + + +# ── Mapping ─────────────────────────────────────────────────────────────────── + +def comply54_to_trace_payload(result: dict, agent_id: str, model: str) -> dict: + overall = result.get("overall", "deny") + audit_id = result.get("audit_id", "unknown") + decisions = result.get("decisions", []) + + # Policy bundle hash: deterministic fingerprint of the pack IDs that ran + pack_ids = sorted({d.get("pack", "") for d in decisions if d.get("pack")}) + bundle_input = json.dumps(pack_ids, sort_keys=True).encode() + bundle_hash = f"sha256:{hashlib.sha256(bundle_input).hexdigest()}" + + # Jurisdictions covered by this evaluation + jurisdictions = sorted({d.get("jurisdiction", "") for d in decisions if d.get("jurisdiction")}) + + # Violations summary (embedded as a non-standard extension claim) + violations = [ + { + "pack": d.get("pack"), + "regulation": d.get("regulation"), + "action": d.get("action"), + "messages": d.get("messages", [])[:1], # first message only for compactness + } + for d in decisions + if d.get("action") != "allow" + ] + + appraisal_status = _APPRAISAL_MAP.get(overall, "contraindicated") + + provider, _, model_id = model.partition("/") + + return { + # ── Required TRACE EAT envelope ────────────────────────────────────── + "eat_profile": "tag:agentrust.io,2026:trace-v0.1", + "iat": int(time.time()), + "subject": f"spiffe://comply54.io/agent/{agent_id}", + + # ── Model identity (the agent being governed, not comply54) ────────── + "model": { + "provider": provider or "unknown", + "model_id": model_id or model, + "version": "unknown", + "weights_digest": "sha256:not-attested", + }, + + # ── Runtime (software-only — Level 0) ──────────────────────────────── + "runtime": { + "platform": "software-only", + "measurement": "sha384:" + "0" * 96, + "rim_uri": "https://github.com/comply54/comply54", + }, + + # ── Policy evidence: comply54 pack bundle ──────────────────────────── + "policy": { + "bundle_hash": bundle_hash, + "enforcement_mode": "enforce", + "version": "0.1.0", + }, + + # ── Data classification ─────────────────────────────────────────────── + "data_class": "confidential", + + # ── Build provenance (software-only) ───────────────────────────────── + "build_provenance": { + "slsa_level": 0, + "builder": "https://github.com/comply54/comply54", + "digest": "sha256:not-attested", + }, + + # ── Appraisal: the comply54 decision ───────────────────────────────── + "appraisal": { + "status": appraisal_status, + "verifier": "https://github.com/comply54/comply54", + "policy_ref": f"comply54-v0.1.0/{','.join(pack_ids)}", + }, + + # ── Transparency (no log anchor at Level 0) ─────────────────────────── + "transparency": "", + + # ── comply54-specific extension claims ─────────────────────────────── + "comply54": { + "audit_id": audit_id, + "overall": overall, + "jurisdictions": jurisdictions, + "packs_evaluated": pack_ids, + "violations": violations, + }, + } + + +# ── CLI ─────────────────────────────────────────────────────────────────────── + +def main() -> None: + parser = argparse.ArgumentParser(description="Convert comply54 ComplianceResult to TRACE v0.1 JWT") + parser.add_argument("result_json", help="Path to comply54 ComplianceResult JSON file") + parser.add_argument("--agent-id", default="fintech-agent", help="Agent SPIFFE identity suffix") + parser.add_argument("--model", default="unknown/unknown", help="Model in provider/model-id format") + parser.add_argument("--out", default="claim.jwt", help="Output JWT file (default: claim.jwt)") + args = parser.parse_args() + + with open(args.result_json) as f: + result = json.load(f) + + payload = comply54_to_trace_payload(result, args.agent_id, args.model) + key = load_or_generate_key() + payload["cnf"] = {"jwk": private_key_to_jwk(key)} + + token = jwt.encode(payload, key, algorithm="EdDSA", headers={"alg": "EdDSA", "typ": "JWT"}) + + Path(args.out).write_text(token) + print(token) + + +if __name__ == "__main__": + main() diff --git a/integrations/comply54/tests/__init__.py b/integrations/comply54/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integrations/comply54/tests/test_comply54_to_trace.py b/integrations/comply54/tests/test_comply54_to_trace.py new file mode 100644 index 0000000..b35a6c6 --- /dev/null +++ b/integrations/comply54/tests/test_comply54_to_trace.py @@ -0,0 +1,181 @@ +""" +Tests for comply54 -> TRACE v0.1 adapter. +Run: pip install -r requirements.txt && python -m pytest tests/ -v +""" + +import json +import base64 +import pytest +import jwt as pyjwt +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from comply54_to_trace import comply54_to_trace_payload, load_or_generate_key, private_key_to_jwk + + +# ── Fixtures ────────────────────────────────────────────────────────────────── + +ALLOW_RESULT = { + "overall": "allow", + "audit_id": "test-audit-001", + "decisions": [ + {"pack": "nigeria/cbn", "regulation": "CBN Transaction Controls", + "jurisdiction": "NG", "action": "allow", "messages": []}, + {"pack": "nigeria/ndpa", "regulation": "Nigeria Data Protection Act 2023", + "jurisdiction": "NG", "action": "allow", "messages": []}, + {"pack": "universal/pii-leakage", "regulation": "OWASP LLM06", + "jurisdiction": "UNIVERSAL", "action": "allow", "messages": []}, + ], +} + +DENY_RESULT = { + "overall": "deny", + "audit_id": "test-audit-002", + "decisions": [ + {"pack": "nigeria/cbn", "regulation": "CBN Transaction Controls", + "jurisdiction": "NG", "action": "deny", + "messages": ["CBN NIP cap exceeded: ₦15,000,000 > ₦10,000,000 limit"]}, + {"pack": "nigeria/ndpa", "regulation": "Nigeria Data Protection Act 2023", + "jurisdiction": "NG", "action": "allow", "messages": []}, + ], +} + +ESCALATE_RESULT = { + "overall": "escalate", + "audit_id": "test-audit-003", + "decisions": [ + {"pack": "nigeria/nfiu-aml", "regulation": "NFIU AML Guidelines", + "jurisdiction": "NG", "action": "escalate", + "messages": ["Currency Transaction Report required: ₦6,000,000 exceeds ₦5,000,000 threshold"]}, + ], +} + +PAN_AFRICAN_RESULT = { + "overall": "deny", + "audit_id": "test-audit-004", + "decisions": [ + {"pack": "nigeria/cbn", "regulation": "CBN Transaction Controls", "jurisdiction": "NG", "action": "deny", "messages": ["CBN NIP cap exceeded"]}, + {"pack": "kenya/kdpa", "regulation": "Kenya Data Protection Act 2019", "jurisdiction": "KE", "action": "allow", "messages": []}, + {"pack": "south-africa/popia", "regulation": "POPIA", "jurisdiction": "ZA", "action": "allow", "messages": []}, + {"pack": "ghana/dpa", "regulation": "Ghana DPA 2012", "jurisdiction": "GH", "action": "allow", "messages": []}, + ], +} + + +# ── Appraisal mapping ───────────────────────────────────────────────────────── + +class TestAppraisalMapping: + def test_allow_maps_to_affirming(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["appraisal"]["status"] == "affirming" + + def test_deny_maps_to_contraindicated(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["appraisal"]["status"] == "contraindicated" + + def test_escalate_maps_to_warning(self): + payload = comply54_to_trace_payload(ESCALATE_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["appraisal"]["status"] == "warning" + + +# ── Required TRACE EAT envelope fields ─────────────────────────────────────── + +class TestTraceEnvelope: + def test_eat_profile_present(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1" + + def test_iat_is_integer(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert isinstance(payload["iat"], int) + assert payload["iat"] > 0 + + def test_subject_contains_agent_id(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "payments-agent", "anthropic/claude-sonnet-4-6") + assert "payments-agent" in payload["subject"] + assert payload["subject"].startswith("spiffe://") + + def test_policy_bundle_hash_is_sha256(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["policy"]["bundle_hash"].startswith("sha256:") + assert len(payload["policy"]["bundle_hash"]) == 71 # "sha256:" + 64 hex chars + + def test_policy_bundle_hash_is_deterministic(self): + p1 = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + p2 = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert p1["policy"]["bundle_hash"] == p2["policy"]["bundle_hash"] + + def test_runtime_platform_is_software_only(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["runtime"]["platform"] == "software-only" + + def test_model_provider_parsed_correctly(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "openai/gpt-4o") + assert payload["model"]["provider"] == "openai" + assert payload["model"]["model_id"] == "gpt-4o" + + +# ── comply54 extension claims ───────────────────────────────────────────────── + +class TestComply54Claims: + def test_audit_id_preserved(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["comply54"]["audit_id"] == "test-audit-002" + + def test_overall_decision_preserved(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert payload["comply54"]["overall"] == "deny" + + def test_jurisdictions_extracted(self): + payload = comply54_to_trace_payload(PAN_AFRICAN_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert "NG" in payload["comply54"]["jurisdictions"] + assert "KE" in payload["comply54"]["jurisdictions"] + assert "ZA" in payload["comply54"]["jurisdictions"] + + def test_packs_evaluated_sorted(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + packs = payload["comply54"]["packs_evaluated"] + assert packs == sorted(packs) + + def test_violations_only_non_allow(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + for v in payload["comply54"]["violations"]: + assert v["action"] != "allow" + + def test_allow_result_has_no_violations(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + assert len(payload["comply54"]["violations"]) == 0 + + +# ── JWT signing ─────────────────────────────────────────────────────────────── + +class TestJWTSigning: + def test_key_generation_returns_ed25519(self): + key = load_or_generate_key() + assert isinstance(key, Ed25519PrivateKey) + + def test_jwk_has_correct_fields(self): + key = load_or_generate_key() + jwk = private_key_to_jwk(key) + assert jwk["kty"] == "OKP" + assert jwk["crv"] == "Ed25519" + assert "x" in jwk + + def test_signed_jwt_is_decodable(self): + payload = comply54_to_trace_payload(DENY_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + key = load_or_generate_key() + payload["cnf"] = {"jwk": private_key_to_jwk(key)} + token = pyjwt.encode(payload, key, algorithm="EdDSA", headers={"alg": "EdDSA", "typ": "JWT"}) + decoded = pyjwt.decode(token, options={"verify_signature": False}) + assert decoded["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1" + assert decoded["appraisal"]["status"] == "contraindicated" + + def test_signed_jwt_has_three_parts(self): + payload = comply54_to_trace_payload(ALLOW_RESULT, "agent-1", "anthropic/claude-sonnet-4-6") + key = load_or_generate_key() + payload["cnf"] = {"jwk": private_key_to_jwk(key)} + token = pyjwt.encode(payload, key, algorithm="EdDSA", headers={"alg": "EdDSA", "typ": "JWT"}) + assert len(token.split(".")) == 3