Skip to content

x509-cert: provide parsing profiles #987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-----