use std::io;
use std::path::Path;
use std::ptr;
use tracing::debug;
use windows_sys::Win32::Foundation;
use windows_sys::Win32::Security::Authentication::Identity;
use windows_sys::Win32::Security::Cryptography;
use super::cred::CredKind;
use super::errors::sec_status_to_io_error;
use super::handshake::SecCtx;
pub(crate) fn query_remote_cert_der(ctx: &SecCtx) -> io::Result<Vec<u8>> {
let mut cert_ctx_ptr: *mut Cryptography::CERT_CONTEXT = ptr::null_mut();
let status = unsafe {
Identity::QueryContextAttributesW(
ctx.raw(),
Identity::SECPKG_ATTR_REMOTE_CERT_CONTEXT,
&mut cert_ctx_ptr as *mut _ as *mut _,
)
};
if status != Foundation::SEC_E_OK {
return Err(sec_status_to_io_error(
status,
"QueryContextAttributesW(REMOTE_CERT_CONTEXT) failed",
));
}
if cert_ctx_ptr.is_null() {
return Err(io::Error::other(
"QueryContextAttributesW returned SEC_E_OK with null CERT_CONTEXT",
));
}
let der = unsafe {
let cert_ctx = &*cert_ctx_ptr;
std::slice::from_raw_parts(cert_ctx.pbCertEncoded, cert_ctx.cbCertEncoded as usize).to_vec()
};
unsafe {
Cryptography::CertFreeCertificateContext(cert_ctx_ptr);
}
Ok(der)
}
pub(crate) fn validate_after_handshake(
ctx: &SecCtx,
kind: CredKind,
server_certificate_path: Option<&Path>,
) -> Result<(), ValidationError> {
match (kind, server_certificate_path) {
(CredKind::AutoValidate, _) => {
debug!("win_tls: validate skipped (AutoValidate; SChannel did chain build inline)");
Ok(())
}
(CredKind::NoValidate, _) => {
debug!("win_tls: validate skipped (NoValidate; TrustServerCertificate=Yes)");
Ok(())
}
(CredKind::ManualValidate, Some(path)) => {
let der = query_remote_cert_der(ctx).map_err(ValidationError::QueryCert)?;
validate_pinned_cert(path, &der)
}
(CredKind::ManualValidate, None) => Err(ValidationError::ConfigMismatch(
"CredKind::ManualValidate requires a server_certificate_path".to_string(),
)),
}
}
fn validate_pinned_cert(path: &Path, der: &[u8]) -> Result<(), ValidationError> {
let result = super::super::certificate_validator::validate_server_certificate(path, der)
.map_err(ValidationError::Pin);
match &result {
Ok(()) => debug!(
der_len = der.len(),
"win_tls: validate (ManualValidate) pin match OK"
),
Err(e) => {
debug!(der_len = der.len(), error = %e, "win_tls: validate (ManualValidate) pin FAILED")
}
}
result
}
#[derive(Debug)]
pub(crate) enum ValidationError {
QueryCert(io::Error),
Pin(crate::error::Error),
ConfigMismatch(String),
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::QueryCert(e) => write!(f, "failed to fetch remote certificate: {e}"),
Self::Pin(e) => write!(f, "certificate pin validation failed: {e}"),
Self::ConfigMismatch(s) => write!(f, "{s}"),
}
}
}
impl std::error::Error for ValidationError {}
impl From<ValidationError> for crate::error::Error {
fn from(e: ValidationError) -> Self {
match e {
ValidationError::Pin(inner) => inner,
other => crate::error::Error::ImplementationError(other.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn novalidate_is_a_noop() {
let dummy = SecCtx::for_test_only();
let r = validate_after_handshake(&dummy, CredKind::NoValidate, None);
assert!(r.is_ok());
}
#[test]
fn autovalidate_is_a_noop_too() {
let dummy = SecCtx::for_test_only();
let r = validate_after_handshake(&dummy, CredKind::AutoValidate, None);
assert!(r.is_ok());
}
#[test]
fn manualvalidate_without_path_is_config_error() {
let dummy = SecCtx::for_test_only();
let r = validate_after_handshake(&dummy, CredKind::ManualValidate, None);
assert!(matches!(r, Err(ValidationError::ConfigMismatch(_))));
}
#[test]
fn manualvalidate_with_path_on_dummy_ctx_fails_to_query_cert() {
let dummy = SecCtx::for_test_only();
let path = std::path::PathBuf::from("nonexistent-cert.cer");
let r = validate_after_handshake(&dummy, CredKind::ManualValidate, Some(&path));
assert!(matches!(r, Err(ValidationError::QueryCert(_))));
}
#[test]
fn query_remote_cert_der_on_dummy_ctx_errors() {
let dummy = SecCtx::for_test_only();
assert!(query_remote_cert_der(&dummy).is_err());
}
fn fixture(name: &str) -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("test_certificates")
.join(name)
}
#[test]
fn validate_pinned_cert_matches_identical_der() {
let der_path = fixture("valid_cert.der");
let der = std::fs::read(&der_path).expect("read der fixture");
let r = validate_pinned_cert(&der_path, &der);
assert!(r.is_ok(), "expected pin match, got {r:?}");
}
#[test]
fn validate_pinned_cert_missing_pin_file_is_pin_error() {
let der = std::fs::read(fixture("valid_cert.der")).expect("read der fixture");
let r = validate_pinned_cert(std::path::Path::new("definitely-missing-pin.cer"), &der);
assert!(matches!(r, Err(ValidationError::Pin(_))));
}
#[test]
fn validate_pinned_cert_mismatch_is_pin_error() {
let pin_path = fixture("valid_cert.der");
let mut der = std::fs::read(&pin_path).expect("read der fixture");
let mid = der.len() / 2;
der[mid] ^= 0xFF;
let r = validate_pinned_cert(&pin_path, &der);
assert!(matches!(r, Err(ValidationError::Pin(_))));
}
#[test]
fn validation_error_display_renders_each_variant() {
let q = ValidationError::QueryCert(io::Error::other("boom"));
assert!(q.to_string().contains("fetch remote certificate"));
let p = ValidationError::Pin(crate::error::Error::NoServerCertificate);
assert!(p.to_string().contains("pin validation failed"));
let c = ValidationError::ConfigMismatch("wired wrong".to_string());
assert_eq!(c.to_string(), "wired wrong");
}
#[test]
fn validation_error_converts_to_crate_error() {
let pin = ValidationError::Pin(crate::error::Error::NoServerCertificate);
assert!(matches!(
crate::error::Error::from(pin),
crate::error::Error::NoServerCertificate
));
let cfg = ValidationError::ConfigMismatch("nope".to_string());
assert!(matches!(
crate::error::Error::from(cfg),
crate::error::Error::ImplementationError(_)
));
let q = ValidationError::QueryCert(io::Error::other("boom"));
assert!(matches!(
crate::error::Error::from(q),
crate::error::Error::ImplementationError(_)
));
}
}