diff --git a/Cargo.lock b/Cargo.lock index 349af047..48ba578d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,9 +281,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" dependencies = [ "jobserver", "libc", @@ -688,9 +688,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastly-shared" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8c7df017449664ec1ee065dac781b5d4ab405e44533c2483cfb31b3f1c6124" +checksum = "c0aab3b493a65563fc700c3dac7d26be3f5cb1c095b4ff9485e141a29ae88fe6" dependencies = [ "bitflags 1.3.2", "http", @@ -710,7 +710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b0377f1edc77dbd1118507bc7a66e4ab64d2b90c66f90726dc801e73a8c68f9" dependencies = [ "cfg-if", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -756,7 +756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d167b646a876ba8fda6b50ac645cfd96242553cbaf0ca4fccaa39afcbf0801f" dependencies = [ "io-lifetimes 1.0.11", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -1114,7 +1114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -1433,18 +1433,18 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", @@ -1453,9 +1453,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -1625,13 +1625,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -1646,9 +1646,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -1712,9 +1712,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.6" +version = "0.38.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" dependencies = [ "bitflags 2.3.3", "errno", @@ -1852,18 +1852,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", @@ -2007,7 +2007,7 @@ dependencies = [ "cap-std", "fd-lock", "io-lifetimes 2.0.2", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", "winx 0.36.1", ] @@ -2020,14 +2020,14 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -2085,6 +2085,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls-listener" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81294c017957a1a69794f506723519255879e15a870507faf45dfed288b763dd" +dependencies = [ + "futures-util", + "hyper", + "pin-project-lite", + "thiserror", + "tokio", + "tokio-rustls", +] + [[package]] name = "tokio" version = "1.29.1" @@ -2328,13 +2342,18 @@ name = "viceroy" version = "0.6.2" dependencies = [ "anyhow", + "base64", "clap", "futures", "hyper", "itertools", "libc", + "rustls", + "rustls-pemfile", "serde_json", + "tls-listener", "tokio", + "tokio-rustls", "tracing", "tracing-futures", "tracing-subscriber", @@ -2365,6 +2384,7 @@ dependencies = [ "regex", "rustls", "rustls-native-certs", + "rustls-pemfile", "semver 0.10.0", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 9d99e006..604c77c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,14 @@ opt-level = 1 [workspace.dependencies] anyhow = "1.0.31" +base64 = "0.21.2" hyper = { version = "=0.14.26", features = ["full"] } itertools = "0.10.5" +rustls = { version = "0.21.5", features = ["dangerous_configuration"] } +rustls-pemfile = "1.0.3" serde_json = "1.0.59" tokio = { version = "1.21.2", features = ["full"] } +tokio-rustls = "0.24.1" tracing = "0.1.37" tracing-futures = "0.2.5" futures = "0.3.24" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6fd93bb2..65c1eba8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -31,11 +31,16 @@ path = "src/main.rs" [dependencies] anyhow = { workspace = true } +base64 = { workspace = true } hyper = { workspace = true } itertools = { workspace = true } serde_json = { workspace = true } clap = { version = "^4.0.18", features = ["derive"] } +rustls = { workspace = true } +rustls-pemfile = { workspace = true } +tls-listener = { version = "^0.7.0", features = ["rustls", "hyper-h1", "tokio-net", "rt"] } tokio = { workspace = true } +tokio-rustls = { workspace = true } tracing = { workspace = true } tracing-futures = { workspace = true } tracing-subscriber = { version = "^0.3.16", features = ["env-filter", "fmt"] } diff --git a/cli/tests/integration/client_certs.rs b/cli/tests/integration/client_certs.rs new file mode 100644 index 00000000..8704f8eb --- /dev/null +++ b/cli/tests/integration/client_certs.rs @@ -0,0 +1,184 @@ +use crate::common::{Test, TestResult}; +use base64::engine::{general_purpose, Engine}; +use hyper::http::response; +use hyper::server::conn::AddrIncoming; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Request, Server, StatusCode}; +use rustls::server::{AllowAnyAuthenticatedClient, ServerConfig}; +use rustls::{Certificate, PrivateKey, RootCertStore}; +use std::io::Cursor; +use std::net::SocketAddr; +use std::sync::Arc; +use tls_listener::TlsListener; +use tokio_rustls::server::TlsStream; +use tokio_rustls::TlsAcceptor; + +// So, let's say you want to regenerate some of the keys used in these tests, +// because they've expired or you want to try different algorithms. To do so, +// you need to: +// +// Create a key for the certificate authority: +// > openssl genrsa -des3 -out ca.key 2048 +// You must set a passphrase for this key. In this case, I chose "Viceroy" +// +// Now we create a root certificate, or a CA certificate that we can use as +// our "known good" authority. This one will last for 10 years. You'll get +// asked a bunch of questions that don't actually matter: +// > openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem +// +// Now we create a server certificate key: +// > openssl genrsa -out server.key 2048 +// Then create a certificate signing request (CSR), which is an annoying middle +// step: +// > openssl req -new -key server.key -out server.csr +// Which will also ask you a bunch of questions that don't much matter. At this +// point, it's important to know what you're going to use it for. In this case, +// we want a server to run on localhost. Must TLS/HTTPS things get very picky +// about what certificates they'll accept for a server, so we need to mark the +// certificate appropriately; in this case, as being associated with localhost. +// So we'll create an extension file called 'server.ext' that contains: +// +// authorityKeyIdentifier=keyid,issuer +// basicConstraints=CA:FALSE +// keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +// subjectAltName = @alt_names +// [alt_names] +// DNS.1 = localhost +// IP.1 = 127.0.0.1 +// +// Now we can create a signed certificate to go with our server key, by running: +// > openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \ +// -out server.crt -days 3650 -sha256 -extfile server.ext +// +// Repeat this for as many keys as you need. In the case of these tests, we need +// another one for the client. + +// NOTE(ACW): This test setup is much more complicated than it feels like it should +// be, but this is the only consistent way I can build a server that requires and +// passes back TLS client certificates. + +struct Watcher { + inner: AllowAnyAuthenticatedClient, +} + +impl rustls::server::ClientCertVerifier for Watcher { + fn client_auth_root_subjects(&self) -> &[rustls::DistinguishedName] { + tracing::warn!("client_auth_root_subjects"); + self.inner.client_auth_root_subjects() + } + + fn verify_client_cert( + &self, + end_entity: &Certificate, + intermediates: &[Certificate], + now: std::time::SystemTime, + ) -> Result { + tracing::warn!("varify_client_cert"); + self.inner + .verify_client_cert(end_entity, intermediates, now) + } +} + +fn build_server_tls_config() -> ServerConfig { + let mut roots = RootCertStore::empty(); + let ca_cert_bytes = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../test-fixtures/data/ca.pem" + )); + + let mut ca_cursor = Cursor::new(ca_cert_bytes); + let mut root_certs = rustls_pemfile::certs(&mut ca_cursor).expect("pem ca certs"); + for cert in root_certs.drain(..) { + roots.add(&Certificate(cert)).expect("can add root certs"); + } + + let server_cert_bytes: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../test-fixtures/data/server.crt" + )); + let mut server_cursor = Cursor::new(server_cert_bytes); + let server_cert_list: Vec = rustls_pemfile::certs(&mut server_cursor) + .expect("can read server cert") + .into_iter() + .map(Certificate) + .collect(); + + let server_key_bytes: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../test-fixtures/data/server.key" + )); + let mut key_cursor = Cursor::new(server_key_bytes); + let server_key = rustls_pemfile::rsa_private_keys(&mut key_cursor) + .expect("have a key") + .into_iter() + .map(PrivateKey) + .next() + .expect("have one key"); + + ServerConfig::builder() + .with_safe_default_cipher_suites() + .with_safe_default_kx_groups() + .with_safe_default_protocol_versions() + .expect("basic tls server config") + .with_client_cert_verifier(Arc::new(Watcher { + inner: AllowAnyAuthenticatedClient::new(roots), + })) + .with_single_cert(server_cert_list, server_key) + .expect("valid server cert") +} + +#[tokio::test(flavor = "multi_thread")] +async fn client_certs_work() -> TestResult { + // Set up the test harness + let test = Test::using_fixture("mutual-tls.wasm"); + let server_addr: SocketAddr = "127.0.0.1:0".parse().expect("localhost parses"); + let incoming = AddrIncoming::bind(&server_addr).expect("bind"); + let bound_port = incoming.local_addr().port(); + + let acceptor = TlsAcceptor::from(Arc::new(build_server_tls_config())); + let listener = TlsListener::new_hyper(acceptor, incoming); + + let service = make_service_fn(|stream: &TlsStream<_>| { + let (_, server_connection) = stream.get_ref(); + let peer_certs = server_connection.peer_certificates().map(|x| x.to_vec()); + async move { + Ok::<_, std::io::Error>(service_fn(move |_req| { + let peer_certs = peer_certs.clone(); + + async { + match peer_certs { + None => response::Builder::new() + .status(401) + .body("could not identify client certificate".to_string()), + Some(vec) if vec.len() != 1 => response::Builder::new() + .status(406) + .body(format!("can only handle 1 cert, got {}", vec.len())), + Some(mut cert_vec) => { + let Certificate(cert) = cert_vec.remove(0); + let base64_cert = general_purpose::STANDARD.encode(cert); + response::Builder::new().status(200).body(base64_cert) + } + } + } + })) + } + }); + let server = Server::builder(listener).serve(service); + tokio::spawn(server); + + let resp = test + .against( + Request::post("/") + .header("port", bound_port) + .body("Hello, Viceroy!") + .unwrap(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.into_body().read_into_string().await?, + "Hello, Viceroy!" + ); + + Ok(()) +} diff --git a/cli/tests/integration/common/backends.rs b/cli/tests/integration/common/backends.rs index f539c3f2..3c086120 100644 --- a/cli/tests/integration/common/backends.rs +++ b/cli/tests/integration/common/backends.rs @@ -73,6 +73,7 @@ impl TestBackends { override_host: backend.override_host.clone(), cert_host: backend.cert_host.clone(), use_sni: backend.use_sni, + client_cert: None, }; backends.insert(name.to_string(), Arc::new(backend_config)); } @@ -150,7 +151,7 @@ impl TestBackends { !inner.servers_are_running, "cannot start TestBackend servers more than once" ); - for (name, mut backend) in inner.map.iter_mut() { + for (name, backend) in inner.map.iter_mut() { let Some(service) = backend.test_service.as_ref() else { panic!("no service defined for backend {name}"); }; diff --git a/cli/tests/integration/main.rs b/cli/tests/integration/main.rs index 3b18ff9b..7405db83 100644 --- a/cli/tests/integration/main.rs +++ b/cli/tests/integration/main.rs @@ -1,5 +1,6 @@ mod async_io; mod body; +mod client_certs; mod common; mod dictionary_lookup; mod downstream_req; diff --git a/cli/tests/trap-test/Cargo.lock b/cli/tests/trap-test/Cargo.lock index 9baaa000..47fe388e 100644 --- a/cli/tests/trap-test/Cargo.lock +++ b/cli/tests/trap-test/Cargo.lock @@ -256,9 +256,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" dependencies = [ "jobserver", "libc", @@ -628,9 +628,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastly-shared" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8c7df017449664ec1ee065dac781b5d4ab405e44533c2483cfb31b3f1c6124" +checksum = "c0aab3b493a65563fc700c3dac7d26be3f5cb1c095b4ff9485e141a29ae88fe6" dependencies = [ "bitflags 1.3.2", "http", @@ -644,7 +644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b0377f1edc77dbd1118507bc7a66e4ab64d2b90c66f90726dc801e73a8c68f9" dependencies = [ "cfg-if", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -690,7 +690,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d167b646a876ba8fda6b50ac645cfd96242553cbaf0ca4fccaa39afcbf0801f" dependencies = [ "io-lifetimes 1.0.11", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -1071,7 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", ] @@ -1383,18 +1383,18 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", @@ -1403,9 +1403,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -1575,13 +1575,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -1596,9 +1596,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -1662,9 +1662,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.6" +version = "0.38.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee020b1716f0a80e2ace9b03441a749e402e86712f15f16fe8a8f75afac732f" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" dependencies = [ "bitflags 2.3.3", "errno", @@ -1802,18 +1802,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", @@ -1951,7 +1951,7 @@ dependencies = [ "cap-std", "fd-lock", "io-lifetimes 2.0.2", - "rustix 0.38.6", + "rustix 0.38.7", "windows-sys", "winx 0.36.1", ] @@ -2295,6 +2295,7 @@ dependencies = [ "regex", "rustls", "rustls-native-certs", + "rustls-pemfile", "semver 0.10.0", "serde", "serde_derive", diff --git a/lib/Cargo.toml b/lib/Cargo.toml index f44351d9..98c04ed6 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -39,6 +39,7 @@ lazy_static = "^1.4.0" regex = "^1.3.9" rustls = "^0.21.1" rustls-native-certs = "^0.6.3" +rustls-pemfile = "^1.0.3" semver = "^0.10.0" serde = "^1.0.145" serde_derive = "^1.0.114" diff --git a/lib/compute-at-edge-abi/typenames.witx b/lib/compute-at-edge-abi/typenames.witx index 3b8524b5..71a53f13 100644 --- a/lib/compute-at-edge-abi/typenames.witx +++ b/lib/compute-at-edge-abi/typenames.witx @@ -182,7 +182,9 @@ $ca_cert $ciphers $sni_hostname - $dont_pool)) + $dont_pool + $client_cert + )) (typename $dynamic_backend_config (record @@ -201,6 +203,9 @@ (field $ciphers_len u32) (field $sni_hostname (@witx pointer (@witx char8))) (field $sni_hostname_len u32) + (field $client_certificate (@witx pointer (@witx char8))) + (field $client_certificate_len u32) + (field $client_key $secret_handle) )) ;;; TLS client certificate verified result from downstream. diff --git a/lib/src/config.rs b/lib/src/config.rs index 25ec92bd..82a8cc38 100644 --- a/lib/src/config.rs +++ b/lib/src/config.rs @@ -29,7 +29,7 @@ pub type Dictionaries = HashMap; /// Types and deserializers for backend configuration settings. mod backends; -pub use self::backends::Backend; +pub use self::backends::{Backend, ClientCertError, ClientCertInfo}; pub type Backends = HashMap>; diff --git a/lib/src/config/backends.rs b/lib/src/config/backends.rs index a8c13ed8..f52bb156 100644 --- a/lib/src/config/backends.rs +++ b/lib/src/config/backends.rs @@ -1,8 +1,12 @@ +mod client_cert_info; + use { hyper::{header::HeaderValue, Uri}, std::{collections::HashMap, sync::Arc}, }; +pub use self::client_cert_info::{ClientCertError, ClientCertInfo}; + /// A single backend definition. #[derive(Clone, Debug)] pub struct Backend { @@ -10,6 +14,7 @@ pub struct Backend { pub override_host: Option, pub cert_host: Option, pub use_sni: bool, + pub client_cert: Option, } /// A map of [`Backend`] definitions, keyed by their name. @@ -128,6 +133,8 @@ mod deserialization { override_host, cert_host, use_sni, + // NOTE: Update when we support client certs in static backends + client_cert: None, }) } } diff --git a/lib/src/config/backends/client_cert_info.rs b/lib/src/config/backends/client_cert_info.rs new file mode 100644 index 00000000..3a340b9c --- /dev/null +++ b/lib/src/config/backends/client_cert_info.rs @@ -0,0 +1,65 @@ +use rustls::{Certificate, PrivateKey}; +use std::fmt; +use std::io::Cursor; + +#[derive(Clone)] +pub struct ClientCertInfo { + certificates: Vec, + key: PrivateKey, +} + +impl fmt::Debug for ClientCertInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.certs().fmt(f) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ClientCertError { + #[error("Certificate/key read error: {0}")] + CertificateRead(#[from] std::io::Error), + #[error("No keys found for client certificate")] + NoKeysFound, + #[error("Too many keys found for client certificate (found {0})")] + TooManyKeys(usize), +} + +impl ClientCertInfo { + pub fn new(certificate_bytes: &[u8], certificate_key: &[u8]) -> Result { + let mut certificate_bytes_reader = Cursor::new(certificate_bytes); + let mut key_bytes_reader = Cursor::new(certificate_key); + let cert_info = rustls_pemfile::read_all(&mut certificate_bytes_reader)?; + let key_info = rustls_pemfile::read_all(&mut key_bytes_reader)?; + + let mut certificates = Vec::new(); + let mut keys = Vec::new(); + + for item in cert_info.into_iter().chain(key_info) { + match item { + rustls_pemfile::Item::X509Certificate(x) => certificates.push(Certificate(x)), + rustls_pemfile::Item::RSAKey(x) => keys.push(PrivateKey(x)), + rustls_pemfile::Item::PKCS8Key(x) => keys.push(PrivateKey(x)), + rustls_pemfile::Item::ECKey(x) => keys.push(PrivateKey(x)), + _ => {} + } + } + + let key = if keys.is_empty() { + return Err(ClientCertError::NoKeysFound); + } else if keys.len() > 1 { + return Err(ClientCertError::TooManyKeys(keys.len())); + } else { + keys.remove(0) + }; + + Ok(ClientCertInfo { certificates, key }) + } + + pub fn certs(&self) -> Vec { + self.certificates.clone() + } + + pub fn key(&self) -> PrivateKey { + self.key.clone() + } +} diff --git a/lib/src/error.rs b/lib/src/error.rs index 712d981e..baaec6c1 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -135,6 +135,9 @@ pub enum Error { #[error("String conversion error")] ToStr(#[from] http::header::ToStrError), + + #[error("invalid client certificate")] + InvalidClientCert(#[from] crate::config::ClientCertError), } impl Error { @@ -152,7 +155,7 @@ impl Error { Error::Unsupported { .. } => FastlyStatus::Unsupported, Error::HandleError { .. } => FastlyStatus::Badf, Error::InvalidStatusCode { .. } => FastlyStatus::Inval, - Error::UnknownBackend(_) => FastlyStatus::Inval, + Error::UnknownBackend(_) | Error::InvalidClientCert(_) => FastlyStatus::Inval, // Map specific kinds of `hyper::Error` into their respective error codes. Error::HyperError(e) if e.is_parse() => FastlyStatus::Httpinvalid, Error::HyperError(e) if e.is_user() => FastlyStatus::Httpuser, diff --git a/lib/src/upstream.rs b/lib/src/upstream.rs index 4b2f5aff..353d7a90 100644 --- a/lib/src/upstream.rs +++ b/lib/src/upstream.rs @@ -9,7 +9,7 @@ use crate::{ use futures::Future; use http::{uri, HeaderValue}; use hyper::{client::HttpConnector, header, Client, HeaderMap, Request, Response, Uri}; -use rustls::client::ServerName; +use rustls::client::{ServerName, WantsTransparencyPolicyOrClientCert}; use std::{ io, pin::Pin, @@ -31,43 +31,51 @@ static GZIP_VALUES: [HeaderValue; 2] = [ /// Viceroy's preloaded TLS configuration. /// -/// Setting up client configuration is meant to be done once per process. However, we need -/// two distinct configurations, because backends may choose whether to employ SNI, and that -/// setting is baked into the configuration data. +/// We now have too many options to fully precompute this value, so what this actually +/// holds is a partially-complete TLS config builder, waiting for the point at which +/// we decide whether or not to provide a client certificate and whether or not to use +/// SNI. #[derive(Clone)] pub struct TlsConfig { - with_sni: Arc, - without_sni: Arc, + partial_config: + rustls::ConfigBuilder, } -fn setup_rustls(with_sni: bool) -> Result { - let mut roots = rustls::RootCertStore::empty(); - match rustls_native_certs::load_native_certs() { - Ok(certs) => { +impl TlsConfig { + pub fn new() -> Result { + let mut roots = rustls::RootCertStore::empty(); + match rustls_native_certs::load_native_certs() { + Ok(certs) => { + for cert in certs { + roots.add(&rustls::Certificate(cert.0)).unwrap(); + } + } + Err(err) => return Err(Error::BadCerts(err)), + } + if roots.is_empty() { + warn!("no CA certificates available"); + } + + static TEST_CA_PEM: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../test-fixtures/data/ca.pem" + )); + let mut test_ca_cursor = std::io::Cursor::new(TEST_CA_PEM); + // we're OK with all of the rest of this failing, because it could just be an odd build + // and this is only used in testing. obviously, if this doesn't work during a testing + // run, then the test will fail (with an invalid peer certificate), so we're covered on + // that side. + if let Ok(certs) = rustls_pemfile::certs(&mut test_ca_cursor) { for cert in certs { - roots.add(&rustls::Certificate(cert.0)).unwrap(); + let _ = roots.add(&rustls::Certificate(cert)); } } - Err(err) => return Err(Error::BadCerts(err)), - } - if roots.is_empty() { - warn!("no CA certificates available"); - } - let mut config = rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(roots) - .with_no_client_auth(); - config.enable_sni = with_sni; - Ok(config) -} + let partial_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots); -impl TlsConfig { - pub fn new() -> Result { - Ok(TlsConfig { - with_sni: Arc::new(setup_rustls(true)?), - without_sni: Arc::new(setup_rustls(false)?), - }) + Ok(TlsConfig { partial_config }) } } @@ -127,11 +135,15 @@ impl hyper::service::Service for BackendConnector { let tcp = connect_fut.await.map_err(Box::new)?; if backend.uri.scheme_str() == Some("https") { - let connector = if backend.use_sni { - TlsConnector::from(config.with_sni.clone()) + let mut config = if let Some(certed_key) = &backend.client_cert { + config + .partial_config + .with_client_auth_cert(certed_key.certs(), certed_key.key())? } else { - TlsConnector::from(config.without_sni.clone()) + config.partial_config.with_no_client_auth() }; + config.enable_sni = backend.use_sni; + let connector = TlsConnector::from(Arc::new(config)); let cert_host = backend .cert_host diff --git a/lib/src/wiggle_abi/req_impl.rs b/lib/src/wiggle_abi/req_impl.rs index bcd51418..173680be 100644 --- a/lib/src/wiggle_abi/req_impl.rs +++ b/lib/src/wiggle_abi/req_impl.rs @@ -1,5 +1,9 @@ //! fastly_req` hostcall implementations. +use super::SecretStoreError; +use crate::config::ClientCertInfo; +use crate::secret_store::SecretLookup; + use { crate::{ config::Backend, @@ -310,6 +314,41 @@ impl FastlyHttpReq for Session { true }; + let client_cert = if backend_info_mask.contains(BackendConfigOptions::CLIENT_CERT) { + let cert_slice = config + .client_certificate + .as_array(config.client_certificate_len) + .as_slice()? + .ok_or(Error::SharedMemory)?; + let key_lookup = + self.secret_lookup(config.client_key) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(config.client_key), + ))?; + let key = match &key_lookup { + SecretLookup::Standard { + store_name, + secret_name, + } => self + .secret_stores() + .get_store(store_name) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(config.client_key), + ))? + .get_secret(secret_name) + .ok_or(Error::SecretStoreError( + SecretStoreError::InvalidSecretHandle(config.client_key), + ))? + .plaintext(), + + SecretLookup::Injected { plaintext } => plaintext, + }; + + Some(ClientCertInfo::new(&cert_slice, key)?) + } else { + None + }; + let new_backend = Backend { uri: Uri::builder() .scheme(scheme) @@ -319,6 +358,7 @@ impl FastlyHttpReq for Session { override_host, cert_host, use_sni, + client_cert, }; if !self.add_backend(name, new_backend) { diff --git a/test-fixtures/Cargo.lock b/test-fixtures/Cargo.lock index cf3eadd0..a1c5e763 100644 --- a/test-fixtures/Cargo.lock +++ b/test-fixtures/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "bitflags" version = "1.3.2" @@ -64,9 +70,9 @@ dependencies = [ [[package]] name = "fastly" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b59249dde10f901c578a8eb50577e3c6ee062b0e75f70f9d852da05b75c664d" +checksum = "9c29ec9a24274e7fe2782be8590b188de464b03e04af850676f3295a5abcd6ae" dependencies = [ "anyhow", "bytes", @@ -88,9 +94,9 @@ dependencies = [ [[package]] name = "fastly-macros" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68ee83e8e1c9613d0448f50687a363b289501952f3c046f6cbdcdd82f29e7c2" +checksum = "0b7dee579ae7c714595ee6cc4224006cb2aec430ef27fc3778f14c4f5541df01" dependencies = [ "proc-macro2", "quote", @@ -99,9 +105,9 @@ dependencies = [ [[package]] name = "fastly-shared" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8c7df017449664ec1ee065dac781b5d4ab405e44533c2483cfb31b3f1c6124" +checksum = "c0aab3b493a65563fc700c3dac7d26be3f5cb1c095b4ff9485e141a29ae88fe6" dependencies = [ "bitflags", "http", @@ -110,9 +116,9 @@ dependencies = [ [[package]] name = "fastly-sys" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a5c0dc150d4e1eb2bfdbe169df6bed8d7fafda38ac5a792d800544b35d6b58" +checksum = "3ef9cb90a69d9c7dc99ba10685a856f1054807fbb78744ffcc36151cfcc92b63" dependencies = [ "bitflags", "fastly-shared", @@ -218,6 +224,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.15" @@ -226,18 +241,18 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", @@ -306,11 +321,13 @@ dependencies = [ name = "test-fixtures" version = "0.1.0" dependencies = [ + "base64", "bytes", "fastly", "fastly-shared", "fastly-sys", "http", + "rustls-pemfile", "serde", ] diff --git a/test-fixtures/Cargo.toml b/test-fixtures/Cargo.toml index 4d66bbd1..8dc2cc51 100644 --- a/test-fixtures/Cargo.toml +++ b/test-fixtures/Cargo.toml @@ -8,9 +8,11 @@ license = "Apache-2.0 WITH LLVM-exception" publish = false [dependencies] -fastly = "^0.9.5" -fastly-shared = "^0.9.5" -fastly-sys = "^0.9.5" +base64 = "0.21.2" +fastly = "^0.9.6" +fastly-shared = "^0.9.6" +fastly-sys = "^0.9.6" bytes = "1.0.0" -http = "0.2.1" +http = "0.2.9" +rustls-pemfile = "1.0.3" serde = "1.0.114" diff --git a/test-fixtures/data/ca.key b/test-fixtures/data/ca.key new file mode 100644 index 00000000..5da7f6d5 --- /dev/null +++ b/test-fixtures/data/ca.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,6403E194E2FD15C4 + +JXGAIQ6/3eVUy7R7PulfsnuNfTQye+HOw9EfLwsz6Tm6AxEiQkkXLyVP7BrmPRfa +J1WCngcSwyp1GpOD8qfQOaM0dxS+/bvVvB4eocTxgkDB42MG2HRZpJRrWGQ1VxUT +vIdlV/zTyjC7aAQj5tTJkKN11+lwFxpcvlb3fNbQB23xWdi8yYGTHrEBw6cl5Szi +ipByvcbreKVH4eGrJLcC/H4wyrCe7CwzbP8deHNsH5WrxEWgkkjJMBAk6rzyp9su +F6FNkJLup4Cfk7gmkpQsXs0Y+8LVnBfhc3dQrxdXZjVccFppvRDnoa9uzMQuS56U +jP+wloYeEVS7hfualKJFMNoC3oB7sI6PQGgYi047EkTEIBFFv1ryBcUAwzSlNdoe +2MF1b4OpXrkR0J718QSE/ZpXe4K5HFtRtaE5b6Y47PbMoE3q+ldONEK2jLTvPtIv +DOZWk8H67U0TOM53UQO5fVAntmPPuEiCX2cm0rISF7dwlgYG1obhdko3vir/6cJw +Cg9WbF5dGZYUUbZeNVdXHT+b7MaQOskXXBf+gUVRX/KIRLLP+yH1T06zxzYNHuSD +JBkaklRuXBZnyRPTrqaFNmfgo9XoDY4Yd1QeKfD6Os5bvZeqBYgj3S+vmRUYZeE+ +PYKjzhIMjNmMP7ZP+h0iYs2rt6bRCgjTsd15ZjalPm+0f702sS51G1C8mdwy5ScQ +jX+1PTgpcfZ9aWZLmabC1Cvg1wOH0foAzbtag9qH/FUZgaLP0lfDhlAHCJwOrpRd +3kegjfI1SXE2NGGq5GULU3bP50VYlWxpQNgWVImLnnpTCANfIV6QBx7JVVIxAJps +LtzSt3fjU5wjNDE/GbwzzzA1DxissZzrXM2Q+ac69qPOE38e0PDsRahh2qf+1Db9 +ozUvKOMbk8PAWUr5dtp9SFYgaF6gGoQY5JswP9tqHjTLYYYXZR21auZSIQXiG2yC +wPBsSpnscBQSzo21+95f3jvrnq3HUFwcU5jE1W+RVhCQAQi0ZQdsYTyTsB5OksS3 +hXNBWFTVIOJjyUa0nwsh3hnLKwvl0/Sb8n5qssRMrx6ltN/JadA0I7dBHvjMr9n4 +Z1LkXQSKiGePcbzE8X4WhGApx6LwQgQm8OMqB3vw+HsGRmmzvpuy54mZgl8SV6ob +R62uMVbuvNTJAr4XpGlsiZ+2FWKysvIXj5VEGwSfEHw8PIOuFgtJSS455iEokdid +JJ9tfenJOCVBKLN5dYxzKbsOEvPdXIal9mM9HCPIqpIMhHRAzV1d47KUIrP6i8b8 +O8SXUZJz+JYvCUw6VYOorA7QRx8KBmZVRlJ3h5mocQ3BQIExu+XRQ8SVSYu/aedh +zkDn9F0jg+LGLh10hh+L97glWWw48uFVFjMXUDhrcn2bA1xyvK0WNW8fnZ2pxjSj +ArDxC1qLiH0dRDAzXYOktmm7ToJppz7ncXnRAA7C9ZbGxj8w9D43Ypew+JBPjBKm +XoXNsZLd8j7v66IJfKavA8+wV8u9GKsy6XaCX3y4/qzeMKiElii1bqcLz+Xvo3ZS +kFPwN016SrFfF6jfwIaknZr2u9QELa5B5EIUsGC3PCwGUsqDNTkP3g== +-----END RSA PRIVATE KEY----- diff --git a/test-fixtures/data/ca.pem b/test-fixtures/data/ca.pem new file mode 100644 index 00000000..f60d93b5 --- /dev/null +++ b/test-fixtures/data/ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDqTCCApGgAwIBAgIUDXDr/2fouphqlB8iJASenWOr/XwwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y +dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh +c3RseS5jb20wHhcNMjMwNzI3MDAwODU5WhcNMzMwNzI0MDAwODU5WjBkMQswCQYD +VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G +A1UECgwHVmljZXJveTEfMB0GCSqGSIb3DQEJARYQYXdpY2tAZmFzdGx5LmNvbTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxXdG4C6yEeLTtFPOXWTv1N +eEeJMLcAoupB9u3x0PYT+w+0ruAympviqGbEiyZL/qMKLYenLiQO+72VCISW5qfB +ZoCpwDxBon5TDUZ98JU93nVRml7uOg25G+KTs3aeJt6+rFDPNaNyxVcKgCuURB4y +mwgosLUvxoEffFnHlURETLN4aSGQ6TLp8YEJp4EudTVo/l+kdhm6sLZMBkmUxnnl +muEc8ePAr1igYchz2tbcWRjzxoUOuEdoKaW2OCElNObt2WYPWzHs+6p1K8+KyTRY +/pVOFtA43nuWmk++UHFthBAw9IqBuO0FMJr4SULnKfiTh5E9F+nZ0Q/1nfzzsAMC +AwEAAaNTMFEwHQYDVR0OBBYEFGYM6HhP8yZ17eXw5nOfQ971u1l9MB8GA1UdIwQY +MBaAFGYM6HhP8yZ17eXw5nOfQ971u1l9MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAFmFkUodKTXeT683GEj4SoiMbDL8d3x+Vc+kvLPC2Jloru4R +Qo0USu3eJZjNxKjmPbLii8gzf5ZmZHdytWQ+5irYjXBHrE9tPgmpavhM+0otpnUd +vYosnfwv/aQEIiqeMkpqzbSKvb2I+TVpAC1xb6qbYE95tnsX/KEdAoJ/SAcZLGYQ +LKGTjz3eKlgUWy69uwzHXkie8hxDVRlyA7cFY4AAqsLhL2KQPWtMT7fRKrVKfLYd +Qq7tJAMLnPnAdAUousI0RDcLpB8adGkhZH66lL4oV9U+aQ0dA0oiqSKZtMoHeWbr +/L0ti7ZOfxOxRRCzt8KdLo/kGNTfAz+74P0MY80= +-----END CERTIFICATE----- diff --git a/test-fixtures/data/ca.srl b/test-fixtures/data/ca.srl new file mode 100644 index 00000000..7183f75d --- /dev/null +++ b/test-fixtures/data/ca.srl @@ -0,0 +1 @@ +3A9F7B82F32561D05823FDF2AE90DE1DB771E518 diff --git a/test-fixtures/data/client.crt b/test-fixtures/data/client.crt new file mode 100644 index 00000000..1c0ce356 --- /dev/null +++ b/test-fixtures/data/client.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDvjCCAqagAwIBAgIUOp97gvMlYdBYI/3yrpDeHbdx5RgwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y +dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh +c3RseS5jb20wHhcNMjMwNzI3MDAxOTU0WhcNMzMwNzI0MDAxOTU0WjB1MQswCQYD +VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G +A1UECgwHVmljZXJveTEPMA0GA1UECwwGQ2xpZW50MR8wHQYJKoZIhvcNAQkBFhBh +d2lja0BmYXN0bHkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +z27x1GpD46K6b9/3PNyZYKgTL9GBbpLAVF8Uebd34ftUfnWZ3ER+x6A1YbacHnL1 +12diPPevyYkpXuiujwCeswYNrZHEtiRfAvrzBRhnhL8owQTxjOcG4EOzR7Je556F +Tq8kNth5iHckORjmXiV9ZahbLv/zBFpkXpDeze62zd8y9chPNEqcrLZBOb4UoKXm +Ot1lIdeo23nysR4rC6XemWNSFcZv9zagUzliMeca3XN2RIUAFZv4o+gYPqqXQi+0 +a+OOq0jnKpawW+avn2UG7wzXGlLcVOvLe5BOCA1RfWtR8w03MFdvoBAesXJ4xGX1 +ROUzelldedmpqtvORdhmGQIDAQABo1cwVTAfBgNVHSMEGDAWgBRmDOh4T/Mmde3l +8OZzn0Pe9btZfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAaBgNVHREEEzARggls +b2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBAJ84GzmmqsmmtqXcmZIH +i644p8wIc/DXPqb7zzAVm9FXpFgW3mN4xu1JYWu+rb1sge8uIm7Vt5Isd4CZ89XI +F2Q2DS/rKMQmjgSDReWm9G+qZROwuhNDzK85e73Rw2EdX6cXtAGR1h3IdOTIv1FC +UElFER31U8i4J9pxUZF/FTzlPEA1agqMsO6hQlj/A9B6TtzL7SSxCFBBaFbNCLMC +D/WCrIoklNV5TwutYG80EYZhJlfUJPDQBphkcetDBI0L/KL/n20bg8OR/epGD5++ +qKIulxf9iUR5QHm2fWKdTLOuADmV+lc925gIqGhFhjVvpNPOcdckecQUp3vCNu2/ +HrM= +-----END CERTIFICATE----- diff --git a/test-fixtures/data/client.csr b/test-fixtures/data/client.csr new file mode 100644 index 00000000..8737a782 --- /dev/null +++ b/test-fixtures/data/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICujCCAaICAQAwdTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8G +A1UEBwwIUG9ydGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxDzANBgNVBAsMBkNsaWVu +dDEfMB0GCSqGSIb3DQEJARYQYXdpY2tAZmFzdGx5LmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAM9u8dRqQ+Oium/f9zzcmWCoEy/RgW6SwFRfFHm3 +d+H7VH51mdxEfsegNWG2nB5y9ddnYjz3r8mJKV7oro8AnrMGDa2RxLYkXwL68wUY +Z4S/KMEE8YznBuBDs0eyXueehU6vJDbYeYh3JDkY5l4lfWWoWy7/8wRaZF6Q3s3u +ts3fMvXITzRKnKy2QTm+FKCl5jrdZSHXqNt58rEeKwul3pljUhXGb/c2oFM5YjHn +Gt1zdkSFABWb+KPoGD6ql0IvtGvjjqtI5yqWsFvmr59lBu8M1xpS3FTry3uQTggN +UX1rUfMNNzBXb6AQHrFyeMRl9UTlM3pZXXnZqarbzkXYZhkCAwEAAaAAMA0GCSqG +SIb3DQEBCwUAA4IBAQAAU/iqM3cx76vLFcV18xnnpi7jQjkxTvP5uq/7gisChgv1 +Z26DS6ZYihlFKJYt0fFXCh9CG61+ttkw6Cz53G62+iV/C3+0tamn4G8EQddZshg7 +QpRousktgfJKHIIQLfzqb6fe8G49jmQlAjdseenBn1dHndxRLNOtM2c/smaGaafU +yTdPCdLDleqfCcUMvMjGp/WYo9j33Mw4ZN1C9IUjFc0+3Ythsw0hrZmOkr0X90v4 +H77tZsS+ySWbeInD7Ku6IJzDimYAlXNG251LFdgSdX0dkxCfTmhFf9Z6Gl/jNufO +ZEiHpX9oR7tFq89Ww6eMgW8gKJoyH9+1N91PtxCo +-----END CERTIFICATE REQUEST----- diff --git a/test-fixtures/data/client.key b/test-fixtures/data/client.key new file mode 100644 index 00000000..57a7df31 --- /dev/null +++ b/test-fixtures/data/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAz27x1GpD46K6b9/3PNyZYKgTL9GBbpLAVF8Uebd34ftUfnWZ +3ER+x6A1YbacHnL112diPPevyYkpXuiujwCeswYNrZHEtiRfAvrzBRhnhL8owQTx +jOcG4EOzR7Je556FTq8kNth5iHckORjmXiV9ZahbLv/zBFpkXpDeze62zd8y9chP +NEqcrLZBOb4UoKXmOt1lIdeo23nysR4rC6XemWNSFcZv9zagUzliMeca3XN2RIUA +FZv4o+gYPqqXQi+0a+OOq0jnKpawW+avn2UG7wzXGlLcVOvLe5BOCA1RfWtR8w03 +MFdvoBAesXJ4xGX1ROUzelldedmpqtvORdhmGQIDAQABAoIBAQCsbu6KhDehMDHJ +NCWjK0I4zh78/iyZDVbiDBPKRpBag4GuifX329yD95LIgnNvAGOKxz8rrT4sy19f +rQ8Ggx5pdVvDcExUmRF+Obvw/WN4PywSoBhn59iYbs7Gh+lKo0Tvrrns+bC1l0y+ +RguiMYn3CqeZ/1w1vyp2TflYuNqvcR4zMzJ4dN474CCLPIUX9OfK21Lbv/UMdguF +Rs/BuStucqaCzEtTLyZYlxQc1i8S8Uy2yukXR6TYWJOsWZj0KIgH/YI7ZgzvTIxL +ax4Hn4jIHPFSJ+vl2ehDKffkQQ0lzm60ASkjaJY6GsFoTQzsmuafpLIAoJbDbZR1 +txPSFC+BAoGBAPbp6+LsXoEY+4RfStg4c/oLWmK3aTxzQzMY90vxnMm6SJTwTPAm +pO+Pp2UGyEGHV7hg3d+ItWpM9QGVmsjm+punIfc0W/0+AVUonjPLfv44dz7+geYt +/oeMv4RTqCclROvtQTqV6hHn4E3Xg061miEe6OxYmqfZuLD2nv2VlsQRAoGBANcR +GAqeClQtraTnu+yU9U+FJZfvSxs1yHr7XItCMtwxeU6+nipa+3pXNnKu0dKKekUG +PCdUipXgggA6OUm2YFKPUhiXJUNoHCj45Tkv2NshGplW33U3NcCkDqL7vvZoBBfP +OPxEVRVEIlwp/WzEambs9MjWoecEaOe7/3UCVumJAoGANlfVquQLCK7O7JtshZon +LGlDQ2bKqptTtvNPuk87CssNHnqk9FYNBwy+8uVDPejjzZjEPGaCRxsY8XhT0NPF +ZGysdRP5CwuSj4OZDh1DngAffqXVQSvuUTcRD7a506PIP4TATnygP8ChBYDhTXl6 +qr961EnMABVTKN+eroE15YECgYEAv+YLyqV71+KuNx9i6lV7kcnfYnNtU8koqruQ +tt2Jnjoy4JVrcaWfEGmzNp9Qr4lKUj6e/AUOZ29c8DEDnwcxaVliynhLEptZzSFQ +/zb3S4d9QWdnmiJ6Pvrj6H+yxBDJ3ijT0xxxwrj547y/2QZlXpN+U5pX+ldP974i +0dgVjukCgYEArxv0dO2VEguWLx5YijHiN72nDDI+skbfkQkvWQjA7x8R9Xx1SWUl +WeyeaaV5rqfJZF1wBCK5VJndjbOGhPh6u/0mpeYw4Ty3+CKN2WoikQO27qYfMZW5 +vvT7m9ZR+gkm2TjZ+pZuilz2gqu/yMJKl8Fi8Q7dsb8eWedWQXjbUZg= +-----END RSA PRIVATE KEY----- diff --git a/test-fixtures/data/server.crt b/test-fixtures/data/server.crt new file mode 100644 index 00000000..7ba09314 --- /dev/null +++ b/test-fixtures/data/server.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDvjCCAqagAwIBAgIUOp97gvMlYdBYI/3yrpDeHbdx5RcwDQYJKoZIhvcNAQEL +BQAwZDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8GA1UEBwwIUG9y +dGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxHzAdBgkqhkiG9w0BCQEWEGF3aWNrQGZh +c3RseS5jb20wHhcNMjMwNzI3MDAxODA5WhcNMzMwNzI0MDAxODA5WjB1MQswCQYD +VQQGEwJVUzEPMA0GA1UECAwGT3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEQMA4G +A1UECgwHVmljZXJveTEPMA0GA1UECwwGU2VydmVyMR8wHQYJKoZIhvcNAQkBFhBh +d2lja0BmYXN0bHkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +vaJbeui08EYihCfktu3Ary6CnxNmsg8kCXPddRdGa9+QJGGkSNBf4AseYI/jXPpO +/eMixAnlv5ebie9vTazKPNxXwDOzoFu3Gt8jFKwI0jsmkcIIk1wC0CDQrsAm50uC +la6zEGWLF/Fd5lfoXZ5Jo20LgHV7t9tXsZCzT3RgC0dKkrnGumJK5g1OYGN6l4wh +i2zOCMvIT/V5Am7epCZx1GS6O2mVLZhrfgpfFiRx6o1QV07XXnW+LFz8Rk5cwOUl +t4K9h0DSc9p5yPvgz3DDw2qA4rcr7BK18ePUJR1khey8aDQWzHfSfVj8OwLlhLN8 +ixdXv+YLvGKaOsVH2cvt2wIDAQABo1cwVTAfBgNVHSMEGDAWgBRmDOh4T/Mmde3l +8OZzn0Pe9btZfTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAaBgNVHREEEzARggls +b2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBAFE8Kycq90Dnij9WJx5n +BuR+KU/o4nV3NjVHmo503KxIW6PFuOMSc65Uxq0Q6IZmJvzovdnCRRda+n9c/1dV +APFTwG5QCOJOEzK7SgJpvxlkZ+39fMBPpakcBOUobFqifaltKum5x3pT9zn1n2jx +EGay3RWT45wXtKBEd0zZ/vMu0fFGoMBzT696ywVMzbSI7o745UN3fwrE8FVGkJXk +V5F+6ugjXpqhqPi3rZRfC+ZJolFe+blj5wlXScwg/W5BKQDbotCZFCswk00J++/M +516yQItLQUU4lYJzVAqlrHIwJeBhAinHlpfNRqQJtxVE2VIzlMRp4n2VxZO64IfH +xaw= +-----END CERTIFICATE----- diff --git a/test-fixtures/data/server.csr b/test-fixtures/data/server.csr new file mode 100644 index 00000000..32e85cc6 --- /dev/null +++ b/test-fixtures/data/server.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICujCCAaICAQAwdTELMAkGA1UEBhMCVVMxDzANBgNVBAgMBk9yZWdvbjERMA8G +A1UEBwwIUG9ydGxhbmQxEDAOBgNVBAoMB1ZpY2Vyb3kxDzANBgNVBAsMBlNlcnZl +cjEfMB0GCSqGSIb3DQEJARYQYXdpY2tAZmFzdGx5LmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAL2iW3rotPBGIoQn5LbtwK8ugp8TZrIPJAlz3XUX +RmvfkCRhpEjQX+ALHmCP41z6Tv3jIsQJ5b+Xm4nvb02syjzcV8Azs6BbtxrfIxSs +CNI7JpHCCJNcAtAg0K7AJudLgpWusxBlixfxXeZX6F2eSaNtC4B1e7fbV7GQs090 +YAtHSpK5xrpiSuYNTmBjepeMIYtszgjLyE/1eQJu3qQmcdRkujtplS2Ya34KXxYk +ceqNUFdO1151vixc/EZOXMDlJbeCvYdA0nPaecj74M9ww8NqgOK3K+wStfHj1CUd +ZIXsvGg0Fsx30n1Y/DsC5YSzfIsXV7/mC7ximjrFR9nL7dsCAwEAAaAAMA0GCSqG +SIb3DQEBCwUAA4IBAQBza1mDXtkyCeUbQzaNW0QamUWmGTijIAP6klE4w2Wy0cN5 +XrkkRQXMRGDfwOnkTqsv8p1AHKjPdnC2fY4a4fn5qR4/6dBVB40889UH39w1qcDW +L+ybOPDA/UjhiXoL2EiW5iLyHeK/ttHFgvq5niHUmYk/ZVElS4xD0FAIIagxDa/u +zTXYwlf+ZT4KdjPdx/s9evRQQWrS5Z06Rv9Lzkl5A9WCUXgyGw16MvU0rA/SFEgu +98XqUY7wa5DC7RuW38IKk06CGBDWNc3l1ZxQ+VB/sym702nbV2HFZT46viflll1a +FjgQEu8ztbDfgooRGGHimZZXA2dBEHirgdM0bdzy +-----END CERTIFICATE REQUEST----- diff --git a/test-fixtures/data/server.ext b/test-fixtures/data/server.ext new file mode 100644 index 00000000..fc44f57d --- /dev/null +++ b/test-fixtures/data/server.ext @@ -0,0 +1,8 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 diff --git a/test-fixtures/data/server.key b/test-fixtures/data/server.key new file mode 100644 index 00000000..cc688f1f --- /dev/null +++ b/test-fixtures/data/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAvaJbeui08EYihCfktu3Ary6CnxNmsg8kCXPddRdGa9+QJGGk +SNBf4AseYI/jXPpO/eMixAnlv5ebie9vTazKPNxXwDOzoFu3Gt8jFKwI0jsmkcII +k1wC0CDQrsAm50uCla6zEGWLF/Fd5lfoXZ5Jo20LgHV7t9tXsZCzT3RgC0dKkrnG +umJK5g1OYGN6l4whi2zOCMvIT/V5Am7epCZx1GS6O2mVLZhrfgpfFiRx6o1QV07X +XnW+LFz8Rk5cwOUlt4K9h0DSc9p5yPvgz3DDw2qA4rcr7BK18ePUJR1khey8aDQW +zHfSfVj8OwLlhLN8ixdXv+YLvGKaOsVH2cvt2wIDAQABAoIBAEvF8z3SfHJB5Arg +kfBSYhrdv83mh7OAf0rTpFrkOPxjsYoIBggeUyEH8FRvSk9dqXCjcMHanpYG81yT +cusbrxfQh7PCNPVPkIPJQ5BACapPfmLhoGGZc3pMknYxS5pCPuSmkOBtYr3ncTjY +SX4XAJ+vs9fZmdzmZU0LX8rQ2ovGeFrbX0jRr3g6B85VsAeGJ9TDSSerPnO0mtFI +ZSdl1wttuZErRRygt0zf00wlVh1iPXXe8J3oW4spFPwCPKahhNc7Ezz6HWo6ml+h +Dlmbcjz1QIqKLd8DCmGh/gx7X1lC/AKEq2c6KIMDoaBKjICwh9Kho0/OsJQ3+BKn +/yvBsvECgYEA+ccNBnmLd+VrLtTh7+dKRAlQsDkirnvZ3bpA3QJWAA97QYpz5riy +IDQO3UV6jkcMbcaj3wOwKAi9k3FwCPaz93+//W6JpQWnwjH9c/ziogAJ/g6Pman/ +hHTKJO044nCZh3WLQpSOwTz0yFRQM6q1cEollhT2hKnFxbtDzVZx+CMCgYEAwlu+ +e6UAs3xrftiX70UkPbGNBLoP0wNJFqT4TautmVbUF3s2tSdyf/wbWuhTIsYYwRdv +B5i7Or1e/DsVH82cq8z9ZB15EVTR4awshLVJz15RvqEtITfZrZ2hdFf0CKk/9J4w +hzmRG14qz/GlL8CVqork5F4bd10jZscVdQqA8ukCgYBlCzEpvWG+TwDdISGFe3t/ +qoUJxRNSoqewGvjCb3965shl6yyX2X+1p1mcCc9aX0OX5RPF1CgfCeonC2zXM3X6 +WaPBUkY8i90hojd2BIdqIbnpHNravvqvCs/7wDuS3xo8wkBj3tUhNxePMwx+2kAr +/NLXtANGB6gKJYd4OdBBIQKBgD4gf3ocm2XETsRETgTQ8C28VJx/MVG9Sh6v6yNA +zoQmijNbUniDvIkGuGPNwc1qzzzh1b7y5l53bCZqaG07F2qfYxweg7WzjEd79tsQ +7CAaQT0TXk6xAKcLrTF4b+xY1bXG3zJKh4TdDAhecPQbtnvGXDZXkqYMIqXW25gH +HIMJAoGAUzABCDKoI1qsznMVqM7jC77akGHlwlWI9BmYhWOXIrO5th5CHFJK+YBE +sG44dwnd2Sv/o2REn34cM8HETq7cyuW+Et2N+VyogyGlS3W+HCHkMYINU/ZPeGMF +wiwXklON2CtatN31JDEeMusyySki5I/lw9at2TvDsdrLGeP4i3I= +-----END RSA PRIVATE KEY----- diff --git a/test-fixtures/src/bin/mutual-tls.rs b/test-fixtures/src/bin/mutual-tls.rs new file mode 100644 index 00000000..8e30395c --- /dev/null +++ b/test-fixtures/src/bin/mutual-tls.rs @@ -0,0 +1,46 @@ +use base64::engine::{Engine, general_purpose}; +use fastly::{Backend, Error, Request, Response}; +use fastly::http::StatusCode; +use fastly::secret_store::Secret; +use std::str::FromStr; + +/// Pass everything from the downstream request through to the backend, then pass everything back +/// from the upstream request to the downstream response. +fn main() -> Result<(), Error> { + let client_req = Request::from_client(); + let certificate = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/data/client.crt")); + let key_bytes = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/data/client.key")); + let key_secret = Secret::from_bytes(key_bytes.to_vec()).expect("can inject key"); + + let Some(port_str) = client_req.get_header_str("Port") else { + panic!("Couldn't find out what port to use!"); + }; + let port = u16::from_str(port_str).unwrap(); + + let backend = Backend::builder("mtls-backend", format!("localhost:{}", port)) + .enable_ssl() + .provide_client_certificate(certificate, key_secret) + .finish() + .expect("can build backend"); + + let resp = Request::get("http://localhost/") + .with_header("header", "is-a-thing") + .with_body("hello") + .send(backend) + .unwrap(); + + assert_eq!(resp.get_status(), StatusCode::OK); + let body = resp.into_body().into_string(); + let mut cert_cursor = std::io::Cursor::new(certificate); + let mut info = rustls_pemfile::certs(&mut cert_cursor).expect("got certs"); + assert_eq!(info.len(), 1); + let reflected_cert = info.remove(0); + let base64_cert = general_purpose::STANDARD.encode(reflected_cert); + assert_eq!(body, base64_cert); + + Response::from_status(200) + .with_body("Hello, Viceroy!") + .send_to_client(); + + Ok(()) +}