use rpki::data_model::common::BigUnsigned; use rpki::data_model::manifest::{ManifestDecodeError, ManifestEContent, ManifestObject}; use rpki::data_model::signed_object::RpkiSignedObject; fn len_bytes(len: usize) -> Vec { if len < 128 { vec![len as u8] } else { let mut tmp = Vec::new(); let mut n = len; while n > 0 { tmp.push((n & 0xFF) as u8); n >>= 8; } tmp.reverse(); let mut out = vec![0x80 | (tmp.len() as u8)]; out.extend(tmp); out } } fn tlv(tag: u8, content: &[u8]) -> Vec { let mut out = vec![tag]; out.extend(len_bytes(content.len())); out.extend_from_slice(content); out } fn der_integer_bytes(bytes: &[u8]) -> Vec { tlv(0x02, bytes) } fn der_integer_u64(v: u64) -> Vec { let mut bytes = Vec::new(); let mut n = v; if n == 0 { bytes.push(0); } else { while n > 0 { bytes.push((n & 0xFF) as u8); n >>= 8; } bytes.reverse(); if bytes[0] & 0x80 != 0 { bytes.insert(0, 0); } } der_integer_bytes(&bytes) } fn der_oid(oid: &str) -> Vec { use std::str::FromStr; use der_parser::asn1_rs::ToDer; let oid = der_parser::Oid::from_str(oid).unwrap(); oid.to_der_vec().unwrap() } fn der_generalized_time_z(s: &str) -> Vec { tlv(0x18, s.as_bytes()) } fn der_ia5(s: &str) -> Vec { tlv(0x16, s.as_bytes()) } fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec { let mut content = vec![unused]; content.extend_from_slice(bytes); tlv(0x03, &content) } fn der_sequence(children: Vec>) -> Vec { let mut content = Vec::new(); for c in children { content.extend(c); } tlv(0x30, &content) } fn der_cs_explicit(tag_no: u8, inner_der: Vec) -> Vec { tlv(0xA0 | (tag_no & 0x1F), &inner_der) } fn manifest_der( version: Option, manifest_number: Vec, this_update: Vec, next_update: Vec, file_hash_alg_oid: &str, file_list: Vec>, ) -> Vec { let mut fields = Vec::new(); if let Some(v) = version { fields.push(der_cs_explicit(0, der_integer_u64(v))); } fields.push(der_integer_bytes(&manifest_number)); fields.push(this_update); fields.push(next_update); fields.push(der_oid(file_hash_alg_oid)); fields.push(der_sequence(file_list)); der_sequence(fields) } fn file_and_hash(file: &str, unused: u8, hash: &[u8]) -> Vec { der_sequence(vec![der_ia5(file), der_bit_string(unused, hash)]) } #[test] fn manifest_econtent_version_must_be_zero_when_present() { let der = manifest_der( Some(1), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidManifestVersion(1))); } #[test] fn manifest_number_too_long_rejected() { let long = vec![1u8; 21]; let der = manifest_der( Some(0), long, der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::ManifestNumberTooLong)); } #[test] fn manifest_number_negative_rejected() { // INTEGER -1 encoded as 0xFF let der = manifest_der( Some(0), vec![0xFF], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidManifestNumber)); } #[test] fn this_update_and_next_update_must_be_generalized_time() { let der = manifest_der( Some(0), vec![1], tlv(0x17, b"240101000000Z"), // UTCTime der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidThisUpdate)); let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), tlv(0x17, b"240101000000Z"), // UTCTime rpki::data_model::oid::OID_SHA256, vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidNextUpdate)); } #[test] fn next_update_must_be_later() { let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260102000000Z"), der_generalized_time_z("20260101000000Z"), rpki::data_model::oid::OID_SHA256, vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::NextUpdateNotLater)); } #[test] fn file_hash_alg_must_be_sha256() { let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), "1.2.3.4", vec![], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidFileHashAlg(_))); } #[test] fn file_list_entry_validation() { let hash = vec![0u8; 32]; // Invalid filename (bad char) let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![file_and_hash("bad!.roa", 0, &hash)], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidFileName(_))); // Non-octet-aligned BIT STRING let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![file_and_hash("ok.roa", 1, &hash)], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::HashNotOctetAligned)); // Wrong hash length let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![file_and_hash("ok.roa", 0, &[0u8; 31])], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidHashLength(31))); } #[test] fn manifest_object_requires_correct_econtent_type() { let so_der = std::fs::read( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ) .expect("read MFT fixture"); let mut so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object"); so.signed_data.encap_content_info.econtent_type = "1.2.3.4".to_string(); let err = ManifestObject::from_signed_object(so).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidEContentType(_))); } #[test] fn manifest_sequence_length_is_validated() { let der = der_sequence(vec![der_integer_u64(0)]); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!( err, ManifestDecodeError::InvalidManifestSequenceLen(_) )); } #[test] fn file_list_must_be_sequence_and_entry_shape_validated() { // Patch last byte to change SEQUENCE tag 0x30 to NULL tag 0x05 in the inner fileList. // Build a manifest with fileList = NULL explicitly. let der = { let mut fields = Vec::new(); fields.push(der_cs_explicit(0, der_integer_u64(0))); fields.push(der_integer_u64(1)); fields.push(der_generalized_time_z("20260101000000Z")); fields.push(der_generalized_time_z("20260102000000Z")); fields.push(der_oid(rpki::data_model::oid::OID_SHA256)); fields.push(tlv(0x05, &[])); der_sequence(fields) }; let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidFileList)); // FileAndHash not SEQUENCE of 2 let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![der_sequence(vec![der_ia5("ok.roa")])], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidFileAndHash)); } #[test] fn version_tag_must_be_context_specific_0() { let der = { let mut fields = Vec::new(); // Wrong tag number [1] fields.push(der_cs_explicit(1, der_integer_u64(0))); fields.push(der_integer_u64(1)); fields.push(der_generalized_time_z("20260101000000Z")); fields.push(der_generalized_time_z("20260102000000Z")); fields.push(der_oid(rpki::data_model::oid::OID_SHA256)); fields.push(der_sequence(vec![])); der_sequence(fields) }; let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::Parse(_))); } #[test] fn parse_manifest_number_helper_exercises_zero_encoding() { let z = der_parser::num_bigint::BigUint::from(0u32); let n = BigUnsigned::from_biguint(&z); assert_eq!(n.bytes_be, vec![0]); } #[test] fn manifest_econtent_trailing_bytes_are_rejected() { let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![], ); let mut bad = der.clone(); bad.push(0); let err = ManifestEContent::decode_der(&bad).unwrap_err(); assert!(matches!(err, ManifestDecodeError::TrailingBytes(1))); } #[test] fn manifest_version_rejects_trailing_bytes_inside_explicit_tag() { let mut version_inner = der_integer_u64(0); version_inner.extend(tlv(0x05, &[])); let der = { let mut fields = Vec::new(); fields.push(der_cs_explicit(0, version_inner)); fields.push(der_integer_u64(1)); fields.push(der_generalized_time_z("20260101000000Z")); fields.push(der_generalized_time_z("20260102000000Z")); fields.push(der_oid(rpki::data_model::oid::OID_SHA256)); fields.push(der_sequence(vec![])); der_sequence(fields) }; let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::Parse(_))); } #[test] fn manifest_rejects_hash_with_wrong_type() { let hash = vec![0u8; 32]; let entry = der_sequence(vec![der_ia5("ok.roa"), tlv(0x04, &hash)]); // OCTET STRING, not BIT STRING let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![entry], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidHashType)); } #[test] fn file_name_validation_branches_are_exercised() { let hash = vec![0u8; 32]; let cases = [ "noext", // missing '.' ".roa", // empty base "a.roaa", // ext len != 3 "a.r0a", // ext not alphabetic "a.txt", // ext not allowlisted ]; for name in cases { let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![file_and_hash(name, 0, &hash)], ); let err = ManifestEContent::decode_der(&der).unwrap_err(); assert!(matches!(err, ManifestDecodeError::InvalidFileName(_))); } } #[test] fn iana_registry_filename_extensions_are_accepted() { let hash = vec![0u8; 32]; for ext in rpki::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS { let name = format!("ok.{ext}"); let der = manifest_der( Some(0), vec![1], der_generalized_time_z("20260101000000Z"), der_generalized_time_z("20260102000000Z"), rpki::data_model::oid::OID_SHA256, vec![file_and_hash(&name, 0, &hash)], ); ManifestEContent::decode_der(&der).expect("manifest should accept IANA extension"); } }