summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2021-08-09 15:55:00 +0200
committerGitHub <noreply@github.com>2021-08-09 15:55:00 +0200
commit3ab50b355141f744a0acec1a5cc3b3b95247d4b1 (patch)
tree5ab6c3a216f5ce5cc5ee8fbc12e99dfac09496d7
parentf402904e6e227ee60a88d991cb9818d5340f5c1d (diff)
feat: support client certificates for connectTls (#11598)
Co-authored-by: Daniel Lamando <dan@danopia.net> Co-authored-by: Erik Price <github@erikprice.net>
-rw-r--r--cli/tests/unit/tls_test.ts68
-rw-r--r--extensions/net/02_tls.js4
-rw-r--r--extensions/net/lib.deno_net.d.ts5
-rw-r--r--extensions/net/lib.deno_net.unstable.d.ts26
-rw-r--r--extensions/net/ops_tls.rs78
-rw-r--r--test_util/src/lib.rs117
6 files changed, 262 insertions, 36 deletions
diff --git a/cli/tests/unit/tls_test.ts b/cli/tests/unit/tls_test.ts
index 46a27b7f0..8472d93e0 100644
--- a/cli/tests/unit/tls_test.ts
+++ b/cli/tests/unit/tls_test.ts
@@ -11,6 +11,7 @@ import {
unitTest,
} from "./test_util.ts";
import { BufReader, BufWriter } from "../../../test_util/std/io/bufio.ts";
+import { readAll } from "../../../test_util/std/io/util.ts";
import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts";
const encoder = new TextEncoder();
@@ -26,7 +27,7 @@ function unreachable(): never {
unitTest(async function connectTLSNoPerm() {
await assertThrowsAsync(async () => {
- await Deno.connectTls({ hostname: "github.com", port: 443 });
+ await Deno.connectTls({ hostname: "deno.land", port: 443 });
}, Deno.errors.PermissionDenied);
});
@@ -51,7 +52,7 @@ unitTest(
unitTest(async function connectTLSCertFileNoReadPerm() {
await assertThrowsAsync(async () => {
await Deno.connectTls({
- hostname: "github.com",
+ hostname: "deno.land",
port: 443,
certFile: "cli/tests/tls/RootCA.crt",
});
@@ -985,3 +986,66 @@ unitTest(
conn.close();
},
);
+
+unitTest(
+ { perms: { read: true, net: true } },
+ async function connectTLSBadClientCertPrivateKey(): Promise<void> {
+ await assertThrowsAsync(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certChain: "bad data",
+ privateKey: await Deno.readTextFile("cli/tests/tls/localhost.key"),
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+unitTest(
+ { perms: { read: true, net: true } },
+ async function connectTLSBadPrivateKey(): Promise<void> {
+ await assertThrowsAsync(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certChain: await Deno.readTextFile("cli/tests/tls/localhost.crt"),
+ privateKey: "bad data",
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+unitTest(
+ { perms: { read: true, net: true } },
+ async function connectTLSNotPrivateKey(): Promise<void> {
+ await assertThrowsAsync(async () => {
+ await Deno.connectTls({
+ hostname: "deno.land",
+ port: 443,
+ certChain: await Deno.readTextFile("cli/tests/tls/localhost.crt"),
+ privateKey: "",
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+unitTest(
+ { perms: { read: true, net: true } },
+ async function connectWithClientCert() {
+ // The test_server running on port 4552 responds with 'PASS' if client
+ // authentication was successful. Try it by running test_server and
+ // curl --key cli/tests/tls/localhost.key \
+ // --cert cli/tests/tls/localhost.crt \
+ // --cacert cli/tests/tls/RootCA.crt https://localhost:4552/
+ const conn = await Deno.connectTls({
+ hostname: "localhost",
+ port: 4552,
+ certChain: await Deno.readTextFile("cli/tests/tls/localhost.crt"),
+ privateKey: await Deno.readTextFile("cli/tests/tls/localhost.key"),
+ certFile: "cli/tests/tls/RootCA.crt",
+ });
+ const result = decoder.decode(await readAll(conn));
+ assertEquals(result, "PASS");
+ conn.close();
+ },
+);
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)?
diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs
index 228e568f1..93a02c98e 100644
--- a/test_util/src/lib.rs
+++ b/test_util/src/lib.rs
@@ -37,9 +37,10 @@ use std::sync::MutexGuard;
use std::task::Context;
use std::task::Poll;
use tempfile::TempDir;
+use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
-use tokio_rustls::rustls;
+use tokio_rustls::rustls::{self, Session};
use tokio_rustls::TlsAcceptor;
use tokio_tungstenite::accept_async;
@@ -56,6 +57,7 @@ const DOUBLE_REDIRECTS_PORT: u16 = 4548;
const INF_REDIRECTS_PORT: u16 = 4549;
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
const AUTH_REDIRECT_PORT: u16 = 4551;
+const TLS_CLIENT_AUTH_PORT: u16 = 4552;
const HTTPS_PORT: u16 = 5545;
const WS_PORT: u16 = 4242;
const WSS_PORT: u16 = 4243;
@@ -224,6 +226,7 @@ async fn auth_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
async fn run_ws_server(addr: &SocketAddr) {
let listener = TcpListener::bind(addr).await.unwrap();
+ println!("ready: ws"); // Eye catcher for HttpServerCount
while let Ok((stream, _addr)) = listener.accept().await {
tokio::spawn(async move {
let ws_stream_fut = accept_async(stream);
@@ -260,18 +263,28 @@ async fn run_ws_close_server(addr: &SocketAddr) {
async fn get_tls_config(
cert: &str,
key: &str,
+ ca: &str,
) -> io::Result<Arc<rustls::ServerConfig>> {
let mut cert_path = root_path();
let mut key_path = root_path();
+ let mut ca_path = root_path();
cert_path.push(cert);
key_path.push(key);
+ ca_path.push(ca);
let cert_file = std::fs::File::open(cert_path)?;
let key_file = std::fs::File::open(key_path)?;
+ let ca_file = std::fs::File::open(ca_path)?;
let mut cert_reader = io::BufReader::new(cert_file);
let cert = rustls::internal::pemfile::certs(&mut cert_reader)
.expect("Cannot load certificate");
+
+ let mut ca_cert_reader = io::BufReader::new(ca_file);
+ let ca_cert = rustls::internal::pemfile::certs(&mut ca_cert_reader)
+ .expect("Cannot load CA certificate")
+ .remove(0);
+
let mut key_reader = io::BufReader::new(key_file);
let key = {
let pkcs8_key =
@@ -290,7 +303,12 @@ async fn get_tls_config(
match key {
Some(key) => {
- let mut config = rustls::ServerConfig::new(rustls::NoClientAuth::new());
+ let mut root_cert_store = rustls::RootCertStore::empty();
+ root_cert_store.add(&ca_cert).unwrap();
+ // Allow (but do not require) client authentication.
+ let allow_client_auth =
+ rustls::AllowAnyAnonymousOrAuthenticatedClient::new(root_cert_store);
+ let mut config = rustls::ServerConfig::new(allow_client_auth);
config
.set_single_cert(cert, key)
.map_err(|e| {
@@ -307,10 +325,14 @@ async fn get_tls_config(
async fn run_wss_server(addr: &SocketAddr) {
let cert_file = "cli/tests/tls/localhost.crt";
let key_file = "cli/tests/tls/localhost.key";
+ let ca_cert_file = "cli/tests/tls/RootCA.pem";
- let tls_config = get_tls_config(cert_file, key_file).await.unwrap();
+ let tls_config = get_tls_config(cert_file, key_file, ca_cert_file)
+ .await
+ .unwrap();
let tls_acceptor = TlsAcceptor::from(tls_config);
let listener = TcpListener::bind(addr).await.unwrap();
+ println!("ready: wss"); // Eye catcher for HttpServerCount
while let Ok((stream, _addr)) = listener.accept().await {
let acceptor = tls_acceptor.clone();
@@ -338,6 +360,71 @@ async fn run_wss_server(addr: &SocketAddr) {
}
}
+/// This server responds with 'PASS' if client authentication was successful. Try it by running
+/// test_server and
+/// curl --key cli/tests/tls/localhost.key \
+/// --cert cli/tests/tls/localhost.crt \
+/// --cacert cli/tests/tls/RootCA.crt https://localhost:4552/
+async fn run_tls_client_auth_server() {
+ let cert_file = "cli/tests/tls/localhost.crt";
+ let key_file = "cli/tests/tls/localhost.key";
+ let ca_cert_file = "cli/tests/tls/RootCA.pem";
+ let tls_config = get_tls_config(cert_file, key_file, ca_cert_file)
+ .await
+ .unwrap();
+ let tls_acceptor = TlsAcceptor::from(tls_config);
+
+ // Listen on ALL addresses that localhost can resolves to.
+ let accept = |listener: tokio::net::TcpListener| {
+ async {
+ let result = listener.accept().await;
+ Some((result, listener))
+ }
+ .boxed()
+ };
+
+ let host_and_port = &format!("localhost:{}", TLS_CLIENT_AUTH_PORT);
+
+ let listeners = tokio::net::lookup_host(host_and_port)
+ .await
+ .expect(host_and_port)
+ .inspect(|address| println!("{} -> {}", host_and_port, address))
+ .map(tokio::net::TcpListener::bind)
+ .collect::<futures::stream::FuturesUnordered<_>>()
+ .collect::<Vec<_>>()
+ .await
+ .into_iter()
+ .map(|s| s.unwrap())
+ .map(|listener| futures::stream::unfold(listener, accept))
+ .collect::<Vec<_>>();
+
+ println!("ready: tls client auth"); // Eye catcher for HttpServerCount
+
+ let mut listeners = futures::stream::select_all(listeners);
+
+ while let Some(Ok((stream, _addr))) = listeners.next().await {
+ let acceptor = tls_acceptor.clone();
+ tokio::spawn(async move {
+ match acceptor.accept(stream).await {
+ Ok(mut tls_stream) => {
+ let (_, tls_session) = tls_stream.get_mut();
+ // We only need to check for the presence of client certificates
+ // here. Rusttls ensures that they are valid and signed by the CA.
+ let response = match tls_session.get_peer_certificates() {
+ Some(_certs) => b"PASS",
+ None => b"FAIL",
+ };
+ tls_stream.write_all(response).await.unwrap();
+ }
+
+ Err(e) => {
+ eprintln!("TLS accept error: {:?}", e);
+ }
+ }
+ });
+ }
+}
+
async fn absolute_redirect(
req: Request<Body>,
) -> hyper::Result<Response<Body>> {
@@ -775,14 +862,15 @@ async fn wrap_main_https_server() {
let main_server_https_addr = SocketAddr::from(([127, 0, 0, 1], HTTPS_PORT));
let cert_file = "cli/tests/tls/localhost.crt";
let key_file = "cli/tests/tls/localhost.key";
- let tls_config = get_tls_config(cert_file, key_file)
+ let ca_cert_file = "cli/tests/tls/RootCA.pem";
+ let tls_config = get_tls_config(cert_file, key_file, ca_cert_file)
.await
.expect("Cannot get TLS config");
loop {
let tcp = TcpListener::bind(&main_server_https_addr)
.await
.expect("Cannot bind TCP");
- println!("tls ready");
+ println!("ready: https"); // Eye catcher for HttpServerCount
let tls_acceptor = TlsAcceptor::from(tls_config.clone());
// Prepare a long-running future stream to accept and serve cients.
let incoming_tls_stream = async_stream::stream! {
@@ -832,6 +920,8 @@ pub async fn run_all_servers() {
let ws_close_addr = SocketAddr::from(([127, 0, 0, 1], WS_CLOSE_PORT));
let ws_close_server_fut = run_ws_close_server(&ws_close_addr);
+ let tls_client_auth_server_fut = run_tls_client_auth_server();
+
let main_server_fut = wrap_main_server();
let main_server_https_fut = wrap_main_https_server();
@@ -840,6 +930,7 @@ pub async fn run_all_servers() {
redirect_server_fut,
ws_server_fut,
wss_server_fut,
+ tls_client_auth_server_fut,
ws_close_server_fut,
another_redirect_server_fut,
auth_redirect_server_fut,
@@ -856,7 +947,7 @@ pub async fn run_all_servers() {
futures::future::poll_fn(move |cx| {
let poll_result = server_fut.poll_unpin(cx);
if !replace(&mut did_print_ready, true) {
- println!("ready");
+ println!("ready: server_fut"); // Eye catcher for HttpServerCount
}
poll_result
})
@@ -985,17 +1076,15 @@ impl HttpServerCount {
let stdout = test_server.stdout.as_mut().unwrap();
use std::io::{BufRead, BufReader};
let lines = BufReader::new(stdout).lines();
- let mut ready = false;
- let mut tls_ready = false;
+
+ // Wait for all the servers to report being ready.
+ let mut ready_count = 0;
for maybe_line in lines {
if let Ok(line) = maybe_line {
- if line.starts_with("ready") {
- ready = true;
- }
- if line.starts_with("tls ready") {
- tls_ready = true;
+ if line.starts_with("ready:") {
+ ready_count += 1;
}
- if ready && tls_ready {
+ if ready_count == 5 {
break;
}
} else {