diff options
author | Luca Casonato <hello@lcas.dev> | 2021-09-30 09:26:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-30 09:26:15 +0200 |
commit | 0d7a417f332a57fb3e89250a1ce250b929d0b2f7 (patch) | |
tree | 8f63043fcf6c5419d6d213a196c54a8b421e3d8b | |
parent | 62920e4ef5bed131c125c4b8b5bdb8250584946f (diff) |
feat(tls): custom in memory CA certificates (#12219)
This adds support for using in memory CA certificates for
`Deno.startTLS`, `Deno.connectTLS` and `Deno.createHttpClient`.
`certFile` is deprecated in `startTls` and `connectTls`, and removed
from `Deno.createHttpClient`.
-rw-r--r-- | cli/dts/lib.deno.unstable.d.ts | 47 | ||||
-rw-r--r-- | cli/file_fetcher.rs | 2 | ||||
-rw-r--r-- | cli/http_util.rs | 84 | ||||
-rw-r--r-- | cli/tests/unit/README.md | 2 | ||||
-rw-r--r-- | cli/tests/unit/fetch_test.ts | 39 | ||||
-rw-r--r-- | cli/tests/unit/http_test.ts | 4 | ||||
-rw-r--r-- | cli/tests/unit/tls_test.ts | 70 | ||||
-rw-r--r-- | ext/fetch/22_http_client.js | 1 | ||||
-rw-r--r-- | ext/fetch/lib.rs | 40 | ||||
-rw-r--r-- | ext/net/02_tls.js | 5 | ||||
-rw-r--r-- | ext/net/lib.deno_net.d.ts | 17 | ||||
-rw-r--r-- | ext/net/ops_tls.rs | 40 | ||||
-rw-r--r-- | ext/tls/lib.rs | 12 | ||||
-rw-r--r-- | ext/websocket/lib.rs | 2 | ||||
-rw-r--r-- | test_util/src/lib.rs | 61 |
15 files changed, 266 insertions, 160 deletions
diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index bfc4536f0..db59980ec 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -754,7 +754,8 @@ declare namespace Deno { * A custom HttpClient for use with `fetch`. * * ```ts - * const client = Deno.createHttpClient({ caData: await Deno.readTextFile("./ca.pem") }); + * const caCert = await Deno.readTextFile("./ca.pem"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ] }); * const req = await fetch("https://myserver.com", { client }); * ``` */ @@ -767,11 +768,16 @@ declare namespace Deno { * The options used when creating a [HttpClient]. */ export interface CreateHttpClientOptions { - /** A certificate authority to use when validating TLS certificates. Certificate data must be PEM encoded. - */ - caData?: string; + /** A list of root certificates that will be used in addition to the + * default root certificates to verify the peer's certificate. + * + * Must be in PEM format. */ + caCerts?: string[]; + /** A HTTP proxy to use for new connections. */ proxy?: Proxy; + /** PEM formatted client certificate chain. */ certChain?: string; + /** PEM formatted (RSA or PKCS8) private key of client certificate. */ privateKey?: string; } @@ -789,7 +795,8 @@ declare namespace Deno { * Create a custom HttpClient for to use with `fetch`. * * ```ts - * const client = Deno.createHttpClient({ caData: await Deno.readTextFile("./ca.pem") }); + * const caCert = await Deno.readTextFile("./ca.pem"); + * const client = Deno.createHttpClient({ caCerts: [ caCert ] }); * const response = await fetch("https://myserver.com", { client }); * ``` * @@ -1194,11 +1201,11 @@ declare namespace Deno { options: ConnectOptions | UnixConnectOptions, ): Promise<Conn>; - export interface ConnectTlsClientCertOptions { + export interface ConnectTlsOptions { /** PEM formatted client certificate chain. */ - certChain: string; + certChain?: string; /** PEM formatted (RSA or PKCS8) private key of client certificate. */ - privateKey: string; + privateKey?: string; } /** **UNSTABLE** New API, yet to be vetted. @@ -1216,30 +1223,38 @@ declare namespace Deno { * * Requires `allow-net` permission. */ - export function connectTls( - options: ConnectTlsOptions & ConnectTlsClientCertOptions, - ): Promise<Conn>; + export function connectTls(options: ConnectTlsOptions): 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`. */ hostname?: string; - /** Server certificate file. */ + /** + * @deprecated This option is deprecated and will be removed in a future + * release. + * + * Server certificate file. + */ certFile?: string; + /** A list of root certificates that will be used in addition to the + * default root certificates to verify the peer's certificate. + * + * Must be in PEM format. */ + caCerts?: string[]; } /** **UNSTABLE**: new API, yet to be vetted. * * Start TLS handshake from an existing connection using - * an optional cert file, hostname (default is "127.0.0.1"). The - * cert file is optional and if not included Mozilla's root certificates will - * be used (see also https://github.com/ctz/webpki-roots for specifics) + * an optional cert file, hostname (default is "127.0.0.1"). Specifying CA + * certs is optional. By default the configured root certificates are used. * Using this function requires that the other end of the connection is * prepared for TLS handshake. * * ```ts * const conn = await Deno.connect({ port: 80, hostname: "127.0.0.1" }); - * const tlsConn = await Deno.startTls(conn, { certFile: "./certs/my_custom_root_CA.pem", hostname: "localhost" }); + * const caCert = await Deno.readTextFile("./certs/my_custom_root_CA.pem"); + * const tlsConn = await Deno.startTls(conn, { caCerts: [caCert], hostname: "localhost" }); * ``` * * Requires `allow-net` permission. diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 131685e49..c4c89fec1 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -255,7 +255,7 @@ impl FileFetcher { http_client: create_http_client( get_user_agent(), root_cert_store, - None, + vec![], None, unsafely_ignore_certificate_errors, None, diff --git a/cli/http_util.rs b/cli/http_util.rs index 61b1abcbe..521acadfa 100644 --- a/cli/http_util.rs +++ b/cli/http_util.rs @@ -143,11 +143,11 @@ mod tests { use deno_tls::create_http_client; use std::fs::read; - fn create_test_client(ca_data: Option<Vec<u8>>) -> Client { + fn create_test_client() -> Client { create_http_client( "test_client".to_string(), None, - ca_data, + vec![], None, None, None, @@ -160,7 +160,7 @@ mod tests { let _http_server_guard = test_util::http_server(); // Relies on external http server. See target/debug/test_server let url = Url::parse("http://127.0.0.1:4545/fixture.json").unwrap(); - let client = create_test_client(None); + let client = create_test_client(); let result = fetch_once(FetchOnceArgs { client, url, @@ -184,7 +184,7 @@ mod tests { // Relies on external http server. See target/debug/test_server let url = Url::parse("http://127.0.0.1:4545/053_import_compression/gziped") .unwrap(); - let client = create_test_client(None); + let client = create_test_client(); let result = fetch_once(FetchOnceArgs { client, url, @@ -209,7 +209,7 @@ mod tests { async fn test_fetch_with_etag() { let _http_server_guard = test_util::http_server(); let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap(); - let client = create_test_client(None); + let client = create_test_client(); let result = fetch_once(FetchOnceArgs { client: client.clone(), url: url.clone(), @@ -245,7 +245,7 @@ mod tests { // Relies on external http server. See target/debug/test_server let url = Url::parse("http://127.0.0.1:4545/053_import_compression/brotli") .unwrap(); - let client = create_test_client(None); + let client = create_test_client(); let result = fetch_once(FetchOnceArgs { client, url, @@ -274,7 +274,7 @@ mod tests { let url = Url::parse("http://127.0.0.1:4546/fixture.json").unwrap(); // Dns resolver substitutes `127.0.0.1` with `localhost` let target_url = Url::parse("http://localhost:4545/fixture.json").unwrap(); - let client = create_test_client(None); + let client = create_test_client(); let result = fetch_once(FetchOnceArgs { client, url, @@ -336,15 +336,13 @@ mod tests { let client = create_http_client( version::get_user_agent(), None, - Some( - read( - test_util::testdata_path() - .join("tls/RootCA.pem") - .to_str() - .unwrap(), - ) - .unwrap(), - ), + vec![read( + test_util::testdata_path() + .join("tls/RootCA.pem") + .to_str() + .unwrap(), + ) + .unwrap()], None, None, None, @@ -375,7 +373,7 @@ mod tests { let client = create_http_client( version::get_user_agent(), None, // This will load mozilla certs by default - None, + vec![], None, None, None, @@ -408,7 +406,7 @@ mod tests { let client = create_http_client( version::get_user_agent(), Some(deno_tls::rustls::RootCertStore::empty()), // no certs loaded at all - None, + vec![], None, None, None, @@ -439,15 +437,13 @@ mod tests { let client = create_http_client( version::get_user_agent(), None, - Some( - read( - test_util::testdata_path() - .join("tls/RootCA.pem") - .to_str() - .unwrap(), - ) - .unwrap(), - ), + vec![read( + test_util::testdata_path() + .join("tls/RootCA.pem") + .to_str() + .unwrap(), + ) + .unwrap()], None, None, None, @@ -480,15 +476,13 @@ mod tests { let client = create_http_client( version::get_user_agent(), None, - Some( - read( - test_util::testdata_path() - .join("tls/RootCA.pem") - .to_str() - .unwrap(), - ) - .unwrap(), - ), + vec![read( + test_util::testdata_path() + .join("tls/RootCA.pem") + .to_str() + .unwrap(), + ) + .unwrap()], None, None, None, @@ -534,15 +528,13 @@ mod tests { let client = create_http_client( version::get_user_agent(), None, - Some( - read( - test_util::testdata_path() - .join("tls/RootCA.pem") - .to_str() - .unwrap(), - ) - .unwrap(), - ), + vec![read( + test_util::testdata_path() + .join("tls/RootCA.pem") + .to_str() + .unwrap(), + ) + .unwrap()], None, None, None, @@ -574,7 +566,7 @@ mod tests { let _g = test_util::http_server(); let url_str = "http://127.0.0.1:4545/bad_redirect"; let url = Url::parse(url_str).unwrap(); - let client = create_test_client(None); + let client = create_test_client(); let result = fetch_once(FetchOnceArgs { client, url, diff --git a/cli/tests/unit/README.md b/cli/tests/unit/README.md index fef3ea98b..adaa1ed0e 100644 --- a/cli/tests/unit/README.md +++ b/cli/tests/unit/README.md @@ -16,7 +16,7 @@ unitTest(function simpleTestFn(): void { unitTest( { ignore: Deno.build.os === "windows", - perms: { read: true, write: true }, + permissions: { read: true, write: true }, }, function complexTestFn(): void { // test code here diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts index 5bce2af43..a2bd1741b 100644 --- a/cli/tests/unit/fetch_test.ts +++ b/cli/tests/unit/fetch_test.ts @@ -997,40 +997,16 @@ unitTest(function fetchResponseEmptyConstructor() { assertEquals([...response.headers], []); }); -// TODO(lucacasonato): reenable this test unitTest( - { permissions: { net: true }, ignore: true }, + { permissions: { net: true, read: true } }, async function fetchCustomHttpClientParamCertificateSuccess(): Promise< void > { - const client = Deno.createHttpClient( - { - caData: `-----BEGIN CERTIFICATE----- -MIIDIzCCAgugAwIBAgIJAMKPPW4tsOymMA0GCSqGSIb3DQEBCwUAMCcxCzAJBgNV -BAYTAlVTMRgwFgYDVQQDDA9FeGFtcGxlLVJvb3QtQ0EwIBcNMTkxMDIxMTYyODIy -WhgPMjExODA5MjcxNjI4MjJaMCcxCzAJBgNVBAYTAlVTMRgwFgYDVQQDDA9FeGFt -cGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMH/IO -2qtHfyBKwANNPB4K0q5JVSg8XxZdRpTTlz0CwU0oRO3uHrI52raCCfVeiQutyZop -eFZTDWeXGudGAFA2B5m3orWt0s+touPi8MzjsG2TQ+WSI66QgbXTNDitDDBtTVcV -5G3Ic+3SppQAYiHSekLISnYWgXLl+k5CnEfTowg6cjqjVr0KjL03cTN3H7b+6+0S -ws4rYbW1j4ExR7K6BFNH6572yq5qR20E6GqlY+EcOZpw4CbCk9lS8/CWuXze/vMs -OfDcc6K+B625d27wyEGZHedBomT2vAD7sBjvO8hn/DP1Qb46a8uCHR6NSfnJ7bXO -G1igaIbgY1zXirNdAgMBAAGjUDBOMB0GA1UdDgQWBBTzut+pwwDfqmMYcI9KNWRD -hxcIpTAfBgNVHSMEGDAWgBTzut+pwwDfqmMYcI9KNWRDhxcIpTAMBgNVHRMEBTAD -AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB9AqSbZ+hEglAgSHxAMCqRFdhVu7MvaQM0 -P090mhGlOCt3yB7kdGfsIrUW6nQcTz7PPQFRaJMrFHPvFvPootkBUpTYR4hTkdce -H6RCRu2Jxl4Y9bY/uezd9YhGCYfUtfjA6/TH9FcuZfttmOOlxOt01XfNvVMIR6RM -z/AYhd+DeOXjr35F/VHeVpnk+55L0PYJsm1CdEbOs5Hy1ecR7ACuDkXnbM4fpz9I -kyIWJwk2zJReKcJMgi1aIinDM9ao/dca1G99PHOw8dnr4oyoTiv8ao6PWiSRHHMi -MNf4EgWfK+tZMnuqfpfO9740KzfcVoMNo4QJD4yn5YxroUOO/Azi ------END CERTIFICATE----- -`, - }, - ); - const response = await fetch( - "https://localhost:5545/fixture.json", - { client }, - ); + const caCert = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem"); + const client = Deno.createHttpClient({ caCerts: [caCert] }); + const response = await fetch("https://localhost:5545/fixture.json", { + client, + }); const json = await response.json(); assertEquals(json.name, "deno"); client.close(); @@ -1250,6 +1226,7 @@ unitTest( void > { const data = "Hello World"; + const caCert = await Deno.readTextFile("cli/tests/testdata/tls/RootCA.crt"); const client = Deno.createHttpClient({ certChain: await Deno.readTextFile( "cli/tests/testdata/tls/localhost.crt", @@ -1257,7 +1234,7 @@ unitTest( privateKey: await Deno.readTextFile( "cli/tests/testdata/tls/localhost.key", ), - caData: await Deno.readTextFile("cli/tests/testdata/tls/RootCA.crt"), + caCerts: [caCert], }); const response = await fetch("https://localhost:5552/echo_server", { client, diff --git a/cli/tests/unit/http_test.ts b/cli/tests/unit/http_test.ts index 7cca8d89e..fe6f1aba2 100644 --- a/cli/tests/unit/http_test.ts +++ b/cli/tests/unit/http_test.ts @@ -233,8 +233,8 @@ unitTest( listener.close(); })(); - const caData = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem"); - const client = Deno.createHttpClient({ caData }); + const caCert = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem"); + const client = Deno.createHttpClient({ caCerts: [caCert] }); const resp = await fetch(`https://${hostname}:${port}/`, { client, headers: { "connection": "close" }, diff --git a/cli/tests/unit/tls_test.ts b/cli/tests/unit/tls_test.ts index 391c51504..b2382833f 100644 --- a/cli/tests/unit/tls_test.ts +++ b/cli/tests/unit/tls_test.ts @@ -182,7 +182,7 @@ unitTest( const conn = await Deno.connectTls({ hostname, port, - certFile: "cli/tests/testdata/tls/RootCA.pem", + caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], }); assert(conn.rid > 0); const w = new BufWriter(conn); @@ -230,7 +230,7 @@ async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> { const connectPromise = Deno.connectTls({ hostname: "localhost", port, - certFile: "cli/tests/testdata/tls/RootCA.pem", + caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], }); const endpoints = await Promise.all([acceptPromise, connectPromise]); @@ -570,7 +570,7 @@ async function tlsWithTcpFailureTestImpl( Deno.connectTls({ hostname: "localhost", port: tcpPort, - certFile: "cli/tests/testdata/tls/RootCA.crt", + caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], }), ]); @@ -1052,7 +1052,69 @@ unitTest( privateKey: await Deno.readTextFile( "cli/tests/testdata/tls/localhost.key", ), - certFile: "cli/tests/testdata/tls/RootCA.crt", + caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +unitTest( + { permissions: { read: true, net: true } }, + async function connectTLSCaCerts() { + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4557, + caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +unitTest( + { permissions: { read: true, net: true } }, + async function connectTLSCertFile() { + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4557, + certFile: "cli/tests/testdata/tls/RootCA.pem", + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +unitTest( + { permissions: { read: true, net: true } }, + async function startTLSCaCerts() { + const plainConn = await Deno.connect({ + hostname: "localhost", + port: 4557, + }); + const conn = await Deno.startTls(plainConn, { + hostname: "localhost", + caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +unitTest( + { permissions: { read: true, net: true } }, + async function startTLSCertFile() { + const plainConn = await Deno.connect({ + hostname: "localhost", + port: 4557, + }); + const conn = await Deno.startTls(plainConn, { + hostname: "localhost", + certFile: "cli/tests/testdata/tls/RootCA.pem", }); const result = decoder.decode(await readAll(conn)); assertEquals(result, "PASS"); diff --git a/ext/fetch/22_http_client.js b/ext/fetch/22_http_client.js index 60b069aa7..592256c71 100644 --- a/ext/fetch/22_http_client.js +++ b/ext/fetch/22_http_client.js @@ -19,6 +19,7 @@ * @returns {HttpClient} */ function createHttpClient(options) { + options.caCerts ??= []; return new HttpClient(core.opSync("op_create_http_client", options)); } diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 3085e7826..b422c2741 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -40,8 +40,6 @@ use serde::Serialize; use std::borrow::Cow; use std::cell::RefCell; use std::convert::From; -use std::fs::File; -use std::io::Read; use std::path::Path; use std::path::PathBuf; use std::pin::Pin; @@ -87,7 +85,7 @@ pub fn init<P: FetchPermissions + 'static>( create_http_client( user_agent.clone(), root_cert_store.clone(), - None, + vec![], proxy.clone(), unsafely_ignore_certificate_errors.clone(), client_cert_chain_and_key.clone(), @@ -465,13 +463,10 @@ impl HttpClientResource { } } -#[derive(Deserialize, Default, Debug)] +#[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -#[serde(default)] pub struct CreateHttpClientOptions { - ca_stores: Option<Vec<String>>, - ca_file: Option<String>, - ca_data: Option<ByteString>, + ca_certs: Vec<String>, proxy: Option<Proxy>, cert_chain: Option<String>, private_key: Option<String>, @@ -485,11 +480,6 @@ pub fn op_create_http_client<FP>( where FP: FetchPermissions + 'static, { - if let Some(ca_file) = args.ca_file.clone() { - let permissions = state.borrow_mut::<FP>(); - permissions.check_read(&PathBuf::from(ca_file))?; - } - if let Some(proxy) = args.proxy.clone() { let permissions = state.borrow_mut::<FP>(); let url = Url::parse(&proxy.url)?; @@ -512,13 +502,16 @@ where }; let defaults = state.borrow::<HttpClientDefaults>(); - let cert_data = - get_cert_data(args.ca_file.as_deref(), args.ca_data.as_deref())?; + let ca_certs = args + .ca_certs + .into_iter() + .map(|cert| cert.into_bytes()) + .collect::<Vec<_>>(); let client = create_http_client( defaults.user_agent.clone(), defaults.root_cert_store.clone(), - cert_data, + ca_certs, args.proxy, defaults.unsafely_ignore_certificate_errors.clone(), client_cert_chain_and_key, @@ -527,18 +520,3 @@ where let rid = state.resource_table.add(HttpClientResource::new(client)); Ok(rid) } - -fn get_cert_data( - ca_file: Option<&str>, - ca_data: Option<&[u8]>, -) -> Result<Option<Vec<u8>>, AnyError> { - if let Some(ca_data) = ca_data { - Ok(Some(ca_data.to_vec())) - } else if let Some(ca_file) = ca_file { - let mut buf = Vec::new(); - File::open(ca_file)?.read_to_end(&mut buf)?; - Ok(Some(buf)) - } else { - Ok(None) - } -} diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 343ec2e4f..9f8fb314c 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -28,6 +28,7 @@ hostname = "127.0.0.1", transport = "tcp", certFile = undefined, + caCerts = [], certChain = undefined, privateKey = undefined, }) { @@ -36,6 +37,7 @@ hostname, transport, certFile, + caCerts, certChain, privateKey, }); @@ -70,12 +72,13 @@ async function startTls( conn, - { hostname = "127.0.0.1", certFile } = {}, + { hostname = "127.0.0.1", certFile = undefined, caCerts = [] } = {}, ) { const res = await opStartTls({ rid: conn.rid, hostname, certFile, + caCerts, }); return new Conn(res.rid, res.remoteAddr, res.localAddr); } diff --git a/ext/net/lib.deno_net.d.ts b/ext/net/lib.deno_net.d.ts index dd2e4677d..45f1194fb 100644 --- a/ext/net/lib.deno_net.d.ts +++ b/ext/net/lib.deno_net.d.ts @@ -121,8 +121,18 @@ declare namespace Deno { /** A literal IP address or host name that can be resolved to an IP address. * If not specified, defaults to `127.0.0.1`. */ hostname?: string; - /** Server certificate file. */ + /** + * @deprecated This option is deprecated and will be removed in a future + * release. + * + * Server certificate file. + */ certFile?: string; + /** A list of root certificates that will be used in addition to the + * default root certificates to verify the peer's certificate. + * + * Must be in PEM format. */ + caCerts?: string[]; } /** Establishes a secure connection over TLS (transport layer security) using @@ -131,10 +141,11 @@ declare namespace Deno { * be used (see also https://github.com/ctz/webpki-roots for specifics) * * ```ts + * const caCert = await Deno.readTextFile("./certs/my_custom_root_CA.pem"); * const conn1 = await Deno.connectTls({ port: 80 }); - * const conn2 = await Deno.connectTls({ certFile: "./certs/my_custom_root_CA.pem", hostname: "192.0.2.1", port: 80 }); + * const conn2 = await Deno.connectTls({ caCerts: [caCert], hostname: "192.0.2.1", port: 80 }); * const conn3 = await Deno.connectTls({ hostname: "[2001:db8::1]", port: 80 }); - * const conn4 = await Deno.connectTls({ certFile: "./certs/my_custom_root_CA.pem", hostname: "golang.org", port: 80}); + * const conn4 = await Deno.connectTls({ caCerts: [caCert], hostname: "golang.org", port: 80}); * ``` * * Requires `allow-net` permission. diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 17367af54..d6618440f 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -649,6 +649,7 @@ pub struct ConnectTlsArgs { hostname: String, port: u16, cert_file: Option<String>, + ca_certs: Vec<String>, cert_chain: Option<String>, private_key: Option<String>, } @@ -658,6 +659,7 @@ pub struct ConnectTlsArgs { struct StartTlsArgs { rid: ResourceId, cert_file: Option<String>, + ca_certs: Vec<String>, hostname: String, } @@ -685,13 +687,16 @@ where } } - let ca_data = match cert_file { - Some(path) => { - let mut buf = Vec::new(); - File::open(path)?.read_to_end(&mut buf)?; - Some(buf) - } - _ => None, + let mut ca_certs = args + .ca_certs + .into_iter() + .map(|s| s.into_bytes()) + .collect::<Vec<_>>(); + + if let Some(path) = cert_file { + let mut buf = Vec::new(); + File::open(path)?.read_to_end(&mut buf)?; + ca_certs.push(buf); }; let hostname_dns = DNSNameRef::try_from_ascii_str(hostname) @@ -724,7 +729,7 @@ where let tls_config = Arc::new(create_client_config( root_cert_store, - ca_data, + ca_certs, unsafely_ignore_certificate_errors, )?); let tls_stream = @@ -786,13 +791,16 @@ where } } - let ca_data = match cert_file { - Some(path) => { - let mut buf = Vec::new(); - File::open(path)?.read_to_end(&mut buf)?; - Some(buf) - } - _ => None, + let mut ca_certs = args + .ca_certs + .into_iter() + .map(|s| s.into_bytes()) + .collect::<Vec<_>>(); + + if let Some(path) = cert_file { + let mut buf = Vec::new(); + File::open(path)?.read_to_end(&mut buf)?; + ca_certs.push(buf); }; let root_cert_store = state @@ -812,7 +820,7 @@ where let remote_addr = tcp_stream.peer_addr()?; let mut tls_config = create_client_config( root_cert_store, - ca_data, + ca_certs, unsafely_ignore_certificate_errors, )?; diff --git a/ext/tls/lib.rs b/ext/tls/lib.rs index 7632da5e6..076ef59fb 100644 --- a/ext/tls/lib.rs +++ b/ext/tls/lib.rs @@ -136,7 +136,7 @@ pub fn create_default_root_cert_store() -> RootCertStore { pub fn create_client_config( root_cert_store: Option<RootCertStore>, - ca_data: Option<Vec<u8>>, + ca_certs: Vec<Vec<u8>>, unsafely_ignore_certificate_errors: Option<Vec<String>>, ) -> Result<ClientConfig, AnyError> { let mut tls_config = ClientConfig::new(); @@ -144,11 +144,11 @@ pub fn create_client_config( tls_config.root_store = root_cert_store.unwrap_or_else(create_default_root_cert_store); - // If a custom cert is specified, add it to the store - if let Some(cert) = ca_data { + // If custom certs are specified, add them to the store + for cert in ca_certs { let reader = &mut BufReader::new(Cursor::new(cert)); // This function does not return specific errors, if it fails give a generic message. - if let Err(_err) = tls_config.root_store.add_pem_file(reader) { + if let Err(()) = tls_config.root_store.add_pem_file(reader) { return Err(anyhow!("Unable to add pem file to certificate store")); } } @@ -215,14 +215,14 @@ pub fn load_private_keys(bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> { pub fn create_http_client( user_agent: String, root_cert_store: Option<RootCertStore>, - ca_data: Option<Vec<u8>>, + ca_certs: Vec<Vec<u8>>, proxy: Option<Proxy>, unsafely_ignore_certificate_errors: Option<Vec<String>>, client_cert_chain_and_key: Option<(String, String)>, ) -> Result<Client, AnyError> { let mut tls_config = create_client_config( root_cert_store, - ca_data, + ca_certs, unsafely_ignore_certificate_errors, )?; diff --git a/ext/websocket/lib.rs b/ext/websocket/lib.rs index f9f11e591..dbb88dc8d 100644 --- a/ext/websocket/lib.rs +++ b/ext/websocket/lib.rs @@ -252,7 +252,7 @@ where Some("wss") => { let tls_config = create_client_config( root_cert_store, - None, + vec![], unsafely_ignore_certificate_errors, )?; let tls_connector = TlsConnector::from(Arc::new(tls_config)); diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 8bfe5caa0..f20601340 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -59,6 +59,7 @@ const REDIRECT_ABSOLUTE_PORT: u16 = 4550; const AUTH_REDIRECT_PORT: u16 = 4551; const TLS_CLIENT_AUTH_PORT: u16 = 4552; const BASIC_AUTH_REDIRECT_PORT: u16 = 4554; +const TLS_PORT: u16 = 4557; const HTTPS_PORT: u16 = 5545; const HTTPS_CLIENT_AUTH_PORT: u16 = 5552; const WS_PORT: u16 = 4242; @@ -451,6 +452,62 @@ async fn run_tls_client_auth_server() { } } +/// This server responds with 'PASS' if client authentication was successful. Try it by running +/// test_server and +/// curl --cacert cli/tests/testdata/tls/RootCA.crt https://localhost:4553/ +async fn run_tls_server() { + let cert_file = "tls/localhost.crt"; + let key_file = "tls/localhost.key"; + let ca_cert_file = "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_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"); // 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) => { + tls_stream.write_all(b"PASS").await.unwrap(); + } + + Err(e) => { + eprintln!("TLS accept error: {:?}", e); + } + } + }); + } +} + async fn absolute_redirect( req: Request<Body>, ) -> hyper::Result<Response<Body>> { @@ -1016,6 +1073,7 @@ 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_server_fut = run_tls_server(); let tls_client_auth_server_fut = run_tls_client_auth_server(); let client_auth_server_https_fut = wrap_client_auth_https_server(); let main_server_fut = wrap_main_server(); @@ -1026,6 +1084,7 @@ pub async fn run_all_servers() { redirect_server_fut, ws_server_fut, wss_server_fut, + tls_server_fut, tls_client_auth_server_fut, ws_close_server_fut, another_redirect_server_fut, @@ -1182,7 +1241,7 @@ impl HttpServerCount { if line.starts_with("ready:") { ready_count += 1; } - if ready_count == 5 { + if ready_count == 6 { break; } } else { |