Merge "Implement mls-rs-crypto-traits backed by BoringSSL." into main
This commit is contained in:
commit
a8fc566120
11 changed files with 2773 additions and 0 deletions
55
mls/mls-rs-crypto-boringssl/Android.bp
Normal file
55
mls/mls-rs-crypto-boringssl/Android.bp
Normal file
|
@ -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,
|
||||||
|
}
|
176
mls/mls-rs-crypto-boringssl/LICENSE-apache
Normal file
176
mls/mls-rs-crypto-boringssl/LICENSE-apache
Normal file
|
@ -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
|
2
mls/mls-rs-crypto-boringssl/OWNERS
Normal file
2
mls/mls-rs-crypto-boringssl/OWNERS
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
cinlin@google.com
|
||||||
|
guillaumee@google.com
|
334
mls/mls-rs-crypto-boringssl/src/aead.rs
Normal file
334
mls/mls-rs-crypto-boringssl/src/aead.rs
Normal file
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
280
mls/mls-rs-crypto-boringssl/src/ecdh.rs
Normal file
280
mls/mls-rs-crypto-boringssl/src/ecdh.rs
Normal file
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
284
mls/mls-rs-crypto-boringssl/src/eddsa.rs
Normal file
284
mls/mls-rs-crypto-boringssl/src/eddsa.rs
Normal file
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
152
mls/mls-rs-crypto-boringssl/src/hash.rs
Normal file
152
mls/mls-rs-crypto-boringssl/src/hash.rs
Normal file
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
541
mls/mls-rs-crypto-boringssl/src/hpke.rs
Normal file
541
mls/mls-rs-crypto-boringssl/src/hpke.rs
Normal file
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
250
mls/mls-rs-crypto-boringssl/src/kdf.rs
Normal file
250
mls/mls-rs-crypto-boringssl/src/kdf.rs
Normal file
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
676
mls/mls-rs-crypto-boringssl/src/lib.rs
Normal file
676
mls/mls-rs-crypto-boringssl/src/lib.rs
Normal file
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
mls/mls-rs-crypto-boringssl/src/test_helpers.rs
Normal file
23
mls/mls-rs-crypto-boringssl/src/test_helpers.rs
Normal file
|
@ -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()
|
||||||
|
}
|
Loading…
Reference in a new issue