Merge "Implement mls-rs-crypto-traits backed by BoringSSL." into main am: a8fc566120 am: ab1d17e8d2

Original change: https://android-review.googlesource.com/c/platform/system/security/+/3089793

Change-Id: I5509007155819dc315b09941cc385ed60daa74b3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Cindy Lin 2024-05-30 10:06:17 +00:00 committed by Automerger Merge Worker
commit 6942187dbd
11 changed files with 2773 additions and 0 deletions

View 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,
}

View 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

View file

@ -0,0 +1,2 @@
cinlin@google.com
guillaumee@google.com

View 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(),
);
}
}
}

View 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 DiffieHellman.
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)
);
}
}
}

View 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)
);
}
}
}

View 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());
}
}

View 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(&params, remote_key, info) {
Some((ctx, encapsulated_key)) => Ok((encapsulated_key, ContextS(ctx.into()))),
None => Err(HpkeError::BoringsslError),
}
}
/// Sets up HPKE sender context and encrypts `pt` with optional associated data `aad`.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn seal(
&self,
remote_key: &HpkePublicKey,
info: &[u8],
aad: Option<&[u8]>,
pt: &[u8],
) -> Result<HpkeCiphertext, HpkeError> {
let (kem_output, mut ctx) = self.setup_sender(remote_key, info).await?;
Ok(HpkeCiphertext { kem_output, ciphertext: ctx.seal(aad, pt).await? })
}
/// Sets up HPKE receiver context.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn setup_receiver(
&self,
enc: &[u8],
local_secret: &HpkeSecretKey,
info: &[u8],
) -> Result<ContextR, HpkeError> {
let params = Self::cipher_suite_to_params(self.0)?;
match hpke::RecipientContext::new(&params, local_secret, enc, info) {
Some(ctx) => Ok(ContextR(ctx.into())),
None => Err(HpkeError::BoringsslError),
}
}
/// Sets up HPKE receiver context and decrypts `ciphertext` with optional associated data `aad`.
#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
pub async fn open(
&self,
ciphertext: &HpkeCiphertext,
local_secret: &HpkeSecretKey,
info: &[u8],
aad: Option<&[u8]>,
) -> Result<Vec<u8>, HpkeError> {
let mut ctx = self.setup_receiver(&ciphertext.kem_output, local_secret, info).await?;
ctx.open(aad, &ciphertext.ciphertext).await
}
fn cipher_suite_to_params(cipher_suite: CipherSuite) -> Result<hpke::Params, HpkeError> {
match cipher_suite {
CipherSuite::CURVE25519_AES128 => Ok(hpke::Params::new(
hpke::Kem::X25519HkdfSha256,
hpke::Kdf::HkdfSha256,
hpke::Aead::Aes128Gcm,
)),
CipherSuite::CURVE25519_CHACHA => Ok(hpke::Params::new(
hpke::Kem::X25519HkdfSha256,
hpke::Kdf::HkdfSha256,
hpke::Aead::Chacha20Poly1305,
)),
_ => Err(HpkeError::UnsupportedCipherSuite),
}
}
}
#[cfg(all(not(mls_build_async), test))]
mod test {
use super::{DhKem, Hpke, KdfWrapper};
use crate::ecdh::Ecdh;
use crate::kdf::Kdf;
use crate::test_helpers::decode_hex;
use mls_rs_core::crypto::{
CipherSuite, HpkeContextR, HpkeContextS, HpkePublicKey, HpkeSecretKey,
};
use mls_rs_crypto_traits::{AeadId, KdfId, KemId, KemType};
use std::thread;
// https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1-8
fn hpke_suite_id(cipher_suite: CipherSuite) -> Vec<u8> {
[
b"HPKE",
&(KemId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8],
&(KdfId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8],
&(AeadId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8],
]
.concat()
}
#[test]
fn kdf_labeled_extract() {
let cipher_suite = CipherSuite::CURVE25519_AES128;
let suite_id = hpke_suite_id(cipher_suite);
let kdf = KdfWrapper::new(suite_id, Kdf::new(cipher_suite).unwrap());
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let shared_secret: [u8; 32] =
decode_hex("fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc");
let expected_secret: [u8; 32] =
decode_hex("12fff91991e93b48de37e7daddb52981084bd8aa64289c3788471d9a9712f397");
let label = b"secret";
let secret = kdf.labeled_extract(&shared_secret, label, &[]).unwrap();
assert_eq!(secret, expected_secret);
}
#[test]
fn kdf_labeled_expand() {
let cipher_suite = CipherSuite::CURVE25519_AES128;
let suite_id = hpke_suite_id(cipher_suite);
let kdf = KdfWrapper::new(suite_id, Kdf::new(cipher_suite).unwrap());
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let secret: [u8; 32] =
decode_hex("12fff91991e93b48de37e7daddb52981084bd8aa64289c3788471d9a9712f397");
let key_schedule_ctx : [u8; 65] = decode_hex("00725611c9d98c07c03f60095cd32d400d8347d45ed67097bbad50fc56da742d07cb6cffde367bb0565ba28bb02c90744a20f5ef37f30523526106f637abb05449");
let expected_key: [u8; 16] = decode_hex("4531685d41d65f03dc48f6b8302c05b0");
let label = b"key";
let key = kdf.labeled_expand(&secret, label, &key_schedule_ctx, 16).unwrap();
assert_eq!(key, expected_key);
}
#[test]
fn dh_kem_kem_id() {
let cipher_suite = CipherSuite::CURVE25519_CHACHA;
let dh = Ecdh::new(cipher_suite).unwrap();
let kdf = Kdf::new(cipher_suite).unwrap();
let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
assert_eq!(kem.kem_id(), 32);
}
#[test]
fn dh_kem_generate() {
let cipher_suite = CipherSuite::CURVE25519_AES128;
let dh = Ecdh::new(cipher_suite).unwrap();
let kdf = Kdf::new(cipher_suite).unwrap();
let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
assert!(kem.generate().is_ok());
}
#[test]
fn dh_kem_derive() {
let cipher_suite = CipherSuite::CURVE25519_CHACHA;
let dh = Ecdh::new(cipher_suite).unwrap();
let kdf = Kdf::new(cipher_suite).unwrap();
let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.2.1
let ikm: [u8; 32] =
decode_hex("909a9b35d3dc4713a5e72a4da274b55d3d3821a37e5d099e74a647db583a904b"); // ikmE
let expected_sk = HpkeSecretKey::from(
decode_hex::<32>("f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600")
.to_vec(),
); // skEm
let expected_pk = HpkePublicKey::from(
decode_hex::<32>("1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a")
.to_vec(),
); // pkEm
let (sk, pk) = kem.derive(&ikm).unwrap();
assert_eq!(sk, expected_sk);
assert_eq!(pk, expected_pk);
}
#[test]
fn dh_kem_public_key_validate() {
let cipher_suite = CipherSuite::CURVE25519_AES128;
let dh = Ecdh::new(cipher_suite).unwrap();
let kdf = Kdf::new(cipher_suite).unwrap();
let kem = DhKem::new(cipher_suite, dh, kdf).unwrap();
// https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1
let public_key = HpkePublicKey::from(
decode_hex::<32>("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
.to_vec(),
);
assert!(kem.public_key_validate(&public_key).is_ok());
}
#[test]
fn hpke_seal_open() {
let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let receiver_pub_key = HpkePublicKey::from(
decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
.to_vec(),
);
let receiver_priv_key = HpkeSecretKey::from(
decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
.to_vec(),
);
let info = b"some_info";
let plaintext = b"plaintext";
let associated_data = b"some_ad";
let ct = hpke.seal(&receiver_pub_key, info, Some(associated_data), plaintext).unwrap();
assert_eq!(
plaintext.as_ref(),
hpke.open(&ct, &receiver_priv_key, info, Some(associated_data)).unwrap(),
);
}
#[test]
fn hpke_context_seal_open() {
let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let receiver_pub_key = HpkePublicKey::from(
decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
.to_vec(),
);
let receiver_priv_key = HpkeSecretKey::from(
decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
.to_vec(),
);
let info = b"some_info";
let plaintext = b"plaintext";
let associated_data = b"some_ad";
let (enc, mut sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap();
let mut receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap();
let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap();
assert_eq!(plaintext.as_ref(), receiver_ctx.open(Some(associated_data), &ct).unwrap(),);
}
#[test]
fn hpke_context_seal_open_multithreaded() {
let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let receiver_pub_key = HpkePublicKey::from(
decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
.to_vec(),
);
let receiver_priv_key = HpkeSecretKey::from(
decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
.to_vec(),
);
let info = b"some_info";
let plaintext = b"plaintext";
let associated_data = b"some_ad";
let (enc, mut sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap();
let mut receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap();
let pool = thread::spawn(move || {
for _ in 1..100 {
let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap();
assert_eq!(
plaintext.as_ref(),
receiver_ctx.open(Some(associated_data), &ct).unwrap(),
);
}
});
pool.join().unwrap();
}
#[test]
fn hpke_context_export() {
let hpke = Hpke::new(CipherSuite::CURVE25519_AES128);
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let receiver_pub_key = HpkePublicKey::from(
decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
.to_vec(),
);
let receiver_priv_key = HpkeSecretKey::from(
decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8")
.to_vec(),
);
let info = b"some_info";
let exporter_ctx = b"export_ctx";
let (enc, sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap();
let receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap();
assert_eq!(
sender_ctx.export(exporter_ctx, 32).unwrap(),
receiver_ctx.export(exporter_ctx, 32).unwrap(),
);
}
#[test]
fn hpke_unsupported_cipher_suites() {
// https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1
let receiver_pub_key = HpkePublicKey::from(
decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d")
.to_vec(),
);
for suite in vec![
CipherSuite::P256_AES128,
CipherSuite::P384_AES256,
CipherSuite::P521_AES256,
CipherSuite::CURVE448_CHACHA,
CipherSuite::CURVE448_AES256,
] {
assert!(Hpke::new(suite).setup_sender(&receiver_pub_key, b"some_info").is_err());
}
}
}

View 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)
);
}
}

View 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());
}
}
}

View 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()
}