Skip to content

Commit

Permalink
feat: support client certificates for connectTls (#11598)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Lamando <dan@danopia.net>
Co-authored-by: Erik Price <github@erikprice.net>
  • Loading branch information
3 people authored Aug 9, 2021
1 parent f402904 commit 3ab50b3
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 36 deletions.
68 changes: 66 additions & 2 deletions cli/tests/unit/tls_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
});

Expand All @@ -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",
});
Expand Down Expand Up @@ -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();
},
);
4 changes: 4 additions & 0 deletions extensions/net/02_tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 3 additions & 2 deletions extensions/net/lib.deno_net.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
26 changes: 26 additions & 0 deletions extensions/net/lib.deno_net.unstable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`. */
Expand Down
78 changes: 60 additions & 18 deletions extensions/net/ops_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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);

Expand All @@ -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"))?;

Expand All @@ -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")
}
Expand All @@ -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() {
Expand All @@ -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>,
Expand Down Expand Up @@ -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)?
Expand Down
Loading

0 comments on commit 3ab50b3

Please sign in to comment.