Skip to content

Commit

Permalink
Rework Certificate issuance API, make DER/PEM serialization stable (#205
Browse files Browse the repository at this point in the history
)

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
  • Loading branch information
cpu authored Jan 16, 2024
1 parent a3831c9 commit 30489d7
Show file tree
Hide file tree
Showing 17 changed files with 735 additions and 543 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rcgen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rcgen"
version = "0.12.0"
version = "0.13.0"
documentation = "https://docs.rs/rcgen"
description.workspace = true
repository.workspace = true
Expand Down
12 changes: 7 additions & 5 deletions rcgen/examples/rsa-irc-openssl.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use rcgen::CertifiedKey;

fn main() -> Result<(), Box<dyn std::error::Error>> {
use rcgen::{date_time_ymd, Certificate, CertificateParams, DistinguishedName};
use std::fmt::Write;
Expand All @@ -15,8 +17,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let key_pair = rcgen::KeyPair::from_pem(&key_pair_pem)?;
params.key_pair = Some(key_pair);

let cert = Certificate::from_params(params)?;
let pem_serialized = cert.serialize_pem()?;
let CertifiedKey { cert, key_pair } = Certificate::generate_self_signed(params)?;
let pem_serialized = cert.pem();
let pem = pem::parse(&pem_serialized)?;
let der_serialized = pem.contents();
let hash = ring::digest::digest(&ring::digest::SHA512, der_serialized);
Expand All @@ -26,11 +28,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
println!("sha-512 fingerprint: {hash_hex}");
println!("{pem_serialized}");
println!("{}", cert.serialize_private_key_pem());
println!("{}", key_pair.serialize_pem());
std::fs::create_dir_all("certs/")?;
fs::write("certs/cert.pem", pem_serialized.as_bytes())?;
fs::write("certs/cert.der", der_serialized)?;
fs::write("certs/key.pem", cert.serialize_private_key_pem().as_bytes())?;
fs::write("certs/key.der", cert.serialize_private_key_der())?;
fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?;
fs::write("certs/key.der", key_pair.serialize_der())?;
Ok(())
}
12 changes: 7 additions & 5 deletions rcgen/examples/rsa-irc.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use rcgen::CertifiedKey;

fn main() -> Result<(), Box<dyn std::error::Error>> {
use rand::rngs::OsRng;
use rsa::pkcs8::EncodePrivateKey;
Expand All @@ -21,8 +23,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let key_pair = rcgen::KeyPair::try_from(private_key_der.as_bytes()).unwrap();
params.key_pair = Some(key_pair);

let cert = Certificate::from_params(params)?;
let pem_serialized = cert.serialize_pem()?;
let CertifiedKey { cert, key_pair } = Certificate::generate_self_signed(params)?;
let pem_serialized = cert.pem();
let pem = pem::parse(&pem_serialized)?;
let der_serialized = pem.contents();
let hash = ring::digest::digest(&ring::digest::SHA512, der_serialized);
Expand All @@ -32,11 +34,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
println!("sha-512 fingerprint: {hash_hex}");
println!("{pem_serialized}");
println!("{}", cert.serialize_private_key_pem());
println!("{}", key_pair.serialize_pem());
std::fs::create_dir_all("certs/")?;
fs::write("certs/cert.pem", pem_serialized.as_bytes())?;
fs::write("certs/cert.der", der_serialized)?;
fs::write("certs/key.pem", cert.serialize_private_key_pem().as_bytes())?;
fs::write("certs/key.der", cert.serialize_private_key_der())?;
fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?;
fs::write("certs/key.der", key_pair.serialize_der())?;
Ok(())
}
16 changes: 8 additions & 8 deletions rcgen/examples/sign-leaf-with-ca.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use rcgen::{
BasicConstraints, Certificate, CertificateParams, DnType, DnValue::PrintableString,
ExtendedKeyUsagePurpose, IsCa, KeyUsagePurpose,
BasicConstraints, Certificate, CertificateParams, CertifiedKey, DnType,
DnValue::PrintableString, ExtendedKeyUsagePurpose, IsCa, KeyUsagePurpose,
};
use time::{Duration, OffsetDateTime};

/// Example demonstrating signing end-endity certificate with ca
fn main() {
let ca = new_ca();
let ca = new_ca().cert;
let end_entity = new_end_entity();

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

let ca_cert_pem = ca.serialize_pem().unwrap();
let ca_cert_pem = ca.pem();
println!("ca certificate: {ca_cert_pem}",);
}

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

params.not_before = yesterday;
params.not_after = tomorrow;
Certificate::from_params(params).unwrap()
Certificate::generate_self_signed(params).unwrap()
}

fn new_end_entity() -> Certificate {
Expand All @@ -47,7 +47,7 @@ fn new_end_entity() -> Certificate {
.push(ExtendedKeyUsagePurpose::ServerAuth);
params.not_before = yesterday;
params.not_after = tomorrow;
Certificate::from_params(params).unwrap()
Certificate::generate_self_signed(params).unwrap().cert
}

fn validity_period() -> (OffsetDateTime, OffsetDateTime) {
Expand Down
14 changes: 8 additions & 6 deletions rcgen/examples/simple.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use rcgen::{date_time_ymd, Certificate, CertificateParams, DistinguishedName, DnType, SanType};
use rcgen::{
date_time_ymd, Certificate, CertificateParams, CertifiedKey, DistinguishedName, DnType, SanType,
};
use std::fs;

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

let cert = Certificate::from_params(params)?;
let CertifiedKey { cert, key_pair } = Certificate::generate_self_signed(params)?;

let pem_serialized = cert.serialize_pem()?;
let pem_serialized = cert.pem();
let pem = pem::parse(&pem_serialized)?;
let der_serialized = pem.contents();
println!("{pem_serialized}");
println!("{}", cert.serialize_private_key_pem());
println!("{}", key_pair.serialize_pem());
fs::create_dir_all("certs/")?;
fs::write("certs/cert.pem", pem_serialized.as_bytes())?;
fs::write("certs/cert.der", der_serialized)?;
fs::write("certs/key.pem", cert.serialize_private_key_pem().as_bytes())?;
fs::write("certs/key.der", cert.serialize_private_key_der())?;
fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?;
fs::write("certs/key.der", key_pair.serialize_der())?;
Ok(())
}
54 changes: 40 additions & 14 deletions rcgen/src/crl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::oid::*;
use crate::ENCODE_CONFIG;
use crate::{
write_distinguished_name, write_dt_utc_or_generalized, write_x509_authority_key_identifier,
write_x509_extension,
write_x509_extension, DistinguishedName, KeyPair,
};
use crate::{Certificate, Error, KeyIdMethod, KeyUsagePurpose, SerialNumber, SignatureAlgorithm};

Expand All @@ -26,7 +26,7 @@ use crate::{Certificate, Error, KeyIdMethod, KeyUsagePurpose, SerialNumber, Sign
/// let mut issuer_params = CertificateParams::new(vec!["crl.issuer.example.com".to_string()]);
/// issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
/// issuer_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::CrlSign];
/// let issuer = Certificate::from_params(issuer_params).unwrap();
/// let issuer = Certificate::generate_self_signed(issuer_params).unwrap();
/// // Describe a revoked certificate.
/// let revoked_cert = RevokedCertParams{
/// serial_number: SerialNumber::from(9999),
Expand Down Expand Up @@ -64,19 +64,31 @@ impl CertificateRevocationList {
}
/// Serializes the certificate revocation list (CRL) in binary DER format, signed with
/// the issuing certificate authority's key.
pub fn serialize_der_with_signer(&self, ca: &Certificate) -> Result<Vec<u8>, Error> {
pub fn serialize_der_with_signer(
&self,
ca: &Certificate,
ca_key: &KeyPair,
) -> Result<Vec<u8>, Error> {
if !ca.params.key_usages.is_empty()
&& !ca.params.key_usages.contains(&KeyUsagePurpose::CrlSign)
{
return Err(Error::IssuerNotCrlSigner);
}
self.params.serialize_der_with_signer(ca)
self.params.serialize_der_with_signer(
self.params.alg,
ca_key,
&ca.params.distinguished_name,
)
}
/// Serializes the certificate revocation list (CRL) in ASCII PEM format, signed with
/// the issuing certificate authority's key.
#[cfg(feature = "pem")]
pub fn serialize_pem_with_signer(&self, ca: &Certificate) -> Result<String, Error> {
let contents = self.serialize_der_with_signer(ca)?;
pub fn serialize_pem_with_signer(
&self,
ca: &Certificate,
ca_key: &KeyPair,
) -> Result<String, Error> {
let contents = self.serialize_der_with_signer(ca, ca_key)?;
let p = Pem::new("X509 CRL", contents);
Ok(pem::encode_config(&p, ENCODE_CONFIG))
}
Expand Down Expand Up @@ -172,29 +184,40 @@ pub struct CertificateRevocationListParams {
}

impl CertificateRevocationListParams {
fn serialize_der_with_signer(&self, ca: &Certificate) -> Result<Vec<u8>, Error> {
fn serialize_der_with_signer(
&self,
sig_alg: &SignatureAlgorithm,
issuer: &KeyPair,
issuer_name: &DistinguishedName,
) -> Result<Vec<u8>, Error> {
yasna::try_construct_der(|writer| {
// https://www.rfc-editor.org/rfc/rfc5280#section-5.1
writer.write_sequence(|writer| {
let tbs_cert_list_serialized = yasna::try_construct_der(|writer| {
self.write_crl(writer, ca)?;
self.write_crl(writer, sig_alg, issuer, issuer_name)?;
Ok::<(), Error>(())
})?;

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

// Write signatureAlgorithm
ca.params.alg.write_alg_ident(writer.next());
sig_alg.write_alg_ident(writer.next());

// Write signature
ca.key_pair.sign(&tbs_cert_list_serialized, writer.next())?;
issuer.sign(&tbs_cert_list_serialized, writer.next())?;

Ok(())
})
})
}
fn write_crl(&self, writer: DERWriter, ca: &Certificate) -> Result<(), Error> {
fn write_crl(
&self,
writer: DERWriter,
sig_alg: &SignatureAlgorithm,
issuer: &KeyPair,
issuer_name: &DistinguishedName,
) -> Result<(), Error> {
writer.write_sequence(|writer| {
// Write CRL version.
// RFC 5280 §5.1.2.1:
Expand All @@ -211,12 +234,12 @@ impl CertificateRevocationListParams {
// RFC 5280 §5.1.2.2:
// This field MUST contain the same algorithm identifier as the
// signatureAlgorithm field in the sequence CertificateList
ca.params.alg.write_alg_ident(writer.next());
sig_alg.write_alg_ident(writer.next());

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

// Write thisUpdate date.
// RFC 5280 §5.1.2.4:
Expand Down Expand Up @@ -252,7 +275,10 @@ impl CertificateRevocationListParams {
writer.next().write_tagged(Tag::context(0), |writer| {
writer.write_sequence(|writer| {
// Write authority key identifier.
write_x509_authority_key_identifier(writer.next(), ca);
write_x509_authority_key_identifier(
writer.next(),
self.key_identifier_method.derive(issuer.public_key_der()),
);

// Write CRL number.
write_x509_extension(writer.next(), OID_CRL_NUMBER, false, |writer| {
Expand Down
25 changes: 5 additions & 20 deletions rcgen/src/csr.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#[cfg(feature = "x509-parser")]
use crate::{DistinguishedName, SanType};
#[cfg(feature = "pem")]
use pem::Pem;
use crate::{DistinguishedName, Error, SanType};
use std::hash::Hash;

use crate::{Certificate, CertificateParams, Error, PublicKeyData, SignatureAlgorithm};
use crate::{CertificateParams, PublicKeyData, SignatureAlgorithm};

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

/// Data for a certificate signing request
pub struct CertificateSigningRequest {
/// Parameters for a certificate signing request
pub struct CertificateSigningRequestParams {
/// Parameters for the certificate to be signed.
pub params: CertificateParams,
/// Public key to include in the certificate signing request.
pub public_key: PublicKey,
}

impl CertificateSigningRequest {
impl CertificateSigningRequestParams {
/// Parse a certificate signing request from the ASCII PEM format
///
/// See [`from_der`](Self::from_der) for more details.
Expand Down Expand Up @@ -92,17 +90,4 @@ impl CertificateSigningRequest {
public_key: PublicKey { alg, raw },
})
}
/// Serializes the requested certificate, signed with another certificate's key, in binary DER format
pub fn serialize_der_with_signer(&self, ca: &Certificate) -> Result<Vec<u8>, Error> {
self.params.serialize_der_with_signer(&self.public_key, ca)
}
/// Serializes the requested certificate, signed with another certificate's key, to the ASCII PEM format
#[cfg(feature = "pem")]
pub fn serialize_pem_with_signer(&self, ca: &Certificate) -> Result<String, Error> {
let contents = self
.params
.serialize_der_with_signer(&self.public_key, ca)?;
let p = Pem::new("CERTIFICATE", contents);
Ok(pem::encode_config(&p, crate::ENCODE_CONFIG))
}
}
22 changes: 21 additions & 1 deletion rcgen/src/key_pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::sign_algo::SignAlgo;
use crate::ENCODE_CONFIG;
use crate::{Error, SignatureAlgorithm};

/// A key pair vairant
/// A key pair variant
#[allow(clippy::large_enum_variant)]
pub(crate) enum KeyPairKind {
/// A Ecdsa key pair
Expand Down Expand Up @@ -211,6 +211,26 @@ impl KeyPair {
}
}

/// Validate a provided key pair's compatibility with `sig_alg` or generate a new one.
///
/// If a provided `existing_key_pair` is not compatible with the `sig_alg` an error is
/// returned.
///
/// If `None` is provided for `existing_key_pair` a new key pair compatible with `sig_alg`
/// is generated from scratch.
pub(crate) fn validate_or_generate(
existing_key_pair: &mut Option<KeyPair>,
sig_alg: &'static SignatureAlgorithm,
) -> Result<Self, Error> {
match existing_key_pair.take() {
Some(kp) if !kp.is_compatible(sig_alg) => {
return Err(Error::CertificateKeyPairMismatch)
},
Some(kp) => Ok(kp),
None => KeyPair::generate(sig_alg),
}
}

/// Get the raw public key of this key pair
///
/// The key is in raw format, as how [`ring::signature::KeyPair::public_key`]
Expand Down
Loading

0 comments on commit 30489d7

Please sign in to comment.