Skip to content

Commit 5e294ca

Browse files
committed
x509-cert: add a CRL builder
1 parent 6b9ab0a commit 5e294ca

File tree

4 files changed

+309
-4
lines changed

4 files changed

+309
-4
lines changed

x509-cert/src/builder.rs

+122-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ use spki::{
1313
use crate::{
1414
AlgorithmIdentifier, SubjectPublicKeyInfo,
1515
certificate::{Certificate, TbsCertificate, Version},
16-
ext::{AsExtension, Extensions},
16+
crl::{CertificateList, RevokedCert, TbsCertList},
17+
ext::{
18+
AsExtension, Extensions,
19+
pkix::{AuthorityKeyIdentifier, CrlNumber, SubjectKeyIdentifier},
20+
},
1721
serial_number::SerialNumber,
18-
time::Validity,
22+
time::{Time, Validity},
1923
};
2024

2125
pub mod profile;
@@ -411,3 +415,119 @@ where
411415
<T as Builder>::finalize(self, signer)
412416
}
413417
}
418+
419+
/// X.509 CRL builder
420+
pub struct CrlBuilder {
421+
tbs: TbsCertList,
422+
}
423+
424+
impl CrlBuilder {
425+
/// Create a `CrlBuilder` with the given issuer and the given monotonic [`CrlNumber`]
426+
#[cfg(feature = "std")]
427+
pub fn new(issuer: &Certificate, crl_number: CrlNumber) -> der::Result<Self> {
428+
let this_update = Time::now()?;
429+
Self::new_with_this_update(issuer, crl_number, this_update)
430+
}
431+
432+
/// Create a `CrlBuilder` with the given issuer, a given monotonic [`CrlNumber`], and valid
433+
/// from the given `this_update` start validity date.
434+
pub fn new_with_this_update(
435+
issuer: &Certificate,
436+
crl_number: CrlNumber,
437+
this_update: Time,
438+
) -> der::Result<Self> {
439+
// Replaced later when the finalize is called
440+
let signature_alg = AlgorithmIdentifier {
441+
oid: NULL_OID,
442+
parameters: None,
443+
};
444+
445+
let issuer_name = issuer.tbs_certificate.subject().clone();
446+
447+
let mut crl_extensions = Extensions::new();
448+
crl_extensions.push(crl_number.to_extension(&issuer_name, &crl_extensions)?);
449+
let aki = match issuer
450+
.tbs_certificate
451+
.get_extension::<AuthorityKeyIdentifier>()?
452+
{
453+
Some((_, aki)) => aki,
454+
None => {
455+
let ski = SubjectKeyIdentifier::try_from(
456+
issuer
457+
.tbs_certificate
458+
.subject_public_key_info()
459+
.owned_to_ref(),
460+
)?;
461+
AuthorityKeyIdentifier {
462+
// KeyIdentifier must be the same as subjectKeyIdentifier
463+
key_identifier: Some(ski.0.clone()),
464+
// other fields must not be present.
465+
..Default::default()
466+
}
467+
}
468+
};
469+
crl_extensions.push(aki.to_extension(&issuer_name, &crl_extensions)?);
470+
471+
let tbs = TbsCertList {
472+
version: Version::V2,
473+
signature: signature_alg,
474+
issuer: issuer_name,
475+
this_update,
476+
next_update: None,
477+
revoked_certificates: None,
478+
crl_extensions: Some(crl_extensions),
479+
};
480+
481+
Ok(Self { tbs })
482+
}
483+
484+
/// Make the CRL valid until the given `next_update`
485+
pub fn with_next_update(mut self, next_update: Option<Time>) -> Self {
486+
self.tbs.next_update = next_update;
487+
self
488+
}
489+
490+
/// Add certificates to the revocation list
491+
pub fn with_certificates<I>(mut self, revoked: I) -> Self
492+
where
493+
I: Iterator<Item = RevokedCert>,
494+
{
495+
let certificates = self
496+
.tbs
497+
.revoked_certificates
498+
.get_or_insert_with(vec::Vec::new);
499+
500+
let mut revoked: vec::Vec<RevokedCert> = revoked.collect();
501+
certificates.append(&mut revoked);
502+
503+
self
504+
}
505+
}
506+
507+
impl Builder for CrlBuilder {
508+
type Output = CertificateList;
509+
510+
fn finalize<S>(&mut self, cert_signer: &S) -> Result<vec::Vec<u8>>
511+
where
512+
S: Keypair + DynSignatureAlgorithmIdentifier,
513+
S::VerifyingKey: EncodePublicKey,
514+
{
515+
self.tbs.signature = cert_signer.signature_algorithm_identifier()?;
516+
517+
self.tbs.to_der().map_err(Error::from)
518+
}
519+
520+
fn assemble<S>(self, signature: BitString, _signer: &S) -> Result<Self::Output>
521+
where
522+
S: Keypair + DynSignatureAlgorithmIdentifier,
523+
S::VerifyingKey: EncodePublicKey,
524+
{
525+
let signature_algorithm = self.tbs.signature.clone();
526+
527+
Ok(CertificateList {
528+
tbs_cert_list: self.tbs,
529+
signature_algorithm,
530+
signature,
531+
})
532+
}
533+
}

x509-cert/src/crl.rs

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use alloc::vec::Vec;
1414
use der::asn1::BitString;
1515
use der::{Sequence, ValueOrd};
1616

17+
#[cfg(feature = "pem")]
18+
use der::pem::PemLabel;
19+
1720
/// `CertificateList` as defined in [RFC 5280 Section 5.1].
1821
///
1922
/// ```text
@@ -33,6 +36,11 @@ pub struct CertificateList<P: Profile = Rfc5280> {
3336
pub signature: BitString,
3437
}
3538

39+
#[cfg(feature = "pem")]
40+
impl<P: Profile> PemLabel for CertificateList<P> {
41+
const PEM_LABEL: &'static str = "X509 CRL";
42+
}
43+
3644
/// Implicit intermediate structure from the ASN.1 definition of `TBSCertList`.
3745
///
3846
/// This type is used for the `revoked_certificates` field of `TbsCertList`.

x509-cert/test-support/src/openssl.rs

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
fs::File,
2+
fs::{self, File},
33
io::{Read, Write},
44
process::{Command, Stdio},
55
};
@@ -21,7 +21,7 @@ fn check_openssl_output(command_and_args: &[&str], pem: &[u8]) -> String {
2121
.stderr(Stdio::inherit())
2222
.stdout(Stdio::piped())
2323
.spawn()
24-
.expect("zlint failed");
24+
.expect("openssl failed");
2525
let mut stdout = child.stdout.take().unwrap();
2626
let exit_status = child.wait().expect("get openssl x509 status");
2727

@@ -38,6 +38,44 @@ pub fn check_certificate(pem: &[u8]) -> String {
3838
check_openssl_output(&["x509"], pem)
3939
}
4040

41+
pub fn check_crl(pem: &[u8]) -> String {
42+
check_openssl_output(&["crl"], pem)
43+
}
44+
4145
pub fn check_request(pem: &[u8]) -> String {
4246
check_openssl_output(&["req", "-verify"], pem)
4347
}
48+
49+
pub fn verify(trust_anchor: &[u8], leaf: &[u8], crl: &[u8]) -> String {
50+
let tmp_dir = tempdir().expect("create tempdir");
51+
let trust_anchor_path = tmp_dir.path().join("trust_anchor.pem");
52+
let leaf_path = tmp_dir.path().join("leaf.pem");
53+
let crl_path = tmp_dir.path().join("crl.pem");
54+
55+
fs::write(&trust_anchor_path, trust_anchor).expect("Write trust anchor");
56+
fs::write(&leaf_path, leaf).expect("Write leaf");
57+
fs::write(&crl_path, crl).expect("Write crl");
58+
59+
let mut child = Command::new("openssl")
60+
.arg("verify")
61+
.arg("-CRLfile")
62+
.arg(&crl_path)
63+
.arg("-trusted")
64+
.arg(&trust_anchor_path)
65+
.arg("--")
66+
.arg(&leaf_path)
67+
.stderr(Stdio::inherit())
68+
.stdout(Stdio::piped())
69+
.spawn()
70+
.expect("openssl failed");
71+
let mut stdout = child.stdout.take().unwrap();
72+
let exit_status = child.wait().expect("get openssl verify status");
73+
74+
assert!(exit_status.success(), "openssl failed");
75+
let mut output_buf = Vec::new();
76+
stdout
77+
.read_to_end(&mut output_buf)
78+
.expect("read openssl output");
79+
80+
String::from_utf8(output_buf.clone()).unwrap()
81+
}

x509-cert/tests/builder_crl.rs

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#![cfg(all(feature = "builder", feature = "pem", feature = "std"))]
2+
3+
use std::{str::FromStr, time::Duration};
4+
5+
use der::{EncodePem, pem::LineEnding};
6+
use p256::{NistP256, ecdsa::DerSignature, pkcs8::DecodePrivateKey};
7+
use rand::rng;
8+
use x509_cert::{
9+
SubjectPublicKeyInfo,
10+
builder::{
11+
Builder, CertificateBuilder, CrlBuilder,
12+
profile::{self, cabf::tls::CertificateType},
13+
},
14+
crl::RevokedCert,
15+
ext::pkix::{CrlNumber, name::GeneralNames},
16+
name::Name,
17+
serial_number::SerialNumber,
18+
time::{Time, Validity},
19+
};
20+
use x509_cert_test_support::openssl;
21+
22+
const PKCS8_PUBLIC_KEY_DER: &[u8] = include_bytes!("examples/p256-pub.der");
23+
const PKCS8_PRIVATE_KEY_DER: &[u8] = include_bytes!("examples/p256-priv.der");
24+
25+
fn ecdsa_signer() -> ecdsa::SigningKey<NistP256> {
26+
let secret_key = p256::SecretKey::from_pkcs8_der(PKCS8_PRIVATE_KEY_DER).unwrap();
27+
ecdsa::SigningKey::from(secret_key)
28+
}
29+
30+
#[test]
31+
fn crl_signer() {
32+
let mut rng = rng();
33+
let serial_number = SerialNumber::generate(&mut rng);
34+
let validity = Validity::from_now(Duration::new(5, 0)).unwrap();
35+
let subject =
36+
Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap();
37+
let profile = profile::cabf::Root::new(false, subject).expect("create root profile");
38+
let pub_key = SubjectPublicKeyInfo::try_from(PKCS8_PUBLIC_KEY_DER).expect("get ecdsa pub key");
39+
40+
let signer = ecdsa_signer();
41+
let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key)
42+
.expect("Create certificate");
43+
44+
let ca_certificate = builder.build::<_, DerSignature>(&signer).unwrap();
45+
46+
let crl_number = CrlNumber::try_from(42u128).unwrap();
47+
48+
let builder = CrlBuilder::new(&ca_certificate, crl_number)
49+
.unwrap()
50+
.with_certificates(
51+
vec![
52+
RevokedCert {
53+
serial_number: SerialNumber::generate(&mut rng),
54+
revocation_date: Time::now().unwrap(),
55+
crl_entry_extensions: None,
56+
},
57+
RevokedCert {
58+
serial_number: SerialNumber::generate(&mut rng),
59+
revocation_date: Time::now().unwrap(),
60+
crl_entry_extensions: None,
61+
},
62+
]
63+
.into_iter(),
64+
);
65+
66+
let crl = builder.build::<_, DerSignature>(&signer).unwrap();
67+
68+
let pem = crl.to_pem(LineEnding::LF).expect("generate pem");
69+
println!("{}", openssl::check_crl(pem.as_bytes()));
70+
}
71+
72+
/// Use `openssl verify` to run a mock certificate chain against a newly signed CRL.
73+
#[test]
74+
fn crl_verify() {
75+
let mut rng = rng();
76+
let signer = ecdsa_signer();
77+
78+
let serial_number = SerialNumber::generate(&mut rng);
79+
let validity = Validity::from_now(Duration::new(60, 0)).unwrap();
80+
let subject = Name::from_str("CN=root,O=World domination Inc,C=US").unwrap();
81+
let profile = profile::cabf::Root::new(false, subject.clone()).expect("create root profile");
82+
let pub_key = SubjectPublicKeyInfo::try_from(PKCS8_PUBLIC_KEY_DER).expect("get ecdsa pub key");
83+
84+
let builder = CertificateBuilder::new(profile, serial_number, validity, pub_key)
85+
.expect("Create certificate");
86+
87+
let ca_certificate = builder.build::<_, DerSignature>(&signer).unwrap();
88+
89+
let serial_number = SerialNumber::generate(&mut rng);
90+
let delegated = Name::from_str("CN=example.com,O=World domination Inc,C=US").unwrap();
91+
let profile = profile::cabf::tls::Subscriber {
92+
certificate_type: CertificateType::domain_validated(delegated, GeneralNames::new())
93+
.expect("create domain validated"),
94+
issuer: subject,
95+
client_auth: false,
96+
#[cfg(feature = "hazmat")]
97+
tls12_options: Default::default(),
98+
#[cfg(feature = "hazmat")]
99+
enable_data_encipherment: false,
100+
};
101+
let pub_key = SubjectPublicKeyInfo::try_from(PKCS8_PUBLIC_KEY_DER).expect("get ecdsa pub key");
102+
103+
let builder = CertificateBuilder::new(profile, serial_number.clone(), validity, pub_key)
104+
.expect("Create certificate");
105+
106+
let leaf_certificate = builder.build::<_, DerSignature>(&signer).unwrap();
107+
108+
let crl_number = CrlNumber::try_from(42u128).unwrap();
109+
110+
let builder = CrlBuilder::new(&ca_certificate, crl_number)
111+
.unwrap()
112+
.with_certificates(
113+
vec![RevokedCert {
114+
serial_number,
115+
revocation_date: Time::now().unwrap(),
116+
crl_entry_extensions: None,
117+
}]
118+
.into_iter(),
119+
);
120+
121+
let crl = builder.build::<_, DerSignature>(&signer).unwrap();
122+
123+
println!(
124+
"{}",
125+
openssl::verify(
126+
ca_certificate
127+
.to_pem(LineEnding::LF)
128+
.expect("ca: generate pem")
129+
.as_bytes(),
130+
leaf_certificate
131+
.to_pem(LineEnding::LF)
132+
.expect("leaf: generate pem")
133+
.as_bytes(),
134+
crl.to_pem(LineEnding::LF)
135+
.expect("crl: generate pem")
136+
.as_bytes(),
137+
)
138+
);
139+
}

0 commit comments

Comments
 (0)