Implement mls-rs-crypto-traits backed by BoringSSL.

Fix: 302021139
Test: Presubmit
Change-Id: Iaefa21d3fb69f92d735875778f3f96e1878d0876
diff --git a/mls/mls-rs-crypto-boringssl/src/aead.rs b/mls/mls-rs-crypto-boringssl/src/aead.rs
new file mode 100644
index 0000000..eaa33a9
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/aead.rs
@@ -0,0 +1,334 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Authenticated encryption with additional data.
+
+use bssl_crypto::aead::{Aead, Aes128Gcm, Aes256Gcm, Chacha20Poly1305};
+use mls_rs_core::crypto::CipherSuite;
+use mls_rs_core::error::IntoAnyError;
+use mls_rs_crypto_traits::{AeadId, AeadType, AES_TAG_LEN};
+
+use core::array::TryFromSliceError;
+use thiserror::Error;
+
+/// Errors returned from AEAD.
+#[derive(Debug, Error)]
+pub enum AeadError {
+    /// Error returned when conversion from slice to array fails.
+    #[error(transparent)]
+    TryFromSliceError(#[from] TryFromSliceError),
+    /// Error returned when the ciphertext is invalid.
+    #[error("AEAD ciphertext was invalid")]
+    InvalidCiphertext,
+    /// Error returned when the ciphertext length is too short.
+    #[error("AEAD ciphertext of length {len}, expected length at least {min_len}")]
+    TooShortCiphertext {
+        /// Invalid ciphertext length.
+        len: usize,
+        /// Minimum ciphertext length.
+        min_len: usize,
+    },
+    /// Error returned when the plaintext is empty.
+    #[error("message cannot be empty")]
+    EmptyPlaintext,
+    /// Error returned when the key length is invalid.
+    #[error("AEAD key of invalid length {len}, expected length {expected_len}")]
+    InvalidKeyLen {
+        /// Invalid key length.
+        len: usize,
+        /// Expected key length.
+        expected_len: usize,
+    },
+    /// Error returned when the nonce size is invalid.
+    #[error("AEAD nonce of invalid length {len}, expected length {expected_len}")]
+    InvalidNonceLen {
+        /// Invalid nonce length.
+        len: usize,
+        /// Expected nonce length.
+        expected_len: usize,
+    },
+    /// Error returned when unsupported cipher suite is requested.
+    #[error("unsupported cipher suite")]
+    UnsupportedCipherSuite,
+}
+
+impl IntoAnyError for AeadError {
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+/// AeadType implementation backed by BoringSSL.
+#[derive(Clone)]
+pub struct AeadWrapper(AeadId);
+
+impl AeadWrapper {
+    /// Creates a new AeadWrapper.
+    pub fn new(cipher_suite: CipherSuite) -> Option<Self> {
+        AeadId::new(cipher_suite).map(Self)
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl AeadType for AeadWrapper {
+    type Error = AeadError;
+
+    async fn seal<'a>(
+        &self,
+        key: &[u8],
+        data: &[u8],
+        aad: Option<&'a [u8]>,
+        nonce: &[u8],
+    ) -> Result<Vec<u8>, AeadError> {
+        if data.is_empty() {
+            return Err(AeadError::EmptyPlaintext);
+        }
+        if key.len() != self.key_size() {
+            return Err(AeadError::InvalidKeyLen { len: key.len(), expected_len: self.key_size() });
+        }
+        if nonce.len() != self.nonce_size() {
+            return Err(AeadError::InvalidNonceLen {
+                len: nonce.len(),
+                expected_len: self.nonce_size(),
+            });
+        }
+
+        let nonce_array = nonce[..self.nonce_size()].try_into()?;
+
+        match self.0 {
+            AeadId::Aes128Gcm => {
+                let cipher = Aes128Gcm::new(key[..self.key_size()].try_into()?);
+                Ok(cipher.seal(nonce_array, data, aad.unwrap_or_default()))
+            }
+            AeadId::Aes256Gcm => {
+                let cipher = Aes256Gcm::new(key[..self.key_size()].try_into()?);
+                Ok(cipher.seal(nonce_array, data, aad.unwrap_or_default()))
+            }
+            AeadId::Chacha20Poly1305 => {
+                let cipher = Chacha20Poly1305::new(key[..self.key_size()].try_into()?);
+                Ok(cipher.seal(nonce_array, data, aad.unwrap_or_default()))
+            }
+            _ => Err(AeadError::UnsupportedCipherSuite),
+        }
+    }
+
+    async fn open<'a>(
+        &self,
+        key: &[u8],
+        ciphertext: &[u8],
+        aad: Option<&'a [u8]>,
+        nonce: &[u8],
+    ) -> Result<Vec<u8>, AeadError> {
+        if ciphertext.len() < AES_TAG_LEN {
+            return Err(AeadError::TooShortCiphertext {
+                len: ciphertext.len(),
+                min_len: AES_TAG_LEN,
+            });
+        }
+        if key.len() != self.key_size() {
+            return Err(AeadError::InvalidKeyLen { len: key.len(), expected_len: self.key_size() });
+        }
+        if nonce.len() != self.nonce_size() {
+            return Err(AeadError::InvalidNonceLen {
+                len: nonce.len(),
+                expected_len: self.nonce_size(),
+            });
+        }
+
+        let nonce_array = nonce[..self.nonce_size()].try_into()?;
+
+        match self.0 {
+            AeadId::Aes128Gcm => {
+                let cipher = Aes128Gcm::new(key[..self.key_size()].try_into()?);
+                cipher
+                    .open(nonce_array, ciphertext, aad.unwrap_or_default())
+                    .ok_or(AeadError::InvalidCiphertext)
+            }
+            AeadId::Aes256Gcm => {
+                let cipher = Aes256Gcm::new(key[..self.key_size()].try_into()?);
+                cipher
+                    .open(nonce_array, ciphertext, aad.unwrap_or_default())
+                    .ok_or(AeadError::InvalidCiphertext)
+            }
+            AeadId::Chacha20Poly1305 => {
+                let cipher = Chacha20Poly1305::new(key[..self.key_size()].try_into()?);
+                cipher
+                    .open(nonce_array, ciphertext, aad.unwrap_or_default())
+                    .ok_or(AeadError::InvalidCiphertext)
+            }
+            _ => Err(AeadError::UnsupportedCipherSuite),
+        }
+    }
+
+    #[inline(always)]
+    fn key_size(&self) -> usize {
+        self.0.key_size()
+    }
+
+    fn nonce_size(&self) -> usize {
+        self.0.nonce_size()
+    }
+
+    fn aead_id(&self) -> u16 {
+        self.0 as u16
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::{AeadError, AeadWrapper};
+    use assert_matches::assert_matches;
+    use mls_rs_core::crypto::CipherSuite;
+    use mls_rs_crypto_traits::{AeadType, AES_TAG_LEN};
+
+    fn get_aeads() -> Vec<AeadWrapper> {
+        [
+            CipherSuite::CURVE25519_AES128,
+            CipherSuite::CURVE25519_CHACHA,
+            CipherSuite::CURVE448_AES256,
+        ]
+        .into_iter()
+        .map(|suite| AeadWrapper::new(suite).unwrap())
+        .collect()
+    }
+
+    #[test]
+    fn seal_and_open() {
+        for aead in get_aeads() {
+            let key = vec![42u8; aead.key_size()];
+            let nonce = vec![42u8; aead.nonce_size()];
+            let plaintext = b"message";
+
+            let ciphertext = aead.seal(&key, plaintext, None, &nonce).unwrap();
+            assert_eq!(
+                plaintext,
+                aead.open(&key, ciphertext.as_slice(), None, &nonce).unwrap().as_slice(),
+                "open failed for AEAD with ID {}",
+                aead.aead_id(),
+            );
+        }
+    }
+
+    #[test]
+    fn seal_and_open_with_invalid_key() {
+        for aead in get_aeads() {
+            let data = b"top secret data that's long enough";
+            let nonce = vec![42u8; aead.nonce_size()];
+
+            let key_short = vec![42u8; aead.key_size() - 1];
+            assert_matches!(
+                aead.seal(&key_short, data, None, &nonce),
+                Err(AeadError::InvalidKeyLen { .. }),
+                "seal with short key should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+            assert_matches!(
+                aead.open(&key_short, data, None, &nonce),
+                Err(AeadError::InvalidKeyLen { .. }),
+                "open with short key should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+
+            let key_long = vec![42u8; aead.key_size() + 1];
+            assert_matches!(
+                aead.seal(&key_long, data, None, &nonce),
+                Err(AeadError::InvalidKeyLen { .. }),
+                "seal with long key should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+            assert_matches!(
+                aead.open(&key_long, data, None, &nonce),
+                Err(AeadError::InvalidKeyLen { .. }),
+                "open with long key should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+        }
+    }
+
+    #[test]
+    fn invalid_ciphertext() {
+        for aead in get_aeads() {
+            let key = vec![42u8; aead.key_size()];
+            let nonce = vec![42u8; aead.nonce_size()];
+
+            let ciphertext_short = [0u8; AES_TAG_LEN - 1];
+            assert_matches!(
+                aead.open(&key, &ciphertext_short, None, &nonce),
+                Err(AeadError::TooShortCiphertext { .. }),
+                "open with short ciphertext should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+        }
+    }
+
+    #[test]
+    fn associated_data_mismatch() {
+        for aead in get_aeads() {
+            let key = vec![42u8; aead.key_size()];
+            let nonce = vec![42u8; aead.nonce_size()];
+
+            let ciphertext = aead.seal(&key, b"message", Some(b"foo"), &nonce).unwrap();
+            assert_matches!(
+                aead.open(&key, &ciphertext, Some(b"bar"), &nonce),
+                Err(AeadError::InvalidCiphertext),
+                "open with incorrect associated data should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+            assert_matches!(
+                aead.open(&key, &ciphertext, None, &nonce),
+                Err(AeadError::InvalidCiphertext),
+                "open with incorrect associated data should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+        }
+    }
+
+    #[test]
+    fn invalid_nonce() {
+        for aead in get_aeads() {
+            let key = vec![42u8; aead.key_size()];
+            let data = b"top secret data that's long enough";
+
+            let nonce_short = vec![42u8; aead.nonce_size() - 1];
+            assert_matches!(
+                aead.seal(&key, data, None, &nonce_short),
+                Err(AeadError::InvalidNonceLen { .. }),
+                "seal with short nonce should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+            assert_matches!(
+                aead.open(&key, data, None, &nonce_short),
+                Err(AeadError::InvalidNonceLen { .. }),
+                "open with short nonce should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+
+            let nonce_long = vec![42u8; aead.nonce_size() + 1];
+            assert_matches!(
+                aead.seal(&key, data, None, &nonce_long),
+                Err(AeadError::InvalidNonceLen { .. }),
+                "seal with long nonce should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+            assert_matches!(
+                aead.open(&key, data, None, &nonce_long),
+                Err(AeadError::InvalidNonceLen { .. }),
+                "open with long nonce should fail for AEAD with ID {}",
+                aead.aead_id(),
+            );
+        }
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/ecdh.rs b/mls/mls-rs-crypto-boringssl/src/ecdh.rs
new file mode 100644
index 0000000..74ba8df
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/ecdh.rs
@@ -0,0 +1,280 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Elliptic curve Diffie–Hellman.
+
+use bssl_crypto::x25519;
+use mls_rs_core::crypto::{CipherSuite, HpkePublicKey, HpkeSecretKey};
+use mls_rs_core::error::IntoAnyError;
+use mls_rs_crypto_traits::{Curve, DhType};
+
+use core::array::TryFromSliceError;
+use thiserror::Error;
+
+/// Errors returned from ECDH.
+#[derive(Debug, Error)]
+pub enum EcdhError {
+    /// Error returned when conversion from slice to array fails.
+    #[error(transparent)]
+    TryFromSliceError(#[from] TryFromSliceError),
+    /// Error returned when the public key is invalid.
+    #[error("ECDH public key was invalid")]
+    InvalidPubKey,
+    /// Error returned when the private key length is invalid.
+    #[error("ECDH private key of invalid length {len}, expected length {expected_len}")]
+    InvalidPrivKeyLen {
+        /// Invalid key length.
+        len: usize,
+        /// Expected key length.
+        expected_len: usize,
+    },
+    /// Error returned when the public key length is invalid.
+    #[error("ECDH public key of invalid length {len}, expected length {expected_len}")]
+    InvalidPubKeyLen {
+        /// Invalid key length.
+        len: usize,
+        /// Expected key length.
+        expected_len: usize,
+    },
+    /// Error returned when unsupported cipher suite is requested.
+    #[error("unsupported cipher suite")]
+    UnsupportedCipherSuite,
+}
+
+impl IntoAnyError for EcdhError {
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+/// DhType implementation backed by BoringSSL.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Ecdh(Curve);
+
+impl Ecdh {
+    /// Creates a new Ecdh.
+    pub fn new(cipher_suite: CipherSuite) -> Option<Self> {
+        Curve::from_ciphersuite(cipher_suite, /*for_sig=*/ false).map(Self)
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl DhType for Ecdh {
+    type Error = EcdhError;
+
+    async fn dh(
+        &self,
+        secret_key: &HpkeSecretKey,
+        public_key: &HpkePublicKey,
+    ) -> Result<Vec<u8>, Self::Error> {
+        if self.0 != Curve::X25519 {
+            return Err(EcdhError::UnsupportedCipherSuite);
+        }
+        if secret_key.len() != x25519::PRIVATE_KEY_LEN {
+            return Err(EcdhError::InvalidPrivKeyLen {
+                len: secret_key.len(),
+                expected_len: x25519::PRIVATE_KEY_LEN,
+            });
+        }
+        if public_key.len() != x25519::PUBLIC_KEY_LEN {
+            return Err(EcdhError::InvalidPubKeyLen {
+                len: public_key.len(),
+                expected_len: x25519::PUBLIC_KEY_LEN,
+            });
+        }
+
+        let private_key = x25519::PrivateKey(secret_key[..x25519::PRIVATE_KEY_LEN].try_into()?);
+        match private_key.compute_shared_key(public_key[..x25519::PUBLIC_KEY_LEN].try_into()?) {
+            Some(x) => Ok(x.to_vec()),
+            None => Err(EcdhError::InvalidPubKey),
+        }
+    }
+
+    async fn to_public(&self, secret_key: &HpkeSecretKey) -> Result<HpkePublicKey, Self::Error> {
+        if self.0 != Curve::X25519 {
+            return Err(EcdhError::UnsupportedCipherSuite);
+        }
+        if secret_key.len() != x25519::PRIVATE_KEY_LEN {
+            return Err(EcdhError::InvalidPrivKeyLen {
+                len: secret_key.len(),
+                expected_len: x25519::PRIVATE_KEY_LEN,
+            });
+        }
+
+        let private_key = x25519::PrivateKey(secret_key[..x25519::PRIVATE_KEY_LEN].try_into()?);
+        Ok(private_key.to_public().to_vec().into())
+    }
+
+    async fn generate(&self) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> {
+        if self.0 != Curve::X25519 {
+            return Err(EcdhError::UnsupportedCipherSuite);
+        }
+
+        let (public_key, private_key) = x25519::PrivateKey::generate();
+        Ok((private_key.0.to_vec().into(), public_key.to_vec().into()))
+    }
+
+    fn bitmask_for_rejection_sampling(&self) -> Option<u8> {
+        self.0.curve_bitmask()
+    }
+
+    fn public_key_validate(&self, key: &HpkePublicKey) -> Result<(), Self::Error> {
+        if self.0 != Curve::X25519 {
+            return Err(EcdhError::UnsupportedCipherSuite);
+        }
+
+        // bssl_crypto does not implement validation of curve25519 public keys.
+        // Note: Neither does x25519_dalek used by RustCrypto's implementation of this function.
+        if key.len() != x25519::PUBLIC_KEY_LEN {
+            return Err(EcdhError::InvalidPubKeyLen {
+                len: key.len(),
+                expected_len: x25519::PUBLIC_KEY_LEN,
+            });
+        }
+        Ok(())
+    }
+
+    fn secret_key_size(&self) -> usize {
+        self.0.secret_key_size()
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::{DhType, Ecdh, EcdhError};
+    use crate::test_helpers::decode_hex;
+    use assert_matches::assert_matches;
+    use mls_rs_core::crypto::{CipherSuite, HpkePublicKey, HpkeSecretKey};
+
+    #[test]
+    fn dh() {
+        // https://github.com/C2SP/wycheproof/blob/cd27d6419bedd83cbd24611ec54b6d4bfdb0cdca/testvectors/x25519_test.json#L23
+        let private_key = HpkeSecretKey::from(
+            decode_hex::<32>("c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475")
+                .to_vec(),
+        );
+        let public_key = HpkePublicKey::from(
+            decode_hex::<32>("504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829")
+                .to_vec(),
+        );
+        let expected_shared_secret: [u8; 32] =
+            decode_hex("436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320");
+
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_eq!(x25519.dh(&private_key, &public_key).unwrap(), expected_shared_secret);
+    }
+
+    #[test]
+    fn dh_invalid_key() {
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_AES128).unwrap();
+
+        let private_key_short =
+            HpkeSecretKey::from(decode_hex::<16>("c8a9d5a91091ad851c668b0736c1c9a0").to_vec());
+        let public_key = HpkePublicKey::from(
+            decode_hex::<32>("504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829")
+                .to_vec(),
+        );
+        assert_matches!(
+            x25519.dh(&private_key_short, &public_key),
+            Err(EcdhError::InvalidPrivKeyLen { .. })
+        );
+
+        let private_key = HpkeSecretKey::from(
+            decode_hex::<32>("c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475")
+                .to_vec(),
+        );
+        let public_key_short =
+            HpkePublicKey::from(decode_hex::<16>("504a36999f489cd2fdbc08baff3d88fa").to_vec());
+        assert_matches!(
+            x25519.dh(&private_key, &public_key_short),
+            Err(EcdhError::InvalidPubKeyLen { .. })
+        );
+    }
+
+    #[test]
+    fn to_public() {
+        // https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1
+        let private_key = HpkeSecretKey::from(
+            decode_hex::<32>("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
+                .to_vec(),
+        );
+        let expected_public_key = HpkePublicKey::from(
+            decode_hex::<32>("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
+                .to_vec(),
+        );
+
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_CHACHA).unwrap();
+        assert_eq!(x25519.to_public(&private_key).unwrap(), expected_public_key);
+    }
+
+    #[test]
+    fn to_public_invalid_key() {
+        let private_key_short =
+            HpkeSecretKey::from(decode_hex::<16>("c8a9d5a91091ad851c668b0736c1c9a0").to_vec());
+
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_CHACHA).unwrap();
+        assert_matches!(
+            x25519.to_public(&private_key_short),
+            Err(EcdhError::InvalidPrivKeyLen { .. })
+        );
+    }
+
+    #[test]
+    fn generate() {
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert!(x25519.generate().is_ok());
+    }
+
+    #[test]
+    fn public_key_validate() {
+        // https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1
+        let public_key = HpkePublicKey::from(
+            decode_hex::<32>("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
+                .to_vec(),
+        );
+
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert!(x25519.public_key_validate(&public_key).is_ok());
+    }
+
+    #[test]
+    fn public_key_validate_invalid_key() {
+        let public_key_short =
+            HpkePublicKey::from(decode_hex::<16>("504a36999f489cd2fdbc08baff3d88fa").to_vec());
+
+        let x25519 = Ecdh::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(
+            x25519.public_key_validate(&public_key_short),
+            Err(EcdhError::InvalidPubKeyLen { .. })
+        );
+    }
+
+    #[test]
+    fn unsupported_cipher_suites() {
+        for suite in vec![
+            CipherSuite::P256_AES128,
+            CipherSuite::P384_AES256,
+            CipherSuite::P521_AES256,
+            CipherSuite::CURVE448_CHACHA,
+            CipherSuite::CURVE448_AES256,
+        ] {
+            assert_matches!(
+                Ecdh::new(suite).unwrap().generate(),
+                Err(EcdhError::UnsupportedCipherSuite)
+            );
+        }
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/eddsa.rs b/mls/mls-rs-crypto-boringssl/src/eddsa.rs
new file mode 100644
index 0000000..473b756
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/eddsa.rs
@@ -0,0 +1,284 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Edwards-curve digital signature algorithm.
+
+use bssl_crypto::{ed25519, InvalidSignatureError};
+use mls_rs_core::crypto::{CipherSuite, SignaturePublicKey, SignatureSecretKey};
+use mls_rs_crypto_traits::Curve;
+
+use core::array::TryFromSliceError;
+use thiserror::Error;
+
+/// Errors returned from EdDSA.
+#[derive(Debug, Error)]
+pub enum EdDsaError {
+    /// Error returned when conversion from slice to array fails.
+    #[error(transparent)]
+    TryFromSliceError(#[from] TryFromSliceError),
+    /// Error returned on an invalid signature.
+    #[error("invalid signature")]
+    InvalidSig(InvalidSignatureError),
+    /// Error returned when the private key length is invalid.
+    #[error("EdDSA private key of invalid length {len}, expected length {expected_len}")]
+    InvalidPrivKeyLen {
+        /// Invalid key length.
+        len: usize,
+        /// Expected key length.
+        expected_len: usize,
+    },
+    /// Error returned when the public key length is invalid.
+    #[error("EdDSA public key of invalid length {len}, expected length {expected_len}")]
+    InvalidPubKeyLen {
+        /// Invalid key length.
+        len: usize,
+        /// Expected key length.
+        expected_len: usize,
+    },
+    /// Error returned when the signature length is invalid.
+    #[error("EdDSA signature of invalid length {len}, expected length {expected_len}")]
+    InvalidSigLen {
+        /// Invalid signature length.
+        len: usize,
+        /// Expected signature length.
+        expected_len: usize,
+    },
+    /// Error returned when unsupported cipher suite is requested.
+    #[error("unsupported cipher suite")]
+    UnsupportedCipherSuite,
+}
+
+// Explicitly implemented as InvalidSignatureError's as_dyn_error does not satisfy trait bounds.
+impl From<InvalidSignatureError> for EdDsaError {
+    fn from(e: InvalidSignatureError) -> Self {
+        EdDsaError::InvalidSig(e)
+    }
+}
+
+/// EdDSA implementation backed by BoringSSL.
+#[derive(Clone, Debug, Copy, PartialEq, Eq)]
+pub struct EdDsa(Curve);
+
+impl EdDsa {
+    /// Creates a new EdDsa.
+    pub fn new(cipher_suite: CipherSuite) -> Option<Self> {
+        Curve::from_ciphersuite(cipher_suite, /*for_sig=*/ true).map(Self)
+    }
+
+    /// Generates a key pair.
+    pub fn signature_key_generate(
+        &self,
+    ) -> Result<(SignatureSecretKey, SignaturePublicKey), EdDsaError> {
+        if self.0 != Curve::Ed25519 {
+            return Err(EdDsaError::UnsupportedCipherSuite);
+        }
+
+        let private_key = ed25519::PrivateKey::generate();
+        let public_key = private_key.to_public();
+        Ok((private_key.to_seed().to_vec().into(), public_key.as_bytes().to_vec().into()))
+    }
+
+    /// Derives the public key from the private key.
+    pub fn signature_key_derive_public(
+        &self,
+        secret_key: &SignatureSecretKey,
+    ) -> Result<SignaturePublicKey, EdDsaError> {
+        if self.0 != Curve::Ed25519 {
+            return Err(EdDsaError::UnsupportedCipherSuite);
+        }
+        if secret_key.len() != ed25519::SEED_LEN {
+            return Err(EdDsaError::InvalidPrivKeyLen {
+                len: secret_key.len(),
+                expected_len: ed25519::SEED_LEN,
+            });
+        }
+
+        let private_key =
+            ed25519::PrivateKey::from_seed(secret_key[..ed25519::SEED_LEN].try_into()?);
+        Ok(private_key.to_public().as_bytes().to_vec().into())
+    }
+
+    /// Signs `data` using `secret_key`.
+    pub fn sign(
+        &self,
+        secret_key: &SignatureSecretKey,
+        data: &[u8],
+    ) -> Result<Vec<u8>, EdDsaError> {
+        if self.0 != Curve::Ed25519 {
+            return Err(EdDsaError::UnsupportedCipherSuite);
+        }
+        if secret_key.len() != ed25519::SEED_LEN {
+            return Err(EdDsaError::InvalidPrivKeyLen {
+                len: secret_key.len(),
+                expected_len: ed25519::SEED_LEN,
+            });
+        }
+
+        let private_key =
+            ed25519::PrivateKey::from_seed(secret_key[..ed25519::SEED_LEN].try_into()?);
+        Ok(private_key.sign(data).to_vec())
+    }
+
+    /// Verifies `signature` is a valid signature of `data` using `public_key`.
+    pub fn verify(
+        &self,
+        public_key: &SignaturePublicKey,
+        signature: &[u8],
+        data: &[u8],
+    ) -> Result<(), EdDsaError> {
+        if self.0 != Curve::Ed25519 {
+            return Err(EdDsaError::UnsupportedCipherSuite);
+        }
+        if public_key.len() != ed25519::PUBLIC_KEY_LEN {
+            return Err(EdDsaError::InvalidPubKeyLen {
+                len: public_key.len(),
+                expected_len: ed25519::PUBLIC_KEY_LEN,
+            });
+        }
+        if signature.len() != ed25519::SIGNATURE_LEN {
+            return Err(EdDsaError::InvalidSigLen {
+                len: signature.len(),
+                expected_len: ed25519::SIGNATURE_LEN,
+            });
+        }
+
+        let public_key = ed25519::PublicKey::from_bytes(
+            public_key.as_bytes()[..ed25519::PUBLIC_KEY_LEN].try_into()?,
+        );
+        match public_key.verify(data, signature[..ed25519::SIGNATURE_LEN].try_into()?) {
+            Ok(_) => Ok(()),
+            Err(e) => Err(EdDsaError::InvalidSig(e)),
+        }
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::{EdDsa, EdDsaError};
+    use crate::test_helpers::decode_hex;
+    use assert_matches::assert_matches;
+    use mls_rs_core::crypto::{CipherSuite, SignaturePublicKey, SignatureSecretKey};
+
+    #[test]
+    fn signature_key_generate() {
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert!(ed25519.signature_key_generate().is_ok());
+    }
+
+    #[test]
+    fn signature_key_derive_public() {
+        // Test 1 from https://www.rfc-editor.org/rfc/rfc8032#section-7.1
+        let private_key = SignatureSecretKey::from(
+            decode_hex::<32>("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
+                .to_vec(),
+        );
+        let expected_public_key = SignaturePublicKey::from(
+            decode_hex::<32>("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
+                .to_vec(),
+        );
+
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_CHACHA).unwrap();
+        assert_eq!(ed25519.signature_key_derive_public(&private_key).unwrap(), expected_public_key);
+    }
+
+    #[test]
+    fn signature_key_derive_public_invalid_key() {
+        let private_key_short =
+            SignatureSecretKey::from(decode_hex::<16>("9d61b19deffd5a60ba844af492ec2cc4").to_vec());
+
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_CHACHA).unwrap();
+        assert_matches!(
+            ed25519.signature_key_derive_public(&private_key_short),
+            Err(EdDsaError::InvalidPrivKeyLen { .. })
+        );
+    }
+
+    #[test]
+    fn sign_verify() {
+        // Test 3 from https://www.rfc-editor.org/rfc/rfc8032#section-7.1
+        let private_key = SignatureSecretKey::from(
+            decode_hex::<32>("c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7")
+                .to_vec(),
+        );
+        let data: [u8; 2] = decode_hex("af82");
+        let expected_sig = decode_hex::<64>("6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a").to_vec();
+
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_AES128).unwrap();
+        let sig = ed25519.sign(&private_key, &data).unwrap();
+        assert_eq!(sig, expected_sig);
+
+        let public_key = ed25519.signature_key_derive_public(&private_key).unwrap();
+        assert!(ed25519.verify(&public_key, &sig, &data).is_ok());
+    }
+
+    #[test]
+    fn sign_invalid_key() {
+        let private_key_short =
+            SignatureSecretKey::from(decode_hex::<16>("c5aa8df43f9f837bedb7442f31dcb7b1").to_vec());
+
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(
+            ed25519.sign(&private_key_short, &decode_hex::<2>("af82")),
+            Err(EdDsaError::InvalidPrivKeyLen { .. })
+        );
+    }
+
+    #[test]
+    fn verify_invalid_key() {
+        let public_key_short =
+            SignaturePublicKey::from(decode_hex::<16>("fc51cd8e6218a1a38da47ed00230f058").to_vec());
+        let sig = decode_hex::<64>("6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a").to_vec();
+        let data: [u8; 2] = decode_hex("af82");
+
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(
+            ed25519.verify(&public_key_short, &sig, &data),
+            Err(EdDsaError::InvalidPubKeyLen { .. })
+        );
+    }
+
+    #[test]
+    fn verify_invalid_sig() {
+        let public_key = SignaturePublicKey::from(
+            decode_hex::<32>("fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025")
+                .to_vec(),
+        );
+        let sig_short =
+            decode_hex::<32>("6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac")
+                .to_vec();
+        let data: [u8; 2] = decode_hex("af82");
+
+        let ed25519 = EdDsa::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(
+            ed25519.verify(&public_key, &sig_short, &data),
+            Err(EdDsaError::InvalidSigLen { .. })
+        );
+    }
+
+    #[test]
+    fn unsupported_cipher_suites() {
+        for suite in vec![
+            CipherSuite::P256_AES128,
+            CipherSuite::P384_AES256,
+            CipherSuite::P521_AES256,
+            CipherSuite::CURVE448_CHACHA,
+            CipherSuite::CURVE448_AES256,
+        ] {
+            assert_matches!(
+                EdDsa::new(suite).unwrap().signature_key_generate(),
+                Err(EdDsaError::UnsupportedCipherSuite)
+            );
+        }
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/hash.rs b/mls/mls-rs-crypto-boringssl/src/hash.rs
new file mode 100644
index 0000000..397fb9d
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/hash.rs
@@ -0,0 +1,152 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Hash functions and hash-based message authentication codes.
+
+use bssl_crypto::digest;
+use bssl_crypto::hmac::{HmacSha256, HmacSha512};
+use mls_rs_core::crypto::CipherSuite;
+use thiserror::Error;
+
+/// Errors returned from hash functions and HMACs.
+#[derive(Debug, Error)]
+pub enum HashError {
+    /// Error returned when unsupported cipher suite is requested.
+    #[error("unsupported cipher suite")]
+    UnsupportedCipherSuite,
+}
+
+/// Hash function and HMAC implementations backed by BoringSSL.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[repr(u16)]
+pub enum Hash {
+    /// SHA-256.
+    Sha256,
+    /// SHA-384.
+    Sha384,
+    /// SHA-512.
+    Sha512,
+}
+
+impl Hash {
+    /// Creates a new Hash.
+    pub fn new(cipher_suite: CipherSuite) -> Result<Self, HashError> {
+        match cipher_suite {
+            CipherSuite::CURVE25519_AES128
+            | CipherSuite::P256_AES128
+            | CipherSuite::CURVE25519_CHACHA => Ok(Hash::Sha256),
+            CipherSuite::P384_AES256 => Ok(Hash::Sha384),
+            CipherSuite::CURVE448_AES256
+            | CipherSuite::CURVE448_CHACHA
+            | CipherSuite::P521_AES256 => Ok(Hash::Sha512),
+            _ => Err(HashError::UnsupportedCipherSuite),
+        }
+    }
+
+    /// Hashes `data`.
+    pub fn hash(&self, data: &[u8]) -> Vec<u8> {
+        match self {
+            Hash::Sha256 => digest::Sha256::hash(data).to_vec(),
+            Hash::Sha384 => digest::Sha384::hash(data).to_vec(),
+            Hash::Sha512 => digest::Sha512::hash(data).to_vec(),
+        }
+    }
+
+    /// Computes the HMAC of `data` using `key`.
+    pub fn mac(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, HashError> {
+        match self {
+            Hash::Sha256 => Ok(HmacSha256::mac(key, data).to_vec()),
+            Hash::Sha384 => Err(HashError::UnsupportedCipherSuite),
+            Hash::Sha512 => Ok(HmacSha512::mac(key, data).to_vec()),
+        }
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::{Hash, HashError};
+    use crate::test_helpers::decode_hex;
+    use assert_matches::assert_matches;
+    use mls_rs_core::crypto::CipherSuite;
+
+    // bssl_crypto::hmac test vectors.
+
+    #[test]
+    fn sha256() {
+        let hash = Hash::new(CipherSuite::P256_AES128).unwrap();
+        assert_eq!(
+            hash.hash(&decode_hex::<4>("74ba2521")),
+            decode_hex::<32>("b16aa56be3880d18cd41e68384cf1ec8c17680c45a02b1575dc1518923ae8b0e")
+        );
+    }
+
+    #[test]
+    fn sha384() {
+        let hash = Hash::new(CipherSuite::P384_AES256).unwrap();
+        assert_eq!(
+            hash.hash(b"abc"),
+            decode_hex::<48>("cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7")
+        );
+    }
+
+    #[test]
+    fn sha512() {
+        let hash = Hash::new(CipherSuite::CURVE448_CHACHA).unwrap();
+        assert_eq!(
+            hash.hash(&decode_hex::<4>("23be86d5")),
+            decode_hex::<64>(concat!(
+                "76d42c8eadea35a69990c63a762f330614a4699977f058adb988f406fb0be8f2",
+                "ea3dce3a2bbd1d827b70b9b299ae6f9e5058ee97b50bd4922d6d37ddc761f8eb"
+            ))
+        );
+    }
+
+    #[test]
+    fn hmac_sha256() {
+        let expected = vec![
+            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
+            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
+            0x2e, 0x32, 0xcf, 0xf7,
+        ];
+        let key: [u8; 20] = [0x0b; 20];
+        let data = b"Hi There";
+
+        let hmac = Hash::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_eq!(expected, hmac.mac(&key, data).unwrap());
+    }
+
+    #[test]
+    fn hmac_sha384() {
+        let key: [u8; 20] = [0x0b; 20];
+        let data = b"Hi There";
+
+        let hmac = Hash::new(CipherSuite::P384_AES256).unwrap();
+        assert_matches!(hmac.mac(&key, data), Err(HashError::UnsupportedCipherSuite));
+    }
+
+    #[test]
+    fn hmac_sha512() {
+        let expected = vec![
+            135, 170, 124, 222, 165, 239, 97, 157, 79, 240, 180, 36, 26, 29, 108, 176, 35, 121,
+            244, 226, 206, 78, 194, 120, 122, 208, 179, 5, 69, 225, 124, 222, 218, 168, 51, 183,
+            214, 184, 167, 2, 3, 139, 39, 78, 174, 163, 244, 228, 190, 157, 145, 78, 235, 97, 241,
+            112, 46, 105, 108, 32, 58, 18, 104, 84,
+        ];
+        let key: [u8; 20] = [0x0b; 20];
+        let data = b"Hi There";
+
+        let hmac = Hash::new(CipherSuite::CURVE448_CHACHA).unwrap();
+        assert_eq!(expected, hmac.mac(&key, data).unwrap());
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/hpke.rs b/mls/mls-rs-crypto-boringssl/src/hpke.rs
new file mode 100644
index 0000000..4bb4aa2
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/hpke.rs
@@ -0,0 +1,541 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Hybrid public key encryption.
+
+use bssl_crypto::hpke;
+use mls_rs_core::crypto::{
+    CipherSuite, HpkeCiphertext, HpkeContextR, HpkeContextS, HpkePublicKey, HpkeSecretKey,
+};
+use mls_rs_core::error::{AnyError, IntoAnyError};
+use mls_rs_crypto_traits::{DhType, KdfType, KemId, KemResult, KemType};
+use std::sync::Mutex;
+use thiserror::Error;
+
+/// Errors returned from HPKE.
+#[derive(Debug, Error)]
+pub enum HpkeError {
+    /// Error returned from BoringSSL.
+    #[error("BoringSSL error")]
+    BoringsslError,
+    /// Error returned from Diffie-Hellman operations.
+    #[error(transparent)]
+    DhError(AnyError),
+    /// Error returned from KDF operations.
+    #[error(transparent)]
+    KdfError(AnyError),
+    /// Error returned when unsupported cipher suite is requested.
+    #[error("unsupported cipher suite")]
+    UnsupportedCipherSuite,
+}
+
+impl IntoAnyError for HpkeError {
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub(crate) struct KdfWrapper<KDF: KdfType> {
+    suite_id: Vec<u8>,
+    kdf: KDF,
+}
+
+impl<KDF: KdfType> KdfWrapper<KDF> {
+    pub fn new(suite_id: Vec<u8>, kdf: KDF) -> Self {
+        Self { suite_id, kdf }
+    }
+
+    // https://www.rfc-editor.org/rfc/rfc9180.html#section-4-9
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn labeled_extract(
+        &self,
+        salt: &[u8],
+        label: &[u8],
+        ikm: &[u8],
+    ) -> Result<Vec<u8>, <KDF as KdfType>::Error> {
+        self.kdf.extract(salt, &[b"HPKE-v1" as &[u8], &self.suite_id, label, ikm].concat()).await
+    }
+
+    // https://www.rfc-editor.org/rfc/rfc9180.html#section-4-9
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn labeled_expand(
+        &self,
+        key: &[u8],
+        label: &[u8],
+        info: &[u8],
+        len: usize,
+    ) -> Result<Vec<u8>, <KDF as KdfType>::Error> {
+        let labeled_info =
+            [&(len as u16).to_be_bytes() as &[u8], b"HPKE-v1", &self.suite_id, label, info]
+                .concat();
+        self.kdf.expand(key, &labeled_info, len).await
+    }
+}
+
+/// KemType implementation backed by BoringSSL.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DhKem<DH: DhType, KDF: KdfType> {
+    dh: DH,
+    kdf: KdfWrapper<KDF>,
+    kem_id: KemId,
+    n_secret: usize,
+}
+
+impl<DH: DhType, KDF: KdfType> DhKem<DH, KDF> {
+    /// Creates a new DhKem.
+    pub fn new(cipher_suite: CipherSuite, dh: DH, kdf: KDF) -> Option<Self> {
+        // https://www.rfc-editor.org/rfc/rfc9180.html#section-4.1-5
+        let kem_id = KemId::new(cipher_suite)?;
+        let suite_id = [b"KEM", &(kem_id as u16).to_be_bytes() as &[u8]].concat();
+
+        let kdf = KdfWrapper::new(suite_id, kdf);
+
+        Some(Self { dh, kdf, kem_id, n_secret: kem_id.n_secret() })
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl<DH: DhType, KDF: KdfType> KemType for DhKem<DH, KDF> {
+    type Error = HpkeError;
+
+    fn kem_id(&self) -> u16 {
+        self.kem_id as u16
+    }
+
+    async fn generate(&self) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> {
+        if self.kem_id != KemId::DhKemX25519Sha256 {
+            return Err(HpkeError::UnsupportedCipherSuite);
+        }
+
+        let kem = hpke::Kem::X25519HkdfSha256;
+        let (public_key, private_key) = kem.generate_keypair();
+        Ok((private_key.to_vec().into(), public_key.to_vec().into()))
+    }
+
+    // https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.3-8
+    async fn derive(&self, ikm: &[u8]) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> {
+        let dkp_prk = match self.kdf.labeled_extract(&[], b"dkp_prk", ikm).await {
+            Ok(p) => p,
+            Err(e) => return Err(HpkeError::KdfError(e.into_any_error())),
+        };
+        let sk =
+            match self.kdf.labeled_expand(&dkp_prk, b"sk", &[], self.dh.secret_key_size()).await {
+                Ok(s) => s.into(),
+                Err(e) => return Err(HpkeError::KdfError(e.into_any_error())),
+            };
+        let pk = match self.dh.to_public(&sk).await {
+            Ok(p) => p,
+            Err(e) => return Err(HpkeError::KdfError(e.into_any_error())),
+        };
+        Ok((sk, pk))
+    }
+
+    fn public_key_validate(&self, key: &HpkePublicKey) -> Result<(), Self::Error> {
+        match self.dh.public_key_validate(key) {
+            Ok(_) => Ok(()),
+            Err(e) => Err(HpkeError::DhError(e.into_any_error())),
+        }
+    }
+
+    // Using BoringSSL's HPKE implementation so this is not needed.
+    async fn encap(&self, _remote_pk: &HpkePublicKey) -> Result<KemResult, Self::Error> {
+        unimplemented!();
+    }
+
+    // Using BoringSSL's HPKE implementation so this is not needed.
+    async fn decap(
+        &self,
+        _enc: &[u8],
+        _secret_key: &HpkeSecretKey,
+        _public_key: &HpkePublicKey,
+    ) -> Result<Vec<u8>, Self::Error> {
+        unimplemented!();
+    }
+}
+
+/// HpkeContextS implementation backed by BoringSSL.
+pub struct ContextS(pub Mutex<hpke::SenderContext>);
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl HpkeContextS for ContextS {
+    type Error = HpkeError;
+
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    async fn seal(&mut self, aad: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.0.lock().unwrap().seal(data, aad.unwrap_or_default()))
+    }
+
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    async fn export(&self, exporter_context: &[u8], len: usize) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.0.lock().unwrap().export(exporter_context, len).to_vec())
+    }
+}
+
+/// HpkeContextR implementation backed by BoringSSL.
+pub struct ContextR(pub Mutex<hpke::RecipientContext>);
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl HpkeContextR for ContextR {
+    type Error = HpkeError;
+
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    async fn open(
+        &mut self,
+        aad: Option<&[u8]>,
+        ciphertext: &[u8],
+    ) -> Result<Vec<u8>, Self::Error> {
+        self.0
+            .lock()
+            .unwrap()
+            .open(ciphertext, aad.unwrap_or_default())
+            .ok_or(HpkeError::BoringsslError)
+    }
+
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    async fn export(&self, exporter_context: &[u8], len: usize) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.0.lock().unwrap().export(exporter_context, len).to_vec())
+    }
+}
+
+/// HPKE implementation backed by BoringSSL.
+#[derive(Clone)]
+pub struct Hpke(pub CipherSuite);
+
+impl Hpke {
+    /// Creates a new Hpke.
+    pub fn new(cipher_suite: CipherSuite) -> Self {
+        Self(cipher_suite)
+    }
+
+    /// Sets up HPKE sender context.
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn setup_sender(
+        &self,
+        remote_key: &HpkePublicKey,
+        info: &[u8],
+    ) -> Result<(Vec<u8>, ContextS), HpkeError> {
+        let params = Self::cipher_suite_to_params(self.0)?;
+        match hpke::SenderContext::new(&params, remote_key, info) {
+            Some((ctx, encapsulated_key)) => Ok((encapsulated_key, ContextS(ctx.into()))),
+            None => Err(HpkeError::BoringsslError),
+        }
+    }
+
+    /// Sets up HPKE sender context and encrypts `pt` with optional associated data `aad`.
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn seal(
+        &self,
+        remote_key: &HpkePublicKey,
+        info: &[u8],
+        aad: Option<&[u8]>,
+        pt: &[u8],
+    ) -> Result<HpkeCiphertext, HpkeError> {
+        let (kem_output, mut ctx) = self.setup_sender(remote_key, info).await?;
+        Ok(HpkeCiphertext { kem_output, ciphertext: ctx.seal(aad, pt).await? })
+    }
+
+    /// Sets up HPKE receiver context.
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn setup_receiver(
+        &self,
+        enc: &[u8],
+        local_secret: &HpkeSecretKey,
+        info: &[u8],
+    ) -> Result<ContextR, HpkeError> {
+        let params = Self::cipher_suite_to_params(self.0)?;
+        match hpke::RecipientContext::new(&params, local_secret, enc, info) {
+            Some(ctx) => Ok(ContextR(ctx.into())),
+            None => Err(HpkeError::BoringsslError),
+        }
+    }
+
+    /// Sets up HPKE receiver context and decrypts `ciphertext` with optional associated data `aad`.
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn open(
+        &self,
+        ciphertext: &HpkeCiphertext,
+        local_secret: &HpkeSecretKey,
+        info: &[u8],
+        aad: Option<&[u8]>,
+    ) -> Result<Vec<u8>, HpkeError> {
+        let mut ctx = self.setup_receiver(&ciphertext.kem_output, local_secret, info).await?;
+        ctx.open(aad, &ciphertext.ciphertext).await
+    }
+
+    fn cipher_suite_to_params(cipher_suite: CipherSuite) -> Result<hpke::Params, HpkeError> {
+        match cipher_suite {
+            CipherSuite::CURVE25519_AES128 => Ok(hpke::Params::new(
+                hpke::Kem::X25519HkdfSha256,
+                hpke::Kdf::HkdfSha256,
+                hpke::Aead::Aes128Gcm,
+            )),
+            CipherSuite::CURVE25519_CHACHA => Ok(hpke::Params::new(
+                hpke::Kem::X25519HkdfSha256,
+                hpke::Kdf::HkdfSha256,
+                hpke::Aead::Chacha20Poly1305,
+            )),
+            _ => Err(HpkeError::UnsupportedCipherSuite),
+        }
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::{DhKem, Hpke, KdfWrapper};
+    use crate::ecdh::Ecdh;
+    use crate::kdf::Kdf;
+    use crate::test_helpers::decode_hex;
+    use mls_rs_core::crypto::{
+        CipherSuite, HpkeContextR, HpkeContextS, HpkePublicKey, HpkeSecretKey,
+    };
+    use mls_rs_crypto_traits::{AeadId, KdfId, KemId, KemType};
+    use std::thread;
+
+    // https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1-8
+    fn hpke_suite_id(cipher_suite: CipherSuite) -> Vec<u8> {
+        [
+            b"HPKE",
+            &(KemId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8],
+            &(KdfId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8],
+            &(AeadId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8],
+        ]
+        .concat()
+    }
+
+    #[test]
+    fn kdf_labeled_extract() {
+        let cipher_suite = CipherSuite::CURVE25519_AES128;
+        let suite_id = hpke_suite_id(cipher_suite);
+        let kdf = KdfWrapper::new(suite_id, Kdf::new(cipher_suite).unwrap());
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let shared_secret: [u8; 32] =
+            decode_hex("fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc");
+        let expected_secret: [u8; 32] =
+            decode_hex("12fff91991e93b48de37e7daddb52981084bd8aa64289c3788471d9a9712f397");
+        let label = b"secret";
+
+        let secret = kdf.labeled_extract(&shared_secret, label, &[]).unwrap();
+        assert_eq!(secret, expected_secret);
+    }
+
+    #[test]
+    fn kdf_labeled_expand() {
+        let cipher_suite = CipherSuite::CURVE25519_AES128;
+        let suite_id = hpke_suite_id(cipher_suite);
+        let kdf = KdfWrapper::new(suite_id, Kdf::new(cipher_suite).unwrap());
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let secret: [u8; 32] =
+            decode_hex("12fff91991e93b48de37e7daddb52981084bd8aa64289c3788471d9a9712f397");
+        let key_schedule_ctx : [u8; 65] = decode_hex("00725611c9d98c07c03f60095cd32d400d8347d45ed67097bbad50fc56da742d07cb6cffde367bb0565ba28bb02c90744a20f5ef37f30523526106f637abb05449");
+        let expected_key: [u8; 16] = decode_hex("4531685d41d65f03dc48f6b8302c05b0");
+        let label = b"key";
+
+        let key = kdf.labeled_expand(&secret, label, &key_schedule_ctx, 16).unwrap();
+        assert_eq!(key, expected_key);
+    }
+
+    #[test]
+    fn dh_kem_kem_id() {
+        let cipher_suite = CipherSuite::CURVE25519_CHACHA;
+        let dh = Ecdh::new(cipher_suite).unwrap();
+        let kdf = Kdf::new(cipher_suite).unwrap();
+        let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
+
+        assert_eq!(kem.kem_id(), 32);
+    }
+
+    #[test]
+    fn dh_kem_generate() {
+        let cipher_suite = CipherSuite::CURVE25519_AES128;
+        let dh = Ecdh::new(cipher_suite).unwrap();
+        let kdf = Kdf::new(cipher_suite).unwrap();
+        let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
+
+        assert!(kem.generate().is_ok());
+    }
+
+    #[test]
+    fn dh_kem_derive() {
+        let cipher_suite = CipherSuite::CURVE25519_CHACHA;
+        let dh = Ecdh::new(cipher_suite).unwrap();
+        let kdf = Kdf::new(cipher_suite).unwrap();
+        let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.2.1
+        let ikm: [u8; 32] =
+            decode_hex("909a9b35d3dc4713a5e72a4da274b55d3d3821a37e5d099e74a647db583a904b"); // ikmE
+        let expected_sk = HpkeSecretKey::from(
+            decode_hex::<32>("f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600")
+                .to_vec(),
+        ); // skEm
+        let expected_pk = HpkePublicKey::from(
+            decode_hex::<32>("1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a")
+                .to_vec(),
+        ); // pkEm
+
+        let (sk, pk) = kem.derive(&ikm).unwrap();
+        assert_eq!(sk, expected_sk);
+        assert_eq!(pk, expected_pk);
+    }
+
+    #[test]
+    fn dh_kem_public_key_validate() {
+        let cipher_suite = CipherSuite::CURVE25519_AES128;
+        let dh = Ecdh::new(cipher_suite).unwrap();
+        let kdf = Kdf::new(cipher_suite).unwrap();
+        let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
+
+        // https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1
+        let public_key = HpkePublicKey::from(
+            decode_hex::<32>("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
+                .to_vec(),
+        );
+        assert!(kem.public_key_validate(&public_key).is_ok());
+    }
+
+    #[test]
+    fn hpke_seal_open() {
+        let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let receiver_pub_key = HpkePublicKey::from(
+            decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
+                .to_vec(),
+        );
+        let receiver_priv_key = HpkeSecretKey::from(
+            decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
+                .to_vec(),
+        );
+
+        let info = b"some_info";
+        let plaintext = b"plaintext";
+        let associated_data = b"some_ad";
+
+        let ct = hpke.seal(&receiver_pub_key, info, Some(associated_data), plaintext).unwrap();
+        assert_eq!(
+            plaintext.as_ref(),
+            hpke.open(&ct, &receiver_priv_key, info, Some(associated_data)).unwrap(),
+        );
+    }
+
+    #[test]
+    fn hpke_context_seal_open() {
+        let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let receiver_pub_key = HpkePublicKey::from(
+            decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
+                .to_vec(),
+        );
+        let receiver_priv_key = HpkeSecretKey::from(
+            decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
+                .to_vec(),
+        );
+
+        let info = b"some_info";
+        let plaintext = b"plaintext";
+        let associated_data = b"some_ad";
+
+        let (enc, mut sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap();
+        let mut receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap();
+        let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap();
+        assert_eq!(plaintext.as_ref(), receiver_ctx.open(Some(associated_data), &ct).unwrap(),);
+    }
+
+    #[test]
+    fn hpke_context_seal_open_multithreaded() {
+        let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let receiver_pub_key = HpkePublicKey::from(
+            decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
+                .to_vec(),
+        );
+        let receiver_priv_key = HpkeSecretKey::from(
+            decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
+                .to_vec(),
+        );
+
+        let info = b"some_info";
+        let plaintext = b"plaintext";
+        let associated_data = b"some_ad";
+
+        let (enc, mut sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap();
+        let mut receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap();
+
+        let pool = thread::spawn(move || {
+            for _ in 1..100 {
+                let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap();
+                assert_eq!(
+                    plaintext.as_ref(),
+                    receiver_ctx.open(Some(associated_data), &ct).unwrap(),
+                );
+            }
+        });
+        pool.join().unwrap();
+    }
+
+    #[test]
+    fn hpke_context_export() {
+        let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
+
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let receiver_pub_key = HpkePublicKey::from(
+            decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
+                .to_vec(),
+        );
+        let receiver_priv_key = HpkeSecretKey::from(
+            decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
+                .to_vec(),
+        );
+
+        let info = b"some_info";
+        let exporter_ctx = b"export_ctx";
+
+        let (enc, sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap();
+        let receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap();
+        assert_eq!(
+            sender_ctx.export(exporter_ctx, 32).unwrap(),
+            receiver_ctx.export(exporter_ctx, 32).unwrap(),
+        );
+    }
+
+    #[test]
+    fn hpke_unsupported_cipher_suites() {
+        // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+        let receiver_pub_key = HpkePublicKey::from(
+            decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
+                .to_vec(),
+        );
+
+        for suite in vec![
+            CipherSuite::P256_AES128,
+            CipherSuite::P384_AES256,
+            CipherSuite::P521_AES256,
+            CipherSuite::CURVE448_CHACHA,
+            CipherSuite::CURVE448_AES256,
+        ] {
+            assert!(Hpke::new(suite).setup_sender(&receiver_pub_key, b"some_info").is_err());
+        }
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/kdf.rs b/mls/mls-rs-crypto-boringssl/src/kdf.rs
new file mode 100644
index 0000000..6b88d37
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/kdf.rs
@@ -0,0 +1,250 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Key derivation function.
+
+use bssl_crypto::digest;
+use bssl_crypto::hkdf::{HkdfSha256, HkdfSha512, Prk, Salt};
+use mls_rs_core::crypto::CipherSuite;
+use mls_rs_core::error::IntoAnyError;
+use mls_rs_crypto_traits::{KdfId, KdfType};
+use thiserror::Error;
+
+/// Errors returned from KDF.
+#[derive(Debug, Error)]
+pub enum KdfError {
+    /// Error returned when the input key material (IKM) is too short.
+    #[error("KDF IKM of length {len}, expected length at least {min_len}")]
+    TooShortIkm {
+        /// Invalid IKM length.
+        len: usize,
+        /// Minimum IKM length.
+        min_len: usize,
+    },
+    /// Error returned when the pseudorandom key (PRK) is too short.
+    #[error("KDF PRK of length {len}, expected length at least {min_len}")]
+    TooShortPrk {
+        /// Invalid PRK length.
+        len: usize,
+        /// Minimum PRK length.
+        min_len: usize,
+    },
+    /// Error returned when the output key material (OKM) requested it too long.
+    #[error("KDF OKM of length {len} requested, expected length at most {max_len}")]
+    TooLongOkm {
+        /// Invalid OKM length.
+        len: usize,
+        /// Maximum OKM length.
+        max_len: usize,
+    },
+    /// Error returned when unsupported cipher suite is requested.
+    #[error("unsupported cipher suite")]
+    UnsupportedCipherSuite,
+}
+
+impl IntoAnyError for KdfError {
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+/// KdfType implementation backed by BoringSSL.
+#[derive(Clone)]
+pub struct Kdf(KdfId);
+
+impl Kdf {
+    /// Creates a new Kdf.
+    pub fn new(cipher_suite: CipherSuite) -> Option<Self> {
+        KdfId::new(cipher_suite).map(Self)
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl KdfType for Kdf {
+    type Error = KdfError;
+
+    async fn extract(&self, salt: &[u8], ikm: &[u8]) -> Result<Vec<u8>, KdfError> {
+        if ikm.is_empty() {
+            return Err(KdfError::TooShortIkm { len: 0, min_len: 1 });
+        }
+
+        let salt = if salt.is_empty() { Salt::None } else { Salt::NonEmpty(salt) };
+
+        match self.0 {
+            KdfId::HkdfSha256 => {
+                Ok(HkdfSha256::extract(ikm, salt).as_bytes()[..self.extract_size()].to_vec())
+            }
+            KdfId::HkdfSha512 => {
+                Ok(HkdfSha512::extract(ikm, salt).as_bytes()[..self.extract_size()].to_vec())
+            }
+            _ => Err(KdfError::UnsupportedCipherSuite),
+        }
+    }
+
+    async fn expand(&self, prk: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>, KdfError> {
+        if prk.len() < self.extract_size() {
+            return Err(KdfError::TooShortPrk { len: prk.len(), min_len: self.extract_size() });
+        }
+
+        match self.0 {
+            KdfId::HkdfSha256 => match Prk::new::<digest::Sha256>(prk) {
+                Some(hkdf) => {
+                    let mut out = vec![0; len];
+                    match hkdf.expand_into(info, &mut out) {
+                        Ok(_) => Ok(out),
+                        Err(_) => {
+                            Err(KdfError::TooLongOkm { len, max_len: HkdfSha256::MAX_OUTPUT_LEN })
+                        }
+                    }
+                }
+                None => Err(KdfError::TooShortPrk { len: prk.len(), min_len: self.extract_size() }),
+            },
+            KdfId::HkdfSha512 => match Prk::new::<digest::Sha512>(prk) {
+                Some(hkdf) => {
+                    let mut out = vec![0; len];
+                    match hkdf.expand_into(info, &mut out) {
+                        Ok(_) => Ok(out),
+                        Err(_) => {
+                            Err(KdfError::TooLongOkm { len, max_len: HkdfSha512::MAX_OUTPUT_LEN })
+                        }
+                    }
+                }
+                None => Err(KdfError::TooShortPrk { len: prk.len(), min_len: self.extract_size() }),
+            },
+            _ => Err(KdfError::UnsupportedCipherSuite),
+        }
+    }
+
+    fn extract_size(&self) -> usize {
+        self.0.extract_size()
+    }
+
+    fn kdf_id(&self) -> u16 {
+        self.0 as u16
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::{Kdf, KdfError, KdfType};
+    use crate::test_helpers::decode_hex;
+    use assert_matches::assert_matches;
+    use bssl_crypto::hkdf::{HkdfSha256, HkdfSha512};
+    use mls_rs_core::crypto::CipherSuite;
+
+    #[test]
+    fn sha256() {
+        // https://www.rfc-editor.org/rfc/rfc5869.html#appendix-A.1
+        let salt: [u8; 13] = decode_hex("000102030405060708090a0b0c");
+        let ikm: [u8; 22] = decode_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+        let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9");
+        let expected_prk: [u8; 32] =
+            decode_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5");
+        let expected_okm: [u8; 42] = decode_hex(
+            "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
+        );
+
+        let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap();
+        let prk = kdf.extract(&salt, &ikm).unwrap();
+        assert_eq!(prk, expected_prk);
+        assert_eq!(kdf.expand(&prk, &info, 42).unwrap(), expected_okm);
+    }
+
+    #[test]
+    fn sha512() {
+        // https://github.com/C2SP/wycheproof/blob/cd27d6419bedd83cbd24611ec54b6d4bfdb0cdca/testvectors/hkdf_sha512_test.json#L141
+        let salt: [u8; 16] = decode_hex("1d6f3b38a1e607b5e6bcd4af1800a9d3");
+        let ikm: [u8; 16] = decode_hex("5d3db20e8238a90b62a600fa57fdb318");
+        let info: [u8; 20] = decode_hex("2bc5f39032b6fc87da69ba8711ce735b169646fd");
+        let expected_okm: [u8; 42] = decode_hex(
+            "8c3cf7122dcb5eb7efaf02718f1faf70bca20dcb75070e9d0871a413a6c05fc195a75aa9ffc349d70aae",
+        );
+
+        let kdf = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap();
+        let prk = kdf.extract(&salt, &ikm).unwrap();
+        assert_eq!(kdf.expand(&prk, &info, 42).unwrap(), expected_okm);
+    }
+
+    #[test]
+    fn sha256_extract_short_ikm() {
+        let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(kdf.extract(b"salty", b""), Err(KdfError::TooShortIkm { .. }));
+    }
+
+    #[test]
+    fn sha256_expand_short_prk() {
+        let prk_short: [u8; 16] = decode_hex("077709362c2e32df0ddc3f0dc47bba63");
+        let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9");
+
+        let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(kdf.expand(&prk_short, &info, 42), Err(KdfError::TooShortPrk { .. }));
+    }
+
+    #[test]
+    fn sha256_expand_long_okm() {
+        // https://www.rfc-editor.org/rfc/rfc5869.html#appendix-A.1
+        let prk: [u8; 32] =
+            decode_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5");
+        let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9");
+
+        let kdf = Kdf::new(CipherSuite::CURVE25519_AES128).unwrap();
+        assert_matches!(
+            kdf.expand(&prk, &info, HkdfSha256::MAX_OUTPUT_LEN + 1),
+            Err(KdfError::TooLongOkm { .. })
+        );
+    }
+
+    #[test]
+    fn sha512_extract_short_ikm() {
+        let kdf = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap();
+        assert_matches!(kdf.extract(b"salty", b""), Err(KdfError::TooShortIkm { .. }));
+    }
+
+    #[test]
+    fn sha512_expand_short_prk() {
+        let prk_short: [u8; 16] = decode_hex("077709362c2e32df0ddc3f0dc47bba63");
+        let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9");
+
+        let kdf = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap();
+        assert_matches!(kdf.expand(&prk_short, &info, 42), Err(KdfError::TooShortPrk { .. }));
+    }
+
+    #[test]
+    fn sha512_expand_long_okm() {
+        // https://github.com/C2SP/wycheproof/blob/cd27d6419bedd83cbd24611ec54b6d4bfdb0cdca/testvectors/hkdf_sha512_test.json#L141
+        let salt: [u8; 16] = decode_hex("1d6f3b38a1e607b5e6bcd4af1800a9d3");
+        let ikm: [u8; 16] = decode_hex("5d3db20e8238a90b62a600fa57fdb318");
+        let info: [u8; 20] = decode_hex("2bc5f39032b6fc87da69ba8711ce735b169646fd");
+
+        let kdf_sha512 = Kdf::new(CipherSuite::CURVE448_CHACHA).unwrap();
+        let prk = kdf_sha512.extract(&salt, &ikm).unwrap();
+        assert_matches!(
+            kdf_sha512.expand(&prk, &info, HkdfSha512::MAX_OUTPUT_LEN + 1),
+            Err(KdfError::TooLongOkm { .. })
+        );
+    }
+
+    #[test]
+    fn unsupported_cipher_suites() {
+        let ikm: [u8; 22] = decode_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+        let salt: [u8; 13] = decode_hex("000102030405060708090a0b0c");
+
+        assert_matches!(
+            Kdf::new(CipherSuite::P384_AES256).unwrap().extract(&salt, &ikm),
+            Err(KdfError::UnsupportedCipherSuite)
+        );
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/lib.rs b/mls/mls-rs-crypto-boringssl/src/lib.rs
new file mode 100644
index 0000000..806bd87
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/lib.rs
@@ -0,0 +1,676 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+//! Implements mls_rs_core's CryptoProvider and CipherSuiteProvider backed by BoringSSL.
+
+pub mod aead;
+pub mod ecdh;
+pub mod eddsa;
+pub mod hash;
+pub mod hpke;
+pub mod kdf;
+
+#[cfg(test)]
+mod test_helpers;
+
+use mls_rs_core::crypto::{
+    CipherSuite, CipherSuiteProvider, CryptoProvider, HpkeCiphertext, HpkePublicKey, HpkeSecretKey,
+    SignaturePublicKey, SignatureSecretKey,
+};
+use mls_rs_core::error::{AnyError, IntoAnyError};
+use mls_rs_crypto_traits::{AeadType, KdfType, KemType};
+use thiserror::Error;
+use zeroize::Zeroizing;
+
+use aead::AeadWrapper;
+use ecdh::Ecdh;
+use eddsa::{EdDsa, EdDsaError};
+use hash::{Hash, HashError};
+use hpke::{ContextR, ContextS, DhKem, Hpke, HpkeError};
+use kdf::Kdf;
+
+/// Errors returned from BoringsslCryptoProvider.
+#[derive(Debug, Error)]
+pub enum BoringsslCryptoError {
+    /// Error returned from hash functions and HMACs.
+    #[error(transparent)]
+    HashError(#[from] HashError),
+    /// Error returned from KEMs.
+    #[error(transparent)]
+    KemError(AnyError),
+    /// Error returned from KDFs.
+    #[error(transparent)]
+    KdfError(AnyError),
+    /// Error returned from AEADs.
+    #[error(transparent)]
+    AeadError(AnyError),
+    /// Error returned from HPKE.
+    #[error(transparent)]
+    HpkeError(#[from] HpkeError),
+    /// Error returned from EdDSA.
+    #[error(transparent)]
+    EdDsaError(#[from] EdDsaError),
+}
+
+impl IntoAnyError for BoringsslCryptoError {
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+/// CryptoProvider trait implementation backed by BoringSSL.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct BoringsslCryptoProvider {
+    /// Available cipher suites.
+    pub enabled_cipher_suites: Vec<CipherSuite>,
+}
+
+impl BoringsslCryptoProvider {
+    /// Creates a new BoringsslCryptoProvider.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Sets the enabled cipher suites.
+    pub fn with_enabled_cipher_suites(enabled_cipher_suites: Vec<CipherSuite>) -> Self {
+        Self { enabled_cipher_suites }
+    }
+
+    /// Returns all available cipher suites.
+    pub fn all_supported_cipher_suites() -> Vec<CipherSuite> {
+        vec![CipherSuite::CURVE25519_AES128, CipherSuite::CURVE25519_CHACHA]
+    }
+}
+
+impl Default for BoringsslCryptoProvider {
+    fn default() -> Self {
+        Self { enabled_cipher_suites: Self::all_supported_cipher_suites() }
+    }
+}
+
+impl CryptoProvider for BoringsslCryptoProvider {
+    type CipherSuiteProvider = BoringsslCipherSuite<DhKem<Ecdh, Kdf>, Kdf, AeadWrapper>;
+
+    fn supported_cipher_suites(&self) -> Vec<CipherSuite> {
+        self.enabled_cipher_suites.clone()
+    }
+
+    fn cipher_suite_provider(
+        &self,
+        cipher_suite: CipherSuite,
+    ) -> Option<Self::CipherSuiteProvider> {
+        if !self.enabled_cipher_suites.contains(&cipher_suite) {
+            return None;
+        }
+
+        let ecdh = Ecdh::new(cipher_suite)?;
+        let kdf = Kdf::new(cipher_suite)?;
+        let kem = DhKem::new(cipher_suite, ecdh, kdf.clone())?;
+        let aead = AeadWrapper::new(cipher_suite)?;
+
+        BoringsslCipherSuite::new(cipher_suite, kem, kdf, aead)
+    }
+}
+
+/// CipherSuiteProvider trait implementation backed by BoringSSL.
+#[derive(Clone)]
+pub struct BoringsslCipherSuite<KEM, KDF, AEAD>
+where
+    KEM: KemType + Clone,
+    KDF: KdfType + Clone,
+    AEAD: AeadType + Clone,
+{
+    cipher_suite: CipherSuite,
+    hash: Hash,
+    kem: KEM,
+    kdf: KDF,
+    aead: AEAD,
+    hpke: Hpke,
+    eddsa: EdDsa,
+}
+
+impl<KEM, KDF, AEAD> BoringsslCipherSuite<KEM, KDF, AEAD>
+where
+    KEM: KemType + Clone,
+    KDF: KdfType + Clone,
+    AEAD: AeadType + Clone,
+{
+    /// Creates a new BoringsslCipherSuite.
+    pub fn new(cipher_suite: CipherSuite, kem: KEM, kdf: KDF, aead: AEAD) -> Option<Self> {
+        Some(Self {
+            cipher_suite,
+            hash: Hash::new(cipher_suite).ok()?,
+            kem,
+            kdf,
+            aead,
+            hpke: Hpke::new(cipher_suite),
+            eddsa: EdDsa::new(cipher_suite)?,
+        })
+    }
+
+    /// Returns random bytes generated via BoringSSL.
+    pub fn random_bytes(&self, out: &mut [u8]) -> Result<(), BoringsslCryptoError> {
+        bssl_crypto::rand_bytes(out);
+        Ok(())
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)]
+impl<KEM, KDF, AEAD> CipherSuiteProvider for BoringsslCipherSuite<KEM, KDF, AEAD>
+where
+    KEM: KemType + Clone + Send + Sync,
+    KDF: KdfType + Clone + Send + Sync,
+    AEAD: AeadType + Clone + Send + Sync,
+{
+    type Error = BoringsslCryptoError;
+    type HpkeContextS = ContextS;
+    type HpkeContextR = ContextR;
+
+    fn cipher_suite(&self) -> CipherSuite {
+        self.cipher_suite
+    }
+
+    fn random_bytes(&self, out: &mut [u8]) -> Result<(), Self::Error> {
+        self.random_bytes(out)
+    }
+
+    async fn hash(&self, data: &[u8]) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.hash.hash(data))
+    }
+
+    async fn mac(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.hash.mac(key, data)?)
+    }
+
+    async fn kem_generate(&self) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> {
+        self.kem.generate().await.map_err(|e| BoringsslCryptoError::KemError(e.into_any_error()))
+    }
+
+    async fn kem_derive(&self, ikm: &[u8]) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> {
+        self.kem.derive(ikm).await.map_err(|e| BoringsslCryptoError::KemError(e.into_any_error()))
+    }
+
+    fn kem_public_key_validate(&self, key: &HpkePublicKey) -> Result<(), Self::Error> {
+        self.kem
+            .public_key_validate(key)
+            .map_err(|e| BoringsslCryptoError::KemError(e.into_any_error()))
+    }
+
+    async fn kdf_extract(
+        &self,
+        salt: &[u8],
+        ikm: &[u8],
+    ) -> Result<Zeroizing<Vec<u8>>, Self::Error> {
+        self.kdf
+            .extract(salt, ikm)
+            .await
+            .map_err(|e| BoringsslCryptoError::KdfError(e.into_any_error()))
+            .map(Zeroizing::new)
+    }
+
+    async fn kdf_expand(
+        &self,
+        prk: &[u8],
+        info: &[u8],
+        len: usize,
+    ) -> Result<Zeroizing<Vec<u8>>, Self::Error> {
+        self.kdf
+            .expand(prk, info, len)
+            .await
+            .map_err(|e| BoringsslCryptoError::KdfError(e.into_any_error()))
+            .map(Zeroizing::new)
+    }
+
+    fn kdf_extract_size(&self) -> usize {
+        self.kdf.extract_size()
+    }
+
+    async fn aead_seal(
+        &self,
+        key: &[u8],
+        data: &[u8],
+        aad: Option<&[u8]>,
+        nonce: &[u8],
+    ) -> Result<Vec<u8>, Self::Error> {
+        self.aead
+            .seal(key, data, aad, nonce)
+            .await
+            .map_err(|e| BoringsslCryptoError::AeadError(e.into_any_error()))
+    }
+
+    async fn aead_open(
+        &self,
+        key: &[u8],
+        cipher_text: &[u8],
+        aad: Option<&[u8]>,
+        nonce: &[u8],
+    ) -> Result<Zeroizing<Vec<u8>>, Self::Error> {
+        self.aead
+            .open(key, cipher_text, aad, nonce)
+            .await
+            .map_err(|e| BoringsslCryptoError::AeadError(e.into_any_error()))
+            .map(Zeroizing::new)
+    }
+
+    fn aead_key_size(&self) -> usize {
+        self.aead.key_size()
+    }
+
+    fn aead_nonce_size(&self) -> usize {
+        self.aead.nonce_size()
+    }
+
+    async fn hpke_setup_s(
+        &self,
+        remote_key: &HpkePublicKey,
+        info: &[u8],
+    ) -> Result<(Vec<u8>, Self::HpkeContextS), Self::Error> {
+        Ok(self.hpke.setup_sender(remote_key, info).await?)
+    }
+
+    async fn hpke_seal(
+        &self,
+        remote_key: &HpkePublicKey,
+        info: &[u8],
+        aad: Option<&[u8]>,
+        pt: &[u8],
+    ) -> Result<HpkeCiphertext, Self::Error> {
+        Ok(self.hpke.seal(remote_key, info, aad, pt).await?)
+    }
+
+    async fn hpke_setup_r(
+        &self,
+        enc: &[u8],
+        local_secret: &HpkeSecretKey,
+        // Other implementations use `_local_public` to skip derivation of the public from the
+        // private key for the KEM decapsulation step, but BoringSSL's API does not accept a public
+        // key and instead derives it under the hood.
+        _local_public: &HpkePublicKey,
+        info: &[u8],
+    ) -> Result<Self::HpkeContextR, Self::Error> {
+        Ok(self.hpke.setup_receiver(enc, local_secret, info).await?)
+    }
+
+    async fn hpke_open(
+        &self,
+        ciphertext: &HpkeCiphertext,
+        local_secret: &HpkeSecretKey,
+        // Other implementations use `_local_public` to skip derivation of the public from the
+        // private key for hpke_setup_r()'s KEM decapsulation step, but BoringSSL's API does not
+        // accept a public key and instead derives it under the hood.
+        _local_public: &HpkePublicKey,
+        info: &[u8],
+        aad: Option<&[u8]>,
+    ) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.hpke.open(ciphertext, local_secret, info, aad).await?)
+    }
+
+    async fn signature_key_generate(
+        &self,
+    ) -> Result<(SignatureSecretKey, SignaturePublicKey), Self::Error> {
+        Ok(self.eddsa.signature_key_generate()?)
+    }
+
+    async fn signature_key_derive_public(
+        &self,
+        secret_key: &SignatureSecretKey,
+    ) -> Result<SignaturePublicKey, Self::Error> {
+        Ok(self.eddsa.signature_key_derive_public(secret_key)?)
+    }
+
+    async fn sign(
+        &self,
+        secret_key: &SignatureSecretKey,
+        data: &[u8],
+    ) -> Result<Vec<u8>, Self::Error> {
+        Ok(self.eddsa.sign(secret_key, data)?)
+    }
+
+    async fn verify(
+        &self,
+        public_key: &SignaturePublicKey,
+        signature: &[u8],
+        data: &[u8],
+    ) -> Result<(), Self::Error> {
+        Ok(self.eddsa.verify(public_key, signature, data)?)
+    }
+}
+
+#[cfg(all(not(mls_build_async), test))]
+mod test {
+    use super::BoringsslCryptoProvider;
+    use crate::test_helpers::decode_hex;
+    use mls_rs_core::crypto::{
+        CipherSuite, CipherSuiteProvider, CryptoProvider, HpkeContextR, HpkeContextS,
+        HpkePublicKey, HpkeSecretKey, SignaturePublicKey, SignatureSecretKey,
+    };
+
+    fn get_cipher_suites() -> Vec<CipherSuite> {
+        vec![CipherSuite::CURVE25519_AES128, CipherSuite::CURVE25519_CHACHA]
+    }
+
+    #[test]
+    fn supported_cipher_suites() {
+        let bssl = BoringsslCryptoProvider::new();
+        assert_eq!(bssl.supported_cipher_suites().len(), 2);
+    }
+
+    #[test]
+    fn unsupported_cipher_suites() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in vec![
+            CipherSuite::P256_AES128,
+            CipherSuite::CURVE448_AES256,
+            CipherSuite::P521_AES256,
+            CipherSuite::CURVE448_CHACHA,
+            CipherSuite::P384_AES256,
+        ] {
+            assert!(bssl.cipher_suite_provider(suite).is_none());
+        }
+    }
+
+    #[test]
+    fn cipher_suite() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            assert_eq!(crypto.cipher_suite(), suite);
+        }
+    }
+
+    #[test]
+    fn random_bytes() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            let mut buf = [0; 32];
+            let _ = crypto.random_bytes(&mut buf);
+        }
+    }
+
+    #[test]
+    fn hash() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            assert_eq!(
+                crypto.hash(&decode_hex::<4>("74ba2521")).unwrap(),
+                // bssl_crypto::hmac test vector.
+                decode_hex::<32>(
+                    "b16aa56be3880d18cd41e68384cf1ec8c17680c45a02b1575dc1518923ae8b0e"
+                )
+            );
+        }
+    }
+
+    #[test]
+    fn mac() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // bssl_crypto::hmac test vector.
+            let expected = vec![
+                0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
+                0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
+                0x2e, 0x32, 0xcf, 0xf7,
+            ];
+            let key: [u8; 20] = [0x0b; 20];
+            let data = b"Hi There";
+
+            assert_eq!(crypto.mac(&key, data).unwrap(), expected);
+        }
+    }
+
+    #[test]
+    fn kem_generate() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            assert!(crypto.kem_generate().is_ok());
+        }
+    }
+
+    #[test]
+    fn kem_derive() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+            let ikm: [u8; 32] =
+                decode_hex("7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234");
+            let expected_sk = HpkeSecretKey::from(
+                decode_hex::<32>(
+                    "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736",
+                )
+                .to_vec(),
+            );
+            let expected_pk = HpkePublicKey::from(
+                decode_hex::<32>(
+                    "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431",
+                )
+                .to_vec(),
+            );
+
+            let (sk, pk) = crypto.kem_derive(&ikm).unwrap();
+            assert_eq!(sk, expected_sk);
+            assert_eq!(pk, expected_pk);
+        }
+    }
+
+    #[test]
+    fn kem_public_key_validate() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1
+            let public_key = HpkePublicKey::from(
+                decode_hex::<32>(
+                    "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a",
+                )
+                .to_vec(),
+            );
+            assert!(crypto.kem_public_key_validate(&public_key).is_ok());
+        }
+    }
+
+    #[test]
+    fn kdf_extract_and_expand() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // https://www.rfc-editor.org/rfc/rfc5869.html#appendix-A.1
+            let ikm: [u8; 22] = decode_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+            let salt: [u8; 13] = decode_hex("000102030405060708090a0b0c");
+            let info: [u8; 10] = decode_hex("f0f1f2f3f4f5f6f7f8f9");
+            let expected_prk: [u8; 32] =
+                decode_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5");
+            let expected_okm : [u8; 42] = decode_hex(
+                    "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
+                );
+
+            let prk = crypto.kdf_extract(&salt, &ikm).unwrap();
+            assert_eq!(prk.as_ref(), expected_prk);
+            assert_eq!(crypto.kdf_expand(&prk.as_ref(), &info, 42).unwrap().as_ref(), expected_okm);
+        }
+    }
+
+    #[test]
+    fn kdf_extract_size() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            assert_eq!(crypto.kdf_extract_size(), 32);
+        }
+    }
+
+    #[test]
+    fn aead() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            let key = vec![42u8; crypto.aead_key_size()];
+            let associated_data = vec![42u8, 12];
+            let nonce = vec![42u8; crypto.aead_nonce_size()];
+            let plaintext = b"message";
+
+            let ciphertext =
+                crypto.aead_seal(&key, plaintext, Some(&associated_data), &nonce).unwrap();
+            assert_eq!(
+                plaintext,
+                crypto
+                    .aead_open(&key, ciphertext.as_slice(), Some(&associated_data), &nonce)
+                    .unwrap()
+                    .as_slice()
+            );
+        }
+    }
+
+    #[test]
+    fn hpke_setup_seal_open_export() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+            let receiver_pub_key = HpkePublicKey::from(
+                decode_hex::<32>(
+                    "3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d",
+                )
+                .to_vec(),
+            );
+            let receiver_priv_key = HpkeSecretKey::from(
+                decode_hex::<32>(
+                    "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8",
+                )
+                .to_vec(),
+            );
+
+            let info = b"some_info";
+            let plaintext = b"plaintext";
+            let associated_data = b"some_ad";
+            let exporter_ctx = b"export_ctx";
+
+            let (enc, mut sender_ctx) = crypto.hpke_setup_s(&receiver_pub_key, info).unwrap();
+            let mut receiver_ctx =
+                crypto.hpke_setup_r(&enc, &receiver_priv_key, &receiver_pub_key, info).unwrap();
+            let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap();
+            assert_eq!(plaintext.as_ref(), receiver_ctx.open(Some(associated_data), &ct).unwrap(),);
+            assert_eq!(
+                sender_ctx.export(exporter_ctx, 32).unwrap(),
+                receiver_ctx.export(exporter_ctx, 32).unwrap(),
+            );
+        }
+    }
+
+    #[test]
+    fn hpke_seal_open() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
+            let receiver_pub_key = HpkePublicKey::from(
+                decode_hex::<32>(
+                    "3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d",
+                )
+                .to_vec(),
+            );
+            let receiver_priv_key = HpkeSecretKey::from(
+                decode_hex::<32>(
+                    "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8",
+                )
+                .to_vec(),
+            );
+
+            let info = b"some_info";
+            let plaintext = b"plaintext";
+            let associated_data = b"some_ad";
+
+            let ct = crypto
+                .hpke_seal(&receiver_pub_key, info, Some(associated_data), plaintext)
+                .unwrap();
+            assert_eq!(
+                plaintext.as_ref(),
+                crypto
+                    .hpke_open(
+                        &ct,
+                        &receiver_priv_key,
+                        &receiver_pub_key,
+                        info,
+                        Some(associated_data)
+                    )
+                    .unwrap(),
+            );
+        }
+    }
+
+    #[test]
+    fn signature_key_generate() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            assert!(crypto.signature_key_generate().is_ok());
+        }
+    }
+
+    #[test]
+    fn signature_key_derive_public() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // Test 1 from https://www.rfc-editor.org/rfc/rfc8032#section-7.1
+            let private_key = SignatureSecretKey::from(
+                decode_hex::<32>(
+                    "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
+                )
+                .to_vec(),
+            );
+            let expected_public_key = SignaturePublicKey::from(
+                decode_hex::<32>(
+                    "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
+                )
+                .to_vec(),
+            );
+
+            assert_eq!(
+                crypto.signature_key_derive_public(&private_key).unwrap(),
+                expected_public_key
+            );
+        }
+    }
+
+    #[test]
+    fn sign_verify() {
+        let bssl = BoringsslCryptoProvider::new();
+        for suite in get_cipher_suites() {
+            let crypto = bssl.cipher_suite_provider(suite).unwrap();
+            // Test 3 from https://www.rfc-editor.org/rfc/rfc8032#section-7.1
+            let private_key = SignatureSecretKey::from(
+                decode_hex::<32>(
+                    "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
+                )
+                .to_vec(),
+            );
+            let data: [u8; 2] = decode_hex("af82");
+            let expected_sig = decode_hex::<64>("6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a").to_vec();
+
+            let sig = crypto.sign(&private_key, &data).unwrap();
+            assert_eq!(sig, expected_sig);
+
+            let public_key = crypto.signature_key_derive_public(&private_key).unwrap();
+            assert!(crypto.verify(&public_key, &sig, &data).is_ok());
+        }
+    }
+}
diff --git a/mls/mls-rs-crypto-boringssl/src/test_helpers.rs b/mls/mls-rs-crypto-boringssl/src/test_helpers.rs
new file mode 100644
index 0000000..0b07fac
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/test_helpers.rs
@@ -0,0 +1,23 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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
+//
+//     http://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.
+
+pub(crate) fn decode_hex<const N: usize>(s: &str) -> [u8; N] {
+    (0..s.len())
+        .step_by(2)
+        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("Invalid hex string"))
+        .collect::<Vec<u8>>()
+        .as_slice()
+        .try_into()
+        .unwrap()
+}