Skip to content

Commit f570f59

Browse files
authored
x509-cert: provide parsing profiles (#987)
This allow the user to relax checks when parsing certificate and cover for non rfc5280 compliant x509 certificates.
1 parent 4309bc2 commit f570f59

File tree

6 files changed

+118
-28
lines changed

6 files changed

+118
-28
lines changed

x509-cert/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ rust-version = "1.65"
1616

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

2222
# optional dependencies

x509-cert/src/builder.rs

+2
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@ where
280280
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.8
281281
issuer_unique_id: None,
282282
subject_unique_id: None,
283+
284+
_profile: Default::default(),
283285
};
284286

285287
let extensions = profile.build_extensions(

x509-cert/src/certificate.rs

+53-8
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,49 @@
33
use crate::{name::Name, serial_number::SerialNumber, time::Validity};
44
use alloc::vec::Vec;
55
use const_oid::AssociatedOid;
6-
use core::cmp::Ordering;
6+
use core::{cmp::Ordering, fmt::Debug, marker::PhantomData};
77
use der::asn1::BitString;
8-
use der::{Decode, Enumerated, Error, ErrorKind, Sequence, ValueOrd};
8+
use der::{Decode, Enumerated, Error, ErrorKind, Sequence, Tag, ValueOrd};
99
use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};
1010

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

14+
/// [`Profile`] allows the consumer of this crate to customize the behavior when parsing
15+
/// certificates.
16+
/// By default, parsing will be made in a rfc5280-compliant manner.
17+
pub trait Profile: PartialEq + Debug + Eq + Clone {
18+
/// Checks to run when parsing serial numbers
19+
fn check_serial_number(serial: &SerialNumber<Self>) -> der::Result<()> {
20+
// See the note in `SerialNumber::new`: we permit lengths of 21 bytes here,
21+
// since some X.509 implementations interpret the limit of 20 bytes to refer
22+
// to the pre-encoded value.
23+
if serial.inner.len() > SerialNumber::<Self>::MAX_DECODE_LEN {
24+
Err(Tag::Integer.value_error())
25+
} else {
26+
Ok(())
27+
}
28+
}
29+
}
30+
31+
#[derive(Debug, PartialEq, Eq, Clone)]
32+
/// Parse certificates with rfc5280-compliant checks
33+
pub struct Rfc5280;
34+
35+
impl Profile for Rfc5280 {}
36+
37+
#[cfg(feature = "hazmat")]
38+
#[derive(Debug, PartialEq, Eq, Clone)]
39+
/// Parse raw x509 certificate and disable all the checks
40+
pub struct Raw;
41+
42+
#[cfg(feature = "hazmat")]
43+
impl Profile for Raw {
44+
fn check_serial_number(_serial: &SerialNumber<Self>) -> der::Result<()> {
45+
Ok(())
46+
}
47+
}
48+
1449
/// Certificate `Version` as defined in [RFC 5280 Section 4.1].
1550
///
1651
/// ```text
@@ -45,6 +80,9 @@ impl Default for Version {
4580
}
4681
}
4782

83+
/// X.509 `TbsCertificate` as defined in [RFC 5280 Section 4.1]
84+
pub type TbsCertificate = TbsCertificateInner<Rfc5280>;
85+
4886
/// X.509 `TbsCertificate` as defined in [RFC 5280 Section 4.1]
4987
///
5088
/// ASN.1 structure containing the names of the subject and issuer, a public
@@ -73,7 +111,7 @@ impl Default for Version {
73111
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
74112
#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
75113
#[allow(missing_docs)]
76-
pub struct TbsCertificate {
114+
pub struct TbsCertificateInner<P: Profile = Rfc5280> {
77115
/// The certificate version
78116
///
79117
/// Note that this value defaults to Version 1 per the RFC. However,
@@ -83,7 +121,7 @@ pub struct TbsCertificate {
83121
#[asn1(context_specific = "0", default = "Default::default")]
84122
pub version: Version,
85123

86-
pub serial_number: SerialNumber,
124+
pub serial_number: SerialNumber<P>,
87125
pub signature: AlgorithmIdentifierOwned,
88126
pub issuer: Name,
89127
pub validity: Validity,
@@ -98,9 +136,11 @@ pub struct TbsCertificate {
98136

99137
#[asn1(context_specific = "3", tag_mode = "EXPLICIT", optional = "true")]
100138
pub extensions: Option<crate::ext::Extensions>,
139+
140+
pub(crate) _profile: PhantomData<P>,
101141
}
102142

103-
impl TbsCertificate {
143+
impl<P: Profile> TbsCertificateInner<P> {
104144
/// Decodes a single extension
105145
///
106146
/// Returns an error if multiple of these extensions is present. Returns
@@ -132,6 +172,11 @@ impl TbsCertificate {
132172
}
133173
}
134174

175+
/// X.509 certificates are defined in [RFC 5280 Section 4.1].
176+
///
177+
/// [RFC 5280 Section 4.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1
178+
pub type Certificate = CertificateInner<Rfc5280>;
179+
135180
/// X.509 certificates are defined in [RFC 5280 Section 4.1].
136181
///
137182
/// ```text
@@ -146,14 +191,14 @@ impl TbsCertificate {
146191
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
147192
#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
148193
#[allow(missing_docs)]
149-
pub struct Certificate {
150-
pub tbs_certificate: TbsCertificate,
194+
pub struct CertificateInner<P: Profile = Rfc5280> {
195+
pub tbs_certificate: TbsCertificateInner<P>,
151196
pub signature_algorithm: AlgorithmIdentifierOwned,
152197
pub signature: BitString,
153198
}
154199

155200
#[cfg(feature = "pem")]
156-
impl PemLabel for Certificate {
201+
impl<P: Profile> PemLabel for CertificateInner<P> {
157202
const PEM_LABEL: &'static str = "CERTIFICATE";
158203
}
159204

x509-cert/src/serial_number.rs

+22-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
//! X.509 serial number
22
3-
use core::fmt::Display;
3+
use core::{fmt::Display, marker::PhantomData};
44

55
use der::{
66
asn1::{self, Int},
77
DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd,
88
Writer,
99
};
1010

11+
use crate::certificate::{Profile, Rfc5280};
12+
1113
/// [RFC 5280 Section 4.1.2.2.] Serial Number
1214
///
1315
/// The serial number MUST be a positive integer assigned by the CA to
@@ -25,16 +27,17 @@ use der::{
2527
/// that are negative or zero. Certificate users SHOULD be prepared to
2628
/// gracefully handle such certificates.
2729
#[derive(Clone, Debug, Eq, PartialEq, ValueOrd, PartialOrd, Ord)]
28-
pub struct SerialNumber {
29-
inner: Int,
30+
pub struct SerialNumber<P: Profile = Rfc5280> {
31+
pub(crate) inner: Int,
32+
_profile: PhantomData<P>,
3033
}
3134

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

3639
/// See notes in `SerialNumber::new` and `SerialNumber::decode_value`.
37-
const MAX_DECODE_LEN: Length = Length::new(21);
40+
pub(crate) const MAX_DECODE_LEN: Length = Length::new(21);
3841

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

5457
Ok(Self {
5558
inner: inner.into(),
59+
_profile: PhantomData,
5660
})
5761
}
5862

@@ -63,7 +67,7 @@ impl SerialNumber {
6367
}
6468
}
6569

66-
impl EncodeValue for SerialNumber {
70+
impl<P: Profile> EncodeValue for SerialNumber<P> {
6771
fn value_len(&self) -> Result<Length> {
6872
self.inner.value_len()
6973
}
@@ -73,22 +77,21 @@ impl EncodeValue for SerialNumber {
7377
}
7478
}
7579

76-
impl<'a> DecodeValue<'a> for SerialNumber {
80+
impl<'a, P: Profile> DecodeValue<'a> for SerialNumber<P> {
7781
fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> {
7882
let inner = Int::decode_value(reader, header)?;
83+
let serial = Self {
84+
inner,
85+
_profile: PhantomData,
86+
};
7987

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

87-
Ok(Self { inner })
90+
Ok(serial)
8891
}
8992
}
9093

91-
impl FixedTag for SerialNumber {
94+
impl<P: Profile> FixedTag for SerialNumber<P> {
9295
const TAG: Tag = <Int as FixedTag>::TAG;
9396
}
9497

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

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

160163
// Creating a new serial with the maximum encoding succeeds.
@@ -163,7 +166,7 @@ mod tests {
163166
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
164167
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
165168
];
166-
assert!(SerialNumber::new(&just_enough).is_ok());
169+
assert!(SerialNumber::<Rfc5280>::new(&just_enough).is_ok());
167170
}
168171
}
169172

x509-cert/tests/certificate.rs

+24
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,27 @@ fn decode_cert_negative_serial_number() {
412412
let reencoded = cert.to_der().unwrap();
413413
assert_eq!(der_encoded_cert, reencoded.as_slice());
414414
}
415+
416+
#[cfg(all(feature = "pem", feature = "hazmat"))]
417+
#[test]
418+
fn decode_cert_overlength_serial_number() {
419+
use der::{pem::LineEnding, DecodePem, EncodePem};
420+
use x509_cert::certificate::CertificateInner;
421+
422+
let pem_encoded_cert = include_bytes!("examples/qualcomm.pem");
423+
424+
assert!(Certificate::from_pem(pem_encoded_cert).is_err());
425+
426+
let cert = CertificateInner::<x509_cert::certificate::Raw>::from_pem(pem_encoded_cert).unwrap();
427+
assert_eq!(
428+
cert.tbs_certificate.serial_number.as_bytes(),
429+
&[
430+
0, 132, 206, 11, 246, 160, 254, 130, 78, 229, 229, 6, 202, 168, 157, 120, 198, 21, 1,
431+
98, 87, 113
432+
]
433+
);
434+
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 22);
435+
436+
let reencoded = cert.to_pem(LineEnding::LF).unwrap();
437+
assert_eq!(pem_encoded_cert, reencoded.as_bytes());
438+
}

x509-cert/tests/examples/qualcomm.pem

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICnDCCAiGgAwIBAgIWAITOC/ag/oJO5eUGyqideMYVAWJXcTAKBggqhkjOPQQD
3+
AzB2MSQwIgYDVQQKDBtRdWFsY29tbSBUZWNobm9sb2dpZXMsIEluYy4xKjAoBgNV
4+
BAsMIVF1YWxjb21tIENyeXB0b2dyYXBoaWMgT3BlcmF0aW9uczEiMCAGA1UEAwwZ
5+
UU1DIEF0dGVzdGF0aW9uIFJvb3QgQ0EgNDAeFw0xNzA4MDEyMjE2MzJaFw0yNzA4
6+
MDEyMjE2MzJaMH4xJDAiBgNVBAoMG1F1YWxjb21tIFRlY2hub2xvZ2llcywgSW5j
7+
LjEqMCgGA1UECwwhUXVhbGNvbW0gQ3J5cHRvZ3JhcGhpYyBPcGVyYXRpb25zMSow
8+
KAYDVQQDDCFRTUMgQXR0ZXN0YXRpb24gUm9vdCBDQSA0IFN1YkNBIDEwdjAQBgcq
9+
hkjOPQIBBgUrgQQAIgNiAAQDsjssSUEFLyyBe17UmO3pMzqKS+V1jfQkhq7a7zmH
10+
LCrPFmfaKLm0/szdzZxn+zwhoYen3fgJIuZUaip8wAQxLe4550c1ZBl3iSTvYUbe
11+
J+gBz2DiJHRBOtY1bQH35NWjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P
12+
AQH/BAQDAgEGMB0GA1UdDgQWBBTrVYStHPbaTn4k7bPerqZAmJcuXzAfBgNVHSME
13+
GDAWgBQBBnkODO3o7rgWy996xOf1BxR4VTAKBggqhkjOPQQDAwNpADBmAjEAmpM/
14+
Xvfawl4/A3jd0VVb6lOBh0Jy+zFz1Jz/hw+Xpm9G4XJCscBE7r7lbe2Xc1DHAjEA
15+
psnskI8pLJQwL80QzAwP3HvgyDUeedNpxnYNK797vqJu6uRMLsZBVHatLM1R4gyE
16+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)