From 6ec3c2b64f743ced8d9391d8518eb13f5d3ebda3 Mon Sep 17 00:00:00 2001 From: Cindy Lin Date: Thu, 16 May 2024 07:39:23 +0000 Subject: [PATCH] Implement mls-rs-crypto-traits backed by BoringSSL. Fix: 302021139 Test: Presubmit Change-Id: Iaefa21d3fb69f92d735875778f3f96e1878d0876 --- mls/mls-rs-crypto-boringssl/Android.bp | 55 ++ mls/mls-rs-crypto-boringssl/LICENSE-apache | 176 +++++ mls/mls-rs-crypto-boringssl/OWNERS | 2 + mls/mls-rs-crypto-boringssl/src/aead.rs | 334 +++++++++ mls/mls-rs-crypto-boringssl/src/ecdh.rs | 280 ++++++++ mls/mls-rs-crypto-boringssl/src/eddsa.rs | 284 ++++++++ mls/mls-rs-crypto-boringssl/src/hash.rs | 152 ++++ mls/mls-rs-crypto-boringssl/src/hpke.rs | 541 ++++++++++++++ mls/mls-rs-crypto-boringssl/src/kdf.rs | 250 +++++++ mls/mls-rs-crypto-boringssl/src/lib.rs | 676 ++++++++++++++++++ .../src/test_helpers.rs | 23 + 11 files changed, 2773 insertions(+) create mode 100644 mls/mls-rs-crypto-boringssl/Android.bp create mode 100644 mls/mls-rs-crypto-boringssl/LICENSE-apache create mode 100644 mls/mls-rs-crypto-boringssl/OWNERS create mode 100644 mls/mls-rs-crypto-boringssl/src/aead.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/ecdh.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/eddsa.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/hash.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/hpke.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/kdf.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/lib.rs create mode 100644 mls/mls-rs-crypto-boringssl/src/test_helpers.rs diff --git a/mls/mls-rs-crypto-boringssl/Android.bp b/mls/mls-rs-crypto-boringssl/Android.bp new file mode 100644 index 00000000..b3636407 --- /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 00000000..831fbc53 --- /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 00000000..f9092f6d --- /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 00000000..eaa33a92 --- /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, 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 { + 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, 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, 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 { + [ + 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 00000000..74ba8df7 --- /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, 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 { + 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, 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 { + 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 { + 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 00000000..473b756d --- /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 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 { + 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 { + 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, 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 00000000..397fb9d4 --- /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 { + 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 { + 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, 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 00000000..4bb4aa23 --- /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, Self> { + Ok(self.into()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct KdfWrapper { + suite_id: Vec, + kdf: KDF, +} + +impl KdfWrapper { + pub fn new(suite_id: Vec, 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, ::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, ::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: DH, + kdf: KdfWrapper, + kem_id: KemId, + n_secret: usize, +} + +impl DhKem { + /// Creates a new DhKem. + pub fn new(cipher_suite: CipherSuite, dh: DH, kdf: KDF) -> Option { + // 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 KemType for DhKem { + 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 { + unimplemented!(); + } + + // Using BoringSSL's HPKE implementation so this is not needed. + async fn decap( + &self, + _enc: &[u8], + _secret_key: &HpkeSecretKey, + _public_key: &HpkePublicKey, + ) -> Result, Self::Error> { + unimplemented!(); + } +} + +/// HpkeContextS implementation backed by BoringSSL. +pub struct ContextS(pub Mutex); + +#[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, 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, Self::Error> { + Ok(self.0.lock().unwrap().export(exporter_context, len).to_vec()) + } +} + +/// HpkeContextR implementation backed by BoringSSL. +pub struct ContextR(pub Mutex); + +#[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, 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, 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, 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 { + 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 { + 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, 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 { + 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 { + [ + 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 00000000..6b88d374 --- /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, 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 { + 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, 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, 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::(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::(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 00000000..806bd877 --- /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, 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, +} + +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) -> Self { + Self { enabled_cipher_suites } + } + + /// Returns all available cipher suites. + pub fn all_supported_cipher_suites() -> Vec { + 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, Kdf, AeadWrapper>; + + fn supported_cipher_suites(&self) -> Vec { + self.enabled_cipher_suites.clone() + } + + fn cipher_suite_provider( + &self, + cipher_suite: CipherSuite, + ) -> Option { + 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 +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 BoringsslCipherSuite +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 { + 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 CipherSuiteProvider for BoringsslCipherSuite +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, Self::Error> { + Ok(self.hash.hash(data)) + } + + async fn mac(&self, key: &[u8], data: &[u8]) -> Result, 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>, 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>, 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, 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>, 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, 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 { + 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 { + 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, 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 { + Ok(self.eddsa.signature_key_derive_public(secret_key)?) + } + + async fn sign( + &self, + secret_key: &SignatureSecretKey, + data: &[u8], + ) -> Result, 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 { + 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 00000000..0b07fac3 --- /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(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::>() + .as_slice() + .try_into() + .unwrap() +}