#![allow(deprecated)]
use std::collections::BTreeMap;
use tenuo::*;
const ISSUED_AT: u64 = 1704067200;
const EXPIRES_AT: u64 = 1704070800;
const FUTURE_ISSUED_AT: u64 = 1893456000;
const FUTURE_EXPIRES_AT: u64 = 1893459600;
const ID_A1: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
];
const ID_A3_L0: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
];
const ID_A3_L1: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11,
];
const ID_A3_L2: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
];
const ID_A5: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50,
];
const ID_A6: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60,
];
const ID_A7: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70,
];
const ID_A10_PARENT: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90,
];
const ID_A10_CHILD: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x91,
];
const ID_A11_PARENT: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92,
];
const ID_A11_CHILD: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x93,
];
const ID_A12_PARENT: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0,
];
const ID_A12_CHILD: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA1,
];
const ID_A13_PARENT: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB0,
];
const ID_A13_CHILD: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB1,
];
const ID_A14: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0,
];
const ID_A15_ISSUER: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0,
];
const ID_A15_CHILD: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD1,
];
#[allow(dead_code)]
const ID_A16: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0,
];
const ID_A17_PARENT: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0,
];
const ID_A17_CHILD: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1,
];
const ID_A18: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18,
];
#[allow(dead_code)]
const ID_A2: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
];
fn control_plane_key() -> SigningKey {
SigningKey::from_bytes(&[0x01; 32])
}
fn orchestrator_key() -> SigningKey {
SigningKey::from_bytes(&[0x02; 32])
}
fn worker_key() -> SigningKey {
SigningKey::from_bytes(&[0x03; 32])
}
fn worker2_key() -> SigningKey {
SigningKey::from_bytes(&[0x04; 32])
}
fn attacker_key() -> SigningKey {
SigningKey::from_bytes(&[0xFF; 32])
}
const TEST_VECTOR_NONCE: [u8; 16] = [
0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8,
];
#[test]
fn test_vector_a1_minimal_execution_warrant() {
let expected_payload_hex = "\
aa00010150019471f8000070008000000000000001020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688210f604820158208139\
770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3940582\
0158208a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b4\
0f6f5c061a65920080071a65920e9008031200";
let expected_sig_hex = "\
4396783e89f37eebfa7d25ad7d61d6cddfbb6c58eade0e9ccc6e28759f1eb56b\
3c03873a6232483d05f766481edf9f85560881aed03b6ef25771285409e6d800";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert("path".to_string(), Constraint::Wildcard(Wildcard));
tools.insert("read_file".to_string(), cs);
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A1),
tools,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant = sign_payload(&payload, &control_plane);
assert_eq!(
hex::encode(warrant.payload_bytes()),
expected_payload_hex,
"A.1 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant.signature().to_bytes()),
expected_sig_hex,
"A.1 signature mismatch"
);
}
#[test]
fn test_vector_a1_authorize_as_of() {
let envelope_hex = "\
83015893aa00010150019471f8000070008000000000000001020003a1697265\
61645f66696c65a16b636f6e73747261696e7473a164706174688210f6048201\
58208139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9\
b39405820158208a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3\
748801b40f6f5c061a65920080071a65920e9008031200820158404396783e89\
f37eebfa7d25ad7d61d6cddfbb6c58eade0e9ccc6e28759f1eb56b3c03873a62\
32483d05f766481edf9f85560881aed03b6ef25771285409e6d800";
let warrant = wire::decode(&hex::decode(envelope_hex).unwrap()).expect("A.1 decode");
let orchestrator = orchestrator_key();
let as_of = (ISSUED_AT + 60) as i64;
let mut args = std::collections::HashMap::new();
args.insert(
"path".to_string(),
ConstraintValue::String("/data/q3.pdf".into()),
);
let pop = warrant
.sign_with_timestamp(&orchestrator, "read_file", &args, Some(as_of))
.unwrap();
let authorizer = Authorizer::new().with_trusted_root(control_plane_key().public_key());
authorizer
.authorize_one_as_of(&warrant, "read_file", &args, Some(&pop), &[], as_of)
.expect("A.1 must allow at issued_at+60");
let now_pop = warrant.sign(&orchestrator, "read_file", &args).unwrap();
let live = authorizer.authorize_one(&warrant, "read_file", &args, Some(&now_pop), &[]);
assert!(live.is_err(), "A.1 must expire against the wall clock");
}
fn warrant_from_hex(payload_hex: &str, sig_hex: &str) -> Warrant {
let payload_bytes = hex::decode(payload_hex).unwrap();
let sig_vec = hex::decode(sig_hex).unwrap();
let sig_arr: [u8; 64] = sig_vec.try_into().expect("64-byte signature");
let payload: payload::WarrantPayload =
ciborium::de::from_reader(payload_bytes.as_slice()).expect("payload");
Warrant {
payload,
signature: Signature::from_bytes(&sig_arr).unwrap(),
payload_bytes,
envelope_version: 1,
}
}
#[test]
fn test_vector_a3_authorize_as_of() {
let l0 = warrant_from_hex(
"aa00010150019471f8000070008000000000000010020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e672f646174612f2a04820158208139770ea87d175f56a35466c34c7ecccb8d\
8a91b4ee37a25df60f5b8fc9b39405820158208a88e3dd7409f195fd52db2d3c\
ba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080071a65920e9008\
031200",
"98bcd71626112aded9d4d1aa728580934d908611ea15fb90a44b4efb00ad5114\
5dbe1c5ee1b2ba5790bc1215bd9805b2b06449b271f5a8fd080564cba2335a09",
);
let l1 = warrant_from_hex(
"ab00010150019471f8000070008000000000000011020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e6f2f646174612f7265706f7274732f2a0482015820ed4928c628d1c2c6eae9\
0338905995612959273a5c63f93636c14614ac8737d105820158208139770ea8\
7d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394061a659200\
80071a65920e9008030998201870185e187918411868182318ef1881189a0818\
e018c5189f18ec18cb185d184b18ae18d418a718eb18ca18ca18290b01184112\
18ce18c518fc18641201",
"a3ec5b753afad510ffa1145ce686f930470976dd93b5da08a6bf26fdaaac60d7\
c3420d5c87021fe63713e06f1a2a60360dea7f3776a0f28da0bb3d42c3319906",
);
let l2 = warrant_from_hex(
"ab00010150019471f8000070008000000000000012020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688201a16576616c756574\
2f646174612f7265706f7274732f71332e7064660482015820ca93ac17051870\
71d67b83c7ff0efe8108e8ec4530575d7726879333dbdabe7c0582015820ed49\
28c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1061a\
65920080071a65920e900803099820184a189418bb18941877181e184e18d418\
4c18c40a18cb187f188b01186418cd18b00818af1894188c18b1189518900618\
3718ff186e189818f9189b1202",
"f47307c756b98144fd4eeac30c157e317a307da7630db619001f531c479128fd\
1997c666baf0d020e8d60619bb8644f79a5a0038836d49b2a1f676fc7ee8d307",
);
let as_of = (ISSUED_AT + 60) as i64;
let mut allow = std::collections::HashMap::new();
allow.insert(
"path".to_string(),
ConstraintValue::String("/data/reports/q3.pdf".into()),
);
let pop = l2
.sign_with_timestamp(&worker2_key(), "read_file", &allow, Some(as_of))
.unwrap();
let authorizer = Authorizer::new().with_trusted_root(control_plane_key().public_key());
authorizer
.check_chain_with_pop_args_as_of(
&[l0, l1, l2],
"read_file",
&allow,
&allow,
Some(&pop),
&[],
as_of,
)
.expect("A.3 must allow the exact leaf path at issued_at+60");
}
#[test]
fn test_vector_a3_level0_chain_root() {
let expected_payload_hex = "\
aa00010150019471f8000070008000000000000010020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e672f646174612f2a04820158208139770ea87d175f56a35466c34c7ecccb8d\
8a91b4ee37a25df60f5b8fc9b39405820158208a88e3dd7409f195fd52db2d3c\
ba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080071a65920e9008\
031200";
let expected_sig_hex = "\
98bcd71626112aded9d4d1aa728580934d908611ea15fb90a44b4efb00ad5114\
5dbe1c5ee1b2ba5790bc1215bd9805b2b06449b271f5a8fd080564cba2335a09";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools.insert("read_file".to_string(), cs);
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L0),
tools,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant = sign_payload(&payload, &control_plane);
assert_eq!(
hex::encode(warrant.payload_bytes()),
expected_payload_hex,
"A.3 Level 0 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant.signature().to_bytes()),
expected_sig_hex,
"A.3 Level 0 signature mismatch"
);
}
#[test]
fn test_vector_a3_level1_first_attenuation() {
let expected_payload_hex = "\
ab00010150019471f8000070008000000000000011020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e6f2f646174612f7265706f7274732f2a0482015820ed4928c628d1c2c6eae9\
0338905995612959273a5c63f93636c14614ac8737d105820158208139770ea8\
7d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394061a659200\
80071a65920e9008030998201870185e187918411868182318ef1881189a0818\
e018c5189f18ec18cb185d184b18ae18d418a718eb18ca18ca18290b01184112\
18ce18c518fc18641201";
let expected_sig_hex = "\
a3ec5b753afad510ffa1145ce686f930470976dd93b5da08a6bf26fdaaac60d7\
c3420d5c87021fe63713e06f1a2a60360dea7f3776a0f28da0bb3d42c3319906";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut tools_l0 = BTreeMap::new();
let mut cs_l0 = ConstraintSet::new();
cs_l0.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_l0.insert("read_file".to_string(), cs_l0);
let payload_l0 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L0),
tools: tools_l0,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l0 = sign_payload(&payload_l0, &control_plane);
let parent_hash_l1 = sha256(warrant_l0.payload_bytes());
let mut tools_l1 = BTreeMap::new();
let mut cs_l1 = ConstraintSet::new();
cs_l1.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_l1.insert("read_file".to_string(), cs_l1);
let payload_l1 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L1),
tools: tools_l1,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash_l1),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l1 = sign_payload(&payload_l1, &orchestrator);
assert_eq!(
hex::encode(warrant_l1.payload_bytes()),
expected_payload_hex,
"A.3 Level 1 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant_l1.signature().to_bytes()),
expected_sig_hex,
"A.3 Level 1 signature mismatch"
);
}
#[test]
fn test_vector_a3_level2_most_restricted() {
let expected_payload_hex = "\
ab00010150019471f8000070008000000000000012020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688201a16576616c756574\
2f646174612f7265706f7274732f71332e7064660482015820ca93ac17051870\
71d67b83c7ff0efe8108e8ec4530575d7726879333dbdabe7c0582015820ed49\
28c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1061a\
65920080071a65920e900803099820184a189418bb18941877181e184e18d418\
4c18c40a18cb187f188b01186418cd18b00818af1894188c18b1189518900618\
3718ff186e189818f9189b1202";
let expected_sig_hex = "\
f47307c756b98144fd4eeac30c157e317a307da7630db619001f531c479128fd\
1997c666baf0d020e8d60619bb8644f79a5a0038836d49b2a1f676fc7ee8d307";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let worker2 = worker2_key();
let mut tools_l0 = BTreeMap::new();
let mut cs_l0 = ConstraintSet::new();
cs_l0.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_l0.insert("read_file".to_string(), cs_l0);
let payload_l0 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L0),
tools: tools_l0,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l0 = sign_payload(&payload_l0, &control_plane);
let parent_hash_l1 = sha256(warrant_l0.payload_bytes());
let mut tools_l1 = BTreeMap::new();
let mut cs_l1 = ConstraintSet::new();
cs_l1.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_l1.insert("read_file".to_string(), cs_l1);
let payload_l1 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L1),
tools: tools_l1,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash_l1),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l1 = sign_payload(&payload_l1, &orchestrator);
let parent_hash_l2 = sha256(warrant_l1.payload_bytes());
let mut tools_l2 = BTreeMap::new();
let mut cs_l2 = ConstraintSet::new();
cs_l2.insert(
"path".to_string(),
Constraint::Exact(Exact::new("/data/reports/q3.pdf")),
);
tools_l2.insert("read_file".to_string(), cs_l2);
let payload_l2 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L2),
tools: tools_l2,
holder: worker2.public_key(),
issuer: worker.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 2,
parent_hash: Some(parent_hash_l2),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l2 = sign_payload(&payload_l2, &worker);
assert_eq!(
hex::encode(warrant_l2.payload_bytes()),
expected_payload_hex,
"A.3 Level 2 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant_l2.signature().to_bytes()),
expected_sig_hex,
"A.3 Level 2 signature mismatch"
);
}
#[test]
fn test_vector_a5_expired_warrant() {
let expected_payload_hex = "\
aa00010150019471f8000070008000000000000050020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688210f604820158208139\
770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3940582\
0158208a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b4\
0f6f5c061a65920080071a6592008108031200";
let expected_sig_hex = "\
13c824cd5c27c5fc1b6c0fd36ed0579d3278a1dd8df2b5e941679e25890f3129\
530dfd1ba49d4691bbb56aa30f4eafdeea15e60a4f20c61c56bcf888404f4f0a";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert("path".to_string(), Constraint::Wildcard(Wildcard));
tools.insert("read_file".to_string(), cs);
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A5),
tools,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: ISSUED_AT + 1, max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant = sign_payload(&payload, &control_plane);
assert_eq!(
hex::encode(warrant.payload_bytes()),
expected_payload_hex,
"A.5 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant.signature().to_bytes()),
expected_sig_hex,
"A.5 signature mismatch"
);
assert!(
warrant.is_expired(),
"A.5 warrant should be expired (TTL was 1 second)"
);
}
#[test]
fn test_vector_a6_proof_of_possession() {
let expected_payload_hex = "\
aa00010150019471f8000070008000000000000060020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688201a16576616c756570\
2f646174612f7265706f72742e7064660482015820ed4928c628d1c2c6eae903\
38905995612959273a5c63f93636c14614ac8737d105820158208a88e3dd7409\
f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080\
071a65920e9008011200";
let expected_sig_hex = "\
3c170967a561d9bf81c4d45398fa6defdddfcb87157bde9e597a7e16abca5c22\
6b31199e57ca87953ce814a178c6e018835c8a24c50afbc4bcdc8d485a9d5a0c";
let expected_pop_sig_hex = "\
84f11618ec5b7234287e3fc1dbb6f8c18de9aab1ad60d8bc3e26ba293814a062\
0cae3be2c96baf7698ef959105231d2b4eee57fa247a56c11170d100e66d6f0a";
let control_plane = control_plane_key();
let worker = worker_key();
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert(
"path".to_string(),
Constraint::Exact(Exact::new("/data/report.pdf")),
);
tools.insert("read_file".to_string(), cs);
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A6),
tools,
holder: worker.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 1,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant = sign_payload(&payload, &control_plane);
assert_eq!(
hex::encode(warrant.payload_bytes()),
expected_payload_hex,
"A.6 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant.signature().to_bytes()),
expected_sig_hex,
"A.6 signature mismatch"
);
const POP_CONTEXT: &[u8] = b"tenuo-pop-v1";
const POP_TIMESTAMP_WINDOW: i64 = (ISSUED_AT as i64 / 30) * 30;
let pop_challenge = (
warrant.id().to_string(),
"read_file".to_string(),
vec![("path".to_string(), "/data/report.pdf".to_string())],
POP_TIMESTAMP_WINDOW,
);
let mut challenge_bytes = Vec::new();
ciborium::ser::into_writer(&pop_challenge, &mut challenge_bytes)
.expect("Failed to serialize challenge");
let mut pop_preimage = Vec::new();
pop_preimage.extend_from_slice(POP_CONTEXT);
pop_preimage.extend_from_slice(&challenge_bytes);
let pop_signature = worker.sign(&pop_preimage);
assert_eq!(
hex::encode(pop_signature.to_bytes()),
expected_pop_sig_hex,
"A.6 PoP signature mismatch"
);
assert!(
worker
.public_key()
.verify(&pop_preimage, &pop_signature)
.is_ok(),
"A.6 PoP signature should verify"
);
}
#[test]
fn test_vector_a2_minimal_issuer_warrant() {
const ID_A2: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02,
];
let expected_payload_hex = "\
ac00010150019471f8000070008000000000000002020103a004820158208139\
770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b3940582\
0158208a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b4\
0f6f5c061a65920080071a65920e9008050b8269726561645f66696c656a7772\
6974655f66696c650d031200";
let expected_sig_hex = "\
a00345650d5ede861ee944a42012b8c7b9f8f7172a5f750e7c9bec592118b15e\
ffd554ec7c2d020c10bd38c37369104ae79d91e3acf8bd22b344ba8b1291d707";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Issuer,
id: warrant::WarrantId::from_bytes(ID_A2),
tools: BTreeMap::new(), holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 5,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: Some(vec!["read_file".to_string(), "write_file".to_string()]),
max_issue_depth: Some(3),
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant = sign_payload(&payload, &control_plane);
assert_eq!(
hex::encode(warrant.payload_bytes()),
expected_payload_hex,
"A.2 payload bytes mismatch"
);
assert_eq!(
hex::encode(warrant.signature().to_bytes()),
expected_sig_hex,
"A.2 signature mismatch"
);
assert_eq!(
warrant.r#type(),
warrant::WarrantType::Issuer,
"A.2 should be an Issuer warrant"
);
}
#[test]
fn test_vector_a4_invalid_chain_i1_violation() {
const ID_A4: [u8; 16] = [
0x01, 0x94, 0x71, 0xf8, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40,
];
let expected_invalid_payload_hex = "\
ab00010150019471f8000070008000000000000040020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e672f646174612f2a0482015820ca93ac1705187071d67b83c7ff0efe8108e8\
ec4530575d7726879333dbdabe7c0582015820ed4928c628d1c2c6eae9033890\
5995612959273a5c63f93636c14614ac8737d1061a65920080071a65920e9008\
030998201870185e187918411868182318ef1881189a0818e018c5189f18ec18\
cb185d184b18ae18d418a718eb18ca18ca18290b0118411218ce18c518fc1864\
1201";
let expected_invalid_sig_hex = "\
93d9c6d8a26fb450f9245c9cfec0a34dc8033bb08ed669d6f19502d1da0d35d5\
64b1a3767a2a469353417136ebc6ed9b27645b806c708baadc3dde27b4116f0c";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let worker2 = worker2_key();
let mut tools_l0 = BTreeMap::new();
let mut cs_l0 = ConstraintSet::new();
cs_l0.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_l0.insert("read_file".to_string(), cs_l0);
let payload_l0 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L0),
tools: tools_l0.clone(),
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l0 = sign_payload(&payload_l0, &control_plane);
let parent_hash = sha256(warrant_l0.payload_bytes());
let payload_invalid = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A4),
tools: tools_l0,
holder: worker2.public_key(),
issuer: worker.public_key(), issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let invalid_warrant = sign_payload(&payload_invalid, &worker);
assert_eq!(
hex::encode(invalid_warrant.payload_bytes()),
expected_invalid_payload_hex,
"A.4 invalid payload bytes mismatch"
);
assert_eq!(
hex::encode(invalid_warrant.signature().to_bytes()),
expected_invalid_sig_hex,
"A.4 invalid signature mismatch"
);
assert_ne!(
invalid_warrant.issuer(),
warrant_l0.authorized_holder(),
"A.4 should demonstrate I1 violation: child.issuer != parent.holder"
);
}
#[test]
fn test_vector_a8_warrant_stack_serialization() {
let expected_stack_prefix = "83830158a3aa00010150019471f8000070008000000000000010";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let worker2 = worker2_key();
let mut tools_l0 = BTreeMap::new();
let mut cs_l0 = ConstraintSet::new();
cs_l0.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_l0.insert("read_file".to_string(), cs_l0);
let payload_l0 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L0),
tools: tools_l0,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l0 = sign_payload(&payload_l0, &control_plane);
let parent_hash_l1 = sha256(warrant_l0.payload_bytes());
let mut tools_l1 = BTreeMap::new();
let mut cs_l1 = ConstraintSet::new();
cs_l1.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_l1.insert("read_file".to_string(), cs_l1);
let payload_l1 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L1),
tools: tools_l1,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash_l1),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l1 = sign_payload(&payload_l1, &orchestrator);
let parent_hash_l2 = sha256(warrant_l1.payload_bytes());
let mut tools_l2 = BTreeMap::new();
let mut cs_l2 = ConstraintSet::new();
cs_l2.insert(
"path".to_string(),
Constraint::Exact(Exact::new("/data/reports/q3.pdf")),
);
tools_l2.insert("read_file".to_string(), cs_l2);
let payload_l2 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A3_L2),
tools: tools_l2,
holder: worker2.public_key(),
issuer: worker.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 2,
parent_hash: Some(parent_hash_l2),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_l2 = sign_payload(&payload_l2, &worker);
let warrant_stack = vec![&warrant_l0, &warrant_l1, &warrant_l2];
let mut stack_bytes = Vec::new();
ciborium::ser::into_writer(&warrant_stack, &mut stack_bytes)
.expect("Failed to serialize warrant stack");
assert_eq!(
stack_bytes.len(),
851,
"A.8 WarrantStack should be 851 bytes"
);
let stack_hex = hex::encode(&stack_bytes);
assert!(
stack_hex.starts_with(expected_stack_prefix),
"A.8 WarrantStack prefix mismatch.\nExpected prefix: {}\nGot: {}",
expected_stack_prefix,
&stack_hex[..expected_stack_prefix.len().min(stack_hex.len())]
);
assert_eq!(stack_bytes[0], 0x83, "A.8 should start with array(3)");
let deserialized: Vec<Warrant> =
ciborium::de::from_reader(&stack_bytes[..]).expect("Failed to deserialize WarrantStack");
assert_eq!(
deserialized.len(),
3,
"A.8 should deserialize to 3 warrants"
);
assert_eq!(
deserialized[1].issuer(),
deserialized[0].authorized_holder(),
"A.8 Level 1 issuer should be Level 0 holder"
);
assert_eq!(
deserialized[2].issuer(),
deserialized[1].authorized_holder(),
"A.8 Level 2 issuer should be Level 1 holder"
);
}
#[test]
fn test_vector_a7_extensions_with_cbor() {
let expected_payload_hex = "\
ab00010150019471f8000070008000000000000070020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688201a16576616c756570\
2f646174612f7265706f72742e70646604820158208139770ea87d175f56a354\
66c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b39405820158208a88e3dd7409\
f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080\
071a65920e9008030aa273636f6d2e6578616d706c652e62696c6c696e679838\
18a31864187418651861186d186b186d186c182d187218651873186518611872\
18631868186718701872186f186a186518631874186e18771861187218721861\
186e1874182d18731879187318741865186d186b1863186f18731874185f1863\
1865186e187418651872181910186974636f6d2e6578616d706c652e74726163\
655f69648e186d1872186518711875186518731874182d183118321833183418\
351200";
let expected_sig_hex = "\
e760545471300ee3493c16336d8013b3e815c34fb79179a490570a016d8a0347\
30f22302bded9573b8264d0700e85cd93fbf683ef4648973fa11ae63a50b5900";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let mut extensions = BTreeMap::new();
let trace_id = "request-12345";
let mut trace_id_bytes = Vec::new();
ciborium::ser::into_writer(&trace_id, &mut trace_id_bytes).expect("Failed to encode trace_id");
extensions.insert("com.example.trace_id".to_string(), trace_id_bytes);
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
struct BillingTag {
team: String,
project: String,
cost_center: u32,
}
let billing = BillingTag {
team: "ml-research".to_string(),
project: "warrant-system".to_string(),
cost_center: 4201,
};
let mut billing_bytes = Vec::new();
ciborium::ser::into_writer(&billing, &mut billing_bytes).expect("Failed to encode billing");
extensions.insert("com.example.billing".to_string(), billing_bytes);
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert(
"path".to_string(),
Constraint::Exact(Exact::new("/data/report.pdf")),
);
tools.insert("read_file".to_string(), cs);
let payload_a7 = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A7),
tools,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions,
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_a7 = sign_payload(&payload_a7, &control_plane);
assert_eq!(
hex::encode(warrant_a7.payload_bytes()),
expected_payload_hex,
"A.7 payload bytes should match test vector"
);
assert_eq!(
hex::encode(warrant_a7.signature().to_bytes()),
expected_sig_hex,
"A.7 signature should match test vector"
);
let extensions_map = &warrant_a7.payload.extensions;
assert_eq!(extensions_map.len(), 2, "A.7 should have 2 extensions");
assert!(
extensions_map.contains_key("com.example.trace_id"),
"A.7 should have trace_id extension"
);
assert!(
extensions_map.contains_key("com.example.billing"),
"A.7 should have billing extension"
);
let trace_id_decoded: String =
ciborium::de::from_reader(&extensions_map["com.example.trace_id"][..])
.expect("Failed to decode trace_id");
assert_eq!(
trace_id_decoded, "request-12345",
"A.7 trace_id should match"
);
let billing_decoded: BillingTag =
ciborium::de::from_reader(&extensions_map["com.example.billing"][..])
.expect("Failed to decode billing");
assert_eq!(
billing_decoded.team, "ml-research",
"A.7 billing.team should match"
);
assert_eq!(
billing_decoded.project, "warrant-system",
"A.7 billing.project should match"
);
assert_eq!(
billing_decoded.cost_center, 4201,
"A.7 billing.cost_center should match"
);
let expected_trace_id_cbor = "6d726571756573742d3132333435";
let expected_billing_cbor = "a3647465616d6b6d6c2d72657365617263686770726f6a6563746e77617272616e742d73797374656d6b636f73745f63656e746572191069";
assert_eq!(
hex::encode(&extensions_map["com.example.trace_id"]),
expected_trace_id_cbor,
"A.7 trace_id CBOR encoding should match"
);
assert_eq!(
hex::encode(&extensions_map["com.example.billing"]),
expected_billing_cbor,
"A.7 billing CBOR encoding should match"
);
}
#[test]
fn test_vector_a10_invalid_depth_monotonicity() {
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut tools_parent = BTreeMap::new();
let mut cs_parent = ConstraintSet::new();
cs_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_parent.insert("read_file".to_string(), cs_parent);
let payload_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A10_PARENT),
tools: tools_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_parent = sign_payload(&payload_parent, &control_plane);
let parent_hash = sha256(warrant_parent.payload_bytes());
let mut tools_child = BTreeMap::new();
let mut cs_child = ConstraintSet::new();
cs_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_child.insert("read_file".to_string(), cs_child);
let payload_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A10_CHILD),
tools: tools_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 2, parent_hash: Some(parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_child = sign_payload(&payload_child, &orchestrator);
assert_eq!(warrant_parent.payload.depth, 0, "A.10 parent depth");
assert_eq!(warrant_child.payload.depth, 2, "A.10 child depth (wrong)");
assert_ne!(
warrant_child.payload.depth,
warrant_parent.payload.depth + 1,
"A.10 child should have WRONG depth (I2 violation)"
);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant_parent, warrant_child]);
assert!(
result.is_err(),
"A.10 verify_chain should REJECT depth violation"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("depth") || err_msg.contains("Depth"),
"A.10 error should mention depth: {}",
err_msg
);
}
#[test]
fn test_vector_a11_invalid_capability_monotonicity() {
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut tools_parent = BTreeMap::new();
let mut cs_parent = ConstraintSet::new();
cs_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()), );
tools_parent.insert("read_file".to_string(), cs_parent);
let payload_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A11_PARENT),
tools: tools_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_parent = sign_payload(&payload_parent, &control_plane);
let parent_hash = sha256(warrant_parent.payload_bytes());
let mut tools_child = BTreeMap::new();
let mut cs_child = ConstraintSet::new();
cs_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()), );
tools_child.insert("read_file".to_string(), cs_child);
let payload_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A11_CHILD),
tools: tools_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_child = sign_payload(&payload_child, &orchestrator);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant_parent, warrant_child]);
assert!(
result.is_err(),
"A.11 verify_chain should REJECT capability expansion"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("attenuation")
|| err_msg.contains("Pattern")
|| err_msg.contains("expand"),
"A.11 error should mention attenuation/pattern issue: {}",
err_msg
);
}
#[test]
fn test_vector_a12_invalid_parent_hash() {
let expected_parent_payload_hex = "\
aa00010150019471f80000700080000000000000a0020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e672f646174612f2a04820158208139770ea87d175f56a35466c34c7ecccb8d\
8a91b4ee37a25df60f5b8fc9b39405820158208a88e3dd7409f195fd52db2d3c\
ba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080071a65920e9008\
031200";
let expected_parent_sig_hex = "\
795cfa2f604317b61c770a2e1595968be9fc9ff77846b9c65f1e40570eb17344\
b62d8929ddc1ac1af2a40f1f9a0d817057f2a397a609afeb581e24ca1cb79f0c";
let expected_child_payload_hex = "\
ab00010150019471f80000700080000000000000a1020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e6f2f646174612f7265706f7274732f2a0482015820ed4928c628d1c2c6eae9\
0338905995612959273a5c63f93636c14614ac8737d105820158208139770ea8\
7d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394061a659200\
80071a65920e9008030998200000000000000000000000000000000000000000\
0000000000000000000000001201";
let expected_child_sig_hex = "\
65cc4fc544c331ba682404a444367d644ebd4438a8e731eb84c0f1d0ba575955\
68e94fb3053a20d22727770414f5b7c9f2f7c32841801ec93c07bd842ac9490b";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut tools_parent = BTreeMap::new();
let mut cs_parent = ConstraintSet::new();
cs_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_parent.insert("read_file".to_string(), cs_parent);
let payload_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A12_PARENT),
tools: tools_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_parent = sign_payload(&payload_parent, &control_plane);
assert_eq!(
hex::encode(warrant_parent.payload_bytes()),
expected_parent_payload_hex,
"A.12 parent payload bytes should match"
);
assert_eq!(
hex::encode(warrant_parent.signature().to_bytes()),
expected_parent_sig_hex,
"A.12 parent signature should match"
);
let wrong_parent_hash = [0u8; 32];
let correct_parent_hash = sha256(warrant_parent.payload_bytes());
let mut tools_child = BTreeMap::new();
let mut cs_child = ConstraintSet::new();
cs_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_child.insert("read_file".to_string(), cs_child);
let payload_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A12_CHILD),
tools: tools_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(wrong_parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_child = sign_payload(&payload_child, &orchestrator);
assert_eq!(
hex::encode(warrant_child.payload_bytes()),
expected_child_payload_hex,
"A.12 child payload bytes should match"
);
assert_eq!(
hex::encode(warrant_child.signature().to_bytes()),
expected_child_sig_hex,
"A.12 child signature should match"
);
assert_eq!(
hex::encode(correct_parent_hash),
"9b60b7ae1a679d990d77502db310315455d3b624f9a616f5bb7f34bccbd37914",
"A.12 correct parent hash should match"
);
assert_eq!(
hex::encode(wrong_parent_hash),
"0000000000000000000000000000000000000000000000000000000000000000",
"A.12 wrong parent hash should be all zeros"
);
assert_ne!(
warrant_child.payload.parent_hash.unwrap(),
correct_parent_hash,
"A.12 child should have WRONG parent hash (I5 violation)"
);
let mut tools_verify_parent = BTreeMap::new();
let mut cs_verify_parent = ConstraintSet::new();
cs_verify_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_verify_parent.insert("read_file".to_string(), cs_verify_parent);
let payload_verify_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A12_PARENT),
tools: tools_verify_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_verify_parent = sign_payload(&payload_verify_parent, &control_plane);
let wrong_hash = [0u8; 32];
let mut tools_verify_child = BTreeMap::new();
let mut cs_verify_child = ConstraintSet::new();
cs_verify_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_verify_child.insert("read_file".to_string(), cs_verify_child);
let payload_verify_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A12_CHILD),
tools: tools_verify_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(wrong_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_verify_child = sign_payload(&payload_verify_child, &orchestrator);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant_verify_parent, warrant_verify_child]);
assert!(
result.is_err(),
"A.12 verify_chain should REJECT parent hash mismatch"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("hash") || err_msg.contains("broken"),
"A.12 error should mention hash mismatch: {}",
err_msg
);
}
#[test]
fn test_vector_a13_ttl_extension_attack() {
let expected_parent_payload_hex = "\
aa00010150019471f80000700080000000000000b0020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e672f646174612f2a04820158208139770ea87d175f56a35466c34c7ecccb8d\
8a91b4ee37a25df60f5b8fc9b39405820158208a88e3dd7409f195fd52db2d3c\
ba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080071a65920e9008\
031200";
let expected_parent_sig_hex = "\
221a7bcbe2e9427338c316262d2322edfcc59340814447b0deaf5556dd11ff76\
4ca48a4166aedafa21da6a52e22d9b0b20392ad425c10eaad4221157f730e903";
let expected_child_payload_hex = "ab00010150019471f80000700080000000000000b1020003a169726561645f66696c65a16b636f6e73747261696e7473a164706174688202a1677061747465726e6f2f646174612f7265706f7274732f2a0482015820ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d105820158208139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394061a65920080071a65921ca0080309982018ee18451836184a1844184e18b40f183418b01718d81858184b18431856182918de18251882081862184a188d18e418fc18e618d318eb18d918a318c91201";
let expected_child_sig_hex = "4cc40a8fb7776042dbc0eef0a4833c92b678b3da405d249c58226db34c26e1905f86f2e73e98d891e93d0cd79a1ab3b15d811b4d4cf5f3f6a6d06e8030e8f705";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut tools_parent = BTreeMap::new();
let mut cs_parent = ConstraintSet::new();
cs_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_parent.insert("read_file".to_string(), cs_parent);
let payload_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A13_PARENT),
tools: tools_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: ISSUED_AT,
expires_at: EXPIRES_AT, max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_parent = sign_payload(&payload_parent, &control_plane);
assert_eq!(
hex::encode(warrant_parent.payload_bytes()),
expected_parent_payload_hex,
"A.13 parent payload bytes should match"
);
assert_eq!(
hex::encode(warrant_parent.signature().to_bytes()),
expected_parent_sig_hex,
"A.13 parent signature should match"
);
let parent_hash = sha256(warrant_parent.payload_bytes());
let extended_expires_at = EXPIRES_AT + 3600;
let mut tools_child = BTreeMap::new();
let mut cs_child = ConstraintSet::new();
cs_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_child.insert("read_file".to_string(), cs_child);
let payload_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A13_CHILD),
tools: tools_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: ISSUED_AT,
expires_at: extended_expires_at, max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_child = sign_payload(&payload_child, &orchestrator);
assert_eq!(
hex::encode(warrant_child.payload_bytes()),
expected_child_payload_hex,
"A.13 child payload bytes should match"
);
assert_eq!(
hex::encode(warrant_child.signature().to_bytes()),
expected_child_sig_hex,
"A.13 child signature should match"
);
assert_eq!(
warrant_parent.payload.expires_at, EXPIRES_AT,
"A.13 parent expires_at"
);
assert_eq!(
warrant_child.payload.expires_at, extended_expires_at,
"A.13 child expires_at"
);
assert!(
warrant_child.payload.expires_at > warrant_parent.payload.expires_at,
"A.13 child should have EXTENDED TTL (I3 violation)"
);
let mut tools_verify_parent = BTreeMap::new();
let mut cs_verify_parent = ConstraintSet::new();
cs_verify_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_verify_parent.insert("read_file".to_string(), cs_verify_parent);
let payload_verify_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A13_PARENT),
tools: tools_verify_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_verify_parent = sign_payload(&payload_verify_parent, &control_plane);
let verify_parent_hash = sha256(warrant_verify_parent.payload_bytes());
let verify_extended_expires_at = FUTURE_EXPIRES_AT + 3600;
let mut tools_verify_child = BTreeMap::new();
let mut cs_verify_child = ConstraintSet::new();
cs_verify_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
tools_verify_child.insert("read_file".to_string(), cs_verify_child);
let payload_verify_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A13_CHILD),
tools: tools_verify_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: verify_extended_expires_at, max_depth: 3,
depth: 1,
parent_hash: Some(verify_parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_verify_child = sign_payload(&payload_verify_child, &orchestrator);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant_verify_parent, warrant_verify_child]);
assert!(
result.is_err(),
"A.13 verify_chain should REJECT TTL extension"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("expir") || err_msg.contains("TTL") || err_msg.contains("lifetime"),
"A.13 error should mention expiration/TTL issue: {}",
err_msg
);
}
#[test]
fn test_vector_a14_invalid_signature() {
let expected_forged_payload_hex = "\
aa00010150019471f80000700080000000000000c0020003a169726561645f66\
696c65a16b636f6e73747261696e7473a164706174688202a167706174746572\
6e672f646174612f2a04820158208139770ea87d175f56a35466c34c7ecccb8d\
8a91b4ee37a25df60f5b8fc9b39405820158208a88e3dd7409f195fd52db2d3c\
ba5d72ca6709bf1d94121bf3748801b40f6f5c061a65920080071a65920e9008\
031200";
let expected_forged_sig_hex = "\
0038e4fc6d200a00e3a39987a0e172d8086812331da17e911e0fc2699bde94a7\
e413ad1b7a2ea1886627d822535ab3f469cd43e7f28e4c7c476bede22dcc8a05";
let expected_valid_sig_hex = "\
8e08644ea750b3b09f8593b05fbb9f4d2c1c0b37f07dfe097fb58952ba279228\
eede73926d6d4d2796a2fdf69b28501aaa75439ebcbbd2adb9efd0f04bd84c0e";
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let attacker = attacker_key();
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools.insert("read_file".to_string(), cs);
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A14),
tools,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(), issued_at: ISSUED_AT,
expires_at: EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_forged = sign_payload(&payload, &attacker);
let warrant_valid = sign_payload(&payload, &control_plane);
assert_eq!(
warrant_forged.payload_bytes(),
warrant_valid.payload_bytes(),
"A.14 forged and valid should have identical payload bytes"
);
assert_eq!(
hex::encode(warrant_forged.payload_bytes()),
expected_forged_payload_hex,
"A.14 forged payload bytes should match"
);
assert_eq!(
hex::encode(warrant_forged.signature().to_bytes()),
expected_forged_sig_hex,
"A.14 forged signature should match"
);
assert_eq!(
hex::encode(warrant_valid.signature().to_bytes()),
expected_valid_sig_hex,
"A.14 valid signature should match"
);
assert_eq!(
warrant_forged.payload.issuer,
control_plane.public_key(),
"A.14 forged warrant claims to be from control_plane"
);
assert_ne!(
attacker.public_key(),
control_plane.public_key(),
"A.14 attacker key should differ from control_plane"
);
let mut tools_verify = BTreeMap::new();
let mut cs_verify = ConstraintSet::new();
cs_verify.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_verify.insert("read_file".to_string(), cs_verify);
let payload_verify = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A14),
tools: tools_verify,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_verify_forged = sign_payload(&payload_verify, &attacker);
let warrant_verify_valid = sign_payload(&payload_verify, &control_plane);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result_forged = data_plane.verify_chain(&[warrant_verify_forged]);
assert!(
result_forged.is_err(),
"A.14 verify_chain should REJECT forged signature"
);
let err_msg = result_forged.unwrap_err().to_string();
assert!(
err_msg.contains("ignature") || err_msg.contains("verification"),
"A.14 error should mention signature issue: {}",
err_msg
);
let result_valid = data_plane.verify_chain(&[warrant_verify_valid]);
assert!(
result_valid.is_ok(),
"A.14 verify_chain should ACCEPT valid signature: {:?}",
result_valid.err()
);
}
#[test]
fn test_vector_a15_issuer_constraint_violation() {
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut constraint_bounds = ConstraintSet::new();
constraint_bounds.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
let payload_issuer = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Issuer,
id: warrant::WarrantId::from_bytes(ID_A15_ISSUER),
tools: BTreeMap::new(), holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 5,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: Some(vec!["read_file".to_string()]),
max_issue_depth: Some(3),
constraint_bounds: Some(constraint_bounds),
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_issuer = sign_payload(&payload_issuer, &control_plane);
let parent_hash = sha256(warrant_issuer.payload_bytes());
let mut tools_child = BTreeMap::new();
let mut cs_child = ConstraintSet::new();
cs_child.insert(
"path".to_string(),
Constraint::Exact(Exact::new("/etc/passwd")), );
tools_child.insert("read_file".to_string(), cs_child);
let payload_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A15_CHILD),
tools: tools_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_child = sign_payload(&payload_child, &orchestrator);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant_issuer, warrant_child]);
assert!(
result.is_err(),
"A.15 verify_chain should REJECT constraint violation"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("bound")
|| err_msg.contains("constraint")
|| err_msg.contains("attenuation")
|| err_msg.contains("allowed"),
"A.15 error should mention constraint bounds issue: {}",
err_msg
);
}
#[test]
fn test_vector_a16_self_issuance_violation() {
use std::time::Duration;
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let issuer_warrant = Warrant::builder()
.r#type(warrant::WarrantType::Issuer)
.issuable_tools(vec!["read_file".to_string()])
.ttl(Duration::from_secs(3600))
.holder(orchestrator.public_key())
.build(&control_plane)
.expect("Failed to build issuer warrant");
let self_issue_result = issuer_warrant
.issue_execution_warrant()
.unwrap()
.capability("read_file", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(orchestrator.public_key()) .build(&orchestrator);
assert!(
self_issue_result.is_err(),
"A.16 builder should REJECT Issuer → Execution self-issuance"
);
let err_msg = self_issue_result.unwrap_err().to_string();
assert!(
err_msg.contains("self") || err_msg.contains("Self") || err_msg.contains("issuance"),
"A.16 error should mention self-issuance: {}",
err_msg
);
}
#[test]
fn test_vector_a17_clearance_violation() {
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut tools_parent = BTreeMap::new();
let mut cs_parent = ConstraintSet::new();
cs_parent.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_parent.insert("read_file".to_string(), cs_parent);
let payload_parent = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A17_PARENT),
tools: tools_parent,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: Some(Clearance(5)), session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_parent = sign_payload(&payload_parent, &control_plane);
let parent_hash = sha256(warrant_parent.payload_bytes());
let mut tools_child = BTreeMap::new();
let mut cs_child = ConstraintSet::new();
cs_child.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools_child.insert("read_file".to_string(), cs_child);
let payload_child = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A17_CHILD),
tools: tools_child,
holder: worker.public_key(),
issuer: orchestrator.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 1,
parent_hash: Some(parent_hash),
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: Some(Clearance(6)), session_id: None,
agent_id: None,
required_approvers: None,
min_approvals: None,
};
let warrant_child = sign_payload(&payload_child, &orchestrator);
assert_eq!(
warrant_parent.payload.clearance,
Some(Clearance(5)),
"A.17 parent clearance should be 5"
);
assert_eq!(
warrant_child.payload.clearance,
Some(Clearance(6)),
"A.17 child clearance should be 6"
);
assert!(
warrant_child.payload.clearance > warrant_parent.payload.clearance,
"A.17 child should have HIGHER clearance (violation)"
);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant_parent, warrant_child]);
assert!(
result.is_err(),
"A.17 verify_chain should REJECT clearance escalation"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("clearance")
|| err_msg.contains("Clearance")
|| err_msg.contains("escalat"),
"A.17 error should mention clearance issue: {}",
err_msg
);
}
#[test]
fn test_vector_a18_multisig_configuration() {
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let worker2 = worker2_key();
let mut tools = BTreeMap::new();
let mut cs = ConstraintSet::new();
cs.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
tools.insert("read_file".to_string(), cs);
let payload = payload::WarrantPayload {
version: 1,
warrant_type: warrant::WarrantType::Execution,
id: warrant::WarrantId::from_bytes(ID_A18),
tools,
holder: orchestrator.public_key(),
issuer: control_plane.public_key(),
issued_at: FUTURE_ISSUED_AT,
expires_at: FUTURE_EXPIRES_AT,
max_depth: 3,
depth: 0,
parent_hash: None,
extensions: BTreeMap::new(),
issuable_tools: None,
max_issue_depth: None,
constraint_bounds: None,
clearance: None,
session_id: None,
agent_id: None,
required_approvers: Some(vec![worker.public_key(), worker2.public_key()]),
min_approvals: Some(1), };
let warrant = sign_payload(&payload, &control_plane);
assert!(
warrant.payload.required_approvers.is_some(),
"A.18 warrant should have required_approvers"
);
let approvers = warrant.payload.required_approvers.as_ref().unwrap();
assert_eq!(approvers.len(), 2, "A.18 should have 2 required approvers");
assert_eq!(
warrant.payload.min_approvals,
Some(1),
"A.18 should require minimum 1 approval"
);
assert!(
approvers.contains(&worker.public_key()),
"A.18 approvers should include worker"
);
assert!(
approvers.contains(&worker2.public_key()),
"A.18 approvers should include worker2"
);
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain(&[warrant]);
assert!(
result.is_ok(),
"A.18 verify_chain should ACCEPT valid multi-sig warrant: {:?}",
result.err()
);
}
fn sign_payload(payload: &payload::WarrantPayload, signing_key: &SigningKey) -> Warrant {
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(payload, &mut payload_bytes).expect("Failed to serialize payload");
let mut preimage = Vec::with_capacity(1 + payload_bytes.len());
preimage.push(1); preimage.extend_from_slice(&payload_bytes);
let signature = signing_key.sign(&preimage);
Warrant {
payload: payload.clone(),
signature,
payload_bytes,
envelope_version: 1,
}
}
fn sha256(data: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().into()
}
#[test]
fn test_vector_a19_1_range_constraint() {
use std::collections::HashMap;
use std::time::Duration;
let control_plane = control_plane_key();
let worker = worker_key();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"count".to_string(),
Constraint::Range(Range::new(Some(0.0), Some(100.0)).unwrap()),
);
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("api_call", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.build(&control_plane)
.expect("Failed to build Range warrant");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let mut valid_args: HashMap<String, ConstraintValue> = HashMap::new();
valid_args.insert("count".to_string(), ConstraintValue::Float(50.0));
let pop = warrant.sign(&worker, "api_call", &valid_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&warrant),
"api_call",
&valid_args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.19.1 Range should ACCEPT value within bounds: {:?}",
result.err()
);
let mut invalid_args: HashMap<String, ConstraintValue> = HashMap::new();
invalid_args.insert("count".to_string(), ConstraintValue::Float(150.0));
let pop = warrant.sign(&worker, "api_call", &invalid_args).unwrap();
let result = data_plane.check_chain(&[warrant], "api_call", &invalid_args, Some(&pop), &[]);
assert!(
result.is_err(),
"A.19.1 Range should REJECT value outside bounds"
);
}
#[test]
fn test_vector_a19_2_oneof_constraint() {
use std::collections::HashMap;
use std::time::Duration;
let control_plane = control_plane_key();
let worker = worker_key();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"env".to_string(),
Constraint::OneOf(OneOf::new(vec!["staging", "production"])),
);
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("deploy", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.build(&control_plane)
.expect("Failed to build OneOf warrant");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let mut valid_args: HashMap<String, ConstraintValue> = HashMap::new();
valid_args.insert(
"env".to_string(),
ConstraintValue::String("staging".to_string()),
);
let pop = warrant.sign(&worker, "deploy", &valid_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&warrant),
"deploy",
&valid_args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.19.2 OneOf should ACCEPT value in set: {:?}",
result.err()
);
let mut invalid_args: HashMap<String, ConstraintValue> = HashMap::new();
invalid_args.insert(
"env".to_string(),
ConstraintValue::String("development".to_string()),
);
let pop = warrant.sign(&worker, "deploy", &invalid_args).unwrap();
let result = data_plane.check_chain(&[warrant], "deploy", &invalid_args, Some(&pop), &[]);
assert!(
result.is_err(),
"A.19.2 OneOf should REJECT value not in set"
);
}
#[test]
fn test_vector_a19_3_cidr_constraint() {
use std::collections::HashMap;
use std::time::Duration;
let control_plane = control_plane_key();
let worker = worker_key();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"ip".to_string(),
Constraint::Cidr(Cidr::new("10.0.0.0/8").expect("valid CIDR")),
);
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("connect", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.build(&control_plane)
.expect("Failed to build CIDR warrant");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let mut valid_args: HashMap<String, ConstraintValue> = HashMap::new();
valid_args.insert(
"ip".to_string(),
ConstraintValue::String("10.1.2.3".to_string()),
);
let pop = warrant.sign(&worker, "connect", &valid_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&warrant),
"connect",
&valid_args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.19.3 CIDR should ACCEPT IP in network: {:?}",
result.err()
);
let mut invalid_args: HashMap<String, ConstraintValue> = HashMap::new();
invalid_args.insert(
"ip".to_string(),
ConstraintValue::String("192.168.1.1".to_string()),
);
let pop = warrant.sign(&worker, "connect", &invalid_args).unwrap();
let result = data_plane.check_chain(&[warrant], "connect", &invalid_args, Some(&pop), &[]);
assert!(
result.is_err(),
"A.19.3 CIDR should REJECT IP outside network"
);
}
#[test]
fn test_vector_a20_1_pop_wrong_holder_key() {
use std::collections::HashMap;
use std::time::Duration;
let control_plane = control_plane_key();
let worker = worker_key();
let attacker = SigningKey::generate();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("read_file", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.build(&control_plane)
.expect("Failed to build warrant");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert(
"path".to_string(),
ConstraintValue::String("/data/test.txt".to_string()),
);
let pop = warrant.sign(&attacker, "read_file", &args).unwrap();
let result = data_plane.check_chain(&[warrant], "read_file", &args, Some(&pop), &[]);
assert!(
result.is_err(),
"A.20.1 should REJECT PoP signed by wrong key"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("signature")
|| err_msg.contains("PoP")
|| err_msg.contains("invalid")
|| err_msg.contains("holder"),
"A.20.1 error should mention signature/PoP issue: {}",
err_msg
);
}
#[test]
fn test_vector_a20_2_pop_expired_window() {
use std::collections::HashMap;
use std::time::Duration;
let control_plane = control_plane_key();
let worker = worker_key();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"path".to_string(),
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("read_file", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.build(&control_plane)
.expect("Failed to build warrant");
let authorizer = Authorizer::builder()
.trusted_root(control_plane.public_key())
.build()
.expect("Failed to build authorizer");
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert(
"path".to_string(),
ConstraintValue::String("/data/test.txt".to_string()),
);
let pop = warrant.sign(&worker, "read_file", &args).unwrap();
let result = authorizer.authorize_one(&warrant, "read_file", &args, Some(&pop), &[]);
assert!(
result.is_ok(),
"A.20.2 fresh PoP should PASS with default window: {:?}",
result.err()
);
}
#[test]
fn test_vector_a21_1_valid_multisig_approval() {
use chrono::Utc;
use std::collections::HashMap;
use std::time::Duration;
use tenuo::approval::{compute_request_hash, ApprovalPayload, SignedApproval};
use tenuo::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let control_plane = control_plane_key();
let worker = worker_key();
let approver1 = SigningKey::generate();
let approver2 = SigningKey::generate();
let approver3 = SigningKey::generate();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"amount".to_string(),
Constraint::Range(Range::new(Some(0.0), Some(10000.0)).unwrap()),
);
let mut gates = ApprovalGateMap::new();
gates.insert("transfer".to_string(), ToolApprovalGate::whole_tool());
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("transfer", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.required_approvers(vec![
approver1.public_key(),
approver2.public_key(),
approver3.public_key(),
])
.min_approvals(2)
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&control_plane)
.expect("Failed to build multi-sig warrant");
let authorizer = Authorizer::new().with_trusted_root(control_plane.public_key());
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert("amount".to_string(), ConstraintValue::Float(500.0));
let request_hash = compute_request_hash(
&warrant.id().to_string(),
"transfer",
&args,
Some(&worker.public_key()),
);
let now = Utc::now();
let expires = now + chrono::Duration::hours(1);
let payload1 = ApprovalPayload {
version: 1,
request_hash,
nonce: rand::random(),
external_id: "approver1@example.com".to_string(),
approved_at: now.timestamp() as u64,
expires_at: expires.timestamp() as u64,
extensions: None,
};
let approval1 = SignedApproval::create(payload1, &approver1);
let payload2 = ApprovalPayload {
version: 1,
request_hash,
nonce: rand::random(),
external_id: "approver2@example.com".to_string(),
approved_at: now.timestamp() as u64,
expires_at: expires.timestamp() as u64,
extensions: None,
};
let approval2 = SignedApproval::create(payload2, &approver2);
let pop = warrant.sign(&worker, "transfer", &args).unwrap();
let result = authorizer.authorize_one(
&warrant,
"transfer",
&args,
Some(&pop),
&[approval1, approval2],
);
assert!(
result.is_ok(),
"A.21.1 should ACCEPT with sufficient approvals: {:?}",
result.err()
);
}
#[test]
fn test_vector_a21_2_insufficient_approvals() {
use chrono::Utc;
use std::collections::HashMap;
use std::time::Duration;
use tenuo::approval::{compute_request_hash, ApprovalPayload, SignedApproval};
use tenuo::approval_gate::{encode_approval_gate_map, ApprovalGateMap, ToolApprovalGate};
let control_plane = control_plane_key();
let worker = worker_key();
let approver1 = SigningKey::generate();
let approver2 = SigningKey::generate();
let mut constraint_set = ConstraintSet::new();
constraint_set.insert(
"amount".to_string(),
Constraint::Range(Range::new(Some(0.0), Some(10000.0)).unwrap()),
);
let mut gates = ApprovalGateMap::new();
gates.insert("transfer".to_string(), ToolApprovalGate::whole_tool());
let warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("transfer", constraint_set)
.ttl(Duration::from_secs(3600))
.holder(worker.public_key())
.required_approvers(vec![approver1.public_key(), approver2.public_key()])
.min_approvals(2)
.extension(
"tenuo.approval_gates",
encode_approval_gate_map(&gates).unwrap(),
)
.build(&control_plane)
.expect("Failed to build multi-sig warrant");
let authorizer = Authorizer::new().with_trusted_root(control_plane.public_key());
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert("amount".to_string(), ConstraintValue::Float(500.0));
let request_hash = compute_request_hash(&warrant.id().to_string(), "transfer", &args, None);
let now = Utc::now();
let expires = now + chrono::Duration::hours(1);
let payload = ApprovalPayload {
version: 1,
request_hash,
nonce: rand::random(),
external_id: "approver1@example.com".to_string(),
approved_at: now.timestamp() as u64,
expires_at: expires.timestamp() as u64,
extensions: None,
};
let approval1 = SignedApproval::create(payload, &approver1);
let pop = warrant.sign(&worker, "transfer", &args).unwrap();
let result = authorizer.authorize_one(&warrant, "transfer", &args, Some(&pop), &[approval1]);
assert!(
result.is_err(),
"A.21.2 should REJECT with insufficient approvals"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("approval")
|| err_msg.contains("threshold")
|| err_msg.contains("insufficient")
|| err_msg.contains("Insufficient"),
"A.21.2 error should mention approval issue: {}",
err_msg
);
}
#[test]
fn test_vector_a22_cascading_revocation() {
use std::time::Duration;
use tenuo::revocation::SignedRevocationList;
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let root_warrant = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("read_file", ConstraintSet::new())
.max_depth(3)
.ttl(Duration::from_secs(3600))
.holder(orchestrator.public_key())
.build(&control_plane)
.expect("Failed to build root warrant");
let worker_warrant = root_warrant
.attenuate()
.capability("read_file", ConstraintSet::new())
.ttl(Duration::from_secs(1800))
.holder(worker.public_key())
.build(&orchestrator)
.expect("Failed to build worker warrant");
let srl = SignedRevocationList::builder()
.revoke(worker_warrant.id().to_string())
.version(1)
.build(&control_plane)
.expect("Failed to build SRL");
let authorizer = Authorizer::builder()
.trusted_root(control_plane.public_key())
.revocation_list(srl, control_plane.public_key())
.build()
.expect("Failed to build authorizer with SRL");
let result = authorizer.verify_chain(&[root_warrant, worker_warrant]);
assert!(
result.is_err(),
"A.22 chain with revoked warrant should FAIL"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("revoked") || err_msg.contains("Revoked"),
"A.22 error should mention revocation: {}",
err_msg
);
}
#[test]
fn test_vector_a23_session_mismatch() {
use std::time::Duration;
let control_plane = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let root_with_session = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("read_file", ConstraintSet::new())
.max_depth(3)
.ttl(Duration::from_secs(3600))
.holder(orchestrator.public_key())
.session_id("sess-abc")
.build(&control_plane)
.expect("Failed to build root warrant with session");
let root_no_session = Warrant::builder()
.r#type(warrant::WarrantType::Execution)
.capability("read_file", ConstraintSet::new())
.max_depth(3)
.ttl(Duration::from_secs(3600))
.holder(orchestrator.public_key())
.build(&control_plane)
.expect("Failed to build root warrant without session");
let child_no_session = root_no_session
.attenuate()
.inherit_all()
.holder(worker.public_key())
.build(&orchestrator)
.expect("Failed to build child warrant");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", control_plane.public_key());
let result = data_plane.verify_chain_strict(&[root_with_session, child_no_session]);
assert!(
result.is_err(),
"A.23 verify_chain_strict should REJECT mismatched sessions"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("session")
|| err_msg.contains("Session")
|| err_msg.contains("mismatch")
|| err_msg.contains("parent"),
"A.23 error should mention session/chain issue: {}",
err_msg
);
}
#[test]
fn test_vector_a24_signed_approval_envelope() {
use chrono::{TimeZone, Utc};
use sha2::Digest;
use tenuo::approval::{ApprovalPayload, SignedApproval};
let approver_keypair = SigningKey::from_bytes(&[0x11; 32]);
let holder = SigningKey::from_bytes(&[0x03; 32]);
let mut hasher = sha2::Sha256::new();
hasher.update(b"tnu_wrt_test_warrant_id");
hasher.update(b"read_file");
hasher.update(b"path=/data/sensitive.txt");
hasher.update(holder.public_key().to_bytes());
let request_hash: [u8; 32] = hasher.finalize().into();
let approved_at = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
let expires_at = Utc.with_ymd_and_hms(2024, 1, 1, 1, 0, 0).unwrap();
let payload = ApprovalPayload::new(
request_hash,
TEST_VECTOR_NONCE,
"arn:aws:iam::123456789012:user/security-admin".to_string(),
approved_at,
expires_at,
);
let signed_approval = SignedApproval::create(payload, &approver_keypair);
assert_eq!(
signed_approval.approver_key.to_bytes(),
approver_keypair.public_key().to_bytes(),
"A.24 Approver key should match"
);
assert_eq!(
signed_approval.approval_version, 1,
"A.24 Approval version should be 1"
);
assert!(
!signed_approval.payload.is_empty(),
"A.24 Payload should be non-empty"
);
}
#[test]
fn test_vector_a25_1_urlsafe_constraint() {
use tenuo::constraints::UrlSafe;
let urlsafe = UrlSafe::new();
let valid_input = ConstraintValue::String("https://api.example.com/v1/users".to_string());
assert!(
urlsafe.matches(&valid_input).unwrap_or(false),
"A.25.1 UrlSafe should accept valid external URL"
);
let metadata_url =
ConstraintValue::String("http://169.254.169.254/latest/meta-data/".to_string());
assert!(
!urlsafe.matches(&metadata_url).unwrap_or(true),
"A.25.1 UrlSafe should reject AWS metadata URL"
);
let localhost = ConstraintValue::String("http://localhost:8080/".to_string());
assert!(
!urlsafe.matches(&localhost).unwrap_or(true),
"A.25.1 UrlSafe should reject localhost"
);
}
#[test]
fn test_vector_a25_1_urlsafe_host_policy_erratum() {
use tenuo::constraints::UrlSafe;
let named_host = UrlSafe::with_domains(vec!["api.example.com"]);
assert!(named_host.is_safe("https://api.example.com/data").unwrap());
assert!(
!named_host.is_safe("https://8.8.8.8/").unwrap(),
"A.25.1 erratum: a DNS allowlist must not admit a public IP literal"
);
let exact_ip = UrlSafe::with_domains(vec!["8.8.8.8"]);
assert!(exact_ip.is_safe("https://8.8.8.8/").unwrap());
let idn = UrlSafe::with_domains(vec!["bücher.example"]);
assert!(idn.is_safe("https://xn--bcher-kva.example/").unwrap());
let malformed = UrlSafe::with_domains(vec!["foo.*.example.com"]);
assert!(malformed.validate().is_err());
assert!(malformed.is_safe("https://api.example.com/").is_err());
}
#[test]
fn test_vector_a25_2_subpath_constraint() {
use tenuo::constraints::Subpath;
let subpath = Subpath::new("/home/agent/workspace").expect("Valid subpath");
let valid = ConstraintValue::String("/home/agent/workspace/reports/q3.pdf".to_string());
assert!(
subpath.matches(&valid).unwrap_or(false),
"A.25.2 Subpath should accept contained path"
);
let traversal =
ConstraintValue::String("/home/agent/workspace/../../../etc/passwd".to_string());
assert!(
!subpath.matches(&traversal).unwrap_or(true),
"A.25.2 Subpath should reject traversal attack"
);
let outside = ConstraintValue::String("/etc/passwd".to_string());
assert!(
!subpath.matches(&outside).unwrap_or(true),
"A.25.2 Subpath should reject path outside root"
);
}
#[test]
fn test_vector_a25_3_contains_constraint() {
use tenuo::constraints::Contains;
let contains = Contains::new(vec!["approved", "reviewed"]);
let valid = ConstraintValue::List(vec![
ConstraintValue::String("approved".to_string()),
ConstraintValue::String("reviewed".to_string()),
ConstraintValue::String("urgent".to_string()),
]);
assert!(
contains.matches(&valid).unwrap_or(false),
"A.25.3 Contains should accept list with required values"
);
let missing = ConstraintValue::List(vec![
ConstraintValue::String("approved".to_string()),
ConstraintValue::String("urgent".to_string()),
]);
assert!(
!contains.matches(&missing).unwrap_or(true),
"A.25.3 Contains should reject list missing required values"
);
}
#[test]
fn test_vector_a25_4_subset_constraint() {
use tenuo::constraints::Subset;
let subset = Subset::new(vec!["read", "write", "delete"]);
let valid = ConstraintValue::List(vec![
ConstraintValue::String("read".to_string()),
ConstraintValue::String("write".to_string()),
]);
assert!(
subset.matches(&valid).unwrap_or(false),
"A.25.4 Subset should accept subset of allowed values"
);
let extra = ConstraintValue::List(vec![
ConstraintValue::String("read".to_string()),
ConstraintValue::String("admin".to_string()),
]);
assert!(
!subset.matches(&extra).unwrap_or(true),
"A.25.4 Subset should reject list with extra values"
);
}
#[test]
fn test_vector_a25_5_urlpattern_constraint() {
use tenuo::constraints::UrlPattern;
let pattern = UrlPattern::new("https://api.example.com/v1/*").expect("Valid URL pattern");
let valid = ConstraintValue::String("https://api.example.com/v1/users".to_string());
assert!(
pattern.matches(&valid).unwrap_or(false),
"A.25.5 UrlPattern should accept matching URL"
);
let invalid = ConstraintValue::String("https://evil.com/api/v1/users".to_string());
assert!(
!pattern.matches(&invalid).unwrap_or(true),
"A.25.5 UrlPattern should reject non-matching URL"
);
let wrong_path = ConstraintValue::String("https://api.example.com/v2/users".to_string());
assert!(
!pattern.matches(&wrong_path).unwrap_or(true),
"A.25.5 UrlPattern should reject wrong path"
);
}
#[test]
fn test_vector_a26_1_anyof_constraint_check_chain() {
use std::collections::HashMap;
use std::time::Duration;
use tenuo::constraints::UrlSafe;
let issuer = control_plane_key();
let holder = worker_key();
let mut cs = ConstraintSet::new();
cs.insert(
"env",
constraints::Any::new([
Constraint::Exact(Exact::new("staging")),
Constraint::Exact(Exact::new("production")),
]),
);
let warrant = Warrant::builder()
.capability("deploy", cs)
.ttl(Duration::from_secs(3600))
.holder(holder.public_key())
.build(&issuer)
.expect("A.26.1 should build warrant");
let encoded = wire::encode(&warrant).expect("A.26.1 encode");
let decoded = wire::decode(&encoded).expect("A.26.1 decode");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", issuer.public_key());
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert(
"env".to_string(),
ConstraintValue::String("staging".to_string()),
);
let pop = decoded.sign(&holder, "deploy", &args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"deploy",
&args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.26.1 AnyOf should accept 'staging': {:?}",
result.err()
);
let mut bad_args: HashMap<String, ConstraintValue> = HashMap::new();
bad_args.insert(
"env".to_string(),
ConstraintValue::String("dev".to_string()),
);
let pop = decoded.sign(&holder, "deploy", &bad_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"deploy",
&bad_args,
Some(&pop),
&[],
);
assert!(
result.is_err(),
"A.26.1 AnyOf should reject 'dev' (not in set)"
);
let _ = UrlSafe::new(); }
#[test]
fn test_vector_a26_2_not_constraint_check_chain() {
use std::collections::HashMap;
use std::time::Duration;
let issuer = control_plane_key();
let holder = worker_key();
let mut cs = ConstraintSet::new();
cs.insert(
"role",
constraints::Not::new(Constraint::OneOf(OneOf::new(vec!["admin", "root"]))),
);
let warrant = Warrant::builder()
.capability("action", cs)
.ttl(Duration::from_secs(3600))
.holder(holder.public_key())
.build(&issuer)
.expect("A.26.2 should build warrant");
let encoded = wire::encode(&warrant).expect("A.26.2 encode");
let decoded = wire::decode(&encoded).expect("A.26.2 decode");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", issuer.public_key());
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert(
"role".to_string(),
ConstraintValue::String("user".to_string()),
);
let pop = decoded.sign(&holder, "action", &args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"action",
&args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.26.2 Not should accept 'user': {:?}",
result.err()
);
let mut bad_args: HashMap<String, ConstraintValue> = HashMap::new();
bad_args.insert(
"role".to_string(),
ConstraintValue::String("admin".to_string()),
);
let pop = decoded.sign(&holder, "action", &bad_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"action",
&bad_args,
Some(&pop),
&[],
);
assert!(
result.is_err(),
"A.26.2 Not should reject 'admin' (in exclusion list)"
);
}
#[test]
fn test_vector_a26_3_urlsafe_check_chain() {
use std::collections::HashMap;
use std::time::Duration;
use tenuo::constraints::UrlSafe;
let issuer = control_plane_key();
let holder = worker_key();
let mut cs = ConstraintSet::new();
cs.insert("url", UrlSafe::with_domains(vec!["api.example.com"]));
let warrant = Warrant::builder()
.capability("fetch", cs)
.ttl(Duration::from_secs(3600))
.holder(holder.public_key())
.build(&issuer)
.expect("A.26.3 build");
let encoded = wire::encode(&warrant).expect("A.26.3 encode");
let decoded = wire::decode(&encoded).expect("A.26.3 decode");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", issuer.public_key());
let mut args: HashMap<String, ConstraintValue> = HashMap::new();
args.insert(
"url".to_string(),
ConstraintValue::String("https://api.example.com/data".to_string()),
);
let pop = decoded.sign(&holder, "fetch", &args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"fetch",
&args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.26.3 UrlSafe should accept external URL: {:?}",
result.err()
);
let mut bad_args: HashMap<String, ConstraintValue> = HashMap::new();
bad_args.insert(
"url".to_string(),
ConstraintValue::String("http://169.254.169.254/latest/meta-data/".to_string()),
);
let pop = decoded.sign(&holder, "fetch", &bad_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"fetch",
&bad_args,
Some(&pop),
&[],
);
assert!(
result.is_err(),
"A.26.3 UrlSafe should reject SSRF metadata URL"
);
let mut public_ip_args: HashMap<String, ConstraintValue> = HashMap::new();
public_ip_args.insert(
"url".to_string(),
ConstraintValue::String("https://8.8.8.8/".to_string()),
);
let pop = decoded.sign(&holder, "fetch", &public_ip_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"fetch",
&public_ip_args,
Some(&pop),
&[],
);
assert!(
result.is_err(),
"A.26.3 UrlSafe allowlist should reject an unlisted public IP"
);
}
#[test]
fn test_vector_a27_child_cannot_add_capability() {
use std::time::Duration;
let issuer = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut parent_cs = ConstraintSet::new();
parent_cs.insert(
"path",
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
let parent = Warrant::builder()
.capability("read_file", parent_cs)
.ttl(Duration::from_secs(3600))
.holder(orchestrator.public_key())
.build(&issuer)
.expect("A.27 parent build");
let mut child_cs = ConstraintSet::new();
child_cs.insert(
"path",
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
let result = parent
.attenuate()
.capability("write_file", child_cs)
.holder(worker.public_key())
.build(&orchestrator);
assert!(
result.is_err(),
"A.27 child must not be able to grant a capability the parent never held"
);
}
#[test]
fn test_vector_a28_multihop_attenuation_enforced() {
use std::collections::HashMap;
use std::time::Duration;
let issuer = control_plane_key();
let orchestrator = orchestrator_key();
let worker = worker_key();
let mut cs0 = ConstraintSet::new();
cs0.insert(
"path",
Constraint::Pattern(Pattern::new("/data/*").unwrap()),
);
let root = Warrant::builder()
.capability("read_file", cs0)
.ttl(Duration::from_secs(3600))
.holder(orchestrator.public_key())
.build(&issuer)
.expect("A.28 root build");
let mut cs1 = ConstraintSet::new();
cs1.insert(
"path",
Constraint::Pattern(Pattern::new("/data/reports/*").unwrap()),
);
let mid = root
.attenuate()
.capability("read_file", cs1)
.holder(worker.public_key())
.build(&orchestrator)
.expect("A.28 mid build");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", issuer.public_key());
let mut valid_args: HashMap<String, ConstraintValue> = HashMap::new();
valid_args.insert(
"path".to_string(),
ConstraintValue::String("/data/reports/q3.pdf".to_string()),
);
let pop = mid.sign(&worker, "read_file", &valid_args).unwrap();
let result = data_plane.check_chain(
&[root.clone(), mid.clone()],
"read_file",
&valid_args,
Some(&pop),
&[],
);
assert!(
result.is_ok(),
"A.28 path within attenuated range must be accepted: {:?}",
result.err()
);
let mut bad_args: HashMap<String, ConstraintValue> = HashMap::new();
bad_args.insert(
"path".to_string(),
ConstraintValue::String("/data/logs/app.log".to_string()),
);
let pop = mid.sign(&worker, "read_file", &bad_args).unwrap();
let result = data_plane.check_chain(
&[root.clone(), mid.clone()],
"read_file",
&bad_args,
Some(&pop),
&[],
);
assert!(
result.is_err(),
"A.28 path outside attenuated range must be rejected even if it satisfies root"
);
}
#[test]
fn test_vector_a29_missing_constrained_field_rejected() {
use std::collections::HashMap;
use std::time::Duration;
use tenuo::constraints::Subpath;
let issuer = control_plane_key();
let holder = worker_key();
let mut cs = ConstraintSet::new();
cs.insert("path", Subpath::new("/data").expect("valid subpath"));
let warrant = Warrant::builder()
.capability("read_file", cs)
.ttl(Duration::from_secs(3600))
.holder(holder.public_key())
.build(&issuer)
.expect("A.29 build");
let encoded = wire::encode(&warrant).expect("A.29 encode");
let decoded = wire::decode(&encoded).expect("A.29 decode");
let mut data_plane = DataPlane::new();
data_plane.trust_issuer("root", issuer.public_key());
let empty_args: HashMap<String, ConstraintValue> = HashMap::new();
let pop = decoded.sign(&holder, "read_file", &empty_args).unwrap();
let result = data_plane.check_chain(
std::slice::from_ref(&decoded),
"read_file",
&empty_args,
Some(&pop),
&[],
);
assert!(
result.is_err(),
"A.29 constrained field absent from call arguments must be rejected"
);
}
const RECEIPT_A30_1: &str = concat!(
"a46f726563656970745f76657273696f6e01677061796c6f6164590179a80001",
"0258ec81830158a3aa00010150019471f8000070008000000000003000020003",
"a169726561645f66696c65a16b636f6e73747261696e7473a164706174688202",
"a1677061747465726e672f646174612f2a0482015820ed4928c628d1c2c6eae9",
"0338905995612959273a5c63f93636c14614ac8737d105820158208a88e3dd74",
"09f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a659200",
"80071a65920e9008031200820158407c948aef75e62035b5a5e6ab1e07cbd5ce",
"f372ec94ac0e514b320dbdb976a9f6fba98a81d5cbdbb3e28aca8f97f529f6ac",
"51a36b53e547545338aba0c1cb83020369726561645f66696c650465616c6c6f",
"77051a659200800858404aa574e10e3e19223f987a17e16839b52ae597a2b56e",
"eaf051d14c773c30a3f790f9e24daedea9994d9618b61bd83fc76f7cf5c3764a",
"280200962fe537c39a0909677265712d6133300f582034750f98bd59fcfc946d",
"a45aaabe933be154a4b5094e1c4abf42866505f3c97e6a7369676e65725f6b65",
"79820158201ba4075b77c9e3fb3ecde15cdaf5221f3c10373e623f7b0e1ef763",
"66b0af7137697369676e6174757265820158403dae6ff6eac08c6454b1a97747",
"c7bff3f8dd691798e1eee681b17a9e2e2ffc8a74999ded362f707ebfe5a6734b",
"bbadeed0c96e51be97d2a37f8316a7d172bd08",
);
const RECEIPT_A30_2: &str = concat!(
"a46f726563656970745f76657273696f6e01677061796c6f616459019ca90001",
"0258ec81830158a3aa00010150019471f8000070008000000000003000020003",
"a169726561645f66696c65a16b636f6e73747261696e7473a164706174688202",
"a1677061747465726e672f646174612f2a0482015820ed4928c628d1c2c6eae9",
"0338905995612959273a5c63f93636c14614ac8737d105820158208a88e3dd74",
"09f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a659200",
"80071a65920e9008031200820158407c948aef75e62035b5a5e6ab1e07cbd5ce",
"f372ec94ac0e514b320dbdb976a9f6fba98a81d5cbdbb3e28aca8f97f529f6ac",
"51a36b53e547545338aba0c1cb83020369726561645f66696c650465616c6c6f",
"77051a659200800858404aa574e10e3e19223f987a17e16839b52ae597a2b56e",
"eaf051d14c773c30a3f790f9e24daedea9994d9618b61bd83fc76f7cf5c3764a",
"280200962fe537c39a0909677265712d6133300d5820e135dea2864c53a124c9",
"c42fcd7d0909f865aaf5b66d9ca5d1ee5278fffdc7120f582034750f98bd59fc",
"fc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e6a7369676e6572",
"5f6b6579820158201ba4075b77c9e3fb3ecde15cdaf5221f3c10373e623f7b0e",
"1ef76366b0af7137697369676e617475726582015840cf81c7873a9bc4b87be2",
"254b37222769dee384a36dde5645518f85dff76fc5f5a8fe50817b244c445720",
"e64fd89f2de9fba4e48312d63feffb8664f46ee1ca04",
);
const RECEIPT_A30_3: &str = concat!(
"a46f726563656970745f76657273696f6e01677061796c6f61645901c2ab0001",
"0258ec81830158a3aa00010150019471f8000070008000000000003000020003",
"a169726561645f66696c65a16b636f6e73747261696e7473a164706174688202",
"a1677061747465726e672f646174612f2a0482015820ed4928c628d1c2c6eae9",
"0338905995612959273a5c63f93636c14614ac8737d105820158208a88e3dd74",
"09f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a659200",
"80071a65920e9008031200820158407c948aef75e62035b5a5e6ab1e07cbd5ce",
"f372ec94ac0e514b320dbdb976a9f6fba98a81d5cbdbb3e28aca8f97f529f6ac",
"51a36b53e547545338aba0c1cb83020369726561645f66696c650465616c6c6f",
"77051a659200800858404aa574e10e3e19223f987a17e16839b52ae597a2b56e",
"eaf051d14c773c30a3f790f9e24daedea9994d9618b61bd83fc76f7cf5c3764a",
"280200962fe537c39a0909677265712d6133300b5820a2de9b15bdc8095f2421",
"26aa369233e4f2cada0ee3852482cae5ac9b94b80ff80c182f0d5820e135dea2",
"864c53a124c9c42fcd7d0909f865aaf5b66d9ca5d1ee5278fffdc7120f582034",
"750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e6a",
"7369676e65725f6b6579820158201ba4075b77c9e3fb3ecde15cdaf5221f3c10",
"373e623f7b0e1ef76366b0af7137697369676e617475726582015840f7395fb6",
"7a2bf45c2da552e69e014e1851132558fdc460b941ad0357ab1eaa2a4ad58c79",
"931b1ee377116b3d7a109b9d50dbb29ec1e76cba872273e38eff6e0c",
);
const RECEIPT_A30_4: &str = concat!(
"a46f726563656970745f76657273696f6e01677061796c6f6164590172a90001",
"0258ec81830158a3aa00010150019471f8000070008000000000003000020003",
"a169726561645f66696c65a16b636f6e73747261696e7473a164706174688202",
"a1677061747465726e672f646174612f2a0482015820ed4928c628d1c2c6eae9",
"0338905995612959273a5c63f93636c14614ac8737d105820158208a88e3dd74",
"09f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a659200",
"80071a65920e9008031200820158407c948aef75e62035b5a5e6ab1e07cbd5ce",
"f372ec94ac0e514b320dbdb976a9f6fba98a81d5cbdbb3e28aca8f97f529f6ac",
"51a36b53e547545338aba0c1cb83020369726561645f66696c65046464656e79",
"051a65920080096c7265712d6133302d64656e790a73746f6f6c2d6e6f742d61",
"7574686f72697a65640d5820e135dea2864c53a124c9c42fcd7d0909f865aaf5",
"b66d9ca5d1ee5278fffdc7120f582034750f98bd59fcfc946da45aaabe933be1",
"54a4b5094e1c4abf42866505f3c97e6a7369676e65725f6b6579820158201ba4",
"075b77c9e3fb3ecde15cdaf5221f3c10373e623f7b0e1ef76366b0af71376973",
"69676e61747572658201584077775a6ebefc4dca805c5826bc80046ceba63c46",
"78e8957b26d052139042109d40b791e83bf1c2252d0b6fa7c700be85043cb6c9",
"7f6d5a190274c4956a8f5c0b",
);
const A30_DIGEST_INPUT: &[u8] = b"tenuo-test-vector-a30-revocation-list";
const RECEIPT_A30_5: &str = concat!(
"a46f726563656970745f76657273696f6e01677061796c6f61645901c6aa0001",
"0258ec81830158a3aa00010150019471f8000070008000000000003000020003",
"a169726561645f66696c65a16b636f6e73747261696e7473a164706174688202",
"a1677061747465726e672f646174612f2a0482015820ed4928c628d1c2c6eae9",
"0338905995612959273a5c63f93636c14614ac8737d105820158208a88e3dd74",
"09f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c061a659200",
"80071a65920e9008031200820158407c948aef75e62035b5a5e6ab1e07cbd5ce",
"f372ec94ac0e514b320dbdb976a9f6fba98a81d5cbdbb3e28aca8f97f529f6ac",
"51a36b53e547545338aba0c1cb83020369726561645f66696c650465616c6c6f",
"77051a659200800858404aa574e10e3e19223f987a17e16839b52ae597a2b56e",
"eaf051d14c773c30a3f790f9e24daedea9994d9618b61bd83fc76f7cf5c3764a",
"280200962fe537c39a09096e7265712d6133302d7365636f6e640d5820e135de",
"a2864c53a124c9c42fcd7d0909f865aaf5b66d9ca5d1ee5278fffdc7120e5820",
"ac4582f55beda95c370e8f50ff3286790eef1fec12aee369fcd7fb636a4e9666",
"0f582034750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505",
"f3c97e6a7369676e65725f6b6579820158201ba4075b77c9e3fb3ecde15cdaf5",
"221f3c10373e623f7b0e1ef76366b0af7137697369676e617475726582015840",
"2abc6b9738d742aa79294b6db464e67495c9fd4ba2ab22b5696119cd82f8eba9",
"3227249df96b603a23b8cb92409868f5eb5d91275bd5568ae6cad567a76c8e03",
);
const A30_POLICY_INPUT: &[u8] = b"tenuo-test-vector-a30-policy";
fn decode_receipt(hex_str: &str) -> tenuo::receipt::ReceiptPayload {
let bytes = hex::decode(hex_str).expect("receipt vector decodes from hex");
let receipt: tenuo::receipt::Receipt =
ciborium::from_reader(bytes.as_slice()).expect("receipt vector parses as CBOR");
receipt
.verify_signature()
.expect("receipt vector signature verifies")
}
#[test]
fn a30_1_omits_the_revocation_commitment_entirely() {
let payload = decode_receipt(RECEIPT_A30_1);
assert_eq!(payload.outcome, tenuo::receipt::Outcome::Allow);
assert_eq!(payload.action, "read_file");
assert!(
payload.pop_signature.is_some(),
"an allow without a PoP is evidence of nothing but the signer's word"
);
assert_eq!(payload.srl_version, None);
assert_eq!(
payload.srl_hash, None,
"absent means revocation was never consulted"
);
}
#[test]
fn a30_2_commits_to_an_unversioned_list() {
let payload = decode_receipt(RECEIPT_A30_2);
assert_eq!(
payload.srl_hash,
Some(tenuo::srl_commitment_digest(A30_DIGEST_INPUT)),
"key 13 must be SHA-256 over the list bytes as loaded"
);
assert_eq!(
payload.srl_version, None,
"a plain SignedRevocationList carries no version, and the receipt must \
not invent one"
);
}
#[test]
fn a30_3_commits_to_a_versioned_list() {
let payload = decode_receipt(RECEIPT_A30_3);
assert_eq!(payload.srl_version, Some(47));
assert_eq!(
payload.srl_hash,
Some(tenuo::srl_commitment_digest(A30_DIGEST_INPUT))
);
}
#[test]
fn a30_4_records_a_denial_reached_before_possession_was_proven() {
let payload = decode_receipt(RECEIPT_A30_4);
assert_eq!(payload.outcome, tenuo::receipt::Outcome::Deny);
assert_eq!(
payload.pop_signature, None,
"possession was never established, so the receipt must not imply it was"
);
assert_eq!(
payload.decision_code.as_deref(),
Some("tool-not-authorized"),
"a denial must say why"
);
payload
.check_conditional_requirements()
.expect("a deny carrying decision_code satisfies the conditional requirements");
}
#[test]
fn a30_receipts_reject_a_tampered_signature() {
let mut bytes = hex::decode(RECEIPT_A30_3).expect("decodes");
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
let receipt: tenuo::receipt::Receipt =
ciborium::from_reader(bytes.as_slice()).expect("still parses as CBOR");
assert!(
receipt.verify_signature().is_err(),
"a flipped bit in the signature must not verify"
);
}
#[test]
fn a30_commits_to_the_trusted_root_set() {
for wire in [RECEIPT_A30_1, RECEIPT_A30_3, RECEIPT_A30_4, RECEIPT_A30_5] {
let payload = decode_receipt(wire);
assert!(
payload.trusted_roots_hash.is_some(),
"a receipt that does not say which roots it trusted cannot show the \
chain was rooted in anything legitimate"
);
}
}
#[test]
fn a30_3_commits_to_the_host_ceiling() {
let payload = decode_receipt(RECEIPT_A30_3);
assert_eq!(
payload.policy_definition_hash,
Some(tenuo::policy_commitment_digest(A30_POLICY_INPUT)),
"without key 11 a receipt cannot distinguish an allow under a tight \
ceiling from one under an open ceiling"
);
}
#[test]
fn a30_5_links_to_its_predecessor() {
let previous = hex::decode(RECEIPT_A30_2).expect("decodes");
let previous: tenuo::receipt::Receipt =
ciborium::from_reader(previous.as_slice()).expect("parses");
let payload = decode_receipt(RECEIPT_A30_5);
assert_eq!(
payload.prev_receipt_hash,
Some(previous.digest().expect("digests")),
"the link must commit to the exact predecessor artifact"
);
}
#[test]
fn a30_1_starts_a_chain_with_no_predecessor() {
let payload = decode_receipt(RECEIPT_A30_1);
assert_eq!(payload.prev_receipt_hash, None);
}
#[test]
fn a30_removing_a_receipt_from_the_stream_is_detectable() {
let stream = [RECEIPT_A30_2, RECEIPT_A30_5];
let kept = decode_receipt(stream[1]);
let dropped_predecessor = decode_receipt(stream[0]);
let present: Vec<[u8; 32]> = vec![];
let link = kept.prev_receipt_hash.expect("A.30.5 links backwards");
assert!(
!present.contains(&link),
"with A.30.2 withheld, its successor's link resolves to nothing"
);
assert_eq!(dropped_predecessor.request_id, "req-a30");
}
const A31_ARGS_CBOR: &str =
"a4676472795f72756ef5656c696d69740a64706174686c2f646174612f71332e70646664746167738261626161";
const A31_REQUEST_HASH: &str = "59f290af000b9331df93a643f48fd530f43510243507cd8bfc8aa12623dbab4b";
const A31_POLICY_CBOR: &str = "a268656e636f64696e67a2646b696e64656f6e654f666676616c7565738264757466386561736369696470617468a2646b696e6465756e64657264726f6f74652f64617461";
const A31_POLICY_HASH: &str = "9b53b570975500172890f86347a3338c8ac4ca2b75501d582a2cc0eef152059d";
fn a31_args() -> std::collections::HashMap<String, tenuo::ConstraintValue> {
use tenuo::ConstraintValue;
let mut args = std::collections::HashMap::new();
args.insert(
"path".to_string(),
ConstraintValue::String("/data/q3.pdf".to_string()),
);
args.insert("limit".to_string(), ConstraintValue::Integer(10));
args.insert("dry_run".to_string(), ConstraintValue::Boolean(true));
args.insert(
"tags".to_string(),
ConstraintValue::List(vec![
ConstraintValue::String("b".to_string()),
ConstraintValue::String("a".to_string()),
]),
);
args
}
#[test]
fn a31_1_canonical_args_sort_keys_and_preserve_list_order() {
let encoded = tenuo::approval::canonical_tool_args_cbor(&a31_args()).expect("canonicalizes");
assert_eq!(hex::encode(encoded), A31_ARGS_CBOR);
}
#[test]
fn a31_1_request_hash_matches_the_published_derivation() {
let holder_bytes =
hex::decode("ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1")
.expect("decodes");
let holder = tenuo::PublicKey::from_bytes(&holder_bytes.try_into().unwrap()).expect("key");
let hash = tenuo::approval::compute_request_hash(
"tnu_wrt_019471f8000070008000000000003100",
"read_file",
&a31_args(),
Some(&holder),
);
assert_eq!(hex::encode(hash), A31_REQUEST_HASH);
}
#[test]
fn a31_2_policy_encoding_sorts_and_matches_the_published_derivation() {
let policy = serde_json::json!({
"path": {"kind": "under", "root": "/data"},
"encoding": {"kind": "oneOf", "values": ["utf8", "ascii"]},
});
let bytes = tenuo::canonical_policy_bytes(&policy).expect("canonicalizes");
assert_eq!(hex::encode(&bytes), A31_POLICY_CBOR);
assert_eq!(
hex::encode(tenuo::policy_commitment_digest(&bytes)),
A31_POLICY_HASH
);
}
#[test]
fn a31_2_a_different_policy_commits_differently() {
let tight = serde_json::json!({"path": {"kind": "under", "root": "/data"}});
let open = serde_json::json!({});
let tight_bytes = tenuo::canonical_policy_bytes(&tight).expect("canonicalizes");
let open_bytes = tenuo::canonical_policy_bytes(&open).expect("canonicalizes");
assert_ne!(
tenuo::policy_commitment_digest(&tight_bytes),
tenuo::policy_commitment_digest(&open_bytes)
);
}