diff --git a/keylime-agent/src/error.rs b/keylime-agent/src/error.rs index 23f4e4c2..5b41d98a 100644 --- a/keylime-agent/src/error.rs +++ b/keylime-agent/src/error.rs @@ -89,6 +89,10 @@ pub(crate) enum Error { ListParser(#[from] keylime::list_parser::ListParsingError), #[error("Zip error: {0}")] Zip(#[from] zip::result::ZipError), + #[error("Certificate generation error")] + CertificateGeneration( + #[from] keylime::crypto::x509::CertificateBuilderError, + ), #[error("{0}")] Other(String), } diff --git a/keylime-agent/src/main.rs b/keylime-agent/src/main.rs index a426cb9d..f015f179 100644 --- a/keylime-agent/src/main.rs +++ b/keylime-agent/src/main.rs @@ -55,7 +55,10 @@ use futures::{ future::{ok, TryFutureExt}, try_join, }; -use keylime::{crypto, ima::MeasurementList, list_parser::parse_list, tpm}; +use keylime::{ + crypto, crypto::x509::CertificateBuilder, ima::MeasurementList, + list_parser::parse_list, tpm, +}; use log::*; use openssl::{ pkey::{PKey, Private, Public}, @@ -586,16 +589,16 @@ async fn main() -> Result<()> { let mtls_cert; let ssl_context; if config.agent.enable_agent_mtls { - let contact_ips = vec![config.agent.contact_ip.clone()]; + let contact_ips = vec![config.agent.contact_ip.as_str()]; cert = match config.agent.server_cert.as_ref() { "" => { debug!("The server_cert option was not set in the configuration file"); - crypto::generate_x509( - &nk_priv, - &agent_uuid, - Some(contact_ips), - )? + crypto::x509::CertificateBuilder::new() + .private_key(&nk_priv) + .common_name(&agent_uuid) + .add_ips(contact_ips) + .build()? } path => { let cert_path = Path::new(&path); @@ -607,11 +610,11 @@ async fn main() -> Result<()> { crypto::load_x509_pem(cert_path)? } else { debug!("Generating new mTLS certificate"); - let cert = crypto::generate_x509( - &nk_priv, - &agent_uuid, - Some(contact_ips), - )?; + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&nk_priv) + .common_name(&agent_uuid) + .add_ips(contact_ips) + .build()?; // Write the generated certificate crypto::write_x509(&cert, cert_path)?; cert diff --git a/keylime-agent/src/registrar_agent.rs b/keylime-agent/src/registrar_agent.rs index 8fff336f..78555d5a 100644 --- a/keylime-agent/src/registrar_agent.rs +++ b/keylime-agent/src/registrar_agent.rs @@ -204,6 +204,7 @@ pub(crate) async fn do_register_agent( mod tests { use super::*; use crate::crypto; + use keylime::crypto; use wiremock::matchers::{any, method}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -233,12 +234,12 @@ mod tests { let mock_data = [0u8; 1]; let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] - let cert = crypto::generate_x509( - &priv_key, - "uuid", - Some(vec!["1.2.3.4".to_string()]), - ) - .unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("uuid") + .add_ips(vec!["1.2.3.4"]) + .build() + .unwrap(); //#[allow_ci] let response = do_register_agent( ip, port, @@ -286,12 +287,12 @@ mod tests { let mock_data = [0u8; 1]; let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] - let cert = crypto::generate_x509( - &priv_key, - "uuid", - Some(vec!["1.2.3.4".to_string(), "1.2.3.5".to_string()]), - ) - .unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("uuid") + .add_ips(vec!["1.2.3.4", "1.2.3.5"]) + .build() + .unwrap(); //#[allow_ci] let response = do_register_agent( ip, port, @@ -335,7 +336,11 @@ mod tests { let mock_data = [0u8; 1]; let priv_key = crypto::testing::rsa_generate(2048).unwrap(); //#[allow_ci] - let cert = crypto::generate_x509(&priv_key, "uuid", None).unwrap(); //#[allow_ci] + let cert = crypto::x509::CertificateBuilder::new() + .private_key(&priv_key) + .common_name("uuid") + .build() + .unwrap(); //#[allow_ci] let response = do_register_agent( ip, port, diff --git a/keylime/src/crypto.rs b/keylime/src/crypto.rs index 65ec9afa..820f35f3 100644 --- a/keylime/src/crypto.rs +++ b/keylime/src/crypto.rs @@ -1,10 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2021 Keylime Authors +pub mod x509; + use base64::{engine::general_purpose, Engine as _}; use log::*; use openssl::{ - asn1::Asn1Time, ec::{EcGroupRef, EcKey}, encrypt::Decrypter, hash::MessageDigest, @@ -17,7 +18,7 @@ use openssl::{ ssl::{SslAcceptor, SslAcceptorBuilder, SslMethod, SslVerifyMode}, symm::Cipher, x509::store::X509StoreBuilder, - x509::{extension, X509Name, X509}, + x509::X509, }; use picky_asn1_x509::SubjectPublicKeyInfo; use std::{ @@ -33,18 +34,8 @@ pub const AES_128_KEY_LEN: usize = 16; pub const AES_256_KEY_LEN: usize = 32; pub const AES_BLOCK_SIZE: usize = 16; -static LOCAL_IPS: &[&str] = &["127.0.0.1", "::1"]; -static LOCAL_DNS_NAMES: &[&str] = &["localhost", "localhost.domain"]; - #[derive(Error, Debug)] pub enum CryptoError { - /// Error getting ASN.1 Time from days from now - #[error("failed to get ASN.1 Time for {days} day(s) from now")] - ASN1TimeDaysFromNowError { - days: u32, - source: openssl::error::ErrorStack, - }, - /// Error decoding base64 #[error("failed to decode base64")] Base64DecodeError(#[from] base64::DecodeError), @@ -234,13 +225,6 @@ pub enum CryptoError { source: openssl::error::ErrorStack, }, - /// X509 certificate builder error - #[error("X509 certificate builder error: {message}")] - X509BuilderError { - message: String, - source: openssl::error::ErrorStack, - }, - /// Error loading X509 certificate chain from PEM file #[error("failed to load X509 certificate chain from PEM file")] X509ChainFromPEMError(#[source] openssl::error::ErrorStack), @@ -257,13 +241,6 @@ pub enum CryptoError { #[error("failed to get certificate public key")] X509GetPublicError(#[source] openssl::error::ErrorStack), - /// Error creating X509 Name - #[error("Error creating X509 Name: {message}")] - X509NameError { - message: String, - source: openssl::error::ErrorStack, - }, - /// Error encoding X509 certificate in DER format #[error("failed to encode X509 certificate in DER format")] X509ToDERError(#[source] openssl::error::ErrorStack), @@ -536,7 +513,7 @@ pub fn load_key_pair( None => PKey::private_key_from_pem(&pem) .map_err(CryptoError::PrivateKeyFromPEMError)?, }; - let public = pkey_pub_from_priv(private.clone())?; + let public = pkey_pub_from_priv(&private)?; Ok((public, private)) } @@ -604,7 +581,7 @@ pub fn rsa_generate_pair( key_size: u32, ) -> Result<(PKey, PKey), CryptoError> { let private = rsa_generate(key_size)?; - let public = pkey_pub_from_priv(private.clone())?; + let public = pkey_pub_from_priv(&private)?; Ok((public, private)) } @@ -620,13 +597,13 @@ pub fn ecc_generate_pair( .map_err(CryptoError::ECGeneratePrivateKeyError)?, ) .map_err(CryptoError::PKeyFromEcKeyError)?; - let public = pkey_pub_from_priv(private.clone())?; + let public = pkey_pub_from_priv(&private)?; Ok((public, private)) } fn pkey_pub_from_priv( - privkey: PKey, + privkey: &PKey, ) -> Result, CryptoError> { match privkey.id() { Id::RSA => { @@ -673,113 +650,6 @@ pub fn pkey_pub_to_pem(pubkey: &PKey) -> Result { }) } -pub fn generate_x509( - key: &PKey, - uuid: &str, - additional_ips: Option>, -) -> Result { - let mut name = - X509Name::builder().map_err(|source| CryptoError::X509NameError { - message: "failed to create X509 Name object".into(), - source, - })?; - name.append_entry_by_nid(Nid::COMMONNAME, uuid) - .map_err(|source| CryptoError::X509NameError { - message: "failed to append entry by NID to X509 Name".into(), - source, - })?; - let name = name.build(); - - let valid_from = Asn1Time::days_from_now(0).map_err(|source| { - CryptoError::ASN1TimeDaysFromNowError { days: 0, source } - })?; - let valid_to = Asn1Time::days_from_now(365).map_err(|source| { - CryptoError::ASN1TimeDaysFromNowError { days: 365, source } - })?; - - let mut builder = - X509::builder().map_err(|source| CryptoError::X509BuilderError { - message: "failed to create X509 certificate builder object" - .into(), - source, - })?; - builder.set_version(2).map_err(|source| { - CryptoError::X509BuilderError { - message: "failed to set X509 certificate version".into(), - source, - } - })?; - builder.set_subject_name(&name).map_err(|source| { - CryptoError::X509BuilderError { - message: "failed to set X509 certificate subject name".into(), - source, - } - })?; - builder.set_issuer_name(&name).map_err(|source| { - CryptoError::X509BuilderError { - message: "failed to set X509 issuer name".into(), - source, - } - })?; - builder.set_not_before(&valid_from).map_err(|source| { - CryptoError::X509BuilderError { - message: "failed to set X509 certificate Not Before date".into(), - source, - } - })?; - builder.set_not_after(&valid_to).map_err(|source| { - CryptoError::X509BuilderError { - message: "failed to set X509 certificate Not After date".into(), - source, - } - })?; - builder.set_pubkey(key).map_err(|source| { - CryptoError::X509BuilderError { - message: "failed to set X509 certificate public key".into(), - source, - } - })?; - let mut san = &mut extension::SubjectAlternativeName::new(); - for local_domain_name in LOCAL_DNS_NAMES.iter() { - san = san.dns(local_domain_name); - } - for local_ip in LOCAL_IPS.iter() { - san = san.ip(local_ip); - } - match additional_ips { - None => {} - Some(ips) => { - for ip in ips.iter() { - if !LOCAL_IPS.iter().any(|e| ip.contains(e)) { - san = san.ip(ip); - } - } - } - } - let x509 = - san.build(&builder.x509v3_context(None, None)) - .map_err(|source| CryptoError::X509BuilderError { - message: "failed to build Subject Alternative Name".into(), - source, - })?; - builder.append_extension(x509).map_err(|source| { - CryptoError::X509BuilderError { - message: - "failed to append X509 certificate Subject Alternative Name extension" - .into(), - source, - } - })?; - builder - .sign(key, MessageDigest::sha256()) - .map_err(|source| CryptoError::X509BuilderError { - message: "failed to sign X509 certificate".into(), - source, - })?; - - Ok(builder.build()) -} - pub fn generate_tls_context( tls_cert: &X509, key: &PKey, @@ -1125,7 +995,7 @@ pub mod testing { ) -> Result<(PKey, PKey), CryptoTestError> { let contents = read_to_string(path)?; let private = PKey::private_key_from_pem(contents.as_bytes())?; - let public = pkey_pub_from_priv(private.clone())?; + let public = pkey_pub_from_priv(&private)?; Ok((public, private)) } @@ -1224,6 +1094,7 @@ pub mod testing { #[cfg(test)] mod tests { use super::*; + use crate::crypto::x509::CertificateBuilder; use std::{fs, path::Path}; use testing::{encrypt_aead, rsa_import_pair, rsa_oaep_encrypt}; @@ -1387,7 +1258,7 @@ mod tests { let contents = read_to_string(rsa_key_path); let private = PKey::private_key_from_pem(contents.unwrap().as_bytes()).unwrap(); //#[allow_ci] - let public = pkey_pub_from_priv(private).unwrap(); //#[allow_ci] + let public = pkey_pub_from_priv(&private).unwrap(); //#[allow_ci] let message = String::from("Hello World!"); @@ -1471,10 +1342,14 @@ mod tests { assert_eq!(hex, "db9b1cd3262dee37756a09b9064973589847caa8e53d31a9d142ea2701b1b28abd97838bb9a27068ba305dc8d04a45a1fcf079de54d607666996b3cc54f6b67c"); } - fn test_x509(privkey: PKey, pubkey: PKey) { + fn test_x509(privkey: &PKey, pubkey: PKey) { let tempdir = tempfile::tempdir().unwrap(); //#[allow_ci] - let r = generate_x509(&privkey, "uuidA", None); + let r = CertificateBuilder::new() + .private_key(privkey) + .common_name("uuidA") + .build(); + assert!(r.is_ok()); let cert_a = r.unwrap(); //#[allow_ci] let cert_a_path = tempdir.path().join("cert_a.pem"); @@ -1482,11 +1357,12 @@ mod tests { assert!(r.is_ok()); assert!(cert_a_path.exists()); - let r = generate_x509( - &privkey, - "uuidB", - Some(vec!["1.2.3.4".to_string(), "1.2.3.5".to_string()]), - ); + let r = CertificateBuilder::new() + .private_key(privkey) + .common_name("uuidB") + .add_ips(vec!["1.2.3.4"]) + .build(); + assert!(r.is_ok()); let cert_b = r.unwrap(); //#[allow_ci] let cert_b_path = tempdir.path().join("cert_b.pem"); @@ -1555,14 +1431,14 @@ mod tests { let loaded_list = r.unwrap(); //#[allow_ci] assert!(loaded_list.len() == 2); - let r = generate_tls_context(&loaded_a, &privkey, loaded_list); + let r = generate_tls_context(&loaded_a, privkey, loaded_list); assert!(r.is_ok()); } #[test] fn test_x509_rsa() { let (pubkey, privkey) = rsa_generate_pair(2048).unwrap(); //#[allow_ci] - test_x509(privkey, pubkey); + test_x509(&privkey, pubkey); } #[test] @@ -1570,7 +1446,7 @@ mod tests { fn test_x509_long_rsa() { for length in [3072, 4096] { let (pubkey, privkey) = rsa_generate_pair(length).unwrap(); //#[allow_ci] - test_x509(privkey, pubkey); + test_x509(&privkey, pubkey); } } @@ -1586,10 +1462,9 @@ mod tests { ] { let (pubkey, privkey) = ecc_generate_pair(&group).unwrap(); //#[allow_ci] - test_x509(privkey, pubkey); + test_x509(&privkey, pubkey); } } - #[test] fn test_match_cert_to_template() { for (file_name, template) in diff --git a/keylime/src/crypto/x509.rs b/keylime/src/crypto/x509.rs new file mode 100644 index 00000000..f669de47 --- /dev/null +++ b/keylime/src/crypto/x509.rs @@ -0,0 +1,588 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Keylime Authors +use openssl::{ + asn1::Asn1Time, + hash::MessageDigest, + nid::Nid, + pkey::{PKey, Private}, + x509::{extension, X509Extension, X509Name, X509}, +}; +use thiserror::Error; + +static LOCAL_IPS: &[&str] = &["127.0.0.1", "::1"]; +static LOCAL_DNS_NAMES: &[&str] = &["localhost", "localhost.domain"]; + +#[derive(Error, Debug)] +pub enum CertificateBuilderError { + /// Error getting ASN.1 Time from days from now + #[error("failed to get ASN.1 Time for {days} day(s) from now")] + ASN1TimeDaysFromNowError { + days: u32, + source: openssl::error::ErrorStack, + }, + + /// X509 certificate builder error + #[error("X509 certificate builder error: {message}")] + BuilderError { + message: String, + source: openssl::error::ErrorStack, + }, + + /// Failed to get public key from the private key + #[error("failed to get public key from the private key")] + PubkeyFromPrivError { source: crate::crypto::CryptoError }, + + /// Common name not set on CertificateBuilder + #[error("Common Name not set on CertificateBuilder. Set the common name with the common_name() method from the CertificateBuilder object")] + MissingCommonNameError, + + /// Private key not set on CertificateBuilder + #[error("Private key not set on CertificateBuilder. Set the private key with the private_key() method from the CertificateBuilder object")] + MissingPrivateKeyError, + + /// Error creating X509 Name + #[error("Error creating X509 Name: {message}")] + NameBuilderError { + message: String, + source: openssl::error::ErrorStack, + }, +} + +#[derive(Default)] +pub struct CertificateBuilder<'a> { + common_name: Option<&'a str>, + dns_names: Option>, + extensions: Option>, + hash_algorithm: Option, + ips: Option>, + not_after: Option, + not_before: Option, + private_key: Option<&'a PKey>, + version: Option, +} + +impl<'a> CertificateBuilder<'a> { + /// Create a new CertificateBuilder object + pub fn new() -> CertificateBuilder<'a> { + CertificateBuilder::default() + } + + /// Set the CertificateBuilder Common Name to use when generating the certificate + /// + /// # Arguments: + /// + /// * cn (&str): The subject Common Name + pub fn common_name( + &'a mut self, + cn: &'a str, + ) -> &mut CertificateBuilder<'a> { + self.common_name = Some(cn); + self + } + + /// Set the hash algorithm to be used when signing the certificate + /// + /// # Arguments: + /// + /// * hash_algorithm (MessageDigest): The hash algorithm to be used when signing the certificate + pub fn hash_algorithm( + &'a mut self, + hash_algorithm: MessageDigest, + ) -> &mut CertificateBuilder<'a> { + self.hash_algorithm = Some(hash_algorithm); + self + } + + /// Set the certificate start of validity, in days from now + /// + /// # Arguments: + /// + /// * days_from_now (u32): The number of days from now when the built certificate will become + /// valid + pub fn not_before( + &'a mut self, + days_from_now: u32, + ) -> &mut CertificateBuilder<'a> { + self.not_before = Some(days_from_now); + self + } + + /// Set the certificate expiration date, in days from now + /// + /// # Arguments: + /// + /// * days_from_now (u32): The number of days from now when the built certificate will expire + pub fn not_after( + &'a mut self, + days_from_now: u32, + ) -> &mut CertificateBuilder<'a> { + self.not_after = Some(days_from_now); + self + } + + /// Set the certificate X.509 standard version. + /// + /// # Arguments: + /// + /// * version (i32): The version number. Note that the version is zero-indexed, meaning passing + /// the value `2` corresponds to the version 3 of the X.509 standard + /// + /// If not called, the version 3 of the X.509 standard will be used + pub fn version( + &'a mut self, + version: i32, + ) -> &mut CertificateBuilder<'a> { + self.version = Some(version); + self + } + + /// Set the private key associated with the certificate + /// + /// # Arguments: + /// + /// * private_key (PKey): The private key to be associated with the certificate + pub fn private_key( + &'a mut self, + private_key: &'a PKey, + ) -> &mut CertificateBuilder<'a> { + self.private_key = Some(private_key); + self + } + + /// Set DNS names to add to the Subject Alternative Name + /// + /// # Arguments: + /// + /// * dns_names (Vec<&str>): A Vec<&str> containing DNS names to add to the certificate Subject + /// Alternative Name + pub fn add_dns_names( + &'a mut self, + dns_names: Vec<&'a str>, + ) -> &mut CertificateBuilder<'a> { + match &mut self.dns_names { + None => { + self.dns_names = Some(dns_names); + } + Some(v) => { + for name in dns_names { + v.push(name); + } + } + } + self + } + + /// Set additional IPs to add to the Subject Alternative Name + /// + /// # Arguments: + /// + /// * ips: (Vec<&str>): A Vec<&str> containing IPs to add to the certificate Subject + /// Alternative Name + pub fn add_ips( + &'a mut self, + ips: Vec<&'a str>, + ) -> &mut CertificateBuilder<'a> { + match &mut self.ips { + None => { + self.ips = Some(ips); + } + Some(v) => { + for ip in ips { + v.push(ip); + } + } + } + self + } + + /// Set additional extensions to include in the certificate + /// + /// # Arguments: + /// + /// * extensions (Vec): A Vec containing the additional + /// extensions to include in the certificate + pub fn add_extensions( + &'a mut self, + extensions: Vec, + ) -> &mut CertificateBuilder<'a> { + match &mut self.extensions { + None => { + self.extensions = Some(extensions); + } + Some(v) => { + for extension in extensions { + v.push(extension); + } + } + } + self + } + + /// Generate the certificate using the previously set options + pub fn build(&'a mut self) -> Result { + let mut name_builder = X509Name::builder().map_err(|source| { + CertificateBuilderError::NameBuilderError { + message: "failed to create X509 Name object".into(), + source, + } + })?; + + let mut builder = X509::builder().map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to create X509 certificate builder object" + .into(), + source, + } + })?; + + match self.common_name { + Some(cn) => { + name_builder + .append_entry_by_nid(Nid::COMMONNAME, cn) + .map_err(|source| { + CertificateBuilderError::NameBuilderError { + message: + "failed to set Common Name in Name builder" + .into(), + source, + } + })?; + } + None => { + return Err(CertificateBuilderError::MissingCommonNameError); + } + } + + let name = name_builder.build(); + builder.set_subject_name(&name).map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to set X509 certificate subject name".into(), + source, + } + })?; + + // Self-signed certificate, the issuer is the same as the subject + builder.set_issuer_name(&name).map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to set X509 issuer name".into(), + source, + } + })?; + + // If the not_before is not set, use the default value of 0 to make the certificate valid + // from now + let not_before = self.not_before.unwrap_or(0); + let valid_from = + Asn1Time::days_from_now(not_before).map_err(|source| { + CertificateBuilderError::ASN1TimeDaysFromNowError { + days: not_before, + source, + } + })?; + builder.set_not_before(&valid_from).map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to set X509 certificate Not Before date" + .into(), + source, + } + })?; + + // If the not_after is not set, use the default value of 365 days + let not_after = self.not_after.unwrap_or(365); + let valid_to = + Asn1Time::days_from_now(not_after).map_err(|source| { + CertificateBuilderError::ASN1TimeDaysFromNowError { + days: not_after, + source, + } + })?; + builder.set_not_after(&valid_to).map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to set X509 certificate Not After date" + .into(), + source, + } + })?; + + // If the version is not set, use the default value 2, which corresponds to X.509 version 3 + let v = self.version.unwrap_or(2); + builder.set_version(v).map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to set X509 certificate version".into(), + source, + } + })?; + + match self.private_key { + Some(p) => { + let pubkey = crate::crypto::pkey_pub_from_priv(p).map_err( + |source| CertificateBuilderError::PubkeyFromPrivError { + source, + }, + )?; + + builder.set_pubkey(&pubkey).map_err(|source| { + CertificateBuilderError::BuilderError { + message: "failed to set X509 certificate public key" + .into(), + source, + } + })?; + + let h = + self.hash_algorithm.unwrap_or(MessageDigest::sha256()); + builder + .sign(p, h) + .map_err(|source| CertificateBuilderError::BuilderError { + message: "failed to set X509 certificate builder signing private key and hashing algorithm".into(), + source, + })?; + } + None => { + return Err(CertificateBuilderError::MissingPrivateKeyError); + } + } + + // Build Subject Alternative Name + let mut san = &mut extension::SubjectAlternativeName::new(); + for dns_name in LOCAL_DNS_NAMES.iter() { + san = san.dns(dns_name); + } + + if let Some(dns_names) = &self.dns_names { + for dns_name in + dns_names.iter().filter(|&n| !LOCAL_DNS_NAMES.contains(n)) + { + san = san.dns(dns_name); + } + } + + for local_ip in LOCAL_IPS.iter() { + san = san.ip(local_ip); + } + + if let Some(additional_ips) = &self.ips { + for ip in + additional_ips.iter().filter(|&i| !LOCAL_IPS.contains(i)) + { + san = san.ip(ip); + } + } + + let x509 = san.build(&builder.x509v3_context(None, None)).map_err( + |source| CertificateBuilderError::BuilderError { + message: "failed to build Subject Alternative Name".into(), + source, + }, + )?; + builder.append_extension(x509).map_err(|source| { + CertificateBuilderError::BuilderError { + message: + "failed to append X509 certificate Subject Alternative Name extension" + .into(), + source, + } + })?; + + Ok(builder.build()) + } +} + +// Unit Testing +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::*; + + fn test_generate_certificate( + privkey: PKey, + pubkey: PKey, + ) { + // Minimal certificate + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + let cert_pubkey_pem = + cert.public_key().unwrap().public_key_to_pem().unwrap(); //#[allow_ci] + let pubkey_pem = pubkey.public_key_to_pem().unwrap(); //#[allow_ci] + assert_eq!(cert_pubkey_pem, pubkey_pem); + + // Setting hash algorithm + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .hash_algorithm(MessageDigest::sha512()) + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + let sig_alg = cert + .signature_algorithm() + .object() + .nid() + .signature_algorithms() + .unwrap(); //#[allow_ci] + assert_eq!(sig_alg.digest, openssl::nid::Nid::SHA512); + + // Setting certificate validity not_before + let two_days_from_now = Asn1Time::days_from_now(2).unwrap(); //#[allow_ci] + let r = CertificateBuilder::new() + .not_before(2) + .private_key(&privkey) + .common_name("uuidA") + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + // There is a very small chance that this will fail when the value for the full second + // changes between the time the certificate is generated and now + assert!(cert.not_before() == two_days_from_now); + + // Setting certificate validity not_after + let ten_days_from_now = Asn1Time::days_from_now(10).unwrap(); //#[allow_ci] + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .not_after(10) + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + // There is a very small chance that this will fail when the value for the full second + // changes between the time the certificate is generated and now + assert!(cert.not_after() == ten_days_from_now); + + // Setting certificate version explicitly + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .version(2) + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + assert_eq!(cert.version(), 2); + + // Adding extra DNS names + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .add_dns_names(vec!["hostname", "hostname2"]) + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + let san: Vec = cert + .subject_alt_names() + .unwrap() //#[allow_ci] + .into_iter() + .filter_map(|n| n.dnsname().map(|n| n.to_owned())) + .collect(); + assert!(san.contains(&"hostname".to_string())); + assert!(san.contains(&"hostname2".to_string())); + + // Adding extra IPv4 addresses + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .add_ips(vec!["192.168.0.1", "172.30.1.15"]) + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + let san: Vec> = cert + .subject_alt_names() + .unwrap() //#[allow_ci] + .into_iter() + .filter_map(|i| i.ipaddress().map(|i| i.to_owned())) + .collect(); + assert!(san.contains( + &"192.168.0.1" + .parse::() + .unwrap() //#[allow_ci] + .octets() + .as_ref() + .to_owned() + )); + assert!(san.contains( + &"172.30.1.15" + .parse::() + .unwrap() //#[allow_ci] + .octets() + .as_ref() + .to_owned() + )); + + // Adding extra IPv6 addresses + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .add_ips(vec!["::2:3", "::4:5"]) + .build(); + assert!(r.is_ok()); + let cert = r.unwrap(); //#[allow_ci] + let san: Vec> = cert + .subject_alt_names() + .unwrap() //#[allow_ci] + .into_iter() + .filter_map(|n| n.ipaddress().map(|n| n.to_owned())) + .collect(); + assert!(san.contains( + &"::2:3" + .parse::() + .unwrap() //#[allow_ci] + .octets() + .as_ref() + .to_owned() + )); + assert!(san.contains( + &"::4:5" + .parse::() + .unwrap() //#[allow_ci] + .octets() + .as_ref() + .to_owned() + )); + + // Adding extra extensions + let bc = x509::extension::BasicConstraints::new() + .ca() + .critical() + .build() + .unwrap(); //#[allow_ci] + let r = CertificateBuilder::new() + .private_key(&privkey) + .common_name("uuidA") + .add_extensions(vec![bc]) + .build(); + assert!(r.is_ok()); + } + + #[test] + fn test_generate_rsa_certificate() { + let (pubkey, privkey) = rsa_generate_pair(2048).unwrap(); //#[allow_ci] + test_generate_certificate(privkey, pubkey); + } + + #[test] + #[ignore] + fn test_generate_long_rsa_certificate() { + for length in [3072, 4096] { + let (pubkey, privkey) = rsa_generate_pair(length).unwrap(); //#[allow_ci] + test_generate_certificate(privkey, pubkey); + } + } + + #[test] + fn test_generate_ecc_certificate() { + use openssl::ec::EcGroup; + + for group in [ + EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(), //#[allow_ci] + EcGroup::from_curve_name(Nid::SECP256K1).unwrap(), //#[allow_ci] + EcGroup::from_curve_name(Nid::SECP384R1).unwrap(), //#[allow_ci], + EcGroup::from_curve_name(Nid::SECP521R1).unwrap(), //#[allow_ci] + ] { + let (pubkey, privkey) = ecc_generate_pair(&group).unwrap(); //#[allow_ci] + + test_generate_certificate(privkey, pubkey); + } + } +}