//! OpenSSH certificate builder. use super::{unix_time::UnixTime, CertType, Certificate, Field, OptionsMap}; use crate::{public, Result, Signature, SigningKey}; use alloc::{string::String, vec::Vec}; #[cfg(feature = "rand_core")] use rand_core::CryptoRngCore; #[cfg(feature = "std")] use std::time::SystemTime; #[cfg(doc)] use crate::PrivateKey; /// OpenSSH certificate builder. /// /// This type provides the core functionality of an OpenSSH certificate /// authority. /// /// It can build and sign OpenSSH certificates. /// /// ## Principals /// /// Certificates are valid for a specific set of principal names: /// /// - Usernames for [`CertType::User`]. /// - Hostnames for [`CertType::Host`]. /// /// When building a certificate, you will either need to specify principals /// by calling [`Builder::valid_principal`] one or more times, or explicitly /// marking a certificate as valid for all principals (i.e. "golden ticket") /// using the [`Builder::all_principals_valid`] method. /// /// ## Example /// #[cfg_attr( all(feature = "ed25519", feature = "getrandom", feature = "std"), doc = " ```" )] #[cfg_attr( not(all(feature = "ed25519", feature = "getrandom", feature = "std")), doc = " ```ignore" )] /// # fn main() -> Result<(), Box> { /// use ssh_key::{Algorithm, PrivateKey, certificate, rand_core::OsRng}; /// use std::time::{SystemTime, UNIX_EPOCH}; /// /// // Generate the certificate authority's private key /// let ca_key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519)?; /// /// // Generate a "subject" key to be signed by the certificate authority. /// // Normally a user or host would do this locally and give the certificate /// // authority the public key. /// let subject_private_key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519)?; /// let subject_public_key = subject_private_key.public_key(); /// /// // Create certificate validity window /// let valid_after = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); /// let valid_before = valid_after + (365 * 86400); // e.g. 1 year /// /// // Initialize certificate builder /// let mut cert_builder = certificate::Builder::new_with_random_nonce( /// &mut OsRng, /// subject_public_key, /// valid_after, /// valid_before, /// )?; /// cert_builder.serial(42)?; // Optional: serial number chosen by the CA /// cert_builder.key_id("nobody-cert-02")?; // Optional: CA-specific key identifier /// cert_builder.cert_type(certificate::CertType::User)?; // User or host certificate /// cert_builder.valid_principal("nobody")?; // Unix username or hostname /// cert_builder.comment("nobody@example.com")?; // Comment (typically an email address) /// /// // Sign and return the `Certificate` for `subject_public_key` /// let cert = cert_builder.sign(&ca_key)?; /// # Ok(()) /// # } /// ``` pub struct Builder { public_key: public::KeyData, nonce: Vec, serial: Option, cert_type: Option, key_id: Option, valid_principals: Option>, valid_after: UnixTime, valid_before: UnixTime, critical_options: OptionsMap, extensions: OptionsMap, comment: Option, } impl Builder { /// Recommended size for a nonce. pub const RECOMMENDED_NONCE_SIZE: usize = 16; /// Create a new certificate builder for the given subject's public key. /// /// Also requires a nonce (random value typically 16 or 32 bytes long) and /// the validity window of the certificate as Unix seconds. pub fn new( nonce: impl Into>, public_key: impl Into, valid_after: u64, valid_before: u64, ) -> Result { let valid_after = UnixTime::new(valid_after).map_err(|_| Field::ValidAfter.invalid_error())?; let valid_before = UnixTime::new(valid_before).map_err(|_| Field::ValidBefore.invalid_error())?; if valid_before < valid_after { return Err(Field::ValidBefore.invalid_error()); } Ok(Self { nonce: nonce.into(), public_key: public_key.into(), serial: None, cert_type: None, key_id: None, valid_principals: None, valid_after, valid_before, critical_options: OptionsMap::new(), extensions: OptionsMap::new(), comment: None, }) } /// Create a new certificate builder with the validity window specified /// using [`SystemTime`] values. #[cfg(feature = "std")] pub fn new_with_validity_times( nonce: impl Into>, public_key: impl Into, valid_after: SystemTime, valid_before: SystemTime, ) -> Result { let valid_after = UnixTime::try_from(valid_after).map_err(|_| Field::ValidAfter.invalid_error())?; let valid_before = UnixTime::try_from(valid_before).map_err(|_| Field::ValidBefore.invalid_error())?; Self::new(nonce, public_key, valid_after.into(), valid_before.into()) } /// Create a new certificate builder, generating a random nonce using the /// provided random number generator. #[cfg(feature = "rand_core")] pub fn new_with_random_nonce( rng: &mut impl CryptoRngCore, public_key: impl Into, valid_after: u64, valid_before: u64, ) -> Result { let mut nonce = vec![0u8; Self::RECOMMENDED_NONCE_SIZE]; rng.fill_bytes(&mut nonce); Self::new(nonce, public_key, valid_after, valid_before) } /// Set certificate serial number. /// /// Default: `0`. pub fn serial(&mut self, serial: u64) -> Result<&mut Self> { if self.serial.is_some() { return Err(Field::Serial.invalid_error()); } self.serial = Some(serial); Ok(self) } /// Set certificate type: user or host. /// /// Default: [`CertType::User`]. pub fn cert_type(&mut self, cert_type: CertType) -> Result<&mut Self> { if self.cert_type.is_some() { return Err(Field::Type.invalid_error()); } self.cert_type = Some(cert_type); Ok(self) } /// Set key ID: label to identify this particular certificate. /// /// Default `""` pub fn key_id(&mut self, key_id: impl Into) -> Result<&mut Self> { if self.key_id.is_some() { return Err(Field::KeyId.invalid_error()); } self.key_id = Some(key_id.into()); Ok(self) } /// Add a principal (i.e. username or hostname) to `valid_principals`. pub fn valid_principal(&mut self, principal: impl Into) -> Result<&mut Self> { match &mut self.valid_principals { Some(principals) => principals.push(principal.into()), None => self.valid_principals = Some(vec![principal.into()]), } Ok(self) } /// Mark this certificate as being valid for all principals. /// /// # ⚠️ Security Warning /// /// Use this method with care! It generates "golden ticket" certificates /// which can e.g. authenticate as any user on a system, or impersonate /// any host. pub fn all_principals_valid(&mut self) -> Result<&mut Self> { self.valid_principals = Some(Vec::new()); Ok(self) } /// Add a critical option to this certificate. /// /// Critical options must be recognized or the certificate must be rejected. pub fn critical_option( &mut self, name: impl Into, data: impl Into, ) -> Result<&mut Self> { let name = name.into(); let data = data.into(); if self.critical_options.contains_key(&name) { return Err(Field::CriticalOptions.invalid_error()); } self.critical_options.insert(name, data); Ok(self) } /// Add an extension to this certificate. /// /// Extensions can be unrecognized without impacting the certificate. pub fn extension( &mut self, name: impl Into, data: impl Into, ) -> Result<&mut Self> { let name = name.into(); let data = data.into(); if self.extensions.contains_key(&name) { return Err(Field::Extensions.invalid_error()); } self.extensions.insert(name, data); Ok(self) } /// Add a comment to this certificate. /// /// Default `""` pub fn comment(&mut self, comment: impl Into) -> Result<&mut Self> { if self.comment.is_some() { return Err(Field::Comment.invalid_error()); } self.comment = Some(comment.into()); Ok(self) } /// Sign the certificate using the provided signer type. /// /// The [`PrivateKey`] type can be used as a signer. pub fn sign(self, signing_key: &S) -> Result { // Empty valid principals result in a "golden ticket", so this check // ensures that was explicitly configured via `all_principals_valid`. let valid_principals = match self.valid_principals { Some(principals) => principals, None => return Err(Field::ValidPrincipals.invalid_error()), }; let mut cert = Certificate { nonce: self.nonce, public_key: self.public_key, serial: self.serial.unwrap_or_default(), cert_type: self.cert_type.unwrap_or_default(), key_id: self.key_id.unwrap_or_default(), valid_principals, valid_after: self.valid_after, valid_before: self.valid_before, critical_options: self.critical_options, extensions: self.extensions, reserved: Vec::new(), comment: self.comment.unwrap_or_default(), signature_key: signing_key.public_key(), signature: Signature::placeholder(), }; let mut tbs_cert = Vec::new(); cert.encode_tbs(&mut tbs_cert)?; cert.signature = signing_key.try_sign(&tbs_cert)?; #[cfg(debug_assertions)] cert.validate_at( cert.valid_after.into(), &[cert.signature_key.fingerprint(Default::default())], )?; Ok(cert) } }