diff --git a/financial-services/README.md b/financial-services/README.md index a52b499..8f1bb32 100644 --- a/financial-services/README.md +++ b/financial-services/README.md @@ -124,7 +124,20 @@ python financial-services/agent/credit_risk_agent.py --amount-eur 750000 ## The Cedar policy -`policy/allow.cedar` — the escalation rule: +`policy/allow.cedar` has no catch-all permit: each tool is explicitly permitted only for the `credit-risk-analyst` workflow (declared by the agent via `_cmcp.workflow_id`). Wrong workflow, missing workflow, or an unlisted action is denied by Cedar's default-deny: + +```cedar +permit ( + principal, + action == Action::"Finance.documentReader", + resource +) when { + context has workflow_id && + context.workflow_id == "credit-risk-analyst" +}; +``` + +On top of the workflow-scoped permits, the escalation rule: ```cedar @id("large-exposure-hitl") diff --git a/financial-services/policy/allow.cedar b/financial-services/policy/allow.cedar index 03f54f7..517f47d 100644 --- a/financial-services/policy/allow.cedar +++ b/financial-services/policy/allow.cedar @@ -5,12 +5,44 @@ // Action names follow the cMCP convention: tool_name converted to // PascalCase per underscore segment, e.g. finance.risk_report_writer // becomes Action::"Finance.riskReportWriter". +// +// There is deliberately NO catch-all permit: anything not explicitly +// permitted below is denied (Cedar default-deny). The workflow_id is +// supplied by the agent via the _cmcp request metadata; calls without +// it are denied. + +// Rule 1: the credit-risk-analyst workflow may read client documents. +permit ( + principal, + action == Action::"Finance.documentReader", + resource +) when { + context has workflow_id && + context.workflow_id == "credit-risk-analyst" +}; + +// Rule 2: the credit-risk-analyst workflow may query credit bureaus. +permit ( + principal, + action == Action::"Finance.creditScoreLookup", + resource +) when { + context has workflow_id && + context.workflow_id == "credit-risk-analyst" +}; -// Rule 1: the attested catalog is the allow-list; permit catalog tools and -// let the forbid rules below carry the compliance constraints. -permit (principal, action, resource); +// Rule 3: the credit-risk-analyst workflow may write risk reports. +// Subject to the large-exposure forbid below. +permit ( + principal, + action == Action::"Finance.riskReportWriter", + resource +) when { + context has workflow_id && + context.workflow_id == "credit-risk-analyst" +}; -// Rule 2: large-exposure escalation (MiFID II Art. 25). +// Rule 4: large-exposure escalation (MiFID II Art. 25). // Any risk report above 500,000 EUR requires a human reviewer. The // annotations are returned to the caller as structured advice on deny. @id("large-exposure-hitl") @@ -26,7 +58,7 @@ forbid ( context.arguments.amount_eur > 500000 }; -// Rule 3: confidential financial data may only flow through an attested +// Rule 5: confidential financial data may only flow through an attested // runtime. attestation_platform is supplied by the gateway from the TEE // attestation report; "unknown" means no attestation evidence is present. @id("require-attested-runtime") diff --git a/financial-services/trace-output/example-trust-record.json b/financial-services/trace-output/example-trust-record.json index 2336913..d6cc5cd 100644 --- a/financial-services/trace-output/example-trust-record.json +++ b/financial-services/trace-output/example-trust-record.json @@ -2,45 +2,46 @@ "cmcp_version": "1.0", "trace": { "eat_profile": "tag:agentrust.io,2026:trace-v0.1", - "iat": 1781144717, - "subject": "spiffe://cmcp.gateway/session/27b1aaa2-6972-4224-951e-80cb9c89003a", + "iat": 1781193684, + "subject": "spiffe://cmcp.gateway/session/7bab4703-706e-4f2e-80b3-c3611832cda8", "runtime": { "platform": "tpm2", "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "firmware_version": "software-only-dev-mode" }, "policy": { - "bundle_hash": "sha256:eb87240ced9e589f93d4851f404f892690d2067039b90a4806a676e426bd674f", + "bundle_hash": "sha256:9650dec5dec7afbc8608f0bdb29b9266c1f05990be603255e806479a76e9e23e", "enforcement_mode": "enforce", "version": "credit-risk-v4.2" }, "data_class": "confidential", "tool_transcript": { - "hash": "sha256:f06d8771565a018e3f301cbe59e312cd9684eb2b3c84a7aa2d4a85f45075a6a3", - "call_count": 3 + "hash": "sha256:bf92310550233e5363441d0cc3b4158480e86e1bee4b958123bb39e7c63be262", + "call_count": 4 }, "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", - "x": "uYElaypX9Nuhfdf-TXDGOMof81zP8HKJX2z6iWmAPKU", - "kid": "cmcp-b981256b" + "x": "ZlmbQ6r1FqVml6LsKDifDR4grPSBhhRYZMqc8o0DdAc", + "kid": "cmcp-66599b43" } } }, "gateway": { - "session_id": "27b1aaa2-6972-4224-951e-80cb9c89003a", + "session_id": "7bab4703-706e-4f2e-80b3-c3611832cda8", "gateway_version": "unknown", - "sequence_number": 1, + "sequence_number": 3, + "prev_claim_hash": "sha256:64a2493a624c547e12a5395a3bb1d23c8b42e077277bb88732ab5c7209da7e56", "audit_chain": { - "root": "e09e8744127cd61f36bed71052928f793d9be22f4f3e3b498413816e9f8cffc2", - "tip": "f06d8771565a018e3f301cbe59e312cd9684eb2b3c84a7aa2d4a85f45075a6a3", - "length": 5 + "root": "17e9e977e3a1307f2a738ef1dedc3a879be3065ef9111b06b3e0644517f59457", + "tip": "bf92310550233e5363441d0cc3b4158480e86e1bee4b958123bb39e7c63be262", + "length": 6 }, "call_summary": { - "tool_calls_total": 3, + "tool_calls_total": 4, "tool_calls_allowed": 3, - "tool_calls_denied": 0, + "tool_calls_denied": 1, "tool_calls_faulted": 0, "tools_invoked": [ "finance.credit_score_lookup", @@ -58,9 +59,9 @@ { "from_domain": "pii", "to_domain": "internal", - "call_id": "e9608498-ac0b-4c19-ba67-4d9519c72dde", + "call_id": "1fb8e8c8-d709-4fda-914b-81bbd9a872db", "tool_name": "finance.risk_report_writer", - "sequence_number": 2 + "sequence_number": 3 } ], "edges_represent": "Edges represent temporal adjacency (call order), not data provenance. A -> B means B was called immediately after A within this session." @@ -70,12 +71,12 @@ "hash": "sha256:02640e5bd370cd9e2d383f07dcab8d2d3c5e9e0155ee14c56556d6e7645d86ca", "drift_detected": false }, - "attestation_generated_at": "2026-06-11T00:42:59.157791+00:00", + "attestation_generated_at": "2026-06-11T16:01:05.969369+00:00", "attestation_validity_seconds": 86400, "attestation_stale": false, "catalog_exceptions": [], "call_log_summary": { - "total_calls": 3, + "total_calls": 4, "tools_called": [ "finance.document_reader", "finance.credit_score_lookup", @@ -84,5 +85,5 @@ "suspicious_sequences_detected": 0 } }, - "signature": "wBYpJ89EcFwtJsS4kp0wyVZfGIKZMcazJB4aMQ36mIuhAKtsO04v9KIGlCIk5k818u1PyDqP1gI6_aNDTxgkDQ" + "signature": "Eb4r3WoFBeJ86Zz2DNQPnCCLG11BegMgod-Kjyf6J9V3uKElF5ZiY-SJ5MQwj24EboeXFsU1lKf2Z29ui6FbBA" } diff --git a/healthcare/README.md b/healthcare/README.md index 91a051f..61db13e 100644 --- a/healthcare/README.md +++ b/healthcare/README.md @@ -120,7 +120,20 @@ python healthcare/agent/clinical_decision_agent.py --trigger-hitl ## The Cedar policy -`policy/allow.cedar` — two enforcement rules over a base permit. Annotations on a `forbid` are returned to the caller as structured advice when that rule causes a deny: +`policy/allow.cedar` has no catch-all permit: each EHR tool is explicitly permitted only for the `clinical-decision-support` workflow (declared by the agent via `_cmcp.workflow_id`), and anything else — wrong workflow, missing workflow, unlisted action — is denied by Cedar's default-deny: + +```cedar +permit ( + principal, + action == Action::"Ehr.patientRecordLookup", + resource +) when { + context has workflow_id && + context.workflow_id == "clinical-decision-support" +}; +``` + +On top of the workflow-scoped permits sit two forbid rules. Annotations on a `forbid` are returned to the caller as structured advice when that rule causes a deny: ```cedar @id("hitl-high-risk") diff --git a/healthcare/policy/allow.cedar b/healthcare/policy/allow.cedar index 12075a3..abb8d23 100644 --- a/healthcare/policy/allow.cedar +++ b/healthcare/policy/allow.cedar @@ -5,12 +5,44 @@ // Action names follow the cMCP convention: tool_name converted to // PascalCase per underscore segment, e.g. ehr.treatment_plan_writer // becomes Action::"Ehr.treatmentPlanWriter". +// +// There is deliberately NO catch-all permit: anything not explicitly +// permitted below is denied (Cedar default-deny). The workflow_id is +// supplied by the agent via the _cmcp request metadata; calls without +// it are denied. + +// Rule 1: the clinical-decision-support workflow may look up patient records. +permit ( + principal, + action == Action::"Ehr.patientRecordLookup", + resource +) when { + context has workflow_id && + context.workflow_id == "clinical-decision-support" +}; + +// Rule 2: the clinical-decision-support workflow may run differential diagnosis. +permit ( + principal, + action == Action::"Ehr.clinicalDecisionSupport", + resource +) when { + context has workflow_id && + context.workflow_id == "clinical-decision-support" +}; -// Rule 1: the attested catalog is the allow-list; permit catalog tools and -// let the forbid rules below carry the compliance constraints. -permit (principal, action, resource); +// Rule 3: the clinical-decision-support workflow may write treatment plans. +// Subject to the HITL forbid below. +permit ( + principal, + action == Action::"Ehr.treatmentPlanWriter", + resource +) when { + context has workflow_id && + context.workflow_id == "clinical-decision-support" +}; -// Rule 2: EU AI Act Article 14 human oversight. Treatment plan writes for +// Rule 4: EU AI Act Article 14 human oversight. Treatment plan writes for // high-risk patients are blocked until an attending physician approves. // The annotations are returned to the caller as structured advice on deny. @id("hitl-high-risk") @@ -26,7 +58,7 @@ forbid ( context.arguments.patient_risk_category == "high" }; -// Rule 3: HIPAA PHI may only flow through an attested runtime. +// Rule 5: HIPAA PHI may only flow through an attested runtime. @id("require-attested-runtime") @reason("attested-runtime-required") @regulation("hipaa-164-312") diff --git a/healthcare/trace-output/example-trust-record.json b/healthcare/trace-output/example-trust-record.json index 014fb08..396ccc9 100644 --- a/healthcare/trace-output/example-trust-record.json +++ b/healthcare/trace-output/example-trust-record.json @@ -2,39 +2,40 @@ "cmcp_version": "1.0", "trace": { "eat_profile": "tag:agentrust.io,2026:trace-v0.1", - "iat": 1781138573, - "subject": "spiffe://cmcp.gateway/session/ec7cb161-d4a6-457d-b7e9-ff4e0f9a64f7", + "iat": 1781193632, + "subject": "spiffe://cmcp.gateway/session/f2574e45-58b2-480f-919d-50aa65d07d31", "runtime": { "platform": "tpm2", "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "firmware_version": "software-only-dev-mode" }, "policy": { - "bundle_hash": "sha256:746a35668e06c8b6a6a84aa0a06b603c26b286c3e9ec65b2562e1555077be936", + "bundle_hash": "sha256:bab3158a32ccadfecfd69d5edcc4078fcd1f2a0814c4f33069e352754ed69d9c", "enforcement_mode": "enforce", "version": "clinical-hipaa-v2.1" }, "data_class": "confidential", "tool_transcript": { - "hash": "sha256:6439db6d0d0b4aaab9fb6fbe81bd26ed75c42aeb4198cc980ce5892725fb27c0", + "hash": "sha256:237bef498e84cd658ca00a7673bb114192a110940ed821b44ae4f8c81cf247f5", "call_count": 3 }, "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", - "x": "i8Ia9guwYclt5cUUMRPcB6_EXiXOMN3iv8A0l8-CPQE", - "kid": "cmcp-8bc21af6" + "x": "g2zyVqHtTV9CtQBkS4PS971CMd1ilK0CtihOpuy9v14", + "kid": "cmcp-836cf256" } } }, "gateway": { - "session_id": "ec7cb161-d4a6-457d-b7e9-ff4e0f9a64f7", + "session_id": "f2574e45-58b2-480f-919d-50aa65d07d31", "gateway_version": "unknown", - "sequence_number": 1, + "sequence_number": 3, + "prev_claim_hash": "sha256:dc612793436ff47741f94d67fa3aebf3d126741990cf48a4a29ec7baf63db9f9", "audit_chain": { - "root": "b6e5d7adff480101d3a466d6ab051509be22cf377c96447c43b22ca9bf27861b", - "tip": "6439db6d0d0b4aaab9fb6fbe81bd26ed75c42aeb4198cc980ce5892725fb27c0", + "root": "43656adea27dbd3cd2adefbd0364a70172938185cf6bb61397d21c6bcf4b1761", + "tip": "237bef498e84cd658ca00a7673bb114192a110940ed821b44ae4f8c81cf247f5", "length": 5 }, "call_summary": { @@ -60,7 +61,7 @@ "hash": "sha256:0c8009e105049f68d0f7054e254192e43eec7d567cd1fa45542450d268369647", "drift_detected": false }, - "attestation_generated_at": "2026-06-11T00:42:42.456843+00:00", + "attestation_generated_at": "2026-06-11T16:00:17.584902+00:00", "attestation_validity_seconds": 86400, "attestation_stale": false, "catalog_exceptions": [], @@ -74,5 +75,5 @@ "suspicious_sequences_detected": 0 } }, - "signature": "2fHMRi8zIDMqT292wfhJ7RCPu56L5VgdwRw5bJebJ0znqjUYDUR8ctuFkOimwjgJH9ACJ_k-gS7w25Ha7Dr_AQ" + "signature": "49A-mPB9eY6y5396rtf3-2jn8IsCfvhf5OoRWlBLU9m94MUvK9UYZ6n76dDmqt2F9Bo4xYMe_t7ypuLfgqHhDQ" } diff --git a/startup-tpm/README.md b/startup-tpm/README.md index 2d83d80..695852f 100644 --- a/startup-tpm/README.md +++ b/startup-tpm/README.md @@ -180,7 +180,51 @@ The response is the signed `RuntimeClaim` (see `trace-output/example-trust-recor You can also export the hash-chained audit log for a closed session: ```bash -curl "http://localhost:8443/audit/export?session_id=" | python3 -m json.tool +curl "http://localhost:8443/audit/export?session_id=" > bundle.json +``` + +--- + +## Step 7 — Verify the record (and try to tamper with it) + +Save the claim from Step 6 to `claim.json`, then verify it: + +```bash +cmcp verify claim.json --audit-bundle bundle.json +``` + +``` +[cmcp verify] schema PASS +[cmcp verify] signature PASS +[cmcp verify] attestation_freshness PASS +[cmcp verify] audit_chain PASS +[cmcp verify] audit_bundle PASS (3 entries) +[cmcp verify] RESULT: PASS (verified) +``` + +Now change anything — a counter, a tool name, one character — and verify again: + +```bash +python3 -c " +import json +c = json.load(open('claim.json')) +c['gateway']['call_summary']['tool_calls_total'] += 1 +json.dump(c, open('claim.json', 'w')) +" +cmcp verify claim.json +``` + +``` +[cmcp verify] signature FAIL +[cmcp verify] RESULT: FAIL (partially_verified) +``` + +The signature covers the whole canonical claim, so no field can be altered after the fact. The same holds for the audit bundle: editing any entry breaks both its hash chain and the bundle signature. This is the tamper-evidence guarantee in one command. + +In production, pin the approved hashes so a substituted policy or catalog also fails: + +```bash +cmcp verify claim.json --policy-hash sha256: --catalog-hash sha256: ``` ---