增加rc,完成所有模型的解析,优化error code 的RFC引用
This commit is contained in:
BIN
tests/fixtures/ta/afrinic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/afrinic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/apnic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/apnic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/arin-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/arin-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/lacnic-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/lacnic-ta.cer
vendored
Normal file
Binary file not shown.
BIN
tests/fixtures/ta/ripe-ncc-ta.cer
vendored
Normal file
BIN
tests/fixtures/ta/ripe-ncc-ta.cer
vendored
Normal file
Binary file not shown.
10
tests/fixtures/tal/afrinic.tal
vendored
Normal file
10
tests/fixtures/tal/afrinic.tal
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
rsync://rpki.afrinic.net/repository/AfriNIC.cer
|
||||
https://rpki.afrinic.net/repository/AfriNIC.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxsAqAhWIO+ON2Ef9oRDM
|
||||
pKxv+AfmSLIdLWJtjrvUyDxJPBjgR+kVrOHUeTaujygFUp49tuN5H2C1rUuQavTH
|
||||
vve6xNF5fU3OkTcqEzMOZy+ctkbde2SRMVdvbO22+TH9gNhKDc9l7Vu01qU4LeJH
|
||||
k3X0f5uu5346YrGAOSv6AaYBXVgXxa0s9ZvgqFpim50pReQe/WI3QwFKNgpPzfQL
|
||||
6Y7fDPYdYaVOXPXSKtx7P4s4KLA/ZWmRL/bobw/i2fFviAGhDrjqqqum+/9w1hEl
|
||||
L/vqihVnV18saKTnLvkItA/Bf5i11Yhw2K7qv573YWxyuqCknO/iYLTR1DToBZcZ
|
||||
UQIDAQAB
|
||||
10
tests/fixtures/tal/apnic-rfc7730-https.tal
vendored
Normal file
10
tests/fixtures/tal/apnic-rfc7730-https.tal
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
|
||||
rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RWSL61YAAYumEiU8z8
|
||||
qH2ETVIL01ilxZlzIL9JYSORMN5Cmtf8V2JblIealSqgOTGjvSjEsiV73s67zYQI
|
||||
7C/iSOb96uf3/s86NqbxDiFQGN8qG7RNcdgVuUlAidl8WxvLNI8VhqbAB5uSg/Mr
|
||||
LeSOvXRja041VptAxIhcGzDMvlAJRwkrYK/Mo8P4E2rSQgwqCgae0ebY1CsJ3Cjf
|
||||
i67C1nw7oXqJJovvXJ4apGmEv8az23OLC6Ki54Ul/E6xk227BFttqFV3YMtKx42H
|
||||
cCcDVZZy01n7JjzvO8ccaXmHIgR7utnqhBRNNq5Xc5ZhbkrUsNtiJmrZzVlgU6Ou
|
||||
0wIDAQAB
|
||||
19
tests/fixtures/tal/arin.tal
vendored
Normal file
19
tests/fixtures/tal/arin.tal
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# THIS TRUST ANCHOR LOCATOR IS PROVIDED BY THE AMERICAN REGISTRY FOR
|
||||
# INTERNET NUMBERS (ARIN) "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL ARIN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS PUBLIC KEY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
rsync://rpki.arin.net/repository/arin-rpki-ta.cer
|
||||
https://rrdp.arin.net/arin-rpki-ta.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3lZPjbHvMRV5sDDqfLc/685th5FnreHMJjg8
|
||||
pEZUbG8Y8TQxSBsDebbsDpl3Ov3Cj1WtdrJ3CIfQODCPrrJdOBSrMATeUbPC+JlNf2SRP3UB+VJFgtTj
|
||||
0RN8cEYIuhBW5t6AxQbHhdNQH+A1F/OJdw0q9da2U29Lx85nfFxvnC1EpK9CbLJS4m37+RlpNbT1cba+
|
||||
b+loXpx0Qcb1C4UpJCGDy7uNf5w6/+l7RpATAHqqsX4qCtwwDYlbHzp2xk9owF3mkCxzl0HwncO+sEHH
|
||||
eaL3OjtwdIGrRGeHi2Mpt+mvWHhtQqVG+51MHTyg+nIjWFKKGx1Q9+KDx4wJStwveQIDAQAB
|
||||
4
tests/fixtures/tal/lacnic.tal
vendored
Normal file
4
tests/fixtures/tal/lacnic.tal
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
https://rrdp.lacnic.net/ta/rta-lacnic-rpki.cer
|
||||
rsync://repository.lacnic.net/rpki/lacnic/rta-lacnic-rpki.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqZEzhYK0+PtDOPfub/KRc3MeWx3neXx4/wbnJWGbNAtbYqXg3uU5J4HFzPgk/VIppgSKAhlO0H60DRP48by9gr5/yDHu2KXhOmnMg46sYsUIpfgtBS9+VtrqWziJfb+pkGtuOWeTnj6zBmBNZKK+5AlMCW1WPhrylIcB+XSZx8tk9GS/3SMQ+YfMVwwAyYjsex14Uzto4GjONALE5oh1M3+glRQduD6vzSwOD+WahMbc9vCOTED+2McLHRKgNaQf0YJ9a1jG9oJIvDkKXEqdfqDRktwyoD74cV57bW3tBAexB7GglITbInyQAsmdngtfg2LUMrcROHHP86QPZINjDQIDAQAB
|
||||
10
tests/fixtures/tal/ripe-ncc.tal
vendored
Normal file
10
tests/fixtures/tal/ripe-ncc.tal
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
https://rpki.ripe.net/ta/ripe-ncc-ta.cer
|
||||
rsync://rpki.ripe.net/ta/ripe-ncc-ta.cer
|
||||
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0URYSGqUz2myBsOzeW1j
|
||||
Q6NsxNvlLMyhWknvnl8NiBCs/T/S2XuNKQNZ+wBZxIgPPV2pFBFeQAvoH/WK83Hw
|
||||
A26V2siwm/MY2nKZ+Olw+wlpzlZ1p3Ipj2eNcKrmit8BwBC8xImzuCGaV0jkRB0G
|
||||
Z0hoH6Ml03umLprRsn6v0xOP0+l6Qc1ZHMFVFb385IQ7FQQTcVIxrdeMsoyJq9eM
|
||||
kE6DoclHhF/NlSllXubASQ9KUWqJ0+Ot3QCXr4LXECMfkpkVR2TZT+v5v658bHVs
|
||||
6ZxRD1b6Uk1uQKAyHUbn/tXvP8lrjAibGzVsXDT2L0x4Edx+QdixPgOji3gBMyL2
|
||||
VwIDAQAB
|
||||
14
tests/test_aspa_embedded_ee_cert.rs
Normal file
14
tests/test_aspa_embedded_ee_cert.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
|
||||
#[test]
|
||||
fn aspa_embedded_ee_cert_resources_validate() {
|
||||
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.validate_embedded_ee_cert()
|
||||
.expect("aspa EE cert resources must validate");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError, EeAspaResources};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError};
|
||||
use rpki::data_model::rc::{
|
||||
AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
|
||||
};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
subject_dn: "CN=subject".to_string(),
|
||||
validity_not_before: OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
},
|
||||
kind: ResourceCertKind::Ee,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_aspa() -> AspaEContent {
|
||||
AspaEContent {
|
||||
@@ -11,74 +47,104 @@ fn test_aspa() -> AspaEContent {
|
||||
#[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)
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
aspa.validate_against_ee_cert(&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();
|
||||
let ee = dummy_ee(None, None);
|
||||
let err = aspa.validate_against_ee_cert(&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();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&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();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 64496,
|
||||
max: 64497,
|
||||
}])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&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();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64511)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&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();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet { families: vec![] }),
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeIpResourcesPresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_rdi_present_or_not_single_id() {
|
||||
let aspa = test_aspa();
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesRdiPresent));
|
||||
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(64496),
|
||||
AsIdOrRange::Id(64497),
|
||||
])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesNotSingleId));
|
||||
}
|
||||
|
||||
|
||||
14
tests/test_manifest_embedded_ee_cert.rs
Normal file
14
tests/test_manifest_embedded_ee_cert.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
|
||||
#[test]
|
||||
fn manifest_embedded_ee_cert_resources_validate() {
|
||||
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");
|
||||
mft.validate_embedded_ee_cert()
|
||||
.expect("manifest EE cert resources must validate");
|
||||
}
|
||||
|
||||
498
tests/test_model_print_real_fixtures.rs
Normal file
498
tests/test_model_print_real_fixtures.rs
Normal file
@@ -0,0 +1,498 @@
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaObject};
|
||||
use rpki::data_model::crl::{CrlExtensions, RevokedCert, RpkixCrl};
|
||||
use rpki::data_model::manifest::{FileAndHash, ManifestEContent, ManifestObject};
|
||||
use rpki::data_model::rc::{
|
||||
AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
SubjectInfoAccess,
|
||||
};
|
||||
use rpki::data_model::roa::{RoaEContent, RoaIpAddressFamily, RoaObject};
|
||||
use rpki::data_model::signed_object::{
|
||||
EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignedDataProfiled,
|
||||
SignerInfoProfiled,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TrustAnchor};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct BytesFmt {
|
||||
len: usize,
|
||||
sha256_hex: String,
|
||||
head_hex: String,
|
||||
tail_hex: String,
|
||||
}
|
||||
|
||||
fn bytes_fmt(bytes: &[u8]) -> BytesFmt {
|
||||
let len = bytes.len();
|
||||
let sha = Sha256::digest(bytes);
|
||||
|
||||
let head_len = len.min(16);
|
||||
let tail_len = len.min(16);
|
||||
|
||||
let head = &bytes[..head_len];
|
||||
let tail = &bytes[len.saturating_sub(tail_len)..];
|
||||
|
||||
BytesFmt {
|
||||
len,
|
||||
sha256_hex: hex::encode(sha),
|
||||
head_hex: hex::encode(head),
|
||||
tail_hex: hex::encode(tail),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TalPretty {
|
||||
raw: BytesFmt,
|
||||
comments: Vec<String>,
|
||||
ta_uris: Vec<String>,
|
||||
subject_public_key_info_der: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&Tal> for TalPretty {
|
||||
fn from(v: &Tal) -> Self {
|
||||
Self {
|
||||
raw: bytes_fmt(&v.raw),
|
||||
comments: v.comments.clone(),
|
||||
ta_uris: v.ta_uris.iter().map(|u| u.to_string()).collect(),
|
||||
subject_public_key_info_der: bytes_fmt(&v.subject_public_key_info_der),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TaCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
rc_ca: ResourceCertificatePretty,
|
||||
}
|
||||
|
||||
impl From<&TaCertificate> for TaCertificatePretty {
|
||||
fn from(v: &TaCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
rc_ca: ResourceCertificatePretty::from(&v.rc_ca),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct TrustAnchorPretty {
|
||||
tal: TalPretty,
|
||||
ta_certificate: TaCertificatePretty,
|
||||
resolved_ta_uri: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&TrustAnchor> for TrustAnchorPretty {
|
||||
fn from(v: &TrustAnchor) -> Self {
|
||||
Self {
|
||||
tal: TalPretty::from(&v.tal),
|
||||
ta_certificate: TaCertificatePretty::from(&v.ta_certificate),
|
||||
resolved_ta_uri: v.resolved_ta_uri.as_ref().map(|u| u.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResourceCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
tbs: RpkixTbsCertificatePretty,
|
||||
kind: ResourceCertKind,
|
||||
}
|
||||
|
||||
impl From<&ResourceCertificate> for ResourceCertificatePretty {
|
||||
fn from(v: &ResourceCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
tbs: RpkixTbsCertificatePretty::from(&v.tbs),
|
||||
kind: v.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkixTbsCertificatePretty {
|
||||
version: u32,
|
||||
serial_number: String,
|
||||
signature_algorithm: String,
|
||||
issuer_dn: String,
|
||||
subject_dn: String,
|
||||
validity_not_before: time::OffsetDateTime,
|
||||
validity_not_after: time::OffsetDateTime,
|
||||
subject_public_key_info: BytesFmt,
|
||||
extensions: RcExtensionsPretty,
|
||||
}
|
||||
|
||||
impl From<&RpkixTbsCertificate> for RpkixTbsCertificatePretty {
|
||||
fn from(v: &RpkixTbsCertificate) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
serial_number: hex::encode(v.serial_number.to_bytes_be()),
|
||||
signature_algorithm: v.signature_algorithm.clone(),
|
||||
issuer_dn: v.issuer_dn.clone(),
|
||||
subject_dn: v.subject_dn.clone(),
|
||||
validity_not_before: v.validity_not_before,
|
||||
validity_not_after: v.validity_not_after,
|
||||
subject_public_key_info: bytes_fmt(&v.subject_public_key_info),
|
||||
extensions: RcExtensionsPretty::from(&v.extensions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RcExtensionsPretty {
|
||||
basic_constraints_ca: bool,
|
||||
subject_key_identifier: Option<BytesFmt>,
|
||||
subject_info_access: Option<SubjectInfoAccess>,
|
||||
certificate_policies_oid: Option<String>,
|
||||
ip_resources: Option<IpResourceSet>,
|
||||
as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
impl From<&RcExtensions> for RcExtensionsPretty {
|
||||
fn from(v: &RcExtensions) -> Self {
|
||||
Self {
|
||||
basic_constraints_ca: v.basic_constraints_ca,
|
||||
subject_key_identifier: v.subject_key_identifier.as_ref().map(|b| bytes_fmt(b)),
|
||||
subject_info_access: v.subject_info_access.clone(),
|
||||
certificate_policies_oid: v.certificate_policies_oid.clone(),
|
||||
ip_resources: v.ip_resources.clone(),
|
||||
as_resources: v.as_resources.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkiSignedObjectPretty {
|
||||
raw_der: BytesFmt,
|
||||
content_info_content_type: String,
|
||||
signed_data: SignedDataProfiledPretty,
|
||||
}
|
||||
|
||||
impl From<&RpkiSignedObject> for RpkiSignedObjectPretty {
|
||||
fn from(v: &RpkiSignedObject) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
content_info_content_type: v.content_info_content_type.clone(),
|
||||
signed_data: SignedDataProfiledPretty::from(&v.signed_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignedDataProfiledPretty {
|
||||
version: u32,
|
||||
digest_algorithms: Vec<String>,
|
||||
encap_content_info: EncapsulatedContentInfoPretty,
|
||||
certificates: Vec<ResourceEeCertificatePretty>,
|
||||
crls_present: bool,
|
||||
signer_infos: Vec<SignerInfoProfiledPretty>,
|
||||
}
|
||||
|
||||
impl From<&SignedDataProfiled> for SignedDataProfiledPretty {
|
||||
fn from(v: &SignedDataProfiled) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
digest_algorithms: v.digest_algorithms.clone(),
|
||||
encap_content_info: EncapsulatedContentInfoPretty::from(&v.encap_content_info),
|
||||
certificates: v
|
||||
.certificates
|
||||
.iter()
|
||||
.map(ResourceEeCertificatePretty::from)
|
||||
.collect(),
|
||||
crls_present: v.crls_present,
|
||||
signer_infos: v.signer_infos.iter().map(SignerInfoProfiledPretty::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct EncapsulatedContentInfoPretty {
|
||||
econtent_type: String,
|
||||
econtent: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&EncapsulatedContentInfo> for EncapsulatedContentInfoPretty {
|
||||
fn from(v: &EncapsulatedContentInfo) -> Self {
|
||||
Self {
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
econtent: bytes_fmt(&v.econtent),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ResourceEeCertificatePretty {
|
||||
raw_der: BytesFmt,
|
||||
subject_key_identifier: BytesFmt,
|
||||
spki_der: BytesFmt,
|
||||
sia_signed_object_uris: Vec<String>,
|
||||
resource_cert: ResourceCertificatePretty,
|
||||
}
|
||||
|
||||
impl From<&ResourceEeCertificate> for ResourceEeCertificatePretty {
|
||||
fn from(v: &ResourceEeCertificate) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
subject_key_identifier: bytes_fmt(&v.subject_key_identifier),
|
||||
spki_der: bytes_fmt(&v.spki_der),
|
||||
sia_signed_object_uris: v.sia_signed_object_uris.clone(),
|
||||
resource_cert: ResourceCertificatePretty::from(&v.resource_cert),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignerInfoProfiledPretty {
|
||||
version: u32,
|
||||
sid_ski: BytesFmt,
|
||||
digest_algorithm: String,
|
||||
signature_algorithm: String,
|
||||
signed_attrs: SignedAttrsProfiledPretty,
|
||||
unsigned_attrs_present: bool,
|
||||
signature: BytesFmt,
|
||||
signed_attrs_der_for_signature: BytesFmt,
|
||||
}
|
||||
|
||||
impl From<&SignerInfoProfiled> for SignerInfoProfiledPretty {
|
||||
fn from(v: &SignerInfoProfiled) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
sid_ski: bytes_fmt(&v.sid_ski),
|
||||
digest_algorithm: v.digest_algorithm.clone(),
|
||||
signature_algorithm: v.signature_algorithm.clone(),
|
||||
signed_attrs: SignedAttrsProfiledPretty::from(&v.signed_attrs),
|
||||
unsigned_attrs_present: v.unsigned_attrs_present,
|
||||
signature: bytes_fmt(&v.signature),
|
||||
signed_attrs_der_for_signature: bytes_fmt(&v.signed_attrs_der_for_signature),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct SignedAttrsProfiledPretty {
|
||||
content_type: String,
|
||||
message_digest: BytesFmt,
|
||||
signing_time: rpki::data_model::common::Asn1TimeUtc,
|
||||
other_attrs_present: bool,
|
||||
}
|
||||
|
||||
impl From<&SignedAttrsProfiled> for SignedAttrsProfiledPretty {
|
||||
fn from(v: &SignedAttrsProfiled) -> Self {
|
||||
Self {
|
||||
content_type: v.content_type.clone(),
|
||||
message_digest: bytes_fmt(&v.message_digest),
|
||||
signing_time: v.signing_time,
|
||||
other_attrs_present: v.other_attrs_present,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ManifestObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
manifest: ManifestEContentPretty,
|
||||
}
|
||||
|
||||
impl From<&ManifestObject> for ManifestObjectPretty {
|
||||
fn from(v: &ManifestObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
manifest: ManifestEContentPretty::from(&v.manifest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ManifestEContentPretty {
|
||||
version: u32,
|
||||
manifest_number: String,
|
||||
this_update: time::OffsetDateTime,
|
||||
next_update: time::OffsetDateTime,
|
||||
file_hash_alg: String,
|
||||
files: Vec<FileAndHashPretty>,
|
||||
}
|
||||
|
||||
impl From<&ManifestEContent> for ManifestEContentPretty {
|
||||
fn from(v: &ManifestEContent) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
manifest_number: v.manifest_number.to_hex_upper(),
|
||||
this_update: v.this_update,
|
||||
next_update: v.next_update,
|
||||
file_hash_alg: v.file_hash_alg.clone(),
|
||||
files: v.files.iter().map(FileAndHashPretty::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct FileAndHashPretty {
|
||||
file_name: String,
|
||||
hash_hex: String,
|
||||
}
|
||||
|
||||
impl From<&FileAndHash> for FileAndHashPretty {
|
||||
fn from(v: &FileAndHash) -> Self {
|
||||
Self {
|
||||
file_name: v.file_name.clone(),
|
||||
hash_hex: hex::encode(&v.hash_bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RoaObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
roa: RoaEContentPretty,
|
||||
}
|
||||
|
||||
impl From<&RoaObject> for RoaObjectPretty {
|
||||
fn from(v: &RoaObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
roa: RoaEContentPretty::from(&v.roa),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RoaEContentPretty {
|
||||
version: u32,
|
||||
as_id: u32,
|
||||
ip_addr_blocks: Vec<RoaIpAddressFamily>,
|
||||
}
|
||||
|
||||
impl From<&RoaEContent> for RoaEContentPretty {
|
||||
fn from(v: &RoaEContent) -> Self {
|
||||
Self {
|
||||
version: v.version,
|
||||
as_id: v.as_id,
|
||||
ip_addr_blocks: v.ip_addr_blocks.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct AspaObjectPretty {
|
||||
signed_object: RpkiSignedObjectPretty,
|
||||
econtent_type: String,
|
||||
aspa: AspaEContent,
|
||||
}
|
||||
|
||||
impl From<&AspaObject> for AspaObjectPretty {
|
||||
fn from(v: &AspaObject) -> Self {
|
||||
Self {
|
||||
signed_object: RpkiSignedObjectPretty::from(&v.signed_object),
|
||||
econtent_type: v.econtent_type.clone(),
|
||||
aspa: v.aspa.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct RpkixCrlPretty {
|
||||
raw_der: BytesFmt,
|
||||
version: u32,
|
||||
issuer_dn: String,
|
||||
signature_algorithm_oid: String,
|
||||
this_update: rpki::data_model::common::Asn1TimeUtc,
|
||||
next_update: rpki::data_model::common::Asn1TimeUtc,
|
||||
revoked_certs: Vec<RevokedCert>,
|
||||
extensions: CrlExtensions,
|
||||
}
|
||||
|
||||
impl From<&RpkixCrl> for RpkixCrlPretty {
|
||||
fn from(v: &RpkixCrl) -> Self {
|
||||
Self {
|
||||
raw_der: bytes_fmt(&v.raw_der),
|
||||
version: v.version,
|
||||
issuer_dn: v.issuer_dn.clone(),
|
||||
signature_algorithm_oid: v.signature_algorithm_oid.clone(),
|
||||
this_update: v.this_update,
|
||||
next_update: v.next_update,
|
||||
revoked_certs: v.revoked_certs.clone(),
|
||||
extensions: v.extensions.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_all_models_from_real_fixtures() {
|
||||
// Note: run this test with `cargo test --test test_model_print_real_fixtures -- --nocapture`
|
||||
// to see the output.
|
||||
|
||||
let tal_path = "tests/fixtures/tal/ripe-ncc.tal";
|
||||
println!("== TAL / TA / TrustAnchor ==");
|
||||
println!("Fixture (TAL): {tal_path}");
|
||||
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL");
|
||||
|
||||
let ta_path = "tests/fixtures/ta/ripe-ncc-ta.cer";
|
||||
println!("Fixture (TA): {ta_path}");
|
||||
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
|
||||
let ta = TaCertificate::from_der(&ta_der).expect("parse TA cert");
|
||||
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.iter()
|
||||
.find(|u| u.scheme() == "https")
|
||||
.or_else(|| tal.ta_uris.first())
|
||||
.cloned()
|
||||
.expect("tal has at least one uri");
|
||||
let trust_anchor = TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind trust anchor");
|
||||
println!("{:#?}", TalPretty::from(&trust_anchor.tal));
|
||||
println!("{:#?}", TaCertificatePretty::from(&ta));
|
||||
println!("{:#?}", TrustAnchorPretty::from(&trust_anchor));
|
||||
|
||||
println!();
|
||||
println!("== ResourceCertificate (example non-TA CA cert) ==");
|
||||
let ca_path =
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
|
||||
println!("Fixture (CA cert): {ca_path}");
|
||||
let ca_der = std::fs::read(ca_path).expect("read CA cert fixture");
|
||||
let ca_rc = ResourceCertificate::from_der(&ca_der).expect("parse CA resource certificate");
|
||||
println!("{:#?}", ResourceCertificatePretty::from(&ca_rc));
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / Manifest ==");
|
||||
let mft_path =
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
|
||||
println!("Fixture (MFT): {mft_path}");
|
||||
let mft_der = std::fs::read(mft_path).expect("read MFT fixture");
|
||||
let mft_obj = ManifestObject::decode_der(&mft_der).expect("decode manifest object");
|
||||
println!("{:#?}", ManifestObjectPretty::from(&mft_obj));
|
||||
println!("Manifest.validate_embedded_ee_cert={:?}", mft_obj.validate_embedded_ee_cert());
|
||||
println!("Manifest.verify_signature={:?}", mft_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / ROA ==");
|
||||
let roa_path = "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa";
|
||||
println!("Fixture (ROA): {roa_path}");
|
||||
let roa_der = std::fs::read(roa_path).expect("read ROA fixture");
|
||||
let roa_obj = RoaObject::decode_der(&roa_der).expect("decode ROA object");
|
||||
println!("{:#?}", RoaObjectPretty::from(&roa_obj));
|
||||
println!("ROA.validate_embedded_ee_cert={:?}", roa_obj.validate_embedded_ee_cert());
|
||||
println!("ROA.verify_signature={:?}", roa_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / ASPA ==");
|
||||
let aspa_path =
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa";
|
||||
println!("Fixture (ASPA): {aspa_path}");
|
||||
let aspa_der = std::fs::read(aspa_path).expect("read ASPA fixture");
|
||||
let aspa_obj = AspaObject::decode_der(&aspa_der).expect("decode ASPA object");
|
||||
println!("{:#?}", AspaObjectPretty::from(&aspa_obj));
|
||||
println!("ASPA.validate_embedded_ee_cert={:?}", aspa_obj.validate_embedded_ee_cert());
|
||||
println!("ASPA.verify_signature={:?}", aspa_obj.signed_object.verify_signature());
|
||||
|
||||
println!();
|
||||
println!("== CRL ==");
|
||||
let crl_path = "tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl";
|
||||
println!("Fixture (CRL): {crl_path}");
|
||||
let crl_der = std::fs::read(crl_path).expect("read CRL fixture");
|
||||
let crl = RpkixCrl::decode_der(&crl_der).expect("decode CRL");
|
||||
println!("{:#?}", RpkixCrlPretty::from(&crl));
|
||||
}
|
||||
93
tests/test_rc_from_der_errors.rs
Normal file
93
tests/test_rc_from_der_errors.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use rpki::data_model::rc::{ResourceCertificate, ResourceCertificateError};
|
||||
|
||||
const TEST_NO_SIA_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
|
||||
|
||||
fn decode_b64(b64: &str) -> Vec<u8> {
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine as _;
|
||||
STANDARD.decode(b64).unwrap()
|
||||
}
|
||||
|
||||
fn replace_first(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> bool {
|
||||
if needle.len() != replacement.len() {
|
||||
return false;
|
||||
}
|
||||
for i in 0..=haystack.len().saturating_sub(needle.len()) {
|
||||
if &haystack[i..i + needle.len()] == needle {
|
||||
haystack[i..i + needle.len()].copy_from_slice(replacement);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn replace_all(haystack: &mut [u8], needle: &[u8], replacement: &[u8]) -> usize {
|
||||
if needle.len() != replacement.len() {
|
||||
return 0;
|
||||
}
|
||||
let mut n = 0;
|
||||
let mut i = 0;
|
||||
while i + needle.len() <= haystack.len() {
|
||||
if &haystack[i..i + needle.len()] == needle {
|
||||
haystack[i..i + needle.len()].copy_from_slice(replacement);
|
||||
n += 1;
|
||||
i += needle.len();
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_bytes_after_cert_are_rejected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
der.push(0);
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::TrailingBytes(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_algorithm_mismatch_is_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
// DER encoding of sha256WithRSAEncryption OID:
|
||||
// 06 09 2A 86 48 86 F7 0D 01 01 0B
|
||||
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
|
||||
let mut patched = oid;
|
||||
patched[10] = 0x01; // rsaEncryption, same length encoding
|
||||
assert!(replace_first(&mut der, &oid, &patched));
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::SignatureAlgorithmMismatch));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_signature_algorithm_is_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
let oid = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B];
|
||||
let mut patched = oid;
|
||||
patched[10] = 0x01;
|
||||
let n = replace_all(&mut der, &oid, &patched);
|
||||
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier");
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::UnsupportedSignatureAlgorithm));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_signature_algorithm_parameters_are_detected() {
|
||||
let mut der = decode_b64(TEST_NO_SIA_CERT_DER_B64);
|
||||
// Replace NULL parameters (05 00) right after the sha256WithRSAEncryption OID with
|
||||
// an empty OCTET STRING (04 00) to keep the encoding length unchanged.
|
||||
let alg = [
|
||||
0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00,
|
||||
];
|
||||
let mut patched = alg;
|
||||
patched[11] = 0x04;
|
||||
let n = replace_all(&mut der, &alg, &patched);
|
||||
assert!(n >= 2, "expected to patch at least inner+outer AlgorithmIdentifier parameters");
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResourceCertificateError::InvalidSignatureAlgorithmParameters
|
||||
));
|
||||
}
|
||||
|
||||
43
tests/test_rc_from_der_fixtures.rs
Normal file
43
tests/test_rc_from_der_fixtures.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use rpki::data_model::rc::{ResourceCertKind, ResourceCertificate, SubjectInfoAccess};
|
||||
|
||||
#[test]
|
||||
fn resource_certificate_from_der_parses_ca_fixtures() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
|
||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/ZW5EIqvxKWSSAOsBmoFfKxIjbpI.cer",
|
||||
];
|
||||
|
||||
for path in fixtures {
|
||||
let der = std::fs::read(path).expect("read CA cert fixture");
|
||||
let rc = ResourceCertificate::from_der(&der).expect("parse CA cert fixture");
|
||||
|
||||
assert_eq!(rc.kind, ResourceCertKind::Ca, "fixture should be CA: {path}");
|
||||
assert_eq!(rc.tbs.version, 2, "X.509 v3 encoded as 2: {path}");
|
||||
|
||||
assert_eq!(
|
||||
rc.tbs.extensions.certificate_policies_oid.as_deref(),
|
||||
Some(OID_CP_IPADDR_ASNUMBER),
|
||||
"CA certificatePolicies OID: {path}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(rc.tbs.extensions.subject_info_access, Some(SubjectInfoAccess::Ca(_))),
|
||||
"CA SIA should not contain signedObject accessMethod: {path}"
|
||||
);
|
||||
|
||||
assert!(rc.tbs.extensions.ip_resources.is_some(), "CA should have IP resources: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_certificate_from_der_parses_as_resources_in_apnic_fixture() {
|
||||
let path =
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer";
|
||||
let der = std::fs::read(path).expect("read APNIC CA cert fixture");
|
||||
let rc = ResourceCertificate::from_der(&der).expect("parse APNIC CA cert fixture");
|
||||
|
||||
assert!(rc.tbs.extensions.as_resources.is_some(), "fixture should carry AS resources");
|
||||
}
|
||||
|
||||
93
tests/test_rc_helpers.rs
Normal file
93
tests/test_rc_helpers.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_contains_prefix_afi_mismatch_is_false() {
|
||||
let fam = IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
};
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 0,
|
||||
addr: vec![0; 16],
|
||||
};
|
||||
assert!(!fam.contains_prefix(&p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_inherit_covers_all_prefixes_of_same_afi() {
|
||||
let fam = IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
};
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(fam.contains_prefix(&p));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_resource_set_asnum_single_id_returns_expected_values() {
|
||||
let inherit = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(inherit.asnum_single_id(), None);
|
||||
|
||||
let single_id = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(single_id.asnum_single_id(), Some(64512));
|
||||
|
||||
let single_range = AsResourceSet {
|
||||
asnum: None,
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 64512,
|
||||
max: 64520,
|
||||
}])),
|
||||
};
|
||||
assert_eq!(single_range.asnum_single_id(), None);
|
||||
|
||||
let multiple = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Id(64512),
|
||||
AsIdOrRange::Id(64513),
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
assert_eq!(multiple.asnum_single_id(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_identifier_choice_has_range_detects_ranges() {
|
||||
assert!(!AsIdentifierChoice::Inherit.has_range());
|
||||
assert!(!AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)]).has_range());
|
||||
assert!(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resource_contains_prefix_finds_matching_family() {
|
||||
let fam_v6 = IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
})]),
|
||||
};
|
||||
let set = rpki::data_model::rc::IpResourceSet { families: vec![fam_v6] };
|
||||
|
||||
let p = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 48,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&p));
|
||||
}
|
||||
|
||||
58
tests/test_rc_ip_resource_range_coverage.rs
Normal file
58
tests/test_rc_ip_resource_range_coverage.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ip_resource_range_covers_prefix_ipv4() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![10, 0, 0, 0],
|
||||
max: vec![10, 255, 255, 255],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
|
||||
let inside = IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![10, 1, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&inside));
|
||||
|
||||
let outside = IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![11, 0, 0, 0],
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_resource_range_covers_prefix_ipv6() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
min: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
max: vec![0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
})]),
|
||||
}],
|
||||
};
|
||||
|
||||
let inside = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 64,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(set.contains_prefix(&inside));
|
||||
|
||||
let outside = IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 64,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
135
tests/test_rc_resource_extensions_decode.rs
Normal file
135
tests/test_rc_resource_extensions_decode.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange,
|
||||
IpResourceSet,
|
||||
};
|
||||
|
||||
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_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
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_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_decode_handles_inherit_and_range_endpoint_fill() {
|
||||
// IPv6 family with one range:
|
||||
// - min: 240a:a000::/32 (encoded as 32 bits)
|
||||
// - max: 240a:a008::/31 (encoded as 31 bits, so fill-ones yields 240a:a009:ffff..)
|
||||
let address_family = der_octet_string(&[0x00, 0x02]);
|
||||
let min = der_bit_string(0, &[0x24, 0x0A, 0xA0, 0x00]);
|
||||
let max = der_bit_string(1, &[0x24, 0x0A, 0xA0, 0x08]);
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let choice = der_sequence(vec![range]);
|
||||
let fam = der_sequence(vec![address_family, choice]);
|
||||
let ip_addr_blocks = der_sequence(vec![fam]);
|
||||
|
||||
let decoded = IpResourceSet::decode_extn_value(&ip_addr_blocks).expect("decode ipAddrBlocks");
|
||||
assert_eq!(decoded.families.len(), 1);
|
||||
let fam = &decoded.families[0];
|
||||
assert_eq!(fam.afi, Afi::Ipv6);
|
||||
let IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
|
||||
panic!("expected AddressesOrRanges");
|
||||
};
|
||||
let IpAddressOrRange::Range(r) = &items[0] else {
|
||||
panic!("expected Range");
|
||||
};
|
||||
assert_eq!(r.min[..4], [0x24, 0x0A, 0xA0, 0x00]);
|
||||
assert_eq!(r.max[..4], [0x24, 0x0A, 0xA0, 0x09]);
|
||||
assert!(r.max[4..].iter().all(|&b| b == 0xFF));
|
||||
|
||||
// Inherit branch.
|
||||
let fam_inherit = der_sequence(vec![der_octet_string(&[0x00, 0x01]), der_null()]);
|
||||
let decoded = IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_all_inherit());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_sys_ids_decode_handles_inherit_ids_and_ranges() {
|
||||
// ASIdentifiers with:
|
||||
// - asnum [0] EXPLICIT asIdsOrRanges { id 64496, range 64500..64510 }
|
||||
let as_id_or_ranges = der_sequence(vec![
|
||||
der_integer_u64(64496),
|
||||
der_sequence(vec![der_integer_u64(64500), der_integer_u64(64510)]),
|
||||
]);
|
||||
let asnum = cs_cons(0, as_id_or_ranges);
|
||||
let as_identifiers = der_sequence(vec![asnum]);
|
||||
|
||||
let decoded = AsResourceSet::decode_extn_value(&as_identifiers).expect("decode asIdentifiers");
|
||||
assert!(decoded.rdi.is_none());
|
||||
let Some(asnum) = decoded.asnum else {
|
||||
panic!("missing asnum");
|
||||
};
|
||||
let AsIdentifierChoice::AsIdsOrRanges(items) = asnum else {
|
||||
panic!("expected AsIdsOrRanges");
|
||||
};
|
||||
assert_eq!(items.len(), 2);
|
||||
assert!(matches!(items[0], AsIdOrRange::Id(64496)));
|
||||
assert!(matches!(items[1], AsIdOrRange::Range { min: 64500, max: 64510 }));
|
||||
|
||||
// asnum inherit.
|
||||
let asnum_inherit = cs_cons(0, der_null());
|
||||
let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_asnum_inherit());
|
||||
}
|
||||
|
||||
202
tests/test_rc_resource_extensions_decode_errors.rs
Normal file
202
tests/test_rc_resource_extensions_decode_errors.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use rpki::data_model::rc::{AsResourceSet, IpResourceSet};
|
||||
|
||||
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_sequence(children: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let mut content = Vec::new();
|
||||
for c in children {
|
||||
content.extend(c);
|
||||
}
|
||||
tlv(0x30, &content)
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
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_bit_string(unused: u8, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut content = vec![unused];
|
||||
content.extend_from_slice(bytes);
|
||||
tlv(0x03, &content)
|
||||
}
|
||||
|
||||
fn cs_cons(tag_no: u8, inner_der: Vec<u8>) -> Vec<u8> {
|
||||
tlv(0xA0 | (tag_no & 0x1F), &inner_der)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_decode_rejects_invalid_encodings() {
|
||||
// Not a SEQUENCE.
|
||||
assert!(IpResourceSet::decode_extn_value(&der_null()).is_err());
|
||||
|
||||
// IPAddressFamily wrong shape.
|
||||
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x01])]); // missing choice
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// addressFamily wrong length.
|
||||
let fam_wrong = der_sequence(vec![
|
||||
der_octet_string(&[0x00]), // 1 byte
|
||||
der_null(),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// unsupported AFI.
|
||||
let fam_wrong = der_sequence(vec![der_octet_string(&[0x00, 0x03]), der_null()]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// ipAddressChoice wrong type.
|
||||
let fam_wrong = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_integer_u64(1),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam_wrong])).is_err());
|
||||
|
||||
// BitString with invalid unused-bits value (>7).
|
||||
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
|
||||
let max = der_bit_string(8, &[0x0A, 0xFF, 0xFF, 0xFF]); // invalid unused bits
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// BitString with non-zero bits in the unused tail.
|
||||
let min = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00]);
|
||||
let max = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// Prefix length out of range for IPv4 (40 bits).
|
||||
let prefix = der_bit_string(0, &[0x0A, 0x00, 0x00, 0x00, 0x00]); // 40 bits
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_sys_ids_decode_rejects_invalid_encodings() {
|
||||
// Not a SEQUENCE.
|
||||
assert!(AsResourceSet::decode_extn_value(&der_null()).is_err());
|
||||
|
||||
// Wrong tag class (expects [0]/[1] context-specific).
|
||||
let as_ids = der_sequence(vec![der_integer_u64(64496)]);
|
||||
assert!(AsResourceSet::decode_extn_value(&as_ids).is_err());
|
||||
|
||||
// Duplicate [0] tags.
|
||||
let a0 = cs_cons(0, der_null());
|
||||
let a0_dup = cs_cons(0, der_null());
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![a0, a0_dup])).is_err());
|
||||
|
||||
// Out-of-range ASID (> u32::MAX).
|
||||
let too_big = der_integer_bytes(&[0x01, 0x00, 0x00, 0x00, 0x00]); // 2^32
|
||||
let asnum = cs_cons(0, der_sequence(vec![too_big]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
|
||||
// Range min > max.
|
||||
let bad_range = der_sequence(vec![der_integer_u64(64510), der_integer_u64(64500)]);
|
||||
let asnum = cs_cons(0, der_sequence(vec![bad_range]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
|
||||
// Unsupported element inside asIdsOrRanges.
|
||||
let asnum = cs_cons(0, der_sequence(vec![der_null()]));
|
||||
assert!(AsResourceSet::decode_extn_value(&der_sequence(vec![asnum])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_prefix_bitstring_unused_bits_checks_are_enforced() {
|
||||
// Prefix BitString with non-zero bits in the unused tail (parse_ip_prefix path).
|
||||
let prefix = der_bit_string(1, &[0x0A, 0x00, 0x00, 0x01]); // LSB set, but unused=1
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
|
||||
// Prefix BitString with empty bytes but unused_bits != 0 is invalid.
|
||||
let prefix = der_bit_string(1, &[]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![prefix]),
|
||||
]);
|
||||
assert!(IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addr_blocks_range_upper_bound_can_fill_all_ones_when_bit_len_zero() {
|
||||
// A BIT STRING with 0 bits (content is only the "unused bits" count octet) is allowed.
|
||||
// For an IPAddressRange upper bound, RFC 3779 interprets missing bits as 1s.
|
||||
let min = der_bit_string(0, &[]);
|
||||
let max = der_bit_string(0, &[]);
|
||||
let range = der_sequence(vec![min, max]);
|
||||
let fam = der_sequence(vec![
|
||||
der_octet_string(&[0x00, 0x01]),
|
||||
der_sequence(vec![range]),
|
||||
]);
|
||||
|
||||
let set = IpResourceSet::decode_extn_value(&der_sequence(vec![fam])).expect("decode range with 0-bit endpoints");
|
||||
let fam = set.families.iter().find(|f| f.afi == rpki::data_model::rc::Afi::Ipv4).unwrap();
|
||||
let rpki::data_model::rc::IpAddressChoice::AddressesOrRanges(items) = &fam.choice else {
|
||||
panic!("expected explicit addressesOrRanges");
|
||||
};
|
||||
assert_eq!(items.len(), 1);
|
||||
let rpki::data_model::rc::IpAddressOrRange::Range(r) = &items[0] else {
|
||||
panic!("expected a range");
|
||||
};
|
||||
assert_eq!(r.min, vec![0, 0, 0, 0]);
|
||||
assert_eq!(r.max, vec![0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
}
|
||||
13
tests/test_roa_embedded_ee_cert.rs
Normal file
13
tests/test_roa_embedded_ee_cert.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
|
||||
#[test]
|
||||
fn roa_embedded_ee_cert_resources_validate() {
|
||||
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.validate_embedded_ee_cert()
|
||||
.expect("roa EE cert resources must validate");
|
||||
}
|
||||
@@ -1,7 +1,40 @@
|
||||
use rpki::data_model::roa::{
|
||||
EeResources, IpPrefix, IpResourceSet, RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily,
|
||||
RoaValidateError,
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix, IpResourceSet,
|
||||
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
|
||||
};
|
||||
use rpki::data_model::roa::{RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u8),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
subject_dn: "CN=subject".to_string(),
|
||||
validity_not_before: OffsetDateTime::UNIX_EPOCH,
|
||||
validity_not_after: OffsetDateTime::UNIX_EPOCH,
|
||||
subject_public_key_info: vec![],
|
||||
extensions: RcExtensions {
|
||||
basic_constraints_ca: false,
|
||||
subject_key_identifier: Some(vec![0x01]),
|
||||
subject_info_access: Some(SubjectInfoAccess::Ca(
|
||||
rpki::data_model::rc::SubjectInfoAccessCa {
|
||||
access_descriptions: vec![],
|
||||
},
|
||||
)),
|
||||
certificate_policies_oid: None,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
},
|
||||
},
|
||||
kind: ResourceCertKind::Ee,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||
RoaEContent {
|
||||
@@ -10,7 +43,7 @@ fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
addresses: vec![RoaIpAddress {
|
||||
prefix: IpPrefix {
|
||||
prefix: rpki::data_model::roa::IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
@@ -24,85 +57,120 @@ fn test_roa_single_v4_prefix() -> RoaEContent {
|
||||
#[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],
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 0,
|
||||
addr: vec![0, 0, 0, 0],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
},
|
||||
ip_resources_inherit: false,
|
||||
as_resources_present: false,
|
||||
};
|
||||
roa.validate_against_ee_resources(&ee)
|
||||
}),
|
||||
None,
|
||||
);
|
||||
roa.validate_against_ee_cert(&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();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet { families: vec![] }),
|
||||
Some(AsResourceSet {
|
||||
asnum: None,
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeAsResourcesPresent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_ip_resources_missing() {
|
||||
let roa = test_roa_single_v4_prefix();
|
||||
let ee = dummy_ee(None, None);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::EeIpResourcesMissing));
|
||||
}
|
||||
|
||||
#[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();
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&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],
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::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();
|
||||
}),
|
||||
None,
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&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 {
|
||||
let roa = RoaEContent {
|
||||
version: 0,
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![RoaIpAddressFamily {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 9,
|
||||
addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9
|
||||
addresses: vec![RoaIpAddress {
|
||||
prefix: rpki::data_model::roa::IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![0b1010_0000, 0x12, 0, 0], // 160.18.0.0/16
|
||||
},
|
||||
max_length: None,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
|
||||
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 ee = dummy_ee(
|
||||
Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 9,
|
||||
addr: vec![0b1010_0000, 0, 0, 0], // 160.0.0.0/9
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
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));
|
||||
roa.validate_against_ee_cert(&ee)
|
||||
.expect("160.18.0.0/16 should be covered by 160.0.0.0/9");
|
||||
}
|
||||
|
||||
|
||||
172
tests/test_ta_certificate.rs
Normal file
172
tests/test_ta_certificate.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use rpki::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateError};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate {
|
||||
let t = OffsetDateTime::from_unix_timestamp(0).unwrap();
|
||||
ResourceCertificate {
|
||||
raw_der: Vec::new(),
|
||||
kind: ResourceCertKind::Ca,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version: 2,
|
||||
serial_number: BigUint::from(1u32),
|
||||
signature_algorithm: "1.2.840.113549.1.1.11".into(),
|
||||
issuer_dn: "CN=TA".into(),
|
||||
subject_dn: "CN=TA".into(),
|
||||
validity_not_before: t,
|
||||
validity_not_after: t,
|
||||
subject_public_key_info: Vec::new(),
|
||||
extensions: ext,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_certificate_from_der_parses_downloaded_fixtures() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/ta/afrinic-ta.cer",
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
"tests/fixtures/ta/arin-ta.cer",
|
||||
"tests/fixtures/ta/lacnic-ta.cer",
|
||||
"tests/fixtures/ta/ripe-ncc-ta.cer",
|
||||
];
|
||||
for path in fixtures {
|
||||
let der = std::fs::read(path).expect("read TA fixture");
|
||||
let ta = TaCertificate::from_der(&der).expect("parse TA fixture");
|
||||
assert_eq!(ta.rc_ca.kind, ResourceCertKind::Ca);
|
||||
assert_eq!(
|
||||
ta.rc_ca.tbs.extensions.certificate_policies_oid.as_deref(),
|
||||
Some(OID_CP_IPADDR_ASNUMBER)
|
||||
);
|
||||
assert!(ta.rc_ca.tbs.extensions.subject_key_identifier.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_certificate_rejects_non_self_signed_ca() {
|
||||
// This is a CA cert fixture, but not self-signed (issuer != subject).
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read CA cert fixture");
|
||||
assert!(matches!(
|
||||
TaCertificate::from_der(&der),
|
||||
Err(TaCertificateError::NotSelfSignedIssuerSubject)
|
||||
| Err(TaCertificateError::InvalidSelfSignature(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_constraints_require_policies_and_ski() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: None,
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: None,
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
as_resources: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingOrInvalidCertificatePolicies)
|
||||
));
|
||||
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingSubjectKeyIdentifier)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
// Missing both IP and AS resources.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
basic_constraints_ca: true,
|
||||
subject_key_identifier: Some(vec![1]),
|
||||
subject_info_access: None,
|
||||
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
|
||||
ip_resources: None,
|
||||
as_resources: None,
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesMissing)
|
||||
));
|
||||
|
||||
// IP resources present but empty => resources empty.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesEmpty)
|
||||
));
|
||||
|
||||
// IP resources inherit is rejected.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::IpResourcesInherit)
|
||||
));
|
||||
|
||||
// AS resources inherit is rejected.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::AsResourcesInherit)
|
||||
));
|
||||
|
||||
// Valid non-empty explicit IP resources => OK.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
afi: Afi::Ipv6,
|
||||
prefix_len: 32,
|
||||
addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
})]),
|
||||
}],
|
||||
}),
|
||||
as_resources: None,
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit resources");
|
||||
|
||||
// Valid non-empty explicit AS resources => OK.
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit AS resources");
|
||||
}
|
||||
|
||||
31
tests/test_tal_decode.rs
Normal file
31
tests/test_tal_decode.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use der_parser::der::parse_der;
|
||||
use rpki::data_model::tal::Tal;
|
||||
|
||||
#[test]
|
||||
fn decode_tal_fixtures_smoke() {
|
||||
let fixtures = [
|
||||
"tests/fixtures/tal/afrinic.tal",
|
||||
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||
"tests/fixtures/tal/arin.tal",
|
||||
"tests/fixtures/tal/lacnic.tal",
|
||||
"tests/fixtures/tal/ripe-ncc.tal",
|
||||
];
|
||||
|
||||
for path in fixtures {
|
||||
let raw = std::fs::read(path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&raw).expect("decode TAL fixture");
|
||||
|
||||
assert!(!tal.ta_uris.is_empty(), "TA URI list must be non-empty: {path}");
|
||||
assert!(!tal.subject_public_key_info_der.is_empty(), "SPKI DER must be non-empty: {path}");
|
||||
|
||||
for u in &tal.ta_uris {
|
||||
assert!(matches!(u.scheme(), "rsync" | "https"), "scheme must be allowed: {u}");
|
||||
assert!(!u.path().ends_with('/'), "TA URI must not be a directory: {u}");
|
||||
}
|
||||
|
||||
// SPKI DER must be parseable as a DER object (typically a SEQUENCE).
|
||||
let (rem, _obj) = parse_der(&tal.subject_public_key_info_der).expect("parse spki DER");
|
||||
assert!(rem.is_empty(), "SPKI DER must not have trailing bytes: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
71
tests/test_tal_decode_errors.rs
Normal file
71
tests/test_tal_decode_errors.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use rpki::data_model::tal::{Tal, TalDecodeError};
|
||||
|
||||
fn mk_tal(uris: &[&str], b64_lines: &[&str]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("# comment\n");
|
||||
for u in uris {
|
||||
out.push_str(u);
|
||||
out.push('\n');
|
||||
}
|
||||
out.push('\n'); // separator
|
||||
for l in b64_lines {
|
||||
out.push_str(l);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_separator() {
|
||||
let s = "# c\nhttps://example.invalid/ta.cer\nAAAA\n";
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::MissingSeparatorEmptyLine)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_uris() {
|
||||
let s = "# c\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::MissingTaUris)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_unsupported_scheme() {
|
||||
let s = mk_tal(&["ftp://example.invalid/ta.cer"], &["AAAA"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::UnsupportedUriScheme(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_directory_uri() {
|
||||
let s = mk_tal(&["https://example.invalid/dir/"], &["AAAA"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::UriIsDirectory(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_comment_after_header() {
|
||||
let s = "# c\nhttps://example.invalid/ta.cer\n# late\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::CommentAfterHeader)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_base64() {
|
||||
let s = mk_tal(&["https://example.invalid/ta.cer"], &["not-base64!!!"]);
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::SpkiBase64Decode)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_utf8() {
|
||||
let bytes = [0xFFu8, 0xFEu8];
|
||||
assert!(matches!(Tal::decode_bytes(&bytes), Err(TalDecodeError::InvalidUtf8)));
|
||||
}
|
||||
|
||||
62
tests/test_trust_anchor_bind.rs
Normal file
62
tests/test_trust_anchor_bind.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use rpki::data_model::ta::{TrustAnchor, TrustAnchorError};
|
||||
use rpki::data_model::tal::Tal;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn bind_trust_anchor_with_downloaded_fixtures_succeeds() {
|
||||
let cases = [
|
||||
("tests/fixtures/tal/afrinic.tal", "tests/fixtures/ta/afrinic-ta.cer"),
|
||||
(
|
||||
"tests/fixtures/tal/apnic-rfc7730-https.tal",
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
),
|
||||
("tests/fixtures/tal/arin.tal", "tests/fixtures/ta/arin-ta.cer"),
|
||||
("tests/fixtures/tal/lacnic.tal", "tests/fixtures/ta/lacnic-ta.cer"),
|
||||
("tests/fixtures/tal/ripe-ncc.tal", "tests/fixtures/ta/ripe-ncc-ta.cer"),
|
||||
];
|
||||
|
||||
for (tal_path, ta_path) in cases {
|
||||
let tal_raw = std::fs::read(tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read(ta_path).expect("read TA fixture");
|
||||
|
||||
TrustAnchor::bind(tal.clone(), &ta_der, None).expect("bind without resolved uri");
|
||||
|
||||
// Also exercise the resolved-uri-in-TAL check using one URI from the TAL list.
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.iter()
|
||||
.find(|u| u.scheme() == "https")
|
||||
.or_else(|| tal.ta_uris.first())
|
||||
.expect("tal has ta uris");
|
||||
let resolved = resolved.clone();
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_spki_mismatch() {
|
||||
let tal_raw = std::fs::read("tests/fixtures/tal/ripe-ncc.tal").expect("read TAL fixture");
|
||||
let mut tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/ripe-ncc-ta.cer").expect("read TA fixture");
|
||||
|
||||
// Flip a byte in TAL SPKI to force mismatch.
|
||||
tal.subject_public_key_info_der[0] ^= 0x01;
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, None),
|
||||
Err(TrustAnchorError::TalSpkiMismatch)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_rejects_resolved_uri_not_listed_in_tal() {
|
||||
let tal_raw = std::fs::read("tests/fixtures/tal/afrinic.tal").expect("read TAL fixture");
|
||||
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/afrinic-ta.cer").expect("read TA fixture");
|
||||
|
||||
let bad = Url::parse("https://example.invalid/not-in-tal.cer").unwrap();
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&bad)),
|
||||
Err(TrustAnchorError::ResolvedUriNotInTal(_))
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user