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]);
+}