Skip to content

Commit

Permalink
x509-cert: provide parsing profiles (#987)
Browse files Browse the repository at this point in the history
This allow the user to relax checks when parsing certificate and
cover for non rfc5280 compliant x509 certificates.
  • Loading branch information
baloo authored Apr 20, 2023
1 parent 4309bc2 commit f570f59
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 28 deletions.
2 changes: 1 addition & 1 deletion x509-cert/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ rust-version = "1.65"

[dependencies]
const-oid = { version = "0.9.2", features = ["db"] } # TODO: path = "../const-oid"
der = { version = "0.7.3", features = ["alloc", "derive", "flagset", "oid"] }
der = { version = "0.7.4", features = ["alloc", "derive", "flagset", "oid"] }
spki = { version = "0.7.1", features = ["alloc"] }

# optional dependencies
Expand Down
2 changes: 2 additions & 0 deletions x509-cert/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ where
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.8
issuer_unique_id: None,
subject_unique_id: None,

_profile: Default::default(),
};

let extensions = profile.build_extensions(
Expand Down
61 changes: 53 additions & 8 deletions x509-cert/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,49 @@
use crate::{name::Name, serial_number::SerialNumber, time::Validity};
use alloc::vec::Vec;
use const_oid::AssociatedOid;
use core::cmp::Ordering;
use core::{cmp::Ordering, fmt::Debug, marker::PhantomData};
use der::asn1::BitString;
use der::{Decode, Enumerated, Error, ErrorKind, Sequence, ValueOrd};
use der::{Decode, Enumerated, Error, ErrorKind, Sequence, Tag, ValueOrd};
use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};

#[cfg(feature = "pem")]
use der::pem::PemLabel;

/// [`Profile`] allows the consumer of this crate to customize the behavior when parsing
/// certificates.
/// By default, parsing will be made in a rfc5280-compliant manner.
pub trait Profile: PartialEq + Debug + Eq + Clone {
/// Checks to run when parsing serial numbers
fn check_serial_number(serial: &SerialNumber<Self>) -> der::Result<()> {
// See the note in `SerialNumber::new`: we permit lengths of 21 bytes here,
// since some X.509 implementations interpret the limit of 20 bytes to refer
// to the pre-encoded value.
if serial.inner.len() > SerialNumber::<Self>::MAX_DECODE_LEN {
Err(Tag::Integer.value_error())
} else {
Ok(())
}
}
}

#[derive(Debug, PartialEq, Eq, Clone)]
/// Parse certificates with rfc5280-compliant checks
pub struct Rfc5280;

impl Profile for Rfc5280 {}

#[cfg(feature = "hazmat")]
#[derive(Debug, PartialEq, Eq, Clone)]
/// Parse raw x509 certificate and disable all the checks
pub struct Raw;

#[cfg(feature = "hazmat")]
impl Profile for Raw {
fn check_serial_number(_serial: &SerialNumber<Self>) -> der::Result<()> {
Ok(())
}
}

/// Certificate `Version` as defined in [RFC 5280 Section 4.1].
///
/// ```text
Expand Down Expand Up @@ -45,6 +80,9 @@ impl Default for Version {
}
}

/// X.509 `TbsCertificate` as defined in [RFC 5280 Section 4.1]
pub type TbsCertificate = TbsCertificateInner<Rfc5280>;

/// X.509 `TbsCertificate` as defined in [RFC 5280 Section 4.1]
///
/// ASN.1 structure containing the names of the subject and issuer, a public
Expand Down Expand Up @@ -73,7 +111,7 @@ impl Default for Version {
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
#[allow(missing_docs)]
pub struct TbsCertificate {
pub struct TbsCertificateInner<P: Profile = Rfc5280> {
/// The certificate version
///
/// Note that this value defaults to Version 1 per the RFC. However,
Expand All @@ -83,7 +121,7 @@ pub struct TbsCertificate {
#[asn1(context_specific = "0", default = "Default::default")]
pub version: Version,

pub serial_number: SerialNumber,
pub serial_number: SerialNumber<P>,
pub signature: AlgorithmIdentifierOwned,
pub issuer: Name,
pub validity: Validity,
Expand All @@ -98,9 +136,11 @@ pub struct TbsCertificate {

#[asn1(context_specific = "3", tag_mode = "EXPLICIT", optional = "true")]
pub extensions: Option<crate::ext::Extensions>,

pub(crate) _profile: PhantomData<P>,
}

impl TbsCertificate {
impl<P: Profile> TbsCertificateInner<P> {
/// Decodes a single extension
///
/// Returns an error if multiple of these extensions is present. Returns
Expand Down Expand Up @@ -132,6 +172,11 @@ impl TbsCertificate {
}
}

/// X.509 certificates are defined in [RFC 5280 Section 4.1].
///
/// [RFC 5280 Section 4.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1
pub type Certificate = CertificateInner<Rfc5280>;

/// X.509 certificates are defined in [RFC 5280 Section 4.1].
///
/// ```text
Expand All @@ -146,14 +191,14 @@ impl TbsCertificate {
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
#[allow(missing_docs)]
pub struct Certificate {
pub tbs_certificate: TbsCertificate,
pub struct CertificateInner<P: Profile = Rfc5280> {
pub tbs_certificate: TbsCertificateInner<P>,
pub signature_algorithm: AlgorithmIdentifierOwned,
pub signature: BitString,
}

#[cfg(feature = "pem")]
impl PemLabel for Certificate {
impl<P: Profile> PemLabel for CertificateInner<P> {
const PEM_LABEL: &'static str = "CERTIFICATE";
}

Expand Down
41 changes: 22 additions & 19 deletions x509-cert/src/serial_number.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! X.509 serial number

use core::fmt::Display;
use core::{fmt::Display, marker::PhantomData};

use der::{
asn1::{self, Int},
DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd,
Writer,
};

use crate::certificate::{Profile, Rfc5280};

/// [RFC 5280 Section 4.1.2.2.] Serial Number
///
/// The serial number MUST be a positive integer assigned by the CA to
Expand All @@ -25,16 +27,17 @@ use der::{
/// that are negative or zero. Certificate users SHOULD be prepared to
/// gracefully handle such certificates.
#[derive(Clone, Debug, Eq, PartialEq, ValueOrd, PartialOrd, Ord)]
pub struct SerialNumber {
inner: Int,
pub struct SerialNumber<P: Profile = Rfc5280> {
pub(crate) inner: Int,
_profile: PhantomData<P>,
}

impl SerialNumber {
impl<P: Profile> SerialNumber<P> {
/// Maximum length in bytes for a [`SerialNumber`]
pub const MAX_LEN: Length = Length::new(20);

/// See notes in `SerialNumber::new` and `SerialNumber::decode_value`.
const MAX_DECODE_LEN: Length = Length::new(21);
pub(crate) const MAX_DECODE_LEN: Length = Length::new(21);

/// Create a new [`SerialNumber`] from a byte slice.
///
Expand All @@ -47,12 +50,13 @@ impl SerialNumber {
// RFC 5280 is ambiguous about whether this is valid, so we limit
// `SerialNumber` *encodings* to 20 bytes or fewer while permitting
// `SerialNumber` *decodings* to have up to 21 bytes below.
if inner.value_len()? > SerialNumber::MAX_LEN {
if inner.value_len()? > Self::MAX_LEN {
return Err(ErrorKind::Overlength.into());
}

Ok(Self {
inner: inner.into(),
_profile: PhantomData,
})
}

Expand All @@ -63,7 +67,7 @@ impl SerialNumber {
}
}

impl EncodeValue for SerialNumber {
impl<P: Profile> EncodeValue for SerialNumber<P> {
fn value_len(&self) -> Result<Length> {
self.inner.value_len()
}
Expand All @@ -73,22 +77,21 @@ impl EncodeValue for SerialNumber {
}
}

impl<'a> DecodeValue<'a> for SerialNumber {
impl<'a, P: Profile> DecodeValue<'a> for SerialNumber<P> {
fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> {
let inner = Int::decode_value(reader, header)?;
let serial = Self {
inner,
_profile: PhantomData,
};

// See the note in `SerialNumber::new`: we permit lengths of 21 bytes here,
// since some X.509 implementations interpret the limit of 20 bytes to refer
// to the pre-encoded value.
if inner.len() > SerialNumber::MAX_DECODE_LEN {
return Err(Tag::Integer.value_error());
}
P::check_serial_number(&serial)?;

Ok(Self { inner })
Ok(serial)
}
}

impl FixedTag for SerialNumber {
impl<P: Profile> FixedTag for SerialNumber<P> {
const TAG: Tag = <Int as FixedTag>::TAG;
}

Expand Down Expand Up @@ -131,7 +134,7 @@ impl_from!(usize);
// Implement by hand because the derive would create invalid values.
// Use the constructor to create a valid value.
#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for SerialNumber {
impl<'a, P: Profile> arbitrary::Arbitrary<'a> for SerialNumber<P> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let len = u.int_in_range(0u32..=Self::MAX_LEN.into())?;

Expand All @@ -154,7 +157,7 @@ mod tests {
// Creating a new serial with an oversized encoding (due to high MSB) fails.
{
let too_big = [0x80; 20];
assert!(SerialNumber::new(&too_big).is_err());
assert!(SerialNumber::<Rfc5280>::new(&too_big).is_err());
}

// Creating a new serial with the maximum encoding succeeds.
Expand All @@ -163,7 +166,7 @@ mod tests {
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
];
assert!(SerialNumber::new(&just_enough).is_ok());
assert!(SerialNumber::<Rfc5280>::new(&just_enough).is_ok());
}
}

Expand Down
24 changes: 24 additions & 0 deletions x509-cert/tests/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,27 @@ fn decode_cert_negative_serial_number() {
let reencoded = cert.to_der().unwrap();
assert_eq!(der_encoded_cert, reencoded.as_slice());
}

#[cfg(all(feature = "pem", feature = "hazmat"))]
#[test]
fn decode_cert_overlength_serial_number() {
use der::{pem::LineEnding, DecodePem, EncodePem};
use x509_cert::certificate::CertificateInner;

let pem_encoded_cert = include_bytes!("examples/qualcomm.pem");

assert!(Certificate::from_pem(pem_encoded_cert).is_err());

let cert = CertificateInner::<x509_cert::certificate::Raw>::from_pem(pem_encoded_cert).unwrap();
assert_eq!(
cert.tbs_certificate.serial_number.as_bytes(),
&[
0, 132, 206, 11, 246, 160, 254, 130, 78, 229, 229, 6, 202, 168, 157, 120, 198, 21, 1,
98, 87, 113
]
);
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 22);

let reencoded = cert.to_pem(LineEnding::LF).unwrap();
assert_eq!(pem_encoded_cert, reencoded.as_bytes());
}
16 changes: 16 additions & 0 deletions x509-cert/tests/examples/qualcomm.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICnDCCAiGgAwIBAgIWAITOC/ag/oJO5eUGyqideMYVAWJXcTAKBggqhkjOPQQD
AzB2MSQwIgYDVQQKDBtRdWFsY29tbSBUZWNobm9sb2dpZXMsIEluYy4xKjAoBgNV
BAsMIVF1YWxjb21tIENyeXB0b2dyYXBoaWMgT3BlcmF0aW9uczEiMCAGA1UEAwwZ
UU1DIEF0dGVzdGF0aW9uIFJvb3QgQ0EgNDAeFw0xNzA4MDEyMjE2MzJaFw0yNzA4
MDEyMjE2MzJaMH4xJDAiBgNVBAoMG1F1YWxjb21tIFRlY2hub2xvZ2llcywgSW5j
LjEqMCgGA1UECwwhUXVhbGNvbW0gQ3J5cHRvZ3JhcGhpYyBPcGVyYXRpb25zMSow
KAYDVQQDDCFRTUMgQXR0ZXN0YXRpb24gUm9vdCBDQSA0IFN1YkNBIDEwdjAQBgcq
hkjOPQIBBgUrgQQAIgNiAAQDsjssSUEFLyyBe17UmO3pMzqKS+V1jfQkhq7a7zmH
LCrPFmfaKLm0/szdzZxn+zwhoYen3fgJIuZUaip8wAQxLe4550c1ZBl3iSTvYUbe
J+gBz2DiJHRBOtY1bQH35NWjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P
AQH/BAQDAgEGMB0GA1UdDgQWBBTrVYStHPbaTn4k7bPerqZAmJcuXzAfBgNVHSME
GDAWgBQBBnkODO3o7rgWy996xOf1BxR4VTAKBggqhkjOPQQDAwNpADBmAjEAmpM/
Xvfawl4/A3jd0VVb6lOBh0Jy+zFz1Jz/hw+Xpm9G4XJCscBE7r7lbe2Xc1DHAjEA
psnskI8pLJQwL80QzAwP3HvgyDUeedNpxnYNK797vqJu6uRMLsZBVHatLM1R4gyE
-----END CERTIFICATE-----

0 comments on commit f570f59

Please sign in to comment.