重构error code
This commit is contained in:
parent
cc9f3f21de
commit
a58e507f92
86
specs/arch.excalidraw
Normal file
86
specs/arch.excalidraw
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
|
||||
"elements": [
|
||||
{
|
||||
"id": "A3BjC6kJe019Pes-xjr_L",
|
||||
"type": "rectangle",
|
||||
"x": 307.66668701171875,
|
||||
"y": 719.3333740234375,
|
||||
"width": 321.66668701171875,
|
||||
"height": 104,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0",
|
||||
"roundness": {
|
||||
"type": 3
|
||||
},
|
||||
"seed": 1572721102,
|
||||
"version": 56,
|
||||
"versionNonce": 1402545874,
|
||||
"isDeleted": false,
|
||||
"boundElements": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "KnkPQWiZzaALgJ-eehAOa"
|
||||
}
|
||||
],
|
||||
"updated": 1770174471076,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "KnkPQWiZzaALgJ-eehAOa",
|
||||
"type": "text",
|
||||
"x": 406.6700668334961,
|
||||
"y": 746.3333740234375,
|
||||
"width": 123.65992736816406,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"index": "a0V",
|
||||
"roundness": null,
|
||||
"seed": 1287469326,
|
||||
"version": 49,
|
||||
"versionNonce": 630772558,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1770174490368,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "lib\n(data model)",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 5,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "A3BjC6kJe019Pes-xjr_L",
|
||||
"originalText": "lib\n(data model)",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"gridModeEnabled": false,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
use crate::data_model::oid::OID_CT_ASPA;
|
||||
use crate::data_model::rc::ResourceCertificate;
|
||||
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||
use der_parser::ber::{Class};
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use crate::data_model::signed_object::{
|
||||
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
|
||||
};
|
||||
use der_parser::ber::Class;
|
||||
use der_parser::der::{DerObject, Tag, parse_der};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaObject {
|
||||
@ -11,6 +13,13 @@ pub struct AspaObject {
|
||||
pub aspa: AspaEContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaObjectParsed {
|
||||
pub signed_object: RpkiSignedObjectParsed,
|
||||
pub econtent_type: String,
|
||||
pub aspa: Option<AspaEContentParsed>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaEContent {
|
||||
pub version: u32,
|
||||
@ -18,70 +27,151 @@ pub struct AspaEContent {
|
||||
pub provider_as_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AspaEContentParsed {
|
||||
der: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AspaDecodeError {
|
||||
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
||||
|
||||
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)")]
|
||||
InvalidEContentType(String),
|
||||
|
||||
pub enum AspaParseError {
|
||||
#[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObject(#[from] SignedObjectParseError),
|
||||
#[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("ASPA trailing bytes: {0} bytes (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||
TrailingBytes(usize),
|
||||
}
|
||||
|
||||
#[error("ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)")]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AspaProfileError {
|
||||
#[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObject(#[from] SignedObjectValidateError),
|
||||
|
||||
#[error(
|
||||
"ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)"
|
||||
)]
|
||||
InvalidEContentType(String),
|
||||
|
||||
#[error("ASPA profile decode error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")]
|
||||
ProfileDecode(String),
|
||||
|
||||
#[error(
|
||||
"ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)"
|
||||
)]
|
||||
InvalidAttestationSequence,
|
||||
|
||||
#[error("ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)")]
|
||||
#[error(
|
||||
"ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)"
|
||||
)]
|
||||
VersionMustBeExplicitOne,
|
||||
|
||||
#[error("ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)")]
|
||||
#[error(
|
||||
"ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)"
|
||||
)]
|
||||
CustomerAsIdOutOfRange(u64),
|
||||
|
||||
#[error("ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||
#[error(
|
||||
"ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)"
|
||||
)]
|
||||
EmptyProviders,
|
||||
|
||||
#[error("ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||
#[error(
|
||||
"ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)"
|
||||
)]
|
||||
ProviderAsIdOutOfRange(u64),
|
||||
|
||||
#[error("ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||
#[error(
|
||||
"ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)"
|
||||
)]
|
||||
ProvidersNotStrictlyIncreasing,
|
||||
|
||||
#[error("ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)")]
|
||||
#[error(
|
||||
"ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)"
|
||||
)]
|
||||
ProvidersContainCustomer(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AspaDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] AspaParseError),
|
||||
|
||||
#[error("{0}")]
|
||||
Validate(#[from] AspaProfileError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AspaValidateError {
|
||||
#[error("ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)")]
|
||||
#[error(
|
||||
"ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)"
|
||||
)]
|
||||
EeAsResourcesMissing,
|
||||
|
||||
#[error("ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)")]
|
||||
#[error(
|
||||
"ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)"
|
||||
)]
|
||||
EeAsResourcesInherit,
|
||||
|
||||
#[error("ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)")]
|
||||
#[error(
|
||||
"ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)"
|
||||
)]
|
||||
EeAsResourcesRangePresent,
|
||||
|
||||
#[error("ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)")]
|
||||
#[error(
|
||||
"ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)"
|
||||
)]
|
||||
EeAsResourcesRdiPresent,
|
||||
|
||||
#[error("ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)")]
|
||||
#[error(
|
||||
"ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)"
|
||||
)]
|
||||
EeAsResourcesNotSingleId,
|
||||
|
||||
#[error("ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)")]
|
||||
#[error(
|
||||
"ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)"
|
||||
)]
|
||||
CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 },
|
||||
|
||||
#[error("ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)")]
|
||||
#[error(
|
||||
"ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)"
|
||||
)]
|
||||
EeIpResourcesPresent,
|
||||
}
|
||||
|
||||
impl AspaObject {
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<AspaObjectParsed, AspaParseError> {
|
||||
let signed_object = RpkiSignedObject::parse_der(der)?;
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
let aspa = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent
|
||||
.as_deref()
|
||||
.map(AspaEContent::parse_der)
|
||||
.transpose()?;
|
||||
Ok(AspaObjectParsed {
|
||||
signed_object,
|
||||
econtent_type,
|
||||
aspa,
|
||||
})
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `AspaObject` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `AspaObjectParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), AspaProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||
Self::from_signed_object(signed_object)
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, AspaDecodeError> {
|
||||
@ -91,10 +181,11 @@ impl AspaObject {
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_ASPA {
|
||||
return Err(AspaDecodeError::InvalidEContentType(econtent_type));
|
||||
return Err(AspaProfileError::InvalidEContentType(econtent_type).into());
|
||||
}
|
||||
|
||||
let aspa = AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
let aspa =
|
||||
AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
Ok(Self {
|
||||
aspa,
|
||||
signed_object,
|
||||
@ -110,57 +201,27 @@ impl AspaObject {
|
||||
}
|
||||
|
||||
impl AspaEContent {
|
||||
/// Decode the DER-encoded ASProviderAttestation defined in draft-ietf-sidrops-aspa-profile-21 §3.
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<AspaEContentParsed, AspaParseError> {
|
||||
let (rem, _obj) = parse_der(der).map_err(|e| AspaParseError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(AspaParseError::TrailingBytes(rem.len()));
|
||||
}
|
||||
Ok(AspaEContentParsed { der: der.to_vec() })
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `AspaEContent` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `AspaEContentParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), AspaProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode the DER-encoded ASProviderAttestation defined in
|
||||
/// draft-ietf-sidrops-aspa-profile-21 §3 (`parse + validate`).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
|
||||
let (rem, obj) = parse_der(der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(AspaDecodeError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 3 {
|
||||
return Err(AspaDecodeError::InvalidAttestationSequence);
|
||||
}
|
||||
|
||||
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(AspaDecodeError::VersionMustBeExplicitOne);
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(AspaDecodeError::Parse(
|
||||
"trailing bytes inside ASProviderAttestation.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if v != 1 {
|
||||
return Err(AspaDecodeError::VersionMustBeExplicitOne);
|
||||
}
|
||||
|
||||
let customer_u64 = seq[1]
|
||||
.as_u64()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
if customer_u64 > u32::MAX as u64 {
|
||||
return Err(AspaDecodeError::CustomerAsIdOutOfRange(customer_u64));
|
||||
}
|
||||
let customer_as_id = customer_u64 as u32;
|
||||
|
||||
let providers = parse_providers(&seq[2], customer_as_id)?;
|
||||
|
||||
Ok(Self {
|
||||
version: 1,
|
||||
customer_as_id,
|
||||
provider_as_ids: providers,
|
||||
})
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
/// Validate ASPA payload against the embedded EE resource certificate.
|
||||
@ -206,12 +267,87 @@ impl AspaEContent {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaDecodeError> {
|
||||
impl AspaObjectParsed {
|
||||
pub fn validate_profile(self) -> Result<AspaObject, AspaProfileError> {
|
||||
let signed_object = self.signed_object.validate_profile()?;
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_ASPA {
|
||||
return Err(AspaProfileError::InvalidEContentType(econtent_type));
|
||||
}
|
||||
let aspa = self
|
||||
.aspa
|
||||
.ok_or_else(|| AspaProfileError::ProfileDecode("ASPA.eContent missing".into()))?
|
||||
.validate_profile()?;
|
||||
Ok(AspaObject {
|
||||
signed_object,
|
||||
econtent_type: OID_CT_ASPA.to_string(),
|
||||
aspa,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AspaEContentParsed {
|
||||
pub fn validate_profile(self) -> Result<AspaEContent, AspaProfileError> {
|
||||
let (_rem, obj) =
|
||||
parse_der(&self.der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.len() != 3 {
|
||||
return Err(AspaProfileError::InvalidAttestationSequence);
|
||||
}
|
||||
|
||||
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(AspaProfileError::VersionMustBeExplicitOne);
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(AspaProfileError::ProfileDecode(
|
||||
"trailing bytes inside ASProviderAttestation.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if v != 1 {
|
||||
return Err(AspaProfileError::VersionMustBeExplicitOne);
|
||||
}
|
||||
|
||||
let customer_u64 = seq[1]
|
||||
.as_u64()
|
||||
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if customer_u64 > u32::MAX as u64 {
|
||||
return Err(AspaProfileError::CustomerAsIdOutOfRange(customer_u64));
|
||||
}
|
||||
let customer_as_id = customer_u64 as u32;
|
||||
|
||||
let providers = parse_providers(&seq[2], customer_as_id)?;
|
||||
|
||||
Ok(AspaEContent {
|
||||
version: 1,
|
||||
customer_as_id,
|
||||
provider_as_ids: providers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.is_empty() {
|
||||
return Err(AspaDecodeError::EmptyProviders);
|
||||
return Err(AspaProfileError::EmptyProviders);
|
||||
}
|
||||
|
||||
let mut out: Vec<u32> = Vec::with_capacity(seq.len());
|
||||
@ -219,17 +355,17 @@ fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>,
|
||||
for item in seq {
|
||||
let v = item
|
||||
.as_u64()
|
||||
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if v > u32::MAX as u64 {
|
||||
return Err(AspaDecodeError::ProviderAsIdOutOfRange(v));
|
||||
return Err(AspaProfileError::ProviderAsIdOutOfRange(v));
|
||||
}
|
||||
let asn = v as u32;
|
||||
if asn == customer_as_id {
|
||||
return Err(AspaDecodeError::ProvidersContainCustomer(customer_as_id));
|
||||
return Err(AspaProfileError::ProvidersContainCustomer(customer_as_id));
|
||||
}
|
||||
if let Some(p) = prev {
|
||||
if asn <= p {
|
||||
return Err(AspaDecodeError::ProvidersNotStrictlyIncreasing);
|
||||
return Err(AspaProfileError::ProvidersNotStrictlyIncreasing);
|
||||
}
|
||||
}
|
||||
prev = Some(asn);
|
||||
|
||||
@ -69,7 +69,9 @@ impl BigUnsigned {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §4.1.2.5; RFC 5280 §5.1.2.4-§5.1.2.6)")]
|
||||
#[error(
|
||||
"{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §4.1.2.5; RFC 5280 §5.1.2.4-§5.1.2.6)"
|
||||
)]
|
||||
pub struct InvalidTimeEncodingError {
|
||||
pub field: &'static str,
|
||||
pub year: i32,
|
||||
@ -103,6 +105,5 @@ pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool {
|
||||
///
|
||||
/// Notes:
|
||||
/// - Includes entries marked TEMPORARY/DEPRECATED by IANA (e.g., `asa`, `gbr`).
|
||||
pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] = &[
|
||||
"asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak",
|
||||
];
|
||||
pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] =
|
||||
&["asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak"];
|
||||
|
||||
@ -3,13 +3,12 @@ use crate::data_model::oid::{
|
||||
OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION,
|
||||
OID_SUBJECT_KEY_IDENTIFIER,
|
||||
};
|
||||
use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::prelude::X509Version;
|
||||
use x509_parser::revocation_list::CertificateRevocationList;
|
||||
use x509_parser::certificate::X509Certificate;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
use x509_parser::extensions::{ParsedExtension, X509Extension};
|
||||
use x509_parser::prelude::{FromDer, X509Version};
|
||||
use x509_parser::revocation_list::CertificateRevocationList;
|
||||
use x509_parser::x509::{AlgorithmIdentifier, SubjectPublicKeyInfo};
|
||||
use x509_parser::{asn1_rs::Class as Asn1Class, asn1_rs::Tag as Asn1Tag};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RevokedCert {
|
||||
@ -35,26 +34,93 @@ pub struct RpkixCrl {
|
||||
pub extensions: CrlExtensions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RpkixCrlParsed {
|
||||
pub raw_der: Vec<u8>,
|
||||
pub version: Option<X509Version>,
|
||||
pub issuer_dn: String,
|
||||
pub signature_algorithm: AlgorithmIdentifierValue,
|
||||
pub tbs_signature_algorithm: AlgorithmIdentifierValue,
|
||||
pub this_update: Asn1TimeUtc,
|
||||
pub next_update: Option<Asn1TimeUtc>,
|
||||
pub revoked_certs: Vec<RevokedCertParsed>,
|
||||
pub extensions: Vec<CrlExtensionParsed>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RevokedCertParsed {
|
||||
pub serial_number: BigUnsigned,
|
||||
pub revocation_date: Asn1TimeUtc,
|
||||
pub has_extensions: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CrlExtensionParsed {
|
||||
AuthorityKeyIdentifier {
|
||||
key_identifier: Option<Vec<u8>>,
|
||||
has_other_fields: bool,
|
||||
critical: bool,
|
||||
},
|
||||
CrlNumber {
|
||||
number: der_parser::num_bigint::BigUint,
|
||||
critical: bool,
|
||||
},
|
||||
Other {
|
||||
oid: String,
|
||||
critical: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AlgorithmIdentifierValue {
|
||||
pub oid: String,
|
||||
pub parameters: Option<AlgorithmParametersValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AlgorithmParametersValue {
|
||||
pub class: Asn1Class,
|
||||
pub tag: Asn1Tag,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl AlgorithmIdentifierValue {
|
||||
pub fn params_absent_or_null(&self) -> bool {
|
||||
match &self.parameters {
|
||||
None => true,
|
||||
Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true,
|
||||
Some(_p) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrlDecodeError {
|
||||
pub enum CrlParseError {
|
||||
#[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||
TrailingBytes(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrlProfileError {
|
||||
#[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||
InvalidVersion(Option<u32>),
|
||||
|
||||
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")]
|
||||
InvalidSignatureAlgorithm(String),
|
||||
|
||||
#[error("CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
|
||||
InvalidSignatureAlgorithmParameters,
|
||||
|
||||
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
|
||||
SignatureAlgorithmMismatch,
|
||||
|
||||
#[error(
|
||||
"CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)"
|
||||
)]
|
||||
InvalidSignatureAlgorithm(String),
|
||||
|
||||
#[error(
|
||||
"CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)"
|
||||
)]
|
||||
InvalidSignatureAlgorithmParameters,
|
||||
|
||||
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")]
|
||||
InvalidExtensionsCount(usize),
|
||||
|
||||
@ -64,12 +130,20 @@ pub enum CrlDecodeError {
|
||||
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
|
||||
DuplicateExtension(String),
|
||||
|
||||
#[error("AuthorityKeyIdentifier CRL extension missing (RFC 9829 §3.1; RFC 5280 §5.2.1)")]
|
||||
AkiMissing,
|
||||
|
||||
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||
AkiMissingKeyIdentifier,
|
||||
|
||||
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
||||
#[error(
|
||||
"AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)"
|
||||
)]
|
||||
AkiHasOtherFields,
|
||||
|
||||
#[error("CRLNumber CRL extension missing (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
|
||||
CrlNumberMissing,
|
||||
|
||||
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
|
||||
CrlNumberCritical,
|
||||
|
||||
@ -82,7 +156,9 @@ pub enum CrlDecodeError {
|
||||
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")]
|
||||
NextUpdateMissing,
|
||||
|
||||
#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)")]
|
||||
#[error(
|
||||
"{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)"
|
||||
)]
|
||||
InvalidTimeEncoding {
|
||||
field: &'static str,
|
||||
year: i32,
|
||||
@ -90,64 +166,47 @@ pub enum CrlDecodeError {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrlDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] CrlParseError),
|
||||
|
||||
#[error("{0}")]
|
||||
Validate(#[from] CrlProfileError),
|
||||
}
|
||||
|
||||
impl RpkixCrl {
|
||||
/// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from
|
||||
/// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, CrlDecodeError> {
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<RpkixCrlParsed, CrlParseError> {
|
||||
let (rem, crl) = CertificateRevocationList::from_der(der)
|
||||
.map_err(|e| CrlDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| CrlParseError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CrlDecodeError::TrailingBytes(rem.len()));
|
||||
return Err(CrlParseError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let version = match crl.version() {
|
||||
Some(X509Version::V2) => 2,
|
||||
Some(v) => return Err(CrlDecodeError::InvalidVersion(Some(v.0))),
|
||||
None => return Err(CrlDecodeError::InvalidVersion(None)),
|
||||
};
|
||||
|
||||
let sig_oid = crl.signature_algorithm.algorithm.to_id_string();
|
||||
let tbs_sig_oid = crl.tbs_cert_list.signature.algorithm.to_id_string();
|
||||
if sig_oid != tbs_sig_oid {
|
||||
return Err(CrlDecodeError::SignatureAlgorithmMismatch);
|
||||
}
|
||||
if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION {
|
||||
return Err(CrlDecodeError::InvalidSignatureAlgorithm(sig_oid));
|
||||
}
|
||||
validate_sig_params(&crl.signature_algorithm)?;
|
||||
validate_sig_params(&crl.tbs_cert_list.signature)?;
|
||||
|
||||
let extensions = parse_and_validate_extensions(crl.extensions())?;
|
||||
|
||||
let revoked_certs = crl
|
||||
.iter_revoked_certificates()
|
||||
.map(|rc| {
|
||||
if !rc.extensions().is_empty() {
|
||||
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
||||
}
|
||||
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
|
||||
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
|
||||
Ok(RevokedCert {
|
||||
.map(|rc| RevokedCertParsed {
|
||||
serial_number: BigUnsigned::from_biguint(rc.serial()),
|
||||
revocation_date,
|
||||
revocation_date: crate::data_model::common::asn1_time_to_model(rc.revocation_date),
|
||||
has_extensions: !rc.extensions().is_empty(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
|
||||
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
|
||||
|
||||
let next_update = crl
|
||||
.next_update()
|
||||
.map(crate::data_model::common::asn1_time_to_model)
|
||||
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
||||
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
||||
.map(crate::data_model::common::asn1_time_to_model);
|
||||
|
||||
Ok(RpkixCrl {
|
||||
let extensions =
|
||||
parse_extensions_parse(crl.extensions()).map_err(|e| CrlParseError::Parse(e))?;
|
||||
|
||||
Ok(RpkixCrlParsed {
|
||||
raw_der: der.to_vec(),
|
||||
version,
|
||||
version: crl.version(),
|
||||
issuer_dn: crl.issuer().to_string(),
|
||||
signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(),
|
||||
signature_algorithm: algorithm_identifier_value(&crl.signature_algorithm),
|
||||
tbs_signature_algorithm: algorithm_identifier_value(&crl.tbs_cert_list.signature),
|
||||
this_update,
|
||||
next_update,
|
||||
revoked_certs,
|
||||
@ -155,6 +214,20 @@ impl RpkixCrl {
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from
|
||||
/// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, CrlDecodeError> {
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `RpkixCrl` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `RpkixCrlParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), CrlProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the cryptographic signature on this CRL using the issuer certificate.
|
||||
///
|
||||
/// Signature verification needs the issuer public key (RFC 5280 §6.3.3 (f)-(g)).
|
||||
@ -229,6 +302,59 @@ impl RpkixCrl {
|
||||
}
|
||||
}
|
||||
|
||||
impl RpkixCrlParsed {
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
pub fn validate_profile(self) -> Result<RpkixCrl, CrlProfileError> {
|
||||
let version = match self.version {
|
||||
Some(X509Version::V2) => 2,
|
||||
Some(v) => return Err(CrlProfileError::InvalidVersion(Some(v.0))),
|
||||
None => return Err(CrlProfileError::InvalidVersion(None)),
|
||||
};
|
||||
|
||||
// signatureAlgorithm must match tbsCertList.signature
|
||||
if self.signature_algorithm != self.tbs_signature_algorithm {
|
||||
return Err(CrlProfileError::SignatureAlgorithmMismatch);
|
||||
}
|
||||
let sig_oid = self.signature_algorithm.oid.clone();
|
||||
if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION {
|
||||
return Err(CrlProfileError::InvalidSignatureAlgorithm(sig_oid));
|
||||
}
|
||||
if !self.signature_algorithm.params_absent_or_null() {
|
||||
return Err(CrlProfileError::InvalidSignatureAlgorithmParameters);
|
||||
}
|
||||
|
||||
let extensions = validate_extensions_profile(&self.extensions)?;
|
||||
|
||||
let mut revoked_out = Vec::with_capacity(self.revoked_certs.len());
|
||||
for rc in self.revoked_certs {
|
||||
if rc.has_extensions {
|
||||
return Err(CrlProfileError::EntryExtensionsNotAllowed);
|
||||
}
|
||||
validate_time_encoding_rfc5280("revocationDate", &rc.revocation_date)?;
|
||||
revoked_out.push(RevokedCert {
|
||||
serial_number: rc.serial_number,
|
||||
revocation_date: rc.revocation_date,
|
||||
});
|
||||
}
|
||||
|
||||
validate_time_encoding_rfc5280("thisUpdate", &self.this_update)?;
|
||||
|
||||
let next_update = self.next_update.ok_or(CrlProfileError::NextUpdateMissing)?;
|
||||
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
||||
|
||||
Ok(RpkixCrl {
|
||||
raw_der: self.raw_der,
|
||||
version,
|
||||
issuer_dn: self.issuer_dn,
|
||||
signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(),
|
||||
this_update: self.this_update,
|
||||
next_update,
|
||||
revoked_certs: revoked_out,
|
||||
extensions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CrlVerifyError {
|
||||
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
@ -240,7 +366,9 @@ pub enum CrlVerifyError {
|
||||
#[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")]
|
||||
IssuerSpkiParse(String),
|
||||
|
||||
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)")]
|
||||
#[error(
|
||||
"trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)"
|
||||
)]
|
||||
IssuerSpkiTrailingBytes(usize),
|
||||
|
||||
#[error("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
||||
@ -249,16 +377,22 @@ pub enum CrlVerifyError {
|
||||
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
||||
CrlTrailingBytes(usize),
|
||||
|
||||
#[error("CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))")]
|
||||
#[error(
|
||||
"CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))"
|
||||
)]
|
||||
IssuerSubjectMismatch {
|
||||
crl_issuer_dn: String,
|
||||
issuer_subject_dn: String,
|
||||
},
|
||||
|
||||
#[error("issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))")]
|
||||
#[error(
|
||||
"issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))"
|
||||
)]
|
||||
IssuerKeyUsageMissingCrlSign,
|
||||
|
||||
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))")]
|
||||
#[error(
|
||||
"CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))"
|
||||
)]
|
||||
AkiSkiMismatch,
|
||||
|
||||
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
|
||||
@ -268,7 +402,7 @@ pub enum CrlVerifyError {
|
||||
fn validate_time_encoding_rfc5280(
|
||||
field: &'static str,
|
||||
t: &Asn1TimeUtc,
|
||||
) -> Result<(), CrlDecodeError> {
|
||||
) -> Result<(), CrlProfileError> {
|
||||
let year = t.utc.year();
|
||||
let expected = if year <= 2049 {
|
||||
Asn1TimeEncoding::UtcTime
|
||||
@ -276,7 +410,7 @@ fn validate_time_encoding_rfc5280(
|
||||
Asn1TimeEncoding::GeneralizedTime
|
||||
};
|
||||
if t.encoding != expected {
|
||||
return Err(CrlDecodeError::InvalidTimeEncoding {
|
||||
return Err(CrlProfileError::InvalidTimeEncoding {
|
||||
field,
|
||||
year,
|
||||
encoding: t.encoding,
|
||||
@ -285,86 +419,109 @@ fn validate_time_encoding_rfc5280(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
||||
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
|
||||
fn algorithm_identifier_value(ai: &AlgorithmIdentifier<'_>) -> AlgorithmIdentifierValue {
|
||||
let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue {
|
||||
class: p.class(),
|
||||
tag: p.tag(),
|
||||
data: p.as_bytes().to_vec(),
|
||||
});
|
||||
AlgorithmIdentifierValue {
|
||||
oid: ai.algorithm.to_id_string(),
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtensions, CrlDecodeError> {
|
||||
if exts.len() != 2 {
|
||||
return Err(CrlDecodeError::InvalidExtensionsCount(exts.len()));
|
||||
}
|
||||
|
||||
let mut authority_key_identifier: Option<Vec<u8>> = None;
|
||||
let mut crl_number: Option<BigUnsigned> = None;
|
||||
|
||||
fn parse_extensions_parse(exts: &[X509Extension<'_>]) -> Result<Vec<CrlExtensionParsed>, String> {
|
||||
let mut out = Vec::with_capacity(exts.len());
|
||||
for ext in exts {
|
||||
let oid = ext.oid.to_id_string();
|
||||
match oid.as_str() {
|
||||
OID_AUTHORITY_KEY_IDENTIFIER => {
|
||||
if authority_key_identifier.is_some() {
|
||||
return Err(CrlDecodeError::DuplicateExtension(oid));
|
||||
let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
|
||||
return Err("AKI extension parse failed".to_string());
|
||||
};
|
||||
out.push(CrlExtensionParsed::AuthorityKeyIdentifier {
|
||||
key_identifier: aki.key_identifier.as_ref().map(|k| k.0.to_vec()),
|
||||
has_other_fields: aki.authority_cert_issuer.is_some()
|
||||
|| aki.authority_cert_serial.is_some(),
|
||||
critical: ext.critical,
|
||||
});
|
||||
}
|
||||
let aki = parse_aki(ext)?;
|
||||
authority_key_identifier = Some(aki);
|
||||
OID_CRL_NUMBER => match ext.parsed_extension() {
|
||||
ParsedExtension::CRLNumber(n) => out.push(CrlExtensionParsed::CrlNumber {
|
||||
number: n.clone(),
|
||||
critical: ext.critical,
|
||||
}),
|
||||
_ => return Err("CRLNumber extension parse failed".to_string()),
|
||||
},
|
||||
_ => out.push(CrlExtensionParsed::Other {
|
||||
oid,
|
||||
critical: ext.critical,
|
||||
}),
|
||||
}
|
||||
OID_CRL_NUMBER => {
|
||||
if crl_number.is_some() {
|
||||
return Err(CrlDecodeError::DuplicateExtension(oid));
|
||||
}
|
||||
if ext.critical {
|
||||
return Err(CrlDecodeError::CrlNumberCritical);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn validate_extensions_profile(
|
||||
exts: &[CrlExtensionParsed],
|
||||
) -> Result<CrlExtensions, CrlProfileError> {
|
||||
if exts.len() != 2 {
|
||||
return Err(CrlProfileError::InvalidExtensionsCount(exts.len()));
|
||||
}
|
||||
let n = parse_crl_number(ext)?;
|
||||
if n.bits() > 159 {
|
||||
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
||||
|
||||
let mut seen: Vec<String> = Vec::new();
|
||||
let mut authority_key_identifier: Option<Vec<u8>> = None;
|
||||
let mut crl_number: Option<BigUnsigned> = None;
|
||||
|
||||
for ext in exts {
|
||||
match ext {
|
||||
CrlExtensionParsed::AuthorityKeyIdentifier {
|
||||
key_identifier,
|
||||
has_other_fields,
|
||||
critical: _,
|
||||
} => {
|
||||
let oid = OID_AUTHORITY_KEY_IDENTIFIER.to_string();
|
||||
if seen.iter().any(|s| s == &oid) {
|
||||
return Err(CrlProfileError::DuplicateExtension(oid));
|
||||
}
|
||||
crl_number = Some(BigUnsigned::from_biguint(&n));
|
||||
seen.push(oid.clone());
|
||||
|
||||
if *has_other_fields {
|
||||
return Err(CrlProfileError::AkiHasOtherFields);
|
||||
}
|
||||
let ki = key_identifier
|
||||
.as_ref()
|
||||
.ok_or(CrlProfileError::AkiMissingKeyIdentifier)?;
|
||||
authority_key_identifier = Some(ki.clone());
|
||||
}
|
||||
CrlExtensionParsed::CrlNumber { number, critical } => {
|
||||
let oid = OID_CRL_NUMBER.to_string();
|
||||
if seen.iter().any(|s| s == &oid) {
|
||||
return Err(CrlProfileError::DuplicateExtension(oid));
|
||||
}
|
||||
seen.push(oid.clone());
|
||||
|
||||
if *critical {
|
||||
return Err(CrlProfileError::CrlNumberCritical);
|
||||
}
|
||||
if number.bits() > 159 {
|
||||
return Err(CrlProfileError::CrlNumberOutOfRange);
|
||||
}
|
||||
crl_number = Some(BigUnsigned::from_biguint(number));
|
||||
}
|
||||
CrlExtensionParsed::Other { oid, .. } => {
|
||||
return Err(CrlProfileError::UnsupportedExtension(oid.clone()));
|
||||
}
|
||||
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CrlExtensions {
|
||||
authority_key_identifier: authority_key_identifier.unwrap(),
|
||||
crl_number: crl_number.unwrap(),
|
||||
authority_key_identifier: authority_key_identifier.ok_or(CrlProfileError::AkiMissing)?,
|
||||
crl_number: crl_number.ok_or(CrlProfileError::CrlNumberMissing)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_aki(ext: &X509Extension<'_>) -> Result<Vec<u8>, CrlDecodeError> {
|
||||
let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
|
||||
return Err(CrlDecodeError::Parse("AKI extension parse failed".into()));
|
||||
};
|
||||
validate_aki_profile(aki)?;
|
||||
Ok(aki
|
||||
.key_identifier
|
||||
.as_ref()
|
||||
.ok_or(CrlDecodeError::AkiMissingKeyIdentifier)?
|
||||
.0
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
fn validate_aki_profile(aki: &AuthorityKeyIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
||||
if aki.key_identifier.is_none() {
|
||||
return Err(CrlDecodeError::AkiMissingKeyIdentifier);
|
||||
}
|
||||
if aki.authority_cert_issuer.is_some() || aki.authority_cert_serial.is_some() {
|
||||
return Err(CrlDecodeError::AkiHasOtherFields);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_crl_number(ext: &X509Extension<'_>) -> Result<der_parser::num_bigint::BigUint, CrlDecodeError> {
|
||||
match ext.parsed_extension() {
|
||||
ParsedExtension::CRLNumber(n) => Ok(n.clone()),
|
||||
ParsedExtension::ParseError { error } => Err(CrlDecodeError::Parse(error.to_string())),
|
||||
_ => Err(CrlDecodeError::Parse("CRLNumber extension parse failed".into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
|
||||
cert.extensions()
|
||||
.iter()
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
|
||||
use crate::data_model::rc::ResourceCertificate;
|
||||
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||
use crate::data_model::signed_object::{
|
||||
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
|
||||
};
|
||||
use der_parser::ber::BerObjectContent;
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use der_parser::der::{DerObject, Tag, parse_der};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@ -13,6 +15,13 @@ pub struct ManifestObject {
|
||||
pub manifest: ManifestEContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ManifestObjectParsed {
|
||||
pub signed_object: RpkiSignedObjectParsed,
|
||||
pub econtent_type: String,
|
||||
pub manifest: Option<ManifestEContentParsed>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ManifestEContent {
|
||||
pub version: u32,
|
||||
@ -23,6 +32,11 @@ pub struct ManifestEContent {
|
||||
pub files: Vec<FileAndHash>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ManifestEContentParsed {
|
||||
der: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FileAndHash {
|
||||
pub file_name: String,
|
||||
@ -30,26 +44,38 @@ pub struct FileAndHash {
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestDecodeError {
|
||||
#[error("signed object decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4; RFC 9286 §4)")]
|
||||
SignedObject(#[from] SignedObjectDecodeError),
|
||||
|
||||
pub enum ManifestParseError {
|
||||
#[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObject(#[from] SignedObjectParseError),
|
||||
#[error("DER parse error: {0} (RFC 9286 §4.2; DER)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after DER object: {0} bytes (RFC 9286 §4.2; DER)")]
|
||||
TrailingBytes(usize),
|
||||
}
|
||||
|
||||
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))")]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestProfileError {
|
||||
#[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObject(#[from] SignedObjectValidateError),
|
||||
|
||||
#[error(
|
||||
"eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))"
|
||||
)]
|
||||
InvalidEContentType(String),
|
||||
|
||||
#[error("manifest profile decode error: {0} (RFC 9286 §4.2; DER)")]
|
||||
ProfileDecode(String),
|
||||
|
||||
#[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0} (RFC 9286 §4.2)")]
|
||||
InvalidManifestSequenceLen(usize),
|
||||
|
||||
#[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")]
|
||||
InvalidManifestVersion(u64),
|
||||
|
||||
#[error("Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)")]
|
||||
#[error(
|
||||
"Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)"
|
||||
)]
|
||||
InvalidManifestNumber,
|
||||
|
||||
#[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")]
|
||||
@ -64,7 +90,9 @@ pub enum ManifestDecodeError {
|
||||
#[error("Manifest.nextUpdate must be later than thisUpdate (RFC 9286 §4.2.1)")]
|
||||
NextUpdateNotLater,
|
||||
|
||||
#[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)")]
|
||||
#[error(
|
||||
"Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)"
|
||||
)]
|
||||
InvalidFileHashAlg(String),
|
||||
|
||||
#[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")]
|
||||
@ -79,44 +107,97 @@ pub enum ManifestDecodeError {
|
||||
#[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")]
|
||||
InvalidHashType,
|
||||
|
||||
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)")]
|
||||
#[error(
|
||||
"fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)"
|
||||
)]
|
||||
HashNotOctetAligned,
|
||||
|
||||
#[error("fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)")]
|
||||
#[error(
|
||||
"fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)"
|
||||
)]
|
||||
InvalidHashLength(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] ManifestParseError),
|
||||
|
||||
#[error("{0}")]
|
||||
Validate(#[from] ManifestProfileError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestValidateError {
|
||||
#[error("Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)")]
|
||||
#[error(
|
||||
"Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)"
|
||||
)]
|
||||
EeResourcesMissing,
|
||||
|
||||
#[error("Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)")]
|
||||
#[error(
|
||||
"Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)"
|
||||
)]
|
||||
EeIpResourcesNotInherit,
|
||||
|
||||
#[error("Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)")]
|
||||
#[error(
|
||||
"Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)"
|
||||
)]
|
||||
EeAsResourcesNotInherit,
|
||||
|
||||
#[error("Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)")]
|
||||
#[error(
|
||||
"Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)"
|
||||
)]
|
||||
EeAsResourcesRdiPresent,
|
||||
}
|
||||
|
||||
impl ManifestObject {
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||
Self::from_signed_object(signed_object)
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<ManifestObjectParsed, ManifestParseError> {
|
||||
let signed_object = RpkiSignedObject::parse_der(der)?;
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
let manifest = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent
|
||||
.as_deref()
|
||||
.map(ManifestEContent::parse_der)
|
||||
.transpose()?;
|
||||
Ok(ManifestObjectParsed {
|
||||
signed_object,
|
||||
econtent_type,
|
||||
manifest,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, ManifestDecodeError> {
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `ManifestObject` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `ManifestObjectParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), ManifestProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
pub fn from_signed_object(
|
||||
signed_object: RpkiSignedObject,
|
||||
) -> Result<Self, ManifestDecodeError> {
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_RPKI_MANIFEST {
|
||||
return Err(ManifestDecodeError::InvalidEContentType(econtent_type));
|
||||
return Err(ManifestProfileError::InvalidEContentType(econtent_type).into());
|
||||
}
|
||||
let manifest = ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
let manifest =
|
||||
ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
Ok(Self {
|
||||
signed_object,
|
||||
econtent_type: OID_CT_RPKI_MANIFEST.to_string(),
|
||||
@ -164,18 +245,62 @@ impl ManifestObject {
|
||||
}
|
||||
|
||||
impl ManifestEContent {
|
||||
/// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2.
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||
let (rem, obj) = parse_der(der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<ManifestEContentParsed, ManifestParseError> {
|
||||
let (rem, _obj) = parse_der(der).map_err(|e| ManifestParseError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(ManifestDecodeError::TrailingBytes(rem.len()));
|
||||
return Err(ManifestParseError::TrailingBytes(rem.len()));
|
||||
}
|
||||
Ok(ManifestEContentParsed { der: der.to_vec() })
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `ManifestEContent` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `ManifestEContentParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), ManifestProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2 (`parse + validate`).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManifestObjectParsed {
|
||||
pub fn validate_profile(self) -> Result<ManifestObject, ManifestProfileError> {
|
||||
let signed_object = self.signed_object.validate_profile()?;
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_RPKI_MANIFEST {
|
||||
return Err(ManifestProfileError::InvalidEContentType(econtent_type));
|
||||
}
|
||||
let manifest = self
|
||||
.manifest
|
||||
.ok_or_else(|| ManifestProfileError::ProfileDecode("Manifest.eContent missing".into()))?
|
||||
.validate_profile()?;
|
||||
Ok(ManifestObject {
|
||||
signed_object,
|
||||
econtent_type: OID_CT_RPKI_MANIFEST.to_string(),
|
||||
manifest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ManifestEContentParsed {
|
||||
pub fn validate_profile(self) -> Result<ManifestEContent, ManifestProfileError> {
|
||||
let (_rem, obj) =
|
||||
parse_der(&self.der).map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.len() != 5 && seq.len() != 6 {
|
||||
return Err(ManifestDecodeError::InvalidManifestSequenceLen(seq.len()));
|
||||
return Err(ManifestProfileError::InvalidManifestSequenceLen(seq.len()));
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
@ -183,25 +308,25 @@ impl ManifestEContent {
|
||||
if seq.len() == 6 {
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(ManifestDecodeError::Parse(
|
||||
return Err(ManifestProfileError::ProfileDecode(
|
||||
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
|
||||
));
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
let (rem, inner) = parse_der(inner_der)
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(ManifestDecodeError::Parse(
|
||||
return Err(ManifestProfileError::ProfileDecode(
|
||||
"trailing bytes inside Manifest.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
if v != 0 {
|
||||
return Err(ManifestDecodeError::InvalidManifestVersion(v));
|
||||
return Err(ManifestProfileError::InvalidManifestVersion(v));
|
||||
}
|
||||
version = 0;
|
||||
idx = 1;
|
||||
@ -211,24 +336,24 @@ impl ManifestEContent {
|
||||
idx += 1;
|
||||
|
||||
let this_update =
|
||||
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidThisUpdate)?;
|
||||
parse_generalized_time(&seq[idx], ManifestProfileError::InvalidThisUpdate)?;
|
||||
idx += 1;
|
||||
let next_update =
|
||||
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidNextUpdate)?;
|
||||
parse_generalized_time(&seq[idx], ManifestProfileError::InvalidNextUpdate)?;
|
||||
idx += 1;
|
||||
if next_update <= this_update {
|
||||
return Err(ManifestDecodeError::NextUpdateNotLater);
|
||||
return Err(ManifestProfileError::NextUpdateNotLater);
|
||||
}
|
||||
|
||||
let file_hash_alg = oid_to_string(&seq[idx])?;
|
||||
idx += 1;
|
||||
if file_hash_alg != OID_SHA256 {
|
||||
return Err(ManifestDecodeError::InvalidFileHashAlg(file_hash_alg));
|
||||
return Err(ManifestProfileError::InvalidFileHashAlg(file_hash_alg));
|
||||
}
|
||||
|
||||
let files = parse_file_list_sha256(&seq[idx])?;
|
||||
|
||||
Ok(Self {
|
||||
Ok(ManifestEContent {
|
||||
version,
|
||||
manifest_number,
|
||||
this_update,
|
||||
@ -239,39 +364,39 @@ impl ManifestEContent {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestDecodeError> {
|
||||
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestProfileError> {
|
||||
let n = obj
|
||||
.as_biguint()
|
||||
.map_err(|_e| ManifestDecodeError::InvalidManifestNumber)?;
|
||||
.map_err(|_e| ManifestProfileError::InvalidManifestNumber)?;
|
||||
let out = BigUnsigned::from_biguint(&n);
|
||||
if out.bytes_be.len() > 20 {
|
||||
return Err(ManifestDecodeError::ManifestNumberTooLong);
|
||||
return Err(ManifestProfileError::ManifestNumberTooLong);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_generalized_time(
|
||||
obj: &DerObject<'_>,
|
||||
err: ManifestDecodeError,
|
||||
) -> Result<OffsetDateTime, ManifestDecodeError> {
|
||||
err: ManifestProfileError,
|
||||
) -> Result<OffsetDateTime, ManifestProfileError> {
|
||||
match &obj.content {
|
||||
BerObjectContent::GeneralizedTime(dt) => dt
|
||||
.to_datetime()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string())),
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string())),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestDecodeError> {
|
||||
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|_e| ManifestDecodeError::InvalidFileList)?;
|
||||
.map_err(|_e| ManifestProfileError::InvalidFileList)?;
|
||||
let mut out = Vec::with_capacity(seq.len());
|
||||
for entry in seq {
|
||||
let (file_name, hash_bytes) = parse_file_and_hash(entry)?;
|
||||
validate_file_name(&file_name)?;
|
||||
if hash_bytes.len() != 32 {
|
||||
return Err(ManifestDecodeError::InvalidHashLength(hash_bytes.len()));
|
||||
return Err(ManifestProfileError::InvalidHashLength(hash_bytes.len()));
|
||||
}
|
||||
out.push(FileAndHash {
|
||||
file_name,
|
||||
@ -281,58 +406,58 @@ fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, Manif
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestDecodeError> {
|
||||
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.len() != 2 {
|
||||
return Err(ManifestDecodeError::InvalidFileAndHash);
|
||||
return Err(ManifestProfileError::InvalidFileAndHash);
|
||||
}
|
||||
let file_name = seq[0]
|
||||
.as_str()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?
|
||||
.to_string();
|
||||
let (unused_bits, bits) = match &seq[1].content {
|
||||
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||
_ => return Err(ManifestDecodeError::InvalidHashType),
|
||||
_ => return Err(ManifestProfileError::InvalidHashType),
|
||||
};
|
||||
if unused_bits != 0 {
|
||||
return Err(ManifestDecodeError::HashNotOctetAligned);
|
||||
return Err(ManifestProfileError::HashNotOctetAligned);
|
||||
}
|
||||
Ok((file_name, bits))
|
||||
}
|
||||
|
||||
fn validate_file_name(name: &str) -> Result<(), ManifestDecodeError> {
|
||||
fn validate_file_name(name: &str) -> Result<(), ManifestProfileError> {
|
||||
// RFC 9286 §4.2.2:
|
||||
// 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension.
|
||||
let Some((base, ext)) = name.rsplit_once('.') else {
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
||||
};
|
||||
if base.is_empty() || ext.len() != 3 {
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
||||
}
|
||||
if !base
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
||||
{
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
||||
}
|
||||
if !ext.bytes().all(|b| b.is_ascii_alphabetic()) {
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
||||
}
|
||||
let ext_lower = ext.to_ascii_lowercase();
|
||||
if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS
|
||||
.iter()
|
||||
.any(|&e| e == ext_lower)
|
||||
{
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestDecodeError> {
|
||||
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestProfileError> {
|
||||
let oid = obj
|
||||
.as_oid()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
||||
Ok(oid.to_id_string())
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
pub mod aspa;
|
||||
pub mod common;
|
||||
pub mod crl;
|
||||
pub mod rc;
|
||||
pub mod oid;
|
||||
pub mod signed_object;
|
||||
pub mod manifest;
|
||||
pub mod oid;
|
||||
pub mod rc;
|
||||
pub mod roa;
|
||||
pub mod aspa;
|
||||
pub mod tal;
|
||||
pub mod signed_object;
|
||||
pub mod ta;
|
||||
pub mod tal;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use der_parser::ber::{BerObjectContent, Class};
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use der_parser::der::{DerObject, Tag, parse_der};
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use x509_parser::asn1_rs::{Class as Asn1Class, Tag as Asn1Tag};
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version};
|
||||
|
||||
use crate::data_model::common::algorithm_params_absent_or_null;
|
||||
use crate::data_model::oid::{
|
||||
OID_AD_SIGNED_OBJECT, OID_AUTONOMOUS_SYS_IDS, OID_CP_IPADDR_ASNUMBER, OID_IP_ADDR_BLOCKS,
|
||||
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
|
||||
@ -59,6 +59,62 @@ pub struct RcExtensions {
|
||||
pub as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResourceCertificateParsed {
|
||||
pub raw_der: Vec<u8>,
|
||||
pub version: X509Version,
|
||||
pub serial_number: BigUint,
|
||||
pub signature_algorithm: AlgorithmIdentifierValue,
|
||||
pub tbs_signature_algorithm: AlgorithmIdentifierValue,
|
||||
pub issuer_dn: String,
|
||||
pub subject_dn: String,
|
||||
pub validity_not_before: OffsetDateTime,
|
||||
pub validity_not_after: OffsetDateTime,
|
||||
/// DER encoding of SubjectPublicKeyInfo.
|
||||
pub subject_public_key_info: Vec<u8>,
|
||||
pub extensions: RcExtensionsParsed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AlgorithmIdentifierValue {
|
||||
pub oid: String,
|
||||
pub parameters: Option<AlgorithmParametersValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AlgorithmParametersValue {
|
||||
pub class: Asn1Class,
|
||||
pub tag: Asn1Tag,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl AlgorithmIdentifierValue {
|
||||
pub fn params_absent_or_null(&self) -> bool {
|
||||
match &self.parameters {
|
||||
None => true,
|
||||
Some(p) if p.class == Asn1Class::Universal && p.tag == Asn1Tag::Null => true,
|
||||
Some(_p) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RcExtensionsParsed {
|
||||
pub basic_constraints_ca: Vec<bool>,
|
||||
pub subject_key_identifier: Vec<(Vec<u8>, bool)>,
|
||||
pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>,
|
||||
pub certificate_policies: Vec<(Vec<String>, bool)>,
|
||||
pub ip_resources: Vec<(IpResourceSet, bool)>,
|
||||
pub as_resources: Vec<(AsResourceSet, bool)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SubjectInfoAccessParsed {
|
||||
pub access_descriptions: Vec<AccessDescription>,
|
||||
pub signed_object_uris: Vec<Url>,
|
||||
pub signed_object_access_location_not_uri: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SubjectInfoAccess {
|
||||
Ca(SubjectInfoAccessCa),
|
||||
@ -204,10 +260,7 @@ impl AsResourceSet {
|
||||
}
|
||||
|
||||
pub fn has_any_range(&self) -> bool {
|
||||
self.asnum
|
||||
.as_ref()
|
||||
.map(|c| c.has_range())
|
||||
.unwrap_or(false)
|
||||
self.asnum.as_ref().map(|c| c.has_range()).unwrap_or(false)
|
||||
|| self.rdi.as_ref().map(|c| c.has_range()).unwrap_or(false)
|
||||
}
|
||||
|
||||
@ -243,7 +296,9 @@ impl AsIdentifierChoice {
|
||||
pub fn has_range(&self) -> bool {
|
||||
match self {
|
||||
AsIdentifierChoice::Inherit => false,
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. })),
|
||||
AsIdentifierChoice::AsIdsOrRanges(items) => {
|
||||
items.iter().any(|i| matches!(i, AsIdOrRange::Range { .. }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -255,20 +310,31 @@ pub enum AsIdOrRange {
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResourceCertificateError {
|
||||
pub enum ResourceCertificateParseError {
|
||||
#[error("X.509 parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
||||
TrailingBytes(usize),
|
||||
|
||||
#[error("invalid RFC 3779 IP resources extension encoding (RFC 6487 §4.8.10; RFC 3779 §2.2)")]
|
||||
InvalidIpResourcesEncoding,
|
||||
|
||||
#[error("invalid RFC 3779 AS resources extension encoding (RFC 6487 §4.8.11; RFC 3779 §3.2)")]
|
||||
InvalidAsResourcesEncoding,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResourceCertificateProfileError {
|
||||
#[error("certificate version must be v3 (RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
InvalidVersion,
|
||||
|
||||
#[error("signatureAlgorithm does not match tbsCertificate.signature (RFC 5280 §4.1)")]
|
||||
SignatureAlgorithmMismatch,
|
||||
|
||||
#[error("unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)")]
|
||||
#[error(
|
||||
"unsupported signature algorithm (expected sha256WithRSAEncryption {OID_SHA256_WITH_RSA_ENCRYPTION}) (RFC 7935 §2; RFC 6487 §4)"
|
||||
)]
|
||||
UnsupportedSignatureAlgorithm,
|
||||
|
||||
#[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")]
|
||||
@ -286,45 +352,46 @@ pub enum ResourceCertificateError {
|
||||
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
|
||||
CertificatePoliciesCriticality,
|
||||
|
||||
#[error("certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)")]
|
||||
#[error(
|
||||
"certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)"
|
||||
)]
|
||||
InvalidCertificatePolicy(String),
|
||||
|
||||
#[error("SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)")]
|
||||
#[error(
|
||||
"SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)"
|
||||
)]
|
||||
SignedObjectSiaNotUri,
|
||||
|
||||
#[error("SIA id-ad-signedObject must include at least one rsync:// URI (RFC 6487 §4.8.8.2)")]
|
||||
SignedObjectSiaNoRsync,
|
||||
|
||||
#[error("invalid RFC 3779 IP resources extension (RFC 6487 §4.8.10; RFC 3779 §2.2)")]
|
||||
InvalidIpResources,
|
||||
#[error("ipAddrBlocks criticality must be critical when present (RFC 6487 §4.8.10)")]
|
||||
IpResourcesCriticality,
|
||||
|
||||
#[error("invalid RFC 3779 AS resources extension (RFC 6487 §4.8.11; RFC 3779 §3.2)")]
|
||||
InvalidAsResources,
|
||||
#[error("autonomousSysIds criticality must be critical when present (RFC 6487 §4.8.11)")]
|
||||
AsResourcesCriticality,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResourceCertificateDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] ResourceCertificateParseError),
|
||||
|
||||
#[error("{0}")]
|
||||
Validate(#[from] ResourceCertificateProfileError),
|
||||
}
|
||||
|
||||
pub type ResourceCertificateError = ResourceCertificateDecodeError;
|
||||
|
||||
impl ResourceCertificate {
|
||||
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
|
||||
let (rem, cert) =
|
||||
X509Certificate::from_der(der).map_err(|e| ResourceCertificateError::Parse(e.to_string()))?;
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(
|
||||
der: &[u8],
|
||||
) -> Result<ResourceCertificateParsed, ResourceCertificateParseError> {
|
||||
let (rem, cert) = X509Certificate::from_der(der)
|
||||
.map_err(|e| ResourceCertificateParseError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(ResourceCertificateError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let version = match cert.version() {
|
||||
X509Version::V3 => 2u32,
|
||||
_ => return Err(ResourceCertificateError::InvalidVersion),
|
||||
};
|
||||
|
||||
let outer = &cert.signature_algorithm;
|
||||
let inner = &cert.tbs_certificate.signature;
|
||||
if outer.algorithm != inner.algorithm || outer.parameters != inner.parameters {
|
||||
return Err(ResourceCertificateError::SignatureAlgorithmMismatch);
|
||||
}
|
||||
if outer.algorithm.to_id_string() != OID_SHA256_WITH_RSA_ENCRYPTION {
|
||||
return Err(ResourceCertificateError::UnsupportedSignatureAlgorithm);
|
||||
}
|
||||
if !algorithm_params_absent_or_null(outer) {
|
||||
return Err(ResourceCertificateError::InvalidSignatureAlgorithmParameters);
|
||||
return Err(ResourceCertificateParseError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let validity_not_before = cert.validity().not_before.to_datetime();
|
||||
@ -332,7 +399,62 @@ impl ResourceCertificate {
|
||||
|
||||
let subject_public_key_info = cert.tbs_certificate.subject_pki.raw.to_vec();
|
||||
|
||||
let extensions = parse_extensions(cert.extensions())?;
|
||||
let signature_algorithm = algorithm_identifier_value(&cert.signature_algorithm);
|
||||
let tbs_signature_algorithm = algorithm_identifier_value(&cert.tbs_certificate.signature);
|
||||
let extensions = parse_extensions_parse(cert.extensions())?;
|
||||
|
||||
Ok(ResourceCertificateParsed {
|
||||
raw_der: der.to_vec(),
|
||||
version: cert.version(),
|
||||
serial_number: cert.tbs_certificate.serial.clone(),
|
||||
signature_algorithm,
|
||||
tbs_signature_algorithm,
|
||||
issuer_dn: cert.issuer().to_string(),
|
||||
subject_dn: cert.subject().to_string(),
|
||||
validity_not_before,
|
||||
validity_not_after,
|
||||
subject_public_key_info,
|
||||
extensions,
|
||||
})
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `ResourceCertificate` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `ResourceCertificateParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), ResourceCertificateProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode a resource certificate (`parse + validate`).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
/// Backwards-compatible helper (historical name).
|
||||
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
|
||||
Self::decode_der(der)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceCertificateParsed {
|
||||
pub fn validate_profile(self) -> Result<ResourceCertificate, ResourceCertificateProfileError> {
|
||||
let version = match self.version {
|
||||
X509Version::V3 => 2u32,
|
||||
_ => return Err(ResourceCertificateProfileError::InvalidVersion),
|
||||
};
|
||||
|
||||
if self.signature_algorithm != self.tbs_signature_algorithm {
|
||||
return Err(ResourceCertificateProfileError::SignatureAlgorithmMismatch);
|
||||
}
|
||||
if self.signature_algorithm.oid != OID_SHA256_WITH_RSA_ENCRYPTION {
|
||||
return Err(ResourceCertificateProfileError::UnsupportedSignatureAlgorithm);
|
||||
}
|
||||
if !self.signature_algorithm.params_absent_or_null() {
|
||||
return Err(ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters);
|
||||
}
|
||||
|
||||
let extensions = self.extensions.validate_profile()?;
|
||||
let kind = if extensions.basic_constraints_ca {
|
||||
ResourceCertKind::Ca
|
||||
} else {
|
||||
@ -340,16 +462,16 @@ impl ResourceCertificate {
|
||||
};
|
||||
|
||||
Ok(ResourceCertificate {
|
||||
raw_der: der.to_vec(),
|
||||
raw_der: self.raw_der,
|
||||
tbs: RpkixTbsCertificate {
|
||||
version,
|
||||
serial_number: cert.tbs_certificate.serial.clone(),
|
||||
signature_algorithm: outer.algorithm.to_id_string(),
|
||||
issuer_dn: cert.issuer().to_string(),
|
||||
subject_dn: cert.subject().to_string(),
|
||||
validity_not_before,
|
||||
validity_not_after,
|
||||
subject_public_key_info,
|
||||
serial_number: self.serial_number,
|
||||
signature_algorithm: self.signature_algorithm.oid,
|
||||
issuer_dn: self.issuer_dn,
|
||||
subject_dn: self.subject_dn,
|
||||
validity_not_before: self.validity_not_before,
|
||||
validity_not_after: self.validity_not_after,
|
||||
subject_public_key_info: self.subject_public_key_info,
|
||||
extensions,
|
||||
},
|
||||
kind,
|
||||
@ -357,114 +479,220 @@ impl ResourceCertificate {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_extensions(exts: &[X509Extension<'_>]) -> Result<RcExtensions, ResourceCertificateError> {
|
||||
let mut basic_constraints_ca: Option<bool> = None;
|
||||
let mut ski: Option<Vec<u8>> = None;
|
||||
let mut sia: Option<SubjectInfoAccess> = None;
|
||||
let mut cert_policies_oid: Option<String> = None;
|
||||
impl RcExtensionsParsed {
|
||||
pub fn validate_profile(self) -> Result<RcExtensions, ResourceCertificateProfileError> {
|
||||
if self.basic_constraints_ca.len() > 1 {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"basicConstraints",
|
||||
));
|
||||
}
|
||||
let basic_constraints_ca = self.basic_constraints_ca.first().copied().unwrap_or(false);
|
||||
|
||||
let mut ip_resources: Option<IpResourceSet> = None;
|
||||
let mut as_resources: Option<AsResourceSet> = None;
|
||||
let subject_key_identifier = match self.subject_key_identifier.as_slice() {
|
||||
[] => None,
|
||||
[(ski, critical)] => {
|
||||
if *critical {
|
||||
return Err(ResourceCertificateProfileError::SkiCriticality);
|
||||
}
|
||||
Some(ski.clone())
|
||||
}
|
||||
_ => {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"subjectKeyIdentifier",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let subject_info_access = match self.subject_info_access.as_slice() {
|
||||
[] => None,
|
||||
[(sia, critical)] => {
|
||||
if *critical {
|
||||
return Err(ResourceCertificateProfileError::SiaCriticality);
|
||||
}
|
||||
if sia.signed_object_access_location_not_uri {
|
||||
return Err(ResourceCertificateProfileError::SignedObjectSiaNotUri);
|
||||
}
|
||||
if !sia.signed_object_uris.is_empty()
|
||||
&& !sia.signed_object_uris.iter().any(|u| u.scheme() == "rsync")
|
||||
{
|
||||
return Err(ResourceCertificateProfileError::SignedObjectSiaNoRsync);
|
||||
}
|
||||
if sia.signed_object_uris.is_empty() {
|
||||
Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
|
||||
access_descriptions: sia.access_descriptions.clone(),
|
||||
}))
|
||||
} else {
|
||||
Some(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
|
||||
signed_object_uris: sia.signed_object_uris.clone(),
|
||||
access_descriptions: sia.access_descriptions.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"subjectInfoAccess",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let certificate_policies_oid = match self.certificate_policies.as_slice() {
|
||||
[] => None,
|
||||
[(oids, critical)] => {
|
||||
if !*critical {
|
||||
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
|
||||
}
|
||||
if oids.len() != 1 {
|
||||
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
||||
"expected exactly one policy".into(),
|
||||
));
|
||||
}
|
||||
let policy_oid = oids[0].clone();
|
||||
if policy_oid != OID_CP_IPADDR_ASNUMBER {
|
||||
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
|
||||
policy_oid,
|
||||
));
|
||||
}
|
||||
Some(OID_CP_IPADDR_ASNUMBER.to_string())
|
||||
}
|
||||
_ => {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"certificatePolicies",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let ip_resources = match self.ip_resources.as_slice() {
|
||||
[] => None,
|
||||
[(ip, critical)] => {
|
||||
if !*critical {
|
||||
return Err(ResourceCertificateProfileError::IpResourcesCriticality);
|
||||
}
|
||||
Some(ip.clone())
|
||||
}
|
||||
_ => {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"ipAddrBlocks",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let as_resources = match self.as_resources.as_slice() {
|
||||
[] => None,
|
||||
[(asn, critical)] => {
|
||||
if !*critical {
|
||||
return Err(ResourceCertificateProfileError::AsResourcesCriticality);
|
||||
}
|
||||
Some(asn.clone())
|
||||
}
|
||||
_ => {
|
||||
return Err(ResourceCertificateProfileError::DuplicateExtension(
|
||||
"autonomousSysIds",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RcExtensions {
|
||||
basic_constraints_ca,
|
||||
subject_key_identifier,
|
||||
subject_info_access,
|
||||
certificate_policies_oid,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn algorithm_identifier_value(
|
||||
ai: &x509_parser::x509::AlgorithmIdentifier<'_>,
|
||||
) -> AlgorithmIdentifierValue {
|
||||
let parameters = ai.parameters.as_ref().map(|p| AlgorithmParametersValue {
|
||||
class: p.class(),
|
||||
tag: p.tag(),
|
||||
data: p.as_bytes().to_vec(),
|
||||
});
|
||||
AlgorithmIdentifierValue {
|
||||
oid: ai.algorithm.to_id_string(),
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_extensions_parse(
|
||||
exts: &[X509Extension<'_>],
|
||||
) -> Result<RcExtensionsParsed, ResourceCertificateParseError> {
|
||||
let mut basic_constraints_ca: Vec<bool> = Vec::new();
|
||||
let mut ski: Vec<(Vec<u8>, bool)> = Vec::new();
|
||||
let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new();
|
||||
let mut cert_policies: Vec<(Vec<String>, bool)> = Vec::new();
|
||||
|
||||
let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new();
|
||||
let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new();
|
||||
|
||||
for ext in exts {
|
||||
let oid = ext.oid.to_id_string();
|
||||
match oid.as_str() {
|
||||
crate::data_model::oid::OID_BASIC_CONSTRAINTS => {
|
||||
if basic_constraints_ca.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("basicConstraints"));
|
||||
}
|
||||
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("basicConstraints parse failed".into()));
|
||||
return Err(ResourceCertificateParseError::Parse(
|
||||
"basicConstraints parse failed".into(),
|
||||
));
|
||||
};
|
||||
basic_constraints_ca = Some(bc.ca);
|
||||
basic_constraints_ca.push(bc.ca);
|
||||
}
|
||||
OID_SUBJECT_KEY_IDENTIFIER => {
|
||||
if ski.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("subjectKeyIdentifier"));
|
||||
}
|
||||
if ext.critical {
|
||||
return Err(ResourceCertificateError::SkiCriticality);
|
||||
}
|
||||
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("subjectKeyIdentifier parse failed".into()));
|
||||
return Err(ResourceCertificateParseError::Parse(
|
||||
"subjectKeyIdentifier parse failed".into(),
|
||||
));
|
||||
};
|
||||
ski = Some(s.0.to_vec());
|
||||
ski.push((s.0.to_vec(), ext.critical));
|
||||
}
|
||||
OID_SUBJECT_INFO_ACCESS => {
|
||||
if sia.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("subjectInfoAccess"));
|
||||
}
|
||||
if ext.critical {
|
||||
return Err(ResourceCertificateError::SiaCriticality);
|
||||
}
|
||||
let ParsedExtension::SubjectInfoAccess(s) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("subjectInfoAccess parse failed".into()));
|
||||
return Err(ResourceCertificateParseError::Parse(
|
||||
"subjectInfoAccess parse failed".into(),
|
||||
));
|
||||
};
|
||||
sia = Some(parse_sia(s.accessdescs.as_slice())?);
|
||||
sia.push((parse_sia_parse(s.accessdescs.as_slice())?, ext.critical));
|
||||
}
|
||||
crate::data_model::oid::OID_CERTIFICATE_POLICIES => {
|
||||
if cert_policies_oid.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("certificatePolicies"));
|
||||
}
|
||||
if !ext.critical {
|
||||
return Err(ResourceCertificateError::CertificatePoliciesCriticality);
|
||||
}
|
||||
let ParsedExtension::CertificatePolicies(cp) = ext.parsed_extension() else {
|
||||
return Err(ResourceCertificateError::Parse("certificatePolicies parse failed".into()));
|
||||
};
|
||||
if cp.len() != 1 {
|
||||
return Err(ResourceCertificateError::InvalidCertificatePolicy(
|
||||
"expected exactly one policy".into(),
|
||||
return Err(ResourceCertificateParseError::Parse(
|
||||
"certificatePolicies parse failed".into(),
|
||||
));
|
||||
}
|
||||
let policy_oid = cp[0].policy_id.to_id_string();
|
||||
if policy_oid != OID_CP_IPADDR_ASNUMBER {
|
||||
return Err(ResourceCertificateError::InvalidCertificatePolicy(policy_oid));
|
||||
}
|
||||
cert_policies_oid = Some(OID_CP_IPADDR_ASNUMBER.to_string());
|
||||
};
|
||||
let oids: Vec<String> = cp.iter().map(|p| p.policy_id.to_id_string()).collect();
|
||||
cert_policies.push((oids, ext.critical));
|
||||
}
|
||||
OID_IP_ADDR_BLOCKS => {
|
||||
if ip_resources.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("ipAddrBlocks"));
|
||||
}
|
||||
// Must be critical per RPKI profile; we only enforce when present.
|
||||
if !ext.critical {
|
||||
return Err(ResourceCertificateError::InvalidIpResources);
|
||||
}
|
||||
let parsed = IpResourceSet::decode_extn_value(ext.value)
|
||||
.map_err(|_e| ResourceCertificateError::InvalidIpResources)?;
|
||||
ip_resources = Some(parsed);
|
||||
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
|
||||
ip_resources.push((parsed, ext.critical));
|
||||
}
|
||||
OID_AUTONOMOUS_SYS_IDS => {
|
||||
if as_resources.is_some() {
|
||||
return Err(ResourceCertificateError::DuplicateExtension("autonomousSysIds"));
|
||||
}
|
||||
if !ext.critical {
|
||||
return Err(ResourceCertificateError::InvalidAsResources);
|
||||
}
|
||||
let parsed = AsResourceSet::decode_extn_value(ext.value)
|
||||
.map_err(|_e| ResourceCertificateError::InvalidAsResources)?;
|
||||
as_resources = Some(parsed);
|
||||
.map_err(|_e| ResourceCertificateParseError::InvalidAsResourcesEncoding)?;
|
||||
as_resources.push((parsed, ext.critical));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let basic_constraints_ca = basic_constraints_ca.unwrap_or(false);
|
||||
|
||||
Ok(RcExtensions {
|
||||
Ok(RcExtensionsParsed {
|
||||
basic_constraints_ca,
|
||||
subject_key_identifier: ski,
|
||||
subject_info_access: sia,
|
||||
certificate_policies_oid: cert_policies_oid,
|
||||
certificate_policies: cert_policies,
|
||||
ip_resources,
|
||||
as_resources,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Result<SubjectInfoAccess, ResourceCertificateError> {
|
||||
fn parse_sia_parse(
|
||||
access: &[x509_parser::extensions::AccessDescription<'_>],
|
||||
) -> Result<SubjectInfoAccessParsed, ResourceCertificateParseError> {
|
||||
let mut all = Vec::with_capacity(access.len());
|
||||
let mut signed_object_uris: Vec<Url> = Vec::new();
|
||||
let mut signed_object_access_location_not_uri = false;
|
||||
|
||||
for ad in access {
|
||||
let access_method_oid = ad.access_method.to_id_string();
|
||||
@ -472,13 +700,13 @@ fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Resul
|
||||
x509_parser::extensions::GeneralName::URI(u) => u,
|
||||
_ => {
|
||||
if access_method_oid == OID_AD_SIGNED_OBJECT {
|
||||
return Err(ResourceCertificateError::SignedObjectSiaNotUri);
|
||||
signed_object_access_location_not_uri = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let url =
|
||||
Url::parse(uri).map_err(|_| ResourceCertificateError::Parse(format!("invalid URI: {uri}")))?;
|
||||
let url = Url::parse(uri)
|
||||
.map_err(|_| ResourceCertificateParseError::Parse(format!("invalid URI: {uri}")))?;
|
||||
if access_method_oid == OID_AD_SIGNED_OBJECT {
|
||||
signed_object_uris.push(url.clone());
|
||||
}
|
||||
@ -488,20 +716,11 @@ fn parse_sia(access: &[x509_parser::extensions::AccessDescription<'_>]) -> Resul
|
||||
});
|
||||
}
|
||||
|
||||
if signed_object_uris.is_empty() {
|
||||
return Ok(SubjectInfoAccess::Ca(SubjectInfoAccessCa {
|
||||
Ok(SubjectInfoAccessParsed {
|
||||
access_descriptions: all,
|
||||
}));
|
||||
}
|
||||
|
||||
if !signed_object_uris.iter().any(|u| u.scheme() == "rsync") {
|
||||
return Err(ResourceCertificateError::SignedObjectSiaNoRsync);
|
||||
}
|
||||
|
||||
Ok(SubjectInfoAccess::Ee(SubjectInfoAccessEe {
|
||||
signed_object_uris,
|
||||
access_descriptions: all,
|
||||
}))
|
||||
signed_object_access_location_not_uri,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
|
||||
@ -545,7 +764,9 @@ fn parse_ip_addr_blocks(ext_value: &[u8]) -> Result<IpResourceSet, ()> {
|
||||
|
||||
fn parse_ip_address_or_range(afi: Afi, obj: &DerObject<'_>) -> Result<IpAddressOrRange, ()> {
|
||||
match &obj.content {
|
||||
BerObjectContent::BitString(_, _) => Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?)),
|
||||
BerObjectContent::BitString(_, _) => {
|
||||
Ok(IpAddressOrRange::Prefix(parse_ip_prefix(afi, obj)?))
|
||||
}
|
||||
BerObjectContent::Sequence(_) => {
|
||||
let seq = obj.as_sequence().map_err(|_| ())?;
|
||||
if seq.len() != 2 {
|
||||
@ -595,7 +816,11 @@ fn parse_ip_prefix(afi: Afi, obj: &DerObject<'_>) -> Result<IpPrefix, ()> {
|
||||
/// fewer than `ub` bits. In that case, the missing bits are interpreted as 0s for the lower
|
||||
/// bound and 1s for the upper bound. This is essential to correctly interpret ranges that
|
||||
/// are expressed on non-octet boundaries.
|
||||
fn parse_ip_address_bound(afi: Afi, obj: &DerObject<'_>, fill_remaining_ones: bool) -> Result<Vec<u8>, ()> {
|
||||
fn parse_ip_address_bound(
|
||||
afi: Afi,
|
||||
obj: &DerObject<'_>,
|
||||
fill_remaining_ones: bool,
|
||||
) -> Result<Vec<u8>, ()> {
|
||||
let (unused_bits, bytes) = match &obj.content {
|
||||
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||
_ => return Err(()),
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
|
||||
use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate};
|
||||
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||
use crate::data_model::signed_object::{
|
||||
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
|
||||
};
|
||||
use der_parser::ber::{BerObjectContent, Class};
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use der_parser::der::{DerObject, Tag, parse_der};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaObject {
|
||||
@ -11,6 +13,13 @@ pub struct RoaObject {
|
||||
pub roa: RoaEContent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaObjectParsed {
|
||||
pub signed_object: RpkiSignedObjectParsed,
|
||||
pub econtent_type: String,
|
||||
pub roa: Option<RoaEContentParsed>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaEContent {
|
||||
pub version: u32,
|
||||
@ -18,19 +27,32 @@ pub struct RoaEContent {
|
||||
pub ip_addr_blocks: Vec<RoaIpAddressFamily>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RoaEContentParsed {
|
||||
der: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RoaDecodeError {
|
||||
#[error("SignedObject decode error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObjectDecode(#[from] SignedObjectDecodeError),
|
||||
|
||||
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
|
||||
InvalidEContentType(String),
|
||||
|
||||
pub enum RoaParseError {
|
||||
#[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObject(#[from] SignedObjectParseError),
|
||||
#[error("ROA parse error: {0} (RFC 9582 §4; DER)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("ROA trailing bytes: {0} bytes (RFC 9582 §4; DER)")]
|
||||
TrailingBytes(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RoaProfileError {
|
||||
#[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")]
|
||||
SignedObject(#[from] SignedObjectValidateError),
|
||||
|
||||
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0} (RFC 9582 §3)")]
|
||||
InvalidEContentType(String),
|
||||
|
||||
#[error("ROA profile decode error: {0} (RFC 9582 §4; DER)")]
|
||||
ProfileDecode(String),
|
||||
|
||||
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0} (RFC 9582 §4)")]
|
||||
InvalidAttestationSequenceLen(usize),
|
||||
@ -65,13 +87,19 @@ pub enum RoaDecodeError {
|
||||
#[error("ROAIPAddress.address must be a BIT STRING (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
|
||||
InvalidPrefixBitString,
|
||||
|
||||
#[error("ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)")]
|
||||
#[error(
|
||||
"ROAIPAddress.address has invalid unused bits encoding (RFC 9582 §4.3.2.1; RFC 3779 §2.2.3.8)"
|
||||
)]
|
||||
InvalidPrefixUnusedBits,
|
||||
|
||||
#[error("ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)")]
|
||||
#[error(
|
||||
"ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?} (RFC 9582 §4.3.2.1)"
|
||||
)]
|
||||
PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 },
|
||||
|
||||
#[error("ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)")]
|
||||
#[error(
|
||||
"ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len} (RFC 9582 §4.3.2.2)"
|
||||
)]
|
||||
InvalidMaxLength {
|
||||
afi: RoaAfi,
|
||||
prefix_len: u16,
|
||||
@ -79,6 +107,15 @@ pub enum RoaDecodeError {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RoaDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] RoaParseError),
|
||||
|
||||
#[error("{0}")]
|
||||
Validate(#[from] RoaProfileError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RoaValidateError {
|
||||
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
|
||||
@ -90,7 +127,9 @@ pub enum RoaValidateError {
|
||||
#[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")]
|
||||
EeIpResourcesInherit,
|
||||
|
||||
#[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)")]
|
||||
#[error(
|
||||
"ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len} (RFC 9582 §5; RFC 3779 §2.3)"
|
||||
)]
|
||||
PrefixNotInEeResources {
|
||||
afi: RoaAfi,
|
||||
addr: Vec<u8>,
|
||||
@ -99,9 +138,38 @@ pub enum RoaValidateError {
|
||||
}
|
||||
|
||||
impl RoaObject {
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<RoaObjectParsed, RoaParseError> {
|
||||
let signed_object = RpkiSignedObject::parse_der(der)?;
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
let roa = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent
|
||||
.as_deref()
|
||||
.map(RoaEContent::parse_der)
|
||||
.transpose()?;
|
||||
Ok(RoaObjectParsed {
|
||||
signed_object,
|
||||
econtent_type,
|
||||
roa,
|
||||
})
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `RoaObject` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `RoaObjectParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), RoaProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||
Self::from_signed_object(signed_object)
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
|
||||
@ -111,7 +179,7 @@ impl RoaObject {
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
|
||||
return Err(RoaDecodeError::InvalidEContentType(econtent_type));
|
||||
return Err(RoaProfileError::InvalidEContentType(econtent_type).into());
|
||||
}
|
||||
|
||||
let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
|
||||
@ -166,67 +234,26 @@ pub struct IpPrefix {
|
||||
}
|
||||
|
||||
impl RoaEContent {
|
||||
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4.
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<RoaEContentParsed, RoaParseError> {
|
||||
let (rem, _obj) = parse_der(der).map_err(|e| RoaParseError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(RoaParseError::TrailingBytes(rem.len()));
|
||||
}
|
||||
Ok(RoaEContentParsed { der: der.to_vec() })
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `RoaEContent` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `RoaEContentParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), RoaProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4 (`parse + validate`).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
|
||||
let (rem, obj) = parse_der(der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(RoaDecodeError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 2 && seq.len() != 3 {
|
||||
return Err(RoaDecodeError::InvalidAttestationSequenceLen(seq.len()));
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
let mut version: u32 = 0;
|
||||
if seq.len() == 3 {
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(RoaDecodeError::Parse(
|
||||
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
|
||||
));
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(RoaDecodeError::Parse(
|
||||
"trailing bytes inside RouteOriginAttestation.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if v != 0 {
|
||||
return Err(RoaDecodeError::InvalidVersion(v));
|
||||
}
|
||||
version = 0;
|
||||
idx = 1;
|
||||
}
|
||||
|
||||
let as_id_u64 = seq[idx]
|
||||
.as_u64()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
if as_id_u64 > u32::MAX as u64 {
|
||||
return Err(RoaDecodeError::AsIdOutOfRange(as_id_u64));
|
||||
}
|
||||
let as_id = as_id_u64 as u32;
|
||||
idx += 1;
|
||||
|
||||
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
|
||||
|
||||
let mut out = Self {
|
||||
version,
|
||||
as_id,
|
||||
ip_addr_blocks,
|
||||
};
|
||||
out.canonicalize();
|
||||
Ok(out)
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
pub fn canonicalize(&mut self) {
|
||||
@ -241,7 +268,10 @@ impl RoaEContent {
|
||||
///
|
||||
/// This performs the EE/payload semantic checks that do not require certificate path
|
||||
/// validation.
|
||||
pub fn validate_against_ee_cert(&self, ee: &ResourceCertificate) -> Result<(), RoaValidateError> {
|
||||
pub fn validate_against_ee_cert(
|
||||
&self,
|
||||
ee: &ResourceCertificate,
|
||||
) -> Result<(), RoaValidateError> {
|
||||
if ee.tbs.extensions.as_resources.is_some() {
|
||||
return Err(RoaValidateError::EeAsResourcesPresent);
|
||||
}
|
||||
@ -273,6 +303,91 @@ impl RoaEContent {
|
||||
}
|
||||
}
|
||||
|
||||
impl RoaObjectParsed {
|
||||
pub fn validate_profile(self) -> Result<RoaObject, RoaProfileError> {
|
||||
let signed_object = self.signed_object.validate_profile()?;
|
||||
let econtent_type = signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent_type
|
||||
.clone();
|
||||
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
|
||||
return Err(RoaProfileError::InvalidEContentType(econtent_type));
|
||||
}
|
||||
let roa = self
|
||||
.roa
|
||||
.ok_or_else(|| RoaProfileError::ProfileDecode("ROA.eContent missing".into()))?
|
||||
.validate_profile()?;
|
||||
Ok(RoaObject {
|
||||
signed_object,
|
||||
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
|
||||
roa,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RoaEContentParsed {
|
||||
pub fn validate_profile(self) -> Result<RoaEContent, RoaProfileError> {
|
||||
let (_rem, obj) =
|
||||
parse_der(&self.der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.len() != 2 && seq.len() != 3 {
|
||||
return Err(RoaProfileError::InvalidAttestationSequenceLen(seq.len()));
|
||||
}
|
||||
|
||||
let mut idx = 0;
|
||||
let mut version: u32 = 0;
|
||||
if seq.len() == 3 {
|
||||
let v_obj = &seq[0];
|
||||
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
|
||||
return Err(RoaProfileError::ProfileDecode(
|
||||
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
|
||||
));
|
||||
}
|
||||
let inner_der = v_obj
|
||||
.as_slice()
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
let (rem, inner) =
|
||||
parse_der(inner_der).map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(RoaProfileError::ProfileDecode(
|
||||
"trailing bytes inside RouteOriginAttestation.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if v != 0 {
|
||||
return Err(RoaProfileError::InvalidVersion(v));
|
||||
}
|
||||
version = 0;
|
||||
idx = 1;
|
||||
}
|
||||
|
||||
let as_id_u64 = seq[idx]
|
||||
.as_u64()
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if as_id_u64 > u32::MAX as u64 {
|
||||
return Err(RoaProfileError::AsIdOutOfRange(as_id_u64));
|
||||
}
|
||||
let as_id = as_id_u64 as u32;
|
||||
idx += 1;
|
||||
|
||||
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
|
||||
|
||||
let mut out = RoaEContent {
|
||||
version,
|
||||
as_id,
|
||||
ip_addr_blocks,
|
||||
};
|
||||
out.canonicalize();
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
|
||||
let afi = match p.afi {
|
||||
RoaAfi::Ipv4 => RcAfi::Ipv4,
|
||||
@ -285,60 +400,63 @@ fn roa_prefix_to_rc(p: &IpPrefix) -> RcIpPrefix {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
|
||||
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.is_empty() || seq.len() > 2 {
|
||||
return Err(RoaDecodeError::InvalidIpAddrBlocksLen(seq.len()));
|
||||
return Err(RoaProfileError::InvalidIpAddrBlocksLen(seq.len()));
|
||||
}
|
||||
|
||||
let mut out: Vec<RoaIpAddressFamily> = Vec::new();
|
||||
for fam in seq {
|
||||
let family = parse_ip_address_family(fam)?;
|
||||
if out.iter().any(|f| f.afi == family.afi) {
|
||||
return Err(RoaDecodeError::DuplicateAfi(family.afi));
|
||||
return Err(RoaProfileError::DuplicateAfi(family.afi));
|
||||
}
|
||||
out.push(family);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaDecodeError> {
|
||||
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.len() != 2 {
|
||||
return Err(RoaDecodeError::InvalidIpAddressFamily);
|
||||
return Err(RoaProfileError::InvalidIpAddressFamily);
|
||||
}
|
||||
|
||||
let afi = parse_afi(&seq[0])?;
|
||||
let addresses = parse_roa_addresses(afi, &seq[1])?;
|
||||
if addresses.is_empty() {
|
||||
return Err(RoaDecodeError::EmptyAddressList);
|
||||
return Err(RoaProfileError::EmptyAddressList);
|
||||
}
|
||||
|
||||
Ok(RoaIpAddressFamily { afi, addresses })
|
||||
}
|
||||
|
||||
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaDecodeError> {
|
||||
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaProfileError> {
|
||||
let bytes = obj
|
||||
.as_slice()
|
||||
.map_err(|_e| RoaDecodeError::InvalidAddressFamily)?;
|
||||
.map_err(|_e| RoaProfileError::InvalidAddressFamily)?;
|
||||
if bytes.len() != 2 {
|
||||
return Err(RoaDecodeError::InvalidAddressFamily);
|
||||
return Err(RoaProfileError::InvalidAddressFamily);
|
||||
}
|
||||
match bytes {
|
||||
[0x00, 0x01] => Ok(RoaAfi::Ipv4),
|
||||
[0x00, 0x02] => Ok(RoaAfi::Ipv6),
|
||||
_ => Err(RoaDecodeError::UnsupportedAfi(bytes.to_vec())),
|
||||
_ => Err(RoaProfileError::UnsupportedAfi(bytes.to_vec())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddress>, RoaDecodeError> {
|
||||
fn parse_roa_addresses(
|
||||
afi: RoaAfi,
|
||||
obj: &DerObject<'_>,
|
||||
) -> Result<Vec<RoaIpAddress>, RoaProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
let mut out = Vec::with_capacity(seq.len());
|
||||
for entry in seq {
|
||||
out.push(parse_roa_ip_address(afi, entry)?);
|
||||
@ -346,12 +464,12 @@ fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddr
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaDecodeError> {
|
||||
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaProfileError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
if seq.is_empty() || seq.len() > 2 {
|
||||
return Err(RoaDecodeError::InvalidRoaIpAddress);
|
||||
return Err(RoaProfileError::InvalidRoaIpAddress);
|
||||
}
|
||||
|
||||
let prefix = parse_prefix_bits(afi, &seq[0])?;
|
||||
@ -360,10 +478,10 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
|
||||
Some(m) => {
|
||||
let v = m
|
||||
.as_u64()
|
||||
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
|
||||
.map_err(|e| RoaProfileError::ProfileDecode(e.to_string()))?;
|
||||
let max_len: u16 = v
|
||||
.try_into()
|
||||
.map_err(|_e| RoaDecodeError::InvalidMaxLength {
|
||||
.map_err(|_e| RoaProfileError::InvalidMaxLength {
|
||||
afi,
|
||||
prefix_len: prefix.prefix_len,
|
||||
max_len: u16::MAX,
|
||||
@ -375,7 +493,7 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
|
||||
if let Some(max_len) = max_length {
|
||||
let ub = afi.ub();
|
||||
if max_len > ub || max_len < prefix.prefix_len {
|
||||
return Err(RoaDecodeError::InvalidMaxLength {
|
||||
return Err(RoaProfileError::InvalidMaxLength {
|
||||
afi,
|
||||
prefix_len: prefix.prefix_len,
|
||||
max_len,
|
||||
@ -386,31 +504,31 @@ fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress
|
||||
Ok(RoaIpAddress { prefix, max_length })
|
||||
}
|
||||
|
||||
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaDecodeError> {
|
||||
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaProfileError> {
|
||||
let (unused_bits, bytes) = match &obj.content {
|
||||
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
|
||||
_ => return Err(RoaDecodeError::InvalidPrefixBitString),
|
||||
_ => return Err(RoaProfileError::InvalidPrefixBitString),
|
||||
};
|
||||
|
||||
if unused_bits > 7 {
|
||||
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||
return Err(RoaProfileError::InvalidPrefixUnusedBits);
|
||||
}
|
||||
if bytes.is_empty() {
|
||||
if unused_bits != 0 {
|
||||
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||
return Err(RoaProfileError::InvalidPrefixUnusedBits);
|
||||
}
|
||||
} else if unused_bits != 0 {
|
||||
let mask = (1u8 << unused_bits) - 1;
|
||||
if (bytes[bytes.len() - 1] & mask) != 0 {
|
||||
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
|
||||
return Err(RoaProfileError::InvalidPrefixUnusedBits);
|
||||
}
|
||||
}
|
||||
|
||||
let prefix_len = (bytes.len() * 8)
|
||||
.checked_sub(unused_bits as usize)
|
||||
.ok_or(RoaDecodeError::InvalidPrefixUnusedBits)? as u16;
|
||||
.ok_or(RoaProfileError::InvalidPrefixUnusedBits)? as u16;
|
||||
if prefix_len > afi.ub() {
|
||||
return Err(RoaDecodeError::PrefixLenOutOfRange { afi, prefix_len });
|
||||
return Err(RoaProfileError::PrefixLenOutOfRange { afi, prefix_len });
|
||||
}
|
||||
|
||||
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,10 @@ use url::Url;
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
||||
use crate::data_model::rc::{AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate};
|
||||
use crate::data_model::rc::{
|
||||
AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate,
|
||||
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
|
||||
};
|
||||
use crate::data_model::tal::Tal;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@ -12,103 +15,160 @@ pub struct TaCertificate {
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TaCertificateError {
|
||||
pub enum TaCertificateParseError {
|
||||
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
|
||||
Parse(String),
|
||||
ResourceCertificate(#[from] ResourceCertificateParseError),
|
||||
}
|
||||
|
||||
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1; RFC 6487 §4)")]
|
||||
TrailingBytes(usize),
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TaCertificateParsed {
|
||||
pub rc_parsed: ResourceCertificateParsed,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TaCertificateProfileError {
|
||||
#[error("resource certificate profile error: {0} (RFC 5280 §4; RFC 6487 §4)")]
|
||||
ResourceCertificate(#[from] ResourceCertificateProfileError),
|
||||
|
||||
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
|
||||
NotCa,
|
||||
|
||||
#[error("TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)")]
|
||||
#[error(
|
||||
"TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)"
|
||||
)]
|
||||
NotSelfSignedIssuerSubject,
|
||||
|
||||
#[error("TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)")]
|
||||
InvalidSelfSignature(String),
|
||||
|
||||
#[error("TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)")]
|
||||
#[error(
|
||||
"TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)"
|
||||
)]
|
||||
MissingOrInvalidCertificatePolicies,
|
||||
|
||||
#[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")]
|
||||
MissingSubjectKeyIdentifier,
|
||||
|
||||
#[error("TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)")]
|
||||
#[error(
|
||||
"TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)"
|
||||
)]
|
||||
ResourcesMissing,
|
||||
|
||||
#[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")]
|
||||
ResourcesEmpty,
|
||||
|
||||
#[error("TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)")]
|
||||
#[error(
|
||||
"TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)"
|
||||
)]
|
||||
IpResourcesInherit,
|
||||
|
||||
#[error("TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)")]
|
||||
#[error(
|
||||
"TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)"
|
||||
)]
|
||||
AsResourcesInherit,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TaCertificateDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] TaCertificateParseError),
|
||||
|
||||
#[error("{0}")]
|
||||
Validate(#[from] TaCertificateProfileError),
|
||||
}
|
||||
|
||||
/// Backwards-compatible name: TA certificate errors from parse+validate.
|
||||
pub type TaCertificateError = TaCertificateDecodeError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TaCertificateVerifyError {
|
||||
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 8630 §2.3)")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
||||
TrailingBytes(usize),
|
||||
|
||||
#[error(
|
||||
"TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)"
|
||||
)]
|
||||
InvalidSelfSignature(String),
|
||||
}
|
||||
|
||||
impl TaCertificate {
|
||||
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
|
||||
let rc_ca = ResourceCertificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||
if rc_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(TaCertificateError::NotCa);
|
||||
}
|
||||
|
||||
// Strong self-signed check: issuer==subject AND signature verifies with its own SPKI.
|
||||
let (rem, cert) =
|
||||
X509Certificate::from_der(der).map_err(|e| TaCertificateError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(TaCertificateError::TrailingBytes(rem.len()));
|
||||
}
|
||||
if cert.issuer().to_string() != cert.subject().to_string() {
|
||||
return Err(TaCertificateError::NotSelfSignedIssuerSubject);
|
||||
}
|
||||
cert.verify_signature(None)
|
||||
.map_err(|e| TaCertificateError::InvalidSelfSignature(e.to_string()))?;
|
||||
|
||||
Self::validate_rc_constraints(&rc_ca)?;
|
||||
|
||||
Ok(Self {
|
||||
raw_der: der.to_vec(),
|
||||
rc_ca,
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_der(der: &[u8]) -> Result<TaCertificateParsed, TaCertificateParseError> {
|
||||
Ok(TaCertificateParsed {
|
||||
rc_parsed: ResourceCertificate::parse_der(der)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Profile validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `TaCertificate` is already profile-validated when constructed via `decode_der()` /
|
||||
/// `TaCertificateParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), TaCertificateProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode a TA certificate (`parse + validate`).
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, TaCertificateDecodeError> {
|
||||
Ok(Self::parse_der(der)?.validate_profile()?)
|
||||
}
|
||||
|
||||
/// Backwards-compatible helper (historical name).
|
||||
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
|
||||
Self::decode_der(der)
|
||||
}
|
||||
|
||||
pub fn spki_der(&self) -> &[u8] {
|
||||
&self.rc_ca.tbs.subject_public_key_info
|
||||
}
|
||||
|
||||
/// Verify step of scheme A (`parse → validate → verify`).
|
||||
pub fn verify_self_signature(&self) -> Result<(), TaCertificateVerifyError> {
|
||||
let (rem, cert) = X509Certificate::from_der(&self.raw_der)
|
||||
.map_err(|e| TaCertificateVerifyError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(TaCertificateVerifyError::TrailingBytes(rem.len()));
|
||||
}
|
||||
cert.verify_signature(None)
|
||||
.map_err(|e| TaCertificateVerifyError::InvalidSelfSignature(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate TA-specific semantic constraints on a parsed Resource Certificate.
|
||||
///
|
||||
/// Note: this does not verify the X.509 signature; it is intended for higher-level logic and
|
||||
/// for unit tests that exercise individual constraint branches.
|
||||
pub fn validate_rc_constraints(rc_ca: &ResourceCertificate) -> Result<(), TaCertificateError> {
|
||||
pub fn validate_rc_constraints(
|
||||
rc_ca: &ResourceCertificate,
|
||||
) -> Result<(), TaCertificateProfileError> {
|
||||
if rc_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(TaCertificateError::NotCa);
|
||||
return Err(TaCertificateProfileError::NotCa);
|
||||
}
|
||||
|
||||
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER) {
|
||||
return Err(TaCertificateError::MissingOrInvalidCertificatePolicies);
|
||||
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER)
|
||||
{
|
||||
return Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies);
|
||||
}
|
||||
if rc_ca.tbs.extensions.subject_key_identifier.is_none() {
|
||||
return Err(TaCertificateError::MissingSubjectKeyIdentifier);
|
||||
return Err(TaCertificateProfileError::MissingSubjectKeyIdentifier);
|
||||
}
|
||||
|
||||
let ip = rc_ca.tbs.extensions.ip_resources.as_ref();
|
||||
let asn = rc_ca.tbs.extensions.as_resources.as_ref();
|
||||
if ip.is_none() && asn.is_none() {
|
||||
return Err(TaCertificateError::ResourcesMissing);
|
||||
return Err(TaCertificateProfileError::ResourcesMissing);
|
||||
}
|
||||
|
||||
let mut has_any_resource = false;
|
||||
|
||||
if let Some(ip) = ip {
|
||||
if ip.has_any_inherit() {
|
||||
return Err(TaCertificateError::IpResourcesInherit);
|
||||
return Err(TaCertificateProfileError::IpResourcesInherit);
|
||||
}
|
||||
for fam in &ip.families {
|
||||
match &fam.choice {
|
||||
IpAddressChoice::Inherit => return Err(TaCertificateError::IpResourcesInherit),
|
||||
IpAddressChoice::Inherit => {
|
||||
return Err(TaCertificateProfileError::IpResourcesInherit);
|
||||
}
|
||||
IpAddressChoice::AddressesOrRanges(items) => {
|
||||
if !items.is_empty() {
|
||||
has_any_resource = true;
|
||||
@ -122,7 +182,7 @@ impl TaCertificate {
|
||||
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|
||||
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
|
||||
{
|
||||
return Err(TaCertificateError::AsResourcesInherit);
|
||||
return Err(TaCertificateProfileError::AsResourcesInherit);
|
||||
}
|
||||
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
|
||||
if !items.is_empty() {
|
||||
@ -137,13 +197,33 @@ impl TaCertificate {
|
||||
}
|
||||
|
||||
if !has_any_resource {
|
||||
return Err(TaCertificateError::ResourcesEmpty);
|
||||
return Err(TaCertificateProfileError::ResourcesEmpty);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TaCertificateParsed {
|
||||
pub fn validate_profile(self) -> Result<TaCertificate, TaCertificateProfileError> {
|
||||
let rc_ca = self.rc_parsed.validate_profile()?;
|
||||
if rc_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(TaCertificateProfileError::NotCa);
|
||||
}
|
||||
|
||||
if rc_ca.tbs.issuer_dn != rc_ca.tbs.subject_dn {
|
||||
return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject);
|
||||
}
|
||||
|
||||
TaCertificate::validate_rc_constraints(&rc_ca)?;
|
||||
|
||||
Ok(TaCertificate {
|
||||
raw_der: rc_ca.raw_der.clone(),
|
||||
rc_ca,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TrustAnchor {
|
||||
pub tal: Tal,
|
||||
@ -154,12 +234,20 @@ pub struct TrustAnchor {
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TrustAnchorError {
|
||||
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
|
||||
TaCertificate(#[from] TaCertificateError),
|
||||
TaCertificate(#[from] TaCertificateDecodeError),
|
||||
|
||||
#[error("{0}")]
|
||||
Bind(#[from] TrustAnchorBindError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TrustAnchorBindError {
|
||||
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
|
||||
ResolvedUriNotInTal(String),
|
||||
|
||||
#[error("TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)")]
|
||||
#[error(
|
||||
"TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)"
|
||||
)]
|
||||
TalSpkiMismatch,
|
||||
}
|
||||
|
||||
@ -167,16 +255,28 @@ impl TrustAnchor {
|
||||
/// Bind a TAL and a downloaded TA certificate.
|
||||
///
|
||||
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
|
||||
pub fn bind(tal: Tal, ta_der: &[u8], resolved_uri: Option<&Url>) -> Result<Self, TrustAnchorError> {
|
||||
pub fn bind_der(
|
||||
tal: Tal,
|
||||
ta_der: &[u8],
|
||||
resolved_uri: Option<&Url>,
|
||||
) -> Result<Self, TrustAnchorError> {
|
||||
let ta_certificate = TaCertificate::decode_der(ta_der)?;
|
||||
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
|
||||
}
|
||||
|
||||
pub fn bind(
|
||||
tal: Tal,
|
||||
ta_certificate: TaCertificate,
|
||||
resolved_uri: Option<&Url>,
|
||||
) -> Result<Self, TrustAnchorBindError> {
|
||||
if let Some(u) = resolved_uri {
|
||||
if !tal.ta_uris.iter().any(|x| x == u) {
|
||||
return Err(TrustAnchorError::ResolvedUriNotInTal(u.to_string()));
|
||||
return Err(TrustAnchorBindError::ResolvedUriNotInTal(u.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let ta_certificate = TaCertificate::from_der(ta_der)?;
|
||||
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
|
||||
return Err(TrustAnchorError::TalSpkiMismatch);
|
||||
return Err(TrustAnchorBindError::TalSpkiMismatch);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
use base64::Engine;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TalParsed {
|
||||
pub raw: Vec<u8>,
|
||||
/// Lines split by '\n' and normalized by stripping a trailing '\r' per line.
|
||||
pub lines: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Tal {
|
||||
pub raw: Vec<u8>,
|
||||
@ -10,17 +17,22 @@ pub struct Tal {
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TalDecodeError {
|
||||
pub enum TalParseError {
|
||||
#[error("TAL must be valid UTF-8 (RFC 8630 §2.2)")]
|
||||
InvalidUtf8,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TalProfileError {
|
||||
#[error("TAL comments must appear only at the beginning (RFC 8630 §2.2)")]
|
||||
CommentAfterHeader,
|
||||
|
||||
#[error("TAL must contain at least one TA URI line (RFC 8630 §2.2)")]
|
||||
MissingTaUris,
|
||||
|
||||
#[error("TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)")]
|
||||
#[error(
|
||||
"TAL must contain an empty line separator between URI list and SPKI base64 (RFC 8630 §2.2)"
|
||||
)]
|
||||
MissingSeparatorEmptyLine,
|
||||
|
||||
#[error("TAL TA URI invalid: {0} (RFC 8630 §2.2)")]
|
||||
@ -29,10 +41,14 @@ pub enum TalDecodeError {
|
||||
#[error("TAL TA URI scheme must be rsync or https, got {0} (RFC 8630 §2.2)")]
|
||||
UnsupportedUriScheme(String),
|
||||
|
||||
#[error("TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)")]
|
||||
#[error(
|
||||
"TAL TA URI must reference a single object (must not end with '/'): {0} (RFC 8630 §2.3)"
|
||||
)]
|
||||
UriIsDirectory(String),
|
||||
|
||||
#[error("TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)")]
|
||||
#[error(
|
||||
"TAL must contain base64-encoded SubjectPublicKeyInfo after the separator (RFC 8630 §2.2)"
|
||||
)]
|
||||
MissingSpki,
|
||||
|
||||
#[error("TAL SPKI base64 decode failed (RFC 8630 §2.2)")]
|
||||
@ -42,90 +58,123 @@ pub enum TalDecodeError {
|
||||
SpkiDerEmpty,
|
||||
}
|
||||
|
||||
impl Tal {
|
||||
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
|
||||
let raw = input.to_vec();
|
||||
let text = std::str::from_utf8(input).map_err(|_| TalDecodeError::InvalidUtf8)?;
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TalDecodeError {
|
||||
#[error("{0}")]
|
||||
Parse(#[from] TalParseError),
|
||||
|
||||
let lines: Vec<&str> = text
|
||||
#[error("{0}")]
|
||||
Validate(#[from] TalProfileError),
|
||||
}
|
||||
|
||||
impl Tal {
|
||||
/// Parse step of scheme A (`parse → validate → verify`).
|
||||
pub fn parse_bytes(input: &[u8]) -> Result<TalParsed, TalParseError> {
|
||||
let raw = input.to_vec();
|
||||
let text = std::str::from_utf8(input).map_err(|_| TalParseError::InvalidUtf8)?;
|
||||
|
||||
let lines: Vec<String> = text
|
||||
.split('\n')
|
||||
.map(|l| l.strip_suffix('\r').unwrap_or(l))
|
||||
.map(|l| l.strip_suffix('\r').unwrap_or(l).to_string())
|
||||
.collect();
|
||||
|
||||
Ok(TalParsed { raw, lines })
|
||||
}
|
||||
|
||||
/// Validate step of scheme A (`parse → validate → verify`).
|
||||
///
|
||||
/// `Tal` is already profile-validated when constructed via `decode_bytes()` /
|
||||
/// `TalParsed::validate_profile()`.
|
||||
pub fn validate_profile(&self) -> Result<(), TalProfileError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decode_bytes(input: &[u8]) -> Result<Self, TalDecodeError> {
|
||||
Ok(Self::parse_bytes(input)?.validate_profile()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TalParsed {
|
||||
pub fn validate_profile(self) -> Result<Tal, TalProfileError> {
|
||||
let mut idx = 0usize;
|
||||
|
||||
// 1) Leading comments.
|
||||
let mut comments: Vec<String> = Vec::new();
|
||||
while idx < lines.len() && lines[idx].starts_with('#') {
|
||||
comments.push(lines[idx][1..].to_string());
|
||||
while idx < self.lines.len() && self.lines[idx].starts_with('#') {
|
||||
comments.push(self.lines[idx][1..].to_string());
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// 2) URI list (one or more non-empty lines).
|
||||
let mut ta_uris: Vec<Url> = Vec::new();
|
||||
while idx < lines.len() {
|
||||
let line = lines[idx].trim();
|
||||
while idx < self.lines.len() {
|
||||
let line = self.lines[idx].trim();
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if line.starts_with('#') {
|
||||
return Err(TalDecodeError::CommentAfterHeader);
|
||||
return Err(TalProfileError::CommentAfterHeader);
|
||||
}
|
||||
let url = match Url::parse(line) {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
if !ta_uris.is_empty() {
|
||||
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||
return Err(TalProfileError::MissingSeparatorEmptyLine);
|
||||
}
|
||||
return Err(TalDecodeError::InvalidUri(line.to_string()));
|
||||
return Err(TalProfileError::InvalidUri(line.to_string()));
|
||||
}
|
||||
};
|
||||
match url.scheme() {
|
||||
"rsync" | "https" => {}
|
||||
s => return Err(TalDecodeError::UnsupportedUriScheme(s.to_string())),
|
||||
s => return Err(TalProfileError::UnsupportedUriScheme(s.to_string())),
|
||||
}
|
||||
if url.path().ends_with('/') {
|
||||
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||
return Err(TalProfileError::UriIsDirectory(line.to_string()));
|
||||
}
|
||||
if url.path_segments().and_then(|mut s| s.next_back()).unwrap_or("").is_empty() {
|
||||
return Err(TalDecodeError::UriIsDirectory(line.to_string()));
|
||||
if url
|
||||
.path_segments()
|
||||
.and_then(|mut s| s.next_back())
|
||||
.unwrap_or("")
|
||||
.is_empty()
|
||||
{
|
||||
return Err(TalProfileError::UriIsDirectory(line.to_string()));
|
||||
}
|
||||
ta_uris.push(url);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if ta_uris.is_empty() {
|
||||
return Err(TalDecodeError::MissingTaUris);
|
||||
return Err(TalProfileError::MissingTaUris);
|
||||
}
|
||||
|
||||
// 3) Empty line separator (must exist).
|
||||
if idx >= lines.len() || !lines[idx].trim().is_empty() {
|
||||
return Err(TalDecodeError::MissingSeparatorEmptyLine);
|
||||
if idx >= self.lines.len() || !self.lines[idx].trim().is_empty() {
|
||||
return Err(TalProfileError::MissingSeparatorEmptyLine);
|
||||
}
|
||||
idx += 1;
|
||||
|
||||
// 4) Base64(SPKI DER) remainder; allow line wrapping.
|
||||
let mut b64 = String::new();
|
||||
while idx < lines.len() {
|
||||
let line = lines[idx].trim();
|
||||
while idx < self.lines.len() {
|
||||
let line = self.lines[idx].trim();
|
||||
if !line.is_empty() {
|
||||
b64.push_str(line);
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
if b64.is_empty() {
|
||||
return Err(TalDecodeError::MissingSpki);
|
||||
return Err(TalProfileError::MissingSpki);
|
||||
}
|
||||
|
||||
let spki_der = base64::engine::general_purpose::STANDARD
|
||||
.decode(b64.as_bytes())
|
||||
.map_err(|_| TalDecodeError::SpkiBase64Decode)?;
|
||||
.map_err(|_| TalProfileError::SpkiBase64Decode)?;
|
||||
if spki_der.is_empty() {
|
||||
return Err(TalDecodeError::SpkiDerEmpty);
|
||||
return Err(TalProfileError::SpkiDerEmpty);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
raw,
|
||||
Ok(Tal {
|
||||
raw: self.raw,
|
||||
comments,
|
||||
ta_uris,
|
||||
subject_public_key_info_der: spki_der,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaObject};
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaObject, AspaProfileError};
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
|
||||
#[test]
|
||||
fn decode_aspa_fixture_smoke() {
|
||||
@ -7,6 +8,7 @@ fn decode_aspa_fixture_smoke() {
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
let aspa = AspaObject::decode_der(&der).expect("decode aspa");
|
||||
aspa.validate_profile().expect("validate ASPA profile");
|
||||
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);
|
||||
@ -15,11 +17,23 @@ fn decode_aspa_fixture_smoke() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_aspa_econtent_type() {
|
||||
fn from_signed_object_accepts_aspa_fixture() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
)
|
||||
.expect("read ASPA fixture");
|
||||
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
let aspa = AspaObject::from_signed_object(so).expect("from_signed_object");
|
||||
aspa.validate_profile().expect("validate ASPA profile");
|
||||
}
|
||||
|
||||
#[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(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::InvalidEContentType(_))
|
||||
));
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaEContent};
|
||||
use rpki::data_model::aspa::{AspaDecodeError, AspaEContent, AspaParseError, AspaProfileError};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
@ -68,22 +68,87 @@ fn aspa_attestation_missing_version(customer: u64, providers: Vec<u64>) -> Vec<u
|
||||
der_sequence(vec![der_integer_u64(customer), providers_der])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_bytes_are_rejected_in_parse_step() {
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![64497]);
|
||||
let mut bad = der.clone();
|
||||
bad.push(0);
|
||||
let err = AspaEContent::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Parse(AspaParseError::TrailingBytes(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_tag_must_be_context_specific_0() {
|
||||
// Build a 3-element SEQUENCE but make the first element an INTEGER, not [0] EXPLICIT.
|
||||
let providers_der = der_sequence(vec![der_integer_u64(64497)]);
|
||||
let der = der_sequence(vec![
|
||||
der_integer_u64(1), // wrong tag/class
|
||||
der_integer_u64(64496), // customerASID
|
||||
providers_der, // providers
|
||||
]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::VersionMustBeExplicitOne)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_explicit_tag_rejects_trailing_bytes_inside_inner_der() {
|
||||
let mut version_inner = der_integer_u64(1);
|
||||
version_inner.extend(tlv(0x05, &[])); // NULL after INTEGER
|
||||
|
||||
let providers_der = der_sequence(vec![der_integer_u64(64497)]);
|
||||
let der = der_sequence(vec![
|
||||
cs_explicit(0, version_inner),
|
||||
der_integer_u64(64496),
|
||||
providers_der,
|
||||
]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::ProfileDecode(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_asid_out_of_range_is_rejected() {
|
||||
let der = aspa_attestation_explicit_version(1, 64496, vec![(u32::MAX as u64) + 1]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::ProviderAsIdOutOfRange(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::InvalidAttestationSequence)
|
||||
));
|
||||
|
||||
let der = aspa_attestation_explicit_version(0, 64496, vec![64497]);
|
||||
let err = AspaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, AspaDecodeError::VersionMustBeExplicitOne));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::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(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::CustomerAsIdOutOfRange(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -91,21 +156,32 @@ 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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::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)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaDecodeError::Validate(AspaProfileError::ProvidersContainCustomer(64496))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -11,4 +11,3 @@ fn aspa_embedded_ee_cert_resources_validate() {
|
||||
aspa.validate_embedded_ee_cert()
|
||||
.expect("aspa EE cert resources must validate");
|
||||
}
|
||||
|
||||
|
||||
@ -3,11 +3,14 @@ use time::OffsetDateTime;
|
||||
|
||||
use rpki::data_model::aspa::{AspaEContent, AspaValidateError};
|
||||
use rpki::data_model::rc::{
|
||||
AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind,
|
||||
AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpResourceSet, RcExtensions, ResourceCertKind,
|
||||
ResourceCertificate, RpkixTbsCertificate, SubjectInfoAccess,
|
||||
};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
fn dummy_ee(
|
||||
ip_resources: Option<IpResourceSet>,
|
||||
as_resources: Option<AsResourceSet>,
|
||||
) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
@ -50,7 +53,9 @@ fn validate_accepts_when_customer_matches_ee_asid() {
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
64496,
|
||||
)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
@ -82,10 +87,12 @@ fn validate_rejects_as_resources_inherit_or_ranges() {
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range {
|
||||
min: 64496,
|
||||
max: 64497,
|
||||
}])),
|
||||
},
|
||||
])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
@ -99,12 +106,17 @@ fn validate_rejects_customer_mismatch() {
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64511)])),
|
||||
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 { .. }));
|
||||
assert!(matches!(
|
||||
err,
|
||||
AspaValidateError::CustomerAsIdMismatch { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -113,7 +125,9 @@ fn validate_rejects_ip_resources_present() {
|
||||
let ee = dummy_ee(
|
||||
Some(IpResourceSet { families: vec![] }),
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
64496,
|
||||
)])),
|
||||
rdi: None,
|
||||
}),
|
||||
);
|
||||
@ -127,8 +141,12 @@ fn validate_rejects_rdi_present_or_not_single_id() {
|
||||
let ee = dummy_ee(
|
||||
None,
|
||||
Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64496)])),
|
||||
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();
|
||||
@ -147,4 +165,3 @@ fn validate_rejects_rdi_present_or_not_single_id() {
|
||||
let err = aspa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, AspaValidateError::EeAsResourcesNotSingleId));
|
||||
}
|
||||
|
||||
|
||||
@ -11,4 +11,3 @@ fn verify_aspa_cms_signature_with_embedded_ee_cert() {
|
||||
.verify_signature()
|
||||
.expect("ASPA CMS signature should verify with embedded EE cert");
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use rpki::data_model::common::{
|
||||
algorithm_params_absent_or_null, Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned,
|
||||
Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned, algorithm_params_absent_or_null,
|
||||
};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
use x509_parser::time::ASN1Time;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
|
||||
#[test]
|
||||
fn big_unsigned_helpers() {
|
||||
@ -32,7 +32,10 @@ fn time_encoding_validation() {
|
||||
t.validate_encoding_rfc5280("t").expect("utc ok");
|
||||
|
||||
let t = Asn1TimeUtc {
|
||||
utc: time::OffsetDateTime::parse("2050-01-01T00:00:00Z", &time::format_description::well_known::Rfc3339)
|
||||
utc: time::OffsetDateTime::parse(
|
||||
"2050-01-01T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.unwrap(),
|
||||
encoding: Asn1TimeEncoding::UtcTime,
|
||||
};
|
||||
@ -43,18 +46,12 @@ fn time_encoding_validation() {
|
||||
fn algorithm_params_absent_or_null_helper() {
|
||||
// AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL }
|
||||
// Using sha256WithRSAEncryption with NULL parameters.
|
||||
let alg_null = hex::decode(
|
||||
"300D06092A864886F70D01010B0500",
|
||||
)
|
||||
.unwrap();
|
||||
let alg_null = hex::decode("300D06092A864886F70D01010B0500").unwrap();
|
||||
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_null).expect("parse AlgorithmIdentifier");
|
||||
assert!(algorithm_params_absent_or_null(&id));
|
||||
|
||||
// Same OID, but parameters = INTEGER 1 (invalid for our helper).
|
||||
let alg_int = hex::decode(
|
||||
"300E06092A864886F70D01010B020101",
|
||||
)
|
||||
.unwrap();
|
||||
let alg_int = hex::decode("300E06092A864886F70D01010B020101").unwrap();
|
||||
let (_rem, id) = AlgorithmIdentifier::from_der(&alg_int).expect("parse AlgorithmIdentifier");
|
||||
assert!(!algorithm_params_absent_or_null(&id));
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::crl::Asn1TimeEncoding;
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
|
||||
#[test]
|
||||
fn decode_and_validate_crl_fixture() {
|
||||
@ -42,8 +42,7 @@ fn crl_signature_verification_succeeds_with_issuer_cert() {
|
||||
|
||||
#[test]
|
||||
fn decode_crl_with_revoked_entries() {
|
||||
let der =
|
||||
std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl")
|
||||
let der = std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl")
|
||||
.expect("read CRL fixture with revoked entries");
|
||||
|
||||
let crl = RpkixCrl::decode_der(&der).expect("decode CRL");
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use rpki::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
|
||||
use rpki::data_model::crl::{CrlDecodeError, CrlParseError, CrlVerifyError, RpkixCrl};
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::prelude::X509Certificate;
|
||||
|
||||
const TEST_NO_CRLSIGN_CERT_DER_B64: &str = "MIIDATCCAemgAwIBAgIUCyQLQJn92+gyAzvIz22q1F/97OMwDQYJKoZIhvcNAQELBQAwGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMB4XDTI2MDEyNzAzNTk1OVoXDTM2MDEyNTAzNTk1OVowGjEYMBYGA1UEAwwPVGVzdCBObyBDUkxTaWduMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr/aoMU8J6cddkM2r6F2snd1rCdQPepgo2T2lrqWFcxnQJdcxBL1OYg3wFi95TJmZSeIHIOGauDaJ2abmjgyOUHOC4U68x66JRg4hLkmLxo1cf3uYHWl9Obph6g2qPRvN80ORq70JPuL6mAfUkNiO9hnwK6oQiTzc/rjCQGIFH8kTESBMXLfNCyUpGi+MNztYH6Ha6bKAQuXgd29OFwIkOlGQnYgGC2qBMvnp86eITvV1gTiuI8Ho9m9nZHCmaD7TylvkMDq8Hk5nkIpRcG0uO60SkR2BiMOYe/TNn5dTmHd6bsdbU2GOvgnq1SnqGq3FOWhKIe3ycUJde0uNfZOqRwIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUFjyzfJCDNhFfKxVr06kjUkE23dMwDQYJKoZIhvcNAQELBQADggEBAK98n2gVlwKA3Ob1YeAm9f+8hm7pbvrt0tA8GW180CILjf09k7fKgiRlxqGdZ9ySXjU52+zCqu3MpBXVbI87ZC+zA6uK05n4y1F0n85MJ9hGR2UEiPcqou85X73LvioynnSOy/OV1PjKJXReUsqF3GgDtgcMyFssPJ9s/5DWuUCScUJY6pu0kuIGOLQ/oXUw4TvxUeyz73gOTiAJshVTQoLpHUhj0595S7lArjwi7oLI1b8m8guTknvhk0Sc3tJZmUqOcIvYIs0guHpaeC+sMoF4K+6UTrxxOBdX+fUEWNpUyYXWHjdZq25PbJdHwA/VAW2zYVojaVREligf0Qfo6F4=";
|
||||
|
||||
fn test_no_crlsign_cert_der() -> Vec<u8> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
general_purpose::STANDARD
|
||||
.decode(TEST_NO_CRLSIGN_CERT_DER_B64)
|
||||
.expect("decode base64 cert")
|
||||
@ -67,7 +67,10 @@ fn verify_errors_are_reported() {
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_certificate_der(&bad)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::IssuerCertificateTrailingBytes(1)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
CrlVerifyError::IssuerCertificateTrailingBytes(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -103,7 +106,10 @@ fn decode_rejects_trailing_bytes() {
|
||||
let mut bad = crl_der.clone();
|
||||
bad.push(0);
|
||||
let err = RpkixCrl::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(err, CrlDecodeError::TrailingBytes(1)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
CrlDecodeError::Parse(CrlParseError::TrailingBytes(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -122,6 +128,8 @@ fn verify_rejects_crl_with_trailing_bytes_in_raw_der() {
|
||||
let (_rem, issuer_cert) = X509Certificate::from_der(&issuer_cert_der).unwrap();
|
||||
let spki_der = issuer_cert.public_key().raw.to_vec();
|
||||
|
||||
let err = crl.verify_signature_with_issuer_spki_der(&spki_der).unwrap_err();
|
||||
let err = crl
|
||||
.verify_signature_with_issuer_spki_der(&spki_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, CrlVerifyError::CrlTrailingBytes(1)));
|
||||
}
|
||||
|
||||
161
tests/test_layered_api_m0.rs
Normal file
161
tests/test_layered_api_m0.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rpki::data_model::aspa::AspaEContent;
|
||||
use rpki::data_model::aspa::AspaObject;
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::manifest::ManifestEContent;
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::rc::ResourceCertificate;
|
||||
use rpki::data_model::roa::RoaEContent;
|
||||
use rpki::data_model::roa::RoaObject;
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
use rpki::data_model::ta::{TaCertificate, TrustAnchor};
|
||||
use rpki::data_model::tal::Tal;
|
||||
|
||||
#[test]
|
||||
fn scheme_a_layered_api_smoke() {
|
||||
// TAL / TA / TrustAnchor
|
||||
let tal_path = PathBuf::from("tests/fixtures/tal/ripe-ncc.tal");
|
||||
let tal_bytes = std::fs::read(&tal_path).expect("read TAL fixture");
|
||||
let tal = Tal::parse_bytes(&tal_bytes)
|
||||
.expect("parse TAL")
|
||||
.validate_profile()
|
||||
.expect("validate TAL profile");
|
||||
|
||||
let ta_path = PathBuf::from("tests/fixtures/ta/ripe-ncc-ta.cer");
|
||||
let ta_der = std::fs::read(&ta_path).expect("read TA cert fixture");
|
||||
let ta = TaCertificate::parse_der(&ta_der)
|
||||
.expect("parse TA cert")
|
||||
.validate_profile()
|
||||
.expect("validate TA constraints");
|
||||
ta.verify_self_signature()
|
||||
.expect("verify TA self-signature");
|
||||
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
.first()
|
||||
.cloned()
|
||||
.expect("TAL must include at least one TA URI");
|
||||
let _ta = TrustAnchor::bind(tal, ta, Some(&resolved)).expect("bind trust anchor");
|
||||
|
||||
// A CA resource certificate fixture (used as issuer in other tests).
|
||||
let ca_path = PathBuf::from(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
);
|
||||
let ca_der = std::fs::read(&ca_path).expect("read CA cert fixture");
|
||||
let ca_rc = ResourceCertificate::parse_der(&ca_der)
|
||||
.expect("parse CA resource certificate")
|
||||
.validate_profile()
|
||||
.expect("validate CA resource certificate profile");
|
||||
ca_rc
|
||||
.validate_profile()
|
||||
.expect("validate CA resource certificate profile");
|
||||
|
||||
// Signed object wrapper.
|
||||
let mft_path = PathBuf::from(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let mft_der = std::fs::read(&mft_path).expect("read MFT fixture");
|
||||
let so = RpkiSignedObject::parse_der(&mft_der)
|
||||
.expect("parse signed object")
|
||||
.validate_profile()
|
||||
.expect("validate signed object profile");
|
||||
so.verify().expect("verify CMS signature");
|
||||
|
||||
// Manifest object.
|
||||
let mft_obj = ManifestObject::parse_der(&mft_der)
|
||||
.expect("parse manifest")
|
||||
.validate_profile()
|
||||
.expect("validate manifest profile");
|
||||
mft_obj
|
||||
.validate_profile()
|
||||
.expect("validate manifest profile");
|
||||
mft_obj
|
||||
.validate_embedded_ee_cert()
|
||||
.expect("validate manifest EE resources");
|
||||
mft_obj
|
||||
.signed_object
|
||||
.verify()
|
||||
.expect("verify manifest CMS signature");
|
||||
let mft_ec = ManifestEContent::parse_der(
|
||||
&mft_obj
|
||||
.signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent,
|
||||
)
|
||||
.expect("parse MFT eContent")
|
||||
.validate_profile()
|
||||
.expect("validate MFT eContent profile");
|
||||
mft_ec
|
||||
.validate_profile()
|
||||
.expect("validate MFT eContent profile");
|
||||
|
||||
// ROA object.
|
||||
let roa_path =
|
||||
PathBuf::from("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
|
||||
let roa_der = std::fs::read(&roa_path).expect("read ROA fixture");
|
||||
let roa_obj = RoaObject::parse_der(&roa_der)
|
||||
.expect("parse ROA")
|
||||
.validate_profile()
|
||||
.expect("validate ROA profile");
|
||||
roa_obj
|
||||
.validate_embedded_ee_cert()
|
||||
.expect("validate ROA EE resources");
|
||||
roa_obj
|
||||
.signed_object
|
||||
.verify()
|
||||
.expect("verify ROA CMS signature");
|
||||
let roa_ec = RoaEContent::parse_der(
|
||||
&roa_obj
|
||||
.signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent,
|
||||
)
|
||||
.expect("parse ROA eContent")
|
||||
.validate_profile()
|
||||
.expect("validate ROA eContent profile");
|
||||
roa_ec
|
||||
.validate_profile()
|
||||
.expect("validate ROA eContent profile");
|
||||
|
||||
// ASPA object.
|
||||
let aspa_path = PathBuf::from(
|
||||
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
||||
);
|
||||
let aspa_der = std::fs::read(&aspa_path).expect("read ASPA fixture");
|
||||
let aspa_obj = AspaObject::parse_der(&aspa_der)
|
||||
.expect("parse ASPA")
|
||||
.validate_profile()
|
||||
.expect("validate ASPA profile");
|
||||
aspa_obj
|
||||
.validate_embedded_ee_cert()
|
||||
.expect("validate ASPA EE resources");
|
||||
aspa_obj
|
||||
.signed_object
|
||||
.verify()
|
||||
.expect("verify ASPA CMS signature");
|
||||
let aspa_ec = AspaEContent::parse_der(
|
||||
&aspa_obj
|
||||
.signed_object
|
||||
.signed_data
|
||||
.encap_content_info
|
||||
.econtent,
|
||||
)
|
||||
.expect("parse ASPA eContent")
|
||||
.validate_profile()
|
||||
.expect("validate ASPA eContent profile");
|
||||
aspa_ec
|
||||
.validate_profile()
|
||||
.expect("validate ASPA eContent profile");
|
||||
|
||||
// CRL object.
|
||||
let crl_path = PathBuf::from("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl");
|
||||
let crl_der = std::fs::read(&crl_path).expect("read CRL fixture with revoked entries");
|
||||
let crl = RpkixCrl::parse_der(&crl_der)
|
||||
.expect("parse CRL")
|
||||
.validate_profile()
|
||||
.expect("validate CRL profile");
|
||||
crl.validate_profile().expect("validate CRL profile");
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
use rpki::data_model::manifest::{ManifestEContent, ManifestObject};
|
||||
use rpki::data_model::manifest::{
|
||||
ManifestDecodeError, ManifestEContent, ManifestObject, ManifestProfileError,
|
||||
};
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
|
||||
#[test]
|
||||
fn decode_manifest_fixture_smoke() {
|
||||
@ -7,17 +10,44 @@ fn decode_manifest_fixture_smoke() {
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let mft = ManifestObject::decode_der(&der).expect("decode manifest object");
|
||||
mft.validate_profile().expect("validate manifest profile");
|
||||
|
||||
assert_eq!(mft.manifest.version, 0);
|
||||
assert_eq!(mft.manifest.file_hash_alg, rpki::data_model::oid::OID_SHA256);
|
||||
assert_eq!(
|
||||
mft.manifest.file_hash_alg,
|
||||
rpki::data_model::oid::OID_SHA256
|
||||
);
|
||||
assert!(mft.manifest.next_update > mft.manifest.this_update);
|
||||
assert!(!mft.manifest.files.is_empty());
|
||||
// The manifest file MUST NOT be listed in its own fileList.
|
||||
assert!(mft
|
||||
.manifest
|
||||
assert!(
|
||||
mft.manifest
|
||||
.files
|
||||
.iter()
|
||||
.all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft")));
|
||||
.all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_manifest_econtent_type() {
|
||||
let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa")
|
||||
.expect("read ROA fixture");
|
||||
let err = ManifestObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidEContentType(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_signed_object_accepts_manifest_fixture() {
|
||||
let so_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object");
|
||||
let mft = ManifestObject::from_signed_object(so).expect("from_signed_object");
|
||||
mft.validate_profile().expect("validate manifest profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -32,4 +62,3 @@ fn decode_manifest_econtent_from_fixture_signed_object() {
|
||||
.expect("decode manifest eContent");
|
||||
assert_eq!(e.version, 0);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
use rpki::data_model::common::BigUnsigned;
|
||||
use rpki::data_model::manifest::{ManifestDecodeError, ManifestEContent, ManifestObject};
|
||||
use rpki::data_model::manifest::{
|
||||
ManifestDecodeError, ManifestEContent, ManifestObject, ManifestParseError, ManifestProfileError,
|
||||
};
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
@ -49,8 +51,8 @@ fn der_integer_u64(v: u64) -> Vec<u8> {
|
||||
}
|
||||
|
||||
fn der_oid(oid: &str) -> Vec<u8> {
|
||||
use std::str::FromStr;
|
||||
use der_parser::asn1_rs::ToDer;
|
||||
use std::str::FromStr;
|
||||
let oid = der_parser::Oid::from_str(oid).unwrap();
|
||||
oid.to_der_vec().unwrap()
|
||||
}
|
||||
@ -116,7 +118,10 @@ fn manifest_econtent_version_must_be_zero_when_present() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidManifestVersion(1)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestVersion(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -131,7 +136,10 @@ fn manifest_number_too_long_rejected() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::ManifestNumberTooLong));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::ManifestNumberTooLong)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -146,7 +154,10 @@ fn manifest_number_negative_rejected() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidManifestNumber));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestNumber)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -160,7 +171,10 @@ fn this_update_and_next_update_must_be_generalized_time() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidThisUpdate));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidThisUpdate)
|
||||
));
|
||||
|
||||
let der = manifest_der(
|
||||
Some(0),
|
||||
@ -171,7 +185,10 @@ fn this_update_and_next_update_must_be_generalized_time() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidNextUpdate));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidNextUpdate)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -185,7 +202,10 @@ fn next_update_must_be_later() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::NextUpdateNotLater));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::NextUpdateNotLater)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -199,7 +219,10 @@ fn file_hash_alg_must_be_sha256() {
|
||||
vec![],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileHashAlg(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidFileHashAlg(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -215,7 +238,10 @@ fn file_list_entry_validation() {
|
||||
vec![file_and_hash("bad!.roa", 0, &hash)],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileName(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidFileName(_))
|
||||
));
|
||||
|
||||
// Non-octet-aligned BIT STRING
|
||||
let der = manifest_der(
|
||||
@ -227,7 +253,10 @@ fn file_list_entry_validation() {
|
||||
vec![file_and_hash("ok.roa", 1, &hash)],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::HashNotOctetAligned));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::HashNotOctetAligned)
|
||||
));
|
||||
|
||||
// Wrong hash length
|
||||
let der = manifest_der(
|
||||
@ -239,7 +268,10 @@ fn file_list_entry_validation() {
|
||||
vec![file_and_hash("ok.roa", 0, &[0u8; 31])],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidHashLength(31)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidHashLength(31))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -251,7 +283,10 @@ fn manifest_object_requires_correct_econtent_type() {
|
||||
let mut so = RpkiSignedObject::decode_der(&so_der).expect("decode signed object");
|
||||
so.signed_data.encap_content_info.econtent_type = "1.2.3.4".to_string();
|
||||
let err = ManifestObject::from_signed_object(so).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidEContentType(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidEContentType(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -260,7 +295,7 @@ fn manifest_sequence_length_is_validated() {
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::InvalidManifestSequenceLen(_)
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidManifestSequenceLen(_))
|
||||
));
|
||||
}
|
||||
|
||||
@ -279,7 +314,10 @@ fn file_list_must_be_sequence_and_entry_shape_validated() {
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileList));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidFileList)
|
||||
));
|
||||
|
||||
// FileAndHash not SEQUENCE of 2
|
||||
let der = manifest_der(
|
||||
@ -291,7 +329,10 @@ fn file_list_must_be_sequence_and_entry_shape_validated() {
|
||||
vec![der_sequence(vec![der_ia5("ok.roa")])],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileAndHash));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidFileAndHash)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -308,7 +349,10 @@ fn version_tag_must_be_context_specific_0() {
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::Parse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::ProfileDecode(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -331,7 +375,10 @@ fn manifest_econtent_trailing_bytes_are_rejected() {
|
||||
let mut bad = der.clone();
|
||||
bad.push(0);
|
||||
let err = ManifestEContent::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::TrailingBytes(1)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Parse(ManifestParseError::TrailingBytes(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -350,7 +397,10 @@ fn manifest_version_rejects_trailing_bytes_inside_explicit_tag() {
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::Parse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::ProfileDecode(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -366,7 +416,10 @@ fn manifest_rejects_hash_with_wrong_type() {
|
||||
vec![entry],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidHashType));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidHashType)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -389,7 +442,10 @@ fn file_name_validation_branches_are_exercised() {
|
||||
vec![file_and_hash(name, 0, &hash)],
|
||||
);
|
||||
let err = ManifestEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ManifestDecodeError::InvalidFileName(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestDecodeError::Validate(ManifestProfileError::InvalidFileName(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,89 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::data_model::manifest::{ManifestObject, ManifestValidateError};
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
|
||||
IpResourceSet,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn manifest_embedded_ee_cert_resources_validate() {
|
||||
fn load_manifest_fixture() -> ManifestObject {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
ManifestObject::decode_der(&der).expect("decode manifest")
|
||||
}
|
||||
|
||||
let mft = ManifestObject::decode_der(&der).expect("decode manifest");
|
||||
#[test]
|
||||
fn manifest_embedded_ee_cert_resources_validate() {
|
||||
let mft = load_manifest_fixture();
|
||||
mft.validate_embedded_ee_cert()
|
||||
.expect("manifest EE cert resources must validate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_ip_and_as_resources_missing() {
|
||||
let mft = load_manifest_fixture();
|
||||
let mut ee = mft.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.clone();
|
||||
ee.tbs.extensions.ip_resources = None;
|
||||
ee.tbs.extensions.as_resources = None;
|
||||
let err = mft.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, ManifestValidateError::EeResourcesMissing));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_ip_resources_not_inherit() {
|
||||
let mft = load_manifest_fixture();
|
||||
let mut ee = mft.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.clone();
|
||||
ee.tbs.extensions.ip_resources = Some(IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![]),
|
||||
}],
|
||||
});
|
||||
ee.tbs.extensions.as_resources = None;
|
||||
let err = mft.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestValidateError::EeIpResourcesNotInherit
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_when_as_rdi_present_or_asnum_not_inherit() {
|
||||
let mft = load_manifest_fixture();
|
||||
|
||||
// rdi present is rejected.
|
||||
let mut ee = mft.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.clone();
|
||||
ee.tbs.extensions.ip_resources = None;
|
||||
ee.tbs.extensions.as_resources = Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: Some(AsIdentifierChoice::Inherit),
|
||||
});
|
||||
let err = mft.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestValidateError::EeAsResourcesRdiPresent
|
||||
));
|
||||
|
||||
// asnum not inherit is rejected.
|
||||
let mut ee = mft.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.clone();
|
||||
ee.tbs.extensions.ip_resources = None;
|
||||
ee.tbs.extensions.as_resources = Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
64496,
|
||||
)])),
|
||||
rdi: None,
|
||||
});
|
||||
let err = mft.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ManifestValidateError::EeAsResourcesNotInherit
|
||||
));
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@ 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,
|
||||
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,
|
||||
EncapsulatedContentInfo, ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled,
|
||||
SignedDataProfiled, SignerInfoProfiled,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TrustAnchor};
|
||||
use rpki::data_model::tal::Tal;
|
||||
@ -199,7 +199,11 @@ impl From<&SignedDataProfiled> for SignedDataProfiledPretty {
|
||||
.map(ResourceEeCertificatePretty::from)
|
||||
.collect(),
|
||||
crls_present: v.crls_present,
|
||||
signer_infos: v.signer_infos.iter().map(SignerInfoProfiledPretty::from).collect(),
|
||||
signer_infos: v
|
||||
.signer_infos
|
||||
.iter()
|
||||
.map(SignerInfoProfiledPretty::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -434,6 +438,7 @@ fn print_all_models_from_real_fixtures() {
|
||||
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");
|
||||
println!("TA.verify_self_signature={:?}", ta.verify_self_signature());
|
||||
|
||||
let resolved = tal
|
||||
.ta_uris
|
||||
@ -442,15 +447,15 @@ fn print_all_models_from_real_fixtures() {
|
||||
.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");
|
||||
let trust_anchor =
|
||||
TrustAnchor::bind(tal, ta.clone(), 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";
|
||||
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");
|
||||
@ -458,14 +463,19 @@ fn print_all_models_from_real_fixtures() {
|
||||
|
||||
println!();
|
||||
println!("== Signed Object / Manifest ==");
|
||||
let mft_path =
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
|
||||
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!(
|
||||
"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 ==");
|
||||
@ -474,19 +484,30 @@ fn print_all_models_from_real_fixtures() {
|
||||
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!(
|
||||
"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";
|
||||
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!(
|
||||
"ASPA.validate_embedded_ee_cert={:?}",
|
||||
aspa_obj.validate_embedded_ee_cert()
|
||||
);
|
||||
println!(
|
||||
"ASPA.verify_signature={:?}",
|
||||
aspa_obj.signed_object.verify_signature()
|
||||
);
|
||||
|
||||
println!();
|
||||
println!("== CRL ==");
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
use rpki::data_model::rc::{ResourceCertificate, ResourceCertificateError};
|
||||
use rpki::data_model::rc::{
|
||||
ResourceCertificate, ResourceCertificateError, ResourceCertificateParseError,
|
||||
ResourceCertificateProfileError,
|
||||
};
|
||||
|
||||
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 _;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
STANDARD.decode(b64).unwrap()
|
||||
}
|
||||
|
||||
@ -44,7 +47,10 @@ 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)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResourceCertificateError::Parse(ResourceCertificateParseError::TrailingBytes(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -52,24 +58,41 @@ 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 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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResourceCertificateError::Validate(
|
||||
ResourceCertificateProfileError::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 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");
|
||||
assert!(
|
||||
n >= 2,
|
||||
"expected to patch at least inner+outer AlgorithmIdentifier"
|
||||
);
|
||||
let err = ResourceCertificate::from_der(&der).unwrap_err();
|
||||
assert!(matches!(err, ResourceCertificateError::UnsupportedSignatureAlgorithm));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResourceCertificateError::Validate(
|
||||
ResourceCertificateProfileError::UnsupportedSignatureAlgorithm
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -83,11 +106,15 @@ fn invalid_signature_algorithm_parameters_are_detected() {
|
||||
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");
|
||||
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
|
||||
ResourceCertificateError::Validate(
|
||||
ResourceCertificateProfileError::InvalidSignatureAlgorithmParameters
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,11 @@ fn resource_certificate_from_der_parses_ca_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.kind,
|
||||
ResourceCertKind::Ca,
|
||||
"fixture should be CA: {path}"
|
||||
);
|
||||
assert_eq!(rc.tbs.version, 2, "X.509 v3 encoded as 2: {path}");
|
||||
|
||||
assert_eq!(
|
||||
@ -23,21 +27,28 @@ fn resource_certificate_from_der_parses_ca_fixtures() {
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(rc.tbs.extensions.subject_info_access, Some(SubjectInfoAccess::Ca(_))),
|
||||
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}");
|
||||
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 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");
|
||||
assert!(
|
||||
rc.tbs.extensions.as_resources.is_some(),
|
||||
"fixture should carry AS resources"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange,
|
||||
IpPrefix,
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
|
||||
IpAddressOrRange, IpPrefix,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@ -40,17 +40,21 @@ fn as_resource_set_asnum_single_id_returns_expected_values() {
|
||||
assert_eq!(inherit.asnum_single_id(), None);
|
||||
|
||||
let single_id = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
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 {
|
||||
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range {
|
||||
min: 64512,
|
||||
max: 64520,
|
||||
}])),
|
||||
},
|
||||
])),
|
||||
};
|
||||
assert_eq!(single_range.asnum_single_id(), None);
|
||||
|
||||
@ -68,7 +72,9 @@ fn as_resource_set_asnum_single_id_returns_expected_values() {
|
||||
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());
|
||||
assert!(
|
||||
AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 2 }]).has_range()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -81,13 +87,16 @@ fn ip_resource_contains_prefix_finds_matching_family() {
|
||||
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 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],
|
||||
addr: vec![
|
||||
0x20, 0x01, 0x0d, 0xb8, 0x12, 0x34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
],
|
||||
};
|
||||
assert!(set.contains_prefix(&p));
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix, IpResourceSet,
|
||||
Afi, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange, IpPrefix,
|
||||
IpResourceSet,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@ -7,10 +8,12 @@ fn ip_resource_range_covers_prefix_ipv4() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(
|
||||
IpAddressRange {
|
||||
min: vec![10, 0, 0, 0],
|
||||
max: vec![10, 255, 255, 255],
|
||||
})]),
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
};
|
||||
|
||||
@ -34,10 +37,15 @@ fn ip_resource_range_covers_prefix_ipv6() {
|
||||
let set = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Range(IpAddressRange {
|
||||
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],
|
||||
})]),
|
||||
max: vec![
|
||||
0x20, 0x01, 0x0d, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff,
|
||||
],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
};
|
||||
|
||||
@ -55,4 +63,3 @@ fn ip_resource_range_covers_prefix_ipv6() {
|
||||
};
|
||||
assert!(!set.contains_prefix(&outside));
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use rpki::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsIdOrRange, AsResourceSet, IpAddressChoice, IpAddressOrRange,
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressOrRange,
|
||||
IpResourceSet,
|
||||
};
|
||||
|
||||
@ -100,7 +100,8 @@ fn ip_addr_blocks_decode_handles_inherit_and_range_endpoint_fill() {
|
||||
|
||||
// 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");
|
||||
let decoded =
|
||||
IpResourceSet::decode_extn_value(&der_sequence(vec![fam_inherit])).expect("decode inherit");
|
||||
assert!(decoded.is_all_inherit());
|
||||
}
|
||||
|
||||
@ -125,11 +126,17 @@ fn autonomous_sys_ids_decode_handles_inherit_ids_and_ranges() {
|
||||
};
|
||||
assert_eq!(items.len(), 2);
|
||||
assert!(matches!(items[0], AsIdOrRange::Id(64496)));
|
||||
assert!(matches!(items[1], AsIdOrRange::Range { min: 64500, max: 64510 }));
|
||||
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");
|
||||
let decoded = AsResourceSet::decode_extn_value(&der_sequence(vec![asnum_inherit]))
|
||||
.expect("decode inherit");
|
||||
assert!(decoded.is_asnum_inherit());
|
||||
}
|
||||
|
||||
|
||||
@ -93,10 +93,7 @@ fn ip_addr_blocks_decode_rejects_invalid_encodings() {
|
||||
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),
|
||||
]);
|
||||
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).
|
||||
@ -188,8 +185,13 @@ fn ip_addr_blocks_range_upper_bound_can_fill_all_ones_when_bit_len_zero() {
|
||||
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 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");
|
||||
};
|
||||
|
||||
@ -124,4 +124,3 @@ fn canonicalize_sorts_families_sorts_and_dedups_addresses() {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaObject};
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaObject, RoaProfileError};
|
||||
use rpki::data_model::signed_object::RpkiSignedObject;
|
||||
|
||||
#[test]
|
||||
fn decode_roa_fixture_smoke() {
|
||||
let der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
||||
)
|
||||
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_profile().expect("validate ROA profile");
|
||||
assert_eq!(
|
||||
roa.econtent_type,
|
||||
rpki::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ
|
||||
@ -14,14 +14,24 @@ fn decode_roa_fixture_smoke() {
|
||||
assert_eq!(roa.roa.version, 0);
|
||||
assert_eq!(roa.roa.as_id, 4538);
|
||||
assert!(!roa.roa.ip_addr_blocks.is_empty());
|
||||
assert!(roa
|
||||
.roa
|
||||
assert!(
|
||||
roa.roa
|
||||
.ip_addr_blocks
|
||||
.iter()
|
||||
.all(|f| !f.addresses.is_empty()));
|
||||
.all(|f| !f.addresses.is_empty())
|
||||
);
|
||||
println!("{roa:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_signed_object_accepts_roa_fixture() {
|
||||
let der = std::fs::read("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa")
|
||||
.expect("read ROA fixture");
|
||||
let so = RpkiSignedObject::decode_der(&der).expect("decode signed object");
|
||||
let roa = RoaObject::from_signed_object(so).expect("from_signed_object");
|
||||
roa.validate_profile().expect("validate ROA profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_non_roa_econtent_type() {
|
||||
let der = std::fs::read(
|
||||
@ -29,5 +39,8 @@ fn decode_rejects_non_roa_econtent_type() {
|
||||
)
|
||||
.expect("read MFT fixture");
|
||||
let err = RoaObject::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidEContentType(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidEContentType(_))
|
||||
));
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaEContent};
|
||||
use rpki::data_model::roa::{RoaDecodeError, RoaEContent, RoaParseError, RoaProfileError};
|
||||
|
||||
fn len_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
@ -86,15 +86,184 @@ fn roa_attestation(version: Option<u64>, as_id: u64, families: Vec<Vec<u8>>) ->
|
||||
der_sequence(fields)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_bytes_are_rejected_in_parse_step() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family(
|
||||
[0, 1],
|
||||
vec![roa_ip_address(der_bit_string(0, &[]), None)],
|
||||
)],
|
||||
);
|
||||
let mut bad = der.clone();
|
||||
bad.push(0);
|
||||
let err = RoaEContent::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Parse(RoaParseError::TrailingBytes(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attestation_sequence_len_is_validated() {
|
||||
let der = der_sequence(vec![der_integer_u64(64496)]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidAttestationSequenceLen(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_tag_must_be_context_specific_0() {
|
||||
let der = {
|
||||
let mut fields = Vec::new();
|
||||
fields.push(cs_explicit(1, der_integer_u64(0))); // wrong tag number [1]
|
||||
fields.push(der_integer_u64(64496));
|
||||
fields.push(der_sequence(vec![roa_ip_family(
|
||||
[0, 1],
|
||||
vec![roa_ip_address(der_bit_string(0, &[]), None)],
|
||||
)]));
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::ProfileDecode(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_explicit_tag_rejects_trailing_bytes_inside_inner_der() {
|
||||
let mut version_inner = der_integer_u64(0);
|
||||
version_inner.extend(tlv(0x05, &[])); // NULL after INTEGER
|
||||
|
||||
let der = {
|
||||
let mut fields = Vec::new();
|
||||
fields.push(cs_explicit(0, version_inner));
|
||||
fields.push(der_integer_u64(64496));
|
||||
fields.push(der_sequence(vec![roa_ip_family(
|
||||
[0, 1],
|
||||
vec![roa_ip_address(der_bit_string(0, &[]), None)],
|
||||
)]));
|
||||
der_sequence(fields)
|
||||
};
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::ProfileDecode(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_zero_is_accepted_when_explicitly_encoded() {
|
||||
let der = roa_attestation(
|
||||
Some(0),
|
||||
64496,
|
||||
vec![roa_ip_family(
|
||||
[0, 1],
|
||||
vec![roa_ip_address(der_bit_string(7, &[0x80]), None)], // prefix_len=1
|
||||
)],
|
||||
);
|
||||
let roa = RoaEContent::decode_der(&der).expect("ROA must decode");
|
||||
assert_eq!(roa.version, 0);
|
||||
assert_eq!(roa.as_id, 64496);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_address_family_shape_and_address_family_length_are_validated() {
|
||||
// family SEQUENCE has wrong length (1)
|
||||
let bad_family = der_sequence(vec![der_octet_string(&[0, 1])]);
|
||||
let der = roa_attestation(None, 64496, vec![bad_family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidIpAddressFamily)
|
||||
));
|
||||
|
||||
// addressFamily OCTET STRING has invalid length (3)
|
||||
let family = der_sequence(vec![
|
||||
der_octet_string(&[0, 1, 2]),
|
||||
der_sequence(vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
]);
|
||||
let der = roa_attestation(None, 64496, vec![family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidAddressFamily)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roa_ip_address_shape_and_prefix_encoding_are_validated() {
|
||||
// ROAIPAddress must have 1..2 elements.
|
||||
let family = roa_ip_family([0, 1], vec![der_sequence(vec![])]);
|
||||
let der = roa_attestation(None, 64496, vec![family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidRoaIpAddress)
|
||||
));
|
||||
|
||||
// ROAIPAddress.address must be BIT STRING, not OCTET STRING.
|
||||
let family = roa_ip_family([0, 1], vec![der_sequence(vec![der_octet_string(&[0])])]);
|
||||
let der = roa_attestation(None, 64496, vec![family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidPrefixBitString)
|
||||
));
|
||||
|
||||
// unusedBits > 7 is rejected.
|
||||
let family = roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(8, &[0]), None)]);
|
||||
let der = roa_attestation(None, 64496, vec![family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidPrefixUnusedBits)
|
||||
));
|
||||
|
||||
// empty BIT STRING with unusedBits != 0 is rejected.
|
||||
let family = roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(1, &[]), None)]);
|
||||
let der = roa_attestation(None, 64496, vec![family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidPrefixUnusedBits)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_length_integer_range_is_validated() {
|
||||
// maxLength too large to fit u16 triggers the try_into() error path.
|
||||
let family = roa_ip_family(
|
||||
[0, 1],
|
||||
vec![roa_ip_address(der_bit_string(0, &[0x0A]), Some(70000))],
|
||||
);
|
||||
let der = roa_attestation(None, 64496, vec![family]);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidMaxLength { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[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)])],
|
||||
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)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidVersion(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -102,17 +271,26 @@ 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)])],
|
||||
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(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::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)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidIpAddrBlocksLen(0))
|
||||
));
|
||||
|
||||
let families = vec![
|
||||
roa_ip_family([0, 1], vec![roa_ip_address(der_bit_string(0, &[]), None)]),
|
||||
@ -121,7 +299,10 @@ fn ip_addr_blocks_len_is_validated() {
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::InvalidIpAddrBlocksLen(3)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidIpAddrBlocksLen(3))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -132,7 +313,10 @@ fn duplicate_afi_is_rejected() {
|
||||
];
|
||||
let der = roa_attestation(None, 64496, families);
|
||||
let err = RoaEContent::decode_der(&der).unwrap_err();
|
||||
assert!(matches!(err, RoaDecodeError::DuplicateAfi(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::DuplicateAfi(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -140,17 +324,26 @@ 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)])],
|
||||
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(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::EmptyAddressList)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -164,7 +357,10 @@ fn prefix_unused_bits_must_be_zeroed() {
|
||||
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));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidPrefixUnusedBits)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -177,7 +373,10 @@ fn prefix_len_out_of_range_is_rejected() {
|
||||
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 { .. }));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::PrefixLenOutOfRange { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -188,10 +387,16 @@ fn max_length_range_and_relation_are_validated() {
|
||||
let der = roa_attestation(
|
||||
None,
|
||||
64496,
|
||||
vec![roa_ip_family([0, 1], vec![roa_ip_address(bs.clone(), Some(7))])],
|
||||
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 { .. }));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidMaxLength { .. })
|
||||
));
|
||||
|
||||
// maxLength > ub
|
||||
let der = roa_attestation(
|
||||
@ -200,6 +405,8 @@ fn max_length_range_and_relation_are_validated() {
|
||||
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 { .. }));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaDecodeError::Validate(RoaProfileError::InvalidMaxLength { .. })
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,7 @@ 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",
|
||||
)
|
||||
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");
|
||||
|
||||
@ -2,12 +2,18 @@ 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,
|
||||
Afi, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, IpPrefix,
|
||||
IpResourceSet, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
SubjectInfoAccess,
|
||||
};
|
||||
use rpki::data_model::roa::{
|
||||
RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError,
|
||||
};
|
||||
use rpki::data_model::roa::{RoaAfi, RoaEContent, RoaIpAddress, RoaIpAddressFamily, RoaValidateError};
|
||||
|
||||
fn dummy_ee(ip_resources: Option<IpResourceSet>, as_resources: Option<AsResourceSet>) -> ResourceCertificate {
|
||||
fn dummy_ee(
|
||||
ip_resources: Option<IpResourceSet>,
|
||||
as_resources: Option<AsResourceSet>,
|
||||
) -> ResourceCertificate {
|
||||
ResourceCertificate {
|
||||
raw_der: vec![],
|
||||
tbs: RpkixTbsCertificate {
|
||||
@ -133,7 +139,10 @@ fn validate_rejects_when_prefix_not_covered() {
|
||||
None,
|
||||
);
|
||||
let err = roa.validate_against_ee_cert(&ee).unwrap_err();
|
||||
assert!(matches!(err, RoaValidateError::PrefixNotInEeResources { .. }));
|
||||
assert!(matches!(
|
||||
err,
|
||||
RoaValidateError::PrefixNotInEeResources { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -173,4 +182,3 @@ fn contains_prefix_handles_non_octet_boundary_prefix_len() {
|
||||
roa.validate_against_ee_cert(&ee)
|
||||
.expect("160.18.0.0/16 should be covered by 160.0.0.0/9");
|
||||
}
|
||||
|
||||
|
||||
@ -2,13 +2,10 @@ 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",
|
||||
)
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -12,19 +12,26 @@ fn decode_manifest_signed_object_smoke() {
|
||||
|
||||
assert_eq!(so.content_info_content_type, OID_SIGNED_DATA);
|
||||
assert_eq!(so.signed_data.version, 3);
|
||||
assert_eq!(so.signed_data.digest_algorithms, vec![OID_SHA256.to_string()]);
|
||||
assert_eq!(
|
||||
so.signed_data.digest_algorithms,
|
||||
vec![OID_SHA256.to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
so.signed_data.encap_content_info.econtent_type,
|
||||
OID_CT_RPKI_MANIFEST
|
||||
);
|
||||
assert_eq!(so.signed_data.certificates.len(), 1);
|
||||
assert!(!so.signed_data.certificates[0]
|
||||
assert!(
|
||||
!so.signed_data.certificates[0]
|
||||
.sia_signed_object_uris
|
||||
.is_empty());
|
||||
assert!(so.signed_data.certificates[0]
|
||||
.is_empty()
|
||||
);
|
||||
assert!(
|
||||
so.signed_data.certificates[0]
|
||||
.sia_signed_object_uris
|
||||
.iter()
|
||||
.any(|u| u.starts_with("rsync://")));
|
||||
.any(|u| u.starts_with("rsync://"))
|
||||
);
|
||||
assert_eq!(so.signed_data.signer_infos.len(), 1);
|
||||
println!("{so:#?}")
|
||||
}
|
||||
|
||||
@ -2,7 +2,9 @@ use rpki::data_model::oid::{
|
||||
OID_CMS_ATTR_CONTENT_TYPE, OID_CMS_ATTR_MESSAGE_DIGEST, OID_CMS_ATTR_SIGNING_TIME,
|
||||
OID_RSA_ENCRYPTION, OID_SHA256, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SIGNED_DATA,
|
||||
};
|
||||
use rpki::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||
use rpki::data_model::signed_object::{
|
||||
RpkiSignedObject, SignedObjectDecodeError, SignedObjectParseError, SignedObjectValidateError,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
use x509_parser::prelude::FromDer;
|
||||
@ -14,7 +16,7 @@ const TEST_SIA_HTTPS_CERT_DER_B64: &str = "MIIDODCCAiCgAwIBAgIUBp2fsJYhUBJk711xT
|
||||
const TEST_SIA_DNS_CERT_DER_B64: &str = "MIIDHjCCAgagAwIBAgIUJlS9d2BCJaamLyjYVTjvMNzWDxMwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMVGVzdCBTSUEgZG5zMB4XDTI2MDEyNzA2NTMwOVoXDTM2MDEyNTA2NTMwOVowFzEVMBMGA1UEAwwMVGVzdCBTSUEgZG5zMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnKIddAQzQpDLbM+ZX3qR707L7CZvZ3MDGYN+tvuPXOfhATcOLtEaxu1dK4ZhV4Ou3ZqdxwYauyC+N4An0qCJW8mVr3zhbxathVGW7w4/S9pEV/+8dGW8ypOiqNixtmV++Ww54PguD6uxMk1S3IUOVTJY+QaetMy+SV9lCbOykZys17J56tMBmHRtuOxGPnaLtzZLddWqGhGFSDthSbKX4yToUIhTUl+wIRRYjBjnbGgzH5jV6eHUgrHRk+n567jNa9fe3cuRCGNBe6ny/8NPQnJEksWpA9lGfJDlEFsDIM9cXY78izr6i4JHeErwfusJiSchTT0ePhHXRAYMQoIqywIDAQABo2IwYDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUaY5MXriJ8s5VVa+s09HoSUloKBUwJwYIKwYBBQUHAQsEGzAZMBcGCCsGAQUFBzALggtleGFtcGxlLm5ldDANBgkqhkiG9w0BAQsFAAOCAQEAZSnkWxPTFWepHaK0XvAV145idY0ztEqY9BWUql2Ythzb4rjBAU1TfDRRklnnlE9o9/I6363ltaZBvj95e3CyTu4YGflxEpHsW+4aTth8ty1ee7YSqsdJ8gN08sroIpMTfr6tvWf65cVLSTkB4yP8cnNEM3zGr37zb32ChPXgUFwS9JFf3SMsXudZ4rHougE/PM4pQZvaOl3tFEzohV5MjA2VD38n3y6bVmx3i0Xqze7UZnl06aDKozzTXmFy/DoDRGG2pd2EjoC8gNAqIOL53uRz5nJlp8WEIBMe5Hmokrzv+zkAywVZZtYo1FvonOdg5etH94oMnZEtgV/OO9joRg==";
|
||||
|
||||
fn decode_b64(b64: &str) -> Vec<u8> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
general_purpose::STANDARD
|
||||
.decode(b64)
|
||||
.expect("decode base64 cert")
|
||||
@ -99,8 +101,8 @@ fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
|
||||
}
|
||||
|
||||
fn der_oid(oid: &str) -> Vec<u8> {
|
||||
use std::str::FromStr;
|
||||
use der_parser::asn1_rs::ToDer;
|
||||
use std::str::FromStr;
|
||||
|
||||
let oid = der_parser::Oid::from_str(oid).unwrap();
|
||||
oid.to_der_vec().unwrap()
|
||||
@ -156,7 +158,10 @@ fn signed_attrs_implicit(
|
||||
) -> Vec<u8> {
|
||||
let mut attrs = vec![
|
||||
cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(content_type_oid)),
|
||||
cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(message_digest)),
|
||||
cms_attribute(
|
||||
OID_CMS_ATTR_MESSAGE_DIGEST,
|
||||
der_octet_string(message_digest),
|
||||
),
|
||||
cms_attribute(OID_CMS_ATTR_SIGNING_TIME, signing_time_der),
|
||||
];
|
||||
attrs.extend(extra_attrs);
|
||||
@ -278,7 +283,10 @@ fn trailing_bytes_after_object() {
|
||||
let mut bad = der.clone();
|
||||
bad.push(0);
|
||||
let err = RpkiSignedObject::decode_der(&bad).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::TrailingBytes(1)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Parse(SignedObjectParseError::TrailingBytes(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -297,7 +305,14 @@ fn content_info_content_must_be_tag0_explicit() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
);
|
||||
let ci = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(1, &sd)]);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
@ -313,7 +328,14 @@ fn content_info_content_must_contain_only_one_inner_object() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
);
|
||||
let mut inner = sd.clone();
|
||||
inner.extend(der_null());
|
||||
@ -414,7 +436,14 @@ fn signed_data_unexpected_field_is_rejected() {
|
||||
cs_cons(0, &der_octet_string(b"e")),
|
||||
]),
|
||||
der_null(),
|
||||
der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]),
|
||||
der_set(vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)]),
|
||||
]);
|
||||
let ci = content_info_signed_data(sd);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
@ -433,7 +462,14 @@ fn encap_content_info_length_and_tags_are_validated() {
|
||||
der_integer_u64(3),
|
||||
der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]),
|
||||
encap,
|
||||
der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]),
|
||||
der_set(vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)]),
|
||||
]);
|
||||
let ci = content_info_signed_data(sd);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
@ -448,7 +484,14 @@ fn encap_content_info_length_and_tags_are_validated() {
|
||||
der_integer_u64(3),
|
||||
der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]),
|
||||
encap,
|
||||
der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]),
|
||||
der_set(vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)]),
|
||||
]);
|
||||
let ci = content_info_signed_data(sd);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
@ -465,7 +508,14 @@ fn encap_content_info_length_and_tags_are_validated() {
|
||||
der_integer_u64(3),
|
||||
der_set(vec![algorithm_id(OID_SHA256, Some(der_null()))]),
|
||||
encap,
|
||||
der_set(vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)]),
|
||||
der_set(vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)]),
|
||||
]);
|
||||
let ci = content_info_signed_data(sd);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
@ -501,7 +551,10 @@ fn empty_econtent_is_rejected() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::EContentMissing));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EContentMissing)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -555,7 +608,10 @@ fn signer_info_sequence_len_and_version_are_validated() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::InvalidSignerInfoVersion(1)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignerInfoVersion(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -585,7 +641,10 @@ fn signed_attrs_structure_and_presence_are_validated() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::Parse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsParse(_))
|
||||
));
|
||||
|
||||
// Missing content-type.
|
||||
let signed_attrs = signed_attrs_raw(vec![
|
||||
@ -610,11 +669,17 @@ fn signed_attrs_structure_and_presence_are_validated() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::Parse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsContentTypeMissing)
|
||||
));
|
||||
|
||||
// Missing message-digest.
|
||||
let signed_attrs = signed_attrs_raw(vec![
|
||||
cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)),
|
||||
cms_attribute(
|
||||
OID_CMS_ATTR_CONTENT_TYPE,
|
||||
der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST),
|
||||
),
|
||||
cms_attribute(OID_CMS_ATTR_SIGNING_TIME, tlv(0x17, b"240101000000Z")),
|
||||
]);
|
||||
let si = signer_info(
|
||||
@ -635,11 +700,19 @@ fn signed_attrs_structure_and_presence_are_validated() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::Parse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::SignedAttrsMessageDigestMissing
|
||||
)
|
||||
));
|
||||
|
||||
// Missing signing-time.
|
||||
let signed_attrs = signed_attrs_raw(vec![
|
||||
cms_attribute(OID_CMS_ATTR_CONTENT_TYPE, der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST)),
|
||||
cms_attribute(
|
||||
OID_CMS_ATTR_CONTENT_TYPE,
|
||||
der_oid(rpki::data_model::oid::OID_CT_RPKI_MANIFEST),
|
||||
),
|
||||
cms_attribute(OID_CMS_ATTR_MESSAGE_DIGEST, der_octet_string(&digest)),
|
||||
]);
|
||||
let si = signer_info(
|
||||
@ -660,7 +733,10 @@ fn signed_attrs_structure_and_presence_are_validated() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::Parse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsSigningTimeMissing)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -672,7 +748,14 @@ fn algorithm_identifier_sequence_shape_is_validated() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
);
|
||||
let ci = content_info_signed_data(sd);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
@ -713,8 +796,10 @@ fn ee_certificate_missing_signed_object_sia_is_rejected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::EeCertificateMissingSia
|
||||
| SignedObjectDecodeError::EeCertificateMissingSignedObjectSia
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateMissingSia)
|
||||
| SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::EeCertificateMissingSignedObjectSia
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@ -751,7 +836,9 @@ fn ee_certificate_sia_without_signed_object_access_method_is_rejected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::EeCertificateMissingSignedObjectSia
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::EeCertificateMissingSignedObjectSia
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@ -788,7 +875,9 @@ fn ee_certificate_signed_object_sia_must_be_uri() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::EeCertificateSignedObjectSiaNotUri
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@ -825,7 +914,9 @@ fn ee_certificate_signed_object_sia_requires_rsync_uri() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::EeCertificateSignedObjectSiaNoRsync
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@ -867,7 +958,7 @@ fn signed_attrs_duplicate_content_type_is_rejected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::DuplicateSignedAttribute(ref oid)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::DuplicateSignedAttribute(ref oid))
|
||||
if oid == OID_CMS_ATTR_CONTENT_TYPE
|
||||
));
|
||||
}
|
||||
@ -907,19 +998,26 @@ fn signed_attrs_duplicate_signing_time_is_rejected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::DuplicateSignedAttribute(ref oid)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::DuplicateSignedAttribute(ref oid))
|
||||
if oid == OID_CMS_ATTR_SIGNING_TIME
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_content_info_content_type() {
|
||||
let sd = der_sequence(vec![der_integer_u64(3), der_set(vec![]), der_sequence(vec![der_oid("1.2.3")]) , der_set(vec![])]);
|
||||
let sd = der_sequence(vec![
|
||||
der_integer_u64(3),
|
||||
der_set(vec![]),
|
||||
der_sequence(vec![der_oid("1.2.3")]),
|
||||
der_set(vec![]),
|
||||
]);
|
||||
let ci = der_sequence(vec![der_oid("1.2.3.4"), cs_cons(0, &sd)]);
|
||||
let err = RpkiSignedObject::decode_der(&ci).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidContentInfoContentType(_)
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::InvalidContentInfoContentType(_)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@ -932,12 +1030,19 @@ fn invalid_signed_data_version() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSignedDataVersion(4)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignedDataVersion(4))
|
||||
));
|
||||
}
|
||||
|
||||
@ -953,12 +1058,21 @@ fn invalid_digest_algorithms_count() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidDigestAlgorithmsCount(2)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidDigestAlgorithmsCount(
|
||||
2
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
@ -971,12 +1085,19 @@ fn invalid_digest_algorithm_oid() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidDigestAlgorithm(_)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidDigestAlgorithm(_))
|
||||
));
|
||||
}
|
||||
|
||||
@ -991,7 +1112,10 @@ fn econtent_missing() {
|
||||
]);
|
||||
let so = der_sequence(vec![der_oid(OID_SIGNED_DATA), cs_cons(0, &sd)]);
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::EContentMissing));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EContentMissing)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1003,10 +1127,20 @@ fn crls_present_is_rejected() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
true,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::CrlsPresent));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::CrlsPresent)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1018,10 +1152,20 @@ fn certificates_missing_is_rejected() {
|
||||
b"e".to_vec(),
|
||||
None,
|
||||
false,
|
||||
vec![signer_info(vec![1], OID_SHA256, None, OID_RSA_ENCRYPTION, Some(der_null()), false)],
|
||||
vec![signer_info(
|
||||
vec![1],
|
||||
OID_SHA256,
|
||||
None,
|
||||
OID_RSA_ENCRYPTION,
|
||||
Some(der_null()),
|
||||
false,
|
||||
)],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::CertificatesMissing));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::CertificatesMissing)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1057,7 +1201,7 @@ fn invalid_certificates_count_rejected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidCertificatesCount(2)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidCertificatesCount(2))
|
||||
));
|
||||
}
|
||||
|
||||
@ -1091,7 +1235,10 @@ fn ee_certificate_parse_error_is_reported() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::EeCertificateParse(_)));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateParse(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1133,7 +1280,7 @@ fn invalid_signer_infos_count_rejected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSignerInfosCount(2)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignerInfosCount(2))
|
||||
));
|
||||
}
|
||||
|
||||
@ -1162,7 +1309,10 @@ fn signer_info_errors_are_detected() {
|
||||
vec![si_no_attrs],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::SignedAttrsMissing));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::SignedAttrsMissing)
|
||||
));
|
||||
|
||||
// Invalid signer identifier.
|
||||
let signed_attrs = signed_attrs_implicit(
|
||||
@ -1189,7 +1339,10 @@ fn signer_info_errors_are_detected() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::InvalidSignerIdentifier));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignerIdentifier)
|
||||
));
|
||||
|
||||
// Invalid digest algorithm.
|
||||
let signed_attrs = signed_attrs_implicit(
|
||||
@ -1218,7 +1371,9 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm(_)
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::InvalidSignerInfoDigestAlgorithm(_)
|
||||
)
|
||||
));
|
||||
|
||||
// Invalid signature algorithm OID.
|
||||
@ -1248,7 +1403,7 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSignatureAlgorithm(_)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSignatureAlgorithm(_))
|
||||
));
|
||||
|
||||
// Invalid signing-time value.
|
||||
@ -1278,7 +1433,7 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSigningTimeValue
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::InvalidSigningTimeValue)
|
||||
));
|
||||
|
||||
// signedAttrs has duplicate attribute.
|
||||
@ -1309,7 +1464,7 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::DuplicateSignedAttribute(_)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::DuplicateSignedAttribute(_))
|
||||
));
|
||||
|
||||
// signedAttrs attrValues count != 1.
|
||||
@ -1346,16 +1501,14 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSignedAttributeValuesCount { .. }
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::InvalidSignedAttributeValuesCount { .. }
|
||||
)
|
||||
));
|
||||
|
||||
// content-type mismatch.
|
||||
let signed_attrs = signed_attrs_implicit(
|
||||
"1.2.3.4",
|
||||
&digest,
|
||||
tlv(0x17, b"240101000000Z"),
|
||||
vec![],
|
||||
);
|
||||
let signed_attrs =
|
||||
signed_attrs_implicit("1.2.3.4", &digest, tlv(0x17, b"240101000000Z"), vec![]);
|
||||
let si = signer_info(
|
||||
cert_ski.clone(),
|
||||
OID_SHA256,
|
||||
@ -1376,7 +1529,9 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::ContentTypeAttrMismatch { .. }
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::ContentTypeAttrMismatch { .. }
|
||||
)
|
||||
));
|
||||
|
||||
// message-digest mismatch.
|
||||
@ -1404,7 +1559,10 @@ fn signer_info_errors_are_detected() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::MessageDigestMismatch));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::MessageDigestMismatch)
|
||||
));
|
||||
|
||||
// sid_ski mismatch.
|
||||
let signed_attrs = signed_attrs_implicit(
|
||||
@ -1431,7 +1589,10 @@ fn signer_info_errors_are_detected() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::SidSkiMismatch));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::SidSkiMismatch)
|
||||
));
|
||||
|
||||
// Unsupported signedAttrs attribute.
|
||||
let bad_attr = cms_attribute("1.2.3.4", der_null());
|
||||
@ -1461,7 +1622,7 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::UnsupportedSignedAttribute(_)
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::UnsupportedSignedAttribute(_))
|
||||
));
|
||||
|
||||
// signatureAlgorithm parameters invalid.
|
||||
@ -1491,7 +1652,9 @@ fn signer_info_errors_are_detected() {
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::InvalidSignatureAlgorithmParameters
|
||||
SignedObjectDecodeError::Validate(
|
||||
SignedObjectValidateError::InvalidSignatureAlgorithmParameters
|
||||
)
|
||||
));
|
||||
|
||||
// unsignedAttrs present.
|
||||
@ -1519,5 +1682,8 @@ fn signer_info_errors_are_detected() {
|
||||
vec![si],
|
||||
));
|
||||
let err = RpkiSignedObject::decode_der(&so).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectDecodeError::UnsignedAttrsPresent));
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectDecodeError::Validate(SignedObjectValidateError::UnsignedAttrsPresent)
|
||||
));
|
||||
}
|
||||
|
||||
@ -53,8 +53,8 @@ fn der_null() -> Vec<u8> {
|
||||
}
|
||||
|
||||
fn der_oid(oid: &str) -> Vec<u8> {
|
||||
use std::str::FromStr;
|
||||
use der_parser::asn1_rs::ToDer;
|
||||
use std::str::FromStr;
|
||||
let oid = der_parser::Oid::from_str(oid).unwrap();
|
||||
oid.to_der_vec().unwrap()
|
||||
}
|
||||
@ -140,10 +140,11 @@ fn verify_rejects_spki_der_with_trailing_bytes() {
|
||||
|
||||
let mut spki_der = so.signed_data.certificates[0].spki_der.clone();
|
||||
spki_der.push(0);
|
||||
let err = so
|
||||
.verify_signature_with_ee_spki_der(&spki_der)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, SignedObjectVerifyError::EeSpkiTrailingBytes(1)));
|
||||
let err = so.verify_signature_with_ee_spki_der(&spki_der).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
SignedObjectVerifyError::EeSpkiTrailingBytes(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -157,8 +158,6 @@ fn verify_with_all_zero_modulus_exercises_strip_leading_zeros_fallback() {
|
||||
// modulus INTEGER 0, exponent 65537 (valid exponent encoding); signature verification must fail
|
||||
// but the SPKI parsing path should succeed.
|
||||
let spki_der = rsa_spki_der_with_modulus_bytes(&[0x00], 65537);
|
||||
let err = so
|
||||
.verify_signature_with_ee_spki_der(&spki_der)
|
||||
.unwrap_err();
|
||||
let err = so.verify_signature_with_ee_spki_der(&spki_der).unwrap_err();
|
||||
assert!(matches!(err, SignedObjectVerifyError::InvalidSignature));
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
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,
|
||||
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
|
||||
IpAddressOrRange, IpPrefix, RcExtensions, ResourceCertKind, ResourceCertificate,
|
||||
RpkixTbsCertificate,
|
||||
};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateError};
|
||||
use rpki::data_model::ta::{TaCertificate, TaCertificateDecodeError, TaCertificateProfileError};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
fn dummy_rc_ca(ext: RcExtensions) -> ResourceCertificate {
|
||||
@ -56,8 +57,9 @@ fn ta_certificate_rejects_non_self_signed_ca() {
|
||||
.expect("read CA cert fixture");
|
||||
assert!(matches!(
|
||||
TaCertificate::from_der(&der),
|
||||
Err(TaCertificateError::NotSelfSignedIssuerSubject)
|
||||
| Err(TaCertificateError::InvalidSelfSignature(_))
|
||||
Err(TaCertificateDecodeError::Validate(
|
||||
TaCertificateProfileError::NotSelfSignedIssuerSubject
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
@ -73,7 +75,7 @@ fn ta_constraints_require_policies_and_ski() {
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingOrInvalidCertificatePolicies)
|
||||
Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies)
|
||||
));
|
||||
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
@ -82,7 +84,7 @@ fn ta_constraints_require_policies_and_ski() {
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::MissingSubjectKeyIdentifier)
|
||||
Err(TaCertificateProfileError::MissingSubjectKeyIdentifier)
|
||||
));
|
||||
}
|
||||
|
||||
@ -99,7 +101,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesMissing)
|
||||
Err(TaCertificateProfileError::ResourcesMissing)
|
||||
));
|
||||
|
||||
// IP resources present but empty => resources empty.
|
||||
@ -109,7 +111,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::ResourcesEmpty)
|
||||
Err(TaCertificateProfileError::ResourcesEmpty)
|
||||
));
|
||||
|
||||
// IP resources inherit is rejected.
|
||||
@ -124,7 +126,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::IpResourcesInherit)
|
||||
Err(TaCertificateProfileError::IpResourcesInherit)
|
||||
));
|
||||
|
||||
// AS resources inherit is rejected.
|
||||
@ -138,7 +140,7 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
});
|
||||
assert!(matches!(
|
||||
TaCertificate::validate_rc_constraints(&rc),
|
||||
Err(TaCertificateError::AsResourcesInherit)
|
||||
Err(TaCertificateProfileError::AsResourcesInherit)
|
||||
));
|
||||
|
||||
// Valid non-empty explicit IP resources => OK.
|
||||
@ -146,11 +148,13 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
ip_resources: Some(rpki::data_model::rc::IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv6,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(IpPrefix {
|
||||
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,
|
||||
@ -162,11 +166,12 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
|
||||
let rc = dummy_rc_ca(RcExtensions {
|
||||
ip_resources: None,
|
||||
as_resources: Some(AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(64512)])),
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
64512,
|
||||
)])),
|
||||
rdi: None,
|
||||
}),
|
||||
..rc.tbs.extensions.clone()
|
||||
});
|
||||
TaCertificate::validate_rc_constraints(&rc).expect("valid explicit AS resources");
|
||||
}
|
||||
|
||||
|
||||
@ -15,17 +15,31 @@ fn decode_tal_fixtures_smoke() {
|
||||
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}");
|
||||
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}");
|
||||
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}");
|
||||
assert!(
|
||||
rem.is_empty(),
|
||||
"SPKI DER must not have trailing bytes: {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use rpki::data_model::tal::{Tal, TalDecodeError};
|
||||
use rpki::data_model::tal::{Tal, TalDecodeError, TalParseError, TalProfileError};
|
||||
|
||||
fn mk_tal(uris: &[&str], b64_lines: &[&str]) -> String {
|
||||
let mut out = String::new();
|
||||
@ -20,14 +20,19 @@ 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)
|
||||
Err(TalDecodeError::Validate(
|
||||
TalProfileError::MissingSeparatorEmptyLine
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_missing_uris() {
|
||||
let s = "# c\n\nAAAA\n";
|
||||
assert!(matches!(Tal::decode_bytes(s.as_bytes()), Err(TalDecodeError::MissingTaUris)));
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::Validate(TalProfileError::MissingTaUris))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -35,7 +40,9 @@ 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(_))
|
||||
Err(TalDecodeError::Validate(
|
||||
TalProfileError::UnsupportedUriScheme(_)
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
@ -44,14 +51,19 @@ 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(_))
|
||||
Err(TalDecodeError::Validate(TalProfileError::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)));
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(s.as_bytes()),
|
||||
Err(TalDecodeError::Validate(
|
||||
TalProfileError::CommentAfterHeader
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -59,13 +71,15 @@ 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)
|
||||
Err(TalDecodeError::Validate(TalProfileError::SpkiBase64Decode))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tal_rejects_invalid_utf8() {
|
||||
let bytes = [0xFFu8, 0xFEu8];
|
||||
assert!(matches!(Tal::decode_bytes(&bytes), Err(TalDecodeError::InvalidUtf8)));
|
||||
assert!(matches!(
|
||||
Tal::decode_bytes(&bytes),
|
||||
Err(TalDecodeError::Parse(TalParseError::InvalidUtf8))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,30 @@
|
||||
use rpki::data_model::ta::{TrustAnchor, TrustAnchorError};
|
||||
use rpki::data_model::ta::{TrustAnchor, TrustAnchorBindError, 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/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"),
|
||||
(
|
||||
"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 {
|
||||
@ -20,7 +32,7 @@ fn bind_trust_anchor_with_downloaded_fixtures_succeeds() {
|
||||
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");
|
||||
TrustAnchor::bind_der(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
|
||||
@ -30,7 +42,7 @@ fn bind_trust_anchor_with_downloaded_fixtures_succeeds() {
|
||||
.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");
|
||||
TrustAnchor::bind_der(tal, &ta_der, Some(&resolved)).expect("bind with resolved uri");
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,8 +55,10 @@ fn bind_rejects_spki_mismatch() {
|
||||
// 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)
|
||||
TrustAnchor::bind_der(tal, &ta_der, None),
|
||||
Err(TrustAnchorError::Bind(
|
||||
TrustAnchorBindError::TalSpkiMismatch
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
@ -56,7 +70,9 @@ fn bind_rejects_resolved_uri_not_listed_in_tal() {
|
||||
|
||||
let bad = Url::parse("https://example.invalid/not-in-tal.cer").unwrap();
|
||||
assert!(matches!(
|
||||
TrustAnchor::bind(tal, &ta_der, Some(&bad)),
|
||||
Err(TrustAnchorError::ResolvedUriNotInTal(_))
|
||||
TrustAnchor::bind_der(tal, &ta_der, Some(&bad)),
|
||||
Err(TrustAnchorError::Bind(
|
||||
TrustAnchorBindError::ResolvedUriNotInTal(_)
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user