rust: bssl-tls: More TLS context protocol controls

Bug: 479599893

Signed-off-by: Xiangfei Ding <xfding@google.com>
Change-Id: I98a5071072687b70b5df2641ba920ac76a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/91388
Presubmit-BoringSSL-Verified: boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com <boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/rust/bssl-tls/src/context.rs b/rust/bssl-tls/src/context.rs
index 60f4196..6ccfb56 100644
--- a/rust/bssl-tls/src/context.rs
+++ b/rust/bssl-tls/src/context.rs
@@ -24,8 +24,17 @@
     ptr::NonNull, //
 };
 
+use bssl_crypto::FfiSlice;
+
 use crate::{
-    config::CompliancePolicy,
+    check_lib_error,
+    config::{
+        CompliancePolicy,
+        ConfigurationError,
+        KeyExchangeGroupFlag,
+        KeyExchangeGroups,
+        ProtocolVersion, //
+    },
     connection::{
         Client,
         Server,
@@ -33,7 +42,8 @@
         methods::HasTlsConnectionMethod, //
     },
     context::methods::HasTlsContextMethod,
-    errors::Error, //
+    errors::Error,
+    has_duplicates, //
 };
 
 mod credentials;
@@ -164,6 +174,182 @@
             &mut *(methods as *mut methods::RustContextMethods<M>)
         }
     }
+
+    /// Set the minimum acceptable protocol.
+    ///
+    /// If `version` is set to `None`, the library will choose a default minimum version.
+    /// - For TLS, it is TLS 1.3
+    /// - For DTLS, it is DTLS 1.2
+    ///
+    /// This configuration fails if the protocol version is incompatible with the selected TLS
+    /// flavour, such as setting TLS 1.3 for DTLS flavour.
+    pub fn with_min_protocol(
+        &mut self,
+        version: Option<ProtocolVersion>,
+    ) -> Result<&mut Self, Error> {
+        let version = version.map_or(0, |ver| ver as u16);
+        check_lib_error!(unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by `self`
+            bssl_sys::SSL_CTX_set_min_proto_version(self.ptr(), version)
+        });
+        Ok(self)
+    }
+
+    /// Set the maximum acceptable protocol.
+    ///
+    /// If `version` is set to `None`, the library will choose a default maximum version.
+    /// - For TLS, it is TLS 1.3
+    /// - For DTLS, it is DTLS 1.2
+    ///
+    /// This configuration fails if the protocol version is incompatible with the selected TLS
+    /// flavour, such as setting TLS 1.3 for DTLS flavour.
+    pub fn with_max_protocol(
+        &mut self,
+        version: Option<ProtocolVersion>,
+    ) -> Result<&mut Self, Error> {
+        let version = version.map_or(0, |ver| ver as u16);
+        check_lib_error!(unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by `self`
+            bssl_sys::SSL_CTX_set_max_proto_version(self.ptr(), version)
+        });
+        Ok(self)
+    }
+
+    /// Set acceptable key exchange groups.
+    ///
+    /// This method sets up the acceptable key exchange groups and the list of groups advertised by
+    /// the client.
+    ///
+    /// If the supplied list is empty, the context will revert back to a default list of groups.
+    ///
+    /// If the `flags` list is not empty, its length must match that of `groups`.
+    ///
+    /// This method returns [`ConfigurationError::InvalidParameters`] if the key exchange groups are not unique in the list.
+    pub fn with_key_exchange_groups(
+        &mut self,
+        groups: &[KeyExchangeGroups],
+        flags: &[KeyExchangeGroupFlag],
+    ) -> Result<&mut Self, Error> {
+        // TODO(@xfding): maybe we should use zerocopy here when v0.9 is finalised
+        let groups: &[u16] = unsafe {
+            // Safety: `KeyExchangeGroups` has the same layout as a `u16`,
+            // so `&[KeyExchangeGroups]` and `&[u16]` has the same layout.
+            core::mem::transmute(groups)
+        };
+        let flags: &[u32] = unsafe {
+            // Safety: `KeyExchangeGroupFlag` has the same layout as a `u32` through
+            // `repr(transparent)`.
+            core::mem::transmute(flags)
+        };
+        if !flags.is_empty() && groups.len() != flags.len() {
+            return Err(Error::Configuration(ConfigurationError::InvalidParameters));
+        }
+        if has_duplicates(groups) {
+            return Err(Error::Configuration(
+                ConfigurationError::DuplicatedKeyExchangeGroup,
+            ));
+        }
+
+        check_lib_error!(unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by `self`
+            if flags.is_empty() {
+                bssl_sys::SSL_CTX_set1_group_ids(self.ptr(), groups.as_ffi_ptr(), groups.len())
+            } else {
+                bssl_sys::SSL_CTX_set1_group_ids_with_flags(
+                    self.ptr(),
+                    groups.as_ffi_ptr(),
+                    flags.as_ffi_ptr(),
+                    groups.len(),
+                )
+            }
+        });
+        Ok(self)
+    }
+
+    /// Set a list of cipher suites, in the order of descending preference.
+    ///
+    /// This method accepts a string that conforms to OpenSSL [cipher suite configuration] language
+    /// specification.
+    ///
+    /// # Available cipher rules are:
+    ///
+    /// - `ALL` matches all ciphers, except for deprecated ciphers which must be
+    ///   named explicitly.
+    ///
+    /// - `kRSA`, `kDHE`, `kECDHE`, and `kPSK` match ciphers using plain RSA, DHE,
+    ///   ECDHE, and plain PSK key exchanges, respectively. Note that ECDHE_PSK is
+    ///   matched by `kECDHE` and not `kPSK`.
+    ///
+    /// - `aRSA`, `aECDSA`, and `aPSK` match ciphers authenticated by RSA, ECDSA, and
+    ///   a pre-shared key, respectively.
+    ///
+    /// - `RSA`, `DHE`, `ECDHE`, `PSK`, `ECDSA`, and `PSK` are aliases for the
+    ///   corresponding `k*` or `a*` cipher rule. `RSA` is an alias for `kRSA`, not
+    ///   `aRSA`.
+    ///
+    /// - `3DES`, `AES128`, `AES256`, `AES`, `AESGCM`, `CHACHA20` match ciphers
+    ///   whose bulk cipher use the corresponding encryption scheme. Note that
+    ///   `AES`, `AES128`, and `AES256` match both CBC and GCM ciphers.
+    ///
+    /// - `SHA1`, and its alias `SHA`, match legacy cipher suites using HMAC-SHA1.
+    ///
+    /// # Deprecated cipher rules:
+    ///
+    /// - `kEDH`, `EDH`, `kEECDH`, and `EECDH` are legacy aliases for `kDHE`, `DHE`,
+    ///   `kECDHE`, and `ECDHE`, respectively.
+    ///
+    /// - `HIGH` is an alias for `ALL`.
+    ///
+    /// - `FIPS` is an alias for `HIGH`.
+    ///
+    /// - `SSLv3` and `TLSv1` match ciphers available in TLS 1.1 or earlier.
+    ///   `TLSv1_2` matches ciphers new in TLS 1.2. This is confusing and should not
+    ///   be used.
+    ///
+    /// [cipher suite configuration]: <https://docs.openssl.org/3.0/man1/openssl-ciphers/#cipher-list-format>
+    pub fn with_ciphers(&mut self, cipher_ops: &str) -> Result<&mut Self, Error> {
+        let Ok(cipher_ops) = alloc::ffi::CString::new(cipher_ops) else {
+            return Err(Error::Configuration(ConfigurationError::InvalidString));
+        };
+        check_lib_error!(unsafe {
+            // Safety:
+            // - the validity of the handle `self.0` is witnessed by `self`;
+            // - the string has been checked for internal NUL bytes and NUL-terminated.
+            bssl_sys::SSL_CTX_set_strict_cipher_list(self.ptr(), cipher_ops.as_ptr())
+        });
+        Ok(self)
+    }
+
+    /// Set TLS 1.2 session lifetime in seconds.
+    ///
+    /// If 0 seconds are specified, BoringSSL will pick a default timeout for the connections.
+    pub fn with_tls12_timeout(&mut self, seconds: u32) -> &mut Self {
+        unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by `self`.
+            bssl_sys::SSL_CTX_set_timeout(self.ptr(), seconds)
+        };
+        self
+    }
+
+    /// Set TLS 1.3 session lifetime in seconds.
+    ///
+    /// If 0 seconds are specified, BoringSSL will pick a default timeout for the connections.
+    pub fn with_tls13_timeout(&mut self, seconds: u32) -> &mut Self {
+        unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by `self`.
+            bssl_sys::SSL_CTX_set_session_psk_dhe_timeout(self.ptr(), seconds)
+        };
+        self
+    }
+
+    /// Set whether shutting down the connection sends out a `close_notify` alert.
+    pub fn with_quiet_shutdown(&mut self, quiet: bool) -> &mut Self {
+        unsafe {
+            // Safety: the validity of the handle `self.0` is witnessed by `self`.
+            bssl_sys::SSL_CTX_set_quiet_shutdown(self.ptr(), if quiet { 1 } else { 0 });
+        }
+        self
+    }
 }
 
 impl<M> Drop for TlsContextBuilder<M> {
diff --git a/rust/bssl-tls/src/lib.rs b/rust/bssl-tls/src/lib.rs
index 71e343e..6390057 100644
--- a/rust/bssl-tls/src/lib.rs
+++ b/rust/bssl-tls/src/lib.rs
@@ -47,6 +47,11 @@
 #[doc(hidden)]
 mod macros;
 
+fn has_duplicates<T: Ord + Eq>(list: &[T]) -> bool {
+    let mut seen = alloc::collections::BTreeSet::new();
+    list.iter().any(|elem| !seen.insert(elem))
+}
+
 #[allow(unused)]
 pub(crate) trait Methods {
     /// Safety: `ssl` must outlive `'a` and it must be passed in from BoringSSL