#![cfg(test)]
use super::*;
use crate::attestation::encoding;
use crate::attestation::sign::verify_attestation;
use crate::cbor;
use crate::keys::identity::Ed25519Identity;
const GOLDEN_SEED: &[u8; 32] = b"lys-cose-conformance-test-seed01";
const GOLDEN_PAYLOAD: &[u8] = b"lys attestation conformance payload\n";
const GOLDEN_TIMESTAMP: i64 = 1_700_000_000_000;
const GOLDEN_ARTIFACT_HEX: &str = "d2845850a301270378276170706c69636174696f6e2f766e642e6c79732e6174746573746174696f6e2e76322b63626f72045820214e41cc8475f5bc9af68dbf33fa56e8adf5144a4a81134268f4ae379e103bfda0582ea20158209404f8b8cec8ad98a88b106d9345d518c273f012c1306b8af5103e865997191e021b0000018bcfe568005840ba6eae66b3a04f25e6116e7278ae8b0e2174a2964bb0917ae53ab2ef326c22a706e90cae6886b20ecd086fee4b515de046d450f90de81f9aa46a9b34bd7f1a09";
const OFFSET_UNPROTECTED: usize = 84;
const OFFSET_CLAIMS_HEAD: usize = 85;
fn hex_to_bytes(hex: &str) -> Vec<u8> {
assert_eq!(hex.len() % 2, 0);
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect()
}
fn golden_artifact() -> Vec<u8> {
hex_to_bytes(GOLDEN_ARTIFACT_HEX)
}
fn golden_identity() -> (tempfile::TempDir, Ed25519Identity) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("golden.key");
std::fs::write(&path, GOLDEN_SEED).unwrap();
let identity = Ed25519Identity::load(&path).unwrap();
(dir, identity)
}
fn golden_attestation() -> Attestation {
Attestation::from_cose_bytes(&golden_artifact()).unwrap()
}
fn push_bstr(out: &mut Vec<u8>, bytes: &[u8]) {
let len = u8::try_from(bytes.len()).unwrap();
if len < 24 {
out.push(0x40 | len);
} else {
out.push(0x58);
out.push(len);
}
out.extend_from_slice(bytes);
}
fn assemble(protected: &[u8], unprotected: &[u8], claims: &[u8], signature: &[u8]) -> Vec<u8> {
let mut out = vec![0xd2, 0x84];
push_bstr(&mut out, protected);
out.extend_from_slice(unprotected);
push_bstr(&mut out, claims);
push_bstr(&mut out, signature);
out
}
fn signed_custom(protected: &[u8], claims: &[u8]) -> Vec<u8> {
let (_dir, identity) = golden_identity();
let signature = identity.sign(&cbor::sig_structure_bytes(protected, claims));
assemble(protected, &[0xa0], claims, &signature)
}
#[test]
fn golden_artifact_parses_to_expected_fields() {
let (_dir, identity) = golden_identity();
let att = golden_attestation();
assert_eq!(att.signer_public_key, identity.public_key_bytes());
assert_eq!(att.timestamp, GOLDEN_TIMESTAMP);
verify_attestation(&att, GOLDEN_PAYLOAD).unwrap();
}
#[test]
fn to_cose_bytes_reproduces_the_input_bytes_exactly() {
let att = golden_attestation();
assert_eq!(att.to_cose_bytes(), golden_artifact());
}
#[test]
fn round_trip_from_struct_is_identity() {
let att = golden_attestation();
let restored = Attestation::from_cose_bytes(&att.to_cose_bytes()).unwrap();
assert_eq!(restored, att);
}
#[test]
fn mutant_a_unprotected_header_smuggling_is_rejected() {
let golden = golden_artifact();
let mut mutant = golden[..OFFSET_UNPROTECTED].to_vec();
mutant.extend_from_slice(&[0xa1, 0x0a, 0x0a]); mutant.extend_from_slice(&golden[OFFSET_UNPROTECTED + 1..]);
assert!(Attestation::from_cose_bytes(&mutant).is_err());
}
#[test]
fn mutant_b_untagged_artifact_is_rejected() {
let golden = golden_artifact();
assert!(Attestation::from_cose_bytes(&golden[1..]).is_err());
let mut retagged = golden;
retagged[0] = 0xd3; assert!(Attestation::from_cose_bytes(&retagged).is_err());
}
#[test]
fn mutant_c_indefinite_length_array_is_rejected() {
let golden = golden_artifact();
let mut mutant = vec![0xd2, 0x9f];
mutant.extend_from_slice(&golden[2..]);
mutant.push(0xff);
assert!(Attestation::from_cose_bytes(&mutant).is_err());
}
#[test]
fn mutant_d_oversized_length_head_is_rejected() {
let golden = golden_artifact();
let mut mutant = golden[..OFFSET_CLAIMS_HEAD].to_vec();
mutant.push(0x59);
mutant.push(0x00);
mutant.extend_from_slice(&golden[OFFSET_CLAIMS_HEAD + 1..]);
assert!(Attestation::from_cose_bytes(&mutant).is_err());
}
#[test]
fn mutant_d_noncanonical_timestamp_head_is_rejected() {
let (_dir, identity) = golden_identity();
let protected = encoding::protected_bytes(&identity.public_key_bytes());
let canonical_claims = encoding::claims_bytes(&[0x11; 32], 1);
let mut claims = canonical_claims[..canonical_claims.len() - 1].to_vec();
claims.extend_from_slice(&[0x1b, 0, 0, 0, 0, 0, 0, 0, 1]);
let mutant = signed_custom(&protected, &claims);
assert!(Attestation::from_cose_bytes(&mutant).is_err());
}
#[test]
fn mutant_e_alg_substitution_is_rejected() {
let (_dir, identity) = golden_identity();
let mut protected = encoding::protected_bytes(&identity.public_key_bytes());
assert_eq!(protected[2], 0x27); protected[2] = 0x26; let claims = encoding::claims_bytes(&[0x22; 32], GOLDEN_TIMESTAMP);
let mutant = signed_custom(&protected, &claims);
assert!(Attestation::from_cose_bytes(&mutant).is_err());
}
#[test]
fn mutant_f_old_v1_preimage_signature_parses_but_fails_verification() {
let (_dir, identity) = golden_identity();
let payload_hash: [u8; 32] = {
use sha2::{Digest, Sha256};
Sha256::digest(GOLDEN_PAYLOAD).into()
};
let mut preimage = b"lys/attestation/v1".to_vec();
preimage.extend_from_slice(&GOLDEN_TIMESTAMP.to_le_bytes());
preimage.extend_from_slice(&payload_hash);
let spliced_signature = identity.sign(&preimage);
let artifact = encoding::artifact_bytes(
&identity.public_key_bytes(),
&payload_hash,
GOLDEN_TIMESTAMP,
&spliced_signature,
);
let att = Attestation::from_cose_bytes(&artifact).unwrap();
assert!(verify_attestation(&att, GOLDEN_PAYLOAD).is_err());
}
#[test]
fn trailing_garbage_is_rejected() {
let mut padded = golden_artifact();
padded.push(0x00);
assert!(Attestation::from_cose_bytes(&padded).is_err());
}
#[test]
fn oversize_input_is_rejected_before_parsing() {
let mut oversize = golden_artifact();
oversize.resize(1025, 0x00);
assert!(Attestation::from_cose_bytes(&oversize).is_err());
let zeros = vec![0u8; 4096];
assert!(Attestation::from_cose_bytes(&zeros).is_err());
}
#[test]
fn empty_and_truncated_inputs_are_rejected() {
let golden = golden_artifact();
assert!(Attestation::from_cose_bytes(&[]).is_err());
assert!(Attestation::from_cose_bytes(&golden[..golden.len() - 1]).is_err());
assert!(Attestation::from_cose_bytes(&golden[..10]).is_err());
}
#[test]
fn empty_claims_and_extra_claims_are_rejected() {
let (_dir, identity) = golden_identity();
let protected = encoding::protected_bytes(&identity.public_key_bytes());
let empty_claims = vec![0xa0];
assert!(Attestation::from_cose_bytes(&signed_custom(&protected, &empty_claims)).is_err());
let mut extra_claims = encoding::claims_bytes(&[0x33; 32], GOLDEN_TIMESTAMP);
extra_claims[0] = 0xa3; extra_claims.extend_from_slice(&[0x03, 0x00]); assert!(Attestation::from_cose_bytes(&signed_custom(&protected, &extra_claims)).is_err());
}
#[test]
fn wrong_kid_length_is_rejected() {
let (_dir, identity) = golden_identity();
let canonical = encoding::protected_bytes(&identity.public_key_bytes());
let mut protected = canonical[..canonical.len() - 33].to_vec();
assert_eq!(protected.pop(), Some(0x58)); protected.extend_from_slice(&[0x58, 31]); protected.extend_from_slice(&[0x44; 31]);
let claims = encoding::claims_bytes(&[0x55; 32], GOLDEN_TIMESTAMP);
assert!(Attestation::from_cose_bytes(&signed_custom(&protected, &claims)).is_err());
}
#[test]
fn duplicate_claims_keys_are_rejected() {
let (_dir, identity) = golden_identity();
let protected = encoding::protected_bytes(&identity.public_key_bytes());
let mut claims = vec![0xa2];
for _ in 0..2 {
claims.push(0x01);
claims.push(0x58);
claims.push(32);
claims.extend_from_slice(&[0x66; 32]);
}
assert!(Attestation::from_cose_bytes(&signed_custom(&protected, &claims)).is_err());
}
#[test]
fn reordered_protected_keys_are_rejected() {
let (_dir, identity) = golden_identity();
let canonical = encoding::protected_bytes(&identity.public_key_bytes());
let mut protected = vec![0xa3];
protected.extend_from_slice(&canonical[3..45]); protected.extend_from_slice(&canonical[1..3]); protected.extend_from_slice(&canonical[45..]); assert_eq!(protected.len(), canonical.len());
let claims = encoding::claims_bytes(&[0x77; 32], GOLDEN_TIMESTAMP);
assert!(Attestation::from_cose_bytes(&signed_custom(&protected, &claims)).is_err());
}
#[test]
fn wrong_signature_length_is_rejected() {
let (_dir, identity) = golden_identity();
let protected = encoding::protected_bytes(&identity.public_key_bytes());
let claims = encoding::claims_bytes(&[0x88; 32], GOLDEN_TIMESTAMP);
let artifact = assemble(&protected, &[0xa0], &claims, &[0xab; 63]);
assert!(Attestation::from_cose_bytes(&artifact).is_err());
}
#[test]
fn wrong_content_type_is_rejected() {
let (_dir, identity) = golden_identity();
let mut protected = encoding::protected_bytes(&identity.public_key_bytes());
protected[6] = protected[6].to_ascii_uppercase();
let claims = encoding::claims_bytes(&[0x99; 32], GOLDEN_TIMESTAMP);
assert!(Attestation::from_cose_bytes(&signed_custom(&protected, &claims)).is_err());
}
#[test]
fn full_i64_timestamp_range_parses_round_trips_and_verifies() {
let (_dir, identity) = golden_identity();
let payload_hash: [u8; 32] = {
use sha2::{Digest, Sha256};
Sha256::digest(GOLDEN_PAYLOAD).into()
};
let protected = encoding::protected_bytes(&identity.public_key_bytes());
for ts in [i64::MIN, -1, 0, i64::MAX] {
let claims = encoding::claims_bytes(&payload_hash, ts);
let artifact = signed_custom(&protected, &claims);
let att = Attestation::from_cose_bytes(&artifact).unwrap();
assert_eq!(att.timestamp, ts, "timestamp {ts} did not round-trip");
assert_eq!(att.to_cose_bytes(), artifact);
verify_attestation(&att, GOLDEN_PAYLOAD).unwrap();
}
}
#[test]
fn out_of_i64_range_timestamp_is_rejected() {
let (_dir, identity) = golden_identity();
let protected = encoding::protected_bytes(&identity.public_key_bytes());
let base = encoding::claims_bytes(&[0xaa; 32], 0);
let overflow_heads: &[[u8; 9]] = &[
[0x1b, 0x80, 0, 0, 0, 0, 0, 0, 0], [0x3b, 0x80, 0, 0, 0, 0, 0, 0, 0], ];
for head in overflow_heads {
let mut claims = base[..base.len() - 1].to_vec();
claims.extend_from_slice(head);
let mutant = signed_custom(&protected, &claims);
assert!(Attestation::from_cose_bytes(&mutant).is_err());
}
}
#[test]
fn all_rejections_collapse_to_invalid_signature() {
let golden = golden_artifact();
let mut retagged = golden.clone();
retagged[0] = 0xd3;
let mut padded = golden.clone();
padded.push(0x00);
for bad in [retagged.as_slice(), &padded, &golden[1..], b"junk"] {
let err = Attestation::from_cose_bytes(bad).unwrap_err();
assert!(matches!(err, crate::error::TrustError::InvalidSignature));
assert_eq!(err.to_string(), "invalid signature");
}
}