use std::{
convert::TryInto,
mem::{self, MaybeUninit},
os::raw::c_void,
ptr::{self, NonNull},
sync::Arc,
};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
use rustls::crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider};
use rustls::pki_types;
use rustls::{
CertificateError, DigitallySignedStruct, Error as TlsError, Error::InvalidCertificate,
SignatureScheme,
};
use windows_sys::Win32::{
Foundation::{
CERT_E_CN_NO_MATCH, CERT_E_EXPIRED, CERT_E_INVALID_NAME, CERT_E_UNTRUSTEDROOT,
CERT_E_WRONG_USAGE, CRYPT_E_REVOKED, FILETIME, TRUE,
},
Security::Cryptography::{
CertAddEncodedCertificateToStore, CertCloseStore, CertCreateCertificateChainEngine,
CertFreeCertificateChain, CertFreeCertificateChainEngine, CertFreeCertificateContext,
CertGetCertificateChain, CertOpenStore, CertSetCertificateContextProperty,
CertVerifyCertificateChainPolicy, HTTPSPolicyCallbackData, AUTHTYPE_SERVER,
CERT_CHAIN_CACHE_END_CERT, CERT_CHAIN_CONTEXT,
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS, CERT_CHAIN_POLICY_PARA,
CERT_CHAIN_POLICY_SSL, CERT_CHAIN_POLICY_STATUS,
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT, CERT_CHAIN_REVOCATION_CHECK_END_CERT,
CERT_CONTEXT, CERT_OCSP_RESPONSE_PROP_ID, CERT_SET_PROPERTY_IGNORE_PERSIST_ERROR_FLAG,
CERT_STORE_ADD_ALWAYS, CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG, CERT_STORE_PROV_MEMORY,
CERT_STRONG_SIGN_PARA, CERT_TRUST_IS_PARTIAL_CHAIN, CERT_TRUST_IS_UNTRUSTED_ROOT,
CERT_USAGE_MATCH, CRYPT_INTEGER_BLOB, CTL_USAGE, HCERTSTORE, USAGE_MATCH_TYPE_AND,
X509_ASN_ENCODING,
},
};
use super::{log_server_cert, ALLOWED_EKUS};
#[allow(non_camel_case_types, non_snake_case)]
#[repr(C)]
struct CERT_CHAIN_PARA {
pub cbSize: u32,
pub RequestedUsage: CERT_USAGE_MATCH,
pub RequestedIssuancePolicy: CERT_USAGE_MATCH,
pub dwUrlRetrievalTimeout: u32,
pub fCheckRevocationFreshnessTime: i32, pub dwRevocationFreshnessTime: u32,
pub pftCacheResync: *mut FILETIME,
#[cfg(not(target_vendor = "win7"))]
pub pStrongSignPara: *const CERT_STRONG_SIGN_PARA,
#[cfg(not(target_vendor = "win7"))]
pub dwStrongSignFlags: u32,
}
#[allow(non_camel_case_types, non_snake_case)]
#[repr(C)]
#[derive(Clone, Copy)]
pub struct CERT_CHAIN_ENGINE_CONFIG {
pub cbSize: u32,
pub hRestrictedRoot: HCERTSTORE,
pub hRestrictedTrust: HCERTSTORE,
pub hRestrictedOther: HCERTSTORE,
pub cAdditionalStore: u32,
pub rghAdditionalStore: *mut HCERTSTORE,
pub dwFlags: u32,
pub dwUrlRetrievalTimeout: u32,
pub MaximumCachedCertificates: u32,
pub CycleDetectionModulus: u32,
pub hExclusiveRoot: HCERTSTORE,
pub hExclusiveTrustedPeople: HCERTSTORE,
#[cfg(not(target_vendor = "win7"))]
pub dwExclusiveFlags: u32,
}
use crate::verification::invalid_certificate;
unsafe impl ZeroedWithSize for CERT_CHAIN_PARA {
fn zeroed_with_size() -> Self {
let mut new: Self = unsafe { mem::zeroed() };
new.cbSize = Self::SIZE;
new
}
}
unsafe impl ZeroedWithSize for HTTPSPolicyCallbackData {
fn zeroed_with_size() -> Self {
let mut new: Self = unsafe { mem::zeroed() };
new.Anonymous.cbSize = Self::SIZE;
new
}
}
unsafe impl ZeroedWithSize for CERT_CHAIN_POLICY_PARA {
fn zeroed_with_size() -> Self {
let mut new: Self = unsafe { mem::zeroed() };
new.cbSize = Self::SIZE;
new
}
}
unsafe impl ZeroedWithSize for CERT_CHAIN_ENGINE_CONFIG {
fn zeroed_with_size() -> Self {
let mut new: Self = unsafe { mem::zeroed() };
new.cbSize = Self::SIZE;
new
}
}
struct CertChain {
inner: NonNull<CERT_CHAIN_CONTEXT>,
}
impl CertChain {
fn verify_chain_policy(
&self,
mut server_null_terminated: Vec<u16>,
) -> Result<CERT_CHAIN_POLICY_STATUS, TlsError> {
let mut extra_params = HTTPSPolicyCallbackData::zeroed_with_size();
extra_params.dwAuthType = AUTHTYPE_SERVER;
extra_params.pwszServerName = server_null_terminated.as_mut_ptr();
let mut params = CERT_CHAIN_POLICY_PARA::zeroed_with_size();
params.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
params.pvExtraPolicyPara = NonNull::from(&mut extra_params).cast::<c_void>().as_ptr();
let mut status: MaybeUninit<CERT_CHAIN_POLICY_STATUS> = MaybeUninit::uninit();
let res = unsafe {
CertVerifyCertificateChainPolicy(
CERT_CHAIN_POLICY_SSL,
self.inner.as_ptr(),
¶ms,
status.as_mut_ptr(),
)
};
if res != TRUE {
return Err(TlsError::General(String::from(
"TLS certificate verification was unavailable on the system!",
)));
}
let status = unsafe { status.assume_init() };
Ok(status)
}
}
impl Drop for CertChain {
fn drop(&mut self) {
unsafe { CertFreeCertificateChain(self.inner.as_ptr()) }
}
}
struct Certificate {
inner: NonNull<CERT_CONTEXT>,
}
impl Certificate {
unsafe fn set_property(
&mut self,
prop_id: u32,
prop_data: *const c_void,
) -> Result<(), TlsError> {
call_with_last_error(|| {
(CertSetCertificateContextProperty(
self.inner.as_ptr(),
prop_id,
CERT_SET_PROPERTY_IGNORE_PERSIST_ERROR_FLAG,
prop_data,
) == TRUE)
.then_some(())
})
}
}
impl Drop for Certificate {
fn drop(&mut self) {
unsafe { CertFreeCertificateContext(self.inner.as_ptr()) };
}
}
#[derive(Debug)]
struct CertEngine {
inner: NonNull<c_void>, }
impl CertEngine {
fn new_with_extra_roots(
roots: impl IntoIterator<Item = pki_types::CertificateDer<'static>>,
) -> Result<Self, TlsError> {
let mut exclusive_store = CertificateStore::new()?;
for root in roots {
exclusive_store.add_cert(&root)?;
}
let mut config = CERT_CHAIN_ENGINE_CONFIG::zeroed_with_size();
config.hExclusiveRoot = exclusive_store.inner.as_ptr();
let mut engine = EnginePtr::NULL;
let config = NonNull::from(&config).cast().as_ptr();
let res = unsafe { CertCreateCertificateChainEngine(config, &mut engine) };
#[allow(clippy::as_conversions)]
let engine = call_with_last_error(|| match NonNull::new(engine as *mut c_void) {
Some(c) if res == TRUE => Some(c),
_ => None,
})?;
Ok(Self { inner: engine })
}
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
fn new_with_fake_root(root: &[u8]) -> Result<Self, TlsError> {
use windows_sys::Win32::Security::Cryptography::{
CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL, CERT_CHAIN_ENABLE_CACHE_AUTO_UPDATE,
};
let mut root_store = CertificateStore::new()?;
root_store.add_cert(root)?;
let mut config = CERT_CHAIN_ENGINE_CONFIG::zeroed_with_size();
config.dwFlags = CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL | CERT_CHAIN_ENABLE_CACHE_AUTO_UPDATE;
config.hExclusiveRoot = root_store.inner.as_ptr();
let mut engine = EnginePtr::NULL;
let config = NonNull::from(&config).cast().as_ptr();
let res = unsafe { CertCreateCertificateChainEngine(config, &mut engine) };
#[allow(clippy::as_conversions)]
let engine = call_with_last_error(|| match NonNull::new(engine as *mut c_void) {
Some(c) if res == TRUE => Some(c),
_ => None,
})?;
Ok(Self { inner: engine })
}
}
impl Drop for CertEngine {
fn drop(&mut self) {
unsafe { CertFreeCertificateChainEngine(EnginePtr::from_raw(self.inner)) };
}
}
unsafe impl Sync for CertEngine {}
unsafe impl Send for CertEngine {}
struct CertificateStore {
inner: NonNull<c_void>, engine: Option<CertEngine>, }
impl Drop for CertificateStore {
fn drop(&mut self) {
unsafe { CertCloseStore(self.inner.as_ptr(), 0) };
}
}
impl CertificateStore {
fn new() -> Result<Self, TlsError> {
let store = call_with_last_error(|| {
NonNull::new(unsafe {
CertOpenStore(
CERT_STORE_PROV_MEMORY,
0, 0, CERT_STORE_DEFER_CLOSE_UNTIL_LAST_FREE_FLAG,
ptr::null(),
)
})
})?;
Ok(Self {
inner: store,
engine: None,
})
}
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
fn new_with_fake_root(root: &[u8]) -> Result<Self, TlsError> {
let mut inner = Self::new()?;
let mut root_store = CertificateStore::new()?;
root_store.add_cert(root)?;
let engine = CertEngine::new_with_fake_root(root)?;
inner.engine = Some(engine);
Ok(inner)
}
fn add_cert(&mut self, cert: &[u8]) -> Result<Certificate, TlsError> {
let mut cert_context: *mut CERT_CONTEXT = ptr::null_mut();
let res = unsafe {
CertAddEncodedCertificateToStore(
self.inner.as_ptr(),
X509_ASN_ENCODING,
cert.as_ptr(),
cert.len()
.try_into()
.map_err(|_| InvalidCertificate(CertificateError::BadEncoding))?,
CERT_STORE_ADD_ALWAYS,
&mut cert_context,
)
};
match (res, NonNull::new(cert_context)) {
(TRUE, Some(cert)) => Ok(Certificate { inner: cert }),
_ => Err(InvalidCertificate(CertificateError::BadEncoding)),
}
}
fn new_chain_in(
&self,
certificate: &Certificate,
now: pki_types::UnixTime,
engine: Option<&CertEngine>,
) -> Result<CertChain, TlsError> {
let mut cert_chain = ptr::null_mut();
let mut parameters = CERT_CHAIN_PARA::zeroed_with_size();
#[allow(clippy::as_conversions)]
let usage = CERT_USAGE_MATCH {
dwType: USAGE_MATCH_TYPE_AND,
Usage: CTL_USAGE {
cUsageIdentifier: ALLOWED_EKUS.len() as u32,
rgpszUsageIdentifier: ALLOWED_EKUS.as_ptr() as *mut windows_sys::core::PSTR,
},
};
parameters.RequestedUsage = usage;
#[allow(clippy::as_conversions)]
let time = {
const UNIX_ADJUSTMENT: std::time::Duration =
std::time::Duration::from_secs(11_644_473_600);
let since_unix_epoch = now.as_secs();
let since_windows_epoch = since_unix_epoch + UNIX_ADJUSTMENT.as_secs();
let intervals = (since_windows_epoch * 1_000_000_000) / 100;
FILETIME {
dwLowDateTime: (intervals & u32::MAX as u64) as u32,
dwHighDateTime: (intervals >> 32) as u32,
}
};
const FLAGS: u32 = CERT_CHAIN_REVOCATION_CHECK_END_CERT
| CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT
| CERT_CHAIN_CACHE_END_CERT;
parameters.dwUrlRetrievalTimeout = 10 * 1000;
let res = unsafe {
let parameters = NonNull::from(¶meters).cast().as_ptr();
CertGetCertificateChain(
match engine {
Some(eng) => EnginePtr::from_raw(eng.inner),
None => EnginePtr::NULL,
},
certificate.inner.as_ptr(),
&time,
self.inner.as_ptr(),
parameters,
FLAGS,
ptr::null_mut(),
&mut cert_chain,
)
};
call_with_last_error(|| match NonNull::new(cert_chain) {
Some(c) if res == TRUE => Some(CertChain { inner: c }),
_ => None,
})
}
}
impl EnginePtr for *mut c_void {
fn from_raw(val: NonNull<c_void>) -> Self {
val.as_ptr()
}
const NULL: Self = ptr::null_mut();
}
impl EnginePtr for isize {
#[allow(clippy::as_conversions)]
fn from_raw(val: NonNull<c_void>) -> Self {
val.as_ptr() as isize
}
const NULL: Self = 0;
}
trait EnginePtr: Sized {
fn from_raw(val: NonNull<c_void>) -> Self;
const NULL: Self;
}
fn call_with_last_error<T, F: FnMut() -> Option<T>>(mut call: F) -> Result<T, TlsError> {
if let Some(res) = call() {
Ok(res)
} else {
Err(TlsError::General(
std::io::Error::last_os_error().to_string(),
))
}
}
#[derive(Debug)]
pub struct Verifier {
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
test_only_root_ca_override: Option<pki_types::CertificateDer<'static>>,
crypto_provider: Arc<CryptoProvider>,
extra_roots: Option<CertEngine>,
}
impl Verifier {
#[cfg_attr(docsrs, doc(cfg(all())))]
pub fn new(crypto_provider: Arc<CryptoProvider>) -> Result<Self, TlsError> {
Ok(Self {
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
test_only_root_ca_override: None,
crypto_provider,
extra_roots: None,
})
}
#[cfg_attr(docsrs, doc(cfg(not(target_os = "android"))))]
pub fn new_with_extra_roots(
roots: impl IntoIterator<Item = pki_types::CertificateDer<'static>>,
crypto_provider: Arc<CryptoProvider>,
) -> Result<Self, TlsError> {
let cert_engine = CertEngine::new_with_extra_roots(roots)?;
Ok(Self {
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
test_only_root_ca_override: None,
crypto_provider,
extra_roots: Some(cert_engine),
})
}
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
pub(crate) fn new_with_fake_root(
root: pki_types::CertificateDer<'static>,
crypto_provider: Arc<CryptoProvider>,
) -> Self {
Self {
test_only_root_ca_override: Some(root),
crypto_provider,
extra_roots: None,
}
}
fn verify_certificate(
&self,
primary_cert: &[u8],
intermediate_certs: &[&[u8]],
server: &[u8],
ocsp_data: Option<&[u8]>,
now: pki_types::UnixTime,
) -> Result<(), TlsError> {
#[cfg(any(test, feature = "ffi-testing", feature = "dbg"))]
let mut store = match self.test_only_root_ca_override.as_ref() {
Some(test_only_root_ca_override) => {
CertificateStore::new_with_fake_root(test_only_root_ca_override)?
}
None => CertificateStore::new()?,
};
#[cfg(not(any(test, feature = "ffi-testing", feature = "dbg")))]
let mut store = CertificateStore::new()?;
let mut primary_cert = store.add_cert(primary_cert)?;
for cert in intermediate_certs.iter().copied() {
store.add_cert(cert)?;
}
if let Some(ocsp_data) = ocsp_data {
#[allow(clippy::as_conversions)]
let data = CRYPT_INTEGER_BLOB {
cbData: ocsp_data.len().try_into().map_err(|_| {
invalid_certificate("Malformed OCSP response stapled to server certificate")
})?,
pbData: ocsp_data.as_ptr() as *mut u8,
};
unsafe {
primary_cert.set_property(
CERT_OCSP_RESPONSE_PROP_ID,
NonNull::from(&data).cast::<c_void>().as_ptr(),
)?;
}
}
let server: Vec<u16> = server
.iter()
.map(|c| u16::from(*c))
.chain(Some(0))
.collect();
let mut cert_chain = store.new_chain_in(&primary_cert, now, store.engine.as_ref())?;
let cert_error_status = unsafe { *cert_chain.inner.as_ptr() }
.TrustStatus
.dwErrorStatus;
let extra_roots_may_needed =
(cert_error_status & (CERT_TRUST_IS_PARTIAL_CHAIN | CERT_TRUST_IS_UNTRUSTED_ROOT)) != 0;
if extra_roots_may_needed && self.extra_roots.is_some() {
let mut store = CertificateStore::new()?;
for cert in intermediate_certs.iter().copied() {
store.add_cert(cert)?;
}
cert_chain = store.new_chain_in(&primary_cert, now, self.extra_roots.as_ref())?;
}
let status = cert_chain.verify_chain_policy(server)?;
if status.dwError == 0 {
return Ok(());
}
#[allow(clippy::as_conversions)]
let win_error = status.dwError as i32;
Err(match win_error {
CERT_E_CN_NO_MATCH | CERT_E_INVALID_NAME => {
InvalidCertificate(CertificateError::NotValidForName)
}
CRYPT_E_REVOKED => InvalidCertificate(CertificateError::Revoked),
CERT_E_EXPIRED => InvalidCertificate(CertificateError::Expired),
CERT_E_UNTRUSTEDROOT => InvalidCertificate(CertificateError::UnknownIssuer),
CERT_E_WRONG_USAGE => InvalidCertificate(CertificateError::InvalidPurpose),
error_num => {
let err = std::io::Error::from_raw_os_error(error_num);
invalid_certificate(err.to_string())
}
})
}
}
#[cfg_attr(docsrs, doc(cfg(all())))]
impl ServerCertVerifier for Verifier {
fn verify_server_cert(
&self,
end_entity: &pki_types::CertificateDer<'_>,
intermediates: &[pki_types::CertificateDer<'_>],
server_name: &pki_types::ServerName,
ocsp_response: &[u8],
now: pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, TlsError> {
log_server_cert(end_entity);
let name = server_name.to_str();
let intermediate_certs: Vec<&[u8]> = intermediates.iter().map(|c| c.as_ref()).collect();
let ocsp_data = if !ocsp_response.is_empty() {
Some(ocsp_response)
} else {
None
};
match self.verify_certificate(
end_entity.as_ref(),
&intermediate_certs,
name.as_bytes(),
ocsp_data,
now,
) {
Ok(()) => Ok(rustls::client::danger::ServerCertVerified::assertion()),
Err(e) => {
log::error!("failed to verify TLS certificate: {}", e);
Err(e)
}
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &pki_types::CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls12_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &pki_types::CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls13_signature(
message,
cert,
dss,
&self.crypto_provider.signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.crypto_provider
.signature_verification_algorithms
.supported_schemes()
}
}
unsafe trait ZeroedWithSize: Sized {
const SIZE: u32 = {
let size = core::mem::size_of::<Self>();
#[allow(clippy::as_conversions)]
if size <= u32::MAX as usize {
size as u32
} else {
panic!("structure was larger then DWORD")
}
};
fn zeroed_with_size() -> Self;
}