Skip to content

Commit

Permalink
feat: Deno.ConnectTlsOptions.{cert,key} (#22274)
Browse files Browse the repository at this point in the history
Towards #22197
  • Loading branch information
iuioiua authored Feb 18, 2024
1 parent 3c7057d commit 9a43a2b
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 19 deletions.
32 changes: 30 additions & 2 deletions ext/net/02_tls.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
20 changes: 18 additions & 2 deletions ext/net/lib.deno_net.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 14 additions & 15 deletions ext/net/ops_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ impl Resource for TlsStreamResource {
pub struct ConnectTlsArgs {
cert_file: Option<String>,
ca_certs: Vec<String>,
cert_chain: Option<String>,
private_key: Option<String>,
cert: Option<String>,
key: Option<String>,
alpn_protocols: Option<Vec<String>>,
}

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

Expand Down
123 changes: 123 additions & 0 deletions tests/unit/tls_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,22 @@ Deno.test(
},
);

Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSBadCertKey(): Promise<void> {
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<void> {
Expand All @@ -1190,6 +1206,22 @@ Deno.test(
},
);

Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSBadKey(): Promise<void> {
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<void> {
Expand All @@ -1206,6 +1238,22 @@ Deno.test(
},
);

Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSNotKey(): Promise<void> {
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() {
Expand All @@ -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<void> {
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<void> {
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() {
Expand Down

0 comments on commit 9a43a2b

Please sign in to comment.