Skip to content

Commit 3a1fc2a

Browse files
committed
x509-cert: adds a CertReq builder
1 parent 17af6ab commit 3a1fc2a

9 files changed

Lines changed: 215 additions & 38 deletions

File tree

x509-cert/src/builder.rs

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ use crate::{
1515
pkix::{
1616
AuthorityKeyIdentifier, BasicConstraints, KeyUsage, KeyUsages, SubjectKeyIdentifier,
1717
},
18-
AsExtension, Extension,
18+
AsExtension, Extension, Extensions,
1919
},
2020
name::Name,
21+
request::{CertReq, CertReqInfo, ExtensionReq},
2122
serial_number::SerialNumber,
2223
time::Validity,
2324
};
@@ -142,15 +143,19 @@ impl Profile {
142143
include_subject_key_identifier: false,
143144
..
144145
} => {}
145-
_ => extensions.push(SubjectKeyIdentifier::try_from(spk)?.to_extension(tbs)?),
146+
_ => extensions.push(
147+
SubjectKeyIdentifier::try_from(spk)?.to_extension(&tbs.subject, &extensions)?,
148+
),
146149
}
147150

148151
// Build Authority Key Identifier
149152
match self {
150153
Profile::Root => {}
151154
_ => {
152-
extensions
153-
.push(AuthorityKeyIdentifier::try_from(issuer_spk.clone())?.to_extension(tbs)?);
155+
extensions.push(
156+
AuthorityKeyIdentifier::try_from(issuer_spk.clone())?
157+
.to_extension(&tbs.subject, &extensions)?,
158+
);
154159
}
155160
}
156161

@@ -160,29 +165,31 @@ impl Profile {
160165
ca: true,
161166
path_len_constraint: None,
162167
}
163-
.to_extension(tbs)?,
168+
.to_extension(&tbs.subject, &extensions)?,
164169
Profile::SubCA {
165170
path_len_constraint,
166171
..
167172
} => BasicConstraints {
168173
ca: true,
169174
path_len_constraint: *path_len_constraint,
170175
}
171-
.to_extension(tbs)?,
176+
.to_extension(&tbs.subject, &extensions)?,
172177
Profile::Leaf { .. } => BasicConstraints {
173178
ca: false,
174179
path_len_constraint: None,
175180
}
176-
.to_extension(tbs)?,
181+
.to_extension(&tbs.subject, &extensions)?,
177182
#[cfg(feature = "hazmat")]
178183
Profile::Manual { .. } => unreachable!(),
179184
});
180185

181186
// Build Key Usage extension
182187
match self {
183188
Profile::Root | Profile::SubCA { .. } => {
184-
extensions
185-
.push(KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign).to_extension(tbs)?);
189+
extensions.push(
190+
KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign)
191+
.to_extension(&tbs.subject, &extensions)?,
192+
);
186193
}
187194
Profile::Leaf {
188195
enable_key_agreement,
@@ -197,7 +204,7 @@ impl Profile {
197204
key_usage |= KeyUsages::KeyAgreement;
198205
}
199206

200-
extensions.push(KeyUsage(key_usage).to_extension(tbs)?);
207+
extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?);
201208
}
202209
#[cfg(feature = "hazmat")]
203210
Profile::Manual { .. } => unreachable!(),
@@ -250,6 +257,7 @@ impl Profile {
250257
/// ```
251258
pub struct CertificateBuilder<'s, S> {
252259
tbs: TbsCertificate,
260+
extensions: Extensions,
253261
signer: &'s S,
254262
}
255263

@@ -302,30 +310,26 @@ where
302310
signer_pub.owned_to_ref(),
303311
&tbs,
304312
)?;
305-
if !extensions.is_empty() {
306-
tbs.extensions = Some(extensions);
307-
}
308-
309-
Ok(Self { tbs, signer })
313+
Ok(Self {
314+
tbs,
315+
extensions,
316+
signer,
317+
})
310318
}
311319

312320
/// Add an extension to this certificate
313321
pub fn add_extension<E: AsExtension>(&mut self, extension: &E) -> Result<()> {
314-
if self.tbs.version == Version::V3 {
315-
let ext = extension.to_extension(&self.tbs)?;
316-
317-
if let Some(extensions) = self.tbs.extensions.as_mut() {
318-
extensions.push(ext);
319-
} else {
320-
let extensions = vec![ext];
321-
self.tbs.extensions = Some(extensions);
322-
}
323-
}
322+
let ext = extension.to_extension(&self.tbs.subject, &self.extensions)?;
323+
self.extensions.push(ext);
324324

325325
Ok(())
326326
}
327327

328328
fn finalize(&mut self) {
329+
if !self.extensions.is_empty() {
330+
self.tbs.extensions = Some(self.extensions.clone());
331+
}
332+
329333
if self.tbs.extensions.is_none() {
330334
if self.tbs.issuer_unique_id.is_some() || self.tbs.subject_unique_id.is_some() {
331335
self.tbs.version = Version::V2;
@@ -375,3 +379,95 @@ where
375379
Ok(cert)
376380
}
377381
}
382+
383+
pub struct RequestBuilder<'s, S> {
384+
info: CertReqInfo,
385+
extension_req: ExtensionReq,
386+
signer: &'s S,
387+
}
388+
389+
impl<'s, S> RequestBuilder<'s, S>
390+
where
391+
S: Keypair + DynSignatureAlgorithmIdentifier,
392+
S::VerifyingKey: EncodePublicKey,
393+
{
394+
/// Creates a new certificate request builder
395+
pub fn new(subject: Name, signer: &'s S) -> Result<Self> {
396+
let version = Default::default();
397+
let verifying_key = signer.verifying_key();
398+
let public_key = verifying_key
399+
.to_public_key_der()?
400+
.decode_msg::<SubjectPublicKeyInfoOwned>()?;
401+
let attributes = Default::default();
402+
let extension_req = Default::default();
403+
404+
Ok(Self {
405+
info: CertReqInfo {
406+
version,
407+
subject,
408+
public_key,
409+
attributes,
410+
},
411+
extension_req,
412+
signer,
413+
})
414+
}
415+
416+
/// Add an extension to this certificate request
417+
pub fn add_extension<E: AsExtension>(&mut self, extension: &E) -> Result<()> {
418+
let ext = extension.to_extension(&self.info.subject, &self.extension_req.0)?;
419+
420+
self.extension_req.0.push(ext);
421+
422+
Ok(())
423+
}
424+
425+
fn finalize(&mut self) -> Result<()> {
426+
self.info
427+
.attributes
428+
.add(self.extension_req.clone().try_into()?);
429+
Ok(())
430+
}
431+
432+
/// Run the certificate through the signer and build the end certificate.
433+
pub fn build<Signature>(mut self) -> Result<CertReq>
434+
where
435+
S: Signer<Signature>,
436+
Signature: SignatureEncoding,
437+
{
438+
self.finalize();
439+
440+
let algorithm = self.signer.signature_algorithm_identifier()?;
441+
let signature = self.signer.try_sign(&self.info.to_der()?)?;
442+
let signature = BitString::from_bytes(signature.to_bytes().as_ref())?;
443+
444+
let req = CertReq {
445+
info: self.info,
446+
algorithm,
447+
signature,
448+
};
449+
450+
Ok(req)
451+
}
452+
453+
/// Run the certificate through the signer and build the end certificate.
454+
pub fn build_with_rng<Signature>(mut self, rng: &mut impl CryptoRngCore) -> Result<CertReq>
455+
where
456+
S: RandomizedSigner<Signature>,
457+
Signature: SignatureEncoding,
458+
{
459+
self.finalize();
460+
461+
let algorithm = self.signer.signature_algorithm_identifier()?;
462+
let signature = self.signer.try_sign_with_rng(rng, &self.info.to_der()?)?;
463+
let signature = BitString::from_bytes(signature.to_bytes().as_ref())?;
464+
465+
let req = CertReq {
466+
info: self.info,
467+
algorithm,
468+
signature,
469+
};
470+
471+
Ok(req)
472+
}
473+
}

x509-cert/src/ext.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
//! Standardized X.509 Certificate Extensions
22
3-
use crate::certificate;
43
use const_oid::AssociatedOid;
54
use der::{asn1::OctetString, Sequence, ValueOrd};
65
use spki::ObjectIdentifier;
@@ -49,15 +48,19 @@ pub type Extensions = alloc::vec::Vec<Extension>;
4948
/// builder.
5049
pub trait AsExtension: AssociatedOid + der::Encode {
5150
/// Should the extension be marked critical
52-
fn critical(&self, tbs: &certificate::TbsCertificate) -> bool;
51+
fn critical(&self, subject: &crate::name::Name, extensions: &[Extension]) -> bool;
5352

5453
/// Returns the Extension with the content encoded.
55-
fn to_extension(&self, tbs: &certificate::TbsCertificate) -> Result<Extension, der::Error> {
54+
fn to_extension(
55+
&self,
56+
subject: &crate::name::Name,
57+
extensions: &[Extension],
58+
) -> Result<Extension, der::Error> {
5659
let content = OctetString::new(<Self as der::Encode>::to_der(self)?)?;
5760

5861
Ok(Extension {
5962
extn_id: <Self as AssociatedOid>::OID,
60-
critical: self.critical(tbs),
63+
critical: self.critical(subject, extensions),
6164
extn_value: content,
6265
})
6366
}

x509-cert/src/ext/pkix.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ impl AssociatedOid for SubjectAltName {
7171
impl_newtype!(SubjectAltName, name::GeneralNames);
7272

7373
impl crate::ext::AsExtension for SubjectAltName {
74-
fn critical(&self, tbs: &crate::certificate::TbsCertificate) -> bool {
74+
fn critical(&self, subject: &crate::name::Name, _extensions: &[super::Extension]) -> bool {
7575
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
7676
// Further, if the only subject identity included in the certificate is
7777
// an alternative name form (e.g., an electronic mail address), then the
@@ -83,7 +83,7 @@ impl crate::ext::AsExtension for SubjectAltName {
8383
// subject distinguished name, conforming CAs SHOULD mark the
8484
// subjectAltName extension as non-critical.
8585

86-
tbs.subject.is_empty()
86+
subject.is_empty()
8787
}
8888
}
8989

x509-cert/src/ext/pkix/constraints/basic.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ impl AssociatedOid for BasicConstraints {
2424
}
2525

2626
impl crate::ext::AsExtension for BasicConstraints {
27-
fn critical(&self, _tbs: &crate::certificate::TbsCertificate) -> bool {
27+
fn critical(
28+
&self,
29+
_subject: &crate::name::Name,
30+
_extensions: &[crate::ext::Extension],
31+
) -> bool {
2832
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9
2933
// Conforming CAs MUST include this extension in all CA certificates
3034
// that contain public keys used to validate digital signatures on

x509-cert/src/ext/pkix/keyusage.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ impl AssociatedOid for ExtendedKeyUsage {
139139
impl_newtype!(ExtendedKeyUsage, Vec<ObjectIdentifier>);
140140

141141
impl crate::ext::AsExtension for ExtendedKeyUsage {
142-
fn critical(&self, _tbs: &crate::certificate::TbsCertificate) -> bool {
142+
fn critical(
143+
&self,
144+
_subject: &crate::name::Name,
145+
_extensions: &[crate::ext::Extension],
146+
) -> bool {
143147
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12
144148
// This extension MAY, at the option of the certificate issuer, be
145149
// either critical or non-critical.

x509-cert/src/macros.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ macro_rules! impl_extension {
8686
};
8787
($newtype:ty, critical = $critical:expr) => {
8888
impl crate::ext::AsExtension for $newtype {
89-
fn critical(&self, _tbs: &crate::certificate::TbsCertificate) -> bool {
89+
fn critical(
90+
&self,
91+
_subject: &crate::name::Name,
92+
_extensions: &[crate::ext::Extension],
93+
) -> bool {
9094
$critical
9195
}
9296
}

x509-cert/src/request.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ use der::pem::PemLabel;
1717
/// Version identifier for certification request information.
1818
///
1919
/// (RFC 2986 designates `0` as the only valid version)
20-
#[derive(Clone, Debug, Copy, PartialEq, Eq, Enumerated)]
20+
#[derive(Clone, Debug, Copy, PartialEq, Eq, Enumerated, Default)]
2121
#[asn1(type = "INTEGER")]
2222
#[repr(u8)]
2323
pub enum Version {
2424
/// Denotes PKCS#8 v1
25+
#[default]
2526
V1 = 0,
2627
}
2728

@@ -96,11 +97,28 @@ impl<'a> TryFrom<&'a [u8]> for CertReq {
9697
/// ```
9798
///
9899
/// [RFC 5272 Section 3.1]: https://datatracker.ietf.org/doc/html/rfc5272#section-3.1
99-
#[derive(Clone, Debug, PartialEq, Eq)]
100+
#[derive(Clone, Debug, PartialEq, Eq, Default)]
100101
pub struct ExtensionReq(pub Vec<Extension>);
101102

102103
impl AssociatedOid for ExtensionReq {
103104
const OID: ObjectIdentifier = ID_EXTENSION_REQ;
104105
}
105106

106107
impl_newtype!(ExtensionReq, Vec<Extension>);
108+
109+
use crate::attr::{Attribute, AttributeValue};
110+
use der::asn1::{Any, SetOfVec};
111+
112+
impl TryFrom<ExtensionReq> for Attribute {
113+
type Error = der::Error;
114+
115+
fn try_from(extension_req: ExtensionReq) -> der::Result<Attribute> {
116+
let mut values: SetOfVec<AttributeValue> = Default::default();
117+
values.add(Any::encode_from(&extension_req.0.clone())?);
118+
119+
Ok(Attribute {
120+
oid: ExtensionReq::OID,
121+
values,
122+
})
123+
}
124+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,33 @@ pub fn check_certificate(pem: &[u8]) -> String {
3333

3434
String::from_utf8(output_buf.clone()).unwrap()
3535
}
36+
37+
// TODO(baloo) factorize that out
38+
pub fn check_request(pem: &[u8]) -> String {
39+
let tmp_dir = tempdir().expect("create tempdir");
40+
let cert_path = tmp_dir.path().join("cert.pem");
41+
42+
let mut cert_file = File::create(&cert_path).expect("create pem file");
43+
cert_file.write_all(pem).expect("Create pem file");
44+
45+
let mut child = Command::new("openssl")
46+
.arg("req")
47+
.arg("-in")
48+
.arg(&cert_path)
49+
// .arg("-noout")
50+
.arg("-text")
51+
.stderr(Stdio::inherit())
52+
.stdout(Stdio::piped())
53+
.spawn()
54+
.expect("zlint failed");
55+
let mut stdout = child.stdout.take().unwrap();
56+
let exit_status = child.wait().expect("get openssl x509 req status");
57+
58+
assert!(exit_status.success(), "openssl failed");
59+
let mut output_buf = Vec::new();
60+
stdout
61+
.read_to_end(&mut output_buf)
62+
.expect("read openssl output");
63+
64+
String::from_utf8(output_buf.clone()).unwrap()
65+
}

0 commit comments

Comments
 (0)