diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2021-08-09 15:55:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-09 15:55:00 +0200 |
commit | 3ab50b355141f744a0acec1a5cc3b3b95247d4b1 (patch) | |
tree | 5ab6c3a216f5ce5cc5ee8fbc12e99dfac09496d7 /extensions/net | |
parent | f402904e6e227ee60a88d991cb9818d5340f5c1d (diff) |
feat: support client certificates for connectTls (#11598)
Co-authored-by: Daniel Lamando <dan@danopia.net>
Co-authored-by: Erik Price <github@erikprice.net>
Diffstat (limited to 'extensions/net')
-rw-r--r-- | extensions/net/02_tls.js | 4 | ||||
-rw-r--r-- | extensions/net/lib.deno_net.d.ts | 5 | ||||
-rw-r--r-- | extensions/net/lib.deno_net.unstable.d.ts | 26 | ||||
-rw-r--r-- | extensions/net/ops_tls.rs | 78 |
4 files changed, 93 insertions, 20 deletions
diff --git a/extensions/net/02_tls.js b/extensions/net/02_tls.js index 4fafe9079..343ec2e4f 100644 --- a/extensions/net/02_tls.js +++ b/extensions/net/02_tls.js @@ -28,12 +28,16 @@ hostname = "127.0.0.1", transport = "tcp", certFile = undefined, + certChain = undefined, + privateKey = undefined, }) { const res = await opConnectTls({ port, hostname, transport, certFile, + certChain, + privateKey, }); return new Conn(res.rid, res.remoteAddr, res.localAddr); } diff --git a/extensions/net/lib.deno_net.d.ts b/extensions/net/lib.deno_net.d.ts index 25397f960..d35e01e31 100644 --- a/extensions/net/lib.deno_net.d.ts +++ b/extensions/net/lib.deno_net.d.ts @@ -68,9 +68,10 @@ declare namespace Deno { ): Listener; export interface ListenTlsOptions extends ListenOptions { - /** Server certificate file. */ + /** Path to a file containing a PEM formatted CA certificate. Requires + * `--allow-read`. */ certFile: string; - /** Server public key file. */ + /** Server public key file. Requires `--allow-read`.*/ keyFile: string; transport?: "tcp"; diff --git a/extensions/net/lib.deno_net.unstable.d.ts b/extensions/net/lib.deno_net.unstable.d.ts index adeeb1466..145f232c0 100644 --- a/extensions/net/lib.deno_net.unstable.d.ts +++ b/extensions/net/lib.deno_net.unstable.d.ts @@ -191,6 +191,32 @@ declare namespace Deno { options: ConnectOptions | UnixConnectOptions, ): Promise<Conn>; + export interface ConnectTlsClientCertOptions { + /** PEM formatted client certificate chain. */ + certChain: string; + /** PEM formatted (RSA or PKCS8) private key of client certificate. */ + privateKey: string; + } + + /** **UNSTABLE** New API, yet to be vetted. + * + * Create a TLS connection with an attached client certificate. + * + * ```ts + * const conn = await Deno.connectTls({ + * hostname: "deno.land", + * port: 443, + * certChain: "---- BEGIN CERTIFICATE ----\n ...", + * privateKey: "---- BEGIN PRIVATE KEY ----\n ...", + * }); + * ``` + * + * Requires `allow-net` permission. + */ + export function connectTls( + options: ConnectTlsOptions & ConnectTlsClientCertOptions, + ): Promise<Conn>; + export interface StartTlsOptions { /** A literal IP address or host name that can be resolved to an IP address. * If not specified, defaults to `127.0.0.1`. */ diff --git a/extensions/net/ops_tls.rs b/extensions/net/ops_tls.rs index 124da2f03..7c4563390 100644 --- a/extensions/net/ops_tls.rs +++ b/extensions/net/ops_tls.rs @@ -14,6 +14,7 @@ use deno_core::error::bad_resource_id; use deno_core::error::custom_error; use deno_core::error::generic_error; use deno_core::error::invalid_hostname; +use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::futures::future::poll_fn; use deno_core::futures::ready; @@ -57,6 +58,7 @@ use std::cell::RefCell; use std::convert::From; use std::fs::File; use std::io; +use std::io::BufRead; use std::io::BufReader; use std::io::ErrorKind; use std::ops::Deref; @@ -649,6 +651,8 @@ pub struct ConnectTlsArgs { hostname: String, port: u16, cert_file: Option<String>, + cert_chain: Option<String>, + private_key: Option<String>, } #[derive(Deserialize)] @@ -717,6 +721,7 @@ where let remote_addr = tcp_stream.peer_addr()?; let tls_config = Arc::new(create_client_config(root_cert_store, ca_data)?); + let tls_stream = TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns); @@ -755,6 +760,14 @@ where }; let port = args.port; let cert_file = args.cert_file.as_deref(); + + if args.cert_chain.is_some() { + super::check_unstable2(&state, "ConnectTlsOptions.certChain"); + } + if args.private_key.is_some() { + super::check_unstable2(&state, "ConnectTlsOptions.privateKey"); + } + { let mut s = state.borrow_mut(); let permissions = s.borrow_mut::<NP>(); @@ -788,7 +801,28 @@ where let tcp_stream = TcpStream::connect(connect_addr).await?; let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; - let tls_config = Arc::new(create_client_config(root_cert_store, ca_data)?); + + let mut tls_config = create_client_config(root_cert_store, ca_data)?; + + if args.cert_chain.is_some() || args.private_key.is_some() { + let cert_chain = args + .cert_chain + .ok_or_else(|| type_error("No certificate chain provided"))?; + let private_key = args + .private_key + .ok_or_else(|| type_error("No private key provided"))?; + + // The `remove` is safe because load_private_keys checks that there is at least one key. + let private_key = load_private_keys(private_key.as_bytes())?.remove(0); + + tls_config.set_single_client_cert( + load_certs(&mut cert_chain.as_bytes())?, + private_key, + )?; + } + + let tls_config = Arc::new(tls_config); + let tls_stream = TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns); @@ -812,10 +846,7 @@ where }) } -fn load_certs(path: &str) -> Result<Vec<Certificate>, AnyError> { - let cert_file = File::open(path)?; - let reader = &mut BufReader::new(cert_file); - +fn load_certs(reader: &mut dyn BufRead) -> Result<Vec<Certificate>, AnyError> { let certs = certs(reader) .map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?; @@ -827,6 +858,12 @@ fn load_certs(path: &str) -> Result<Vec<Certificate>, AnyError> { Ok(certs) } +fn load_certs_from_file(path: &str) -> Result<Vec<Certificate>, AnyError> { + let cert_file = File::open(path)?; + let reader = &mut BufReader::new(cert_file); + load_certs(reader) +} + fn key_decode_err() -> AnyError { custom_error("InvalidData", "Unable to decode key") } @@ -836,27 +873,22 @@ fn key_not_found_err() -> AnyError { } /// Starts with -----BEGIN RSA PRIVATE KEY----- -fn load_rsa_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> { - let key_file = File::open(path)?; - let reader = &mut BufReader::new(key_file); - let keys = rsa_private_keys(reader).map_err(|_| key_decode_err())?; +fn load_rsa_keys(mut bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> { + let keys = rsa_private_keys(&mut bytes).map_err(|_| key_decode_err())?; Ok(keys) } /// Starts with -----BEGIN PRIVATE KEY----- -fn load_pkcs8_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> { - let key_file = File::open(path)?; - let reader = &mut BufReader::new(key_file); - let keys = pkcs8_private_keys(reader).map_err(|_| key_decode_err())?; +fn load_pkcs8_keys(mut bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> { + let keys = pkcs8_private_keys(&mut bytes).map_err(|_| key_decode_err())?; Ok(keys) } -fn load_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> { - let path = path.to_string(); - let mut keys = load_rsa_keys(&path)?; +fn load_private_keys(bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> { + let mut keys = load_rsa_keys(bytes)?; if keys.is_empty() { - keys = load_pkcs8_keys(&path)?; + keys = load_pkcs8_keys(bytes)?; } if keys.is_empty() { @@ -866,6 +898,13 @@ fn load_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> { Ok(keys) } +fn load_private_keys_from_file( + path: &str, +) -> Result<Vec<PrivateKey>, AnyError> { + let key_bytes = std::fs::read(path)?; + load_private_keys(&key_bytes) +} + pub struct TlsListenerResource { tcp_listener: AsyncRefCell<TcpListener>, tls_config: Arc<ServerConfig>, @@ -921,7 +960,10 @@ where alpn_protocols.into_iter().map(|s| s.into_bytes()).collect(); } tls_config - .set_single_cert(load_certs(cert_file)?, load_keys(key_file)?.remove(0)) + .set_single_cert( + load_certs_from_file(cert_file)?, + load_private_keys_from_file(key_file)?.remove(0), + ) .expect("invalid key or certificate"); let bind_addr = resolve_addr_sync(hostname, port)? |