add signed object, manifest impl. add coverage script
This commit is contained in:
108
src/data_model/common.rs
Normal file
108
src/data_model/common.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use x509_parser::asn1_rs::Tag;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Asn1TimeEncoding {
|
||||
UtcTime,
|
||||
GeneralizedTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Asn1TimeUtc {
|
||||
pub utc: time::OffsetDateTime,
|
||||
pub encoding: Asn1TimeEncoding,
|
||||
}
|
||||
|
||||
impl Asn1TimeUtc {
|
||||
/// Validate Time encoding rules (RFC 5280): years 1950-2049 use UTCTime,
|
||||
/// other years use GeneralizedTime.
|
||||
pub fn validate_encoding_rfc5280(
|
||||
&self,
|
||||
field: &'static str,
|
||||
) -> Result<(), InvalidTimeEncodingError> {
|
||||
let year = self.utc.year();
|
||||
let expected = if year <= 2049 {
|
||||
Asn1TimeEncoding::UtcTime
|
||||
} else {
|
||||
Asn1TimeEncoding::GeneralizedTime
|
||||
};
|
||||
if self.encoding != expected {
|
||||
return Err(InvalidTimeEncodingError {
|
||||
field,
|
||||
year,
|
||||
encoding: self.encoding,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigUnsigned {
|
||||
/// Minimal big-endian bytes. For zero, this is `[0]`.
|
||||
pub bytes_be: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BigUnsigned {
|
||||
pub fn from_biguint(n: &der_parser::num_bigint::BigUint) -> Self {
|
||||
let mut bytes = n.to_bytes_be();
|
||||
if bytes.is_empty() {
|
||||
bytes.push(0);
|
||||
}
|
||||
Self { bytes_be: bytes }
|
||||
}
|
||||
|
||||
pub fn to_hex_upper(&self) -> String {
|
||||
hex::encode_upper(&self.bytes_be)
|
||||
}
|
||||
|
||||
pub fn to_u64(&self) -> Option<u64> {
|
||||
if self.bytes_be.len() > 8 {
|
||||
return None;
|
||||
}
|
||||
let mut value: u64 = 0;
|
||||
for &b in &self.bytes_be {
|
||||
value = (value << 8) | (b as u64);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
|
||||
pub struct InvalidTimeEncodingError {
|
||||
pub field: &'static str,
|
||||
pub year: i32,
|
||||
pub encoding: Asn1TimeEncoding,
|
||||
}
|
||||
|
||||
pub fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
|
||||
let encoding = if t.is_utctime() {
|
||||
Asn1TimeEncoding::UtcTime
|
||||
} else {
|
||||
Asn1TimeEncoding::GeneralizedTime
|
||||
};
|
||||
Asn1TimeUtc {
|
||||
utc: t.to_datetime(),
|
||||
encoding,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool {
|
||||
match sig.parameters.as_ref() {
|
||||
None => true,
|
||||
Some(p) if p.tag() == Tag::Null => true,
|
||||
Some(_p) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filename extensions registered in IANA "RPKI Repository Name Schemes".
|
||||
///
|
||||
/// Source: <https://www.iana.org/assignments/rpki/rpki.xhtml>
|
||||
/// Snapshot date: 2026-01-28.
|
||||
///
|
||||
/// 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",
|
||||
];
|
||||
@@ -1,52 +1,16 @@
|
||||
pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned};
|
||||
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::asn1_rs::Tag;
|
||||
use x509_parser::certificate::X509Certificate;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
use x509_parser::x509::AlgorithmIdentifier;
|
||||
|
||||
const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
|
||||
const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
||||
const OID_CRL_NUMBER: &str = "2.5.29.20";
|
||||
const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Asn1TimeEncoding {
|
||||
UtcTime,
|
||||
GeneralizedTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Asn1TimeUtc {
|
||||
pub utc: time::OffsetDateTime,
|
||||
pub encoding: Asn1TimeEncoding,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigUnsigned {
|
||||
/// Minimal big-endian bytes. For zero, this is `[0]`.
|
||||
pub bytes_be: Vec<u8>,
|
||||
}
|
||||
|
||||
impl BigUnsigned {
|
||||
pub fn to_hex_upper(&self) -> String {
|
||||
hex::encode_upper(&self.bytes_be)
|
||||
}
|
||||
|
||||
pub fn to_u64(&self) -> Option<u64> {
|
||||
if self.bytes_be.len() > 8 {
|
||||
return None;
|
||||
}
|
||||
let mut value: u64 = 0;
|
||||
for &b in &self.bytes_be {
|
||||
value = (value << 8) | (b as u64);
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RevokedCert {
|
||||
pub serial_number: BigUnsigned,
|
||||
@@ -161,23 +125,23 @@ impl RpkixCrl {
|
||||
if !rc.extensions().is_empty() {
|
||||
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
||||
}
|
||||
let revocation_date = asn1_time_to_model(rc.revocation_date);
|
||||
validate_time_encoding("revocationDate", &revocation_date)?;
|
||||
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
|
||||
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
|
||||
Ok(RevokedCert {
|
||||
serial_number: biguint_to_big_unsigned(rc.serial()),
|
||||
serial_number: BigUnsigned::from_biguint(rc.serial()),
|
||||
revocation_date,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let this_update = asn1_time_to_model(crl.last_update());
|
||||
validate_time_encoding("thisUpdate", &this_update)?;
|
||||
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(asn1_time_to_model)
|
||||
.map(crate::data_model::common::asn1_time_to_model)
|
||||
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
||||
validate_time_encoding("nextUpdate", &next_update)?;
|
||||
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
||||
|
||||
Ok(RpkixCrl {
|
||||
raw_der: der.to_vec(),
|
||||
@@ -301,19 +265,10 @@ pub enum CrlVerifyError {
|
||||
InvalidSignature(String),
|
||||
}
|
||||
|
||||
fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
|
||||
let encoding = if t.is_utctime() {
|
||||
Asn1TimeEncoding::UtcTime
|
||||
} else {
|
||||
Asn1TimeEncoding::GeneralizedTime
|
||||
};
|
||||
Asn1TimeUtc {
|
||||
utc: t.to_datetime(),
|
||||
encoding,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), CrlDecodeError> {
|
||||
fn validate_time_encoding_rfc5280(
|
||||
field: &'static str,
|
||||
t: &Asn1TimeUtc,
|
||||
) -> Result<(), CrlDecodeError> {
|
||||
let year = t.utc.year();
|
||||
let expected = if year <= 2049 {
|
||||
Asn1TimeEncoding::UtcTime
|
||||
@@ -331,10 +286,10 @@ fn validate_time_encoding(field: &'static str, t: &Asn1TimeUtc) -> Result<(), Cr
|
||||
}
|
||||
|
||||
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
||||
match sig.parameters.as_ref() {
|
||||
None => Ok(()),
|
||||
Some(p) if p.tag() == Tag::Null => Ok(()),
|
||||
Some(_p) => Err(CrlDecodeError::InvalidSignatureAlgorithmParameters),
|
||||
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +322,7 @@ fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtens
|
||||
if n.bits() > 159 {
|
||||
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
||||
}
|
||||
crl_number = Some(biguint_to_big_unsigned(&n));
|
||||
crl_number = Some(BigUnsigned::from_biguint(&n));
|
||||
}
|
||||
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
||||
}
|
||||
@@ -410,14 +365,6 @@ fn parse_crl_number(ext: &X509Extension<'_>) -> Result<der_parser::num_bigint::B
|
||||
}
|
||||
}
|
||||
|
||||
fn biguint_to_big_unsigned(n: &der_parser::num_bigint::BigUint) -> BigUnsigned {
|
||||
let mut bytes = n.to_bytes_be();
|
||||
if bytes.is_empty() {
|
||||
bytes.push(0);
|
||||
}
|
||||
BigUnsigned { bytes_be: bytes }
|
||||
}
|
||||
|
||||
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
|
||||
cert.extensions()
|
||||
.iter()
|
||||
|
||||
284
src/data_model/manifest.rs
Normal file
284
src/data_model/manifest.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256};
|
||||
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
|
||||
use der_parser::ber::BerObjectContent;
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
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 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 FileAndHash {
|
||||
pub file_name: String,
|
||||
pub hash_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestDecodeError {
|
||||
#[error("signed object decode error: {0}")]
|
||||
SignedObject(#[from] SignedObjectDecodeError),
|
||||
|
||||
#[error("DER parse error: {0}")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after DER object: {0} bytes")]
|
||||
TrailingBytes(usize),
|
||||
|
||||
#[error("eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0}")]
|
||||
InvalidEContentType(String),
|
||||
|
||||
#[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0}")]
|
||||
InvalidManifestSequenceLen(usize),
|
||||
|
||||
#[error("Manifest.version must be 0, got {0}")]
|
||||
InvalidManifestVersion(u64),
|
||||
|
||||
#[error("Manifest.manifestNumber must be non-negative INTEGER")]
|
||||
InvalidManifestNumber,
|
||||
|
||||
#[error("Manifest.manifestNumber longer than 20 octets")]
|
||||
ManifestNumberTooLong,
|
||||
|
||||
#[error("Manifest.thisUpdate must be GeneralizedTime")]
|
||||
InvalidThisUpdate,
|
||||
|
||||
#[error("Manifest.nextUpdate must be GeneralizedTime")]
|
||||
InvalidNextUpdate,
|
||||
|
||||
#[error("Manifest.nextUpdate must be later than thisUpdate")]
|
||||
NextUpdateNotLater,
|
||||
|
||||
#[error("Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0}")]
|
||||
InvalidFileHashAlg(String),
|
||||
|
||||
#[error("Manifest.fileList must be a SEQUENCE")]
|
||||
InvalidFileList,
|
||||
|
||||
#[error("FileAndHash must be SEQUENCE of 2")]
|
||||
InvalidFileAndHash,
|
||||
|
||||
#[error("fileList file name invalid: {0}")]
|
||||
InvalidFileName(String),
|
||||
|
||||
#[error("fileList hash must be BIT STRING")]
|
||||
InvalidHashType,
|
||||
|
||||
#[error("fileList hash BIT STRING must be octet-aligned (unused bits=0)")]
|
||||
HashNotOctetAligned,
|
||||
|
||||
#[error("fileList hash length invalid for sha256: got {0} bytes")]
|
||||
InvalidHashLength(usize),
|
||||
}
|
||||
|
||||
impl ManifestObject {
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, ManifestDecodeError> {
|
||||
let signed_object = RpkiSignedObject::decode_der(der)?;
|
||||
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(ManifestDecodeError::InvalidEContentType(econtent_type));
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(ManifestDecodeError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 5 && seq.len() != 6 {
|
||||
return Err(ManifestDecodeError::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(ManifestDecodeError::Parse(
|
||||
"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()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(ManifestDecodeError::Parse(
|
||||
"trailing bytes inside Manifest.version".into(),
|
||||
));
|
||||
}
|
||||
let v = inner
|
||||
.as_u64()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
if v != 0 {
|
||||
return Err(ManifestDecodeError::InvalidManifestVersion(v));
|
||||
}
|
||||
version = 0;
|
||||
idx = 1;
|
||||
}
|
||||
|
||||
let manifest_number = parse_manifest_number(&seq[idx])?;
|
||||
idx += 1;
|
||||
|
||||
let this_update =
|
||||
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidThisUpdate)?;
|
||||
idx += 1;
|
||||
let next_update =
|
||||
parse_generalized_time(&seq[idx], ManifestDecodeError::InvalidNextUpdate)?;
|
||||
idx += 1;
|
||||
if next_update <= this_update {
|
||||
return Err(ManifestDecodeError::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));
|
||||
}
|
||||
|
||||
let files = parse_file_list_sha256(&seq[idx])?;
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
manifest_number,
|
||||
this_update,
|
||||
next_update,
|
||||
file_hash_alg: OID_SHA256.to_string(),
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_manifest_number(obj: &DerObject<'_>) -> Result<BigUnsigned, ManifestDecodeError> {
|
||||
let n = obj
|
||||
.as_biguint()
|
||||
.map_err(|_e| ManifestDecodeError::InvalidManifestNumber)?;
|
||||
let out = BigUnsigned::from_biguint(&n);
|
||||
if out.bytes_be.len() > 20 {
|
||||
return Err(ManifestDecodeError::ManifestNumberTooLong);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_generalized_time(
|
||||
obj: &DerObject<'_>,
|
||||
err: ManifestDecodeError,
|
||||
) -> Result<OffsetDateTime, ManifestDecodeError> {
|
||||
match &obj.content {
|
||||
BerObjectContent::GeneralizedTime(dt) => dt
|
||||
.to_datetime()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string())),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result<Vec<FileAndHash>, ManifestDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|_e| ManifestDecodeError::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()));
|
||||
}
|
||||
out.push(FileAndHash {
|
||||
file_name,
|
||||
hash_bytes,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec<u8>), ManifestDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() != 2 {
|
||||
return Err(ManifestDecodeError::InvalidFileAndHash);
|
||||
}
|
||||
let file_name = seq[0]
|
||||
.as_str()
|
||||
.map_err(|e| ManifestDecodeError::Parse(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),
|
||||
};
|
||||
if unused_bits != 0 {
|
||||
return Err(ManifestDecodeError::HashNotOctetAligned);
|
||||
}
|
||||
Ok((file_name, bits))
|
||||
}
|
||||
|
||||
fn validate_file_name(name: &str) -> Result<(), ManifestDecodeError> {
|
||||
// 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()));
|
||||
};
|
||||
if base.is_empty() || ext.len() != 3 {
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
}
|
||||
if !base
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
||||
{
|
||||
return Err(ManifestDecodeError::InvalidFileName(name.to_string()));
|
||||
}
|
||||
if !ext.bytes().all(|b| b.is_ascii_alphabetic()) {
|
||||
return Err(ManifestDecodeError::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()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, ManifestDecodeError> {
|
||||
let oid = obj
|
||||
.as_oid()
|
||||
.map_err(|e| ManifestDecodeError::Parse(e.to_string()))?;
|
||||
Ok(oid.to_id_string())
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
pub mod common;
|
||||
pub mod crl;
|
||||
mod rc;
|
||||
mod tal;
|
||||
mod ta;
|
||||
mod resources;
|
||||
mod oids;
|
||||
pub mod oid;
|
||||
pub mod signed_object;
|
||||
pub mod manifest;
|
||||
|
||||
20
src/data_model/oid.rs
Normal file
20
src/data_model/oid.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
pub const OID_SHA256: &str = "2.16.840.1.101.3.4.2.1";
|
||||
|
||||
pub const OID_SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
|
||||
|
||||
pub const OID_CMS_ATTR_CONTENT_TYPE: &str = "1.2.840.113549.1.9.3";
|
||||
pub const OID_CMS_ATTR_MESSAGE_DIGEST: &str = "1.2.840.113549.1.9.4";
|
||||
pub const OID_CMS_ATTR_SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
|
||||
|
||||
pub const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
|
||||
pub const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
|
||||
|
||||
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
|
||||
pub const OID_CRL_NUMBER: &str = "2.5.29.20";
|
||||
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
|
||||
|
||||
pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26";
|
||||
|
||||
// X.509 extensions / access methods (RFC 5280 / RFC 6487)
|
||||
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";
|
||||
pub const OID_AD_SIGNED_OBJECT: &str = "1.3.6.1.5.5.7.48.11";
|
||||
771
src/data_model/signed_object.rs
Normal file
771
src/data_model/signed_object.rs
Normal file
@@ -0,0 +1,771 @@
|
||||
use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc};
|
||||
use crate::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,
|
||||
OID_AD_SIGNED_OBJECT, OID_SUBJECT_INFO_ACCESS, OID_SUBJECT_KEY_IDENTIFIER,
|
||||
};
|
||||
use der_parser::ber::Class;
|
||||
use der_parser::der::{parse_der, DerObject, Tag};
|
||||
use sha2::{Digest, Sha256};
|
||||
use x509_parser::extensions::GeneralName;
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
use x509_parser::public_key::PublicKey;
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
use x509_parser::certificate::X509Certificate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResourceEeCertificate {
|
||||
pub raw_der: Vec<u8>,
|
||||
pub subject_key_identifier: Vec<u8>,
|
||||
pub spki_der: Vec<u8>,
|
||||
pub sia_signed_object_uris: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RpkiSignedObject {
|
||||
pub raw_der: Vec<u8>,
|
||||
pub content_info_content_type: String,
|
||||
pub signed_data: SignedDataProfiled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SignedDataProfiled {
|
||||
pub version: u32,
|
||||
pub digest_algorithms: Vec<String>,
|
||||
pub encap_content_info: EncapsulatedContentInfo,
|
||||
pub certificates: Vec<ResourceEeCertificate>,
|
||||
pub crls_present: bool,
|
||||
pub signer_infos: Vec<SignerInfoProfiled>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct EncapsulatedContentInfo {
|
||||
pub econtent_type: String,
|
||||
pub econtent: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SignerInfoProfiled {
|
||||
pub version: u32,
|
||||
pub sid_ski: Vec<u8>,
|
||||
pub digest_algorithm: String,
|
||||
pub signature_algorithm: String,
|
||||
pub signed_attrs: SignedAttrsProfiled,
|
||||
pub unsigned_attrs_present: bool,
|
||||
pub signature: Vec<u8>,
|
||||
pub signed_attrs_der_for_signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SignedAttrsProfiled {
|
||||
pub content_type: String,
|
||||
pub message_digest: Vec<u8>,
|
||||
pub signing_time: Asn1TimeUtc,
|
||||
pub other_attrs_present: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SignedObjectDecodeError {
|
||||
#[error("DER parse error: {0}")]
|
||||
Parse(String),
|
||||
|
||||
#[error("trailing bytes after DER object: {0} bytes")]
|
||||
TrailingBytes(usize),
|
||||
|
||||
#[error("ContentInfo.contentType must be SignedData ({OID_SIGNED_DATA}), got {0}")]
|
||||
InvalidContentInfoContentType(String),
|
||||
|
||||
#[error("SignedData.version must be 3, got {0}")]
|
||||
InvalidSignedDataVersion(u64),
|
||||
|
||||
#[error("SignedData.digestAlgorithms must contain exactly one AlgorithmIdentifier, got {0}")]
|
||||
InvalidDigestAlgorithmsCount(usize),
|
||||
|
||||
#[error("digest algorithm must be id-sha256 ({OID_SHA256}), got {0}")]
|
||||
InvalidDigestAlgorithm(String),
|
||||
|
||||
#[error("SignedData.certificates MUST be present")]
|
||||
CertificatesMissing,
|
||||
|
||||
#[error("SignedData.certificates must contain exactly one EE certificate, got {0}")]
|
||||
InvalidCertificatesCount(usize),
|
||||
|
||||
#[error("SignedData.crls MUST be omitted")]
|
||||
CrlsPresent,
|
||||
|
||||
#[error("SignedData.signerInfos must contain exactly one SignerInfo, got {0}")]
|
||||
InvalidSignerInfosCount(usize),
|
||||
|
||||
#[error("SignerInfo.version must be 3, got {0}")]
|
||||
InvalidSignerInfoVersion(u64),
|
||||
|
||||
#[error("SignerInfo.sid must be subjectKeyIdentifier [0]")]
|
||||
InvalidSignerIdentifier,
|
||||
|
||||
#[error("SignerInfo.digestAlgorithm must be id-sha256 ({OID_SHA256}), got {0}")]
|
||||
InvalidSignerInfoDigestAlgorithm(String),
|
||||
|
||||
#[error("SignerInfo.signedAttrs MUST be present")]
|
||||
SignedAttrsMissing,
|
||||
|
||||
#[error("SignerInfo.unsignedAttrs MUST be omitted")]
|
||||
UnsignedAttrsPresent,
|
||||
|
||||
#[error(
|
||||
"SignerInfo.signatureAlgorithm must be rsaEncryption ({OID_RSA_ENCRYPTION}) or \
|
||||
sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}"
|
||||
)]
|
||||
InvalidSignatureAlgorithm(String),
|
||||
|
||||
#[error("SignerInfo.signatureAlgorithm parameters must be absent or NULL")]
|
||||
InvalidSignatureAlgorithmParameters,
|
||||
|
||||
#[error("signedAttrs contains unsupported attribute OID {0}")]
|
||||
UnsupportedSignedAttribute(String),
|
||||
|
||||
#[error("signedAttrs contains duplicate attribute OID {0}")]
|
||||
DuplicateSignedAttribute(String),
|
||||
|
||||
#[error("signedAttrs attribute {oid} attrValues must contain exactly one value, got {count}")]
|
||||
InvalidSignedAttributeValuesCount { oid: String, count: usize },
|
||||
|
||||
#[error("signedAttrs.content-type attrValues must equal eContentType ({econtent_type}), got {attr_content_type}")]
|
||||
ContentTypeAttrMismatch {
|
||||
econtent_type: String,
|
||||
attr_content_type: String,
|
||||
},
|
||||
|
||||
#[error("EncapsulatedContentInfo.eContent MUST be present")]
|
||||
EContentMissing,
|
||||
|
||||
#[error("signedAttrs.message-digest does not match SHA-256(eContent)")]
|
||||
MessageDigestMismatch,
|
||||
|
||||
#[error("EE certificate parse error: {0}")]
|
||||
EeCertificateParse(String),
|
||||
|
||||
#[error("EE certificate missing SubjectKeyIdentifier extension")]
|
||||
EeCertificateMissingSki,
|
||||
|
||||
#[error("EE certificate missing SubjectInfoAccess extension ({OID_SUBJECT_INFO_ACCESS})")]
|
||||
EeCertificateMissingSia,
|
||||
|
||||
#[error("EE certificate SIA missing id-ad-signedObject access method ({OID_AD_SIGNED_OBJECT})")]
|
||||
EeCertificateMissingSignedObjectSia,
|
||||
|
||||
#[error("EE certificate SIA id-ad-signedObject accessLocation must be a URI")]
|
||||
EeCertificateSignedObjectSiaNotUri,
|
||||
|
||||
#[error("EE certificate SIA id-ad-signedObject must include at least one rsync:// URI")]
|
||||
EeCertificateSignedObjectSiaNoRsync,
|
||||
|
||||
#[error("SignerInfo.sid SKI does not match EE certificate SKI")]
|
||||
SidSkiMismatch,
|
||||
|
||||
#[error("invalid signing-time attribute value (expected UTCTime or GeneralizedTime)")]
|
||||
InvalidSigningTimeValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SignedObjectVerifyError {
|
||||
#[error("EE SubjectPublicKeyInfo parse error: {0}")]
|
||||
EeSpkiParse(String),
|
||||
|
||||
#[error("trailing bytes after EE SubjectPublicKeyInfo DER: {0} bytes")]
|
||||
EeSpkiTrailingBytes(usize),
|
||||
|
||||
#[error("unsupported EE public key algorithm (only RSA supported in M3)")]
|
||||
UnsupportedEePublicKeyAlgorithm,
|
||||
|
||||
#[error("EE RSA public exponent invalid")]
|
||||
InvalidEeRsaExponent,
|
||||
|
||||
#[error("signature verification failed")]
|
||||
InvalidSignature,
|
||||
}
|
||||
|
||||
impl RpkiSignedObject {
|
||||
/// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce
|
||||
/// the profile constraints from RFC 6488 §2-§3 and RFC 9589 §4.
|
||||
pub fn decode_der(der: &[u8]) -> Result<Self, SignedObjectDecodeError> {
|
||||
let (rem, obj) = parse_der(der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(SignedObjectDecodeError::TrailingBytes(rem.len()));
|
||||
}
|
||||
|
||||
let content_info_seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if content_info_seq.len() != 2 {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"ContentInfo must be a SEQUENCE of 2 elements".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let content_type = oid_to_string(&content_info_seq[0])?;
|
||||
if content_type != OID_SIGNED_DATA {
|
||||
return Err(SignedObjectDecodeError::InvalidContentInfoContentType(
|
||||
content_type,
|
||||
));
|
||||
}
|
||||
|
||||
let signed_data = parse_signed_data_from_contentinfo(&content_info_seq[1])?;
|
||||
|
||||
Ok(RpkiSignedObject {
|
||||
raw_der: der.to_vec(),
|
||||
content_info_content_type: OID_SIGNED_DATA.to_string(),
|
||||
signed_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify the CMS signature using the embedded EE certificate public key.
|
||||
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
||||
let ee = &self.signed_data.certificates[0];
|
||||
self.verify_signature_with_ee_spki_der(&ee.spki_der)
|
||||
}
|
||||
|
||||
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
|
||||
pub fn verify_signature_with_ee_spki_der(
|
||||
&self,
|
||||
ee_spki_der: &[u8],
|
||||
) -> Result<(), SignedObjectVerifyError> {
|
||||
let (rem, spki) = SubjectPublicKeyInfo::from_der(ee_spki_der)
|
||||
.map_err(|e| SignedObjectVerifyError::EeSpkiParse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(SignedObjectVerifyError::EeSpkiTrailingBytes(rem.len()));
|
||||
}
|
||||
self.verify_signature_with_ee_spki(&spki)
|
||||
}
|
||||
|
||||
/// Verify the CMS signature using a parsed SubjectPublicKeyInfo.
|
||||
pub fn verify_signature_with_ee_spki(
|
||||
&self,
|
||||
ee_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), SignedObjectVerifyError> {
|
||||
let pk = ee_spki
|
||||
.parsed()
|
||||
.map_err(|_e| SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm)?;
|
||||
let (n, e) = match pk {
|
||||
PublicKey::RSA(rsa) => {
|
||||
let n = strip_leading_zeros(rsa.modulus).to_vec();
|
||||
let e = strip_leading_zeros(rsa.exponent).to_vec();
|
||||
let _exp = rsa
|
||||
.try_exponent()
|
||||
.map_err(|_e| SignedObjectVerifyError::InvalidEeRsaExponent)?;
|
||||
(n, e)
|
||||
}
|
||||
_ => return Err(SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm),
|
||||
};
|
||||
|
||||
let signer = &self.signed_data.signer_infos[0];
|
||||
// The message to be verified is the DER encoding of SignedAttributes (SET OF Attribute).
|
||||
let msg = &signer.signed_attrs_der_for_signature;
|
||||
|
||||
let pk = ring::signature::RsaPublicKeyComponents { n, e };
|
||||
pk.verify(
|
||||
&ring::signature::RSA_PKCS1_2048_8192_SHA256,
|
||||
msg,
|
||||
&signer.signature,
|
||||
)
|
||||
.map_err(|_e| SignedObjectVerifyError::InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_signed_data_from_contentinfo(
|
||||
obj: &DerObject<'_>,
|
||||
) -> Result<SignedDataProfiled, SignedObjectDecodeError> {
|
||||
// ContentInfo.content is `[0] EXPLICIT`, but `der-parser` will represent unknown tagged
|
||||
// objects as `Unknown(Any)`. For EXPLICIT tags, the content octets are the full encoding of
|
||||
// the inner object, so we parse it from the object's slice.
|
||||
if obj.class() != Class::ContextSpecific || obj.tag() != Tag(0) {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"ContentInfo.content must be [0] EXPLICIT".into(),
|
||||
));
|
||||
}
|
||||
let inner_der = obj
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
let (rem, inner_obj) =
|
||||
parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"trailing bytes inside ContentInfo.content".into(),
|
||||
));
|
||||
}
|
||||
parse_signed_data(&inner_obj)
|
||||
}
|
||||
|
||||
fn parse_signed_data(obj: &DerObject<'_>) -> Result<SignedDataProfiled, SignedObjectDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() < 4 || seq.len() > 6 {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"SignedData must be a SEQUENCE of 4..6 elements".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let version = seq[0]
|
||||
.as_u64()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if version != 3 {
|
||||
return Err(SignedObjectDecodeError::InvalidSignedDataVersion(version));
|
||||
}
|
||||
|
||||
let digest_set = seq[1]
|
||||
.as_set()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if digest_set.len() != 1 {
|
||||
return Err(SignedObjectDecodeError::InvalidDigestAlgorithmsCount(
|
||||
digest_set.len(),
|
||||
));
|
||||
}
|
||||
let (digest_oid, _params_ok) = parse_algorithm_identifier(&digest_set[0])?;
|
||||
if digest_oid != OID_SHA256 {
|
||||
return Err(SignedObjectDecodeError::InvalidDigestAlgorithm(digest_oid));
|
||||
}
|
||||
let digest_algorithms = vec![OID_SHA256.to_string()];
|
||||
|
||||
let encap_content_info = parse_encapsulated_content_info(&seq[2])?;
|
||||
|
||||
let mut certificates: Option<Vec<ResourceEeCertificate>> = None;
|
||||
let mut crls_present = false;
|
||||
let mut signer_infos_obj: Option<&DerObject<'_>> = None;
|
||||
|
||||
for item in &seq[3..] {
|
||||
if item.class() == Class::ContextSpecific && item.tag() == Tag(0) {
|
||||
if certificates.is_some() {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"SignedData.certificates appears more than once".into(),
|
||||
));
|
||||
}
|
||||
certificates = Some(parse_certificate_set_implicit(item)?);
|
||||
} else if item.class() == Class::ContextSpecific && item.tag() == Tag(1) {
|
||||
crls_present = true;
|
||||
} else if item.class() == Class::Universal && item.tag() == Tag::Set {
|
||||
signer_infos_obj = Some(item);
|
||||
} else {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"unexpected field in SignedData".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if crls_present {
|
||||
return Err(SignedObjectDecodeError::CrlsPresent);
|
||||
}
|
||||
let certificates = certificates.ok_or(SignedObjectDecodeError::CertificatesMissing)?;
|
||||
if certificates.len() != 1 {
|
||||
return Err(SignedObjectDecodeError::InvalidCertificatesCount(
|
||||
certificates.len(),
|
||||
));
|
||||
}
|
||||
|
||||
let signer_infos_obj = signer_infos_obj.ok_or_else(|| {
|
||||
SignedObjectDecodeError::Parse("SignedData.signerInfos missing".into())
|
||||
})?;
|
||||
let signer_infos_set = signer_infos_obj
|
||||
.as_set()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if signer_infos_set.len() != 1 {
|
||||
return Err(SignedObjectDecodeError::InvalidSignerInfosCount(
|
||||
signer_infos_set.len(),
|
||||
));
|
||||
}
|
||||
|
||||
let signer_info = parse_signer_info(&signer_infos_set[0])?;
|
||||
if signer_info.sid_ski != certificates[0].subject_key_identifier {
|
||||
return Err(SignedObjectDecodeError::SidSkiMismatch);
|
||||
}
|
||||
if signer_info.signed_attrs.content_type != encap_content_info.econtent_type {
|
||||
return Err(SignedObjectDecodeError::ContentTypeAttrMismatch {
|
||||
econtent_type: encap_content_info.econtent_type.clone(),
|
||||
attr_content_type: signer_info.signed_attrs.content_type.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let computed = Sha256::digest(&encap_content_info.econtent).to_vec();
|
||||
if computed != signer_info.signed_attrs.message_digest {
|
||||
return Err(SignedObjectDecodeError::MessageDigestMismatch);
|
||||
}
|
||||
|
||||
Ok(SignedDataProfiled {
|
||||
version: 3,
|
||||
digest_algorithms,
|
||||
encap_content_info,
|
||||
certificates,
|
||||
crls_present,
|
||||
signer_infos: vec![signer_info],
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_encapsulated_content_info(obj: &DerObject<'_>) -> Result<EncapsulatedContentInfo, SignedObjectDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() == 1 {
|
||||
return Err(SignedObjectDecodeError::EContentMissing);
|
||||
}
|
||||
if seq.len() != 2 {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"EncapsulatedContentInfo must be SEQUENCE of 1..2".into(),
|
||||
));
|
||||
}
|
||||
let econtent_type = oid_to_string(&seq[0])?;
|
||||
|
||||
let econtent_tagged = &seq[1];
|
||||
if econtent_tagged.class() != Class::ContextSpecific || econtent_tagged.tag() != Tag(0) {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(),
|
||||
));
|
||||
}
|
||||
let inner_der = econtent_tagged
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||
;
|
||||
let (rem, inner_obj) =
|
||||
parse_der(inner_der).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"trailing bytes inside EncapsulatedContentInfo.eContent".into(),
|
||||
));
|
||||
}
|
||||
let econtent = inner_obj
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||
.to_vec();
|
||||
if econtent.is_empty() {
|
||||
return Err(SignedObjectDecodeError::EContentMissing);
|
||||
}
|
||||
|
||||
Ok(EncapsulatedContentInfo {
|
||||
econtent_type,
|
||||
econtent,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_certificate_set_implicit(obj: &DerObject<'_>) -> Result<Vec<ResourceEeCertificate>, SignedObjectDecodeError> {
|
||||
let content = obj
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
let mut input = content;
|
||||
let mut certs = Vec::new();
|
||||
while !input.is_empty() {
|
||||
let (rem, _any_obj) =
|
||||
parse_der(input).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
let consumed = input.len() - rem.len();
|
||||
let der = &input[..consumed];
|
||||
certs.push(parse_ee_certificate(der)?);
|
||||
input = rem;
|
||||
}
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
fn parse_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectDecodeError> {
|
||||
let (rem, cert) = X509Certificate::from_der(der)
|
||||
.map_err(|e| SignedObjectDecodeError::EeCertificateParse(e.to_string()))?;
|
||||
let _ = rem;
|
||||
|
||||
let ski = cert
|
||||
.extensions()
|
||||
.iter()
|
||||
.find(|ext| ext.oid.to_id_string() == OID_SUBJECT_KEY_IDENTIFIER)
|
||||
.and_then(|ext| match ext.parsed_extension() {
|
||||
ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(SignedObjectDecodeError::EeCertificateMissingSki)?;
|
||||
|
||||
let spki_der = cert.public_key().raw.to_vec();
|
||||
|
||||
let sia_signed_object_uris = parse_ee_sia_signed_object_uris(&cert)?;
|
||||
|
||||
Ok(ResourceEeCertificate {
|
||||
raw_der: der.to_vec(),
|
||||
subject_key_identifier: ski,
|
||||
spki_der,
|
||||
sia_signed_object_uris,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_ee_sia_signed_object_uris(
|
||||
cert: &X509Certificate<'_>,
|
||||
) -> Result<Vec<String>, SignedObjectDecodeError> {
|
||||
let mut sia: Option<x509_parser::extensions::SubjectInfoAccess<'_>> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.to_id_string() != OID_SUBJECT_INFO_ACCESS {
|
||||
continue;
|
||||
}
|
||||
match ext.parsed_extension() {
|
||||
ParsedExtension::SubjectInfoAccess(s) => {
|
||||
if sia.is_some() {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"duplicate SubjectInfoAccess extensions".into(),
|
||||
));
|
||||
}
|
||||
sia = Some(s.clone());
|
||||
}
|
||||
ParsedExtension::ParseError { error } => {
|
||||
return Err(SignedObjectDecodeError::Parse(error.to_string()));
|
||||
}
|
||||
_ => {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"SubjectInfoAccess extension parse failed".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sia = sia.ok_or(SignedObjectDecodeError::EeCertificateMissingSia)?;
|
||||
let mut uris: Vec<String> = Vec::new();
|
||||
for ad in sia.iter() {
|
||||
if ad.access_method.to_id_string() != OID_AD_SIGNED_OBJECT {
|
||||
continue;
|
||||
}
|
||||
match &ad.access_location {
|
||||
GeneralName::URI(u) => uris.push((*u).to_string()),
|
||||
_ => return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNotUri),
|
||||
}
|
||||
}
|
||||
if uris.is_empty() {
|
||||
return Err(SignedObjectDecodeError::EeCertificateMissingSignedObjectSia);
|
||||
}
|
||||
if !uris.iter().any(|u| u.starts_with("rsync://")) {
|
||||
return Err(SignedObjectDecodeError::EeCertificateSignedObjectSiaNoRsync);
|
||||
}
|
||||
Ok(uris)
|
||||
}
|
||||
|
||||
fn parse_signer_info(obj: &DerObject<'_>) -> Result<SignerInfoProfiled, SignedObjectDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if seq.len() < 5 || seq.len() > 7 {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"SignerInfo must be a SEQUENCE of 5..7 elements".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let version = seq[0]
|
||||
.as_u64()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if version != 3 {
|
||||
return Err(SignedObjectDecodeError::InvalidSignerInfoVersion(version));
|
||||
}
|
||||
|
||||
let sid = &seq[1];
|
||||
if sid.class() != Class::ContextSpecific || sid.tag() != Tag(0) {
|
||||
return Err(SignedObjectDecodeError::InvalidSignerIdentifier);
|
||||
}
|
||||
let sid_ski = sid
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||
.to_vec();
|
||||
|
||||
let (digest_algorithm, _params_ok) = parse_algorithm_identifier(&seq[2])?;
|
||||
if digest_algorithm != OID_SHA256 {
|
||||
return Err(SignedObjectDecodeError::InvalidSignerInfoDigestAlgorithm(
|
||||
digest_algorithm,
|
||||
));
|
||||
}
|
||||
|
||||
let mut idx = 3;
|
||||
let mut signed_attrs: Option<SignedAttrsProfiled> = None;
|
||||
let mut signed_attrs_der_for_signature: Option<Vec<u8>> = None;
|
||||
|
||||
if seq[idx].class() == Class::ContextSpecific && seq[idx].tag() == Tag(0) {
|
||||
let signed_attrs_obj = &seq[idx];
|
||||
let signed_attrs_content = signed_attrs_obj
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
signed_attrs = Some(parse_signed_attrs_implicit(signed_attrs_content)?);
|
||||
signed_attrs_der_for_signature = Some(make_signed_attrs_der_for_signature(signed_attrs_obj)?);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
let signed_attrs = signed_attrs.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?;
|
||||
let signed_attrs_der_for_signature =
|
||||
signed_attrs_der_for_signature.ok_or(SignedObjectDecodeError::SignedAttrsMissing)?;
|
||||
|
||||
let (signature_algorithm, params_ok) = parse_algorithm_identifier(&seq[idx])?;
|
||||
if !params_ok {
|
||||
return Err(SignedObjectDecodeError::InvalidSignatureAlgorithmParameters);
|
||||
}
|
||||
if signature_algorithm != OID_RSA_ENCRYPTION
|
||||
&& signature_algorithm != OID_SHA256_WITH_RSA_ENCRYPTION
|
||||
{
|
||||
return Err(SignedObjectDecodeError::InvalidSignatureAlgorithm(
|
||||
signature_algorithm,
|
||||
));
|
||||
}
|
||||
idx += 1;
|
||||
|
||||
let signature = seq[idx]
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||
.to_vec();
|
||||
idx += 1;
|
||||
|
||||
let unsigned_attrs_present = seq.get(idx).is_some();
|
||||
if unsigned_attrs_present {
|
||||
return Err(SignedObjectDecodeError::UnsignedAttrsPresent);
|
||||
}
|
||||
|
||||
Ok(SignerInfoProfiled {
|
||||
version: 3,
|
||||
sid_ski,
|
||||
digest_algorithm: OID_SHA256.to_string(),
|
||||
signature_algorithm,
|
||||
signed_attrs,
|
||||
unsigned_attrs_present,
|
||||
signature,
|
||||
signed_attrs_der_for_signature,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_signed_attrs_implicit(input: &[u8]) -> Result<SignedAttrsProfiled, SignedObjectDecodeError> {
|
||||
let mut content_type: Option<String> = None;
|
||||
let mut message_digest: Option<Vec<u8>> = None;
|
||||
let mut signing_time: Option<Asn1TimeUtc> = None;
|
||||
|
||||
let mut remaining = input;
|
||||
while !remaining.is_empty() {
|
||||
let (rem, attr_obj) =
|
||||
parse_der(remaining).map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
remaining = rem;
|
||||
|
||||
let attr_seq = attr_obj
|
||||
.as_sequence()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if attr_seq.len() != 2 {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"Attribute must be SEQUENCE of 2".into(),
|
||||
));
|
||||
}
|
||||
let oid = oid_to_string(&attr_seq[0])?;
|
||||
let values_set = attr_seq[1]
|
||||
.as_set()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if values_set.len() != 1 {
|
||||
return Err(SignedObjectDecodeError::InvalidSignedAttributeValuesCount {
|
||||
oid,
|
||||
count: values_set.len(),
|
||||
});
|
||||
}
|
||||
|
||||
match oid.as_str() {
|
||||
OID_CMS_ATTR_CONTENT_TYPE => {
|
||||
if content_type.is_some() {
|
||||
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
|
||||
}
|
||||
let v = oid_to_string(&values_set[0])?;
|
||||
content_type = Some(v);
|
||||
}
|
||||
OID_CMS_ATTR_MESSAGE_DIGEST => {
|
||||
if message_digest.is_some() {
|
||||
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
|
||||
}
|
||||
let v = values_set[0]
|
||||
.as_slice()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?
|
||||
.to_vec();
|
||||
message_digest = Some(v);
|
||||
}
|
||||
OID_CMS_ATTR_SIGNING_TIME => {
|
||||
if signing_time.is_some() {
|
||||
return Err(SignedObjectDecodeError::DuplicateSignedAttribute(oid));
|
||||
}
|
||||
signing_time = Some(parse_signing_time_value(&values_set[0])?);
|
||||
}
|
||||
_ => {
|
||||
return Err(SignedObjectDecodeError::UnsupportedSignedAttribute(oid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SignedAttrsProfiled {
|
||||
content_type: content_type.ok_or_else(|| {
|
||||
SignedObjectDecodeError::Parse("missing signedAttrs content-type".into())
|
||||
})?,
|
||||
message_digest: message_digest.ok_or_else(|| {
|
||||
SignedObjectDecodeError::Parse("missing signedAttrs message-digest".into())
|
||||
})?,
|
||||
signing_time: signing_time.ok_or_else(|| {
|
||||
SignedObjectDecodeError::Parse("missing signedAttrs signing-time".into())
|
||||
})?,
|
||||
other_attrs_present: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_signing_time_value(obj: &DerObject<'_>) -> Result<Asn1TimeUtc, SignedObjectDecodeError> {
|
||||
match &obj.content {
|
||||
der_parser::ber::BerObjectContent::UTCTime(dt) => Ok(Asn1TimeUtc {
|
||||
utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?,
|
||||
encoding: Asn1TimeEncoding::UtcTime,
|
||||
}),
|
||||
der_parser::ber::BerObjectContent::GeneralizedTime(dt) => Ok(Asn1TimeUtc {
|
||||
utc: dt.to_datetime().map_err(|_| SignedObjectDecodeError::InvalidSigningTimeValue)?,
|
||||
encoding: Asn1TimeEncoding::GeneralizedTime,
|
||||
}),
|
||||
_ => Err(SignedObjectDecodeError::InvalidSigningTimeValue),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_signed_attrs_der_for_signature(obj: &DerObject<'_>) -> Result<Vec<u8>, SignedObjectDecodeError> {
|
||||
// We need the DER encoding of SignedAttributes (SET OF Attribute) as signature input.
|
||||
// The SignedAttributes field in SignerInfo is `[0] IMPLICIT`, so the on-wire bytes start with
|
||||
// a context-specific constructed tag (0xA0 for tag 0). For signature verification, this tag
|
||||
// is replaced with the universal SET tag (0x31), leaving length+content unchanged.
|
||||
//
|
||||
let mut cs_der = obj
|
||||
.to_vec()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if cs_der.is_empty() {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"signedAttrs encoding is empty".into(),
|
||||
));
|
||||
}
|
||||
// The first byte should be the context-specific tag (0xA0) for [0] constructed.
|
||||
// Replace it with universal SET (0x31) for signature input.
|
||||
cs_der[0] = 0x31;
|
||||
Ok(cs_der)
|
||||
}
|
||||
|
||||
fn oid_to_string(obj: &DerObject<'_>) -> Result<String, SignedObjectDecodeError> {
|
||||
let oid = obj
|
||||
.as_oid()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
Ok(oid.to_id_string())
|
||||
}
|
||||
|
||||
fn parse_algorithm_identifier(
|
||||
obj: &DerObject<'_>,
|
||||
) -> Result<(String, bool), SignedObjectDecodeError> {
|
||||
let seq = obj
|
||||
.as_sequence()
|
||||
.map_err(|e| SignedObjectDecodeError::Parse(e.to_string()))?;
|
||||
if seq.is_empty() || seq.len() > 2 {
|
||||
return Err(SignedObjectDecodeError::Parse(
|
||||
"AlgorithmIdentifier must be SEQUENCE of 1..2".into(),
|
||||
));
|
||||
}
|
||||
let oid = oid_to_string(&seq[0])?;
|
||||
let params_ok = match seq.get(1) {
|
||||
None => true,
|
||||
Some(p) => matches!(p.content, der_parser::ber::BerObjectContent::Null),
|
||||
};
|
||||
Ok((oid, params_ok))
|
||||
}
|
||||
|
||||
fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
|
||||
let mut idx = 0;
|
||||
while idx < bytes.len() && bytes[idx] == 0 {
|
||||
idx += 1;
|
||||
}
|
||||
if idx == bytes.len() {
|
||||
&bytes[bytes.len() - 1..]
|
||||
} else {
|
||||
&bytes[idx..]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user