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(fetch): TLS client certificates (mutual authentication) for fetch() #11721

Merged
merged 11 commits into from
Aug 25, 2021
Merged
2 changes: 2 additions & 0 deletions cli/dts/lib.deno.unstable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,8 @@ declare namespace Deno {
*/
caData?: string;
proxy?: Proxy;
certChain?: string;
privateKey?: string;
}

export interface Proxy {
Expand Down
1 change: 1 addition & 0 deletions cli/file_fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ impl FileFetcher {
None,
None,
unsafely_ignore_certificate_errors,
None,
)?,
blob_store,
})
Expand Down
17 changes: 15 additions & 2 deletions cli/http_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,15 @@ mod tests {
use std::fs::read;

fn create_test_client(ca_data: Option<Vec<u8>>) -> Client {
create_http_client("test_client".to_string(), None, ca_data, None, None)
.unwrap()
create_http_client(
"test_client".to_string(),
None,
ca_data,
None,
None,
None,
)
.unwrap()
}

#[tokio::test]
Expand Down Expand Up @@ -340,6 +347,7 @@ mod tests {
),
None,
None,
None,
)
.unwrap();
let result = fetch_once(FetchOnceArgs {
Expand Down Expand Up @@ -370,6 +378,7 @@ mod tests {
None,
None,
None,
None,
)
.unwrap();

Expand Down Expand Up @@ -402,6 +411,7 @@ mod tests {
None,
None,
None,
None,
)
.unwrap();

Expand Down Expand Up @@ -440,6 +450,7 @@ mod tests {
),
None,
None,
None,
)
.unwrap();
let result = fetch_once(FetchOnceArgs {
Expand Down Expand Up @@ -480,6 +491,7 @@ mod tests {
),
None,
None,
None,
)
.unwrap();
let result = fetch_once(FetchOnceArgs {
Expand Down Expand Up @@ -533,6 +545,7 @@ mod tests {
),
None,
None,
None,
)
.unwrap();
let result = fetch_once(FetchOnceArgs {
Expand Down
80 changes: 80 additions & 0 deletions cli/tests/unit/fetch_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1211,3 +1211,83 @@ unitTest(
assertEquals(res.body, null);
},
);

unitTest(
{ perms: { read: true, net: true } },
async function fetchClientCertWrongPrivateKey(): Promise<void> {
await assertThrowsAsync(async () => {
const client = Deno.createHttpClient({
certChain: "bad data",
privateKey: await Deno.readTextFile(
"cli/tests/testdata/tls/localhost.key",
),
});
await fetch("https://localhost:5552/fixture.json", {
client,
});
}, Deno.errors.InvalidData);
},
);

unitTest(
{ perms: { read: true, net: true } },
async function fetchClientCertBadPrivateKey(): Promise<void> {
await assertThrowsAsync(async () => {
const client = Deno.createHttpClient({
certChain: await Deno.readTextFile(
"cli/tests/testdata/tls/localhost.crt",
),
privateKey: "bad data",
});
await fetch("https://localhost:5552/fixture.json", {
client,
});
}, Deno.errors.InvalidData);
},
);

unitTest(
{ perms: { read: true, net: true } },
async function fetchClientCertNotPrivateKey(): Promise<void> {
await assertThrowsAsync(async () => {
const client = Deno.createHttpClient({
certChain: await Deno.readTextFile(
"cli/tests/testdata/tls/localhost.crt",
),
privateKey: "",
});
await fetch("https://localhost:5552/fixture.json", {
client,
});
}, Deno.errors.InvalidData);
},
);

unitTest(
{ perms: { read: true, net: true } },
async function fetchCustomClientPrivateKey(): Promise<
void
> {
const data = "Hello World";
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"),
});
const response = await fetch("https://localhost:5552/echo_server", {
client,
method: "POST",
body: new TextEncoder().encode(data),
});
assertEquals(
response.headers.get("user-agent"),
`Deno/${Deno.version.deno}`,
);
await response.text();
client.close();
},
);
25 changes: 23 additions & 2 deletions ext/fetch/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub fn init<P: FetchPermissions + 'static>(
proxy: Option<Proxy>,
request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>,
unsafely_ignore_certificate_errors: Option<Vec<String>>,
client_cert_chain_and_key: Option<(String, String)>,
) -> Extension {
Extension::builder()
.js(include_js_files!(
Expand Down Expand Up @@ -90,6 +91,7 @@ pub fn init<P: FetchPermissions + 'static>(
None,
proxy.clone(),
unsafely_ignore_certificate_errors.clone(),
client_cert_chain_and_key.clone(),
)
.unwrap()
});
Expand All @@ -100,6 +102,7 @@ pub fn init<P: FetchPermissions + 'static>(
request_builder_hook,
unsafely_ignore_certificate_errors: unsafely_ignore_certificate_errors
.clone(),
client_cert_chain_and_key: client_cert_chain_and_key.clone(),
});
Ok(())
})
Expand All @@ -112,6 +115,7 @@ pub struct HttpClientDefaults {
pub proxy: Option<Proxy>,
pub request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>,
pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
pub client_cert_chain_and_key: Option<(String, String)>,
}

pub trait FetchPermissions {
Expand Down Expand Up @@ -508,6 +512,8 @@ pub struct CreateHttpClientOptions {
ca_file: Option<String>,
ca_data: Option<ByteString>,
proxy: Option<Proxy>,
cert_chain: Option<String>,
private_key: Option<String>,
}

pub fn op_create_http_client<FP>(
Expand All @@ -529,6 +535,21 @@ where
permissions.check_net_url(&url)?;
}

let client_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 defaults = state.borrow::<HttpClientDefaults>();
let cert_data =
get_cert_data(args.ca_file.as_deref(), args.ca_data.as_deref())?;
Expand All @@ -539,8 +560,8 @@ where
cert_data,
args.proxy,
defaults.unsafely_ignore_certificate_errors.clone(),
)
.unwrap();
client_cert_chain_and_key,
)?;

let rid = state.resource_table.add(HttpClientResource::new(client));
Ok(rid)
Expand Down
52 changes: 2 additions & 50 deletions ext/net/ops_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ use deno_core::RcRef;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_tls::create_client_config;
use deno_tls::rustls::internal::pemfile::certs;
use deno_tls::rustls::internal::pemfile::pkcs8_private_keys;
use deno_tls::rustls::internal::pemfile::rsa_private_keys;
use deno_tls::load_certs;
use deno_tls::load_private_keys;
use deno_tls::rustls::Certificate;
use deno_tls::rustls::ClientConfig;
use deno_tls::rustls::ClientSession;
Expand All @@ -58,7 +57,6 @@ use std::cell::RefCell;
use std::convert::From;
use std::fs::File;
use std::io;
use std::io::BufRead;
use std::io::BufReader;
use std::io::ErrorKind;
use std::ops::Deref;
Expand Down Expand Up @@ -862,58 +860,12 @@ where
})
}

fn load_certs(reader: &mut dyn BufRead) -> Result<Vec<Certificate>, AnyError> {
let certs = certs(reader)
.map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?;

if certs.is_empty() {
let e = custom_error("InvalidData", "No certificates found in cert file");
return Err(e);
}

Ok(certs)
}

fn load_certs_from_file(path: &str) -> Result<Vec<Certificate>, AnyError> {
let cert_file = File::open(path)?;
let reader = &mut BufReader::new(cert_file);
load_certs(reader)
}

fn key_decode_err() -> AnyError {
custom_error("InvalidData", "Unable to decode key")
}

fn key_not_found_err() -> AnyError {
custom_error("InvalidData", "No keys found in key file")
}

/// Starts with -----BEGIN RSA PRIVATE KEY-----
fn load_rsa_keys(mut bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> {
let keys = rsa_private_keys(&mut bytes).map_err(|_| key_decode_err())?;
Ok(keys)
}

/// Starts with -----BEGIN PRIVATE KEY-----
fn load_pkcs8_keys(mut bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> {
let keys = pkcs8_private_keys(&mut bytes).map_err(|_| key_decode_err())?;
Ok(keys)
}

fn load_private_keys(bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> {
let mut keys = load_rsa_keys(bytes)?;

if keys.is_empty() {
keys = load_pkcs8_keys(bytes)?;
}

if keys.is_empty() {
return Err(key_not_found_err());
}

Ok(keys)
}

fn load_private_keys_from_file(
path: &str,
) -> Result<Vec<PrivateKey>, AnyError> {
Expand Down
Loading