Skip to content

Commit

Permalink
feat(tls): custom in memory CA certificates (#12219)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
lucacasonato committed Sep 30, 2021
1 parent 62920e4 commit 0d7a417
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 160 deletions.
47 changes: 31 additions & 16 deletions cli/dts/lib.deno.unstable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
* ```
*/
Expand All @@ -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;
}

Expand All @@ -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 });
* ```
*
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion cli/file_fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
84 changes: 38 additions & 46 deletions cli/http_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/unit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 8 additions & 31 deletions cli/tests/unit/fetch_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1250,14 +1226,15 @@ 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",
),
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,
Expand Down
4 changes: 2 additions & 2 deletions cli/tests/unit/http_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Loading

0 comments on commit 0d7a417

Please sign in to comment.