add signed object, manifest impl. add coverage script

This commit is contained in:
2026-01-29 10:35:02 +08:00
parent 5e474fffd2
commit 2fb36712e2
19 changed files with 4114 additions and 76 deletions

93
tests/test_common.rs Normal file
View File

@@ -0,0 +1,93 @@
use rpki::data_model::common::{
algorithm_params_absent_or_null, Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned,
};
use x509_parser::prelude::FromDer;
use x509_parser::x509::AlgorithmIdentifier;
use x509_parser::time::ASN1Time;
#[test]
fn big_unsigned_helpers() {
let n = BigUnsigned { bytes_be: vec![0] };
assert_eq!(n.to_u64(), Some(0));
assert_eq!(n.to_hex_upper(), "00");
let n = BigUnsigned {
bytes_be: vec![0x01, 0x02, 0x03],
};
assert_eq!(n.to_u64(), Some(0x010203));
assert_eq!(n.to_hex_upper(), "010203");
let n = BigUnsigned {
bytes_be: vec![0; 9],
};
assert_eq!(n.to_u64(), None);
}
#[test]
fn time_encoding_validation() {
let t = Asn1TimeUtc {
utc: time::OffsetDateTime::from_unix_timestamp(0).unwrap(),
encoding: Asn1TimeEncoding::UtcTime,
};
t.validate_encoding_rfc5280("t").expect("utc ok");
let t = Asn1TimeUtc {
utc: time::OffsetDateTime::parse("2050-01-01T00:00:00Z", &time::format_description::well_known::Rfc3339)
.unwrap(),
encoding: Asn1TimeEncoding::UtcTime,
};
assert!(t.validate_encoding_rfc5280("t").is_err());
}
#[test]
fn algorithm_params_absent_or_null_helper() {
// AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL }
// Using sha256WithRSAEncryption with NULL parameters.
let alg_null = hex::decode(
"300D06092A864886F70D01010B0500",
)
.unwrap();
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_null).expect("parse AlgorithmIdentifier");
assert!(algorithm_params_absent_or_null(&id));
// Same OID, but parameters = INTEGER 1 (invalid for our helper).
let alg_int = hex::decode(
"300E06092A864886F70D01010B020101",
)
.unwrap();
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_int).expect("parse AlgorithmIdentifier");
assert!(!algorithm_params_absent_or_null(&id));
// parameters absent
let oid = der_parser::Oid::from(&[1u64, 2, 3]).unwrap();
let id = AlgorithmIdentifier::new(oid, None);
assert!(algorithm_params_absent_or_null(&id));
}
#[test]
fn asn1_time_to_model_helper_covers_branches() {
let dt_utc = time::OffsetDateTime::parse(
"2024-01-01T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.unwrap();
let t = ASN1Time::new_utc(dt_utc);
let m = rpki::data_model::common::asn1_time_to_model(t);
assert_eq!(m.encoding, Asn1TimeEncoding::UtcTime);
let dt_gen = time::OffsetDateTime::parse(
"2050-01-01T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.unwrap();
let t = ASN1Time::new_generalized(dt_gen);
let m = rpki::data_model::common::asn1_time_to_model(t);
assert_eq!(m.encoding, Asn1TimeEncoding::GeneralizedTime);
}
#[test]
fn big_unsigned_from_biguint_zero_encodes_as_single_zero_byte() {
let z = der_parser::num_bigint::BigUint::from(0u32);
let n = BigUnsigned::from_biguint(&z);
assert_eq!(n.bytes_be, vec![0]);
}

127
tests/test_crl_errors.rs Normal file
View File

@@ -0,0 +1,127 @@
use rpki::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
use x509_parser::prelude::FromDer;
use x509_parser::prelude::X509Certificate;
const TEST_NO_CRLSIGN_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
fn test_no_crlsign_cert_der() -> Vec<u8> {
use base64::{engine::general_purpose, Engine as _};
general_purpose::STANDARD
.decode(TEST_NO_CRLSIGN_CERT_DER_B64)
.expect("decode base64 cert")
}
#[test]
fn verify_errors_are_reported() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer cert");
let mut crl = RpkixCrl::decode_der(&crl_der).expect("decode");
// IssuerSubjectMismatch.
crl.issuer_dn = "CN=Not The Issuer".to_string();
let err = crl
.verify_signature_with_issuer_certificate_der(&issuer_cert_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerSubjectMismatch { .. }));
// AkiSkiMismatch (force issuer dn to match, then change AKI).
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
crl.issuer_dn = issuer_cert.subject().to_string();
crl.extensions.authority_key_identifier = vec![0u8; 20];
let err = crl
.verify_signature_with_issuer_certificate_der(&issuer_cert_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::AkiSkiMismatch));
// IssuerKeyUsageMissingCrlSign (use a cert with KeyUsage present but without CRLSign).
let no_crlsign_der = test_no_crlsign_cert_der();
let (_rem, no_crlsign_cert) = X509Certificate::from_der(&no_crlsign_der).unwrap();
crl.issuer_dn = no_crlsign_cert.subject().to_string();
let err = crl
.verify_signature_with_issuer_certificate_der(&no_crlsign_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerKeyUsageMissingCrlSign));
// InvalidSignature: verify cryptographically with the wrong SPKI.
let no_crlsign_spki_der = no_crlsign_cert.public_key().raw.to_vec();
let err = crl
.verify_signature_with_issuer_spki_der(&no_crlsign_spki_der)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::InvalidSignature(_)));
// IssuerCertificateParse / trailing bytes.
let err = crl
.verify_signature_with_issuer_certificate_der(b"not a cert")
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerCertificateParse(_)));
let mut bad = issuer_cert_der.clone();
bad.push(0);
let err = crl
.verify_signature_with_issuer_certificate_der(&bad)
.unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerCertificateTrailingBytes(1)));
}
#[test]
fn verify_signature_with_spki_der_paths() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer cert");
let crl = RpkixCrl::decode_der(&crl_der).expect("decode");
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
let spki_der = issuer_cert.public_key().raw.to_vec();
crl.verify_signature_with_issuer_spki_der(&spki_der)
.expect("verify with spki der");
let mut bad = spki_der;
bad.push(0);
let err = crl.verify_signature_with_issuer_spki_der(&bad).unwrap_err();
assert!(matches!(err, CrlVerifyError::IssuerSpkiTrailingBytes(1)));
}
#[test]
fn decode_rejects_trailing_bytes() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let mut bad = crl_der.clone();
bad.push(0);
let err = RpkixCrl::decode_der(&bad).unwrap_err();
assert!(matches!(err, CrlDecodeError::TrailingBytes(1)));
}
#[test]
fn verify_rejects_crl_with_trailing_bytes_in_raw_der() {
let crl_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
)
.expect("read CRL fixture");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer cert");
let mut crl = RpkixCrl::decode_der(&crl_der).expect("decode");
crl.raw_der.push(0);
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
let spki_der = issuer_cert.public_key().raw.to_vec();
let err = crl.verify_signature_with_issuer_spki_der(&spki_der).unwrap_err();
assert!(matches!(err, CrlVerifyError::CrlTrailingBytes(1)));
}

View File

@@ -0,0 +1,35 @@
use rpki::data_model::manifest::{ManifestEContent, ManifestObject};
#[test]
fn decode_manifest_fixture_smoke() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let mft = ManifestObject::decode_der(&der).expect("decode manifest object");
assert_eq!(mft.manifest.version, 0);
assert_eq!(mft.manifest.file_hash_alg, rpki::data_model::oid::OID_SHA256);
assert!(mft.manifest.next_update > mft.manifest.this_update);
assert!(!mft.manifest.files.is_empty());
// The manifest file MUST NOT be listed in its own fileList.
assert!(mft
.manifest
.files
.iter()
.all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft")));
}
#[test]
fn decode_manifest_econtent_from_fixture_signed_object() {
let so_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = rpki::data_model::signed_object::RpkiSignedObject::decode_der(&so_der)
.expect("decode signed object");
let e = ManifestEContent::decode_der(&so.signed_data.encap_content_info.econtent)
.expect("decode manifest eContent");
assert_eq!(e.version, 0);
}

View File

@@ -0,0 +1,411 @@
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");
}
}

View File

@@ -0,0 +1,29 @@
use rpki::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256, OID_SIGNED_DATA};
use rpki::data_model::signed_object::RpkiSignedObject;
#[test]
fn decode_manifest_signed_object_smoke() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
assert_eq!(so.content_info_content_type, OID_SIGNED_DATA);
assert_eq!(so.signed_data.version, 3);
assert_eq!(so.signed_data.digest_algorithms, vec![OID_SHA256.to_string()]);
assert_eq!(
so.signed_data.encap_content_info.econtent_type,
OID_CT_RPKI_MANIFEST
);
assert_eq!(so.signed_data.certificates.len(), 1);
assert!(!so.signed_data.certificates[0]
.sia_signed_object_uris
.is_empty());
assert!(so.signed_data.certificates[0]
.sia_signed_object_uris
.iter()
.any(|u| u.starts_with("rsync://")));
assert_eq!(so.signed_data.signer_infos.len(), 1);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,164 @@
use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectVerifyError};
use x509_parser::prelude::FromDer;
use x509_parser::prelude::X509Certificate;
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_null() -> Vec<u8> {
vec![0x05, 0x00]
}
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_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
let mut content = Vec::new();
for c in children {
content.extend(c);
}
tlv(0x30, &content)
}
fn der_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
let mut content = vec![unused];
content.extend_from_slice(bytes);
tlv(0x03, &content)
}
fn rsa_spki_der_with_modulus_bytes(modulus: &[u8], exponent: u64) -> Vec<u8> {
// SubjectPublicKeyInfo for RSA public key:
// SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
// subjectPublicKey contains RSAPublicKey DER.
let alg = der_sequence(vec![
der_oid(rpki::data_model::oid::OID_RSA_ENCRYPTION),
der_null(),
]);
let rsa_pk = der_sequence(vec![der_integer_bytes(modulus), der_integer_u64(exponent)]);
let spk = der_bit_string(0, &rsa_pk);
der_sequence(vec![alg, spk])
}
#[test]
fn verify_mft_cms_signature_with_embedded_ee_cert() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
so.verify_signature()
.expect("CMS signature should verify with embedded EE cert");
}
#[test]
fn verify_fails_with_wrong_spki() {
let mft_der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&mft_der).expect("decode signed object");
let issuer_cert_der = std::fs::read(
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
)
.expect("read issuer certificate fixture");
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).expect("parse cert");
let wrong_spki_der = issuer_cert.public_key().raw.to_vec();
let err = so
.verify_signature_with_ee_spki_der(&wrong_spki_der)
.unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
}
#[test]
fn verify_fails_with_tampered_signature() {
let 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(&der).expect("decode signed object");
so.signed_data.signer_infos[0].signature[0] ^= 0x01;
let err = so.verify_signature().unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
}
#[test]
fn verify_rejects_spki_der_with_trailing_bytes() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
let mut spki_der = so.signed_data.certificates[0].spki_der.clone();
spki_der.push(0);
let err = so
.verify_signature_with_ee_spki_der(&spki_der)
.unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::EeSpkiTrailingBytes(1)));
}
#[test]
fn verify_with_all_zero_modulus_exercises_strip_leading_zeros_fallback() {
let der = std::fs::read(
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
)
.expect("read MFT fixture");
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
// modulus INTEGER 0, exponent 65537 (valid exponent encoding); signature verification must fail
// but the SPKI parsing path should succeed.
let spki_der = rsa_spki_der_with_modulus_bytes(&[0x00], 65537);
let err = so
.verify_signature_with_ee_spki_der(&spki_der)
.unwrap_err();
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
}