diff --git a/CHANGELOG.md b/CHANGELOG.md index 05dedee6..91cf20e6 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.10.0 - September 29, 2022 - Update x509-parser to 0.14. diff --git a/Cargo.lock b/Cargo.lock index f923b6e3..f181d1c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,41 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-bigint" version = "0.3.2" @@ -198,6 +233,15 @@ dependencies = [ "syn", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -234,6 +278,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.1" @@ -260,9 +313,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.116" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libm" @@ -291,6 +344,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.0" @@ -399,6 +470,12 @@ dependencies = [ "openssl-sys", ] +[[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.72" @@ -430,6 +507,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.3.3" @@ -527,8 +614,10 @@ name = "rcgen" version = "0.10.0" dependencies = [ "botan", + "native-tls", "openssl", "pem", + "pipe", "rand", "ring", "rsa", @@ -539,6 +628,30 @@ 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.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -583,6 +696,39 @@ dependencies = [ "nom", ] +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "smallvec" version = "1.8.0" @@ -634,6 +780,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.30" @@ -804,6 +964,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "x509-parser" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index a376d35e..31c0ef8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,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.14", features = ["verify"] } webpki = { version = "0.22", features = ["std"] } botan = { version = "0.8", features = ["vendored"] } @@ -46,3 +48,7 @@ rsa = "0.6" # 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..b41851ab --- /dev/null +++ b/examples/auto-gen-ca-and-server-tls.rs @@ -0,0 +1,166 @@ +//! 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. +//! +//! This example has the same author as a similar test in +//! https://github.com/iamjpotts/demo_docker_registry_auto_gen_tls/blob/31c85aa06a87a8dbebc457af9b5998038e2daaa2/tests/test_utils/cert_gen.rs +//! which does not have a license. +//! +//! This example auto-gen-ca-and-server-tls is contributed by +//! the same author into rcgen according to the rcgen license: +//! +//! "MIT or Apache License 2.0, at your option." +//! + +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, ExtendedKeyUsagePurpose, 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())]; + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; + + 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()) + } + } + } +}