Implement mls-rs-crypto-traits backed by BoringSSL.
Fix: 302021139
Test: Presubmit
Change-Id: Iaefa21d3fb69f92d735875778f3f96e1878d0876
diff --git a/mls/mls-rs-crypto-boringssl/Android.bp b/mls/mls-rs-crypto-boringssl/Android.bp
new file mode 100644
index 0000000..b363640
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/Android.bp
@@ -0,0 +1,55 @@
+// 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.
+
+package {
+ default_applicable_licenses: ["platform_system_security_mls_rs_crypto_boringssl_license"],
+}
+
+// See: http://go/android-license-faq
+license {
+ name: "platform_system_security_mls_rs_crypto_boringssl_license",
+ visibility: [":__subpackages__"],
+ license_kinds: [
+ "SPDX-license-identifier-Apache-2.0",
+ ],
+ license_text: [
+ "LICENSE-apache",
+ ],
+}
+
+rust_library {
+ name: "libmls_rs_crypto_boringssl",
+ host_supported: true,
+ crate_name: "mls_rs_crypto_boringssl",
+ srcs: ["src/lib.rs"],
+ cfgs: ["mls_build_async"],
+ rustlibs: [
+ "libbssl_crypto",
+ "libmls_rs_codec",
+ "libmls_rs_core",
+ "libmls_rs_crypto_traits",
+ "libthiserror",
+ "libzeroize",
+ ],
+ proc_macros: [
+ "libasync_trait",
+ "libmaybe_async",
+ ],
+ apex_available: [
+ "//apex_available:anyapex",
+ "//apex_available:platform",
+ ],
+ product_available: true,
+ vendor_available: true,
+}
diff --git a/mls/mls-rs-crypto-boringssl/LICENSE-apache b/mls/mls-rs-crypto-boringssl/LICENSE-apache
new file mode 100644
index 0000000..831fbc5
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/LICENSE-apache
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, orother modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/mls/mls-rs-crypto-boringssl/OWNERS b/mls/mls-rs-crypto-boringssl/OWNERS
new file mode 100644
index 0000000..f9092f6
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/OWNERS
@@ -0,0 +1,2 @@
+cinlin@google.com
+guillaumee@google.com
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(¶ms, 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(¶ms, 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()
+}