diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bc79e2..5618914a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changes +## Unreleased + +- Add [example](./examples/auto-gen-ca-and-server-tls.rs) that creates a new CA and new server certificate signed by it. + Contributed by [iamjpotts](https://github.com/iamjpotts). + ## Release 0.11.1 - June 17, 2023 - Make botan a dev-dependency again. Contributed by [mbrubeck](https://github.com/mbrubeck). diff --git a/Cargo.lock b/Cargo.lock index ea74012d..26d7a7fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "botan" version = "0.10.6" @@ -122,6 +128,41 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -184,6 +225,33 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "foreign-types" version = "0.3.2" @@ -256,6 +324,12 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + [[package]] name = "log" version = "0.4.20" @@ -274,6 +348,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -364,7 +456,7 @@ version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -384,6 +476,12 @@ dependencies = [ "syn 2.0.29", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "openssl-sys" version = "0.9.91" @@ -415,6 +513,16 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pipe" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7b8f27da217eb966df4c58d4159ea939431950ca03cf782c22bd7c5c1d8d75" +dependencies = [ + "crossbeam-channel", + "readwrite", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -501,8 +609,10 @@ name = "rcgen" version = "0.11.1" dependencies = [ "botan", + "native-tls", "openssl", "pem", + "pipe", "rand", "ring", "rsa", @@ -513,6 +623,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "readwrite" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73891b98dabbe836d23a094941e6ec891bc4880e771faea98813f2ff27ede473" + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "ring" version = "0.16.20" @@ -559,6 +684,19 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustls-webpki" version = "0.101.4" @@ -569,6 +707,38 @@ dependencies = [ "untrusted", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.186" @@ -661,6 +831,19 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.47" @@ -836,6 +1019,72 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "x509-parser" version = "0.15.1" diff --git a/Cargo.toml b/Cargo.toml index bfa4f23a..72231bb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,9 @@ default = ["pem"] features = ["x509-parser"] [dev-dependencies] +native-tls = "0.2" openssl = "0.10" +pipe = { version = "0.4", features = ["bidirectional"] } x509-parser = { version = "0.15", features = ["verify"] } rustls-webpki = { version = "0.101.0", features = ["std"] } rand = "0.8" @@ -48,3 +50,7 @@ botan = { version = "0.10", features = ["vendored"] } # ignores profile overrides for non leaf packages) [profile.dev.package.num-bigint-dig] opt-level = 3 + +[[example]] +name = "auto-gen-ca-and-server-tls" +required-features = ["x509-parser"] diff --git a/examples/auto-gen-ca-and-server-tls.rs b/examples/auto-gen-ca-and-server-tls.rs new file mode 100644 index 00000000..700f4235 --- /dev/null +++ b/examples/auto-gen-ca-and-server-tls.rs @@ -0,0 +1,155 @@ +//! Auto-generate a CA certificate and a server certificate signed +//! by that CA, and start a TLS server and client using them. +//! +//! Run with: +//! +//! $ cargo run --example auto-gen-ca-and-server-tls --features=x509-parser +//! +//! This doesn't start a network service (not even on localhost). +//! Instead, it creates an in-memory TLS server and an in-memory +//! TLS client in two separate threads in the same process. + +use std::error::Error; +use std::io; +use std::thread; + +use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsConnector, TlsStream}; +use rcgen::{BasicConstraints, CertificateSigningRequest, DnType, IsCa, SanType, KeyUsagePurpose, DistinguishedName, CertificateParams, Certificate, RcgenError}; + +const SAN: &str = "example-server"; + +fn main() -> Result<(), Box> { + let ca = gen_cert_for_ca()?; + + println!("CA private key:\n{}", ca.serialize_private_key_pem()); + println!(); + println!("CA certificate:\n{}", ca.serialize_pem()?); + + let host_cert = gen_cert_for_server(&ca)?; + + println!("Preparing to verify with tls"); + + let leaf_then_ca = format!("{}{}", host_cert.signed_certificate_pem, ca.serialize_pem()?); + + println!(); + println!("Server private key:\n{}", host_cert.private_key_pem); + println!("Server chain:\n{}\n", leaf_then_ca); + + // For use by server + let server_id = Identity::from_pkcs8( + leaf_then_ca.as_bytes(), + host_cert.private_key_pem.as_bytes() + )?; + + // For use by client + let native_ca_cert = native_tls::Certificate::from_pem(ca.serialize_pem()?.as_bytes())?; + + let (p_client, p_server) = pipe::bipipe(); + + let t_client = thread::spawn(move || -> Result<(), String> { + println!("Client creating"); + + let test_client = TlsConnector::builder() + .add_root_certificate(native_ca_cert) + .build() + .map_err(|e| e.to_string())?; + + println!("Client connecting"); + + let _stream = map_tls_io_error(test_client.connect(SAN, p_client))?; + + println!("Client connected"); + Ok(()) + }); + + let t_server = thread::spawn(move || -> Result<(), String> { + println!("Server creating"); + + let test_server = TlsAcceptor::new(server_id) + .map_err(|e| e.to_string())?; + + println!("Server accepting"); + + let _server_tls_stream = map_tls_io_error(test_server.accept(p_server))?; + + println!("Server accepted"); + Ok(()) + }); + + println!("Joining client"); + t_client.join() + .map_err(|e| format!("{:?}", e))??; + + println!("Joining server"); + t_server.join() + .map_err(|e| format!("{:?}", e))??; + + println!(); + println!("Succeeded."); + + Ok(()) +} + +struct ServerCertificate { + private_key_pem: String, + + // Server certificate only; does not include complete certificate chain. + signed_certificate_pem: String, +} + +fn gen_cert_for_ca() -> Result { + let mut dn = DistinguishedName::new(); + dn.push(DnType::CountryName, "USA"); + dn.push(DnType::CommonName, "Auto-Generated CA"); + + let mut params = CertificateParams::default(); + + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; + params.distinguished_name = dn; + params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; + + Certificate::from_params(params) +} + +fn gen_cert_for_server(ca: &Certificate) -> Result { + let mut dn = DistinguishedName::new(); + dn.push(DnType::CountryName, "USA"); + dn.push(DnType::CommonName, "Auto-Generated Server"); + + let mut params = CertificateParams::default(); + + params.is_ca = IsCa::NoCa; + params.alg = &rcgen::PKCS_ECDSA_P256_SHA256; + params.distinguished_name = dn; + params.subject_alt_names = vec![SanType::DnsName(SAN.into())]; + + let unsigned = Certificate::from_params(params)?; + + let request_pem = unsigned.serialize_request_pem()?; + + let csr = CertificateSigningRequest::from_pem(&request_pem)?; + + let signed_pem = csr.serialize_pem_with_signer(&ca)?; + + Ok(ServerCertificate { + private_key_pem: unsigned.serialize_private_key_pem(), + signed_certificate_pem: signed_pem + }) +} + +fn map_tls_io_error(tls_result: Result, HandshakeError>) -> Result, String> +where + S: io::Read + io::Write +{ + match tls_result { + Ok(stream) => Ok(stream), + Err(he) => { + match he { + HandshakeError::Failure(e) => Err(format!("{}", e)), + // Can't directly unwrap because TlsStream doesn't implement Debug trait + HandshakeError::WouldBlock(_) => Err("Would block".into()) + } + } + } +}