464 lines
15 KiB
Rust
464 lines
15 KiB
Rust
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, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
|
|
};
|
|
use der_parser::ber::BerObjectContent;
|
|
use der_parser::der::{DerObject, Tag, parse_der};
|
|
use time::OffsetDateTime;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct ManifestObject {
|
|
pub signed_object: RpkiSignedObject,
|
|
pub econtent_type: String,
|
|
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,
|
|
pub manifest_number: BigUnsigned,
|
|
pub this_update: OffsetDateTime,
|
|
pub next_update: OffsetDateTime,
|
|
pub file_hash_alg: String,
|
|
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,
|
|
pub hash_bytes: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
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),
|
|
}
|
|
|
|
#[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)"
|
|
)]
|
|
InvalidManifestNumber,
|
|
|
|
#[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")]
|
|
ManifestNumberTooLong,
|
|
|
|
#[error("Manifest.thisUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
|
|
InvalidThisUpdate,
|
|
|
|
#[error("Manifest.nextUpdate must be GeneralizedTime (RFC 9286 §4.2)")]
|
|
InvalidNextUpdate,
|
|
|
|
#[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)"
|
|
)]
|
|
InvalidFileHashAlg(String),
|
|
|
|
#[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")]
|
|
InvalidFileList,
|
|
|
|
#[error("FileAndHash must be SEQUENCE of 2 (RFC 9286 §4.2)")]
|
|
InvalidFileAndHash,
|
|
|
|
#[error("fileList file name invalid: {0} (RFC 9286 §4.2.2)")]
|
|
InvalidFileName(String),
|
|
|
|
#[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)"
|
|
)]
|
|
HashNotOctetAligned,
|
|
|
|
#[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)"
|
|
)]
|
|
EeResourcesMissing,
|
|
|
|
#[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)"
|
|
)]
|
|
EeAsResourcesNotInherit,
|
|
|
|
#[error(
|
|
"Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)"
|
|
)]
|
|
EeAsResourcesRdiPresent,
|
|
}
|
|
|
|
impl ManifestObject {
|
|
/// 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,
|
|
})
|
|
}
|
|
|
|
/// 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(ManifestProfileError::InvalidEContentType(econtent_type).into());
|
|
}
|
|
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(),
|
|
manifest,
|
|
})
|
|
}
|
|
|
|
/// Validate the embedded EE certificate resources against RFC 9286 §5.1.
|
|
///
|
|
/// This does **not** perform certificate path validation. It assumes `ee` is a parsed and
|
|
/// profile-validated RPKI EE resource certificate.
|
|
pub fn validate_against_ee_cert(
|
|
&self,
|
|
ee: &ResourceCertificate,
|
|
) -> Result<(), ManifestValidateError> {
|
|
let ip = ee.tbs.extensions.ip_resources.as_ref();
|
|
let asn = ee.tbs.extensions.as_resources.as_ref();
|
|
if ip.is_none() && asn.is_none() {
|
|
return Err(ManifestValidateError::EeResourcesMissing);
|
|
}
|
|
|
|
if let Some(ip) = ip {
|
|
if !ip.is_all_inherit() {
|
|
return Err(ManifestValidateError::EeIpResourcesNotInherit);
|
|
}
|
|
}
|
|
|
|
if let Some(asn) = asn {
|
|
if asn.rdi.is_some() {
|
|
return Err(ManifestValidateError::EeAsResourcesRdiPresent);
|
|
}
|
|
if !asn.is_asnum_inherit() {
|
|
return Err(ManifestValidateError::EeAsResourcesNotInherit);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate this manifest's embedded EE certificate resources.
|
|
pub fn validate_embedded_ee_cert(&self) -> Result<(), ManifestValidateError> {
|
|
let ee = &self.signed_object.signed_data.certificates[0].resource_cert;
|
|
self.validate_against_ee_cert(ee)
|
|
}
|
|
}
|
|
|
|
impl ManifestEContent {
|
|
/// 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(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| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
|
if seq.len() != 5 && seq.len() != 6 {
|
|
return Err(ManifestProfileError::InvalidManifestSequenceLen(seq.len()));
|
|
}
|
|
|
|
let mut idx = 0;
|
|
let mut version: u32 = 0;
|
|
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(ManifestProfileError::ProfileDecode(
|
|
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
|
|
));
|
|
}
|
|
let inner_der = v_obj
|
|
.as_slice()
|
|
.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(ManifestProfileError::ProfileDecode(
|
|
"trailing bytes inside Manifest.version".into(),
|
|
));
|
|
}
|
|
let v = inner
|
|
.as_u64()
|
|
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
|
if v != 0 {
|
|
return Err(ManifestProfileError::InvalidManifestVersion(v));
|
|
}
|
|
version = 0;
|
|
idx = 1;
|
|
}
|
|
|
|
let manifest_number = parse_manifest_number(&seq[idx])?;
|
|
idx += 1;
|
|
|
|
let this_update =
|
|
parse_generalized_time(&seq[idx], ManifestProfileError::InvalidThisUpdate)?;
|
|
idx += 1;
|
|
let next_update =
|
|
parse_generalized_time(&seq[idx], ManifestProfileError::InvalidNextUpdate)?;
|
|
idx += 1;
|
|
if next_update <= this_update {
|
|
return Err(ManifestProfileError::NextUpdateNotLater);
|
|
}
|
|
|
|
let file_hash_alg = oid_to_string(&seq[idx])?;
|
|
idx += 1;
|
|
if file_hash_alg != OID_SHA256 {
|
|
return Err(ManifestProfileError::InvalidFileHashAlg(file_hash_alg));
|
|
}
|
|
|
|
let files = parse_file_list_sha256(&seq[idx])?;
|
|
|
|
Ok(ManifestEContent {
|
|
version,
|
|
manifest_number,
|
|
this_update,
|
|
next_update,
|
|
file_hash_alg: OID_SHA256.to_string(),
|
|
files,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestProfileError> {
|
|
let n = obj
|
|
.as_biguint()
|
|
.map_err(|_e| ManifestProfileError::InvalidManifestNumber)?;
|
|
let out = BigUnsigned::from_biguint(&n);
|
|
if out.bytes_be.len() > 20 {
|
|
return Err(ManifestProfileError::ManifestNumberTooLong);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn parse_generalized_time(
|
|
obj: &DerObject<'_>,
|
|
err: ManifestProfileError,
|
|
) -> Result<OffsetDateTime, ManifestProfileError> {
|
|
match &obj.content {
|
|
BerObjectContent::GeneralizedTime(dt) => dt
|
|
.to_datetime()
|
|
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string())),
|
|
_ => Err(err),
|
|
}
|
|
}
|
|
|
|
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestProfileError> {
|
|
let seq = obj
|
|
.as_sequence()
|
|
.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(ManifestProfileError::InvalidHashLength(hash_bytes.len()));
|
|
}
|
|
out.push(FileAndHash {
|
|
file_name,
|
|
hash_bytes,
|
|
});
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestProfileError> {
|
|
let seq = obj
|
|
.as_sequence()
|
|
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
|
if seq.len() != 2 {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
let file_name = seq[0]
|
|
.as_str()
|
|
.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(ManifestProfileError::InvalidHashType),
|
|
};
|
|
if unused_bits != 0 {
|
|
return Err(ManifestProfileError::HashNotOctetAligned);
|
|
}
|
|
Ok((file_name, bits))
|
|
}
|
|
|
|
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(ManifestProfileError::InvalidFileName(name.to_string()));
|
|
};
|
|
if base.is_empty() || ext.len() != 3 {
|
|
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
|
}
|
|
if !base
|
|
.bytes()
|
|
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
|
{
|
|
return Err(ManifestProfileError::InvalidFileName(name.to_string()));
|
|
}
|
|
if !ext.bytes().all(|b| b.is_ascii_alphabetic()) {
|
|
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(ManifestProfileError::InvalidFileName(name.to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestProfileError> {
|
|
let oid = obj
|
|
.as_oid()
|
|
.map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?;
|
|
Ok(oid.to_id_string())
|
|
}
|