增加aspa对象解析

This commit is contained in:
2026-02-02 15:39:22 +08:00
parent bcd4829486
commit 56ae2ca4fc
44 changed files with 1608 additions and 0 deletions

25
tests/test_aspa_decode.rs Normal file
View 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(_)));
}

View 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)));
}

View 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
View 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");
}

View 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
View 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(_)));
}

View 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 { .. }));
}

View 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(&not_covered));
}

14
tests/test_roa_verify.rs Normal file
View 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");
}

View File

@@ -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:#?}")
}