Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ntex-tls/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changes

## [unreleased]

* Fix Schannel client: drain leftover handshake records, handle TLS 1.3
post-handshake messages (`SEC_I_RENEGOTIATE`), send `close_notify` on
shutdown, and acquire credentials with `SCH_CREDENTIALS`

* Add Schannel server acceptor (`TlsAcceptor`, `ServerConfig::from_pem`)
so Schannel no longer depends on OpenSSL for tests or TLS servers

## [4.0.0-beta.0] - 2026-08-24

* Migrate to ntex-service 5
Expand Down
2 changes: 1 addition & 1 deletion ntex-tls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "ntex-tls"
version = "4.0.0-beta.0"
authors = ["ntex contributors <team@ntex.rs>"]
description = "An implementation of SSL streams for ntex backed by OpenSSL"
description = "TLS streams for ntex (OpenSSL, rustls, and Windows Schannel)"
keywords = ["network", "framework", "async", "futures"]
homepage = "https://ntex.rs"
repository = "https://github.com/ntex-rs/ntex.git"
Expand Down
70 changes: 70 additions & 0 deletions ntex-tls/src/schannel/accept.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::io;

use ntex_io::{Filter, Io, Layer};
use ntex_service::{Ctx, Service, cfg::Cfg, cfg::Configuration};
use ntex_util::{services::Counter, time};

use super::{SchannelFilter, ServerConfig, accept as accept_io};
use crate::{MAX_SSL_ACCEPT_COUNTER, TlsConfig};

/// Support TLS server connections via Windows Schannel.
#[derive(Clone, Debug)]
pub struct TlsAcceptor {
config: ServerConfig,
conns: Counter,
}

impl TlsAcceptor {
/// Create a Schannel acceptor service.
#[must_use]
pub fn new(config: ServerConfig) -> Self {
MAX_SSL_ACCEPT_COUNTER.with(|conns| TlsAcceptor {
config,
conns: conns.clone(),
})
}
}

impl From<ServerConfig> for TlsAcceptor {
fn from(config: ServerConfig) -> Self {
Self::new(config)
}
}

impl<F: Filter, St> Service<St, Io<F>> for TlsAcceptor {
type Res = Io<Layer<SchannelFilter, F>>;
type Error = io::Error;

async fn ready(&self, _: Ctx<'_, Self, St>) -> Result<(), Self::Error> {
if !self.conns.is_available() {
self.conns.available().await;
}
Ok(())
}

async fn call(&self, io: Io<F>, _: Ctx<'_, Self, St>) -> Result<Self::Res, Self::Error> {
let _guard = self.conns.get();
let cfg: Cfg<TlsConfig> = io.cfg().ctx().get();
time::timeout(cfg.handshake_timeout(), accept_io(io, self.config.clone()))
.await
.map_err(|()| io::Error::new(io::ErrorKind::TimedOut, "TLS Handshake timeout"))
.and_then(|item| item)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[ntex::test]
async fn test_schannel_accept() {
let config = ServerConfig::from_pem(
include_str!("../../examples/cert.pem"),
include_str!("../../examples/key.pem"),
)
.unwrap();
let srv = TlsAcceptor::new(config.clone());
assert!(format!("{srv:?}").contains("TlsAcceptor"));
assert!(!config.cert_der().is_empty());
}
}
232 changes: 232 additions & 0 deletions ntex-tls/src/schannel/cert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{io, ptr, slice};

use windows_sys::Win32::Foundation::SEC_E_OK;
use windows_sys::Win32::Security::Cryptography::{
BCRYPTBUFFER_VERSION, BCryptBuffer, BCryptBufferDesc, CERT_CONTEXT, CERT_KEY_PROV_INFO_PROP_ID,
CRYPT_KEY_PROV_INFO, CRYPT_STRING_BASE64HEADER, CertCreateCertificateContext,
CertDuplicateCertificateContext, CertFreeCertificateContext, CertSetCertificateContextProperty,
CryptStringToBinaryA, MS_KEY_STORAGE_PROVIDER, NCRYPT_KEY_HANDLE, NCRYPT_OVERWRITE_KEY_FLAG,
NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NCRYPT_PROV_HANDLE, NCRYPT_SILENT_FLAG,
NCRYPTBUFFER_PKCS_KEY_NAME, NCryptDeleteKey, NCryptFreeObject, NCryptImportKey,
NCryptOpenStorageProvider, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING,
};

static KEY_ID: AtomicU64 = AtomicU64::new(1);

struct PersistedKey(NCRYPT_KEY_HANDLE);

impl Drop for PersistedKey {
fn drop(&mut self) {
if self.0 != 0 {
unsafe {
NCryptDeleteKey(self.0, 0);
}
}
}
}

/// Windows Schannel server configuration.
///
/// Holds a certificate with an associated private key used to accept TLS
/// connections.
pub struct ServerConfig {
cert: *mut CERT_CONTEXT,
key: Arc<PersistedKey>,
}

unsafe impl Send for ServerConfig {}
unsafe impl Sync for ServerConfig {}

impl Clone for ServerConfig {
fn clone(&self) -> Self {
Self {
cert: unsafe { CertDuplicateCertificateContext(self.cert) },
key: self.key.clone(),
}
}
}

impl Drop for ServerConfig {
fn drop(&mut self) {
if !self.cert.is_null() {
unsafe {
CertFreeCertificateContext(self.cert);
}
}
}
}

impl std::fmt::Debug for ServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerConfig").finish()
}
}

impl ServerConfig {
/// Load a PEM-encoded certificate and PKCS#8 private key.
pub fn from_pem(cert_pem: &str, key_pem: &str) -> io::Result<Self> {
let cert_der = decode_pem(cert_pem)?;
let key_der = decode_pem(key_pem)?;

let cert = unsafe {
CertCreateCertificateContext(
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
cert_der.as_ptr(),
u32::try_from(cert_der.len())
.map_err(|_| io::Error::other("TLS certificate is too large"))?,
)
};
if cert.is_null() {
return Err(io::Error::last_os_error());
}

match attach_private_key(cert, &key_der) {
Ok(key) => Ok(Self {
cert,
key: Arc::new(key),
}),
Err(err) => {
unsafe {
CertFreeCertificateContext(cert);
}
Err(err)
}
}
}

/// Certificate in DER encoding.
#[must_use]
pub fn cert_der(&self) -> Vec<u8> {
unsafe {
let cert = &*self.cert;
slice::from_raw_parts(cert.pbCertEncoded, cert.cbCertEncoded as usize).to_vec()
}
}

pub(super) fn cert(&self) -> *mut CERT_CONTEXT {
self.cert
}
}

fn decode_pem(pem: &str) -> io::Result<Vec<u8>> {
let bytes = pem.as_bytes();
let mut len = 0u32;
let ok = unsafe {
CryptStringToBinaryA(
bytes.as_ptr(),
u32::try_from(bytes.len()).map_err(|_| io::Error::other("PEM is too large"))?,
CRYPT_STRING_BASE64HEADER,
ptr::null_mut(),
&raw mut len,
ptr::null_mut(),
ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}

let mut buf = vec![0u8; len as usize];
let ok = unsafe {
CryptStringToBinaryA(
bytes.as_ptr(),
u32::try_from(bytes.len()).map_err(|_| io::Error::other("PEM is too large"))?,
CRYPT_STRING_BASE64HEADER,
buf.as_mut_ptr(),
&raw mut len,
ptr::null_mut(),
ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
buf.truncate(len as usize);
Ok(buf)
}

fn attach_private_key(cert: *mut CERT_CONTEXT, key_der: &[u8]) -> io::Result<PersistedKey> {
let id = KEY_ID.fetch_add(1, Ordering::Relaxed);
let mut name: Vec<u16> = format!("ntex-schannel-{}-{}", std::process::id(), id)
.encode_utf16()
.chain(Some(0))
.collect();

let mut provider: NCRYPT_PROV_HANDLE = 0;
let status =
unsafe { NCryptOpenStorageProvider(&raw mut provider, MS_KEY_STORAGE_PROVIDER, 0) };
if status != SEC_E_OK {
return Err(io::Error::from_raw_os_error(status));
}

let mut name_buf = BCryptBuffer {
cbBuffer: u32::try_from(name.len() * 2).expect("key name fits u32"),
BufferType: NCRYPTBUFFER_PKCS_KEY_NAME,
pvBuffer: name.as_mut_ptr().cast(),
};
let params = BCryptBufferDesc {
ulVersion: BCRYPTBUFFER_VERSION,
cBuffers: 1,
pBuffers: &raw mut name_buf,
};

let mut key: NCRYPT_KEY_HANDLE = 0;
let status = unsafe {
NCryptImportKey(
provider,
0,
NCRYPT_PKCS8_PRIVATE_KEY_BLOB,
&raw const params,
&raw mut key,
key_der.as_ptr(),
u32::try_from(key_der.len()).map_err(|_| io::Error::other("TLS key is too large"))?,
NCRYPT_OVERWRITE_KEY_FLAG | NCRYPT_SILENT_FLAG,
)
};
unsafe {
NCryptFreeObject(provider);
}
if status != SEC_E_OK {
return Err(io::Error::from_raw_os_error(status));
}

let mut prov_name: Vec<u16> = {
let mut s = Vec::new();
let mut p = MS_KEY_STORAGE_PROVIDER;
unsafe {
while *p != 0 {
s.push(*p);
p = p.add(1);
}
}
s.push(0);
s
};
let prov_info = CRYPT_KEY_PROV_INFO {
pwszContainerName: name.as_mut_ptr(),
pwszProvName: prov_name.as_mut_ptr(),
dwProvType: 0,
dwFlags: 0,
cProvParam: 0,
rgProvParam: ptr::null_mut(),
dwKeySpec: 0,
};
let ok = unsafe {
CertSetCertificateContextProperty(
cert,
CERT_KEY_PROV_INFO_PROP_ID,
0,
(&raw const prov_info).cast(),
)
};
if ok == 0 {
unsafe {
NCryptDeleteKey(key, 0);
}
return Err(io::Error::last_os_error());
}

Ok(PersistedKey(key))
}
Loading
Loading