use crate::client::config::ExtraCa;
use crate::error::IoErrorKind;
use rustls_pki_types::{pem::PemObject, CertificateDer};
use std::{fs, path::Path};
pub(crate) fn trust_anchors(extra: &ExtraCa) -> crate::Result<Vec<CertificateDer<'static>>> {
let certs = match extra {
ExtraCa::File(path) => certs_from_file(path)?,
ExtraCa::Bundle(bytes) => certs_from_bundle(bytes)?,
};
if certs.is_empty() {
return Err(crate::Error::Io {
kind: IoErrorKind::InvalidData,
message: format!(
"the CA source `{}` contained no usable certificates",
source_name(extra)
),
});
}
Ok(certs)
}
pub(crate) fn source_name(extra: &ExtraCa) -> String {
match extra {
ExtraCa::File(path) => path.to_string_lossy().into_owned(),
ExtraCa::Bundle(bytes) => format!("in-memory CA bundle ({} bytes)", bytes.len()),
}
}
pub(crate) fn invalid_cert_error<E: std::fmt::Display>(extra: &ExtraCa, err: E) -> crate::Error {
crate::Error::Io {
kind: IoErrorKind::InvalidData,
message: format!(
"the CA source `{}` contains an invalid certificate: {err}",
source_name(extra)
),
}
}
fn strip_bom(bytes: &[u8]) -> &[u8] {
bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes)
}
pub(crate) fn certs_from_file(path: &Path) -> crate::Result<Vec<CertificateDer<'static>>> {
let buf = fs::read(path).map_err(|e| crate::Error::Io {
kind: IoErrorKind::InvalidData,
message: format!("Could not read certificate {}: {e}", path.to_string_lossy()),
})?;
match path.extension() {
Some(ext) if ext.eq_ignore_ascii_case("pem") || ext.eq_ignore_ascii_case("crt") => {
CertificateDer::pem_slice_iter(strip_bom(&buf))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| crate::Error::Io {
kind: IoErrorKind::InvalidData,
message: format!(
"Failed to parse PEM certificate {}: {e}",
path.to_string_lossy()
),
})
}
Some(ext) if ext.eq_ignore_ascii_case("der") && buf.is_empty() => Ok(vec![]),
Some(ext) if ext.eq_ignore_ascii_case("der") => Ok(vec![CertificateDer::from(buf)]),
Some(_) | None => Err(crate::Error::Io {
kind: IoErrorKind::InvalidInput,
message: format!(
"Certificate {} has an unsupported file-extension! Supported types are pem, crt and der.",
path.to_string_lossy()
),
}),
}
}
pub(crate) fn certs_from_bundle(bytes: &[u8]) -> crate::Result<Vec<CertificateDer<'static>>> {
let bytes = strip_bom(bytes);
if looks_like_pem(bytes) {
CertificateDer::pem_slice_iter(bytes)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| crate::Error::Io {
kind: IoErrorKind::InvalidData,
message: format!("Failed to parse PEM CA bundle: {e}"),
})
} else if bytes.is_empty() {
Ok(vec![])
} else {
Ok(vec![CertificateDer::from(bytes.to_vec())])
}
}
fn looks_like_pem(bytes: &[u8]) -> bool {
const MARKER: &[u8] = b"-----BEGIN";
if bytes.starts_with(MARKER) {
return true;
}
bytes
.windows(MARKER.len() + 1)
.any(|w| matches!(w[0], b'\n' | b'\r') && &w[1..] == MARKER)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn certs_from_file_reads_single_pem() {
let chain = certs_from_file(Path::new("docker/certs/server.crt")).unwrap();
assert_eq!(chain.len(), 1);
}
#[test]
fn certs_from_file_reads_multi_pem_chain() {
let chain = certs_from_file(Path::new("docker/certs/server-full.crt")).unwrap();
assert!(
chain.len() >= 2,
"server-full.crt is a multi-certificate chain"
);
assert_ne!(
chain[0].as_ref(),
chain[1].as_ref(),
"the two blocks must be distinct certificates, not a repeated first block"
);
}
#[test]
fn certs_from_file_missing_file_preserves_io_error() {
let err = certs_from_file(Path::new("docker/certs/does-not-exist.crt")).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("Could not read certificate") && msg.contains("does-not-exist.crt"),
"error should name the read failure and path, got: {msg}"
);
}
#[test]
fn certs_from_file_unsupported_extension_errors() {
let err = certs_from_file(Path::new("docker/certs/README.md")).unwrap_err();
assert!(format!("{err:?}").contains("unsupported file-extension"));
}
#[test]
fn certs_from_file_reads_der() {
let der = certs_from_file(Path::new("docker/certs/customCA.crt"))
.unwrap()
.into_iter()
.next()
.unwrap()
.as_ref()
.to_vec();
let mut path = std::env::temp_dir();
path.push(format!(
"tiberius_certs_from_file_{}.der",
std::process::id()
));
std::fs::write(&path, &der).unwrap();
let chain = certs_from_file(&path);
std::fs::remove_file(&path).ok();
assert_eq!(chain.unwrap().len(), 1);
}
#[test]
fn certs_from_file_zero_cert_pem_is_empty() {
let mut path = std::env::temp_dir();
path.push(format!("tiberius_certs_zero_{}.pem", std::process::id()));
std::fs::write(&path, b"# no certificates here\n").unwrap();
let chain = certs_from_file(&path);
std::fs::remove_file(&path).ok();
assert_eq!(chain.unwrap().len(), 0);
}
#[test]
fn certs_from_bundle_sniffs_pem_and_reads_all() {
let bytes = std::fs::read("docker/certs/server-full.crt").unwrap();
let certs = certs_from_bundle(&bytes).unwrap();
assert!(certs.len() >= 2, "PEM bundle must yield every block");
assert_ne!(
certs[0].as_ref(),
certs[1].as_ref(),
"the blocks must be distinct certificates, not a repeated first block"
);
}
#[test]
fn certs_from_bundle_sniffs_der() {
let der = certs_from_file(Path::new("docker/certs/customCA.crt"))
.unwrap()
.into_iter()
.next()
.unwrap()
.as_ref()
.to_vec();
let certs = certs_from_bundle(&der).unwrap();
assert_eq!(certs.len(), 1);
}
#[test]
fn certs_from_bundle_garbage_pem_errors() {
let bytes =
b"-----BEGIN CERTIFICATE-----\nnot valid base64!!!\n-----END CERTIFICATE-----\n";
let err = certs_from_bundle(bytes).unwrap_err();
assert!(format!("{err:?}").contains("Failed to parse PEM CA bundle"));
}
#[test]
fn trust_anchors_file_zero_certs_errors_naming_source() {
let mut path = std::env::temp_dir();
path.push(format!(
"tiberius_trust_anchors_zero_{}.pem",
std::process::id()
));
std::fs::write(&path, b"# no certificates here\n").unwrap();
let err = trust_anchors(&ExtraCa::File(path.clone()));
std::fs::remove_file(&path).ok();
let msg = format!("{:?}", err.unwrap_err());
assert!(
msg.contains("contained no usable certificates") && msg.contains(".pem"),
"error should name the empty source, got: {msg}"
);
}
#[test]
fn trust_anchors_bundle_no_certificate_blocks_errors_naming_source() {
let bundle = b"-----BEGIN PRIVATE KEY-----\nMIIB\n-----END PRIVATE KEY-----\n".to_vec();
let err = trust_anchors(&ExtraCa::Bundle(bundle)).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("contained no usable certificates") && msg.contains("in-memory CA bundle"),
"error should name the empty in-memory source, got: {msg}"
);
}
#[test]
fn trust_anchors_bundle_multi_pem() {
let bytes = std::fs::read("docker/certs/server-full.crt").unwrap();
let certs = trust_anchors(&ExtraCa::Bundle(bytes)).unwrap();
assert!(certs.len() >= 2);
}
#[test]
fn trust_anchors_file_loads_all() {
let certs = trust_anchors(&ExtraCa::File(PathBuf::from(
"docker/certs/server-full.crt",
)))
.unwrap();
assert!(certs.len() >= 2);
}
#[test]
fn empty_der_bundle_is_zero_certs_naming_source() {
assert_eq!(certs_from_bundle(&[]).unwrap().len(), 0);
let err = trust_anchors(&ExtraCa::Bundle(Vec::new())).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("contained no usable certificates") && msg.contains("in-memory CA bundle"),
"empty bundle must be a source-naming zero-cert error, got: {msg}"
);
}
#[test]
fn empty_der_file_is_zero_certs_naming_source() {
let mut path = std::env::temp_dir();
path.push(format!("tiberius_empty_{}.der", std::process::id()));
std::fs::write(&path, b"").unwrap();
assert_eq!(certs_from_file(&path).unwrap().len(), 0);
let err = trust_anchors(&ExtraCa::File(path.clone()));
std::fs::remove_file(&path).ok();
let msg = format!("{:?}", err.unwrap_err());
assert!(
msg.contains("contained no usable certificates") && msg.contains(".der"),
"empty .der must be a source-naming zero-cert error, got: {msg}"
);
}
#[test]
fn bom_prefixed_pem_still_parses() {
let pem = std::fs::read("docker/certs/server-full.crt").unwrap();
let mut with_bom = vec![0xEF, 0xBB, 0xBF];
with_bom.extend_from_slice(&pem);
assert!(
certs_from_bundle(&with_bom).unwrap().len() >= 2,
"BOM-prefixed PEM bundle must parse every block"
);
let mut path = std::env::temp_dir();
path.push(format!("tiberius_bom_{}.pem", std::process::id()));
std::fs::write(&path, &with_bom).unwrap();
let res = certs_from_file(&path);
std::fs::remove_file(&path).ok();
assert!(
res.unwrap().len() >= 2,
"BOM-prefixed PEM file must parse every block"
);
}
#[test]
fn der_with_embedded_begin_marker_is_treated_as_der() {
let der = certs_from_file(Path::new("docker/certs/customCA.crt"))
.unwrap()
.into_iter()
.next()
.unwrap()
.as_ref()
.to_vec();
let mut crafted = der.clone();
let mid = crafted.len() / 2;
crafted.splice(mid..mid, b"-----BEGIN CERTIFICATE-----".iter().copied());
assert!(
!looks_like_pem(&crafted),
"mid-body marker must not sniff PEM"
);
assert!(!looks_like_pem(&der));
assert_eq!(certs_from_bundle(&der).unwrap().len(), 1);
}
#[test]
fn invalid_cert_error_names_source_for_every_shape() {
let file = invalid_cert_error(&ExtraCa::File(PathBuf::from("/tmp/ca.der")), "bad tag");
let m = format!("{file:?}");
assert!(
m.contains("/tmp/ca.der")
&& m.contains("contains an invalid certificate")
&& m.contains("bad tag"),
"file source must be named with the underlying error, got: {m}"
);
let bundle = invalid_cert_error(&ExtraCa::Bundle(vec![0u8; 7]), "bad der");
let m2 = format!("{bundle:?}");
assert!(
m2.contains("in-memory CA bundle (7 bytes)")
&& m2.contains("contains an invalid certificate"),
"bundle source must be named, got: {m2}"
);
}
#[test]
fn looks_like_pem_requires_line_start_marker() {
assert!(looks_like_pem(b"-----BEGIN CERTIFICATE-----\n"));
assert!(looks_like_pem(
b"# a comment\n-----BEGIN CERTIFICATE-----\n"
));
assert!(looks_like_pem(b"lead\r-----BEGIN CERTIFICATE-----\r\n"));
assert!(!looks_like_pem(b"prefix -----BEGIN CERTIFICATE-----"));
assert!(!looks_like_pem(b""));
assert!(!looks_like_pem(b"short"));
}
}