diff --git a/Cargo.lock b/Cargo.lock index b708ca36..6565061f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,6 +2185,7 @@ dependencies = [ "log", "num-bigint", "once_cell", + "rcgen", "ring", "rustls-pemfile 2.1.2", "rustls-pki-types", @@ -2192,6 +2193,7 @@ dependencies = [ "rustversion", "subtle", "tikv-jemallocator", + "time", "webpki-roots 0.26.1", "zeroize", ] diff --git a/ci-bench/Cargo.toml b/ci-bench/Cargo.toml index 2d8369b4..f51b462d 100644 --- a/ci-bench/Cargo.toml +++ b/ci-bench/Cargo.toml @@ -13,7 +13,7 @@ byteorder = "1.4.3" clap = { version = "4.3.21", features = ["derive"] } fxhash = "0.2.1" itertools = "0.12" -pki-types = { package = "rustls-pki-types", version = "1" } +pki-types = { package = "rustls-pki-types", version = "1.4.1" } rayon = "1.7.0" rustls = { path = "../rustls", features = ["ring", "aws_lc_rs"] } rustls-pemfile = "2" diff --git a/rustls/Cargo.toml b/rustls/Cargo.toml index 5e192c55..8cff9390 100644 --- a/rustls/Cargo.toml +++ b/rustls/Cargo.toml @@ -44,7 +44,9 @@ bencher = "0.1.5" env_logger = "0.10" # 0.11 requires 1.71 MSRV even as a dev-dep (due to manifest features) log = "0.4.4" num-bigint = "0.4.4" +rcgen = { version = "0.13", default-features = false, features = ["aws_lc_rs", "pem"] } rustls-pemfile = "2" +time = { version = "0.3.6", default-features = false } webpki-roots = "0.26" [target.'cfg(not(target_env = "msvc"))'.dev-dependencies] @@ -65,6 +67,10 @@ path = "benches/benchmarks.rs" harness = false required-features = ["ring"] +[[example]] +name = "test_ca" +path = "examples/internal/test_ca.rs" + [package.metadata.docs.rs] # all non-default features except fips (cannot build on docs.rs environment) features = ["read_buf", "ring"] diff --git a/rustls/examples/internal/test_ca.rs b/rustls/examples/internal/test_ca.rs new file mode 100644 index 00000000..52e62b8d --- /dev/null +++ b/rustls/examples/internal/test_ca.rs @@ -0,0 +1,321 @@ +use std::collections::HashMap; +use std::env; +use std::fs::{self, File}; +use std::io::Write; +use std::net::IpAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; + +use rcgen::{ + BasicConstraints, CertificateParams, CertificateRevocationListParams, CertifiedKey, + DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, IsCa, KeyIdMethod, KeyPair, + KeyUsagePurpose, RevocationReason, RevokedCertParams, RsaKeySize, SanType, SerialNumber, + SignatureAlgorithm, PKCS_ECDSA_P256_SHA256, PKCS_ECDSA_P384_SHA384, PKCS_ECDSA_P521_SHA512, + PKCS_ED25519, PKCS_RSA_SHA256, PKCS_RSA_SHA384, PKCS_RSA_SHA512, +}; +use time::OffsetDateTime; + +fn main() -> Result<(), Box> { + let mut certified_keys = HashMap::with_capacity(ROLES.len() * SIG_ALGS.len()); + for role in ROLES { + for alg in SIG_ALGS { + // Generate a key pair and serialize it to a PEM encoded file. + let key_pair = alg.key_pair(); + let mut key_pair_file = File::create(role.key_file_path(alg))?; + key_pair_file.write_all(key_pair.serialize_pem().as_bytes())?; + + // Issue a certificate for the key pair. For trust anchors, this will be self-signed. + // Otherwise we dig out the issuer and issuer_key for the issuer, which should have + // been produced in earlier iterations based on the careful ordering of roles. + let cert = match role { + Role::TrustAnchor => role + .params(alg) + .self_signed(&key_pair)?, + Role::Intermediate => { + let issuer: &CertifiedKey = certified_keys + .get(&(Role::TrustAnchor, alg.inner)) + .unwrap(); + role.params(alg) + .signed_by(&key_pair, &issuer.cert, &issuer.key_pair)? + } + Role::EndEntity | Role::Client => { + let issuer = certified_keys + .get(&(Role::Intermediate, alg.inner)) + .unwrap(); + role.params(alg) + .signed_by(&key_pair, &issuer.cert, &issuer.key_pair)? + } + }; + + // Serialize the issued certificate to a PEM encoded file. + let mut cert_file = File::create(role.cert_pem_file_path(alg))?; + cert_file.write_all(cert.pem().as_bytes())?; + // And to a DER encoded file. + let mut cert_file = File::create(role.cert_der_file_path(alg))?; + cert_file.write_all(cert.der())?; + + // If we're not a trust anchor, generate a CRL for the certificate we just issued. + if role != Role::TrustAnchor { + // The CRL will be signed by the issuer of the certificate being revoked. For + // intermediates this will be the trust anchor, and for client/EE certs this will + // be the intermediate. + let issuer = match role { + Role::Intermediate => certified_keys + .get(&(Role::TrustAnchor, alg.inner)) + .unwrap(), + Role::EndEntity | Role::Client => certified_keys + .get(&(Role::Intermediate, alg.inner)) + .unwrap(), + _ => panic!("unexpected role for CRL generation: {role:?}"), + }; + let crl = crl_for_serial( + cert.params() + .serial_number + .clone() + .unwrap(), + ) + .signed_by(&issuer.cert, &issuer.key_pair)?; + let mut crl_file = File::create( + alg.output_directory() + .join(format!("{}.revoked.crl.pem", role.label())), + )?; + crl_file.write_all(crl.pem().unwrap().as_bytes())?; + } + + // When we're issuing end entity or client certs we have a bit of extra work to do + // now that we have full chains in hand. + if matches!(role, Role::EndEntity | Role::Client) { + let root = &certified_keys + .get(&(Role::TrustAnchor, alg.inner)) + .unwrap() + .cert; + let intermediate = &certified_keys + .get(&(Role::Intermediate, alg.inner)) + .unwrap() + .cert; + + // Write the PEM chain and full chain files for the end entity and client certs. + // Chain files include the intermediate and root certs, while full chain files include + // the end entity or client cert as well. + for f in [ + ("chain", &[intermediate, root][..]), + ("fullchain", &[&cert, intermediate, root][..]), + ] { + let mut chain_file = File::create(alg.output_directory().join(format!( + "{}.{}", + role.label(), + f.0 + )))?; + for cert in f.1 { + chain_file.write_all(cert.pem().as_bytes())?; + } + } + } + + certified_keys.insert((role, alg.inner), CertifiedKey { cert, key_pair }); + } + } + + Ok(()) +} + +fn crl_for_serial(serial_number: SerialNumber) -> CertificateRevocationListParams { + let now = OffsetDateTime::now_utc(); + CertificateRevocationListParams { + this_update: now, + next_update: now + Duration::from_secs(60 * 60 * 24 * 5), + crl_number: SerialNumber::from(1234), + issuing_distribution_point: None, + revoked_certs: vec![RevokedCertParams { + serial_number, + revocation_time: now, + reason_code: Some(RevocationReason::KeyCompromise), + invalidity_date: None, + }], + key_identifier_method: KeyIdMethod::Sha256, + } +} + +// Note: these are ordered such that the data dependencies for issuance are satisfied. +const ROLES: [Role; 4] = [ + Role::TrustAnchor, + Role::Intermediate, + Role::EndEntity, + Role::Client, +]; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum Role { + Client, + EndEntity, + Intermediate, + TrustAnchor, +} + +impl Role { + fn params(&self, alg: &'static SigAlgContext) -> CertificateParams { + let mut params = CertificateParams::default(); + params.distinguished_name = self.common_name(alg); + params.use_authority_key_identifier_extension = true; + let serial = SERIAL_NUMBER.fetch_add(1, Ordering::SeqCst); + params.serial_number = Some(SerialNumber::from_slice(&serial.to_be_bytes()[..])); + + match self { + Self::TrustAnchor | Self::Intermediate => { + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.key_usages = ISSUER_KEY_USAGES.to_vec(); + params.extended_key_usages = ISSUER_EXTENDED_KEY_USAGES.to_vec(); + } + Self::EndEntity | Self::Client => { + params.is_ca = IsCa::NoCa; + params.key_usages = EE_KEY_USAGES.to_vec(); + params.subject_alt_names = vec![ + SanType::DnsName(Ia5String::try_from("testserver.com".to_string()).unwrap()), + SanType::DnsName( + Ia5String::try_from("second.testserver.com".to_string()).unwrap(), + ), + SanType::DnsName(Ia5String::try_from("localhost".to_string()).unwrap()), + SanType::IpAddress(IpAddr::from_str("198.51.100.1").unwrap()), + SanType::IpAddress(IpAddr::from_str("2001:db8::1").unwrap()), + ]; + } + } + + // Client certificates additionally get the client auth EKU. + if *self == Self::Client { + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + } + + params + } + + fn common_name(&self, alg: &'static SigAlgContext) -> DistinguishedName { + let mut distinguished_name = DistinguishedName::new(); + distinguished_name.push( + DnType::CommonName, + match self { + Self::Client => "ponytown client".to_owned(), + Self::EndEntity => "testserver.com".to_owned(), + Self::Intermediate => { + format!("ponytown {} level 2 intermediate", alg.issuer_cn) + } + Self::TrustAnchor => format!("ponytown {} CA", alg.issuer_cn), + }, + ); + distinguished_name + } + + fn key_file_path(&self, alg: &'static SigAlgContext) -> PathBuf { + alg.output_directory() + .join(format!("{}.key", self.label())) + } + + fn cert_pem_file_path(&self, alg: &'static SigAlgContext) -> PathBuf { + alg.output_directory() + .join(format!("{}.cert", self.label())) + } + + fn cert_der_file_path(&self, alg: &'static SigAlgContext) -> PathBuf { + alg.output_directory() + .join(format!("{}.der", self.label())) + } + + fn label(&self) -> &'static str { + match self { + Self::Client => "client", + Self::EndEntity => "end", + Self::Intermediate => "inter", + Self::TrustAnchor => "ca", + } + } +} + +// Note: for convenience we use the RSA sigalg digest algorithm to inform the RSA modulus +// size, mapping SHA256 to RSA 2048, SHA384 to RSA 3072, and SHA512 to RSA 4096. +static SIG_ALGS: &[SigAlgContext] = &[ + SigAlgContext { + inner: &PKCS_RSA_SHA256, + issuer_cn: "RSA 2048", + }, + SigAlgContext { + inner: &PKCS_RSA_SHA384, + issuer_cn: "RSA 3072", + }, + SigAlgContext { + inner: &PKCS_RSA_SHA512, + issuer_cn: "RSA 4096", + }, + SigAlgContext { + inner: &PKCS_ECDSA_P256_SHA256, + issuer_cn: "ECDSA p256", + }, + SigAlgContext { + inner: &PKCS_ECDSA_P384_SHA384, + issuer_cn: "ECDSA p384", + }, + SigAlgContext { + inner: &PKCS_ECDSA_P521_SHA512, + issuer_cn: "ECDSA p521", + }, + SigAlgContext { + inner: &PKCS_ED25519, + issuer_cn: "EdDSA", + }, +]; + +struct SigAlgContext { + pub(crate) inner: &'static SignatureAlgorithm, + pub(crate) issuer_cn: &'static str, +} + +impl SigAlgContext { + fn output_directory(&self) -> PathBuf { + let output_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("../") + .join("test-ca") + .join( + self.issuer_cn + .to_lowercase() + .replace(' ', "-"), + ); + fs::create_dir_all(&output_dir).unwrap(); + output_dir + } + + fn key_pair(&self) -> KeyPair { + if *self.inner == PKCS_RSA_SHA256 { + KeyPair::generate_rsa_for(&PKCS_RSA_SHA256, RsaKeySize::_2048) + } else if *self.inner == PKCS_RSA_SHA384 { + KeyPair::generate_rsa_for(&PKCS_RSA_SHA384, RsaKeySize::_3072) + } else if *self.inner == PKCS_RSA_SHA512 { + KeyPair::generate_rsa_for(&PKCS_RSA_SHA512, RsaKeySize::_4096) + } else { + KeyPair::generate_for(self.inner) + } + .unwrap() + } +} + +const ISSUER_KEY_USAGES: &[KeyUsagePurpose; 7] = &[ + KeyUsagePurpose::CrlSign, + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::ContentCommitment, + KeyUsagePurpose::KeyEncipherment, + KeyUsagePurpose::DataEncipherment, + KeyUsagePurpose::KeyAgreement, +]; + +const ISSUER_EXTENDED_KEY_USAGES: &[ExtendedKeyUsagePurpose; 2] = &[ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, +]; + +const EE_KEY_USAGES: &[KeyUsagePurpose; 2] = &[ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::ContentCommitment, +]; + +static SERIAL_NUMBER: AtomicU64 = AtomicU64::new(1);