增加aspa对象解析
This commit is contained in:
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/E0AAy4jgV_BtY7GQELCxALV6Q5U.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/E0AAy4jgV_BtY7GQELCxALV6Q5U.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/IdgNYO8v_dEbdoPuHFTpsjA3l0U.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/IdgNYO8v_dEbdoPuHFTpsjA3l0U.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/UDn6-ZD0WTj18D8nHih3D--ZO_s.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/UDn6-ZD0WTj18D8nHih3D--ZO_s.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/Vvx1bC8uAflbQPltsuM_idhPJzQ.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/Vvx1bC8uAflbQPltsuM_idhPJzQ.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fDvN8JDIeg8h7b5wWCFK_F7J_SY.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fDvN8JDIeg8h7b5wWCFK_F7J_SY.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fykdvBDNLvaFDLOeKcvquSaaNlM.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/fykdvBDNLvaFDLOeKcvquSaaNlM.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/lAN537Pqyt3IsX0kCwc4qs0Cejk.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/lAN537Pqyt3IsX0kCwc4qs0Cejk.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/laI1_vEYtlNzj-K0SXR_yGpd1TA.gbr
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/laI1_vEYtlNzj-K0SXR_yGpd1TA.gbr
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.crl
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.mft
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ovsCA/IOUcOeBGM_Tb4dwfvswY4bnNZYY.mft
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/xF8fZFbI8NRA4qk_N5CLpw6Nmj8.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/xF8fZFbI8NRA4qk_N5CLpw6Nmj8.roa
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/y9dGCqfvXd6vKRjYx3GJikg6pSw.roa
vendored
Normal file
BIN
tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/y9dGCqfvXd6vKRjYx3GJikg6pSw.roa
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.crl
vendored
Normal file
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.crl
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.mft
vendored
Normal file
BIN
tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nlrssf/cdFOuyVdwFjUv6WlHJP3P4MKuI8.mft
vendored
Normal file
Binary file not shown.
25
tests/test_aspa_decode.rs
Normal file
25
tests/test_aspa_decode.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaObject};
|
||||
|
||||
#[test]
|
||||
fn decode_aspa_fixture_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
assert_eq!(aspa.econtent_type, rpki::data_model::oid::OID_CT_ASPA);
|
||||
assert_eq!(aspa.aspa.version, 1);
|
||||
assert_ne!(aspa.aspa.customer_as_id, 0);
|
||||
assert!(!aspa.aspa.provider_as_ids.is_empty());
|
||||
println!("{aspa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_aspa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let err = AspaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
111
tests/test_aspa_econtent_decode_errors.rs
Normal file
111
tests/test_aspa_econtent_decode_errors.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaEContent};
|
||||
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
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 cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn aspa_attestation_explicit_version(version: u64, customer: u64, providers: Vec<u64>) -> Vec<u8> {
|
||||
let providers_der = der_sequence(providers.into_iter().map(der_integer_u64).collect());
|
||||
der_sequence(vec![
|
||||
cs_explicit(0, der_integer_u64(version)),
|
||||
der_integer_u64(customer),
|
||||
providers_der,
|
||||
])
|
||||
}
|
||||
|
||||
fn aspa_attestation_missing_version(customer: u64, providers: Vec<u64>) -> Vec<u8> {
|
||||
let providers_der = der_sequence(providers.into_iter().map(der_integer_u64).collect());
|
||||
der_sequence(vec![der_integer_u64(customer), providers_der])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_must_be_explicit_and_equal_to_one() {
|
||||
let der = aspa_attestation_missing_version(64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::InvalidAttestationSequence));
|
||||
|
||||
let der = aspa_attestation_explicit_version(0, 64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::VersionMustBeExplicitOne));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn customer_asid_out_of_range_is_rejected() {
|
||||
let der = aspa_attestation_explicit_version(1, (u32::MAX as u64) + 1, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::CustomerAsIdOutOfRange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn providers_constraints_are_enforced() {
|
||||
// empty providers
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::EmptyProviders));
|
||||
|
||||
// not strictly increasing (duplicate)
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64497, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersNotStrictlyIncreasing));
|
||||
|
||||
// not strictly increasing (descending)
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64500, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersNotStrictlyIncreasing));
|
||||
|
||||
// contains customer
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64496, 64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::ProvidersContainCustomer(64496)));
|
||||
}
|
||||
|
||||
84
tests/test_aspa_validate_ee_resources.rs
Normal file
84
tests/test_aspa_validate_ee_resources.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError, EeAspaResources};
|
||||
|
||||
fn test_aspa() -> AspaEContent {
|
||||
AspaEContent {
|
||||
version: 1,
|
||||
customer_as_id: 64496,
|
||||
provider_as_ids: vec![64497],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_when_customer_matches_ee_asid() {
|
||||
let aspa = test_aspa();
|
||||
let ee = EeAspaResources {
|
||||
as_id: Some(64496),
|
||||
as_resources_inherit: false,
|
||||
as_resources_range_present: false,
|
||||
ip_resources_present: false,
|
||||
};
|
||||
aspa.validate_against_ee_resources(&ee)
|
||||
.expect("customer must match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_missing_as_resources() {
|
||||
let aspa = test_aspa();
|
||||
let ee = EeAspaResources {
|
||||
as_id: None,
|
||||
as_resources_inherit: false,
|
||||
as_resources_range_present: false,
|
||||
ip_resources_present: false,
|
||||
};
|
||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesMissing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_as_resources_inherit_or_ranges() {
|
||||
let aspa = test_aspa();
|
||||
let ee = EeAspaResources {
|
||||
as_id: Some(64496),
|
||||
as_resources_inherit: true,
|
||||
as_resources_range_present: false,
|
||||
ip_resources_present: false,
|
||||
};
|
||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesInherit));
|
||||
|
||||
let ee = EeAspaResources {
|
||||
as_id: Some(64496),
|
||||
as_resources_inherit: false,
|
||||
as_resources_range_present: true,
|
||||
ip_resources_present: false,
|
||||
};
|
||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesRangePresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_customer_mismatch() {
|
||||
let aspa = test_aspa();
|
||||
let ee = EeAspaResources {
|
||||
as_id: Some(64511),
|
||||
as_resources_inherit: false,
|
||||
as_resources_range_present: false,
|
||||
ip_resources_present: false,
|
||||
};
|
||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::CustomerAsIdMismatch { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_ip_resources_present() {
|
||||
let aspa = test_aspa();
|
||||
let ee = EeAspaResources {
|
||||
as_id: Some(64496),
|
||||
as_resources_inherit: false,
|
||||
as_resources_range_present: false,
|
||||
ip_resources_present: true,
|
||||
};
|
||||
let err = aspa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeIpResourcesPresent));
|
||||
}
|
||||
|
||||
14
tests/test_aspa_verify.rs
Normal file
14
tests/test_aspa_verify.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
|
||||
#[test]
|
||||
fn verify_aspa_cms_signature_with_embedded_ee_cert() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
aspa.signed_object
|
||||
.verify_signature()
|
||||
.expect("ASPA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
127
tests/test_roa_canonicalize.rs
Normal file
127
tests/test_roa_canonicalize.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use rpki::data_model::roa::{IpPrefix, RoaAfi, RoaEContent};
|
||||
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_bit_string_from_prefix(prefix_addr: &[u8], prefix_len: u16) -> Vec<u8> {
|
||||
let byte_len = ((prefix_len as usize) + 7) / 8;
|
||||
let unused = (byte_len * 8) as u16 - prefix_len;
|
||||
let mut bytes = prefix_addr[..byte_len.min(prefix_addr.len())].to_vec();
|
||||
while bytes.len() < byte_len {
|
||||
bytes.push(0);
|
||||
}
|
||||
if !bytes.is_empty() && unused != 0 {
|
||||
let mask = (1u8 << (unused as u8)) - 1;
|
||||
let last = bytes.len() - 1;
|
||||
bytes[last] &= !mask;
|
||||
}
|
||||
let mut content = vec![unused as u8];
|
||||
content.extend(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 roa_ip_address(prefix_bs: Vec<u8>) -> Vec<u8> {
|
||||
der_sequence(vec![prefix_bs])
|
||||
}
|
||||
|
||||
fn roa_ip_family(afi: [u8; 2], addresses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_octet_string(&afi), der_sequence(addresses)])
|
||||
}
|
||||
|
||||
fn roa_attestation(as_id: u64, families: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_integer_u64(as_id), der_sequence(families)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_sorts_families_sorts_and_dedups_addresses() {
|
||||
// Build:
|
||||
// - families are in order: IPv6 then IPv4 (should become IPv4 then IPv6)
|
||||
// - IPv4 addresses contain a duplicate (should dedup)
|
||||
// - IPv4 /24 uses only 3 bytes, should be padded to 4 with host bits cleared
|
||||
let v6 = roa_ip_family(
|
||||
[0, 2],
|
||||
vec![roa_ip_address(der_bit_string_from_prefix(
|
||||
&[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
32,
|
||||
))],
|
||||
);
|
||||
|
||||
let v4_prefix = der_bit_string_from_prefix(&[192, 0, 2, 255], 24);
|
||||
let v4 = roa_ip_family(
|
||||
[0, 1],
|
||||
vec![
|
||||
roa_ip_address(v4_prefix.clone()),
|
||||
roa_ip_address(v4_prefix), // duplicate
|
||||
],
|
||||
);
|
||||
|
||||
let der = roa_attestation(64496, vec![v6, v4]);
|
||||
let roa = RoaEContent::decode_der(&der).expect("decode roa econtent");
|
||||
|
||||
assert_eq!(roa.ip_addr_blocks.len(), 2);
|
||||
assert_eq!(roa.ip_addr_blocks[0].afi, RoaAfi::Ipv4);
|
||||
assert_eq!(roa.ip_addr_blocks[1].afi, RoaAfi::Ipv6);
|
||||
|
||||
let v4_fam = &roa.ip_addr_blocks[0];
|
||||
assert_eq!(v4_fam.addresses.len(), 1);
|
||||
assert_eq!(
|
||||
v4_fam.addresses[0].prefix,
|
||||
IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
33
tests/test_roa_decode.rs
Normal file
33
tests/test_roa_decode.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaObject};
|
||||
|
||||
#[test]
|
||||
fn decode_roa_fixture_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let roa = RoaObject::decode_der(&der).expect("decode roa");
|
||||
assert_eq!(
|
||||
roa.econtent_type,
|
||||
rpki::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ
|
||||
);
|
||||
assert_eq!(roa.roa.version, 0);
|
||||
assert_eq!(roa.roa.as_id, 4538);
|
||||
assert!(!roa.roa.ip_addr_blocks.is_empty());
|
||||
assert!(roa
|
||||
.roa
|
||||
.ip_addr_blocks
|
||||
.iter()
|
||||
.all(|f| !f.addresses.is_empty()));
|
||||
println!("{roa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_roa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let err = RoaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidEContentType(_)));
|
||||
}
|
||||
205
tests/test_roa_econtent_decode_errors.rs
Normal file
205
tests/test_roa_econtent_decode_errors.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaEContent};
|
||||
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
tlv(0x02, &bytes)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, 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 cs_explicit(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
fn roa_ip_address(prefix_bs: Vec<u8>, max_len: Option<u64>) -> Vec<u8> {
|
||||
let mut fields = vec![prefix_bs];
|
||||
if let Some(m) = max_len {
|
||||
fields.push(der_integer_u64(m));
|
||||
}
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
fn roa_ip_family(afi: [u8; 2], addresses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
der_sequence(vec![der_octet_string(&afi), der_sequence(addresses)])
|
||||
}
|
||||
|
||||
fn roa_attestation(version: Option<u64>, as_id: u64, families: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut fields = Vec::new();
|
||||
if let Some(v) = version {
|
||||
fields.push(cs_explicit(0, der_integer_u64(v)));
|
||||
}
|
||||
fields.push(der_integer_u64(as_id));
|
||||
fields.push(der_sequence(families));
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_must_be_zero_when_present() {
|
||||
let der = roa_attestation(
|
||||
Some(1),
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidVersion(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_id_out_of_range_is_rejected() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
(u32::MAX as u64) + 1,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::AsIdOutOfRange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_len_is_validated() {
|
||||
let der = roa_attestation(None, 64496, vec![]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(0)));
|
||||
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 2], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_afi_is_rejected() {
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::DuplicateAfi(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_afi_is_rejected() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0x12, 0x34], vec![roa_ip_address(der_bit_string(0, &[]), None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::UnsupportedAfi(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_address_list_is_rejected() {
|
||||
let der = roa_attestation(None, 64496, vec![roa_ip_family([0, 1], vec![])]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::EmptyAddressList));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_unused_bits_must_be_zeroed() {
|
||||
// prefix_len=1 => unused_bits=7, last byte must have lower 7 bits zero.
|
||||
// Use 0b1000_0001 which has a non-zero unused bit.
|
||||
let bs = der_bit_string(7, &[0b1000_0001]);
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidPrefixUnusedBits));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_len_out_of_range_is_rejected() {
|
||||
// IPv4 ub=32, encode 33 bits: 5 bytes with unused_bits=7 => 40-7=33.
|
||||
let bs = der_bit_string(7, &[0u8; 5]);
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, None)])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::PrefixLenOutOfRange { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_length_range_and_relation_are_validated() {
|
||||
// IPv4, prefix_len=8
|
||||
let bs = der_bit_string(0, &[0x0A]);
|
||||
// maxLength < prefix_len
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs.clone(), Some(7))])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. }));
|
||||
|
||||
// maxLength > ub
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs, Some(33))])],
|
||||
);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidMaxLength { .. }));
|
||||
}
|
||||
|
||||
108
tests/test_roa_validate_ee_resources.rs
Normal file
108
tests/test_roa_validate_ee_resources.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use rpki::data_model::roa::{
|
||||
EeResources, IpPrefix, IpResourceSet, RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily,
|
||||
RoaValidateError,
|
||||
};
|
||||
|
||||
fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||
RoaEContent {
|
||||
version: 0,
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
addresses: vec![RoaIpAddress {
|
||||
prefix: IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
},
|
||||
max_length: Some(24),
|
||||
}],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_when_prefix_is_covered() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = EeResources {
|
||||
ip_resources: IpResourceSet {
|
||||
prefixes: vec![IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 0,
|
||||
addr: vec![0, 0, 0, 0],
|
||||
}],
|
||||
},
|
||||
ip_resources_inherit: false,
|
||||
as_resources_present: false,
|
||||
};
|
||||
roa.validate_against_ee_resources(&ee)
|
||||
.expect("prefix should be covered by 0/0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_as_resources_present() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = EeResources {
|
||||
ip_resources: IpResourceSet { prefixes: vec![] },
|
||||
ip_resources_inherit: false,
|
||||
as_resources_present: true,
|
||||
};
|
||||
let err = roa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeAsResourcesPresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_ip_resources_inherit() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = EeResources {
|
||||
ip_resources: IpResourceSet { prefixes: vec![] },
|
||||
ip_resources_inherit: true,
|
||||
as_resources_present: false,
|
||||
};
|
||||
let err = roa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeIpResourcesInherit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_prefix_not_covered() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = EeResources {
|
||||
ip_resources: IpResourceSet {
|
||||
prefixes: vec![IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: vec![192, 0, 2, 0],
|
||||
}],
|
||||
},
|
||||
ip_resources_inherit: false,
|
||||
as_resources_present: false,
|
||||
};
|
||||
let err = roa.validate_against_ee_resources(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::PrefixNotInEeResources { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_prefix_handles_non_octet_boundary_prefix_len() {
|
||||
let ee_set = IpResourceSet {
|
||||
prefixes: vec![IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 9,
|
||||
addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9
|
||||
}],
|
||||
};
|
||||
|
||||
let covered = IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16
|
||||
};
|
||||
assert!(ee_set.contains_prefix(&covered));
|
||||
|
||||
let not_covered = IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![0b1010_0001, 0x12, 0, 0], // 161.18.0.0/16
|
||||
};
|
||||
assert!(!ee_set.contains_prefix(¬_covered));
|
||||
}
|
||||
|
||||
14
tests/test_roa_verify.rs
Normal file
14
tests/test_roa_verify.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
|
||||
#[test]
|
||||
fn verify_roa_cms_signature_with_embedded_ee_cert() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
.expect("read ROA fixture");
|
||||
let roa = RoaObject::decode_der(&der).expect("decode roa");
|
||||
roa.signed_object
|
||||
.verify_signature()
|
||||
.expect("ROA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
@@ -26,4 +26,5 @@ fn decode_manifest_signed_object_smoke() {
|
||||
.iter()
|
||||
.any(|u| u.starts_with("rsync://")));
|
||||
assert_eq!(so.signed_data.signer_infos.len(), 1);
|
||||
println!("{so:#?}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user