Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deno.ConnectTlsOptions.{cert,key} #22274

Merged
merged 9 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 30 additions & 2 deletions ext/net/02_tls.js
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
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
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
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