Skip to content

Commit c1e4f94

Browse files
committed
x509-cert: provide parsing profiles
This allow the user to relax checks when parsing certificate and cover for non rfc5280 compliant x509 certificates.
1 parent 7b40bd5 commit c1e4f94

File tree

5 files changed

+110
-30
lines changed

5 files changed

+110
-30
lines changed

x509-cert/src/builder.rs

+2
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ where
283283
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.8
284284
issuer_unique_id: None,
285285
subject_unique_id: None,
286+
287+
_profile: Default::default(),
286288
};
287289

288290
let extensions = profile.build_extensions(

x509-cert/src/certificate.rs

+45-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
@@ -73,7 +108,7 @@ impl Default for Version {
73108
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
74109
#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
75110
#[allow(missing_docs)]
76-
pub struct TbsCertificate {
111+
pub struct TbsCertificate<P: Profile = Rfc5280> {
77112
/// The certificate version
78113
///
79114
/// Note that this value defaults to Version 1 per the RFC. However,
@@ -83,7 +118,7 @@ pub struct TbsCertificate {
83118
#[asn1(context_specific = "0", default = "Default::default")]
84119
pub version: Version,
85120

86-
pub serial_number: SerialNumber,
121+
pub serial_number: SerialNumber<P>,
87122
pub signature: AlgorithmIdentifierOwned,
88123
pub issuer: Name,
89124
pub validity: Validity,
@@ -98,9 +133,11 @@ pub struct TbsCertificate {
98133

99134
#[asn1(context_specific = "3", tag_mode = "EXPLICIT", optional = "true")]
100135
pub extensions: Option<crate::ext::Extensions>,
136+
137+
pub(crate) _profile: PhantomData<P>,
101138
}
102139

103-
impl TbsCertificate {
140+
impl<P: Profile> TbsCertificate<P> {
104141
/// Decodes a single extension
105142
///
106143
/// Returns an error if multiple of these extensions is present. Returns
@@ -146,14 +183,14 @@ impl TbsCertificate {
146183
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
147184
#[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)]
148185
#[allow(missing_docs)]
149-
pub struct Certificate {
150-
pub tbs_certificate: TbsCertificate,
186+
pub struct Certificate<P: Profile = Rfc5280> {
187+
pub tbs_certificate: TbsCertificate<P>,
151188
pub signature_algorithm: AlgorithmIdentifierOwned,
152189
pub signature: BitString,
153190
}
154191

155192
#[cfg(feature = "pem")]
156-
impl PemLabel for Certificate {
193+
impl<P: Profile> PemLabel for Certificate<P> {
157194
const PEM_LABEL: &'static str = "CERTIFICATE";
158195
}
159196

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

+25-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use der::{
66
};
77
use hex_literal::hex;
88
use spki::AlgorithmIdentifierRef;
9+
use x509_cert::certificate::Rfc5280;
910
use x509_cert::serial_number::SerialNumber;
1011
use x509_cert::Certificate;
1112
use x509_cert::*;
1213

1314
#[cfg(feature = "pem")]
14-
use der::DecodePem;
15+
use der::{pem::LineEnding, DecodePem, EncodePem};
1516

1617
// TODO - parse and compare extension values
1718
const EXTENSIONS: &[(&str, bool)] = &[
@@ -116,7 +117,7 @@ fn reencode_cert() {
116117
include_bytes!("examples/026EDA6FA1EDFA8C253936C75B5EEBD954BFF452.fake.der");
117118
let defer_cert = DeferDecodeCertificate::from_der(der_encoded_cert).unwrap();
118119

119-
let parsed_tbs = TbsCertificate::from_der(defer_cert.tbs_certificate).unwrap();
120+
let parsed_tbs = TbsCertificate::<Rfc5280>::from_der(defer_cert.tbs_certificate).unwrap();
120121
let reencoded_tbs = parsed_tbs.to_der().unwrap();
121122
assert_eq!(defer_cert.tbs_certificate, reencoded_tbs);
122123

@@ -402,7 +403,7 @@ fn decode_cert() {
402403
fn decode_cert_negative_serial_number() {
403404
let der_encoded_cert = include_bytes!("examples/28903a635b5280fae6774c0b6da7d6baa64af2e8.der");
404405

405-
let cert = Certificate::from_der(der_encoded_cert).unwrap();
406+
let cert = Certificate::<Rfc5280>::from_der(der_encoded_cert).unwrap();
406407
assert_eq!(
407408
cert.tbs_certificate.serial_number.as_bytes(),
408409
// INTEGER (125 bit) -2.370157924795571e+37
@@ -412,3 +413,24 @@ fn decode_cert_negative_serial_number() {
412413
let reencoded = cert.to_der().unwrap();
413414
assert_eq!(der_encoded_cert, reencoded.as_slice());
414415
}
416+
417+
#[cfg(all(feature = "pem", feature = "hazmat"))]
418+
#[test]
419+
fn decode_cert_overlength_serial_number() {
420+
let pem_encoded_cert = include_bytes!("examples/qualcomm.pem");
421+
422+
assert!(Certificate::<Rfc5280>::from_pem(pem_encoded_cert).is_err());
423+
424+
let cert = Certificate::<x509_cert::certificate::Raw>::from_pem(pem_encoded_cert).unwrap();
425+
assert_eq!(
426+
cert.tbs_certificate.serial_number.as_bytes(),
427+
&[
428+
0, 132, 206, 11, 246, 160, 254, 130, 78, 229, 229, 6, 202, 168, 157, 120, 198, 21, 1,
429+
98, 87, 113
430+
]
431+
);
432+
assert_eq!(cert.tbs_certificate.serial_number.as_bytes().len(), 22);
433+
434+
let reencoded = cert.to_pem(LineEnding::LF).unwrap();
435+
assert_eq!(pem_encoded_cert, reencoded.as_bytes());
436+
}

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)