#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use ciborium::value::Value;
use super::*;
const ANCHOR_KEY: [u8; 32] = [0xa1; 32];
const SIGNATURE: [u8; 64] = [0x5e; 64];
const NODE_A: [u8; 32] = [0x11; 32];
const NODE_B: [u8; 32] = [0x22; 32];
fn small(n: usize) -> u8 {
u8::try_from(n).unwrap()
}
fn expected_protected(key: &[u8; 32]) -> Vec<u8> {
let mut out = vec![
0xa4, 0x01, 0x27, 0x03, 0x78, 0x23, ];
out.extend_from_slice(b"application/vnd.lys.receipt.v1+cbor");
out.extend_from_slice(&[0x04, 0x58, 0x20]); out.extend_from_slice(key);
out.extend_from_slice(&[0x19, 0x01, 0x8b, 0x01]); out
}
fn expected_inner_proof(tree_size: u8, leaf_index: u8, nodes: &[[u8; 32]]) -> Vec<u8> {
let mut out = vec![0x83, tree_size, leaf_index]; out.push(0x80 | small(nodes.len())); for node in nodes {
out.extend_from_slice(&[0x58, 0x20]);
out.extend_from_slice(node);
}
out
}
fn expected_unprotected(tree_size: u8, leaf_index: u8, nodes: &[[u8; 32]]) -> Vec<u8> {
let proof = expected_inner_proof(tree_size, leaf_index, nodes);
let mut out = vec![
0xa1, 0x19, 0x01, 0x8c, 0xa1, 0x20, 0x81, ];
assert!(proof.len() < 256);
out.extend_from_slice(&[0x58, small(proof.len())]);
out.extend_from_slice(&proof);
out
}
fn sample() -> Vec<u8> {
artifact_bytes(&ANCHOR_KEY, 4, 1, &[NODE_A, NODE_B], &SIGNATURE)
}
#[test]
fn the_content_type_is_the_frozen_slash_free_string() {
assert_eq!(CONTENT_TYPE, "application/vnd.lys.receipt.v1+cbor");
assert_eq!(CONTENT_TYPE.len(), 35);
assert!(
!CONTENT_TYPE.contains("anchor-receipt"),
"the media type uses the dotted vnd. form, not the context-tag form"
);
}
#[test]
fn protected_bucket_matches_the_specification_bytes() {
let built = protected_bytes(&ANCHOR_KEY);
assert_eq!(built, expected_protected(&ANCHOR_KEY));
assert_eq!(built.len(), 80, "the protected bucket is always 80 bytes");
}
#[test]
fn protected_bucket_is_fixed_width_for_every_key() {
for byte in [0x00u8, 0x7f, 0xff] {
assert_eq!(protected_bytes(&[byte; 32]).len(), 80);
}
}
#[test]
fn protected_labels_ascend_so_numeric_and_bytewise_order_coincide() {
let built = protected_bytes(&ANCHOR_KEY);
let Value::Map(entries) = ciborium::de::from_reader(built.as_slice()).unwrap() else {
panic!("protected bucket is a map");
};
let labels: Vec<i128> = entries
.iter()
.map(|(k, _)| match k {
Value::Integer(i) => i128::from(*i),
other => panic!("non-integer label: {other:?}"),
})
.collect();
assert_eq!(labels, vec![1, 3, 4, 395]);
assert!(labels.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn artifact_layout_matches_the_specification_bytes() {
let built = sample();
let mut expected = vec![0xd2, 0x84]; let protected = expected_protected(&ANCHOR_KEY);
expected.extend_from_slice(&[0x58, 0x50]); expected.extend_from_slice(&protected);
expected.extend_from_slice(&expected_unprotected(4, 1, &[NODE_A, NODE_B]));
expected.push(0xf6); expected.extend_from_slice(&[0x58, 0x40]); expected.extend_from_slice(&SIGNATURE);
assert_eq!(built, expected);
}
#[test]
fn the_payload_slot_is_cbor_nil_because_the_root_is_detached() {
let built = sample();
let offset = 2 + 2 + 80 + expected_unprotected(4, 1, &[NODE_A, NODE_B]).len();
assert_eq!(built[offset], 0xf6, "payload must be nil, never the root");
let root = crate::merkle::root_from_inclusion_path(&[0x33; 32], 1, 4, &{
let mut p = NODE_A.to_vec();
p.extend_from_slice(&NODE_B);
p
})
.unwrap();
assert!(
!built.windows(32).any(|w| w == root),
"the signed root must not be carried in the artifact"
);
}
#[test]
fn the_vdp_reproduces_the_rfc_9942_double_wrapper() {
let built = sample();
let Value::Tag(18, boxed) = ciborium::de::from_reader(built.as_slice()).unwrap() else {
panic!("tagged");
};
let Value::Array(items) = *boxed else {
panic!("4-array")
};
let Value::Map(unprotected) = &items[1] else {
panic!("map")
};
let (label, vdp) = &unprotected[0];
assert_eq!(*label, Value::Integer(396.into()));
let Value::Map(by_type) = vdp else {
panic!("vdp is a map keyed by proof type")
};
let (proof_type, proofs) = &by_type[0];
assert_eq!(*proof_type, Value::Integer((-1).into()));
let Value::Array(proofs) = proofs else {
panic!("the value at -1 is an array of proofs")
};
assert_eq!(proofs.len(), 1);
let Value::Bytes(raw) = &proofs[0] else {
panic!("each proof is a bstr .cbor")
};
let Value::Array(proof) = ciborium::de::from_reader(raw.as_slice()).unwrap() else {
panic!("the wrapped value is the 3-array")
};
assert_eq!(proof.len(), 3);
assert_eq!(proof[0], Value::Integer(4.into()));
assert_eq!(proof[1], Value::Integer(1.into()));
let Value::Array(path) = &proof[2] else {
panic!("path array")
};
assert_eq!(path.len(), 2);
}
#[test]
fn decoding_recovers_every_encoded_field() {
let fields = decode_fields(&sample()).unwrap();
assert_eq!(fields.anchor_public_key, ANCHOR_KEY);
assert_eq!(fields.tree_size, 4);
assert_eq!(fields.leaf_index, 1);
assert_eq!(fields.inclusion_path, vec![NODE_A, NODE_B]);
assert_eq!(fields.signature, SIGNATURE);
}
#[test]
fn large_sizes_and_indices_round_trip_through_the_shortest_form() {
for (size, index) in [(2u64, 0u64), (1000, 999), (u64::MAX, u64::MAX - 1)] {
let nodes = [NODE_A];
let bytes = artifact_bytes(&ANCHOR_KEY, size, index, &nodes, &SIGNATURE);
let fields = decode_fields(&bytes).unwrap();
assert_eq!(fields.tree_size, size);
assert_eq!(fields.leaf_index, index);
}
}
#[test]
fn an_empty_path_decodes_because_it_is_the_true_path_for_a_one_leaf_tree() {
let bytes = artifact_bytes(&ANCHOR_KEY, 1, 0, &[], &SIGNATURE);
let fields = decode_fields(&bytes).unwrap();
assert!(fields.inclusion_path.is_empty());
assert_eq!(fields.tree_size, 1);
}
#[test]
fn structural_mutants_are_all_refused() {
let good = sample();
assert!(decode_fields(&vec![0u8; MAX_ARTIFACT_LEN + 1]).is_err());
let mut untagged = good.clone();
untagged.remove(0);
assert!(decode_fields(&untagged).is_err());
let mut wrong_tag = good.clone();
wrong_tag[0] = 0xd1;
assert!(decode_fields(&wrong_tag).is_err());
assert!(decode_fields(&good[..good.len() - 1]).is_err());
assert!(decode_fields(&[]).is_err());
assert!(decode_fields(&[0xd2]).is_err());
}
#[test]
fn a_non_nil_payload_is_refused() {
let good = sample();
let payload_offset = 2 + 2 + 80 + expected_unprotected(4, 1, &[NODE_A, NODE_B]).len();
assert_eq!(good[payload_offset], 0xf6);
let mut empty_bstr = good.clone();
empty_bstr[payload_offset] = 0x40;
assert!(decode_fields(&empty_bstr).is_err());
let mut falsey = good;
falsey[payload_offset] = 0xf4;
assert!(decode_fields(&falsey).is_err());
}
#[test]
fn every_protected_header_pin_is_enforced() {
const P: usize = 4;
let good = sample();
let mut alg = good.clone();
assert_eq!(alg[P + 2], 0x27, "alg value byte");
alg[P + 2] = 0x26; assert!(decode_fields(&alg).is_err());
let mut alg19 = good.clone();
alg19[P + 2] = 0x32; assert!(decode_fields(&alg19).is_err());
let mut ct = good.clone();
let ct_at = P + 6;
assert_eq!(&ct[ct_at..ct_at + 11], b"application");
ct[ct_at] = b'A';
assert!(decode_fields(&ct).is_err());
let mut vds = good.clone();
let vds_at = P + 79;
assert_eq!(vds[vds_at], 0x01, "vds value byte");
vds[vds_at] = 0x02;
assert!(decode_fields(&vds).is_err());
let mut arity = good;
arity[P] = 0xa3;
assert!(decode_fields(&arity).is_err());
}
#[test]
fn every_vdp_shape_deviation_is_refused() {
let protected = expected_protected(&ANCHOR_KEY);
let proof = expected_inner_proof(4, 1, &[NODE_A, NODE_B]);
let assemble = |unprotected: Vec<u8>| {
let mut out = vec![0xd2, 0x84, 0x58, 0x50];
out.extend_from_slice(&protected);
out.extend_from_slice(&unprotected);
out.push(0xf6);
out.extend_from_slice(&[0x58, 0x40]);
out.extend_from_slice(&SIGNATURE);
out
};
let wrapped = |body: Vec<u8>| {
let mut out = vec![0xa1, 0x19, 0x01, 0x8c];
out.extend_from_slice(&body);
out
};
assert!(decode_fields(&assemble(vec![0xa0])).is_err());
let mut bare = vec![0xa1, 0x20];
bare.extend_from_slice(&proof);
assert!(decode_fields(&assemble(wrapped(bare))).is_err());
let mut bare_bstr = vec![0xa1, 0x20, 0x58, small(proof.len())];
bare_bstr.extend_from_slice(&proof);
assert!(decode_fields(&assemble(wrapped(bare_bstr))).is_err());
let mut unwrapped = vec![0xa1, 0x20, 0x81];
unwrapped.extend_from_slice(&proof);
assert!(decode_fields(&assemble(wrapped(unwrapped))).is_err());
assert!(decode_fields(&assemble(wrapped(vec![0xa1, 0x20, 0x80]))).is_err());
let mut two = vec![0xa1, 0x20, 0x82];
for _ in 0..2 {
two.extend_from_slice(&[0x58, small(proof.len())]);
two.extend_from_slice(&proof);
}
assert!(decode_fields(&assemble(wrapped(two))).is_err());
let mut consistency = vec![0xa1, 0x21, 0x81, 0x58, small(proof.len())];
consistency.extend_from_slice(&proof);
assert!(decode_fields(&assemble(wrapped(consistency))).is_err());
let mut extra = vec![
0xa2,
0x19,
0x01,
0x8c,
0xa1,
0x20,
0x81,
0x58,
small(proof.len()),
];
extra.extend_from_slice(&proof);
extra.extend_from_slice(&[0x01, 0x27]); assert!(decode_fields(&assemble(extra)).is_err());
let mut wrong_label = vec![
0xa1,
0x19,
0x01,
0x8b,
0xa1,
0x20,
0x81,
0x58,
small(proof.len()),
];
wrong_label.extend_from_slice(&proof);
assert!(decode_fields(&assemble(wrong_label)).is_err());
}
#[test]
fn malformed_inner_proofs_are_refused() {
let protected = expected_protected(&ANCHOR_KEY);
let assemble = |proof: Vec<u8>| {
let mut out = vec![0xd2, 0x84, 0x58, 0x50];
out.extend_from_slice(&protected);
out.extend_from_slice(&[0xa1, 0x19, 0x01, 0x8c, 0xa1, 0x20, 0x81]);
assert!(proof.len() < 256);
out.extend_from_slice(&[0x58, small(proof.len())]);
out.extend_from_slice(&proof);
out.push(0xf6);
out.extend_from_slice(&[0x58, 0x40]);
out.extend_from_slice(&SIGNATURE);
out
};
assert!(decode_fields(&assemble(vec![0x82, 0x04, 0x01])).is_err());
assert!(decode_fields(&assemble(vec![0x84, 0x04, 0x01, 0x80, 0x00])).is_err());
assert!(decode_fields(&assemble(vec![0x83, 0x20, 0x01, 0x80])).is_err());
assert!(decode_fields(&assemble(vec![0x83, 0x04, 0x20, 0x80])).is_err());
let mut short_node = vec![0x83, 0x04, 0x01, 0x81, 0x58, 0x1f];
short_node.extend_from_slice(&[0x11; 31]);
assert!(decode_fields(&assemble(short_node)).is_err());
assert!(decode_fields(&assemble(vec![0x83, 0x04, 0x01, 0x81, 0x01])).is_err());
let mut flat = vec![0x83, 0x04, 0x01, 0x58, 0x20];
flat.extend_from_slice(&NODE_A);
assert!(decode_fields(&assemble(flat)).is_err());
}
#[test]
fn a_path_longer_than_any_tree_could_require_is_refused_before_hashing() {
let nodes = vec![NODE_A; MAX_PATH_ELEMENTS + 1];
let bytes = artifact_bytes(&ANCHOR_KEY, u64::MAX, 0, &nodes, &SIGNATURE);
assert!(decode_fields(&bytes).is_err());
let at_limit = vec![NODE_A; MAX_PATH_ELEMENTS];
let bytes = artifact_bytes(&ANCHOR_KEY, u64::MAX, 0, &at_limit, &SIGNATURE);
assert_eq!(
decode_fields(&bytes).unwrap().inclusion_path.len(),
MAX_PATH_ELEMENTS
);
}
#[test]
fn a_signature_of_the_wrong_length_is_refused() {
let protected = expected_protected(&ANCHOR_KEY);
let build = |sig_head: &[u8], sig: &[u8]| {
let mut out = vec![0xd2, 0x84, 0x58, 0x50];
out.extend_from_slice(&protected);
out.extend_from_slice(&expected_unprotected(4, 1, &[NODE_A, NODE_B]));
out.push(0xf6);
out.extend_from_slice(sig_head);
out.extend_from_slice(sig);
out
};
assert!(decode_fields(&build(&[0x58, 0x3f], &[0x5e; 63])).is_err());
assert!(decode_fields(&build(&[0x58, 0x41], &[0x5e; 65])).is_err());
assert!(decode_fields(&build(&[0x40], &[])).is_err());
}
#[test]
fn an_attestation_artifact_never_decodes_as_a_receipt() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("id.key");
std::fs::write(&path, b"receipt-cross-protocol-seed-0001").unwrap();
let identity = crate::Ed25519Identity::load(&path).unwrap();
let attestation = crate::attestation::sign_attestation(b"payload", &identity);
assert!(decode_fields(&attestation.to_cose_bytes()).is_err());
}
#[test]
fn a_receipt_never_decodes_as_an_attestation() {
assert!(crate::attestation::Attestation::from_cose_bytes(&sample()).is_err());
}