#![cfg(test)]
use super::*;
#[test]
fn a_delegation_relabelled_as_a_receipt_is_refused_though_its_signature_is_valid() {
let key = root();
let relabelled = encoding::protected_bytes(
"application/vnd.lys.receipt.v1+cbor",
&key.public_key_bytes(),
);
let mutant = signed_envelope(&relabelled, &encoding::payload_bytes(&claim()), &key);
Ed25519Identity::verify(
&key.public_key_bytes(),
&cbor::sig_structure_bytes(&relabelled, &encoding::payload_bytes(&claim())),
&mutant[mutant.len() - 64..],
)
.unwrap();
assert!(matches!(
verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN),
Err(TrustError::DelegationVerification)
));
}
#[test]
fn a_real_receipt_is_not_a_delegation_and_a_delegation_is_not_a_receipt() {
let key = root();
let mut tree = AppendOnlyTree::<RawLeaf>::new();
tree.append_raw(b"genesis");
tree.append_raw(b"leaf");
let path: Vec<[u8; 32]> = tree
.prove_inclusion(1)
.unwrap()
.as_bytes()
.chunks_exact(32)
.map(|c| <[u8; 32]>::try_from(c).unwrap())
.collect();
let receipt_bytes = receipt::sign_receipt(b"leaf", 1, 2, &path, &key)
.unwrap()
.to_cose_bytes();
let delegation_bytes = sign_delegation(&key, &claim()).unwrap();
receipt::verify_receipt_bytes(&receipt_bytes, b"leaf", &key.public_key_bytes()).unwrap();
verify_delegation(&delegation_bytes, &key.public_key_bytes(), DOMAIN, ORIGIN).unwrap();
assert!(verify_delegation(&receipt_bytes, &key.public_key_bytes(), DOMAIN, ORIGIN).is_err());
assert!(
receipt::verify_receipt_bytes(&delegation_bytes, b"leaf", &key.public_key_bytes()).is_err()
);
let delegation_preimage_bytes =
cbor::sig_structure_bytes(&protected_for(&key), &encoding::payload_bytes(&claim()));
assert_eq!(&delegation_preimage_bytes[..12], b"\x84\x6aSignature1");
}
#[test]
fn a_delegation_signed_by_an_attacker_naming_their_own_key_is_refused() {
let evil = attacker();
let forged = sign_delegation(&evil, &claim()).unwrap();
let self_consistent =
verify_delegation(&forged, &evil.public_key_bytes(), DOMAIN, ORIGIN).unwrap();
assert_eq!(self_consistent.root_public_key, evil.public_key_bytes());
assert_eq!(self_consistent.claim.subject_value, ORIGIN);
assert!(matches!(
verify_delegation(&forged, &root().public_key_bytes(), DOMAIN, ORIGIN),
Err(TrustError::DelegationVerification)
));
}
#[test]
fn substituting_the_kid_in_a_genuine_delegation_breaks_the_signature_too() {
let key = root();
let evil = attacker();
let mutant = encoding::envelope_bytes(
&encoding::protected_bytes(encoding::CONTENT_TYPE, &evil.public_key_bytes()),
&encoding::payload_bytes(&claim()),
&key.sign(&delegation_preimage(&key.public_key_bytes(), &claim())),
);
assert!(verify_delegation(&mutant, &evil.public_key_bytes(), DOMAIN, ORIGIN).is_err());
}
#[test]
fn a_kid_that_is_not_exactly_thirty_two_bytes_is_refused() {
let key = root();
let mut refused = 0;
for len in [0u8, 1, 31, 33, 64] {
let protected = protected_with_kid_len(len);
let mutant = signed_envelope(&protected, &encoding::payload_bytes(&claim()), &key);
assert!(
verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN).is_err(),
"a {len}-byte kid was accepted"
);
refused += 1;
}
assert_eq!(refused, 5, "every kid length must have been tried");
let genuine = protected_with_kid_len(32);
assert_eq!(
genuine,
encoding::protected_bytes(encoding::CONTENT_TYPE, &[0x11; 32])
);
}
#[test]
fn an_untagged_cose_sign1_is_refused_though_its_signature_is_valid() {
let key = root();
let good = sign_delegation(&key, &claim()).unwrap();
verify_delegation(&good, &key.public_key_bytes(), DOMAIN, ORIGIN).unwrap();
assert_eq!(good[0], 0xd2, "tag 18");
let untagged = &good[1..];
assert_eq!(untagged[0], 0x84, "a bare 4-array, which is valid COSE");
assert!(
ciborium::de::from_reader::<ciborium::value::Value, _>(untagged).is_ok(),
"the untagged form must be well-formed CBOR for this test to mean anything"
);
assert!(matches!(
verify_delegation(untagged, &key.public_key_bytes(), DOMAIN, ORIGIN),
Err(TrustError::DelegationVerification)
));
}
#[test]
fn a_permuted_payload_map_is_refused_though_its_signature_is_valid() {
let key = root();
let claim = claim();
let mut permuted = vec![0xa6, 0x03, 0x58, 0x20];
permuted.extend_from_slice(&claim.delegated_public_key);
permuted.extend_from_slice(&[0x01, 0x01, 0x02]);
permuted.push(0x60 | u8::try_from(ORIGIN.len()).unwrap());
permuted.extend_from_slice(ORIGIN.as_bytes());
permuted.extend_from_slice(&[0x04, 0x02, 0x05, 0x1b]);
permuted.extend_from_slice(&claim.not_before_unix_ms.to_be_bytes());
permuted.extend_from_slice(&[0x06, 0x19, 0x01, 0x2c]);
let canonical = encoding::payload_bytes(&claim);
assert_eq!(
permuted.len(),
canonical.len(),
"a permutation, not a different payload"
);
assert_ne!(permuted, canonical);
let mutant = signed_envelope(&protected_for(&key), &permuted, &key);
assert!(
ciborium::de::from_reader::<ciborium::value::Value, _>(mutant.as_slice()).is_ok(),
"the mutant must be well-formed CBOR for this test to mean anything"
);
assert!(matches!(
verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN),
Err(TrustError::DelegationVerification)
));
}
#[test]
fn a_non_empty_unprotected_header_is_refused_though_the_signature_still_verifies() {
let key = root();
let good = sign_delegation(&key, &claim()).unwrap();
verify_delegation(&good, &key.public_key_bytes(), DOMAIN, ORIGIN).unwrap();
let unprotected_at = 4 + encoding::PROTECTED_LEN;
assert_eq!(good[unprotected_at], 0xa0, "the empty map");
let mut mutant = good[..unprotected_at].to_vec();
mutant.extend_from_slice(&[0xa1, 0x01, 0x01]); mutant.extend_from_slice(&good[unprotected_at + 1..]);
assert!(
ciborium::de::from_reader::<ciborium::value::Value, _>(mutant.as_slice()).is_ok(),
"the mutant must be well-formed CBOR for this test to mean anything"
);
assert_eq!(&mutant[mutant.len() - 64..], &good[good.len() - 64..]);
assert!(matches!(
verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN),
Err(TrustError::DelegationVerification)
));
}
#[test]
fn an_unknown_role_is_refused_though_its_signature_is_valid() {
let key = root();
let canonical = encoding::payload_bytes(&claim());
let role_at = role_offset(&canonical);
let mut refused = 0;
for role in [0u8, 1, 4, 7, 23] {
let mut payload = canonical.clone();
payload[role_at + 1] = role;
assert_eq!(payload.len(), canonical.len());
let mutant = signed_envelope(&protected_for(&key), &payload, &key);
assert!(
matches!(
verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN),
Err(TrustError::DelegationVerification)
),
"role {role} was accepted"
);
refused += 1;
}
assert_eq!(refused, 5, "every unknown role must have been tried");
let ok = signed_envelope(&protected_for(&key), &canonical, &key);
verify_delegation(&ok, &key.public_key_bytes(), DOMAIN, ORIGIN).unwrap();
}
#[test]
fn a_flipped_signature_bit_is_refused() {
let key = root();
let good = sign_delegation(&key, &claim()).unwrap();
let at = good.len() - 64;
let mut refused = 0;
for byte in [0usize, 1, 31, 32, 62, 63] {
for bit in [0x01u8, 0x80] {
let mut mutant = good.clone();
mutant[at + byte] ^= bit;
assert!(
verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN).is_err(),
"a flip of bit {bit:#04x} in signature byte {byte} was accepted"
);
refused += 1;
}
}
assert_eq!(refused, 12, "every flip must have been tried");
}
#[test]
fn a_non_canonical_signature_scalar_is_refused() {
let key = root();
let mut mutant = sign_delegation(&key, &claim()).unwrap();
let last = mutant.len() - 1;
mutant[last] |= 0xe0;
assert!(verify_delegation(&mutant, &key.public_key_bytes(), DOMAIN, ORIGIN).is_err());
}