diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index 06a3ecea6..4e3bef4df 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -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. + /// + /// 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 for SslSignatureAlgorithm { @@ -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)?; @@ -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], + ) -> 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] @@ -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 { + 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 { + 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. diff --git a/boring/src/ssl/test/mod.rs b/boring/src/ssl/test/mod.rs index 0159aa392..9e851f701 100644 --- a/boring/src/ssl/test/mod.rs +++ b/boring/src/ssl/test/mod.rs @@ -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; @@ -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(); +}