use std::sync::Arc;
use jmap_base_client::{ClientConfig, ClientError, CustomCaTransport, JmapClient, NoneAuth};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer};
use tokio_rustls::rustls::{self, CertificateError, ServerConfig};
const CA_A: &[u8] = include_bytes!("fixtures/tls/trust-scope/ca-a.der");
const CA_B: &[u8] = include_bytes!("fixtures/tls/trust-scope/ca-b.der");
const LEAF_A: &[u8] = include_bytes!("fixtures/tls/trust-scope/leaf-a.der");
const LEAF_A_KEY: &[u8] = include_bytes!("fixtures/tls/trust-scope/leaf-a.key.der");
const LEAF_B: &[u8] = include_bytes!("fixtures/tls/trust-scope/leaf-b.der");
const LEAF_B_KEY: &[u8] = include_bytes!("fixtures/tls/trust-scope/leaf-b.key.der");
const SESSION_JSON: &str = include_str!("fixtures/jmap/session.json");
async fn serve_once(leaf: &'static [u8], key: &'static [u8]) -> String {
let config =
ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("ring supports the default protocol versions")
.with_no_client_auth()
.with_single_cert(
vec![CertificateDer::from(leaf)],
PrivatePkcs8KeyDer::from(key).into(),
)
.expect("fixture leaf certificate and key must match");
let acceptor = TlsAcceptor::from(Arc::new(config));
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let port = listener.local_addr().expect("local_addr").port();
tokio::spawn(async move {
let (tcp, _) = listener.accept().await.expect("accept");
let Ok(mut tls) = acceptor.accept(tcp).await else {
return;
};
let mut request = Vec::new();
let mut buf = [0u8; 1024];
while !request.windows(4).any(|w| w == b"\r\n\r\n") {
let n = tls.read(&mut buf).await.expect("read request");
assert!(n > 0, "client closed before sending a full request");
request.extend_from_slice(&buf[..n]);
}
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{SESSION_JSON}",
SESSION_JSON.len()
);
tls.write_all(response.as_bytes())
.await
.expect("write response");
tls.shutdown().await.expect("shutdown");
});
format!("https://127.0.0.1:{port}")
}
fn client_trusting(ca: &[u8], base_url: &str) -> JmapClient {
JmapClient::new(
CustomCaTransport::new(ca.to_vec()),
NoneAuth,
base_url,
ClientConfig::default(),
)
.expect("client construction must succeed for a valid DER CA")
}
fn rustls_error_in_chain<'a>(
err: &'a (dyn std::error::Error + 'static),
) -> Option<&'a rustls::Error> {
let mut cur = Some(err);
while let Some(e) = cur {
if let Some(r) = e.downcast_ref::<rustls::Error>() {
return Some(r);
}
cur = match e.downcast_ref::<std::io::Error>() {
Some(io) => io
.get_ref()
.map(|inner| inner as &(dyn std::error::Error + 'static)),
None => e.source(),
};
}
None
}
#[tokio::test]
async fn pinned_ca_accepts_its_own_leaf() {
let url = serve_once(LEAF_B, LEAF_B_KEY).await;
let session = client_trusting(CA_B, &url)
.fetch_session()
.await
.expect("leaf-b is signed by the pinned ca-b");
assert_eq!(session.username, "john@example.com");
}
#[tokio::test]
async fn server_a_is_reachable_when_its_ca_is_pinned() {
let url = serve_once(LEAF_A, LEAF_A_KEY).await;
client_trusting(CA_A, &url)
.fetch_session()
.await
.expect("leaf-a is signed by the pinned ca-a");
}
#[tokio::test]
async fn pinned_ca_rejects_leaf_from_other_ca() {
let url = serve_once(LEAF_A, LEAF_A_KEY).await;
let err = client_trusting(CA_B, &url)
.fetch_session()
.await
.expect_err("leaf-a is not signed by the pinned ca-b");
let ClientError::Http(http) = &err else {
panic!("expected ClientError::Http, got {err:?}");
};
assert_eq!(
rustls_error_in_chain(http),
Some(&rustls::Error::InvalidCertificate(
CertificateError::UnknownIssuer
)),
"source chain of {http:?}"
);
}