use super::certs;
use crate::{
client::config::{ClientCertSource, ClientCertificate, Config, ExtraCa},
error::{Error, IoErrorKind},
};
pub(crate) use async_native_tls::TlsStream;
use async_native_tls::{Certificate, Identity, TlsConnector};
use futures_util::io::{AsyncRead, AsyncWrite};
use secrecy::ExposeSecret;
use std::fs;
use tracing::{event, Level};
fn load_identity(cert: &ClientCertificate) -> crate::Result<Identity> {
match &cert.source {
ClientCertSource::CertAndKey { cert, key } => {
let has_ext = |p: &std::path::Path, exts: &[&str]| {
matches!(
p.extension().and_then(|e| e.to_str()),
Some(ext) if exts.iter().any(|e| ext.eq_ignore_ascii_case(e))
)
};
if !has_ext(cert, &["pem", "crt"]) || !has_ext(key, &["pem", "key"]) {
return Err(Error::Tls(
"The native-tls backend requires PEM certificate and key files; \
for a DER-bundled identity use `Config::client_certificate_pkcs12`."
.to_string(),
));
}
let cert_buf = fs::read(cert).map_err(|e| Error::Io {
kind: IoErrorKind::InvalidData,
message: format!(
"Could not read client certificate {}: {e}",
cert.to_string_lossy()
),
})?;
let key_buf = fs::read(key).map_err(|e| Error::Io {
kind: IoErrorKind::InvalidData,
message: format!(
"Could not read client private key {}: {e}",
key.to_string_lossy()
),
})?;
Ok(Identity::from_pkcs8(&cert_buf, &key_buf)?)
}
ClientCertSource::Pkcs12 { path, password } => {
let buf = fs::read(path).map_err(|e| Error::Io {
kind: IoErrorKind::InvalidData,
message: format!(
"Could not read PKCS#12 identity {}: {e}",
path.to_string_lossy()
),
})?;
Ok(Identity::from_pkcs12(&buf, password.expose_secret())?)
}
}
}
pub(crate) async fn create_tls_stream<S: AsyncRead + AsyncWrite + Unpin + Send>(
config: &Config,
stream: S,
) -> crate::Result<TlsStream<S>> {
let mut builder = TlsConnector::new();
if matches!(config.encryption, crate::EncryptionLevel::Strict) {
builder = builder.request_alpns(&[super::TDS_ALPN_PROTOCOL_NAME]);
}
if let Some(cert) = config.get_client_certificate() {
event!(
Level::DEBUG,
"Presenting a client certificate for mutual TLS."
);
builder = builder.identity(load_identity(cert)?);
}
if config.trust.bypass {
event!(
Level::WARN,
"Trusting the server certificate without validation."
);
builder = builder.danger_accept_invalid_certs(true);
builder = builder.danger_accept_invalid_hostnames(true);
builder = builder.use_sni(false);
} else {
for cert in load_extra_cas(&config.trust.extra_cas)? {
builder = builder.add_root_certificate(cert);
}
}
Ok(builder
.connect(config.get_hostname_in_certificate(), stream)
.await?)
}
fn load_extra_cas(extras: &[ExtraCa]) -> crate::Result<Vec<Certificate>> {
let mut out = Vec::new();
for extra in extras {
for cert in certs::trust_anchors(extra)? {
out.push(
Certificate::from_der(cert.as_ref())
.map_err(|e| certs::invalid_cert_error(extra, e))?,
);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::config::{ClientCertSource, ExtraCa};
use std::path::PathBuf;
#[test]
fn multi_cert_ca_file_loads_all_certs() {
let ders = certs::trust_anchors(&ExtraCa::File(PathBuf::from(
"docker/certs/server-full.crt",
)))
.expect("multi-cert CA file loads");
assert!(ders.len() >= 2, "must not truncate a multi-cert file");
for der in &ders {
Certificate::from_der(der.as_ref()).expect("each DER cert converts for native-tls");
}
}
#[test]
fn multi_cert_ca_bundle_loads_all_certs() {
let bytes = std::fs::read("docker/certs/server-full.crt").unwrap();
let ders = certs::trust_anchors(&ExtraCa::Bundle(bytes)).expect("multi-cert bundle loads");
assert!(ders.len() >= 2, "must not truncate a multi-cert bundle");
for der in &ders {
Certificate::from_der(der.as_ref()).expect("each DER cert converts for native-tls");
}
}
#[test]
fn empty_ca_file_is_hard_error_naming_source() {
let mut path = std::env::temp_dir();
path.push(format!("tiberius_nt_zero_{}.pem", std::process::id()));
std::fs::write(&path, b"# no certs\n").unwrap();
let err = certs::trust_anchors(&ExtraCa::File(path.clone()));
std::fs::remove_file(&path).ok();
assert!(format!("{:?}", err.unwrap_err()).contains("contained no usable certificates"));
}
#[test]
fn invalid_der_extra_ca_names_source() {
let err = match load_extra_cas(&[ExtraCa::Bundle(vec![0x30, 0x03, 0x02, 0x01, 0x7f])]) {
Ok(_) => panic!("an invalid DER extra CA must be a hard error"),
Err(e) => e,
};
let msg = format!("{err:?}");
assert!(
msg.contains("in-memory CA bundle") && msg.contains("contains an invalid certificate"),
"invalid DER must name the source, got: {msg}"
);
}
#[test]
fn multi_cert_extra_cas_load_all_via_helper() {
let certs = load_extra_cas(&[ExtraCa::File(PathBuf::from("docker/certs/server-full.crt"))])
.expect("multi-cert file loads");
assert!(certs.len() >= 2, "must not truncate a multi-cert file");
}
fn identity(cert: &str, key: &str) -> crate::Result<Identity> {
load_identity(&ClientCertificate {
source: ClientCertSource::CertAndKey {
cert: PathBuf::from(cert),
key: PathBuf::from(key),
},
})
}
#[test]
fn key_extension_rejected_as_certificate() {
assert!(matches!(
identity("client.key", "client.key"),
Err(Error::Tls(_))
));
}
#[test]
fn crt_extension_rejected_as_key() {
assert!(matches!(
identity("client.crt", "client.crt"),
Err(Error::Tls(_))
));
}
#[test]
fn valid_extensions_pass_extension_check() {
for (cert, key) in [
("client.crt", "client.key"),
("client.pem", "client.pem"),
("client.crt", "client.pem"),
("client.pem", "client.key"),
] {
match identity(cert, key) {
Err(Error::Tls(_)) => {
panic!("valid extensions {cert}/{key} were wrongly rejected by the check")
}
Err(Error::Io { .. }) => {}
Err(e) => panic!("expected an Io error from the missing file, got {e:?}"),
Ok(_) => panic!("nonexistent files should not yield an identity"),
}
}
}
}