Files
Stalwart/crates/store/src/backend/postgres/tls.rs
Hugh Cole-Baker 448f735d55 Fix Store PostgreSQL channel binding (#3212)
The former behaviour to always use a SHA-256 hash of the server
certificate for the tls-server-end-point channel binding was
incorrect with regard to RFC5929, which specifies if the signature
algorithm uses a single hash function, that hash function should be
used, unless the signature algorithm uses MD5 or SHA-1, in which
case the channel binding should use an SHA-256 hash.

This change attempts to align with the behaviour of OpenSSL, used
by PostgreSQL, in using the signature algorithm's hash function.
2026-07-08 23:00:16 +02:00

199 lines
6.3 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
// Credits: https://github.com/jbg/tokio-postgres-rustls
use std::{
convert::TryFrom,
future::Future,
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use aws_lc_rs::digest;
use futures::future::{FutureExt, TryFutureExt};
use rustls::ClientConfig;
use rustls_pki_types::ServerName;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_postgres::tls::{ChannelBinding, MakeTlsConnect, TlsConnect};
use tokio_rustls::{TlsConnector, client::TlsStream};
use x509_parser::{
asn1_rs::oid,
oid_registry::{
OID_HASH_SHA1, OID_MD5_WITH_RSA, OID_NIST_HASH_SHA256, OID_NIST_HASH_SHA384,
OID_NIST_HASH_SHA512, OID_PKCS1_MD5WITHRSAENC, OID_PKCS1_RSASSAPSS, OID_PKCS1_SHA1WITHRSA,
OID_PKCS1_SHA224WITHRSA, OID_PKCS1_SHA256WITHRSA, OID_PKCS1_SHA384WITHRSA,
OID_PKCS1_SHA512WITHRSA, OID_SHA1_WITH_RSA, OID_SIG_DSA_WITH_SHA1,
OID_SIG_ECDSA_WITH_SHA224, OID_SIG_ECDSA_WITH_SHA256, OID_SIG_ECDSA_WITH_SHA384,
OID_SIG_ECDSA_WITH_SHA512,
},
parse_x509_certificate,
prelude::X509Certificate,
signature_algorithm::RsaSsaPssParams,
};
#[derive(Clone)]
pub struct MakeRustlsConnect {
config: Arc<ClientConfig>,
}
impl MakeRustlsConnect {
pub fn new(config: ClientConfig) -> Self {
Self {
config: Arc::new(config),
}
}
}
impl<S> MakeTlsConnect<S> for MakeRustlsConnect
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
type Stream = RustlsStream<S>;
type TlsConnect = RustlsConnect;
type Error = io::Error;
fn make_tls_connect(&mut self, hostname: &str) -> io::Result<RustlsConnect> {
ServerName::try_from(hostname.to_string())
.map(|dns_name| {
RustlsConnect(Some(RustlsConnectData {
hostname: dns_name,
connector: Arc::clone(&self.config).into(),
}))
})
.or(Ok(RustlsConnect(None)))
}
}
pub struct RustlsConnect(Option<RustlsConnectData>);
struct RustlsConnectData {
hostname: ServerName<'static>,
connector: TlsConnector,
}
impl<S> TlsConnect<S> for RustlsConnect
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
type Stream = RustlsStream<S>;
type Error = io::Error;
type Future = Pin<Box<dyn Future<Output = io::Result<RustlsStream<S>>> + Send>>;
fn connect(self, stream: S) -> Self::Future {
match self.0 {
None => Box::pin(core::future::ready(Err(io::ErrorKind::InvalidInput.into()))),
Some(c) => c
.connector
.connect(c.hostname, stream)
.map_ok(|s| RustlsStream(Box::pin(s)))
.boxed(),
}
}
}
pub struct RustlsStream<S>(Pin<Box<TlsStream<S>>>);
fn cb_digest_for_cert(cert: &X509Certificate<'_>) -> Option<&'static digest::Algorithm> {
let sig_alg = cert.signature_algorithm.oid();
// Signature algorithms that use a digest should use the same digest for channel binding:
if sig_alg == &OID_PKCS1_SHA512WITHRSA || sig_alg == &OID_SIG_ECDSA_WITH_SHA512 {
Some(&digest::SHA512)
} else if sig_alg == &OID_PKCS1_SHA384WITHRSA || sig_alg == &OID_SIG_ECDSA_WITH_SHA384 {
Some(&digest::SHA384)
} else if sig_alg == &OID_PKCS1_MD5WITHRSAENC
|| sig_alg == &OID_MD5_WITH_RSA
|| sig_alg == &OID_PKCS1_SHA1WITHRSA
|| sig_alg == &OID_SHA1_WITH_RSA
|| sig_alg == &OID_SIG_DSA_WITH_SHA1
|| sig_alg == &OID_PKCS1_SHA256WITHRSA
|| sig_alg == &OID_SIG_ECDSA_WITH_SHA256
{
// ...apart from MD5 or SHA1, which use SHA256 for channel binding, as per RFC 5929 section 4.1:
Some(&digest::SHA256)
} else if sig_alg == &OID_PKCS1_SHA224WITHRSA || sig_alg == &OID_SIG_ECDSA_WITH_SHA224 {
Some(&digest::SHA224)
} else if sig_alg == &OID_PKCS1_RSASSAPSS {
// For RSASSA-PSS, the hash algorithm is specified in the parameters of the signature algorithm:
let params_any = cert.signature_algorithm.parameters()?;
let pss = RsaSsaPssParams::try_from(params_any).ok()?;
let alg = pss.hash_algorithm_oid();
if alg == &OID_NIST_HASH_SHA512 {
Some(&digest::SHA512)
} else if alg == &OID_NIST_HASH_SHA384 {
Some(&digest::SHA384)
} else if alg == &OID_NIST_HASH_SHA256 || alg == &OID_HASH_SHA1 {
Some(&digest::SHA256)
} else if alg == &oid!(2.16.840.1.101.3.4.2.4) {
// id-sha224 from RFC 4055 ^
Some(&digest::SHA224)
} else {
None
}
} else {
None
}
}
impl<S> tokio_postgres::tls::TlsStream for RustlsStream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn channel_binding(&self) -> ChannelBinding {
let (_, session) = self.0.get_ref();
match session.peer_certificates() {
Some(certs) if !certs.is_empty() => match parse_x509_certificate(certs[0].as_ref()) {
Ok((_, cert)) => {
if let Some(digest_alg) = cb_digest_for_cert(&cert) {
let dgst = digest::digest(digest_alg, certs[0].as_ref());
ChannelBinding::tls_server_end_point(dgst.as_ref().into())
} else {
ChannelBinding::none()
}
}
Err(_) => ChannelBinding::none(),
},
_ => ChannelBinding::none(),
}
}
}
impl<S> AsyncRead for RustlsStream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context,
buf: &mut ReadBuf<'_>,
) -> Poll<tokio::io::Result<()>> {
self.0.as_mut().poll_read(cx, buf)
}
}
impl<S> AsyncWrite for RustlsStream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context,
buf: &[u8],
) -> Poll<tokio::io::Result<usize>> {
self.0.as_mut().poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<tokio::io::Result<()>> {
self.0.as_mut().poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<tokio::io::Result<()>> {
self.0.as_mut().poll_shutdown(cx)
}
}