20260510 增加strict策略模式并隔离多TA失败

This commit is contained in:
2026-05-11 11:32:17 +08:00
parent 265b6f65d0
commit 51e483d924
12 changed files with 1194 additions and 96 deletions

View File

@@ -2,7 +2,8 @@ use crate::data_model::common::{DerReader, der_take_tlv};
use crate::data_model::oid::OID_CT_ASPA;
use crate::data_model::rc::ResourceCertificate;
use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError,
SignedObjectValidateError,
};
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -91,6 +92,15 @@ pub enum AspaProfileError {
ProvidersContainCustomer(u32),
}
impl From<SignedObjectDecodeError> for AspaProfileError {
fn from(value: SignedObjectDecodeError) -> Self {
match value {
SignedObjectDecodeError::Parse(e) => AspaProfileError::ProfileDecode(e.to_string()),
SignedObjectDecodeError::Validate(e) => AspaProfileError::SignedObject(e),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum AspaDecodeError {
#[error("{0}")]
@@ -173,6 +183,17 @@ impl AspaObject {
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, AspaDecodeError> {
let signed_object =
RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name)
.map_err(AspaProfileError::from)?;
Self::from_signed_object(signed_object)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, AspaDecodeError> {
let econtent_type = signed_object
.signed_data

View File

@@ -3,7 +3,8 @@ use crate::data_model::common::{BigUnsigned, UtcTime};
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,
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError,
SignedObjectValidateError,
};
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -119,6 +120,15 @@ pub enum ManifestProfileError {
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}")]
@@ -186,6 +196,17 @@ impl ManifestObject {
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> {

View File

@@ -8,7 +8,7 @@ use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version}
use serde::{Deserialize, Serialize};
use crate::data_model::common::{
Asn1TimeUtc, InvalidTimeEncodingError, UtcTime, X509NameDer, asn1_time_to_model,
Asn1TimeUtc, DerReader, InvalidTimeEncodingError, UtcTime, X509NameDer, asn1_time_to_model,
};
use crate::data_model::oid::{
OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST,
@@ -389,6 +389,11 @@ pub enum ResourceCertificateProfileError {
#[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")]
InvalidSignatureAlgorithmParameters,
#[error(
"{role} Name strict validation failed: {detail} (RFC 6487 §4.4; RFC 5280 §4.1.2.4/§4.1.2.6)"
)]
StrictName { role: &'static str, detail: String },
#[error("duplicate extension: {0} (RFC 5280 §4.2; RFC 6487 §4.8)")]
DuplicateExtension(&'static str),
@@ -562,17 +567,181 @@ impl ResourceCertificate {
Ok(())
}
pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> {
validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?;
validate_strict_rpki_name(&self.tbs.subject_name, "subject")?;
Ok(())
}
/// Decode a resource certificate (`parse + validate`).
pub fn decode_der(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn decode_der_with_strict_name(der: &[u8]) -> Result<Self, ResourceCertificateDecodeError> {
let cert = Self::decode_der(der)?;
cert.validate_strict_name_profile()?;
Ok(cert)
}
/// Backwards-compatible helper (historical name).
pub fn from_der(der: &[u8]) -> Result<Self, ResourceCertificateError> {
Self::decode_der(der)
}
}
fn validate_strict_rpki_name(
name: &X509NameDer,
role: &'static str,
) -> Result<(), ResourceCertificateProfileError> {
let mut name_seq = DerReader::new(name.as_raw())
.take_sequence()
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let mut common_name_count = 0usize;
let mut serial_number_count = 0usize;
while !name_seq.is_empty() {
let set_bytes = name_seq
.take_tag(0x31)
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let mut rdn_set = DerReader::new(set_bytes);
if rdn_set.is_empty() {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: "RelativeDistinguishedName SET is empty".to_string(),
});
}
while !rdn_set.is_empty() {
let mut attr = rdn_set
.take_sequence()
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let oid = attr
.take_tag(0x06)
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
let (value_tag, _value) = attr
.take_any()
.map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?;
if !attr.is_empty() {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: "AttributeTypeAndValue must be SEQUENCE of 2".to_string(),
});
}
match oid {
// 2.5.4.3 commonName
&[0x55, 0x04, 0x03] => {
common_name_count += 1;
if value_tag != 0x13 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!(
"commonName must be PrintableString, got tag 0x{value_tag:02X}"
),
});
}
}
// 2.5.4.5 serialNumber
&[0x55, 0x04, 0x05] => {
serial_number_count += 1;
if value_tag != 0x13 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!(
"serialNumber must be PrintableString, got tag 0x{value_tag:02X}"
),
});
}
}
_ => {}
}
}
}
if common_name_count != 1 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!("commonName must appear exactly once, got {common_name_count}"),
});
}
if serial_number_count > 1 {
return Err(ResourceCertificateProfileError::StrictName {
role,
detail: format!("serialNumber must appear at most once, got {serial_number_count}"),
});
}
Ok(())
}
#[cfg(test)]
mod strict_name_tests {
use super::*;
fn name_with_attrs(attrs: &[(&[u8], u8, &[u8])]) -> X509NameDer {
let mut rdns = Vec::new();
for (oid, tag, value) in attrs {
let mut attr = Vec::new();
attr.extend(der_tlv(0x06, oid));
attr.extend(der_tlv(*tag, value));
let attr = der_tlv(0x30, &attr);
let rdn = der_tlv(0x31, &attr);
rdns.extend(rdn);
}
X509NameDer(der_tlv(0x30, &rdns))
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
encode_len(value.len(), &mut out);
out.extend_from_slice(value);
out
}
fn encode_len(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | bytes.len() as u8);
out.extend(bytes);
}
#[test]
fn strict_name_accepts_printable_common_name_and_serial_number() {
let name = name_with_attrs(&[
(&[0x55, 0x04, 0x03], 0x13, b"CN1"),
(&[0x55, 0x04, 0x05], 0x13, b"SN1"),
]);
validate_strict_rpki_name(&name, "subject").expect("strict name");
}
#[test]
fn strict_name_rejects_utf8_common_name() {
let name = name_with_attrs(&[(&[0x55, 0x04, 0x03], 0x0C, b"CN1")]);
let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails");
assert!(err.to_string().contains("PrintableString"), "{err}");
}
#[test]
fn strict_name_rejects_duplicate_common_name() {
let name = name_with_attrs(&[
(&[0x55, 0x04, 0x03], 0x13, b"CN1"),
(&[0x55, 0x04, 0x03], 0x13, b"CN2"),
]);
let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails");
assert!(err.to_string().contains("exactly once"), "{err}");
}
}
impl ResourceCertificateParsed {
pub fn validate_profile(self) -> Result<ResourceCertificate, ResourceCertificateProfileError> {
let version = match self.version {

View File

@@ -2,7 +2,8 @@ use crate::data_model::common::{DerReader, der_take_tlv};
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate};
use crate::data_model::signed_object::{
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError,
RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError,
SignedObjectValidateError,
};
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -106,6 +107,15 @@ pub enum RoaProfileError {
},
}
impl From<SignedObjectDecodeError> for RoaProfileError {
fn from(value: SignedObjectDecodeError) -> Self {
match value {
SignedObjectDecodeError::Parse(e) => RoaProfileError::ProfileDecode(e.to_string()),
SignedObjectDecodeError::Validate(e) => RoaProfileError::SignedObject(e),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum RoaDecodeError {
#[error("{0}")]
@@ -171,6 +181,17 @@ impl RoaObject {
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, RoaDecodeError> {
let signed_object =
RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name)
.map_err(RoaProfileError::from)?;
Self::from_signed_object(signed_object)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
let econtent_type = signed_object
.signed_data

View File

@@ -9,10 +9,10 @@ use crate::data_model::oid::{
OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS,
};
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess};
use asn1_rs::{Any, Class, FromBer, Header, Tag};
use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag};
use ring::digest;
use x509_parser::extensions::ParsedExtension;
use x509_parser::prelude::{FromDer, X509Certificate};
use x509_parser::prelude::X509Certificate;
use x509_parser::public_key::PublicKey;
use x509_parser::x509::SubjectPublicKeyInfo;
@@ -321,7 +321,13 @@ impl RpkiSignedObject {
/// This performs encoding/structure parsing only. Profile constraints are enforced by
/// `RpkiSignedObjectParsed::validate_profile`.
pub fn parse_der(der: &[u8]) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
parse_signed_object_content_info(der, der)
parse_signed_object_content_info(der, der, CmsParseMode::BerCompatible)
}
pub fn parse_der_strict_cms(
der: &[u8],
) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
parse_signed_object_content_info(der, der, CmsParseMode::DerStrict)
}
/// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce
@@ -331,6 +337,19 @@ impl RpkiSignedObject {
Ok(parsed.validate_profile()?)
}
pub fn decode_der_with_strict_options(
der: &[u8],
strict_cms_der: bool,
strict_name: bool,
) -> Result<Self, SignedObjectDecodeError> {
let parsed = if strict_cms_der {
Self::parse_der_strict_cms(der)?
} else {
Self::parse_der(der)?
};
Ok(parsed.validate_profile_with_strict_name(strict_name)?)
}
/// Scheme-A naming for signature verification.
pub fn verify(&self) -> Result<(), SignedObjectVerifyError> {
self.verify_signature()
@@ -399,13 +418,20 @@ impl RpkiSignedObject {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CmsParseMode {
BerCompatible,
DerStrict,
}
struct CmsReader<'a> {
buf: &'a [u8],
mode: CmsParseMode,
}
impl<'a> CmsReader<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf }
fn new(buf: &'a [u8], mode: CmsParseMode) -> Self {
Self { buf, mode }
}
fn is_empty(&self) -> bool {
@@ -417,19 +443,19 @@ impl<'a> CmsReader<'a> {
}
fn peek_tag(&self) -> Result<u8, String> {
let (_rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?;
let (_rem, any) = parse_any(self.buf, self.mode)?;
header_to_single_byte_tag(&any.header)
}
fn take_any(&mut self) -> Result<(u8, &'a [u8]), String> {
let (rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?;
let (rem, any) = parse_any(self.buf, self.mode)?;
let tag = header_to_single_byte_tag(&any.header)?;
self.buf = rem;
Ok((tag, any.data))
}
fn take_any_full(&mut self) -> Result<(u8, &'a [u8], &'a [u8]), String> {
let (rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?;
let (rem, any) = parse_any(self.buf, self.mode)?;
let consumed = self.buf.len() - rem.len();
let full = &self.buf[..consumed];
let tag = header_to_single_byte_tag(&any.header)?;
@@ -454,16 +480,21 @@ impl<'a> CmsReader<'a> {
fn take_sequence(&mut self) -> Result<CmsReader<'a>, String> {
let value = self.take_tag(0x30)?;
Ok(CmsReader::new(value))
Ok(CmsReader::new(value, self.mode))
}
fn take_octet_string(&mut self) -> Result<Vec<u8>, String> {
let (rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?;
let (rem, any) = parse_any(self.buf, self.mode)?;
let tag = header_to_single_byte_tag(&any.header)?;
if self.mode == CmsParseMode::DerStrict && tag != 0x04 {
return Err(format!(
"unexpected tag in DER strict mode: got 0x{tag:02X}, expected 0x04"
));
}
if tag != 0x04 && tag != 0x24 {
return Err(format!("unexpected tag: got 0x{tag:02X}, expected 0x04"));
}
let octets = flatten_octet_string(any)?;
let octets = flatten_octet_string(any, self.mode)?;
self.buf = rem;
Ok(octets)
}
@@ -475,7 +506,7 @@ impl<'a> CmsReader<'a> {
fn take_explicit(&mut self, expected_outer_tag: u8) -> Result<(u8, &'a [u8]), String> {
let inner_der = self.take_tag(expected_outer_tag)?;
let (tag, value, rem) = cms_take_tlv(inner_der)?;
let (tag, value, rem) = cms_take_tlv(inner_der, self.mode)?;
if !rem.is_empty() {
return Err("trailing bytes inside EXPLICIT value".into());
}
@@ -484,7 +515,7 @@ impl<'a> CmsReader<'a> {
fn take_explicit_der(&mut self, expected_outer_tag: u8) -> Result<&'a [u8], String> {
let inner_der = self.take_tag(expected_outer_tag)?;
let (_tag, _value, rem) = cms_take_tlv(inner_der)?;
let (_tag, _value, rem) = cms_take_tlv(inner_der, self.mode)?;
if !rem.is_empty() {
return Err("trailing bytes inside EXPLICIT value".into());
}
@@ -495,8 +526,9 @@ impl<'a> CmsReader<'a> {
fn parse_signed_object_content_info(
raw_der: &[u8],
parse_der: &[u8],
mode: CmsParseMode,
) -> Result<RpkiSignedObjectParsed, SignedObjectParseError> {
let mut r = CmsReader::new(parse_der);
let mut r = CmsReader::new(parse_der, mode);
let mut content_info_seq = r
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
@@ -519,6 +551,17 @@ fn parse_signed_object_content_info(
})
}
fn parse_any<'a>(input: &'a [u8], mode: CmsParseMode) -> Result<(&'a [u8], Any<'a>), String> {
match mode {
CmsParseMode::BerCompatible => {
Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))
}
CmsParseMode::DerStrict => {
Any::from_der(input).map_err(|e| format!("DER parse error: {e}"))
}
}
}
fn header_to_single_byte_tag(header: &Header<'_>) -> Result<u8, String> {
let tag_no = header.tag().0;
if tag_no > 30 {
@@ -529,24 +572,27 @@ fn header_to_single_byte_tag(header: &Header<'_>) -> Result<u8, String> {
| tag_no as u8)
}
fn cms_take_tlv(input: &[u8]) -> Result<(u8, &[u8], &[u8]), String> {
let (rem, any) = Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))?;
fn cms_take_tlv(input: &[u8], mode: CmsParseMode) -> Result<(u8, &[u8], &[u8]), String> {
let (rem, any) = parse_any(input, mode)?;
let tag = header_to_single_byte_tag(&any.header)?;
Ok((tag, any.data, rem))
}
fn flatten_octet_string(any: Any<'_>) -> Result<Vec<u8>, String> {
fn flatten_octet_string(any: Any<'_>, mode: CmsParseMode) -> Result<Vec<u8>, String> {
if any.class() != Class::Universal || any.tag() != Tag::OctetString {
return Err("expected OCTET STRING".into());
}
if !any.header.constructed() {
return Ok(any.data.to_vec());
}
if mode == CmsParseMode::DerStrict {
return Err("constructed OCTET STRING is not allowed in DER strict mode".into());
}
let mut out = Vec::new();
let mut input = any.data;
while !input.is_empty() {
let (rem, child) = Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))?;
out.extend(flatten_octet_string(child)?);
out.extend(flatten_octet_string(child, mode)?);
input = rem;
}
Ok(out)
@@ -554,13 +600,20 @@ fn flatten_octet_string(any: Any<'_>) -> Result<Vec<u8>, String> {
impl RpkiSignedObjectParsed {
pub fn validate_profile(self) -> Result<RpkiSignedObject, SignedObjectValidateError> {
self.validate_profile_with_strict_name(false)
}
pub fn validate_profile_with_strict_name(
self,
strict_name: bool,
) -> Result<RpkiSignedObject, SignedObjectValidateError> {
if self.content_info_content_type != OID_SIGNED_DATA {
return Err(SignedObjectValidateError::InvalidContentInfoContentType(
self.content_info_content_type,
));
}
let signed_data = validate_signed_data_profile(self.signed_data)?;
let signed_data = validate_signed_data_profile(self.signed_data, strict_name)?;
Ok(RpkiSignedObject {
raw_der: self.raw_der,
@@ -576,7 +629,7 @@ fn parse_signed_data_from_contentinfo_cursor(
let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| {
SignedObjectParseError::Parse("ContentInfo.content must be [0] EXPLICIT".into())
})?;
let mut r = CmsReader::new(inner_der);
let mut r = CmsReader::new(inner_der, seq.mode);
let signed_data_seq = r
.take_sequence()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
@@ -598,7 +651,7 @@ fn parse_signed_data_cursor(
let digest_set_bytes = seq
.take_tag(0x31)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let mut digest_set = CmsReader::new(digest_set_bytes);
let mut digest_set = CmsReader::new(digest_set_bytes, seq.mode);
let mut digest_algorithms: Vec<AlgorithmIdentifierParsed> = Vec::new();
while !digest_set.is_empty() {
let alg = digest_set
@@ -631,7 +684,7 @@ fn parse_signed_data_cursor(
let content = seq
.take_tag(0xA0)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
certificates = Some(split_der_objects(content)?);
certificates = Some(split_der_objects(content, seq.mode)?);
}
0xA1 => {
crls_present = true;
@@ -647,7 +700,7 @@ fn parse_signed_data_cursor(
let set_bytes = seq
.take_tag(0x31)
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
signer_infos = Some(parse_signer_infos_set_cursor(set_bytes)?);
signer_infos = Some(parse_signer_infos_set_cursor(set_bytes, seq.mode)?);
}
_ => {
return Err(SignedObjectParseError::Parse(
@@ -689,7 +742,7 @@ fn parse_encapsulated_content_info_cursor(
"EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(),
)
})?;
let mut inner = CmsReader::new(inner_der);
let mut inner = CmsReader::new(inner_der, seq.mode);
let octets = inner
.take_octet_string()
.map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
@@ -712,11 +765,14 @@ fn parse_encapsulated_content_info_cursor(
})
}
fn split_der_objects(mut input: &[u8]) -> Result<Vec<Vec<u8>>, SignedObjectParseError> {
fn split_der_objects(
mut input: &[u8],
mode: CmsParseMode,
) -> Result<Vec<Vec<u8>>, SignedObjectParseError> {
let mut out: Vec<Vec<u8>> = Vec::new();
while !input.is_empty() {
let (_tag, _value, rem) =
cms_take_tlv(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
cms_take_tlv(input, mode).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?;
let consumed = input.len() - rem.len();
out.push(input[..consumed].to_vec());
input = rem;
@@ -726,8 +782,9 @@ fn split_der_objects(mut input: &[u8]) -> Result<Vec<Vec<u8>>, SignedObjectParse
fn parse_signer_infos_set_cursor(
set_bytes: &[u8],
mode: CmsParseMode,
) -> Result<Vec<SignerInfoParsed>, SignedObjectParseError> {
let mut set = CmsReader::new(set_bytes);
let mut set = CmsReader::new(set_bytes, mode);
let mut out: Vec<SignerInfoParsed> = Vec::new();
while !set.is_empty() {
let si = set
@@ -738,7 +795,10 @@ fn parse_signer_infos_set_cursor(
Ok(out)
}
fn validate_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedObjectValidateError> {
fn validate_ee_certificate(
der: &[u8],
strict_name: bool,
) -> Result<ResourceEeCertificate, SignedObjectValidateError> {
let (rem, cert) = X509Certificate::from_der(der)
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
if !rem.is_empty() {
@@ -762,6 +822,10 @@ fn validate_ee_certificate(der: &[u8]) -> Result<ResourceEeCertificate, SignedOb
};
}
};
if strict_name {
rc.validate_strict_name_profile()
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
}
let ski = rc
.tbs
@@ -940,6 +1004,7 @@ fn parse_signer_info_cursor(
fn validate_signed_data_profile(
signed_data: SignedDataParsed,
strict_name: bool,
) -> Result<SignedDataProfiled, SignedObjectValidateError> {
if signed_data.version != 3 {
return Err(SignedObjectValidateError::InvalidSignedDataVersion(
@@ -985,7 +1050,7 @@ fn validate_signed_data_profile(
certs.len(),
));
}
let ee = validate_ee_certificate(&certs[0])?;
let ee = validate_ee_certificate(&certs[0], strict_name)?;
if signed_data.signer_infos.len() != 1 {
return Err(SignedObjectValidateError::InvalidSignerInfosCount(
@@ -1414,3 +1479,116 @@ fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
&bytes[idx..]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_cms_der_rejects_constructed_octet_string_fixture() {
let der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
))
.expect("read manifest fixture");
let parsed = RpkiSignedObject::parse_der(&der).expect("parse fixture");
let econtent = parsed
.signed_data
.encap_content_info
.econtent
.clone()
.expect("fixture eContent");
assert_eq!(
parsed.signed_data.encap_content_info.econtent.as_deref(),
Some(econtent.as_slice())
);
let primitive_octets = der_tlv(0x04, &econtent);
let constructed_octets = same_size_constructed_octet_string(&primitive_octets, &econtent);
let mutated = replace_first_subslice(&der, &primitive_octets, &constructed_octets)
.expect("replace eContent OCTET STRING");
let compatible = RpkiSignedObject::parse_der(&mutated).expect("BER-compatible parse");
assert!(
econtent.starts_with(
compatible
.signed_data
.encap_content_info
.econtent
.as_deref()
.expect("compatible eContent")
)
);
let err = RpkiSignedObject::parse_der_strict_cms(&mutated)
.expect_err("DER strict rejects constructed OCTET STRING");
assert!(err.to_string().contains("DER"), "{err}");
}
fn replace_first_subslice(input: &[u8], from: &[u8], to: &[u8]) -> Option<Vec<u8>> {
let pos = input
.windows(from.len())
.position(|candidate| candidate == from)?;
let mut out = Vec::with_capacity(input.len() - from.len() + to.len());
out.extend_from_slice(&input[..pos]);
out.extend_from_slice(to);
out.extend_from_slice(&input[pos + from.len()..]);
Some(out)
}
fn same_size_constructed_octet_string(primitive: &[u8], content: &[u8]) -> Vec<u8> {
assert_eq!(primitive[0], 0x04);
let header_len = tlv_header_len(primitive);
let outer_value_len = primitive.len() - header_len;
let child_len = (0..=outer_value_len)
.rev()
.find(|candidate| 1 + len_len(*candidate) + *candidate == outer_value_len)
.expect("find child length");
let mut out = primitive[..header_len].to_vec();
out[0] = 0x24;
out.extend(der_tlv(0x04, &content[..child_len]));
assert_eq!(out.len(), primitive.len());
out
}
fn tlv_header_len(tlv: &[u8]) -> usize {
if tlv[1] & 0x80 == 0 {
2
} else {
2 + (tlv[1] & 0x7F) as usize
}
}
fn len_len(len: usize) -> usize {
if len < 0x80 {
return 1;
}
let mut value = len;
let mut n = 0usize;
while value > 0 {
n += 1;
value >>= 8;
}
1 + n
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
encode_len(value.len(), &mut out);
out.extend_from_slice(value);
out
}
fn encode_len(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | bytes.len() as u8);
out.extend(bytes);
}
}

View File

@@ -112,6 +112,14 @@ impl TaCertificate {
Ok(Self::parse_der(der)?.validate_profile()?)
}
pub fn decode_der_with_strict_name(der: &[u8]) -> Result<Self, TaCertificateDecodeError> {
let ta = Self::decode_der(der)?;
ta.rc_ca
.validate_strict_name_profile()
.map_err(TaCertificateProfileError::from)?;
Ok(ta)
}
/// Backwards-compatible helper (historical name).
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
Self::decode_der(der)
@@ -264,6 +272,15 @@ impl TrustAnchor {
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
}
pub fn bind_der_with_strict_name(
tal: Tal,
ta_der: &[u8],
resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorError> {
let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?;
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
}
pub fn bind(
tal: Tal,
ta_certificate: TaCertificate,