//! This module contains cryptographic setup code, a long with what policy //! and ciphers we accept. use openssl::ec::{EcGroup, EcKey}; use openssl::error::ErrorStack; use openssl::nid::Nid; use openssl::pkey::{PKeyRef, Private}; use openssl::rsa::Rsa; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; use openssl::x509::{ extension::{ AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, }, X509NameBuilder, X509ReqBuilder, X509, }; use openssl::{asn1, bn, hash, pkey}; use sketching::*; use crate::config::Configuration; use std::fs::File; use std::io::{Read, Write}; use std::path::Path; const CA_VALID_DAYS: u32 = 30; const CERT_VALID_DAYS: u32 = 5; // Basing minimums off https://www.keylength.com setting "year" to 2030 - tested as at 2023-09-25 // // |Method |Date |Symmetric| FM |DL Key| DL Group|Elliptic Curve|Hash| // | --- | --- | --- | --- | --- | --- | --- | ---| // |Lenstra / Verheul|2030 | 93 |2493^2016|165 | 2493 | 176 | 186| // |Lenstra Updated  |2030 | 88 |1698^2063|176 | 1698 | 176 | 176| // |ECRYPT |2029-2068| 256 |15360 |512 | 15360 | 512 | 512| // |NIST |2019-2030| 112 |2048 |224 | 2048 | 224 | 224| // |ANSSI |> 2030 | 128 |3072 |200 | 3072 | 256 | 256| // |NSA |- | 256 |3072 |- | - | 384 | 384| // |RFC3766  |- | - | - | - | - | - | - | // |BSI |- | - | - | - | - | - | - | // DL - Discrete Logarithm // FM - Factoring Modulus const RSA_MIN_KEY_SIZE_BITS: u64 = 2048; const EC_MIN_KEY_SIZE_BITS: u64 = 224; /// returns a signing function that meets a sensible minimum fn get_signing_func() -> hash::MessageDigest { hash::MessageDigest::sha256() } /// Ensure we're enforcing safe minimums for TLS keys pub fn check_privkey_minimums(privkey: &PKeyRef) -> Result<(), String> { if let Ok(key) = privkey.rsa() { if key.size() < (RSA_MIN_KEY_SIZE_BITS / 8) as u32 { Err(format!( "TLS RSA key is less than {} bits!", RSA_MIN_KEY_SIZE_BITS )) } else { debug!( "The RSA private key size is: {} bits, that's OK!", key.size() * 8 ); Ok(()) } } else if let Ok(key) = privkey.ec_key() { // allowing this to panic because ... it's an i32 and hopefully we don't have negative bit lengths? #[allow(clippy::panic)] let key_bits: u64 = key.private_key().num_bits().try_into().unwrap_or_else(|_| { panic!( "Failed to convert EC bitlength {} to u64", key.private_key().num_bits() ) }); if key_bits < EC_MIN_KEY_SIZE_BITS { Err(format!( "TLS EC key is less than {} bits! Got: {}", EC_MIN_KEY_SIZE_BITS, key_bits )) } else { #[cfg(any(test, debug_assertions))] println!("The EC private key size is: {} bits, that's OK!", key_bits); debug!("The EC private key size is: {} bits, that's OK!", key_bits); Ok(()) } } else { error!("TLS key is not RSA or EC, cannot check minimums!"); Ok(()) } } /// From the server configuration, generate an OpenSSL acceptor that we can use /// to build our sockets for HTTPS/LDAPS. pub fn setup_tls(config: &Configuration) -> Result, ()> { match &config.tls_config { Some(tls_config) => { // Signing algorithm minimums are enforced by the SSLAcceptor - it won't start up with a sha1-signed cert. // https://wiki.mozilla.org/Security/Server_Side_TLS let mut ssl_builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).map_err(|openssl_err| { error!("Failed to start TLS builder"); error!(?openssl_err); })?; ssl_builder .set_certificate_chain_file(&tls_config.chain) .map_err(|openssl_err| { error!("Failed to access certificate chain file"); error!(?openssl_err); let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.chain); info!(%diag); })?; ssl_builder .set_private_key_file(&tls_config.key, SslFiletype::PEM) .map_err(|openssl_err| { error!("Failed to access private key file"); error!(?openssl_err); let diag = kanidm_lib_file_permissions::diagnose_path(&tls_config.chain); info!(%diag); })?; ssl_builder.check_private_key().map_err(|openssl_err| { error!("Failed to validate private key"); error!(?openssl_err); })?; let acceptor = ssl_builder.build(); // let's enforce some TLS minimums! let privkey = acceptor.context().private_key().ok_or_else(|| { error!("Failed to access acceptor private key"); })?; check_privkey_minimums(privkey).map_err(|err| { error!("{}", err); })?; Ok(Some(acceptor)) } None => Ok(None), } } fn get_ec_group() -> Result { EcGroup::from_curve_name(Nid::X9_62_PRIME256V1) } #[derive(Debug)] pub(crate) struct CaHandle { key: pkey::PKey, cert: X509, } pub(crate) fn write_ca( key_ar: impl AsRef, cert_ar: impl AsRef, handle: &CaHandle, ) -> Result<(), ()> { let key_path: &Path = key_ar.as_ref(); let cert_path: &Path = cert_ar.as_ref(); let key_pem = handle.key.private_key_to_pem_pkcs8().map_err(|e| { error!(err = ?e, "Failed to convert key to PEM"); })?; let cert_pem = handle.cert.to_pem().map_err(|e| { error!(err = ?e, "Failed to convert cert to PEM"); })?; File::create(key_path) .and_then(|mut file| file.write_all(&key_pem)) .map_err(|e| { error!(err = ?e, "Failed to create {:?}", key_path); })?; File::create(cert_path) .and_then(|mut file| file.write_all(&cert_pem)) .map_err(|e| { error!(err = ?e, "Failed to create {:?}", cert_path); }) } #[derive(Debug)] pub enum KeyType { #[allow(dead_code)] Rsa, Ec, } impl Default for KeyType { fn default() -> Self { Self::Ec } } #[derive(Debug)] pub struct CAConfig { pub key_type: KeyType, pub key_bits: u64, pub skip_enforce_minimums: bool, } impl Default for CAConfig { fn default() -> Self { #[allow(clippy::expect_used)] Self::new(KeyType::Ec, 256, false) .expect("Somehow the defaults failed to pass validation while building a CA Config?") } } impl CAConfig { fn new(key_type: KeyType, key_bits: u64, skip_enforce_minimums: bool) -> Result { let res = Self { key_type, key_bits, skip_enforce_minimums, }; if !skip_enforce_minimums { res.enforce_minimums()?; }; Ok(res) } /// Make sure we're meeting the minimum spec for key length etc fn enforce_minimums(&self) -> Result<(), String> { match self.key_type { KeyType::Rsa => { trace!( "Generating CA Config for RSA Key with {} bits", self.key_bits ); if self.key_bits < RSA_MIN_KEY_SIZE_BITS { return Err(format!( "RSA key size must be at least {} bits", RSA_MIN_KEY_SIZE_BITS )); } } KeyType::Ec => { trace!("Generating CA Config for EcKey with {} bits", self.key_bits); if self.key_bits < EC_MIN_KEY_SIZE_BITS { return Err(format!( "EC key size must be at least {} bits", EC_MIN_KEY_SIZE_BITS )); } } }; Ok(()) } } pub(crate) fn gen_private_key( key_type: &KeyType, key_bits: Option, ) -> Result, ErrorStack> { match key_type { KeyType::Rsa => { let key_bits = key_bits.unwrap_or(RSA_MIN_KEY_SIZE_BITS); let rsa = Rsa::generate(key_bits as u32)?; pkey::PKey::from_rsa(rsa) } KeyType::Ec => { // TODO: take key bitlength and use it for the curve group, somehow? let ecgroup = get_ec_group()?; let eckey = EcKey::generate(&ecgroup)?; pkey::PKey::from_ec_key(eckey) } } } /// build up a CA certificate and key. pub(crate) fn build_ca(ca_config: Option) -> Result { let ca_config = ca_config.unwrap_or_default(); let ca_key = gen_private_key(&ca_config.key_type, Some(ca_config.key_bits))?; if !ca_config.skip_enforce_minimums { check_privkey_minimums(&ca_key).map_err(|err| { admin_error!("failed to build_ca due to privkey minimums {}", err); #[cfg(any(test, debug_assertions))] println!("failed to build_ca due to privkey minimums: {}", err); ErrorStack::get() // this probably should be a real errorstack but... how? })?; } let mut x509_name = X509NameBuilder::new()?; x509_name.append_entry_by_text("C", "AU")?; x509_name.append_entry_by_text("ST", "QLD")?; x509_name.append_entry_by_text("O", "Kanidm")?; x509_name.append_entry_by_text("CN", "Kanidm Generated CA")?; x509_name.append_entry_by_text("OU", "Development and Evaluation - NOT FOR PRODUCTION")?; let x509_name = x509_name.build(); let mut cert_builder = X509::builder()?; // Yes, 2 actually means 3 here ... cert_builder.set_version(2)?; let serial_number = bn::BigNum::from_u32(1).and_then(|serial| serial.to_asn1_integer())?; cert_builder.set_serial_number(&serial_number)?; cert_builder.set_subject_name(&x509_name)?; cert_builder.set_issuer_name(&x509_name)?; let not_before = asn1::Asn1Time::days_from_now(0)?; cert_builder.set_not_before(¬_before)?; let not_after = asn1::Asn1Time::days_from_now(CA_VALID_DAYS)?; cert_builder.set_not_after(¬_after)?; cert_builder.append_extension(BasicConstraints::new().critical().ca().pathlen(0).build()?)?; cert_builder.append_extension( KeyUsage::new() .critical() .key_cert_sign() .crl_sign() .build()?, )?; let subject_key_identifier = SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?; cert_builder.append_extension(subject_key_identifier)?; cert_builder.set_pubkey(&ca_key)?; cert_builder.sign(&ca_key, get_signing_func())?; let ca_cert = cert_builder.build(); Ok(CaHandle { key: ca_key, cert: ca_cert, }) } pub(crate) fn load_ca( ca_key_ar: impl AsRef, ca_cert_ar: impl AsRef, ) -> Result { let ca_key_path: &Path = ca_key_ar.as_ref(); let ca_cert_path: &Path = ca_cert_ar.as_ref(); let mut ca_key_pem = vec![]; File::open(ca_key_path) .and_then(|mut file| file.read_to_end(&mut ca_key_pem)) .map_err(|e| { error!(err = ?e, "Failed to read {:?}", ca_key_path); })?; let mut ca_cert_pem = vec![]; File::open(ca_cert_path) .and_then(|mut file| file.read_to_end(&mut ca_cert_pem)) .map_err(|e| { error!(err = ?e, "Failed to read {:?}", ca_cert_path); })?; let ca_key = pkey::PKey::private_key_from_pem(&ca_key_pem).map_err(|e| { error!(err = ?e, "Failed to convert PEM to key"); })?; check_privkey_minimums(&ca_key).map_err(|err| { #[cfg(any(test, debug_assertions))] println!("{:?}", err); admin_error!("{}", err); })?; let ca_cert = X509::from_pem(&ca_cert_pem).map_err(|e| { error!(err = ?e, "Failed to convert PEM to cert"); })?; Ok(CaHandle { key: ca_key, cert: ca_cert, }) } pub(crate) struct CertHandle { key: pkey::PKey, cert: X509, chain: Vec, } pub(crate) fn write_cert( key_ar: impl AsRef, chain_ar: impl AsRef, cert_ar: impl AsRef, handle: &CertHandle, ) -> Result<(), ()> { let key_path: &Path = key_ar.as_ref(); let chain_path: &Path = chain_ar.as_ref(); let cert_path: &Path = cert_ar.as_ref(); let key_pem = handle.key.private_key_to_pem_pkcs8().map_err(|e| { error!(err = ?e, "Failed to convert key to PEM"); })?; let cert_pem = handle.cert.to_pem().map_err(|e| { error!(err = ?e, "Failed to convert cert to PEM"); })?; let mut chain_pem = cert_pem.clone(); // Build the chain PEM. for ca_cert in &handle.chain { match ca_cert.to_pem() { Ok(c) => { chain_pem.extend_from_slice(&c); } Err(e) => { error!(err = ?e, "Failed to convert cert to PEM"); return Err(()); } } } File::create(key_path) .and_then(|mut file| file.write_all(&key_pem)) .map_err(|e| { error!(err = ?e, "Failed to create {:?}", key_path); })?; File::create(chain_path) .and_then(|mut file| file.write_all(&chain_pem)) .map_err(|e| { error!(err = ?e, "Failed to create {:?}", chain_path); })?; File::create(cert_path) .and_then(|mut file| file.write_all(&cert_pem)) .map_err(|e| { error!(err = ?e, "Failed to create {:?}", cert_path); }) } pub(crate) fn build_cert( domain_name: &str, ca_handle: &CaHandle, key_type: Option, key_bits: Option, ) -> Result { let key_type = key_type.unwrap_or_default(); let int_key = gen_private_key(&key_type, key_bits)?; let mut req_builder = X509ReqBuilder::new()?; req_builder.set_pubkey(&int_key)?; let mut x509_name = X509NameBuilder::new()?; x509_name.append_entry_by_text("C", "AU")?; x509_name.append_entry_by_text("ST", "QLD")?; x509_name.append_entry_by_text("O", "Kanidm")?; x509_name.append_entry_by_text("CN", domain_name)?; // Requirement of packed attestation. x509_name.append_entry_by_text("OU", "Development and Evaluation - NOT FOR PRODUCTION")?; let x509_name = x509_name.build(); req_builder.set_subject_name(&x509_name)?; req_builder.sign(&int_key, get_signing_func())?; let req = req_builder.build(); // == let mut cert_builder = X509::builder()?; // Yes, 2 actually means 3 here ... cert_builder.set_version(2)?; let serial_number = bn::BigNum::from_u32(2).and_then(|serial| serial.to_asn1_integer())?; cert_builder.set_pubkey(&int_key)?; cert_builder.set_serial_number(&serial_number)?; cert_builder.set_subject_name(req.subject_name())?; cert_builder.set_issuer_name(ca_handle.cert.subject_name())?; let not_before = asn1::Asn1Time::days_from_now(0)?; cert_builder.set_not_before(¬_before)?; let not_after = asn1::Asn1Time::days_from_now(CERT_VALID_DAYS)?; cert_builder.set_not_after(¬_after)?; cert_builder.append_extension(BasicConstraints::new().build()?)?; cert_builder.append_extension( KeyUsage::new() .critical() .digital_signature() .key_encipherment() .build()?, )?; cert_builder.append_extension( ExtendedKeyUsage::new() // .critical() .server_auth() .build()?, )?; let subject_key_identifier = SubjectKeyIdentifier::new() .build(&cert_builder.x509v3_context(Some(&ca_handle.cert), None))?; cert_builder.append_extension(subject_key_identifier)?; let auth_key_identifier = AuthorityKeyIdentifier::new() .keyid(false) .issuer(false) .build(&cert_builder.x509v3_context(Some(&ca_handle.cert), None))?; cert_builder.append_extension(auth_key_identifier)?; let subject_alt_name = SubjectAlternativeName::new() .dns(domain_name) .build(&cert_builder.x509v3_context(Some(&ca_handle.cert), None))?; cert_builder.append_extension(subject_alt_name)?; cert_builder.sign(&ca_handle.key, get_signing_func())?; let int_cert = cert_builder.build(); Ok(CertHandle { key: int_key, cert: int_cert, chain: vec![ca_handle.cert.clone()], }) } #[test] // might as well test my logic fn test_enforced_minimums() { let good_ca_configs = vec![ // test rsa 4096 (ok) (KeyType::Rsa, 4096, false), // test rsa 2048 (ok) (KeyType::Rsa, 2048, false), // test ec 256 (ok) (KeyType::Ec, 256, false), ]; good_ca_configs.into_iter().for_each(|config| { dbg!(&config); assert!(CAConfig::new(config.0, config.1, config.2).is_ok()); }); let bad_ca_configs = vec![ // test rsa 1024 (no) (KeyType::Rsa, 1024, false), // test ec 128 (no) (KeyType::Ec, 128, false), ]; bad_ca_configs.into_iter().for_each(|config| { dbg!(&config); assert!(CAConfig::new(config.0, config.1, config.2).is_err()); }); } #[test] fn test_ca_loader() { let ca_key_tempfile = tempfile::NamedTempFile::new().unwrap(); let ca_cert_tempfile = tempfile::NamedTempFile::new().unwrap(); // let's test the defaults first let ca_config = CAConfig::default(); if let Ok(ca) = build_ca(Some(ca_config)) { write_ca(ca_key_tempfile.path(), ca_cert_tempfile.path(), &ca).unwrap(); assert!(load_ca(ca_key_tempfile.path(), ca_cert_tempfile.path()).is_ok()); }; let good_ca_configs = vec![ // test rsa 4096 (ok) (KeyType::Rsa, 4096, false), // test rsa 2048 (ok) (KeyType::Rsa, 2048, false), // test ec 256 (ok) (KeyType::Ec, 256, false), ]; good_ca_configs.into_iter().for_each(|config| { println!("testing good config {:?}", config); let ca_config = CAConfig::new(config.0, config.1, config.2).unwrap(); let ca = build_ca(Some(ca_config)).unwrap(); write_ca(ca_key_tempfile.path(), ca_cert_tempfile.path(), &ca).unwrap(); let ca_result = load_ca(ca_key_tempfile.path(), ca_cert_tempfile.path()); println!("result: {:?}", ca_result); assert!(ca_result.is_ok()); }); let bad_ca_configs = vec![ // test rsa 1024 (bad) (KeyType::Rsa, 1024, true), ]; bad_ca_configs.into_iter().for_each(|config| { println!( "\ntesting bad config keytype: {:?} key size: {}, skip_enforce_minimums: {}", config.0, config.1, config.2 ); let ca_config = CAConfig::new(config.0, config.1, config.2).unwrap(); let ca = build_ca(Some(ca_config)).unwrap(); write_ca(ca_key_tempfile.path(), ca_cert_tempfile.path(), &ca).unwrap(); let ca_result = load_ca(ca_key_tempfile.path(), ca_cert_tempfile.path()); println!("result: {:?}", ca_result); assert!(ca_result.is_err()); }); }