412 lines
13 KiB
Rust
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");
|
|
}
|
|
}
|