rust: bssl-tls: Add ALPN support Bug: 479599893 Signed-off-by: Xiangfei Ding <xfding@google.com> Change-Id: Iaafe381912b4d5d913c1af3a16baa3676a6a6964 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/98794 Reviewed-by: Adam Langley <agl@google.com>
diff --git a/rust/bssl-macros/src/lib.rs b/rust/bssl-macros/src/lib.rs index fca76ba..bcbf7b7 100644 --- a/rust/bssl-macros/src/lib.rs +++ b/rust/bssl-macros/src/lib.rs
@@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. - #![no_std] #[doc(hidden)]
diff --git a/rust/bssl-tls/src/alpn.rs b/rust/bssl-tls/src/alpn.rs new file mode 100644 index 0000000..c8e70fa --- /dev/null +++ b/rust/bssl-tls/src/alpn.rs
@@ -0,0 +1,89 @@ +// Copyright 2026 The BoringSSL Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! ALPN configuration and handling. +//! +//! The ALPN extension [RFC 7301] allows negotiating different application-layer +//! protocols over a single port. +//! This is used, for example, to negotiate HTTP/2. +/// A full list of values is available in [IANA]. +/// +/// [RFC 7301]: <https://datatracker.ietf.org/doc/html/rfc7301> +/// [IANA]: <https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids> +use alloc::vec::Vec; + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub(crate) struct AlpnProtocols(Vec<u8>); + +impl AlpnProtocols { + /// Append one ALPN protocol to the list. + /// + /// This method returns [`Err`] if `config` is empty or longer than 255 bytes, or the list + /// would exceed [`u16::MAX`] bytes. + pub fn append_protocol(&mut self, proto: &[u8]) -> Result<(), ()> { + if proto.is_empty() { + return Err(()); + } + let Ok(len) = u8::try_from(proto.len()) else { + return Err(()); + }; + if self.0.len() + 1 + len as usize > u16::MAX as usize { + return Err(()); + } + + self.0.push(len); + self.0.extend_from_slice(proto); + Ok(()) + } + + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +/// HTTP/2 over TLS protocol identifier. +pub const H2: &[u8] = b"h2"; + +/// HTTP/2 over plain TCP protocol identifier. +pub const H2C: &[u8] = b"h2c"; + +/// HTTP/1.1 protocol identifier. +pub const HTTP11: &[u8] = b"http/1.1"; + +/// WebRTC protocol identifier. +pub const WEBRTC: &[u8] = b"webrtc"; + +/// Confidential WebRTC protocol identifier. +pub const CONFIDENTIAL_WEBRTC: &[u8] = b"c-webrtc"; + +/// CoAP over TLS protocol identifier. +pub const COAP: &[u8] = b"coap"; + +/// CoAP/DTLS protocol identifier. +pub const COAP_DTLS: &[u8] = b"co"; + +/// HTTP/3 protocol identifier. +pub const H3: &[u8] = b"h3"; + +/// ACME protocol identifier. +pub const ACME: &[u8] = b"acme-tls/1"; + +/// IMAP over TLS protocol identifier. +pub const IMAP: &[u8] = b"imap"; + +/// POP3 over TLS protocol identifier. +pub const POP3: &[u8] = b"pop3"; + +/// FTP over TLS protocol identifier. +pub const FTP: &[u8] = b"ftp";
diff --git a/rust/bssl-tls/src/config.rs b/rust/bssl-tls/src/config.rs index 5af6ed1..08dc46b 100644 --- a/rust/bssl-tls/src/config.rs +++ b/rust/bssl-tls/src/config.rs
@@ -112,6 +112,8 @@ InvalidIp, /// Invalid parameters. InvalidParameters, + /// Invalid ALPN protocols. + InvalidAlpnProtocols, } impl core::fmt::Display for ConfigurationError { @@ -129,6 +131,7 @@ ConfigurationError::ValueOutOfRange => f.write_str("value is out of range"), ConfigurationError::InvalidIp => f.write_str("invalid IP address"), ConfigurationError::InvalidParameters => f.write_str("invalid parameters"), + ConfigurationError::InvalidAlpnProtocols => f.write_str("invalid ALPN protocols"), } } }
diff --git a/rust/bssl-tls/src/connection.rs b/rust/bssl-tls/src/connection.rs index 54ae965..df7ae7a 100644 --- a/rust/bssl-tls/src/connection.rs +++ b/rust/bssl-tls/src/connection.rs
@@ -44,6 +44,7 @@ sessions::TlsSession, // }; +mod alpn; mod credentials; pub mod io; pub mod lifecycle;
diff --git a/rust/bssl-tls/src/connection/alpn.rs b/rust/bssl-tls/src/connection/alpn.rs new file mode 100644 index 0000000..8ab11e0 --- /dev/null +++ b/rust/bssl-tls/src/connection/alpn.rs
@@ -0,0 +1,88 @@ +// Copyright 2026 The BoringSSL Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::ptr::null; + +use bssl_crypto::FromFfiSlice; + +use crate::{ + alpn::AlpnProtocols, + config::ConfigurationError, + connection::lifecycle::{ + EstablishedTlsConnection, + TlsConnectionInHandshake, // + }, + errors::Error, + ffi::slice_into_ffi_raw_parts, // +}; + +impl<R, M> TlsConnectionInHandshake<'_, R, M> { + /// Set ALPN protocols. + /// + /// Per [RFC 7301], empty protocol names, names longer than 255 bytes are invalid. + /// Also, the total size of the ALPN protocol list must not exceed [`u16::MAX`] bytes. + /// + /// **This method has no effect after handshake completion.** + /// + /// [RFC 7301]: <https://datatracker.ietf.org/doc/html/rfc7301#section-3.1> + pub fn set_alpn_protocols<'a>( + &mut self, + protocols: impl IntoIterator<Item = &'a [u8]>, + ) -> Result<&mut Self, Error> { + let mut protos = AlpnProtocols::default(); + for proto in protocols { + protos + .append_protocol(proto) + .map_err(|_| Error::Configuration(ConfigurationError::InvalidAlpnProtocols))?; + } + let (protos, len) = slice_into_ffi_raw_parts(protos.as_slice()); + let rc = unsafe { + // Safety: + // - the validity of the handle `self.ptr()` is witnessed by `self`. + // - the validity of ALPN string is guaranteed by `AlpnProtocols` type. + // Note: `SSL_set_alpn_protos` flips the return value around. + bssl_sys::SSL_set_alpn_protos(self.ptr(), protos, len) + }; + if rc == 1 { + Err(Error::extract_lib_err()) + } else { + Ok(self) + } + } +} + +/// Application-layer Protocol Negotiation. +impl<R, M> EstablishedTlsConnection<'_, R, M> { + /// Get the selected Application-layer Protocol. + /// + /// This function returns [`None`] when no ALPN protocol was negotiated. + pub fn get_selected_alpn(&self) -> Option<&[u8]> { + let mut name = null(); + let mut len = 0; + unsafe { + // Safety: the validity of the handle `self.ptr()` is witnessed by `self`. + bssl_sys::SSL_get0_alpn_selected(self.ptr(), &raw mut name, &raw mut len); + } + let Ok(len) = usize::try_from(len) else { + panic!("invalid slice length") + }; + let proto = unsafe { + // Safety: + // - `self` outlives the output slice. + // - `name` is generated by BoringSSL. + u8::from_ffi_ptr(name, len) + }; + (!proto.is_empty()).then_some(proto) + } +}
diff --git a/rust/bssl-tls/src/context.rs b/rust/bssl-tls/src/context.rs index ff016bd..84f7650 100644 --- a/rust/bssl-tls/src/context.rs +++ b/rust/bssl-tls/src/context.rs
@@ -48,6 +48,7 @@ Server, // }; +mod alpn; mod credentials; mod methods; mod sessions;
diff --git a/rust/bssl-tls/src/context/alpn.rs b/rust/bssl-tls/src/context/alpn.rs new file mode 100644 index 0000000..7785bce --- /dev/null +++ b/rust/bssl-tls/src/context/alpn.rs
@@ -0,0 +1,59 @@ +// Copyright 2026 The BoringSSL Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::TlsContextBuilder; + +use crate::{ + alpn::AlpnProtocols, + config::ConfigurationError, + context::SupportedMode, + errors::Error, + ffi::slice_into_ffi_raw_parts, // +}; + +/// ALPN configurations. +impl<M: SupportedMode> TlsContextBuilder<M> { + /// Set ALPN protocols. + /// + /// By passing an empty [`AlpnProtocols`] this method disables ALPN. + /// + /// Per [RFC 7301], empty protocol names, names longer than 255 bytes are invalid. + /// Also, the total size of the ALPN protocol list must not exceed [`u16::MAX`] bytes. + /// + /// [RFC 7301]: <https://datatracker.ietf.org/doc/html/rfc7301#section-3.1> + pub fn set_alpn_protocols<'a>( + &mut self, + protocols: impl IntoIterator<Item = &'a [u8]>, + ) -> Result<&mut Self, Error> { + let mut protos = AlpnProtocols::default(); + for proto in protocols { + protos + .append_protocol(proto) + .map_err(|_| Error::Configuration(ConfigurationError::InvalidAlpnProtocols))?; + } + let (protos, len) = slice_into_ffi_raw_parts(protos.as_slice()); + let rc = unsafe { + // Safety: + // - the validity of the handle `self.ptr()` is witnessed by `self`. + // - the validity of ALPN string is guaranteed by `AlpnProtocols` type. + // Note: `SSL_CTX_set_alpn_protos` flips the return value around for error signal. + bssl_sys::SSL_CTX_set_alpn_protos(self.ptr(), protos, len) + }; + if rc == 1 { + Err(Error::extract_lib_err()) + } else { + Ok(self) + } + } +}
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs index 965c9fb..fa1719b 100644 --- a/rust/bssl-tls/src/lib.rs +++ b/rust/bssl-tls/src/lib.rs
@@ -36,6 +36,7 @@ use core::panic::AssertUnwindSafe; pub mod alerts; +pub mod alpn; pub mod ciphers; pub mod config; pub mod connection;
diff --git a/rust/bssl-tls/src/tests.rs b/rust/bssl-tls/src/tests.rs index d319869..5a4636f 100644 --- a/rust/bssl-tls/src/tests.rs +++ b/rust/bssl-tls/src/tests.rs
@@ -53,6 +53,7 @@ }; use futures::future::join; +mod alpn; mod credentials; mod datagram; mod handshake;
diff --git a/rust/bssl-tls/src/tests/alpn.rs b/rust/bssl-tls/src/tests/alpn.rs new file mode 100644 index 0000000..0a60976 --- /dev/null +++ b/rust/bssl-tls/src/tests/alpn.rs
@@ -0,0 +1,39 @@ +// Copyright 2026 The BoringSSL Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::alpn::{ + AlpnProtocols, + H2, + HTTP11, // +}; + +#[test] +fn alpn_config() { + let mut config = AlpnProtocols::default(); + assert!(config.append_protocol(H2).is_ok()); + assert!(config.append_protocol(HTTP11).is_ok()); + assert_eq!(config.as_slice(), b"\x02h2\x08http/1.1"); + + // Empty protocol is rejected. + let mut config = AlpnProtocols::default(); + assert!(config.append_protocol(b"").is_err()); + + // > 255 bytes is rejected, 255 is accepted. + let mut config = AlpnProtocols::default(); + assert!(config.append_protocol(&[b'a'; 256]).is_err()); + assert_eq!(config.as_slice(), b""); + assert!(config.append_protocol(&[b'a'; 255]).is_ok()); + assert_eq!(config.as_slice()[0], 255); + assert_eq!(&config.as_slice()[1..], &[b'a'; 255]); +}