From 9a43a2b4959be288034ef0c43f638542de2028b8 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Mon, 19 Feb 2024 01:30:58 +1100 Subject: [PATCH] feat: `Deno.ConnectTlsOptions.{cert,key}` (#22274) Towards #22197 --- ext/net/02_tls.js | 32 +++++++++- ext/net/lib.deno_net.d.ts | 20 ++++++- ext/net/ops_tls.rs | 29 +++++---- tests/unit/tls_test.ts | 123 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 19 deletions(-) diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 25fbb521ca..7fde7d12b6 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -51,21 +51,49 @@ async function connectTls({ caCerts = [], certChain = undefined, privateKey = undefined, + cert = undefined, + key = undefined, alpnProtocols = undefined, }) { if (certFile !== undefined) { internals.warnOnDeprecatedApi( "Deno.ConnectTlsOptions.certFile", new Error().stack, - "Pass the cert file contents to the `Deno.ConnectTlsOptions.certChain` option instead.", + "Pass the cert file contents to the `Deno.ConnectTlsOptions.cert` option instead.", + ); + } + if (certChain !== undefined) { + internals.warnOnDeprecatedApi( + "Deno.ConnectTlsOptions.certChain", + new Error().stack, + "Use the `Deno.ConnectTlsOptions.cert` option instead.", + ); + } + if (privateKey !== undefined) { + internals.warnOnDeprecatedApi( + "Deno.ConnectTlsOptions.privateKey", + new Error().stack, + "Use the `Deno.ConnectTlsOptions.key` option instead.", ); } if (transport !== "tcp") { throw new TypeError(`Unsupported transport: '${transport}'`); } + if (certChain !== undefined && cert !== undefined) { + throw new TypeError( + "Cannot specify both `certChain` and `cert`", + ); + } + if (privateKey !== undefined && key !== undefined) { + throw new TypeError( + "Cannot specify both `privateKey` and `key`", + ); + } + cert ??= certChain; + key ??= privateKey; const { 0: rid, 1: localAddr, 2: remoteAddr } = await op_net_connect_tls( { hostname, port }, - { certFile, caCerts, certChain, privateKey, alpnProtocols }, + { certFile, caCerts, cert, key, alpnProtocols }, ); localAddr.transport = "tcp"; remoteAddr.transport = "tcp"; diff --git a/ext/net/lib.deno_net.d.ts b/ext/net/lib.deno_net.d.ts index c56783e9dc..00689f764d 100644 --- a/ext/net/lib.deno_net.d.ts +++ b/ext/net/lib.deno_net.d.ts @@ -348,10 +348,26 @@ declare namespace Deno { * TLS handshake. */ alpnProtocols?: string[]; - /** PEM formatted client certificate chain. */ + /** + * PEM formatted client certificate chain. + * + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + */ certChain?: string; - /** PEM formatted (RSA or PKCS8) private key of client certificate. */ + /** + * PEM formatted (RSA or PKCS8) private key of client certificate. + * + * @deprecated This will be removed in Deno 2.0. See the + * {@link https://docs.deno.com/runtime/manual/advanced/migrate_deprecations | Deno 1.x to 2.x Migration Guide} + * for migration instructions. + */ privateKey?: string; + /** Server private key in PEM format. */ + key?: string; + /** Cert chain in PEM format. */ + cert?: string; } /** Establishes a secure connection over TLS (transport layer security) using diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index d16bface4c..b16dafa711 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -145,8 +145,8 @@ impl Resource for TlsStreamResource { pub struct ConnectTlsArgs { cert_file: Option, ca_certs: Vec, - cert_chain: Option, - private_key: Option, + cert: Option, + key: Option, alpn_protocols: Option>, } @@ -297,24 +297,23 @@ where let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; - let cert_chain_and_key = - 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"))?; - Some((cert_chain, private_key)) - } else { - None - }; + let cert_and_key = if args.cert.is_some() || args.key.is_some() { + let cert = args + .cert + .ok_or_else(|| type_error("No certificate chain provided"))?; + let key = args + .key + .ok_or_else(|| type_error("No private key provided"))?; + Some((cert, key)) + } else { + None + }; let mut tls_config = create_client_config( root_cert_store, ca_certs, unsafely_ignore_certificate_errors, - cert_chain_and_key, + cert_and_key, SocketUse::GeneralSsl, )?; diff --git a/tests/unit/tls_test.ts b/tests/unit/tls_test.ts index 0ad69d3e40..1ac5e8d983 100644 --- a/tests/unit/tls_test.ts +++ b/tests/unit/tls_test.ts @@ -1174,6 +1174,22 @@ Deno.test( }, ); +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSBadCertKey(): Promise { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + cert: "bad data", + key: await Deno.readTextFile( + "tests/testdata/tls/localhost.key", + ), + }); + }, Deno.errors.InvalidData); + }, +); + Deno.test( { permissions: { read: true, net: true } }, async function connectTLSBadPrivateKey(): Promise { @@ -1190,6 +1206,22 @@ Deno.test( }, ); +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSBadKey(): Promise { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + cert: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + key: "bad data", + }); + }, Deno.errors.InvalidData); + }, +); + Deno.test( { permissions: { read: true, net: true } }, async function connectTLSNotPrivateKey(): Promise { @@ -1206,6 +1238,22 @@ Deno.test( }, ); +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTLSNotKey(): Promise { + await assertRejects(async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + cert: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + key: "", + }); + }, Deno.errors.InvalidData); + }, +); + Deno.test( { permissions: { read: true, net: true } }, async function connectWithClientCert() { @@ -1231,6 +1279,81 @@ Deno.test( }, ); +Deno.test( + { permissions: { read: true, net: true } }, + async function connectWithCert() { + // 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/testdata/tls/localhost.key \ + // --cert cli/tests/testdata/tls/localhost.crt \ + // --cacert cli/tests/testdata/tls/RootCA.crt https://localhost:4552/ + const conn = await Deno.connectTls({ + hostname: "localhost", + port: 4552, + cert: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + key: await Deno.readTextFile( + "tests/testdata/tls/localhost.key", + ), + caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")], + }); + const result = decoder.decode(await readAll(conn)); + assertEquals(result, "PASS"); + conn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTlsConflictingCertOptions(): Promise { + await assertRejects( + async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + cert: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + certChain: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + key: await Deno.readTextFile( + "tests/testdata/tls/localhost.key", + ), + }); + }, + TypeError, + "Cannot specify both `certChain` and `cert`", + ); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function connectTlsConflictingKeyOptions(): Promise { + await assertRejects( + async () => { + await Deno.connectTls({ + hostname: "deno.land", + port: 443, + cert: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + privateKey: await Deno.readTextFile( + "tests/testdata/tls/localhost.crt", + ), + key: await Deno.readTextFile( + "tests/testdata/tls/localhost.key", + ), + }); + }, + TypeError, + "Cannot specify both `privateKey` and `key`", + ); + }, +); + Deno.test( { permissions: { read: true, net: true } }, async function connectTLSCaCerts() {