use super::certs;
use crate::{
client::config::{ClientCertSource, ClientCertificate, Config, ExtraCa},
error::{Error, IoErrorKind},
};
use futures_util::io::{AsyncRead, AsyncWrite};
pub(crate) use opentls::async_io::{TlsConnector, TlsStream};
use opentls::{Certificate, Identity};
use secrecy::ExposeSecret;
use std::fs;
use tracing::{event, Level};
fn load_identity(cert: &ClientCertificate) -> crate::Result<Identity> {
match &cert.source {
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())?)
}
ClientCertSource::CertAndKey { .. } => Err(Error::Tls(
"The vendored-openssl (opentls) backend does not support separate \
certificate/key files for client authentication; supply a PKCS#12 \
bundle via `Config::client_certificate_pkcs12` instead."
.to_string(),
)),
}
}
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) {
event!(
Level::WARN,
"OpenTLS does not support ALPN, so the TDS 8.0 ALPN protocol will not be requested. SQL Server will assume TDS 8.0."
);
}
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 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 opentls");
}
}
#[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 opentls");
}
}
#[test]
fn empty_ca_file_is_hard_error_naming_source() {
let mut path = std::env::temp_dir();
path.push(format!("tiberius_ot_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");
}
}