977 lines
33 KiB
Rust
977 lines
33 KiB
Rust
use crate::model::common::der_take_tlv;
|
|
use crate::model::common::{BigUnsigned, UtcTime};
|
|
use crate::model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
|
|
use crate::model::rc::ResourceCertificate;
|
|
use crate::model::signed_object::{
|
|
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError,
|
|
SignedObjectValidateError,
|
|
};
|
|
|
|
#[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: UtcTime,
|
|
pub next_update: UtcTime,
|
|
pub file_hash_alg: String,
|
|
/// DER-encoded content bytes of `Manifest.fileList` (SEQUENCE OF FileAndHash).
|
|
pub file_list_der: Vec<u8>,
|
|
/// Count of FileAndHash entries in `fileList`.
|
|
pub file_count: usize,
|
|
}
|
|
|
|
#[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: [u8; 32],
|
|
}
|
|
|
|
#[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),
|
|
}
|
|
|
|
impl From<SignedObjectDecodeError> for ManifestProfileError {
|
|
fn from(value: SignedObjectDecodeError) -> Self {
|
|
match value {
|
|
SignedObjectDecodeError::Parse(e) => ManifestProfileError::ProfileDecode(e.to_string()),
|
|
SignedObjectDecodeError::Validate(e) => ManifestProfileError::SignedObject(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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 decode_der_with_strict_options(
|
|
der: &[u8],
|
|
strict_cms_der: bool,
|
|
strict_name: bool,
|
|
) -> Result<Self, ManifestDecodeError> {
|
|
let signed_object =
|
|
RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name)
|
|
.map_err(ManifestProfileError::from)?;
|
|
Self::from_signed_object(signed_object)
|
|
}
|
|
|
|
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
|
|
&& !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 (_tag, _value, rem) = der_take_tlv(der).map_err(ManifestParseError::Parse)?;
|
|
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()?)
|
|
}
|
|
|
|
/// Parse and return the manifest fileList.
|
|
///
|
|
/// Note: `ManifestEContent` is profile-validated when produced via `decode_der()`, so this
|
|
/// should only fail due to internal inconsistencies (or if constructed manually).
|
|
pub fn parse_files(&self) -> Result<Vec<FileAndHash>, ManifestProfileError> {
|
|
parse_file_list_sha256_fast(&self.file_list_der)
|
|
}
|
|
|
|
pub fn file_count(&self) -> usize {
|
|
self.file_count
|
|
}
|
|
}
|
|
|
|
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> {
|
|
decode_manifest_econtent_fast(&self.der)
|
|
}
|
|
}
|
|
|
|
fn validate_file_name_bytes(bytes: &[u8]) -> Result<(), ManifestProfileError> {
|
|
// RFC 9286 §4.2.2:
|
|
// 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension.
|
|
if bytes.len() < 5 {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
};
|
|
|
|
// "followed by a single . (DOT), followed by a three letter extension"
|
|
// -> the dot must be exactly 4 bytes from the end.
|
|
let dot_pos = bytes.len() - 4;
|
|
if bytes[dot_pos] != b'.' {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn valid_base_char(b: u8) -> bool {
|
|
// RFC 9286 allowed set: a-zA-Z0-9-_
|
|
b.is_ascii_digit()
|
|
|| b.is_ascii_lowercase()
|
|
|| b.is_ascii_uppercase()
|
|
|| b == b'-'
|
|
|| b == b'_'
|
|
}
|
|
|
|
for &b in &bytes[..dot_pos] {
|
|
if (b & 0x80) != 0 || !valid_base_char(b) {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
}
|
|
}
|
|
|
|
let e0 = bytes[dot_pos + 1];
|
|
let e1 = bytes[dot_pos + 2];
|
|
let e2 = bytes[dot_pos + 3];
|
|
if (e0 & 0x80) != 0 || (e1 & 0x80) != 0 || (e2 & 0x80) != 0 {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn lower_if_alpha(b: u8) -> Option<u8> {
|
|
match b {
|
|
b'a'..=b'z' => Some(b),
|
|
b'A'..=b'Z' => Some(b + 32),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
let Some(l0) = lower_if_alpha(e0) else {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
};
|
|
let Some(l1) = lower_if_alpha(e1) else {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
};
|
|
let Some(l2) = lower_if_alpha(e2) else {
|
|
return Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
));
|
|
};
|
|
|
|
match [l0, l1, l2] {
|
|
// Full IANA list (see `common.rs`).
|
|
[b'a', b's', b'a']
|
|
| [b'c', b'e', b'r']
|
|
| [b'c', b'r', b'l']
|
|
| [b'g', b'b', b'r']
|
|
| [b'm', b'f', b't']
|
|
| [b'r', b'o', b'a']
|
|
| [b's', b'i', b'g']
|
|
| [b't', b'a', b'k'] => Ok(()),
|
|
_ => Err(ManifestProfileError::InvalidFileName(
|
|
String::from_utf8_lossy(bytes).into_owned(),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn decode_manifest_econtent_fast(der: &[u8]) -> Result<ManifestEContent, ManifestProfileError> {
|
|
let (tag, mut seq_content, rem) = der_take_tlv(der)
|
|
.map_err(|e| ManifestProfileError::ProfileDecode(format!("DER decode error: {e}")))?;
|
|
if !rem.is_empty() {
|
|
return Err(ManifestProfileError::ProfileDecode(format!(
|
|
"trailing bytes after DER object: {} bytes",
|
|
rem.len()
|
|
)));
|
|
}
|
|
if tag != 0x30 {
|
|
return Err(ManifestProfileError::ProfileDecode(
|
|
"Manifest eContent must be SEQUENCE".into(),
|
|
));
|
|
}
|
|
|
|
let seq_len = der_count_elements(seq_content).map_err(ManifestProfileError::ProfileDecode)?;
|
|
if seq_len != 5 && seq_len != 6 {
|
|
return Err(ManifestProfileError::InvalidManifestSequenceLen(seq_len));
|
|
}
|
|
|
|
let mut version: u32 = 0;
|
|
if seq_len == 6 {
|
|
let Some(&first_tag) = seq_content.first() else {
|
|
return Err(ManifestProfileError::InvalidManifestSequenceLen(0));
|
|
};
|
|
if first_tag != 0xA0 {
|
|
return Err(ManifestProfileError::ProfileDecode(
|
|
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
|
|
));
|
|
}
|
|
|
|
let (_cs_tag, cs_value, after) = der_take_tlv(seq_content).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.version decode error: {e}"))
|
|
})?;
|
|
seq_content = after;
|
|
|
|
let (inner_tag, inner_value, inner_rem) = der_take_tlv(cs_value).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.version inner decode error: {e}"))
|
|
})?;
|
|
if !inner_rem.is_empty() {
|
|
return Err(ManifestProfileError::ProfileDecode(
|
|
"trailing bytes inside Manifest.version".into(),
|
|
));
|
|
}
|
|
if inner_tag != 0x02 {
|
|
return Err(ManifestProfileError::ProfileDecode(
|
|
"Manifest.version must be [0] EXPLICIT INTEGER".into(),
|
|
));
|
|
}
|
|
let v = der_integer_to_u64(inner_value).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.version decode error: {e}"))
|
|
})?;
|
|
if v != 0 {
|
|
return Err(ManifestProfileError::InvalidManifestVersion(v));
|
|
}
|
|
version = 0;
|
|
}
|
|
|
|
let (mn_tag, mn_value, after) = der_take_tlv(seq_content).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.manifestNumber decode error: {e}"))
|
|
})?;
|
|
seq_content = after;
|
|
if mn_tag != 0x02 {
|
|
return Err(ManifestProfileError::InvalidManifestNumber);
|
|
}
|
|
let manifest_number = der_integer_to_bigunsigned(mn_value)?;
|
|
|
|
let (tu_tag, tu_value, after) = der_take_tlv(seq_content).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.thisUpdate decode error: {e}"))
|
|
})?;
|
|
seq_content = after;
|
|
if tu_tag != 0x18 {
|
|
return Err(ManifestProfileError::InvalidThisUpdate);
|
|
}
|
|
let this_update =
|
|
parse_generalized_time_bytes(tu_value).map_err(ManifestProfileError::ProfileDecode)?;
|
|
|
|
let (nu_tag, nu_value, after) = der_take_tlv(seq_content).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.nextUpdate decode error: {e}"))
|
|
})?;
|
|
seq_content = after;
|
|
if nu_tag != 0x18 {
|
|
return Err(ManifestProfileError::InvalidNextUpdate);
|
|
}
|
|
let next_update =
|
|
parse_generalized_time_bytes(nu_value).map_err(ManifestProfileError::ProfileDecode)?;
|
|
if next_update <= this_update {
|
|
return Err(ManifestProfileError::NextUpdateNotLater);
|
|
}
|
|
|
|
let (oid_tag, oid_value, after) = der_take_tlv(seq_content).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.fileHashAlg decode error: {e}"))
|
|
})?;
|
|
seq_content = after;
|
|
if oid_tag != 0x06 {
|
|
return Err(ManifestProfileError::ProfileDecode(
|
|
"Manifest.fileHashAlg must be OBJECT IDENTIFIER".into(),
|
|
));
|
|
}
|
|
if !oid_content_is_sha256(oid_value) {
|
|
return Err(ManifestProfileError::InvalidFileHashAlg(
|
|
oid_content_to_string(oid_value),
|
|
));
|
|
}
|
|
|
|
let (fl_tag, fl_value, after) = der_take_tlv(seq_content).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("Manifest.fileList decode error: {e}"))
|
|
})?;
|
|
seq_content = after;
|
|
if fl_tag != 0x30 {
|
|
return Err(ManifestProfileError::InvalidFileList);
|
|
}
|
|
let file_count = validate_file_list_sha256_fast(fl_value)?;
|
|
let file_list_der = fl_value.to_vec();
|
|
|
|
if !seq_content.is_empty() {
|
|
return Err(ManifestProfileError::InvalidManifestSequenceLen(seq_len));
|
|
}
|
|
|
|
Ok(ManifestEContent {
|
|
version,
|
|
manifest_number,
|
|
this_update,
|
|
next_update,
|
|
file_hash_alg: OID_SHA256.to_string(),
|
|
file_list_der,
|
|
file_count,
|
|
})
|
|
}
|
|
|
|
fn validate_file_list_sha256_fast(content: &[u8]) -> Result<usize, ManifestProfileError> {
|
|
let mut cur = content;
|
|
let mut count: usize = 0;
|
|
while !cur.is_empty() {
|
|
let (tag, value, rem) = der_take_tlv(cur).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("fileList entry decode error: {e}"))
|
|
})?;
|
|
cur = rem;
|
|
if tag != 0x30 {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
|
|
let mut entry = value;
|
|
let (fn_tag, fn_value, entry_rem) = der_take_tlv(entry).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("fileList fileName decode error: {e}"))
|
|
})?;
|
|
entry = entry_rem;
|
|
if fn_tag != 0x16 {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
if entry.is_empty() {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
validate_file_name_bytes(fn_value)?;
|
|
|
|
let (hash_tag, hash_value, entry_rem) = der_take_tlv(entry).map_err(|_e| {
|
|
// Missing second element should map to "SEQUENCE of 2" shape error.
|
|
ManifestProfileError::InvalidFileAndHash
|
|
})?;
|
|
entry = entry_rem;
|
|
if !entry.is_empty() {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
if hash_tag != 0x03 {
|
|
return Err(ManifestProfileError::InvalidHashType);
|
|
}
|
|
if hash_value.is_empty() {
|
|
return Err(ManifestProfileError::InvalidHashLength(0));
|
|
}
|
|
let unused_bits = hash_value[0];
|
|
if unused_bits != 0 {
|
|
return Err(ManifestProfileError::HashNotOctetAligned);
|
|
}
|
|
let bits = &hash_value[1..];
|
|
if bits.len() != 32 {
|
|
return Err(ManifestProfileError::InvalidHashLength(bits.len()));
|
|
}
|
|
count += 1;
|
|
}
|
|
Ok(count)
|
|
}
|
|
|
|
fn parse_file_list_sha256_fast(content: &[u8]) -> Result<Vec<FileAndHash>, ManifestProfileError> {
|
|
// Heuristic initial capacity (avoid a full pre-scan, which is expensive for xlarge manifests).
|
|
// Each FileAndHash entry is typically tens of bytes; 80 is a conservative average.
|
|
let est = (content.len() / 80).clamp(16, 4096);
|
|
let mut cur = content;
|
|
let mut out: Vec<FileAndHash> = Vec::with_capacity(est);
|
|
while !cur.is_empty() {
|
|
let (tag, value, rem) = der_take_tlv(cur).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("fileList entry decode error: {e}"))
|
|
})?;
|
|
cur = rem;
|
|
if tag != 0x30 {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
let mut entry = value;
|
|
let (fn_tag, fn_value, entry_rem) = der_take_tlv(entry).map_err(|e| {
|
|
ManifestProfileError::ProfileDecode(format!("fileList fileName decode error: {e}"))
|
|
})?;
|
|
entry = entry_rem;
|
|
if fn_tag != 0x16 {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
if entry.is_empty() {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
let file_name = validate_and_copy_file_name(fn_value)?;
|
|
|
|
let (hash_tag, hash_value, entry_rem) = der_take_tlv(entry).map_err(|_e| {
|
|
// Missing second element should map to "SEQUENCE of 2" shape error.
|
|
ManifestProfileError::InvalidFileAndHash
|
|
})?;
|
|
entry = entry_rem;
|
|
if !entry.is_empty() {
|
|
return Err(ManifestProfileError::InvalidFileAndHash);
|
|
}
|
|
if hash_tag != 0x03 {
|
|
return Err(ManifestProfileError::InvalidHashType);
|
|
}
|
|
if hash_value.is_empty() {
|
|
return Err(ManifestProfileError::InvalidHashLength(0));
|
|
}
|
|
let unused_bits = hash_value[0];
|
|
if unused_bits != 0 {
|
|
return Err(ManifestProfileError::HashNotOctetAligned);
|
|
}
|
|
let bits = &hash_value[1..];
|
|
if bits.len() != 32 {
|
|
return Err(ManifestProfileError::InvalidHashLength(bits.len()));
|
|
}
|
|
let mut hash_bytes = [0u8; 32];
|
|
hash_bytes.copy_from_slice(bits);
|
|
out.push(FileAndHash {
|
|
file_name,
|
|
hash_bytes,
|
|
});
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
fn validate_and_copy_file_name(bytes: &[u8]) -> Result<String, ManifestProfileError> {
|
|
validate_file_name_bytes(bytes)?;
|
|
String::from_utf8(bytes.to_vec()).map_err(|_| {
|
|
ManifestProfileError::InvalidFileName(String::from_utf8_lossy(bytes).into_owned())
|
|
})
|
|
}
|
|
|
|
fn der_count_elements(mut input: &[u8]) -> Result<usize, String> {
|
|
let mut count: usize = 0;
|
|
while !input.is_empty() {
|
|
let (_tag, _value, rem) = der_take_tlv(input)?;
|
|
input = rem;
|
|
count += 1;
|
|
}
|
|
Ok(count)
|
|
}
|
|
|
|
fn der_integer_to_u64(bytes: &[u8]) -> Result<u64, String> {
|
|
if bytes.is_empty() {
|
|
return Err("INTEGER empty".into());
|
|
}
|
|
// Reject negative (two's complement).
|
|
if bytes[0] & 0x80 != 0 {
|
|
return Err("INTEGER is negative".into());
|
|
}
|
|
if bytes.len() > 8 {
|
|
return Err("INTEGER too large".into());
|
|
}
|
|
let mut v: u64 = 0;
|
|
for &b in bytes {
|
|
v = (v << 8) | (b as u64);
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
fn der_integer_to_bigunsigned(bytes: &[u8]) -> Result<BigUnsigned, ManifestProfileError> {
|
|
if bytes.is_empty() {
|
|
return Err(ManifestProfileError::InvalidManifestNumber);
|
|
}
|
|
// Two's complement: for non-negative values, a leading 0x00 may be present.
|
|
if bytes[0] & 0x80 != 0 {
|
|
return Err(ManifestProfileError::InvalidManifestNumber);
|
|
}
|
|
let mut start = 0usize;
|
|
while start + 1 < bytes.len() && bytes[start] == 0 {
|
|
start += 1;
|
|
}
|
|
let mut minimal = bytes[start..].to_vec();
|
|
if minimal.is_empty() {
|
|
minimal.push(0);
|
|
}
|
|
if minimal.len() > 20 {
|
|
return Err(ManifestProfileError::ManifestNumberTooLong);
|
|
}
|
|
Ok(BigUnsigned { bytes_be: minimal })
|
|
}
|
|
|
|
fn parse_generalized_time_bytes(bytes: &[u8]) -> Result<UtcTime, String> {
|
|
// Accept "YYYYMMDDHHMMSSZ" and also allow optional fractional seconds (".fff...Z").
|
|
if !bytes.is_ascii() {
|
|
return Err("GeneralizedTime not ASCII".into());
|
|
}
|
|
let s = std::str::from_utf8(bytes).map_err(|e| e.to_string())?;
|
|
if !s.ends_with('Z') {
|
|
return Err("GeneralizedTime must end with 'Z'".into());
|
|
}
|
|
let core = &s[..s.len() - 1];
|
|
let (main, frac) = core
|
|
.split_once('.')
|
|
.map_or((core, None), |(a, b)| (a, Some(b)));
|
|
if main.len() != 14 || !main.bytes().all(|b| b.is_ascii_digit()) {
|
|
return Err("GeneralizedTime must be YYYYMMDDHHMMSS[.fff]Z".into());
|
|
}
|
|
let year: i32 = main[0..4].parse().map_err(|_| "bad year")?;
|
|
let month: u8 = main[4..6].parse().map_err(|_| "bad month")?;
|
|
let day: u8 = main[6..8].parse().map_err(|_| "bad day")?;
|
|
let hour: u8 = main[8..10].parse().map_err(|_| "bad hour")?;
|
|
let minute: u8 = main[10..12].parse().map_err(|_| "bad minute")?;
|
|
let second: u8 = main[12..14].parse().map_err(|_| "bad second")?;
|
|
|
|
let nanosecond: u32 = if let Some(frac) = frac {
|
|
if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
|
|
return Err("bad fractional seconds".into());
|
|
}
|
|
let mut ns: u32 = 0;
|
|
let mut scale: u32 = 1_000_000_000;
|
|
for (i, ch) in frac.bytes().enumerate() {
|
|
if i >= 9 {
|
|
break;
|
|
}
|
|
scale /= 10;
|
|
ns += ((ch - b'0') as u32) * scale;
|
|
}
|
|
ns
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let date = time::Date::from_calendar_date(
|
|
year,
|
|
time::Month::try_from(month).map_err(|_| "bad month")?,
|
|
day,
|
|
)
|
|
.map_err(|e| e.to_string())?;
|
|
let t =
|
|
time::Time::from_hms_nano(hour, minute, second, nanosecond).map_err(|e| e.to_string())?;
|
|
Ok(date.with_time(t).assume_utc())
|
|
}
|
|
|
|
fn oid_content_is_sha256(bytes: &[u8]) -> bool {
|
|
// 2.16.840.1.101.3.4.2.1
|
|
let mut arcs = oid_content_iter(bytes);
|
|
const EXPECTED: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 1];
|
|
for &e in EXPECTED {
|
|
match arcs.next() {
|
|
Some(v) if v == e => {}
|
|
_ => return false,
|
|
}
|
|
}
|
|
arcs.next().is_none()
|
|
}
|
|
|
|
fn oid_content_to_string(bytes: &[u8]) -> String {
|
|
let arcs: Vec<u64> = oid_content_iter(bytes).collect();
|
|
if arcs.is_empty() {
|
|
return "<invalid oid>".to_string();
|
|
}
|
|
let mut s = String::new();
|
|
for (i, a) in arcs.iter().enumerate() {
|
|
if i > 0 {
|
|
s.push('.');
|
|
}
|
|
s.push_str(&a.to_string());
|
|
}
|
|
s
|
|
}
|
|
|
|
fn oid_content_iter(bytes: &[u8]) -> impl Iterator<Item = u64> + '_ {
|
|
struct It<'a> {
|
|
bytes: &'a [u8],
|
|
pos: usize,
|
|
first_done: bool,
|
|
first_a0: u64,
|
|
first_a1: u64,
|
|
emit_first_idx: u8,
|
|
}
|
|
impl<'a> Iterator for It<'a> {
|
|
type Item = u64;
|
|
fn next(&mut self) -> Option<u64> {
|
|
if !self.first_done {
|
|
if self.bytes.is_empty() {
|
|
self.first_done = true;
|
|
return None;
|
|
}
|
|
let first = self.bytes[0] as u64;
|
|
self.first_a0 = first / 40;
|
|
self.first_a1 = first % 40;
|
|
self.pos = 1;
|
|
self.first_done = true;
|
|
self.emit_first_idx = 0;
|
|
}
|
|
if self.emit_first_idx == 0 {
|
|
self.emit_first_idx = 1;
|
|
return Some(self.first_a0);
|
|
}
|
|
if self.emit_first_idx == 1 {
|
|
self.emit_first_idx = 2;
|
|
return Some(self.first_a1);
|
|
}
|
|
if self.pos >= self.bytes.len() {
|
|
return None;
|
|
}
|
|
let mut v: u64 = 0;
|
|
while self.pos < self.bytes.len() {
|
|
let b = self.bytes[self.pos];
|
|
self.pos += 1;
|
|
v = (v << 7) | ((b & 0x7F) as u64);
|
|
if b & 0x80 == 0 {
|
|
return Some(v);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
It {
|
|
bytes,
|
|
pos: 0,
|
|
first_done: false,
|
|
first_a0: 0,
|
|
first_a1: 0,
|
|
emit_first_idx: 0,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn tlv(tag: u8, value: &[u8]) -> Vec<u8> {
|
|
assert!(value.len() < 128);
|
|
let mut out = Vec::with_capacity(2 + value.len());
|
|
out.push(tag);
|
|
out.push(value.len() as u8);
|
|
out.extend_from_slice(value);
|
|
out
|
|
}
|
|
|
|
fn tlv_long_len(tag: u8, len_bytes: &[u8], value: &[u8]) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(2 + len_bytes.len() + value.len());
|
|
out.push(tag);
|
|
out.push(0x80 | (len_bytes.len() as u8));
|
|
out.extend_from_slice(len_bytes);
|
|
out.extend_from_slice(value);
|
|
out
|
|
}
|
|
|
|
#[test]
|
|
fn der_take_tlv_supports_short_and_long_form_lengths_and_errors() {
|
|
let v = b"abc";
|
|
let der = tlv(0x04, v);
|
|
let (tag, val, rem) = der_take_tlv(&der).expect("short len");
|
|
assert_eq!(tag, 0x04);
|
|
assert_eq!(val, v);
|
|
assert!(rem.is_empty());
|
|
|
|
// Long-form length with 1 length byte (130).
|
|
let v = vec![b'x'; 130];
|
|
let der = tlv_long_len(0x04, &[0x82], &v);
|
|
let (tag, val, rem) = der_take_tlv(&der).expect("long len 1");
|
|
assert_eq!(tag, 0x04);
|
|
assert_eq!(val.len(), 130);
|
|
assert!(rem.is_empty());
|
|
|
|
// Long-form length with 2 length bytes (256).
|
|
let v = vec![b'y'; 256];
|
|
let der = tlv_long_len(0x04, &[0x01, 0x00], &v);
|
|
let (tag, val, rem) = der_take_tlv(&der).expect("long len 2");
|
|
assert_eq!(tag, 0x04);
|
|
assert_eq!(val.len(), 256);
|
|
assert!(rem.is_empty());
|
|
|
|
assert!(der_take_tlv(&[]).is_err());
|
|
assert!(der_take_tlv(&[0x04]).is_err());
|
|
|
|
// High-tag-number form not supported.
|
|
assert!(der_take_tlv(&[0x1F, 0x01, 0x00]).is_err());
|
|
|
|
// Indefinite length is not allowed in DER.
|
|
assert!(der_take_tlv(&[0x04, 0x80]).is_err());
|
|
|
|
// Invalid long-form length encoding.
|
|
assert!(der_take_tlv(&[0x04, 0x81]).is_err());
|
|
assert!(der_take_tlv(&[0x04, 0x89]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_generalized_time_bytes_accepts_fraction_and_rejects_invalid() {
|
|
let t = parse_generalized_time_bytes(b"20260101000000Z").expect("basic time");
|
|
assert_eq!(t.year(), 2026);
|
|
|
|
let t = parse_generalized_time_bytes(b"20260101000000.1Z").expect("fractional");
|
|
assert_eq!(t.nanosecond(), 100_000_000);
|
|
|
|
assert!(parse_generalized_time_bytes(b"20260101000000").is_err());
|
|
assert!(parse_generalized_time_bytes(b"20260101000000+00").is_err());
|
|
assert!(parse_generalized_time_bytes(b"2026010100000Z").is_err());
|
|
assert!(parse_generalized_time_bytes(b"20261301000000Z").is_err());
|
|
assert!(parse_generalized_time_bytes(b"20260132000000Z").is_err());
|
|
assert!(parse_generalized_time_bytes(&[0xFF]).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn oid_helpers_accept_sha256_and_format_invalid() {
|
|
// 2.16.840.1.101.3.4.2.1
|
|
let sha256_oid_content = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01];
|
|
assert!(oid_content_is_sha256(&sha256_oid_content));
|
|
assert!(!oid_content_is_sha256(&[0x55, 0x04, 0x03])); // 2.5.4.3
|
|
|
|
assert_eq!(oid_content_to_string(&[]), "<invalid oid>".to_string());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_file_list_sha256_fast_counts_and_rejects_bad_hash() {
|
|
fn file_and_hash(file: &str, digest: u8) -> Vec<u8> {
|
|
let mut hash = vec![0u8; 33];
|
|
hash[0] = 0; // unused bits
|
|
for b in &mut hash[1..] {
|
|
*b = digest;
|
|
}
|
|
let ia5 = tlv(0x16, file.as_bytes());
|
|
let bit = tlv(0x03, &hash);
|
|
let mut entry = Vec::new();
|
|
entry.extend_from_slice(&ia5);
|
|
entry.extend_from_slice(&bit);
|
|
tlv(0x30, &entry)
|
|
}
|
|
|
|
let mut list = Vec::new();
|
|
list.extend_from_slice(&file_and_hash("A.cer", 0xAA));
|
|
list.extend_from_slice(&file_and_hash("B.roa", 0xBB));
|
|
assert_eq!(validate_file_list_sha256_fast(&list).expect("count"), 2);
|
|
|
|
// Wrong hash length.
|
|
let mut bad = Vec::new();
|
|
let ia5 = tlv(0x16, b"A.cer");
|
|
let bit = tlv(0x03, &[0u8; 2]); // too short
|
|
let mut entry = Vec::new();
|
|
entry.extend_from_slice(&ia5);
|
|
entry.extend_from_slice(&bit);
|
|
bad.extend_from_slice(&tlv(0x30, &entry));
|
|
assert!(matches!(
|
|
validate_file_list_sha256_fast(&bad),
|
|
Err(ManifestProfileError::InvalidHashLength(_))
|
|
));
|
|
}
|
|
}
|