rpki/tests/test_manifest_decode_errors.rs

412 lines
13 KiB
Rust

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<u8> {
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<u8> {
let mut out = vec![tag];
out.extend(len_bytes(content.len()));
out.extend_from_slice(content);
out
}
fn der_integer_bytes(bytes: &[u8]) -> Vec<u8> {
tlv(0x02, bytes)
}
fn der_integer_u64(v: u64) -> Vec<u8> {
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<u8> {
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<u8> {
tlv(0x18, s.as_bytes())
}
fn der_ia5(s: &str) -> Vec<u8> {
tlv(0x16, s.as_bytes())
}
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
let mut content = vec![unused];
content.extend_from_slice(bytes);
tlv(0x03, &content)
}
fn der_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
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<u8>) -> Vec<u8> {
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
}
fn manifest_der(
version: Option<u64>,
manifest_number: Vec<u8>,
this_update: Vec<u8>,
next_update: Vec<u8>,
file_hash_alg_oid: &str,
file_list: Vec<Vec<u8>>,
) -> Vec<u8> {
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<u8> {
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");
}
}