|
| 1 | +use bon::Builder; |
| 2 | +use rsa::pkcs8::EncodePublicKey; |
| 3 | +use snafu::{ResultExt, Snafu}; |
| 4 | +use stackable_operator::time::Duration; |
| 5 | +use tracing::debug; |
| 6 | +use x509_cert::{ |
| 7 | + builder::{Builder, CertificateBuilder, Profile}, |
| 8 | + der::{DecodePem, referenced::OwnedToRef}, |
| 9 | + ext::pkix::AuthorityKeyIdentifier, |
| 10 | + name::Name, |
| 11 | + serial_number::SerialNumber, |
| 12 | + spki::SubjectPublicKeyInfoOwned, |
| 13 | + time::Validity, |
| 14 | +}; |
| 15 | + |
| 16 | +use super::CertificateAuthority; |
| 17 | +use crate::{ |
| 18 | + CertificatePair, |
| 19 | + ca::{DEFAULT_CA_VALIDITY, PEM_LINE_ENDING, SDP_ROOT_CA_SUBJECT}, |
| 20 | + keys::CertificateKeypair, |
| 21 | +}; |
| 22 | + |
| 23 | +/// Defines all error variants which can occur when creating a CA |
| 24 | +#[derive(Debug, Snafu)] |
| 25 | +pub enum CreateCertificateAuthorityError<E> |
| 26 | +where |
| 27 | + E: std::error::Error + 'static, |
| 28 | +{ |
| 29 | + #[snafu(display("failed to parse validity"))] |
| 30 | + ParseValidity { source: x509_cert::der::Error }, |
| 31 | + |
| 32 | + #[snafu(display("failed to parse \"{subject}\" as subject"))] |
| 33 | + ParseSubject { |
| 34 | + source: x509_cert::der::Error, |
| 35 | + subject: String, |
| 36 | + }, |
| 37 | + |
| 38 | + #[snafu(display("failed to create signing key pair"))] |
| 39 | + CreateSigningKeyPair { source: E }, |
| 40 | + |
| 41 | + #[snafu(display("failed to serialize public key as PEM"))] |
| 42 | + SerializePublicKey { source: x509_cert::spki::Error }, |
| 43 | + |
| 44 | + #[snafu(display("failed to decode SPKI from PEM"))] |
| 45 | + DecodeSpkiFromPem { source: x509_cert::der::Error }, |
| 46 | + |
| 47 | + #[snafu(display("failed to parse AuthorityKeyIdentifier"))] |
| 48 | + ParseAuthorityKeyIdentifier { source: x509_cert::der::Error }, |
| 49 | + |
| 50 | + #[snafu(display("failed to create certificate builder"))] |
| 51 | + CreateCertificateBuilder { source: x509_cert::builder::Error }, |
| 52 | + |
| 53 | + #[snafu(display("failed to add certificate extension"))] |
| 54 | + AddCertificateExtension { source: x509_cert::builder::Error }, |
| 55 | + |
| 56 | + #[snafu(display("failed to build certificate"))] |
| 57 | + BuildCertificate { source: x509_cert::builder::Error }, |
| 58 | +} |
| 59 | + |
| 60 | +/// This builder builds certificate authorities of type [`CertificateAuthority`]. |
| 61 | +/// |
| 62 | +/// Example code to construct a CA: |
| 63 | +/// |
| 64 | +/// ```no_run |
| 65 | +/// use stackable_certs::{ |
| 66 | +/// keys::ecdsa, |
| 67 | +/// ca::{CertificateAuthority, CertificateAuthorityBuilder}, |
| 68 | +/// }; |
| 69 | +/// |
| 70 | +/// let ca: CertificateAuthority<ecdsa::SigningKey> = CertificateAuthorityBuilder::builder() |
| 71 | +/// .build() |
| 72 | +/// .build_ca() |
| 73 | +/// .expect("failed to build CA"); |
| 74 | +/// ``` |
| 75 | +#[derive(Builder)] |
| 76 | +pub struct CertificateAuthorityBuilder<'a, SKP> |
| 77 | +where |
| 78 | + SKP: CertificateKeypair, |
| 79 | + <SKP::SigningKey as signature::Keypair>::VerifyingKey: EncodePublicKey, |
| 80 | +{ |
| 81 | + /// Required subject of the certificate authority, usually starts with `CN=`. |
| 82 | + #[builder(default = SDP_ROOT_CA_SUBJECT)] |
| 83 | + subject: &'a str, |
| 84 | + |
| 85 | + /// Validity/lifetime of the certificate. |
| 86 | + /// |
| 87 | + /// If not specified the default of [`DEFAULT_CA_VALIDITY`] will be used. |
| 88 | + #[builder(default = DEFAULT_CA_VALIDITY)] |
| 89 | + validity: Duration, |
| 90 | + |
| 91 | + /// Serial number of the generated certificate. |
| 92 | + /// |
| 93 | + /// If not specified a random serial will be generated. |
| 94 | + serial_number: Option<u64>, |
| 95 | + |
| 96 | + /// Cryptographic keypair used to sign leaf certificates. |
| 97 | + /// |
| 98 | + /// If not specified a random keypair will be generated. |
| 99 | + signing_key_pair: Option<SKP>, |
| 100 | +} |
| 101 | + |
| 102 | +impl<SKP> CertificateAuthorityBuilder<'_, SKP> |
| 103 | +where |
| 104 | + SKP: CertificateKeypair, |
| 105 | + <SKP::SigningKey as signature::Keypair>::VerifyingKey: EncodePublicKey, |
| 106 | +{ |
| 107 | + pub fn build_ca( |
| 108 | + self, |
| 109 | + ) -> Result<CertificateAuthority<SKP>, CreateCertificateAuthorityError<SKP::Error>> { |
| 110 | + let serial_number = |
| 111 | + SerialNumber::from(self.serial_number.unwrap_or_else(|| rand::random::<u64>())); |
| 112 | + let validity = Validity::from_now(*self.validity).context(ParseValiditySnafu)?; |
| 113 | + let subject: Name = self.subject.parse().context(ParseSubjectSnafu { |
| 114 | + subject: self.subject, |
| 115 | + })?; |
| 116 | + let signing_key_pair = match self.signing_key_pair { |
| 117 | + Some(signing_key_pair) => signing_key_pair, |
| 118 | + None => SKP::new().context(CreateSigningKeyPairSnafu)?, |
| 119 | + }; |
| 120 | + |
| 121 | + let spki_pem = signing_key_pair |
| 122 | + .verifying_key() |
| 123 | + .to_public_key_pem(PEM_LINE_ENDING) |
| 124 | + .context(SerializePublicKeySnafu)?; |
| 125 | + |
| 126 | + let spki = SubjectPublicKeyInfoOwned::from_pem(spki_pem.as_bytes()) |
| 127 | + .context(DecodeSpkiFromPemSnafu)?; |
| 128 | + |
| 129 | + // There are multiple default extensions included in the profile. For |
| 130 | + // the root profile, these are: |
| 131 | + // |
| 132 | + // - BasicConstraints marked as critical and CA = true |
| 133 | + // - SubjectKeyIdentifier with the 160-bit SHA-1 hash of the subject |
| 134 | + // public key. |
| 135 | + // - KeyUsage with KeyCertSign and CRLSign bits set. Ideally we also |
| 136 | + // want to include the DigitalSignature bit, which for example is |
| 137 | + // required for CA certs which want to sign an OCSP response. |
| 138 | + // Currently, the root profile doesn't include that bit. |
| 139 | + // |
| 140 | + // The root profile doesn't add the AuthorityKeyIdentifier extension. |
| 141 | + // We manually add it below by using the 160-bit SHA-1 hash of the |
| 142 | + // subject public key. This conforms to one of the outlined methods for |
| 143 | + // generating key identifiers outlined in RFC 5280, section 4.2.1.2. |
| 144 | + // |
| 145 | + // Prepare extensions so we can avoid clones. |
| 146 | + let aki = AuthorityKeyIdentifier::try_from(spki.owned_to_ref()) |
| 147 | + .context(ParseAuthorityKeyIdentifierSnafu)?; |
| 148 | + |
| 149 | + let signer = signing_key_pair.signing_key(); |
| 150 | + let mut builder = CertificateBuilder::new( |
| 151 | + Profile::Root, |
| 152 | + serial_number, |
| 153 | + validity, |
| 154 | + subject, |
| 155 | + spki, |
| 156 | + signer, |
| 157 | + ) |
| 158 | + .context(CreateCertificateBuilderSnafu)?; |
| 159 | + |
| 160 | + // Add extension constructed above |
| 161 | + builder |
| 162 | + .add_extension(&aki) |
| 163 | + .context(AddCertificateExtensionSnafu)?; |
| 164 | + |
| 165 | + debug!("create and sign CA certificate"); |
| 166 | + let certificate = builder.build().context(BuildCertificateSnafu)?; |
| 167 | + |
| 168 | + Ok(CertificateAuthority { |
| 169 | + certificate_pair: CertificatePair { |
| 170 | + certificate, |
| 171 | + key_pair: signing_key_pair, |
| 172 | + }, |
| 173 | + }) |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +#[cfg(test)] |
| 178 | +mod tests { |
| 179 | + use x509_cert::certificate::TbsCertificateInner; |
| 180 | + |
| 181 | + use super::*; |
| 182 | + use crate::keys::{ecdsa, rsa}; |
| 183 | + |
| 184 | + #[test] |
| 185 | + fn minimal_ca() { |
| 186 | + let ca: CertificateAuthority<ecdsa::SigningKey> = CertificateAuthorityBuilder::builder() |
| 187 | + .build() |
| 188 | + .build_ca() |
| 189 | + .expect("failed to build CA"); |
| 190 | + |
| 191 | + assert_ca_cert_attributes( |
| 192 | + &ca.ca_cert().tbs_certificate, |
| 193 | + SDP_ROOT_CA_SUBJECT, |
| 194 | + DEFAULT_CA_VALIDITY, |
| 195 | + None, |
| 196 | + ) |
| 197 | + } |
| 198 | + |
| 199 | + #[test] |
| 200 | + fn customized_ca() { |
| 201 | + let ca = CertificateAuthorityBuilder::builder() |
| 202 | + .subject("CN=Test") |
| 203 | + .serial_number(42) |
| 204 | + .signing_key_pair(rsa::SigningKey::new().unwrap()) |
| 205 | + .validity(Duration::from_days_unchecked(13)) |
| 206 | + .build() |
| 207 | + .build_ca() |
| 208 | + .expect("failed to build CA"); |
| 209 | + |
| 210 | + assert_ca_cert_attributes( |
| 211 | + &ca.ca_cert().tbs_certificate, |
| 212 | + "CN=Test", |
| 213 | + Duration::from_days_unchecked(13), |
| 214 | + Some(42), |
| 215 | + ) |
| 216 | + } |
| 217 | + |
| 218 | + fn assert_ca_cert_attributes( |
| 219 | + ca_cert: &TbsCertificateInner, |
| 220 | + subject: &str, |
| 221 | + validity: Duration, |
| 222 | + serial_number: Option<u64>, |
| 223 | + ) { |
| 224 | + assert_eq!(ca_cert.subject, subject.parse().unwrap()); |
| 225 | + |
| 226 | + let not_before = ca_cert.validity.not_before.to_system_time(); |
| 227 | + let not_after = ca_cert.validity.not_after.to_system_time(); |
| 228 | + assert_eq!( |
| 229 | + not_after |
| 230 | + .duration_since(not_before) |
| 231 | + .expect("Failed to calculate duration between notBefore and notAfter"), |
| 232 | + *validity |
| 233 | + ); |
| 234 | + |
| 235 | + if let Some(serial_number) = serial_number { |
| 236 | + assert_eq!(ca_cert.serial_number, SerialNumber::from(serial_number)) |
| 237 | + } else { |
| 238 | + assert_ne!(ca_cert.serial_number, SerialNumber::from(0_u64)) |
| 239 | + } |
| 240 | + } |
| 241 | +} |
0 commit comments