Skip to content
Merged
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
88 changes: 88 additions & 0 deletions boring/src/ssl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,43 @@ impl SslSignatureAlgorithm {
SslSignatureAlgorithm(ffi::SSL_SIGN_RSA_PSS_RSAE_SHA512 as _);

pub const ED25519: SslSignatureAlgorithm = SslSignatureAlgorithm(ffi::SSL_SIGN_ED25519 as _);

// ML-DSA codepoints are hardcoded from the IANA TLS Signature Scheme
// registry so that this crate continues to compile against older
// BoringSSL versions that predate the SSL_SIGN_ML_DSA_* defines.
pub const ML_DSA_44: SslSignatureAlgorithm = SslSignatureAlgorithm(0x0904);

pub const ML_DSA_65: SslSignatureAlgorithm = SslSignatureAlgorithm(0x0905);

pub const ML_DSA_87: SslSignatureAlgorithm = SslSignatureAlgorithm(0x0906);

/// Returns the name of this signature algorithm, or `None` if unknown.
Comment thread
lukevalenta marked this conversation as resolved.
///
/// For ECDSA algorithms the TLS 1.3 form is returned
/// (e.g. `ecdsa_secp256r1_sha256`), not the TLS 1.2 form (`ecdsa_sha256`).
#[corresponds(SSL_get_signature_algorithm_name)]
#[must_use]
pub fn name(&self) -> Option<&'static str> {
unsafe {
// Pass `include_curve = 1` to get the TLS 1.3 form for ECDSA algorithms
// (e.g. `ecdsa_secp256r1_sha256` rather than the TLS 1.2 `ecdsa_sha256`).
let ptr = ffi::SSL_get_signature_algorithm_name(self.0, 1);
if ptr.is_null() {
None
} else {
CStr::from_ptr(ptr).to_str().ok()
}
}
}
}

impl fmt::Display for SslSignatureAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.name() {
Some(name) => f.write_str(name),
None => write!(f, "unknown ({:#06x})", self.0),
}
}
}

impl From<u16> for SslSignatureAlgorithm {
Expand Down Expand Up @@ -1966,6 +2003,12 @@ impl SslContextBuilder {
}

/// Sets the context's supported signature algorithms.
///
/// Prefer [`set_verify_algorithm_prefs`](Self::set_verify_algorithm_prefs),
/// which takes raw IANA codepoints rather than an OpenSSL-style colon-separated
/// string. Note that unlike `set_sigalgs_list`, `set_verify_algorithm_prefs`
/// only configures the verify preference list and does not also set the
/// signing algorithm prefs.
#[corresponds(SSL_CTX_set1_sigalgs_list)]
pub fn set_sigalgs_list(&mut self, sigalgs: &str) -> Result<(), ErrorStack> {
let sigalgs = CString::new(sigalgs).map_err(ErrorStack::internal_error)?;
Expand Down Expand Up @@ -2938,6 +2981,21 @@ impl SslRef {
unsafe { cvt_0i(ffi::SSL_set1_curves_list(self.as_ptr(), curves.as_ptr())).map(|_| ()) }
}

#[corresponds(SSL_set_verify_algorithm_prefs)]
pub fn set_verify_algorithm_prefs(
&mut self,
prefs: &[SslSignatureAlgorithm],
Comment thread
lukevalenta marked this conversation as resolved.
) -> Result<(), ErrorStack> {
unsafe {
cvt_0i(ffi::SSL_set_verify_algorithm_prefs(
self.as_ptr(),
prefs.as_ptr().cast(),
prefs.len(),
))
.map(|_| ())
}
}

/// Returns the curve ID (aka group ID) used for this `SslRef`.
#[corresponds(SSL_get_curve_id)]
#[must_use]
Expand Down Expand Up @@ -3146,6 +3204,36 @@ impl SslRef {
}
}

/// Returns the signature algorithm used by the peer in the most recent TLS handshake,
/// or `None` if no signature was produced (e.g. session resumption).
#[corresponds(SSL_get_peer_signature_algorithm)]
#[must_use]
pub fn peer_signature_algorithm(&self) -> Option<SslSignatureAlgorithm> {
let sigalg = unsafe { ffi::SSL_get_peer_signature_algorithm(self.as_ptr()) };
if sigalg == 0 {
None
} else {
Some(SslSignatureAlgorithm(sigalg))
}
}

/// Returns the signature algorithm this side used to sign the current TLS handshake,
/// or `None` if not applicable.
///
/// BoringSSL only retains this value during the handshake; to observe it post-handshake,
/// capture it from an [`SslContextBuilder::set_info_callback`] handler at
/// [`SslInfoCallbackMode::HANDSHAKE_DONE`].
#[corresponds(SSL_get_signature_algorithm_used)]
#[must_use]
pub fn signature_algorithm_used(&self) -> Option<SslSignatureAlgorithm> {
let sigalg = unsafe { ffi::SSL_get_signature_algorithm_used(self.as_ptr()) };
if sigalg == 0 {
None
} else {
Some(SslSignatureAlgorithm(sigalg))
}
}

/// Returns a short string describing the state of the session.
///
/// Returns empty string if the state wasn't valid UTF-8.
Expand Down
192 changes: 191 additions & 1 deletion boring/src/ssl/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use crate::srtp::SrtpProfileId;
use crate::ssl::test::server::Server;
use crate::ssl::{
self, ExtensionType, ShutdownResult, ShutdownState, Ssl, SslAcceptor, SslAcceptorBuilder,
SslConnector, SslContext, SslFiletype, SslMethod, SslOptions, SslStream, SslVerifyMode,
SslConnector, SslContext, SslFiletype, SslInfoCallbackMode, SslMethod, SslOptions,
SslSignatureAlgorithm, SslStream, SslVerifyMode,
};
use crate::ssl::{HandshakeError, SslVersion};
use crate::x509::store::X509StoreBuilder;
Expand Down Expand Up @@ -1315,3 +1316,192 @@ fn ex_data_drop() {
assert_eq!(102, d1.load(Relaxed));
assert_eq!(202, d2.load(Relaxed));
}

#[test]
fn peer_signature_algorithm() {
// Default handshake: client should observe the server's CertificateVerify signature.
let server = Server::builder().build();
let s = server.client().connect();
let sigalg = s.ssl().peer_signature_algorithm();
assert!(
sigalg.is_some(),
"client should see peer (server) signature algorithm after handshake",
);
assert!(
sigalg.unwrap().name().is_some(),
"peer signature algorithm should have a resolvable name",
);
}

#[test]
fn peer_signature_algorithm_mtls_server_sees_client() {
// Server requires a client certificate; verify that the server-side SslRef
// surfaces the client's CertificateVerify signature scheme as its peer sig alg.
// Observe from a server-side HANDSHAKE_DONE info callback so the capture
// happens synchronously during the server's accept, before the client's
// connect() returns.
let captured = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));

let mut server_builder = Server::builder();
{
let mut store = X509StoreBuilder::new().unwrap();
store.add_cert(X509::from_pem(ROOT_CERT).unwrap()).unwrap();
server_builder
.ctx()
.set_verify_cert_store(store.build())
.unwrap();
server_builder.ctx().set_verify(SslVerifyMode::PEER);
let captured_cb = std::sync::Arc::clone(&captured);
server_builder
.ctx()
.set_info_callback(move |ssl, mode, _value| {
if mode == SslInfoCallbackMode::HANDSHAKE_DONE {
if let Some(sa) = ssl.peer_signature_algorithm() {
assert!(sa.name().is_some(), "client sig scheme should have a name");
captured_cb.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
});
}
let server = server_builder.build();

let mut client_builder = server.client_with_root_ca();
client_builder
.ctx()
.set_certificate_chain_file("test/cert.pem")
.unwrap();
client_builder
.ctx()
.set_private_key_file("test/key.pem", SslFiletype::PEM)
.unwrap();
let _ = client_builder.connect();

assert!(
captured.load(std::sync::atomic::Ordering::SeqCst),
"server should observe client signature scheme during mTLS",
);
}

#[test]
fn signature_algorithm_used_server_default() {
// BoringSSL only retains signature_algorithm_used during the handshake, so we
// capture it from a HANDSHAKE_DONE info callback.
let captured = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));

let mut server_builder = Server::builder();
{
let captured_cb = std::sync::Arc::clone(&captured);
server_builder
.ctx()
.set_info_callback(move |ssl, mode, _value| {
if mode == SslInfoCallbackMode::HANDSHAKE_DONE {
if let Some(sa) = ssl.signature_algorithm_used() {
assert!(sa.name().is_some());
captured_cb.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
});
}
let server = server_builder.build();
let _ = server.client_with_root_ca().connect();

assert!(
captured.load(std::sync::atomic::Ordering::SeqCst),
"server should observe signature_algorithm_used at HANDSHAKE_DONE",
);
}

#[test]
fn signature_algorithm_used_post_handshake_returns_none() {
// BoringSSL drops the value after the handshake. Confirm the binding
// surfaces that contract: calling on the client SslStream post-handshake
// returns None.
let server = Server::builder().build();
let s = server.client_with_root_ca().connect();
assert!(
s.ssl().signature_algorithm_used().is_none(),
"BoringSSL discards signature_algorithm_used after handshake",
);
}

#[test]
fn signature_algorithm_used_mtls_client() {
// Client side mTLS: capture the sig scheme the client used in CertificateVerify.
let mut server_builder = Server::builder();
{
let mut store = X509StoreBuilder::new().unwrap();
store.add_cert(X509::from_pem(ROOT_CERT).unwrap()).unwrap();
server_builder
.ctx()
.set_verify_cert_store(store.build())
.unwrap();
server_builder.ctx().set_verify(SslVerifyMode::PEER);
}
let server = server_builder.build();

let captured = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let captured_cb = std::sync::Arc::clone(&captured);

let mut client_builder = server.client_with_root_ca();
client_builder
.ctx()
.set_certificate_chain_file("test/cert.pem")
.unwrap();
client_builder
.ctx()
.set_private_key_file("test/key.pem", SslFiletype::PEM)
.unwrap();
client_builder
.ctx()
.set_info_callback(move |ssl, mode, _value| {
if mode == SslInfoCallbackMode::HANDSHAKE_DONE {
if let Some(sa) = ssl.signature_algorithm_used() {
assert!(sa.name().is_some());
captured_cb.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
});
let _ = client_builder.connect();

assert!(
captured.load(std::sync::atomic::Ordering::SeqCst),
"client should observe signature_algorithm_used at HANDSHAKE_DONE in mTLS",
);
}

// ===========================================================================
// per-connection verify algorithm prefs
// ===========================================================================

#[test]
fn set_verify_algorithm_prefs_ssl_accepts_matching() {
// Client restricts verify prefs to a scheme the server can satisfy.
let server = Server::builder().build();

let client_builder = server.client();
let mut ssl_builder = client_builder.build().builder();
ssl_builder
.ssl()
.set_verify_algorithm_prefs(&[SslSignatureAlgorithm::RSA_PSS_RSAE_SHA256])
.expect("verify prefs should be accepted");
let s = ssl_builder.connect();
let sa = s.ssl().peer_signature_algorithm().unwrap();
assert_eq!(sa, SslSignatureAlgorithm::RSA_PSS_RSAE_SHA256);
}

#[test]
fn set_verify_algorithm_prefs_ssl_rejects_unsatisfiable() {
// Client restricts verify prefs to a scheme the server's RSA cert cannot
// satisfy. The handshake must fail.
let mut server_builder = Server::builder();
server_builder.should_error();
let server = server_builder.build();

let client_builder = server.client();
let mut ssl_builder = client_builder.build().builder();
ssl_builder
.ssl()
.set_verify_algorithm_prefs(&[SslSignatureAlgorithm::ED25519])
.expect("verify prefs should be accepted by setter");
let _err = ssl_builder.connect_err();
}
Loading