//! Regenerate the policy-receipt conformance vectors. //! //! Three canonical scenarios: //! 1. Allow with named rule (filesystem read against `read-workspace`). //! 2. Explicit deny (filesystem read against `no-secrets`). //! 3. No-matching-allow (network connect that no rule allows). //! //! For each, the producer emits the canonical signed-core CBOR hex and //! the COSE_Sign1 envelope hex. Python + Go reference verifiers MUST //! reproduce both byte-for-byte against the same test keys. //! //! Output goes to `conformance/vectors-policy.json` when an argument is //! passed; otherwise prints to stdout for inspection. //! //! Run: //! cargo run -p jouleclaw-policy-receipt --example gen_policy_vectors \ //! -- crates/jouleclaw-policy-receipt/conformance/vectors-policy.json use ed25519_dalek::SigningKey; use jouleclaw_policy_receipt::{ canonical_signed_core, canonical_wire, cose_sign1_envelope, PolicyActionWire, PolicyDecisionWire, PolicyReceipt, }; use serde_json::{json, Value}; use std::fs; const STEWARD: &str = "did:keri:EJ9testStewardAID000000000000000"; const ENGINE_ID: &str = "engine-A"; const CLOSED_AT: &str = "2026-06-02T12:00:00Z"; /// Same Ed25519 seed JCR-1 uses for its v1 + v1.1 cascade vectors — /// readers cross-verify both receipt families against the same key. const SIGNING_SEED_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; fn key() -> SigningKey { let mut seed = [0u8; 32]; let bytes = hex_decode(SIGNING_SEED_HEX); seed.copy_from_slice(&bytes[..32]); SigningKey::from_bytes(&seed) } fn hex_decode(s: &str) -> Vec { let mut out = Vec::with_capacity(s.len() / 2); for chunk in s.as_bytes().chunks(2) { let hi = ascii_hex(chunk[0]); let lo = ascii_hex(chunk[1]); out.push((hi << 4) | lo); } out } fn ascii_hex(c: u8) -> u8 { match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, b'A'..=b'F' => c - b'A' + 10, _ => 0, } } fn hex_encode(bytes: &[u8]) -> String { let mut s = String::with_capacity(bytes.len() * 2); for b in bytes { s.push_str(&format!("{b:02x}")); } s } fn receipt( n: u64, audit_seq: u64, decision: PolicyDecisionWire, action: PolicyActionWire, agent: &str, ) -> PolicyReceipt { // Conformance vectors stay byte-equal: @context is NOT auto-set // here so the frozen wire JSON matches what the spec shipped. PolicyReceipt { context: None, jc_receipt: "policy/1".to_string(), id: format!("polrcpt-{n:04}"), closed_at: CLOSED_AT.to_string(), steward: STEWARD.to_string(), engine_id: ENGINE_ID.to_string(), audit_sequence: audit_seq, policy_version: 1, // The first three vectors carry no digest, which is what keeps their // signed core byte-identical to the shipped `policy/1` bytes. Vector 4 // carries one. policy_digest: None, agent: agent.to_string(), action, decision, } } fn make_vector( n: u64, description: &str, receipt: PolicyReceipt, key: &SigningKey, ) -> Value { let wire = canonical_wire(&receipt); let core = canonical_signed_core(&receipt).expect("signed core"); let envelope = cose_sign1_envelope(&receipt, key).expect("envelope"); json!({ "n": n, "description": description, "receipt": wire, "signed_core_cbor_hex": hex_encode(&core), "cose_sign1_hex": hex_encode(&envelope), }) } fn main() { let key = key(); let public_key = ed25519_dalek::VerifyingKey::from(&key).to_bytes(); let v1 = make_vector( 1, "Allow — filesystem read of a workspace file under the read-workspace rule.", receipt( 1, 1, PolicyDecisionWire::Allow { rule: "read-workspace".to_string(), }, PolicyActionWire::Filesystem { path: "/workspace/notes.md".to_string(), mode: "read".to_string(), }, "did:key:zAlpha", ), &key, ); let v2 = make_vector( 2, "Explicit deny — filesystem read of a workspace secret shadowed by the no-secrets rule.", receipt( 2, 2, PolicyDecisionWire::Deny { reason: "explicit_deny".to_string(), rule: Some("no-secrets".to_string()), }, PolicyActionWire::Filesystem { path: "/workspace/.secrets/k.pem".to_string(), mode: "read".to_string(), }, "did:key:zAlpha", ), &key, ); let v3 = make_vector( 3, "No-matching-allow (deny-by-default) — network connect against an empty policy.", receipt( 3, 3, PolicyDecisionWire::Deny { reason: "no_matching_allow".to_string(), rule: None, }, PolicyActionWire::Network { host: "api.example.com".to_string(), port: 443, method: "connect".to_string(), }, "did:key:zBeta", ), &key, ); // Vector 4 — the policy binding. Same decision as vector 1, with the // digest of the rules that made it. Its signed core therefore differs from // vector 1's by exactly that key, which is the additivity claim made // checkable: vectors 1-3 keep the bytes they shipped with, and a verifier // that encodes the published map needs no change to handle this one. let mut bound = receipt( 4, 4, PolicyDecisionWire::Allow { rule: "read-workspace".to_string() }, PolicyActionWire::Filesystem { path: "/workspace/notes.md".to_string(), mode: "read".to_string(), }, "did:key:zAlpha", ); // The digest of a real policy, not a literal: a hand-written constant here // would not detect a change to the rendering it is supposed to pin. let policy = jouleclaw_policy::Policy::new(vec![jouleclaw_policy::Rule { name: "read-workspace".to_string(), effect: jouleclaw_policy::Effect::Allow, agents: jouleclaw_policy::AgentScope::one_of(["did:key:zAlpha"]), action: jouleclaw_policy::ActionPredicate::Filesystem { path_glob: "/workspace/**".to_string(), modes: [jouleclaw_policy::FsMode::Read].into_iter().collect(), }, }]); bound.policy_digest = Some(policy.digest_hex()); let v4 = make_vector( 4, "Policy-bound allow — same decision as vector 1, with the digest of the \ rules that produced it. Vectors 1-3 omit the field and keep the signed \ core they shipped with.", bound, &key, ); let doc = json!({ "spec": "JouleClaw Policy Receipt v1", "schema_id": "policy/1", "canonical_cbor": "RFC 8949 4.2.1 core deterministic (length-first key order)", "envelope": "COSE_Sign1 (RFC 9052), EdDSA/Ed25519", "vector_count": 4, "test_key": { "ed25519_seed_hex": SIGNING_SEED_HEX, "ed25519_public_raw_hex": hex_encode(&public_key), "steward_kid": STEWARD, "cose_alg": "EdDSA (-8)", "note": "TEST KEY ONLY. Same seed as JCR-1 conformance/test-keys.json." }, "vectors": [v1, v2, v3, v4] }); let pretty = serde_json::to_string_pretty(&doc).expect("pretty json"); match std::env::args().nth(1) { Some(path) => { fs::write(&path, format!("{pretty}\n")).expect("write"); eprintln!("wrote {path}"); } None => println!("{pretty}"), } }