Skip to content

Commit 30489d7

Browse files
authored
Rework Certificate issuance API, make DER/PEM serialization stable (#205)
Previously a `Certificate` was a container for `CertificateParams` and a `KeyPair`, most commonly created from a `CertificateParams` instance. Serializing the `Certificate` (either as self signed with `serialize_pem` or `serialize_der`, or signed by an issuer with `serialize_pem_with_signer` or `serialize_der_with_signer`) would issue a certificate and produce the serialized form in one operation. The net result is that if a user wanted both DER and PEM serializations they would likely call `serialize_der(_with_signer)` and then `serialize_pem(_with_signer)` and mistakenly end up with the encoding of two distinct certificates, not the PEM and DER encoding of the same cert. Since the `KeyPair` contains a private key this API design also meant that the `Certificate` type had to be handled with care, and `Zeroized`. This branch reworks the issuance API and `Certificate` type to better match user expectation: `Certificate` is only public material and represents an issued certificate that can be serialized in a stable manner in DER or PEM encoding. I recommend reviewing this commit-by-commit, but here is a summary of the most notable API changes: * `Certificate::from_params` and `Certificate::serialize_der` and `Certificate::serialize_pem` for issuing a self-signed certificate are replaced with `Certificate::generate_self_signed()` and calling `der` or `pem` on the result. * `Certificate::from_params` and `Certificate::serialize_der_with_signer` and `Certificate::serialize_pem_with_signer` for issuing a certificate signed by another certificate are replaced with `Certificate::generate()` and calling `der` or `pem` on the result. * `CertificateSigningRequest::serialize_der_with_signer` and `CertificateSigningRequest::serialize_pem_with_signer` for issuing a certificate from a CSR are replaced with `Certificate::from_request` and calling `der` or `pem` on the result. The `CertificateSigningRequest` type is renamed to `CertificateSigningRequestParams` to better emphasize its role and match the other `*Params` types that already exist. * Since we now calculate the DER encoding of the certificate at `Certificate` construction time, the `pem` and `der` fns are now infallible. * Since `Certificate` no longer holds `KeyPair`, the `generate` fns now expect a `&KeyPair` argument for the signer when issuing a certificate signed by another certificate. * The generation fns now return a `CertifiedKey` that contains both a `Certificate` and a `KeyPair`. For params that specify a compatible `KeyPair` it is passed through in the `CertifiedKey` as-is. For params without a `KeyPair` a newly generated `KeyPair` is used. In the future we should look at harmonizing the creation of `CertificateSigningRequest` and `CertificateRevocationList` to better match this updated API. Unfortunately I don't have time to handle that at the moment. Since this API surface is relatively niche compared to the `Certificate` issuance flow it felt valuable to resolve #62 without blocking on this future work. Resolves #62
1 parent a3831c9 commit 30489d7

17 files changed

+735
-543
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rcgen/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rcgen"
3-
version = "0.12.0"
3+
version = "0.13.0"
44
documentation = "https://docs.rs/rcgen"
55
description.workspace = true
66
repository.workspace = true

rcgen/examples/rsa-irc-openssl.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use rcgen::CertifiedKey;
2+
13
fn main() -> Result<(), Box<dyn std::error::Error>> {
24
use rcgen::{date_time_ymd, Certificate, CertificateParams, DistinguishedName};
35
use std::fmt::Write;
@@ -15,8 +17,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1517
let key_pair = rcgen::KeyPair::from_pem(&key_pair_pem)?;
1618
params.key_pair = Some(key_pair);
1719

18-
let cert = Certificate::from_params(params)?;
19-
let pem_serialized = cert.serialize_pem()?;
20+
let CertifiedKey { cert, key_pair } = Certificate::generate_self_signed(params)?;
21+
let pem_serialized = cert.pem();
2022
let pem = pem::parse(&pem_serialized)?;
2123
let der_serialized = pem.contents();
2224
let hash = ring::digest::digest(&ring::digest::SHA512, der_serialized);
@@ -26,11 +28,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
2628
});
2729
println!("sha-512 fingerprint: {hash_hex}");
2830
println!("{pem_serialized}");
29-
println!("{}", cert.serialize_private_key_pem());
31+
println!("{}", key_pair.serialize_pem());
3032
std::fs::create_dir_all("certs/")?;
3133
fs::write("certs/cert.pem", pem_serialized.as_bytes())?;
3234
fs::write("certs/cert.der", der_serialized)?;
33-
fs::write("certs/key.pem", cert.serialize_private_key_pem().as_bytes())?;
34-
fs::write("certs/key.der", cert.serialize_private_key_der())?;
35+
fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?;
36+
fs::write("certs/key.der", key_pair.serialize_der())?;
3537
Ok(())
3638
}

rcgen/examples/rsa-irc.rs

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use rcgen::CertifiedKey;
2+
13
fn main() -> Result<(), Box<dyn std::error::Error>> {
24
use rand::rngs::OsRng;
35
use rsa::pkcs8::EncodePrivateKey;
@@ -21,8 +23,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
2123
let key_pair = rcgen::KeyPair::try_from(private_key_der.as_bytes()).unwrap();
2224
params.key_pair = Some(key_pair);
2325

24-
let cert = Certificate::from_params(params)?;
25-
let pem_serialized = cert.serialize_pem()?;
26+
let CertifiedKey { cert, key_pair } = Certificate::generate_self_signed(params)?;
27+
let pem_serialized = cert.pem();
2628
let pem = pem::parse(&pem_serialized)?;
2729
let der_serialized = pem.contents();
2830
let hash = ring::digest::digest(&ring::digest::SHA512, der_serialized);
@@ -32,11 +34,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3234
});
3335
println!("sha-512 fingerprint: {hash_hex}");
3436
println!("{pem_serialized}");
35-
println!("{}", cert.serialize_private_key_pem());
37+
println!("{}", key_pair.serialize_pem());
3638
std::fs::create_dir_all("certs/")?;
3739
fs::write("certs/cert.pem", pem_serialized.as_bytes())?;
3840
fs::write("certs/cert.der", der_serialized)?;
39-
fs::write("certs/key.pem", cert.serialize_private_key_pem().as_bytes())?;
40-
fs::write("certs/key.der", cert.serialize_private_key_der())?;
41+
fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?;
42+
fs::write("certs/key.der", key_pair.serialize_der())?;
4143
Ok(())
4244
}

rcgen/examples/sign-leaf-with-ca.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
use rcgen::{
2-
BasicConstraints, Certificate, CertificateParams, DnType, DnValue::PrintableString,
3-
ExtendedKeyUsagePurpose, IsCa, KeyUsagePurpose,
2+
BasicConstraints, Certificate, CertificateParams, CertifiedKey, DnType,
3+
DnValue::PrintableString, ExtendedKeyUsagePurpose, IsCa, KeyUsagePurpose,
44
};
55
use time::{Duration, OffsetDateTime};
66

77
/// Example demonstrating signing end-endity certificate with ca
88
fn main() {
9-
let ca = new_ca();
9+
let ca = new_ca().cert;
1010
let end_entity = new_end_entity();
1111

12-
let end_entity_pem = end_entity.serialize_pem_with_signer(&ca).unwrap();
12+
let end_entity_pem = end_entity.pem();
1313
println!("directly signed end-entity certificate: {end_entity_pem}");
1414

15-
let ca_cert_pem = ca.serialize_pem().unwrap();
15+
let ca_cert_pem = ca.pem();
1616
println!("ca certificate: {ca_cert_pem}",);
1717
}
1818

19-
fn new_ca() -> Certificate {
19+
fn new_ca() -> CertifiedKey {
2020
let mut params = CertificateParams::new(Vec::default());
2121
let (yesterday, tomorrow) = validity_period();
2222
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
@@ -32,7 +32,7 @@ fn new_ca() -> Certificate {
3232

3333
params.not_before = yesterday;
3434
params.not_after = tomorrow;
35-
Certificate::from_params(params).unwrap()
35+
Certificate::generate_self_signed(params).unwrap()
3636
}
3737

3838
fn new_end_entity() -> Certificate {
@@ -47,7 +47,7 @@ fn new_end_entity() -> Certificate {
4747
.push(ExtendedKeyUsagePurpose::ServerAuth);
4848
params.not_before = yesterday;
4949
params.not_after = tomorrow;
50-
Certificate::from_params(params).unwrap()
50+
Certificate::generate_self_signed(params).unwrap().cert
5151
}
5252

5353
fn validity_period() -> (OffsetDateTime, OffsetDateTime) {

rcgen/examples/simple.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use rcgen::{date_time_ymd, Certificate, CertificateParams, DistinguishedName, DnType, SanType};
1+
use rcgen::{
2+
date_time_ymd, Certificate, CertificateParams, CertifiedKey, DistinguishedName, DnType, SanType,
3+
};
24
use std::fs;
35

46
fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -17,17 +19,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1719
SanType::DnsName("localhost".to_string()),
1820
];
1921

20-
let cert = Certificate::from_params(params)?;
22+
let CertifiedKey { cert, key_pair } = Certificate::generate_self_signed(params)?;
2123

22-
let pem_serialized = cert.serialize_pem()?;
24+
let pem_serialized = cert.pem();
2325
let pem = pem::parse(&pem_serialized)?;
2426
let der_serialized = pem.contents();
2527
println!("{pem_serialized}");
26-
println!("{}", cert.serialize_private_key_pem());
28+
println!("{}", key_pair.serialize_pem());
2729
fs::create_dir_all("certs/")?;
2830
fs::write("certs/cert.pem", pem_serialized.as_bytes())?;
2931
fs::write("certs/cert.der", der_serialized)?;
30-
fs::write("certs/key.pem", cert.serialize_private_key_pem().as_bytes())?;
31-
fs::write("certs/key.der", cert.serialize_private_key_der())?;
32+
fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?;
33+
fs::write("certs/key.der", key_pair.serialize_der())?;
3234
Ok(())
3335
}

rcgen/src/crl.rs

+40-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::oid::*;
99
use crate::ENCODE_CONFIG;
1010
use crate::{
1111
write_distinguished_name, write_dt_utc_or_generalized, write_x509_authority_key_identifier,
12-
write_x509_extension,
12+
write_x509_extension, DistinguishedName, KeyPair,
1313
};
1414
use crate::{Certificate, Error, KeyIdMethod, KeyUsagePurpose, SerialNumber, SignatureAlgorithm};
1515

@@ -26,7 +26,7 @@ use crate::{Certificate, Error, KeyIdMethod, KeyUsagePurpose, SerialNumber, Sign
2626
/// let mut issuer_params = CertificateParams::new(vec!["crl.issuer.example.com".to_string()]);
2727
/// issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
2828
/// issuer_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::CrlSign];
29-
/// let issuer = Certificate::from_params(issuer_params).unwrap();
29+
/// let issuer = Certificate::generate_self_signed(issuer_params).unwrap();
3030
/// // Describe a revoked certificate.
3131
/// let revoked_cert = RevokedCertParams{
3232
/// serial_number: SerialNumber::from(9999),
@@ -64,19 +64,31 @@ impl CertificateRevocationList {
6464
}
6565
/// Serializes the certificate revocation list (CRL) in binary DER format, signed with
6666
/// the issuing certificate authority's key.
67-
pub fn serialize_der_with_signer(&self, ca: &Certificate) -> Result<Vec<u8>, Error> {
67+
pub fn serialize_der_with_signer(
68+
&self,
69+
ca: &Certificate,
70+
ca_key: &KeyPair,
71+
) -> Result<Vec<u8>, Error> {
6872
if !ca.params.key_usages.is_empty()
6973
&& !ca.params.key_usages.contains(&KeyUsagePurpose::CrlSign)
7074
{
7175
return Err(Error::IssuerNotCrlSigner);
7276
}
73-
self.params.serialize_der_with_signer(ca)
77+
self.params.serialize_der_with_signer(
78+
self.params.alg,
79+
ca_key,
80+
&ca.params.distinguished_name,
81+
)
7482
}
7583
/// Serializes the certificate revocation list (CRL) in ASCII PEM format, signed with
7684
/// the issuing certificate authority's key.
7785
#[cfg(feature = "pem")]
78-
pub fn serialize_pem_with_signer(&self, ca: &Certificate) -> Result<String, Error> {
79-
let contents = self.serialize_der_with_signer(ca)?;
86+
pub fn serialize_pem_with_signer(
87+
&self,
88+
ca: &Certificate,
89+
ca_key: &KeyPair,
90+
) -> Result<String, Error> {
91+
let contents = self.serialize_der_with_signer(ca, ca_key)?;
8092
let p = Pem::new("X509 CRL", contents);
8193
Ok(pem::encode_config(&p, ENCODE_CONFIG))
8294
}
@@ -172,29 +184,40 @@ pub struct CertificateRevocationListParams {
172184
}
173185

174186
impl CertificateRevocationListParams {
175-
fn serialize_der_with_signer(&self, ca: &Certificate) -> Result<Vec<u8>, Error> {
187+
fn serialize_der_with_signer(
188+
&self,
189+
sig_alg: &SignatureAlgorithm,
190+
issuer: &KeyPair,
191+
issuer_name: &DistinguishedName,
192+
) -> Result<Vec<u8>, Error> {
176193
yasna::try_construct_der(|writer| {
177194
// https://www.rfc-editor.org/rfc/rfc5280#section-5.1
178195
writer.write_sequence(|writer| {
179196
let tbs_cert_list_serialized = yasna::try_construct_der(|writer| {
180-
self.write_crl(writer, ca)?;
197+
self.write_crl(writer, sig_alg, issuer, issuer_name)?;
181198
Ok::<(), Error>(())
182199
})?;
183200

184201
// Write tbsCertList
185202
writer.next().write_der(&tbs_cert_list_serialized);
186203

187204
// Write signatureAlgorithm
188-
ca.params.alg.write_alg_ident(writer.next());
205+
sig_alg.write_alg_ident(writer.next());
189206

190207
// Write signature
191-
ca.key_pair.sign(&tbs_cert_list_serialized, writer.next())?;
208+
issuer.sign(&tbs_cert_list_serialized, writer.next())?;
192209

193210
Ok(())
194211
})
195212
})
196213
}
197-
fn write_crl(&self, writer: DERWriter, ca: &Certificate) -> Result<(), Error> {
214+
fn write_crl(
215+
&self,
216+
writer: DERWriter,
217+
sig_alg: &SignatureAlgorithm,
218+
issuer: &KeyPair,
219+
issuer_name: &DistinguishedName,
220+
) -> Result<(), Error> {
198221
writer.write_sequence(|writer| {
199222
// Write CRL version.
200223
// RFC 5280 §5.1.2.1:
@@ -211,12 +234,12 @@ impl CertificateRevocationListParams {
211234
// RFC 5280 §5.1.2.2:
212235
// This field MUST contain the same algorithm identifier as the
213236
// signatureAlgorithm field in the sequence CertificateList
214-
ca.params.alg.write_alg_ident(writer.next());
237+
sig_alg.write_alg_ident(writer.next());
215238

216239
// Write issuer.
217240
// RFC 5280 §5.1.2.3:
218241
// The issuer field MUST contain a non-empty X.500 distinguished name (DN).
219-
write_distinguished_name(writer.next(), &ca.params.distinguished_name);
242+
write_distinguished_name(writer.next(), issuer_name);
220243

221244
// Write thisUpdate date.
222245
// RFC 5280 §5.1.2.4:
@@ -252,7 +275,10 @@ impl CertificateRevocationListParams {
252275
writer.next().write_tagged(Tag::context(0), |writer| {
253276
writer.write_sequence(|writer| {
254277
// Write authority key identifier.
255-
write_x509_authority_key_identifier(writer.next(), ca);
278+
write_x509_authority_key_identifier(
279+
writer.next(),
280+
self.key_identifier_method.derive(issuer.public_key_der()),
281+
);
256282

257283
// Write CRL number.
258284
write_x509_extension(writer.next(), OID_CRL_NUMBER, false, |writer| {

rcgen/src/csr.rs

+5-20
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
#[cfg(feature = "x509-parser")]
2-
use crate::{DistinguishedName, SanType};
3-
#[cfg(feature = "pem")]
4-
use pem::Pem;
2+
use crate::{DistinguishedName, Error, SanType};
53
use std::hash::Hash;
64

7-
use crate::{Certificate, CertificateParams, Error, PublicKeyData, SignatureAlgorithm};
5+
use crate::{CertificateParams, PublicKeyData, SignatureAlgorithm};
86

97
/// A public key, extracted from a CSR
108
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -23,15 +21,15 @@ impl PublicKeyData for PublicKey {
2321
}
2422
}
2523

26-
/// Data for a certificate signing request
27-
pub struct CertificateSigningRequest {
24+
/// Parameters for a certificate signing request
25+
pub struct CertificateSigningRequestParams {
2826
/// Parameters for the certificate to be signed.
2927
pub params: CertificateParams,
3028
/// Public key to include in the certificate signing request.
3129
pub public_key: PublicKey,
3230
}
3331

34-
impl CertificateSigningRequest {
32+
impl CertificateSigningRequestParams {
3533
/// Parse a certificate signing request from the ASCII PEM format
3634
///
3735
/// See [`from_der`](Self::from_der) for more details.
@@ -92,17 +90,4 @@ impl CertificateSigningRequest {
9290
public_key: PublicKey { alg, raw },
9391
})
9492
}
95-
/// Serializes the requested certificate, signed with another certificate's key, in binary DER format
96-
pub fn serialize_der_with_signer(&self, ca: &Certificate) -> Result<Vec<u8>, Error> {
97-
self.params.serialize_der_with_signer(&self.public_key, ca)
98-
}
99-
/// Serializes the requested certificate, signed with another certificate's key, to the ASCII PEM format
100-
#[cfg(feature = "pem")]
101-
pub fn serialize_pem_with_signer(&self, ca: &Certificate) -> Result<String, Error> {
102-
let contents = self
103-
.params
104-
.serialize_der_with_signer(&self.public_key, ca)?;
105-
let p = Pem::new("CERTIFICATE", contents);
106-
Ok(pem::encode_config(&p, crate::ENCODE_CONFIG))
107-
}
10893
}

rcgen/src/key_pair.rs

+21-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::sign_algo::SignAlgo;
1717
use crate::ENCODE_CONFIG;
1818
use crate::{Error, SignatureAlgorithm};
1919

20-
/// A key pair vairant
20+
/// A key pair variant
2121
#[allow(clippy::large_enum_variant)]
2222
pub(crate) enum KeyPairKind {
2323
/// A Ecdsa key pair
@@ -211,6 +211,26 @@ impl KeyPair {
211211
}
212212
}
213213

214+
/// Validate a provided key pair's compatibility with `sig_alg` or generate a new one.
215+
///
216+
/// If a provided `existing_key_pair` is not compatible with the `sig_alg` an error is
217+
/// returned.
218+
///
219+
/// If `None` is provided for `existing_key_pair` a new key pair compatible with `sig_alg`
220+
/// is generated from scratch.
221+
pub(crate) fn validate_or_generate(
222+
existing_key_pair: &mut Option<KeyPair>,
223+
sig_alg: &'static SignatureAlgorithm,
224+
) -> Result<Self, Error> {
225+
match existing_key_pair.take() {
226+
Some(kp) if !kp.is_compatible(sig_alg) => {
227+
return Err(Error::CertificateKeyPairMismatch)
228+
},
229+
Some(kp) => Ok(kp),
230+
None => KeyPair::generate(sig_alg),
231+
}
232+
}
233+
214234
/// Get the raw public key of this key pair
215235
///
216236
/// The key is in raw format, as how [`ring::signature::KeyPair::public_key`]

0 commit comments

Comments
 (0)