Skip to content

Commit

Permalink
feat(fetch): mTLS client certificates for fetch() (denoland#11721)
Browse files Browse the repository at this point in the history
This commit adds support for specifying client certificates when using fetch, by means of `Deno.createHttpClient`.
  • Loading branch information
cryptographix committed Aug 25, 2021
1 parent 5d814a4 commit dccf4cb
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 56 deletions.
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

0 comments on commit dccf4cb

Please sign in to comment.