From b09fab5409409ec86ea43125bb8cda16794ccb39 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Fri, 3 May 2024 16:09:46 -0600 Subject: [PATCH 01/13] feat: TLS resolver --- Cargo.lock | 5 +- Cargo.toml | 2 +- ext/fetch/lib.rs | 15 +-- ext/kv/remote.rs | 10 +- ext/net/lib.rs | 4 + ext/net/ops_tls.rs | 63 +++++++++-- ext/tls/Cargo.toml | 1 + ext/tls/lib.rs | 49 ++++----- ext/tls/tls_key.rs | 251 ++++++++++++++++++++++++++++++++++++++++++ ext/websocket/lib.rs | 14 ++- runtime/web_worker.rs | 3 +- runtime/worker.rs | 3 +- 12 files changed, 356 insertions(+), 64 deletions(-) create mode 100644 ext/tls/tls_key.rs diff --git a/Cargo.lock b/Cargo.lock index 45152eace0949f..22125cd3554534 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1911,6 +1911,7 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki", "serde", + "tokio", "webpki-roots", ] @@ -5455,9 +5456,9 @@ dependencies = [ [[package]] name = "rustls-tokio-stream" -version = "0.2.17" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded7a36e8ac05b8ada77a84c5ceec95361942ee9dedb60a82f93f788a791aae8" +checksum = "a600f217ae82dd9430c546cb4742ddf9391893146fc614ba5bda93a71029701a" dependencies = [ "futures", "rustls", diff --git a/Cargo.toml b/Cargo.toml index 243469d8ea3360..43a23674f62e77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ ring = "^0.17.0" rusqlite = { version = "=0.29.0", features = ["unlock_notify", "bundled"] } rustls = "0.21.11" rustls-pemfile = "1.0.0" -rustls-tokio-stream = "=0.2.17" +rustls-tokio-stream = "=0.2.21" rustls-webpki = "0.101.4" rustyline = "=13.0.0" saffron = "=0.1.0" diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 3e43370d3b7523..416ce8c6405efe 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -80,7 +80,7 @@ pub struct Options { pub request_builder_hook: Option Result>, pub unsafely_ignore_certificate_errors: Option>, - pub client_cert_chain_and_key: Option, + pub client_cert_chain_and_key: TlsKeys, pub file_fetch_handler: Rc, } @@ -101,7 +101,7 @@ impl Default for Options { proxy: None, request_builder_hook: None, unsafely_ignore_certificate_errors: None, - client_cert_chain_and_key: None, + client_cert_chain_and_key: TlsKeys::Null, file_fetch_handler: Rc::new(DefaultFileFetchHandler), } } @@ -205,7 +205,7 @@ pub fn create_client_from_options( unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: options.client_cert_chain_and_key.clone(), + client_cert_chain_and_key: Some(options.client_cert_chain_and_key.clone()), pool_max_idle_per_host: None, pool_idle_timeout: None, http1: true, @@ -832,11 +832,6 @@ where permissions.check_net_url(&url, "Deno.createHttpClient()")?; } - let client_cert_chain_and_key = match tls_keys { - TlsKeys::Null => None, - TlsKeys::Static(key) => Some(key.clone()), - }; - let options = state.borrow::(); let ca_certs = args .ca_certs @@ -853,7 +848,7 @@ where unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key, + client_cert_chain_and_key: tls_keys.clone().try_into().unwrap(), pool_max_idle_per_host: args.pool_max_idle_per_host, pool_idle_timeout: args.pool_idle_timeout.and_then( |timeout| match timeout { @@ -915,7 +910,7 @@ pub fn create_http_client( options.root_cert_store, options.ca_certs, options.unsafely_ignore_certificate_errors, - options.client_cert_chain_and_key, + options.client_cert_chain_and_key.into(), deno_tls::SocketUse::Http, )?; diff --git a/ext/kv/remote.rs b/ext/kv/remote.rs index 88127fc8fa20a5..9d5e099c73b9d8 100644 --- a/ext/kv/remote.rs +++ b/ext/kv/remote.rs @@ -16,7 +16,7 @@ use deno_fetch::CreateHttpClientOptions; use deno_tls::rustls::RootCertStore; use deno_tls::Proxy; use deno_tls::RootCertStoreProvider; -use deno_tls::TlsKey; +use deno_tls::TlsKeys; use denokv_remote::MetadataEndpoint; use denokv_remote::Remote; use url::Url; @@ -27,7 +27,7 @@ pub struct HttpOptions { pub root_cert_store_provider: Option>, pub proxy: Option, pub unsafely_ignore_certificate_errors: Option>, - pub client_cert_chain_and_key: Option, + pub client_cert_chain_and_key: TlsKeys, } impl HttpOptions { @@ -135,7 +135,11 @@ impl DatabaseHandler unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: options.client_cert_chain_and_key.clone(), + client_cert_chain_and_key: options + .client_cert_chain_and_key + .clone() + .try_into() + .unwrap(), pool_max_idle_per_host: None, pool_idle_timeout: None, http1: false, diff --git a/ext/net/lib.rs b/ext/net/lib.rs index d137aa315a47a4..fa8074b345c27f 100644 --- a/ext/net/lib.rs +++ b/ext/net/lib.rs @@ -87,6 +87,10 @@ deno_core::extension!(deno_net, ops_tls::op_tls_key_null, ops_tls::op_tls_key_static, ops_tls::op_tls_key_static_from_file

, + ops_tls::op_tls_cert_resolver_create, + ops_tls::op_tls_cert_resolver_poll, + ops_tls::op_tls_cert_resolver_resolve, + ops_tls::op_tls_cert_resolver_resolve_error, ops_tls::op_tls_start

, ops_tls::op_net_connect_tls

, ops_tls::op_net_listen_tls

, diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 487adf3bc732ce..f43513e56fe6dc 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -29,12 +29,16 @@ use deno_core::ResourceId; use deno_tls::create_client_config; use deno_tls::load_certs; use deno_tls::load_private_keys; +use deno_tls::new_resolver; use deno_tls::rustls::Certificate; +use deno_tls::rustls::ClientConnection; use deno_tls::rustls::PrivateKey; use deno_tls::rustls::ServerConfig; use deno_tls::rustls::ServerName; +use deno_tls::ServerConfigProvider; use deno_tls::SocketUse; use deno_tls::TlsKey; +use deno_tls::TlsKeyLookup; use deno_tls::TlsKeys; use rustls_tokio_stream::TlsStreamRead; use rustls_tokio_stream::TlsStreamWrite; @@ -64,6 +68,7 @@ pub(crate) const TLS_BUFFER_SIZE: Option = pub struct TlsListener { pub(crate) tcp_listener: TcpListener, pub(crate) tls_config: Arc, + pub(crate) server_config_provider: Option, } impl TlsListener { @@ -228,6 +233,44 @@ where )) } +#[op2] +pub fn op_tls_cert_resolver_create<'s>( + scope: &mut v8::HandleScope<'s>, +) -> v8::Local<'s, v8::Array> { + let (resolver, lookup) = new_resolver(); + let resolver = + deno_core::cppgc::make_cppgc_object(scope, TlsKeys::Resolver(resolver)); + let lookup = deno_core::cppgc::make_cppgc_object(scope, lookup); + v8::Array::new_with_elements(scope, &[resolver.into(), lookup.into()]) +} + +#[op2(async)] +#[string] +pub async fn op_tls_cert_resolver_poll( + #[cppgc] lookup: &TlsKeyLookup, +) -> Option { + lookup.poll().await +} + +#[op2(fast)] +pub fn op_tls_cert_resolver_resolve( + #[cppgc] lookup: &TlsKeyLookup, + #[string] sni: String, + #[string] cert: String, + #[string] key: String, +) { + lookup.resolve(sni, Ok((cert, key))) +} + +#[op2(fast)] +pub fn op_tls_cert_resolver_resolve_error( + #[cppgc] lookup: &TlsKeyLookup, + #[string] sni: String, + #[string] error: String, +) { + lookup.resolve(sni, Err(anyhow!(error))) +} + #[op2] #[serde] pub fn op_tls_start( @@ -287,7 +330,7 @@ where root_cert_store, ca_certs, unsafely_ignore_certificate_errors, - None, + TlsKeys::Null, SocketUse::GeneralSsl, )?; @@ -299,8 +342,7 @@ where let tls_config = Arc::new(tls_config); let tls_stream = TlsStream::new_client_side( tcp_stream, - tls_config, - hostname_dns, + ClientConnection::new(tls_config, hostname_dns)?, TLS_BUFFER_SIZE, ); @@ -367,15 +409,11 @@ where let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; - let cert_and_key = match key_pair { - TlsKeys::Null => None, - TlsKeys::Static(key) => Some(key.clone()), - }; let mut tls_config = create_client_config( root_cert_store, ca_certs, unsafely_ignore_certificate_errors, - cert_and_key, + key_pair.clone(), SocketUse::GeneralSsl, )?; @@ -388,8 +426,7 @@ where let tls_stream = TlsStream::new_client_side( tcp_stream, - tls_config, - hostname_dns, + ClientConnection::new(tls_config, hostname_dns)?, TLS_BUFFER_SIZE, ); @@ -448,11 +485,12 @@ where .with_safe_defaults() .with_no_client_auth(); - let mut tls_config = match keys { + let mut tls_config = match keys.clone() { TlsKeys::Null => Err(anyhow!("Deno.listenTls requires a key")), TlsKeys::Static(TlsKey(cert, key)) => tls_config - .with_single_cert(cert.clone(), key.clone()) + .with_single_cert(cert, key) .map_err(|e| anyhow!(e)), + TlsKeys::Resolver(resolver) => unimplemented!(), } .map_err(|e| { custom_error("InvalidData", "Error creating TLS certificate").context(e) @@ -473,6 +511,7 @@ where let tls_listener_resource = NetworkListenerResource::new(TlsListener { tcp_listener, tls_config: tls_config.into(), + server_config_provider: None, }); let rid = state.resource_table.add(tls_listener_resource); diff --git a/ext/tls/Cargo.toml b/ext/tls/Cargo.toml index f72bc7262e9077..b9ce34a8ff4e57 100644 --- a/ext/tls/Cargo.toml +++ b/ext/tls/Cargo.toml @@ -22,4 +22,5 @@ rustls-pemfile.workspace = true rustls-tokio-stream.workspace = true rustls-webpki.workspace = true serde.workspace = true +tokio.workspace = true webpki-roots.workspace = true diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index 7e68971e2ef028..4c487aff99fd8b 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -30,6 +30,9 @@ use std::io::Cursor; use std::sync::Arc; use std::time::SystemTime; +mod tls_key; +pub use tls_key::*; + pub type Certificate = rustls::Certificate; pub type PrivateKey = rustls::PrivateKey; pub type RootCertStore = rustls::RootCertStore; @@ -175,7 +178,7 @@ pub fn create_client_config( root_cert_store: Option, ca_certs: Vec>, unsafely_ignore_certificate_errors: Option>, - maybe_cert_chain_and_key: Option, + maybe_cert_chain_and_key: TlsKeys, socket_use: SocketUse, ) -> Result { if let Some(ic_allowlist) = unsafely_ignore_certificate_errors { @@ -189,14 +192,15 @@ pub fn create_client_config( // However it's not really feasible to deduplicate it as the `client_config` instances // are not type-compatible - one wants "client cert", the other wants "transparency policy // or client cert". - let mut client = - if let Some(TlsKey(cert_chain, private_key)) = maybe_cert_chain_and_key { - client_config - .with_client_auth_cert(cert_chain, private_key) - .expect("invalid client key or certificate") - } else { - client_config.with_no_client_auth() - }; + let mut client = if let TlsKeys::Static(TlsKey(cert_chain, private_key)) = + maybe_cert_chain_and_key + { + client_config + .with_client_auth_cert(cert_chain, private_key) + .expect("invalid client key or certificate") + } else { + client_config.with_no_client_auth() + }; add_alpn(&mut client, socket_use); return Ok(client); @@ -226,14 +230,13 @@ pub fn create_client_config( root_cert_store }); - let mut client = - if let Some(TlsKey(cert_chain, private_key)) = maybe_cert_chain_and_key { - client_config - .with_client_auth_cert(cert_chain, private_key) - .expect("invalid client key or certificate") - } else { - client_config.with_no_client_auth() - }; + let mut client = match maybe_cert_chain_and_key { + TlsKeys::Static(TlsKey(cert_chain, private_key)) => client_config + .with_client_auth_cert(cert_chain, private_key) + .expect("invalid client key or certificate"), + TlsKeys::Null => client_config.with_no_client_auth(), + TlsKeys::Resolver(_) => unimplemented!(), + }; add_alpn(&mut client, socket_use); Ok(client) @@ -325,15 +328,3 @@ pub fn load_private_keys(bytes: &[u8]) -> Result, AnyError> { Ok(keys) } - -/// A loaded key. -// FUTURE(mmastrac): add resolver enum value to support dynamic SNI -pub enum TlsKeys { - // TODO(mmastrac): We need Option<&T> for cppgc -- this is a workaround - Null, - Static(TlsKey), -} - -/// A TLS certificate/private key pair. -#[derive(Clone, Debug)] -pub struct TlsKey(pub Vec, pub PrivateKey); diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs new file mode 100644 index 00000000000000..ce1f71d16f2c1c --- /dev/null +++ b/ext/tls/tls_key.rs @@ -0,0 +1,251 @@ +use crate::Certificate; +use crate::PrivateKey; +use deno_core::anyhow::anyhow; +use deno_core::error::AnyError; +use deno_core::futures::future::Either; +use deno_core::unsync::spawn; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::Debug; +use std::future::ready; +use std::future::Future; +use std::rc::Rc; + +type ErrorType = Rc; + +/// A TLS certificate/private key pair. +#[derive(Clone, Debug)] +pub struct TlsKey(pub Vec, pub PrivateKey); + +#[derive(Clone, Debug, Default)] +pub enum TlsKeys { + // TODO(mmastrac): We need Option<&T> for cppgc -- this is a workaround + #[default] + Null, + Static(TlsKey), + Resolver(TlsKeyResolver), +} + +impl TryInto> for TlsKeys { + type Error = Self; + fn try_into(self) -> Result, Self::Error> { + match self { + Self::Null => Ok(None), + Self::Static(key) => Ok(Some(key)), + Self::Resolver(_) => Err(self), + } + } +} + +impl From> for TlsKeys { + fn from(value: Option) -> Self { + match value { + None => TlsKeys::Null, + Some(key) => TlsKeys::Static(key), + } + } +} + +enum TlsKeyState { + Resolving( + tokio::sync::broadcast::Receiver>, + ), + Resolved(Result<(String, String), ErrorType>), +} + +struct TlsKeyResolverInner { + resolution_tx: tokio::sync::mpsc::UnboundedSender<( + String, + tokio::sync::broadcast::Sender>, + )>, + cache: RefCell>, +} + +#[derive(Clone)] +pub struct TlsKeyResolver { + inner: Rc, +} + +impl Debug for TlsKeyResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TlsKeyResolver").finish() + } +} + +pub fn new_resolver() -> (TlsKeyResolver, TlsKeyLookup) { + let (resolution_tx, resolution_rx) = tokio::sync::mpsc::unbounded_channel(); + ( + TlsKeyResolver { + inner: Rc::new(TlsKeyResolverInner { + resolution_tx, + cache: Default::default(), + }), + }, + TlsKeyLookup { + resolution_rx: RefCell::new(resolution_rx), + pending: Default::default(), + }, + ) +} + +impl TlsKeyResolver { + /// Resolve the certificate and key for a given host. This immediately spawns a task in the + /// background and is therefore cancellation-safe. + pub fn resolve( + &self, + sni: String, + ) -> impl Future> { + let mut cache = self.inner.cache.borrow_mut(); + let mut recv = match cache.get(&sni) { + None => { + eprintln!("send"); + let (tx, rx) = tokio::sync::broadcast::channel(1); + cache.insert(sni.clone(), TlsKeyState::Resolving(rx.resubscribe())); + _ = self.inner.resolution_tx.send((sni.clone(), tx)); + rx + } + Some(TlsKeyState::Resolving(recv)) => recv.resubscribe(), + Some(TlsKeyState::Resolved(res)) => { + return Either::Left(ready(res.clone().map_err(|_| anyhow!("Failed")))); + } + }; + drop(cache); + + // Make this cancellation safe + let inner = self.inner.clone(); + let handle = spawn(async move { + let res = recv.recv().await?; + let mut cache = inner.cache.borrow_mut(); + match cache.get(&sni) { + None | Some(TlsKeyState::Resolving(..)) => { + cache.insert(sni, TlsKeyState::Resolved(res.clone())); + } + Some(TlsKeyState::Resolved(..)) => { + // Someone beat us to it + } + } + Ok(res.map_err(|_| anyhow!("Failed"))?) + }); + Either::Right(async move { + let res = handle.await?; + res + }) + } +} + +pub struct TlsKeyLookup { + resolution_rx: RefCell< + tokio::sync::mpsc::UnboundedReceiver<( + String, + tokio::sync::broadcast::Sender>, + )>, + >, + pending: RefCell< + HashMap< + String, + tokio::sync::broadcast::Sender>, + >, + >, +} + +impl TlsKeyLookup { + /// Only one poll call may be active at any time. This method holds a `RefCell` lock. + pub async fn poll(&self) -> Option { + eprintln!("poll"); + if let Some((sni, sender)) = self.resolution_rx.borrow_mut().recv().await { + eprintln!("got {sni}"); + self.pending.borrow_mut().insert(sni.clone(), sender); + Some(sni) + } else { + None + } + } + + /// Resolve a previously polled item. + pub fn resolve(&self, sni: String, res: Result<(String, String), AnyError>) { + _ = self + .pending + .borrow_mut() + .remove(&sni) + .unwrap() + .send(res.map_err(|e| Rc::new(e))); + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use deno_core::unsync::spawn; + + #[tokio::test] + async fn test_resolve_once() { + let (resolver, lookup) = new_resolver(); + let task = spawn(async move { + while let Some(sni) = lookup.poll().await { + lookup.resolve( + sni.clone(), + Ok((format!("{sni}-cert"), format!("{sni}-key"))), + ); + } + }); + + let (cert, key) = resolver.resolve("example.com".to_owned()).await.unwrap(); + assert_eq!("example.com-cert", cert); + assert_eq!("example.com-key", key); + drop(resolver); + + task.await.unwrap(); + } + + #[tokio::test] + async fn test_resolve_concurrent() { + let (resolver, lookup) = new_resolver(); + let task = spawn(async move { + while let Some(sni) = lookup.poll().await { + lookup.resolve( + sni.clone(), + Ok((format!("{sni}-cert"), format!("{sni}-key"))), + ); + } + }); + + let f1 = resolver.resolve("example.com".to_owned()); + let f2 = resolver.resolve("example.com".to_owned()); + + let (cert, key) = f1.await.unwrap(); + assert_eq!("example.com-cert", cert); + assert_eq!("example.com-key", key); + let (cert, key) = f2.await.unwrap(); + assert_eq!("example.com-cert", cert); + assert_eq!("example.com-key", key); + drop(resolver); + + task.await.unwrap(); + } + + #[tokio::test] + async fn test_resolve_multiple_concurrent() { + let (resolver, lookup) = new_resolver(); + let task = spawn(async move { + while let Some(sni) = lookup.poll().await { + lookup.resolve( + sni.clone(), + Ok((format!("{sni}-cert"), format!("{sni}-key"))), + ); + } + }); + + let f1 = resolver.resolve("example1.com".to_owned()); + let f2 = resolver.resolve("example2.com".to_owned()); + + let (cert, key) = f1.await.unwrap(); + assert_eq!("example1.com-cert", cert); + assert_eq!("example1.com-key", key); + let (cert, key) = f2.await.unwrap(); + assert_eq!("example2.com-cert", cert); + assert_eq!("example2.com-key", key); + drop(resolver); + + task.await.unwrap(); + } +} diff --git a/ext/websocket/lib.rs b/ext/websocket/lib.rs index e4df9d3d35b80b..06a75faabd9b64 100644 --- a/ext/websocket/lib.rs +++ b/ext/websocket/lib.rs @@ -23,8 +23,10 @@ use deno_core::ToJsBuffer; use deno_net::raw::NetworkStream; use deno_tls::create_client_config; use deno_tls::rustls::ClientConfig; +use deno_tls::rustls::ClientConnection; use deno_tls::RootCertStoreProvider; use deno_tls::SocketUse; +use deno_tls::TlsKeys; use http::header::CONNECTION; use http::header::UPGRADE; use http::HeaderName; @@ -236,8 +238,7 @@ async fn handshake_http1_wss( ServerName::try_from(domain).map_err(|_| invalid_hostname(domain))?; let mut tls_connector = TlsStream::new_client_side( tcp_socket, - tls_config.into(), - dnsname, + ClientConnection::new(tls_config.into(), dnsname)?, NonZeroUsize::new(65536), ); // If we can bail on an http/1.1 ALPN mismatch here, we can avoid doing extra work @@ -261,8 +262,11 @@ async fn handshake_http2_wss( let dnsname = ServerName::try_from(domain).map_err(|_| invalid_hostname(domain))?; // We need to better expose the underlying errors here - let mut tls_connector = - TlsStream::new_client_side(tcp_socket, tls_config.into(), dnsname, None); + let mut tls_connector = TlsStream::new_client_side( + tcp_socket, + ClientConnection::new(tls_config.into(), dnsname)?, + None, + ); let handshake = tls_connector.handshake().await?; if handshake.alpn.is_none() { bail!("Didn't receive h2 alpn, aborting connection"); @@ -332,7 +336,7 @@ pub fn create_ws_client_config( root_cert_store, vec![], unsafely_ignore_certificate_errors, - None, + TlsKeys::Null, socket_use, ) } diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 27fe633ad441c0..5ec91da24cbfd2 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -47,6 +47,7 @@ use deno_io::Stdio; use deno_kv::dynamic::MultiBackendDbHandler; use deno_terminal::colors; use deno_tls::RootCertStoreProvider; +use deno_tls::TlsKeys; use deno_web::create_entangled_message_port; use deno_web::serialize_transferables; use deno_web::BlobStore; @@ -477,7 +478,7 @@ impl WebWorker { unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: None, + client_cert_chain_and_key: TlsKeys::Null, proxy: None, }, ), diff --git a/runtime/worker.rs b/runtime/worker.rs index ee6b256ff66a3b..d663c8dbf7aed7 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -39,6 +39,7 @@ use deno_http::DefaultHttpPropertyExtractor; use deno_io::Stdio; use deno_kv::dynamic::MultiBackendDbHandler; use deno_tls::RootCertStoreProvider; +use deno_tls::TlsKeys; use deno_web::BlobStore; use log::debug; @@ -449,7 +450,7 @@ impl MainWorker { unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: None, + client_cert_chain_and_key: TlsKeys::Null, proxy: None, }, ), From c09b83c251d61fba0c1924b9ed6c56cc67863b27 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 10:23:06 -0600 Subject: [PATCH 02/13] Hook up resolver --- Cargo.lock | 1 + ext/net/ops_tls.rs | 77 +++++++++++++++++++++------------- ext/tls/Cargo.toml | 1 + ext/tls/tls_key.rs | 64 ++++++++++++++++++++++++++++ tests/integration/run_tests.rs | 15 +++++-- 5 files changed, 126 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22125cd3554534..250db442272670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1911,6 +1911,7 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki", "serde", + "static_assertions", "tokio", "webpki-roots", ] diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index f43513e56fe6dc..f7d3b4783f3203 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -16,7 +16,9 @@ use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::invalid_hostname; use deno_core::error::AnyError; +use deno_core::futures::FutureExt; use deno_core::op2; +use deno_core::unsync::spawn; use deno_core::v8; use deno_core::AsyncRefCell; use deno_core::AsyncResult; @@ -30,6 +32,7 @@ use deno_tls::create_client_config; use deno_tls::load_certs; use deno_tls::load_private_keys; use deno_tls::new_resolver; +use deno_tls::rustls::server::ClientHello; use deno_tls::rustls::Certificate; use deno_tls::rustls::ClientConnection; use deno_tls::rustls::PrivateKey; @@ -39,6 +42,7 @@ use deno_tls::ServerConfigProvider; use deno_tls::SocketUse; use deno_tls::TlsKey; use deno_tls::TlsKeyLookup; +use deno_tls::TlsKeyResolver; use deno_tls::TlsKeys; use rustls_tokio_stream::TlsStreamRead; use rustls_tokio_stream::TlsStreamWrite; @@ -67,15 +71,26 @@ pub(crate) const TLS_BUFFER_SIZE: Option = pub struct TlsListener { pub(crate) tcp_listener: TcpListener, - pub(crate) tls_config: Arc, + pub(crate) tls_config: Option>, pub(crate) server_config_provider: Option, } impl TlsListener { pub async fn accept(&self) -> std::io::Result<(TlsStream, SocketAddr)> { let (tcp, addr) = self.tcp_listener.accept().await?; - let tls = - TlsStream::new_server_side(tcp, self.tls_config.clone(), TLS_BUFFER_SIZE); + let tls = if let Some(provider) = &self.server_config_provider { + TlsStream::new_server_side_acceptor( + tcp, + provider.clone(), + TLS_BUFFER_SIZE, + ) + } else { + TlsStream::new_server_side( + tcp, + self.tls_config.clone().unwrap(), + TLS_BUFFER_SIZE, + ) + }; Ok((tls, addr)) } pub fn local_addr(&self) -> std::io::Result { @@ -481,38 +496,44 @@ where .check_net(&(&addr.hostname, Some(addr.port)), "Deno.listenTls()")?; } - let tls_config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth(); - - let mut tls_config = match keys.clone() { - TlsKeys::Null => Err(anyhow!("Deno.listenTls requires a key")), - TlsKeys::Static(TlsKey(cert, key)) => tls_config - .with_single_cert(cert, key) - .map_err(|e| anyhow!(e)), - TlsKeys::Resolver(resolver) => unimplemented!(), - } - .map_err(|e| { - custom_error("InvalidData", "Error creating TLS certificate").context(e) - })?; - - if let Some(alpn_protocols) = args.alpn_protocols { - tls_config.alpn_protocols = - alpn_protocols.into_iter().map(|s| s.into_bytes()).collect(); - } - let bind_addr = resolve_addr_sync(&addr.hostname, addr.port)? .next() .ok_or_else(|| generic_error("No resolved address found"))?; let tcp_listener = TcpListener::bind_direct(bind_addr, args.reuse_port)?; let local_addr = tcp_listener.local_addr()?; + let alpn = args + .alpn_protocols + .unwrap_or_default() + .into_iter() + .map(|s| s.into_bytes()) + .collect(); + let listener = match keys.clone() { + TlsKeys::Null => Err(anyhow!("Deno.listenTls requires a key")), + TlsKeys::Static(TlsKey(cert, key)) => { + let mut tls_config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert, key) + .map_err(|e| anyhow!(e))?; + tls_config.alpn_protocols = alpn; + Ok(TlsListener { + tcp_listener, + tls_config: Some(tls_config.into()), + server_config_provider: None, + }) + } + TlsKeys::Resolver(resolver) => Ok(TlsListener { + tcp_listener, + tls_config: None, + server_config_provider: Some(resolver.into_server_config_provider(alpn)), + }), + } + .map_err(|e| { + custom_error("InvalidData", "Error creating TLS certificate").context(e) + })?; - let tls_listener_resource = NetworkListenerResource::new(TlsListener { - tcp_listener, - tls_config: tls_config.into(), - server_config_provider: None, - }); + let tls_listener_resource = NetworkListenerResource::new(listener); let rid = state.resource_table.add(tls_listener_resource); diff --git a/ext/tls/Cargo.toml b/ext/tls/Cargo.toml index b9ce34a8ff4e57..47a39efe2a14c7 100644 --- a/ext/tls/Cargo.toml +++ b/ext/tls/Cargo.toml @@ -22,5 +22,6 @@ rustls-pemfile.workspace = true rustls-tokio-stream.workspace = true rustls-webpki.workspace = true serde.workspace = true +static_assertions = "1" tokio.workspace = true webpki-roots.workspace = true diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index ce1f71d16f2c1c..771c04c078b90c 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -1,15 +1,24 @@ +use crate::load_certs; +use crate::load_private_keys; use crate::Certificate; use crate::PrivateKey; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::futures::future::Either; +use deno_core::futures::FutureExt; use deno_core::unsync::spawn; +use rustls::ServerConfig; +use rustls_tokio_stream::ServerConfigProvider; use std::cell::RefCell; use std::collections::HashMap; use std::fmt::Debug; use std::future::ready; use std::future::Future; +use std::io::BufReader; +use std::io::ErrorKind; use std::rc::Rc; +use std::sync::Arc; +use tokio::sync::oneshot; type ErrorType = Rc; @@ -66,6 +75,61 @@ pub struct TlsKeyResolver { inner: Rc, } +static_assertions::assert_impl_all!((Arc, std::io::Error): Send, Sync); + +impl TlsKeyResolver { + async fn resolve_internal( + &self, + sni: String, + alpn: Vec>, + ) -> Result, AnyError> { + let (cert, key) = self.resolve(sni).await?; + let cert = load_certs(&mut BufReader::new(cert.as_bytes()))?; + let key = load_private_keys(key.as_bytes())? + .into_iter() + .next() + .unwrap(); + + let mut tls_config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert, key)?; + tls_config.alpn_protocols = alpn; + Ok(tls_config.into()) + } + + pub fn into_server_config_provider( + self, + alpn: Vec>, + ) -> ServerConfigProvider { + let (tx, mut rx) = + tokio::sync::mpsc::unbounded_channel::<(_, oneshot::Sender<_>)>(); + + // We don't want to make the resolver multi-threaded, but the `ServerConfigProvider` is + // required to be wrapped in an Arc. To fix this, we spawn a task in our current runtime + // to respond to the requests. + spawn(async move { + while let Some((sni, txr)) = rx.recv().await { + _ = txr.send(self.resolve_internal(sni, alpn.clone()).await); + } + }); + + Arc::new(move |hello| { + // Take ownership of the SNI information + let sni = hello.server_name().unwrap_or_default().to_owned(); + let (txr, rxr) = tokio::sync::oneshot::channel::<_>(); + _ = tx.send((sni, txr)); + rxr + .map(|res| match res { + Err(e) => Err(std::io::Error::new(ErrorKind::InvalidData, e)), + Ok(Err(e)) => Err(std::io::Error::new(ErrorKind::InvalidData, e)), + Ok(Ok(res)) => Ok(res), + }) + .boxed() + }) + } +} + impl Debug for TlsKeyResolver { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TlsKeyResolver").finish() diff --git a/tests/integration/run_tests.rs b/tests/integration/run_tests.rs index 5e5d6935cb367c..4db5053ef2bfb9 100644 --- a/tests/integration/run_tests.rs +++ b/tests/integration/run_tests.rs @@ -13,6 +13,7 @@ use deno_core::serde_json::json; use deno_core::url; use deno_fetch::reqwest; use deno_tls::rustls; +use deno_tls::rustls::ClientConnection; use deno_tls::rustls_pemfile; use deno_tls::TlsStream; use pretty_assertions::assert_eq; @@ -5375,8 +5376,11 @@ async fn listen_tls_alpn() { let tcp_stream = tokio::net::TcpStream::connect("localhost:4504") .await .unwrap(); - let mut tls_stream = - TlsStream::new_client_side(tcp_stream, cfg, hostname, None); + let mut tls_stream = TlsStream::new_client_side( + tcp_stream, + ClientConnection::new(cfg, hostname).unwrap(), + None, + ); let handshake = tls_stream.handshake().await.unwrap(); @@ -5424,8 +5428,11 @@ async fn listen_tls_alpn_fail() { let tcp_stream = tokio::net::TcpStream::connect("localhost:4505") .await .unwrap(); - let mut tls_stream = - TlsStream::new_client_side(tcp_stream, cfg, hostname, None); + let mut tls_stream = TlsStream::new_client_side( + tcp_stream, + ClientConnection::new(cfg, hostname).unwrap(), + None, + ); tls_stream.handshake().await.unwrap_err(); From 074cc751197ccc8c1cdce91c2fe8ce3a112021ab Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 12:10:27 -0600 Subject: [PATCH 03/13] . --- ext/net/02_tls.js | 48 ++++++++++++++++- ext/net/ops_tls.rs | 48 ++++++++++------- ext/tls/tls_key.rs | 115 ++++++++++++++++++++--------------------- tests/unit/tls_test.ts | 51 ++++++++++++++++++ 4 files changed, 182 insertions(+), 80 deletions(-) diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 0b775047f622b9..dc82a9ae1397cc 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -6,6 +6,10 @@ import { op_net_accept_tls, op_net_connect_tls, op_net_listen_tls, + op_tls_cert_resolver_create, + op_tls_cert_resolver_poll, + op_tls_cert_resolver_resolve, + op_tls_cert_resolver_resolve_error, op_tls_handshake, op_tls_key_null, op_tls_key_static, @@ -87,9 +91,10 @@ async function connectTls({ keyFile, privateKey, }); + const serverName = arguments[0][serverNameSymbol] ?? null; const { 0: rid, 1: localAddr, 2: remoteAddr } = await op_net_connect_tls( { hostname, port }, - { certFile: deprecatedCertFile, caCerts, alpnProtocols }, + { certFile: deprecatedCertFile, caCerts, alpnProtocols, serverName }, keyPair, ); localAddr.transport = "tcp"; @@ -133,6 +138,10 @@ class TlsListener extends Listener { * interfaces. */ function hasTlsKeyPairOptions(options) { + // TODO(mmastrac): remove this temporary symbol when the API lands + if (options[resolverSymbol] !== undefined) { + return true; + } return (options.cert !== undefined || options.key !== undefined || options.certFile !== undefined || options.keyFile !== undefined || options.privateKey !== undefined || @@ -159,6 +168,12 @@ function loadTlsKeyPair(api, { privateKey = undefined; } + // TODO(mmastrac): remove this temporary symbol when the API lands + if (arguments[1][resolverSymbol] !== undefined) { + console.log("resolver"); + return createTlsKeyResolver(arguments[1][resolverSymbol]); + } + // Check for "pem" format if (keyFormat !== undefined && keyFormat !== "pem") { throw new TypeError('If `keyFormat` is specified, it must be "pem"'); @@ -275,6 +290,37 @@ async function startTls( return new TlsConn(rid, remoteAddr, localAddr); } +const resolverSymbol = Symbol("useResolver"); +const serverNameSymbol = Symbol("serverName"); + +function createTlsKeyResolver(callback) { + const { 0: resolver, 1: lookup } = op_tls_cert_resolver_create(); + (async () => { + while (true) { + const sni = await op_tls_cert_resolver_poll(lookup); + if (typeof sni !== "string") { + break; + } + try { + const key = callback(sni); + if (!hasTlsKeyPairOptions(key)) { + op_tls_cert_resolver_resolve_error(lookup, sni, "Invalid key"); + } else { + const resolved = loadTlsKeyPair("Deno.listenTls", key); + op_tls_cert_resolver_resolve(lookup, sni, resolved); + } + } catch (e) { + op_tls_cert_resolver_resolve_error(lookup, sni, e.message); + } + } + })(); + return resolver; +} + +internals.resolverSymbol = resolverSymbol; +internals.serverNameSymbol = serverNameSymbol; +internals.createTlsKeyResolver = createTlsKeyResolver; + export { connectTls, hasTlsKeyPairOptions, diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index f7d3b4783f3203..8572cedac1b282 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -11,14 +11,13 @@ use crate::DefaultTlsOptions; use crate::NetPermissions; use crate::UnsafelyIgnoreCertificateErrors; use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; use deno_core::error::bad_resource; use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::invalid_hostname; use deno_core::error::AnyError; -use deno_core::futures::FutureExt; use deno_core::op2; -use deno_core::unsync::spawn; use deno_core::v8; use deno_core::AsyncRefCell; use deno_core::AsyncResult; @@ -32,7 +31,6 @@ use deno_tls::create_client_config; use deno_tls::load_certs; use deno_tls::load_private_keys; use deno_tls::new_resolver; -use deno_tls::rustls::server::ClientHello; use deno_tls::rustls::Certificate; use deno_tls::rustls::ClientConnection; use deno_tls::rustls::PrivateKey; @@ -42,8 +40,8 @@ use deno_tls::ServerConfigProvider; use deno_tls::SocketUse; use deno_tls::TlsKey; use deno_tls::TlsKeyLookup; -use deno_tls::TlsKeyResolver; use deno_tls::TlsKeys; +use deno_tls::TlsKeysHolder; use rustls_tokio_stream::TlsStreamRead; use rustls_tokio_stream::TlsStreamWrite; use serde::Deserialize; @@ -184,6 +182,7 @@ pub struct ConnectTlsArgs { cert_file: Option, ca_certs: Vec, alpn_protocols: Option>, + server_name: Option, } #[derive(Deserialize)] @@ -199,7 +198,10 @@ pub struct StartTlsArgs { pub fn op_tls_key_null<'s>( scope: &mut v8::HandleScope<'s>, ) -> Result, AnyError> { - Ok(deno_core::cppgc::make_cppgc_object(scope, TlsKeys::Null)) + Ok(deno_core::cppgc::make_cppgc_object( + scope, + TlsKeysHolder::from(TlsKeys::Null), + )) } #[op2] @@ -215,7 +217,7 @@ pub fn op_tls_key_static<'s>( .unwrap(); Ok(deno_core::cppgc::make_cppgc_object( scope, - TlsKeys::Static(TlsKey(cert, key)), + TlsKeysHolder::from(TlsKeys::Static(TlsKey(cert, key))), )) } @@ -244,7 +246,7 @@ where .unwrap(); Ok(deno_core::cppgc::make_cppgc_object( scope, - TlsKeys::Static(TlsKey(cert, key)), + TlsKeysHolder::from(TlsKeys::Static(TlsKey(cert, key))), )) } @@ -253,8 +255,10 @@ pub fn op_tls_cert_resolver_create<'s>( scope: &mut v8::HandleScope<'s>, ) -> v8::Local<'s, v8::Array> { let (resolver, lookup) = new_resolver(); - let resolver = - deno_core::cppgc::make_cppgc_object(scope, TlsKeys::Resolver(resolver)); + let resolver = deno_core::cppgc::make_cppgc_object( + scope, + TlsKeysHolder::from(TlsKeys::Resolver(resolver)), + ); let lookup = deno_core::cppgc::make_cppgc_object(scope, lookup); v8::Array::new_with_elements(scope, &[resolver.into(), lookup.into()]) } @@ -271,10 +275,12 @@ pub async fn op_tls_cert_resolver_poll( pub fn op_tls_cert_resolver_resolve( #[cppgc] lookup: &TlsKeyLookup, #[string] sni: String, - #[string] cert: String, - #[string] key: String, -) { - lookup.resolve(sni, Ok((cert, key))) + #[cppgc] key: &TlsKeysHolder, +) -> Result<(), AnyError> { + let TlsKeys::Static(key) = key.take() else { + bail!("unexpected key type"); + }; + Ok(lookup.resolve(sni, Ok(key))) } #[op2(fast)] @@ -377,7 +383,7 @@ pub async fn op_net_connect_tls( state: Rc>, #[serde] addr: IpAddr, #[serde] args: ConnectTlsArgs, - #[cppgc] key_pair: &TlsKeys, + #[cppgc] key_pair: &TlsKeysHolder, ) -> Result<(ResourceId, IpAddr, IpAddr), AnyError> where NP: NetPermissions + 'static, @@ -414,8 +420,12 @@ where .borrow() .borrow::() .root_cert_store()?; - let hostname_dns = ServerName::try_from(&*addr.hostname) - .map_err(|_| invalid_hostname(&addr.hostname))?; + let hostname_dns = if let Some(server_name) = args.server_name { + ServerName::try_from(server_name.as_str()) + } else { + ServerName::try_from(&*addr.hostname) + } + .map_err(|_| invalid_hostname(&addr.hostname))?; let connect_addr = resolve_addr(&addr.hostname, addr.port) .await? .next() @@ -428,7 +438,7 @@ where root_cert_store, ca_certs, unsafely_ignore_certificate_errors, - key_pair.clone(), + key_pair.take(), SocketUse::GeneralSsl, )?; @@ -481,7 +491,7 @@ pub fn op_net_listen_tls( state: &mut OpState, #[serde] addr: IpAddr, #[serde] args: ListenTlsArgs, - #[cppgc] keys: &TlsKeys, + #[cppgc] keys: &TlsKeysHolder, ) -> Result<(ResourceId, IpAddr), AnyError> where NP: NetPermissions + 'static, @@ -508,7 +518,7 @@ where .into_iter() .map(|s| s.into_bytes()) .collect(); - let listener = match keys.clone() { + let listener = match keys.take() { TlsKeys::Null => Err(anyhow!("Deno.listenTls requires a key")), TlsKeys::Static(TlsKey(cert, key)) => { let mut tls_config = ServerConfig::builder() diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index 771c04c078b90c..7682259380a16c 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -1,5 +1,3 @@ -use crate::load_certs; -use crate::load_private_keys; use crate::Certificate; use crate::PrivateKey; use deno_core::anyhow::anyhow; @@ -14,16 +12,17 @@ use std::collections::HashMap; use std::fmt::Debug; use std::future::ready; use std::future::Future; -use std::io::BufReader; use std::io::ErrorKind; use std::rc::Rc; use std::sync::Arc; +use tokio::sync::broadcast; +use tokio::sync::mpsc; use tokio::sync::oneshot; type ErrorType = Rc; /// A TLS certificate/private key pair. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TlsKey(pub Vec, pub PrivateKey); #[derive(Clone, Debug, Default)] @@ -35,6 +34,20 @@ pub enum TlsKeys { Resolver(TlsKeyResolver), } +pub struct TlsKeysHolder(RefCell); + +impl TlsKeysHolder { + pub fn take(&self) -> TlsKeys { + std::mem::take(&mut *self.0.borrow_mut()) + } +} + +impl From for TlsKeysHolder { + fn from(value: TlsKeys) -> Self { + TlsKeysHolder(RefCell::new(value)) + } +} + impl TryInto> for TlsKeys { type Error = Self; fn try_into(self) -> Result, Self::Error> { @@ -56,16 +69,14 @@ impl From> for TlsKeys { } enum TlsKeyState { - Resolving( - tokio::sync::broadcast::Receiver>, - ), - Resolved(Result<(String, String), ErrorType>), + Resolving(broadcast::Receiver>), + Resolved(Result), } struct TlsKeyResolverInner { - resolution_tx: tokio::sync::mpsc::UnboundedSender<( + resolution_tx: mpsc::UnboundedSender<( String, - tokio::sync::broadcast::Sender>, + broadcast::Sender>, )>, cache: RefCell>, } @@ -75,25 +86,18 @@ pub struct TlsKeyResolver { inner: Rc, } -static_assertions::assert_impl_all!((Arc, std::io::Error): Send, Sync); - impl TlsKeyResolver { async fn resolve_internal( &self, sni: String, alpn: Vec>, ) -> Result, AnyError> { - let (cert, key) = self.resolve(sni).await?; - let cert = load_certs(&mut BufReader::new(cert.as_bytes()))?; - let key = load_private_keys(key.as_bytes())? - .into_iter() - .next() - .unwrap(); + let key = self.resolve(sni).await?; let mut tls_config = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() - .with_single_cert(cert, key)?; + .with_single_cert(key.0, key.1)?; tls_config.alpn_protocols = alpn; Ok(tls_config.into()) } @@ -102,8 +106,7 @@ impl TlsKeyResolver { self, alpn: Vec>, ) -> ServerConfigProvider { - let (tx, mut rx) = - tokio::sync::mpsc::unbounded_channel::<(_, oneshot::Sender<_>)>(); + let (tx, mut rx) = mpsc::unbounded_channel::<(_, oneshot::Sender<_>)>(); // We don't want to make the resolver multi-threaded, but the `ServerConfigProvider` is // required to be wrapped in an Arc. To fix this, we spawn a task in our current runtime @@ -137,7 +140,7 @@ impl Debug for TlsKeyResolver { } pub fn new_resolver() -> (TlsKeyResolver, TlsKeyLookup) { - let (resolution_tx, resolution_rx) = tokio::sync::mpsc::unbounded_channel(); + let (resolution_tx, resolution_rx) = mpsc::unbounded_channel(); ( TlsKeyResolver { inner: Rc::new(TlsKeyResolverInner { @@ -158,12 +161,12 @@ impl TlsKeyResolver { pub fn resolve( &self, sni: String, - ) -> impl Future> { + ) -> impl Future> { let mut cache = self.inner.cache.borrow_mut(); let mut recv = match cache.get(&sni) { None => { eprintln!("send"); - let (tx, rx) = tokio::sync::broadcast::channel(1); + let (tx, rx) = broadcast::channel(1); cache.insert(sni.clone(), TlsKeyState::Resolving(rx.resubscribe())); _ = self.inner.resolution_tx.send((sni.clone(), tx)); rx @@ -199,17 +202,13 @@ impl TlsKeyResolver { pub struct TlsKeyLookup { resolution_rx: RefCell< - tokio::sync::mpsc::UnboundedReceiver<( + mpsc::UnboundedReceiver<( String, - tokio::sync::broadcast::Sender>, + broadcast::Sender>, )>, >, - pending: RefCell< - HashMap< - String, - tokio::sync::broadcast::Sender>, - >, - >, + pending: + RefCell>>>, } impl TlsKeyLookup { @@ -226,7 +225,8 @@ impl TlsKeyLookup { } /// Resolve a previously polled item. - pub fn resolve(&self, sni: String, res: Result<(String, String), AnyError>) { + pub fn resolve(&self, sni: String, res: Result) { + eprintln!("resolved {sni}"); _ = self .pending .borrow_mut() @@ -240,22 +240,27 @@ impl TlsKeyLookup { pub mod tests { use super::*; use deno_core::unsync::spawn; + use rustls::Certificate; + use rustls::PrivateKey; + + fn tls_key_for_test(sni: &str) -> TlsKey { + TlsKey( + vec![Certificate(format!("{sni}-cert").into_bytes())], + PrivateKey(format!("{sni}-key").into_bytes()), + ) + } #[tokio::test] async fn test_resolve_once() { let (resolver, lookup) = new_resolver(); let task = spawn(async move { while let Some(sni) = lookup.poll().await { - lookup.resolve( - sni.clone(), - Ok((format!("{sni}-cert"), format!("{sni}-key"))), - ); + lookup.resolve(sni.clone(), Ok(tls_key_for_test(&sni))); } }); - let (cert, key) = resolver.resolve("example.com".to_owned()).await.unwrap(); - assert_eq!("example.com-cert", cert); - assert_eq!("example.com-key", key); + let key = resolver.resolve("example.com".to_owned()).await.unwrap(); + assert_eq!(tls_key_for_test("example.com"), key); drop(resolver); task.await.unwrap(); @@ -266,22 +271,17 @@ pub mod tests { let (resolver, lookup) = new_resolver(); let task = spawn(async move { while let Some(sni) = lookup.poll().await { - lookup.resolve( - sni.clone(), - Ok((format!("{sni}-cert"), format!("{sni}-key"))), - ); + lookup.resolve(sni.clone(), Ok(tls_key_for_test(&sni))); } }); let f1 = resolver.resolve("example.com".to_owned()); let f2 = resolver.resolve("example.com".to_owned()); - let (cert, key) = f1.await.unwrap(); - assert_eq!("example.com-cert", cert); - assert_eq!("example.com-key", key); - let (cert, key) = f2.await.unwrap(); - assert_eq!("example.com-cert", cert); - assert_eq!("example.com-key", key); + let key = f1.await.unwrap(); + assert_eq!(tls_key_for_test("example.com"), key); + let key = f2.await.unwrap(); + assert_eq!(tls_key_for_test("example.com"), key); drop(resolver); task.await.unwrap(); @@ -292,22 +292,17 @@ pub mod tests { let (resolver, lookup) = new_resolver(); let task = spawn(async move { while let Some(sni) = lookup.poll().await { - lookup.resolve( - sni.clone(), - Ok((format!("{sni}-cert"), format!("{sni}-key"))), - ); + lookup.resolve(sni.clone(), Ok(tls_key_for_test(&sni))); } }); let f1 = resolver.resolve("example1.com".to_owned()); let f2 = resolver.resolve("example2.com".to_owned()); - let (cert, key) = f1.await.unwrap(); - assert_eq!("example1.com-cert", cert); - assert_eq!("example1.com-key", key); - let (cert, key) = f2.await.unwrap(); - assert_eq!("example2.com-cert", cert); - assert_eq!("example2.com-key", key); + let key = f1.await.unwrap(); + assert_eq!(tls_key_for_test("example.com"), key); + let key = f2.await.unwrap(); + assert_eq!(tls_key_for_test("example.com"), key); drop(resolver); task.await.unwrap(); diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 5be05b73e3fb53..8c597a96472914 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -11,6 +11,8 @@ import { BufReader, BufWriter } from "@std/io/mod.ts"; import { readAll } from "@std/io/read_all.ts"; import { writeAll } from "@std/io/write_all.ts"; import { TextProtoReader } from "../testdata/run/textproto.ts"; +// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol +const { resolverSymbol, serverNameSymbol } = Deno[Deno.internal]; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -1646,3 +1648,52 @@ Deno.test( listener.close(); }, ); + +Deno.test( + { permissions: { net: true, read: true } }, + async function listenResolver() { + const listener = Deno.listenTls({ + hostname: "localhost", + port: 0, + [resolverSymbol]: (sni: string) => { + console.log(sni); + return { + cert, + key, + }; + }, + }); + + { + const conn = await Deno.connectTls({ + hostname: "localhost", + [serverNameSymbol]: "server-1", + port: listener.addr.port, + }); + const [handshake, serverConn] = await Promise.all([ + conn.handshake(), + listener.accept(), + ]); + console.log("connected", handshake, serverConn); + conn.close(); + serverConn.close(); + } + + { + const conn = await Deno.connectTls({ + hostname: "localhost", + [serverNameSymbol]: "server-2", + port: listener.addr.port, + }); + const [handshake, serverConn] = await Promise.all([ + conn.handshake(), + listener.accept(), + ]); + console.log("connected", handshake, serverConn); + conn.close(); + serverConn.close(); + } + + listener.close(); + }, +); From 144797d474f3f041942084a159de78d4b86d0238 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 12:36:44 -0600 Subject: [PATCH 04/13] . --- ext/net/02_tls.js | 1 - ext/tls/lib.rs | 12 +++++------- ext/tls/tls_key.rs | 4 ---- tests/unit/tls_test.ts | 10 +++++----- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index dc82a9ae1397cc..3f0ff8f39dea4c 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -170,7 +170,6 @@ function loadTlsKeyPair(api, { // TODO(mmastrac): remove this temporary symbol when the API lands if (arguments[1][resolverSymbol] !== undefined) { - console.log("resolver"); return createTlsKeyResolver(arguments[1][resolverSymbol]); } diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index 4c487aff99fd8b..5122264bf179ec 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -192,14 +192,12 @@ pub fn create_client_config( // However it's not really feasible to deduplicate it as the `client_config` instances // are not type-compatible - one wants "client cert", the other wants "transparency policy // or client cert". - let mut client = if let TlsKeys::Static(TlsKey(cert_chain, private_key)) = - maybe_cert_chain_and_key - { - client_config + let mut client = match maybe_cert_chain_and_key { + TlsKeys::Static(TlsKey(cert_chain, private_key)) => client_config .with_client_auth_cert(cert_chain, private_key) - .expect("invalid client key or certificate") - } else { - client_config.with_no_client_auth() + .expect("invalid client key or certificate"), + TlsKeys::Null => client_config.with_no_client_auth(), + TlsKeys::Resolver(_) => unimplemented!(), }; add_alpn(&mut client, socket_use); diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index 7682259380a16c..b5f9009cf3b7a5 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -165,7 +165,6 @@ impl TlsKeyResolver { let mut cache = self.inner.cache.borrow_mut(); let mut recv = match cache.get(&sni) { None => { - eprintln!("send"); let (tx, rx) = broadcast::channel(1); cache.insert(sni.clone(), TlsKeyState::Resolving(rx.resubscribe())); _ = self.inner.resolution_tx.send((sni.clone(), tx)); @@ -214,9 +213,7 @@ pub struct TlsKeyLookup { impl TlsKeyLookup { /// Only one poll call may be active at any time. This method holds a `RefCell` lock. pub async fn poll(&self) -> Option { - eprintln!("poll"); if let Some((sni, sender)) = self.resolution_rx.borrow_mut().recv().await { - eprintln!("got {sni}"); self.pending.borrow_mut().insert(sni.clone(), sender); Some(sni) } else { @@ -226,7 +223,6 @@ impl TlsKeyLookup { /// Resolve a previously polled item. pub fn resolve(&self, sni: String, res: Result) { - eprintln!("resolved {sni}"); _ = self .pending .borrow_mut() diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 8c597a96472914..67f510d2a0cb05 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -1652,11 +1652,12 @@ Deno.test( Deno.test( { permissions: { net: true, read: true } }, async function listenResolver() { + let sniRequests = []; const listener = Deno.listenTls({ hostname: "localhost", port: 0, [resolverSymbol]: (sni: string) => { - console.log(sni); + sniRequests.push(sni); return { cert, key, @@ -1670,11 +1671,10 @@ Deno.test( [serverNameSymbol]: "server-1", port: listener.addr.port, }); - const [handshake, serverConn] = await Promise.all([ + const [_handshake, serverConn] = await Promise.all([ conn.handshake(), listener.accept(), ]); - console.log("connected", handshake, serverConn); conn.close(); serverConn.close(); } @@ -1685,15 +1685,15 @@ Deno.test( [serverNameSymbol]: "server-2", port: listener.addr.port, }); - const [handshake, serverConn] = await Promise.all([ + const [_handshake, serverConn] = await Promise.all([ conn.handshake(), listener.accept(), ]); - console.log("connected", handshake, serverConn); conn.close(); serverConn.close(); } + assertEquals(sniRequests, ["server-1", "server-2"]); listener.close(); }, ); From 99e3f1c7168c36836c7e9ed1df2e0d743ed07a79 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 15:22:33 -0600 Subject: [PATCH 05/13] . --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- ext/net/02_tls.js | 2 ++ tests/unit/tls_test.ts | 53 +++++++++++++++++++++--------------------- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 250db442272670..a1a99d103f5119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5457,9 +5457,9 @@ dependencies = [ [[package]] name = "rustls-tokio-stream" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a600f217ae82dd9430c546cb4742ddf9391893146fc614ba5bda93a71029701a" +checksum = "c478c030dfd68498e6c59168d9eec4f8bead33152a5f3095ad4bdbdcea09d466" dependencies = [ "futures", "rustls", diff --git a/Cargo.toml b/Cargo.toml index 43a23674f62e77..75c94e5f178cb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ ring = "^0.17.0" rusqlite = { version = "=0.29.0", features = ["unlock_notify", "bundled"] } rustls = "0.21.11" rustls-pemfile = "1.0.0" -rustls-tokio-stream = "=0.2.21" +rustls-tokio-stream = "=0.2.23" rustls-webpki = "0.101.4" rustyline = "=13.0.0" saffron = "=0.1.0" diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 3f0ff8f39dea4c..92df2b56f19c24 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -91,6 +91,8 @@ async function connectTls({ keyFile, privateKey, }); + // TODO(mmastrac): We only expose this feature via symbol for now. This should actually be a feature + // in Deno.connectTls, however. const serverName = arguments[0][serverNameSymbol] ?? null; const { 0: rid, 1: localAddr, 2: remoteAddr } = await op_net_connect_tls( { hostname, port }, diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 67f510d2a0cb05..98f70ffe16f5a5 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -18,6 +18,8 @@ const encoder = new TextEncoder(); const decoder = new TextDecoder(); const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt"); const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key"); +const certEcc = Deno.readTextFileSync("tests/testdata/tls/localhost_ecc.crt"); +const keyEcc = Deno.readTextFileSync("tests/testdata/tls/localhost_ecc.key"); const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")]; async function sleep(msec: number) { @@ -1652,48 +1654,47 @@ Deno.test( Deno.test( { permissions: { net: true, read: true } }, async function listenResolver() { - let sniRequests = []; + let sniRequests: string[] = []; + const keys = { + "server-1": { cert, key }, + "server-2": { cert: certEcc, key: keyEcc }, + "fail-server-3": { cert: "(invalid)", key: "(bad)" }, + }; const listener = Deno.listenTls({ hostname: "localhost", port: 0, [resolverSymbol]: (sni: string) => { sniRequests.push(sni); - return { - cert, - key, - }; + return keys[sni]!; }, }); - { + for ( + const server of ["server-1", "server-2", "fail-server-3", "fail-server-4"] + ) { const conn = await Deno.connectTls({ hostname: "localhost", - [serverNameSymbol]: "server-1", + [serverNameSymbol]: server, port: listener.addr.port, }); - const [_handshake, serverConn] = await Promise.all([ - conn.handshake(), - listener.accept(), - ]); - conn.close(); - serverConn.close(); - } - - { - const conn = await Deno.connectTls({ - hostname: "localhost", - [serverNameSymbol]: "server-2", - port: listener.addr.port, - }); - const [_handshake, serverConn] = await Promise.all([ - conn.handshake(), - listener.accept(), - ]); + const serverConn = await listener.accept(); + if (server.startsWith("fail-")) { + await assertRejects(async () => await conn.handshake()); + await assertRejects(async () => await serverConn.handshake()); + } else { + await conn.handshake(); + await serverConn.handshake(); + } conn.close(); serverConn.close(); } - assertEquals(sniRequests, ["server-1", "server-2"]); + assertEquals(sniRequests, [ + "server-1", + "server-2", + "fail-server-3", + "fail-server-4", + ]); listener.close(); }, ); From c1db8ee6f2b6bb82c10372d239689b9d34f5c8f7 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 15:33:10 -0600 Subject: [PATCH 06/13] . --- Cargo.lock | 1 - ext/tls/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1a99d103f5119..b825ddb2bdfa20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1911,7 +1911,6 @@ dependencies = [ "rustls-tokio-stream", "rustls-webpki", "serde", - "static_assertions", "tokio", "webpki-roots", ] diff --git a/ext/tls/Cargo.toml b/ext/tls/Cargo.toml index 47a39efe2a14c7..b9ce34a8ff4e57 100644 --- a/ext/tls/Cargo.toml +++ b/ext/tls/Cargo.toml @@ -22,6 +22,5 @@ rustls-pemfile.workspace = true rustls-tokio-stream.workspace = true rustls-webpki.workspace = true serde.workspace = true -static_assertions = "1" tokio.workspace = true webpki-roots.workspace = true From 940b241abd10257fb78ca8f374ae00465f99b20e Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 15:45:40 -0600 Subject: [PATCH 07/13] . --- ext/fetch/lib.rs | 4 +++- ext/net/02_tls.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 416ce8c6405efe..f3025897628362 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -205,7 +205,9 @@ pub fn create_client_from_options( unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: Some(options.client_cert_chain_and_key.clone()), + client_cert_chain_and_key: Some( + options.client_cert_chain_and_key.clone(), + ), pool_max_idle_per_host: None, pool_idle_timeout: None, http1: true, diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 92df2b56f19c24..24c9d525bcb8c2 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -303,7 +303,7 @@ function createTlsKeyResolver(callback) { break; } try { - const key = callback(sni); + const key = await callback(sni); if (!hasTlsKeyPairOptions(key)) { op_tls_cert_resolver_resolve_error(lookup, sni, "Invalid key"); } else { From 6144136f318dac1915f8cd39e535480da587017c Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 17:00:43 -0600 Subject: [PATCH 08/13] . --- ext/fetch/lib.rs | 8 +++++--- ext/net/02_tls.js | 5 +++-- ext/net/ops_tls.rs | 3 ++- ext/tls/tls_key.rs | 16 +++++++++------- tests/unit/tls_test.ts | 2 +- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index f3025897628362..9c9339862c15d5 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -205,9 +205,11 @@ pub fn create_client_from_options( unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: Some( - options.client_cert_chain_and_key.clone(), - ), + client_cert_chain_and_key: options + .client_cert_chain_and_key + .clone() + .try_into() + .unwrap_or_default(), pool_max_idle_per_host: None, pool_idle_timeout: None, http1: true, diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 24c9d525bcb8c2..e51df7424a8e6d 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -20,6 +20,7 @@ const { Number, ObjectDefineProperty, TypeError, + SymbolFor, } = primordials; import { Conn, Listener } from "ext:deno_net/01_net.js"; @@ -291,8 +292,8 @@ async function startTls( return new TlsConn(rid, remoteAddr, localAddr); } -const resolverSymbol = Symbol("useResolver"); -const serverNameSymbol = Symbol("serverName"); +const resolverSymbol = SymbolFor("unstableSniResolver"); +const serverNameSymbol = SymbolFor("unstableServerName"); function createTlsKeyResolver(callback) { const { 0: resolver, 1: lookup } = op_tls_cert_resolver_create(); diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 8572cedac1b282..c529859087c578 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -280,7 +280,8 @@ pub fn op_tls_cert_resolver_resolve( let TlsKeys::Static(key) = key.take() else { bail!("unexpected key type"); }; - Ok(lookup.resolve(sni, Ok(key))) + lookup.resolve(sni, Ok(key)); + Ok(()) } #[op2(fast)] diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index b5f9009cf3b7a5..79c1fab7cd54e0 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -1,7 +1,9 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::Certificate; use crate::PrivateKey; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; +use deno_core::futures::future::poll_fn; use deno_core::futures::future::Either; use deno_core::futures::FutureExt; use deno_core::unsync::spawn; @@ -190,16 +192,14 @@ impl TlsKeyResolver { // Someone beat us to it } } - Ok(res.map_err(|_| anyhow!("Failed"))?) + res.map_err(|_| anyhow!("Failed")) }); - Either::Right(async move { - let res = handle.await?; - res - }) + Either::Right(async move { handle.await? }) } } pub struct TlsKeyLookup { + #[allow(clippy::type_complexity)] resolution_rx: RefCell< mpsc::UnboundedReceiver<( String, @@ -213,7 +213,9 @@ pub struct TlsKeyLookup { impl TlsKeyLookup { /// Only one poll call may be active at any time. This method holds a `RefCell` lock. pub async fn poll(&self) -> Option { - if let Some((sni, sender)) = self.resolution_rx.borrow_mut().recv().await { + if let Some((sni, sender)) = + poll_fn(|cx| self.resolution_rx.borrow_mut().poll_recv(cx)).await + { self.pending.borrow_mut().insert(sni.clone(), sender); Some(sni) } else { @@ -228,7 +230,7 @@ impl TlsKeyLookup { .borrow_mut() .remove(&sni) .unwrap() - .send(res.map_err(|e| Rc::new(e))); + .send(res.map_err(Rc::new)); } } diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 98f70ffe16f5a5..40ba61a6f284d9 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -1654,7 +1654,7 @@ Deno.test( Deno.test( { permissions: { net: true, read: true } }, async function listenResolver() { - let sniRequests: string[] = []; + const sniRequests: string[] = []; const keys = { "server-1": { cert, key }, "server-2": { cert: certEcc, key: keyEcc }, From 14f87e5cdfcb97d6e92d56f8da1d68b6cc067b3c Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 17:24:02 -0600 Subject: [PATCH 09/13] . --- ext/tls/tls_key.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index 79c1fab7cd54e0..5b71b957914377 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -298,9 +298,9 @@ pub mod tests { let f2 = resolver.resolve("example2.com".to_owned()); let key = f1.await.unwrap(); - assert_eq!(tls_key_for_test("example.com"), key); + assert_eq!(tls_key_for_test("example1.com"), key); let key = f2.await.unwrap(); - assert_eq!(tls_key_for_test("example.com"), key); + assert_eq!(tls_key_for_test("example2.com"), key); drop(resolver); task.await.unwrap(); From 811933367a76eaf29b7eab5daa2496f7486b8c48 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 18:06:45 -0600 Subject: [PATCH 10/13] . --- ext/fetch/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 9c9339862c15d5..21ca040277e33f 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -46,6 +46,7 @@ use deno_tls::RootCertStoreProvider; use data_url::DataUrl; use deno_tls::TlsKey; use deno_tls::TlsKeys; +use deno_tls::TlsKeysHolder; use http_v02::header::CONTENT_LENGTH; use http_v02::Uri; use reqwest::header::HeaderMap; @@ -825,7 +826,7 @@ fn default_true() -> bool { pub fn op_fetch_custom_client( state: &mut OpState, #[serde] args: CreateHttpClientArgs, - #[cppgc] tls_keys: &deno_tls::TlsKeys, + #[cppgc] tls_keys: &TlsKeysHolder, ) -> Result where FP: FetchPermissions + 'static, @@ -852,7 +853,7 @@ where unsafely_ignore_certificate_errors: options .unsafely_ignore_certificate_errors .clone(), - client_cert_chain_and_key: tls_keys.clone().try_into().unwrap(), + client_cert_chain_and_key: tls_keys.take().try_into().unwrap(), pool_max_idle_per_host: args.pool_max_idle_per_host, pool_idle_timeout: args.pool_idle_timeout.and_then( |timeout| match timeout { From 0d4d886720a2de3c08239072c78161f984a3883f Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 18:19:43 -0600 Subject: [PATCH 11/13] . --- tests/integration/js_unit_tests.rs | 3 +- tests/unit/tls_sni_test.ts | 60 ++++++++++++++++++++++++++++++ tests/unit/tls_test.ts | 50 ------------------------- 3 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 tests/unit/tls_sni_test.ts diff --git a/tests/integration/js_unit_tests.rs b/tests/integration/js_unit_tests.rs index 2bf78034e944b7..cbae4a0b8c4518 100644 --- a/tests/integration/js_unit_tests.rs +++ b/tests/integration/js_unit_tests.rs @@ -94,6 +94,7 @@ util::unit_test_factory!( text_encoding_test, timers_test, tls_test, + tls_sni_test, truncate_test, tty_color_test, tty_test, @@ -129,7 +130,7 @@ fn js_unit_test(test: String) { .arg("--no-prompt"); // TODO(mmastrac): it would be better to just load a test CA for all tests - let deno = if test == "websocket_test" { + let deno = if test == "websocket_test" || test == "tls_sni_test" { deno.arg("--unsafely-ignore-certificate-errors") } else { deno diff --git a/tests/unit/tls_sni_test.ts b/tests/unit/tls_sni_test.ts new file mode 100644 index 00000000000000..404f8016e3a2bc --- /dev/null +++ b/tests/unit/tls_sni_test.ts @@ -0,0 +1,60 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { assertEquals, assertRejects } from "./test_util.ts"; +// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol +const { resolverSymbol, serverNameSymbol } = Deno[Deno.internal]; + +const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt"); +const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key"); +const certEcc = Deno.readTextFileSync("tests/testdata/tls/localhost_ecc.crt"); +const keyEcc = Deno.readTextFileSync("tests/testdata/tls/localhost_ecc.key"); + +Deno.test( + { permissions: { net: true, read: true } }, + async function listenResolver() { + const sniRequests: string[] = []; + const keys: Record = { + "server-1": { cert, key }, + "server-2": { cert: certEcc, key: keyEcc }, + "fail-server-3": { cert: "(invalid)", key: "(bad)" }, + }; + const opts: unknown = { + hostname: "localhost", + port: 0, + [resolverSymbol]: (sni: string) => { + sniRequests.push(sni); + return keys[sni]!; + }, + }; + const listener = Deno.listenTls( + opts, + ); + + for ( + const server of ["server-1", "server-2", "fail-server-3", "fail-server-4"] + ) { + const conn = await Deno.connectTls({ + hostname: "localhost", + [serverNameSymbol]: server, + port: listener.addr.port, + }); + const serverConn = await listener.accept(); + if (server.startsWith("fail-")) { + await assertRejects(async () => await conn.handshake()); + await assertRejects(async () => await serverConn.handshake()); + } else { + await conn.handshake(); + await serverConn.handshake(); + } + conn.close(); + serverConn.close(); + } + + assertEquals(sniRequests, [ + "server-1", + "server-2", + "fail-server-3", + "fail-server-4", + ]); + listener.close(); + }, +); diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 40ba61a6f284d9..6ac4e14a56dff1 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -11,8 +11,6 @@ import { BufReader, BufWriter } from "@std/io/mod.ts"; import { readAll } from "@std/io/read_all.ts"; import { writeAll } from "@std/io/write_all.ts"; import { TextProtoReader } from "../testdata/run/textproto.ts"; -// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol -const { resolverSymbol, serverNameSymbol } = Deno[Deno.internal]; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -1650,51 +1648,3 @@ Deno.test( listener.close(); }, ); - -Deno.test( - { permissions: { net: true, read: true } }, - async function listenResolver() { - const sniRequests: string[] = []; - const keys = { - "server-1": { cert, key }, - "server-2": { cert: certEcc, key: keyEcc }, - "fail-server-3": { cert: "(invalid)", key: "(bad)" }, - }; - const listener = Deno.listenTls({ - hostname: "localhost", - port: 0, - [resolverSymbol]: (sni: string) => { - sniRequests.push(sni); - return keys[sni]!; - }, - }); - - for ( - const server of ["server-1", "server-2", "fail-server-3", "fail-server-4"] - ) { - const conn = await Deno.connectTls({ - hostname: "localhost", - [serverNameSymbol]: server, - port: listener.addr.port, - }); - const serverConn = await listener.accept(); - if (server.startsWith("fail-")) { - await assertRejects(async () => await conn.handshake()); - await assertRejects(async () => await serverConn.handshake()); - } else { - await conn.handshake(); - await serverConn.handshake(); - } - conn.close(); - serverConn.close(); - } - - assertEquals(sniRequests, [ - "server-1", - "server-2", - "fail-server-3", - "fail-server-4", - ]); - listener.close(); - }, -); From 3ce35b7d29b8c5fc56916d24d881953c1caf1461 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Mon, 6 May 2024 19:02:43 -0600 Subject: [PATCH 12/13] docs --- ext/tls/tls_key.rs | 12 ++++++++++++ tests/unit/tls_test.ts | 2 -- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index 5b71b957914377..7abc326da99663 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -1,4 +1,16 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +//! These represent the various types of TLS keys we support for both client and server +//! connections. +//! +//! A TLS key will most often be static, and will loaded from a certificate and key file +//! or string. These are represented by `TlsKey`, which is stored in `TlsKeys::Static`. +//! +//! In more complex cases, you may need a `TlsKeyResolver`/`TlsKeyLookup` pair, which +//! requires polling of the `TlsKeyLookup` lookup queue. The underlying channels that used for +//! key lookup can handle closing one end of the pair, in which case they will just +//! attempt to clean up the associated resources. + use crate::Certificate; use crate::PrivateKey; use deno_core::anyhow::anyhow; diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 6ac4e14a56dff1..5be05b73e3fb53 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -16,8 +16,6 @@ const encoder = new TextEncoder(); const decoder = new TextDecoder(); const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt"); const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key"); -const certEcc = Deno.readTextFileSync("tests/testdata/tls/localhost_ecc.crt"); -const keyEcc = Deno.readTextFileSync("tests/testdata/tls/localhost_ecc.key"); const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")]; async function sleep(msec: number) { From 179b6e8a009f7c98b8d26db33bb6f9afc671b7d8 Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Thu, 9 May 2024 10:06:54 -0600 Subject: [PATCH 13/13] Update ext/tls/tls_key.rs Signed-off-by: Matt Mastracci --- ext/tls/tls_key.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/tls/tls_key.rs b/ext/tls/tls_key.rs index 7abc326da99663..18064a91a05155 100644 --- a/ext/tls/tls_key.rs +++ b/ext/tls/tls_key.rs @@ -223,7 +223,8 @@ pub struct TlsKeyLookup { } impl TlsKeyLookup { - /// Only one poll call may be active at any time. This method holds a `RefCell` lock. + /// Multiple `poll` calls are safe, but this method is not starvation-safe. Generally + /// only one `poll`er should be active at any time. pub async fn poll(&self) -> Option { if let Some((sni, sender)) = poll_fn(|cx| self.resolution_rx.borrow_mut().poll_recv(cx)).await