| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| // Copyright by contributors to this project. |
| // SPDX-License-Identifier: (Apache-2.0 OR MIT) |
| |
| use alloc::vec::Vec; |
| use core::fmt::{self, Debug}; |
| use mls_rs_codec::{MlsEncode, MlsSize}; |
| use mls_rs_core::error::IntoAnyError; |
| |
| use crate::client::MlsError; |
| use crate::crypto::{CipherSuiteProvider, SignaturePublicKey, SignatureSecretKey}; |
| |
| #[derive(Clone, MlsSize, MlsEncode)] |
| struct SignContent { |
| #[mls_codec(with = "mls_rs_codec::byte_vec")] |
| label: Vec<u8>, |
| #[mls_codec(with = "mls_rs_codec::byte_vec")] |
| content: Vec<u8>, |
| } |
| |
| impl Debug for SignContent { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.debug_struct("SignContent") |
| .field("label", &mls_rs_core::debug::pretty_bytes(&self.label)) |
| .field("content", &mls_rs_core::debug::pretty_bytes(&self.content)) |
| .finish() |
| } |
| } |
| |
| impl SignContent { |
| pub fn new(label: &str, content: Vec<u8>) -> Self { |
| Self { |
| label: [b"MLS 1.0 ", label.as_bytes()].concat(), |
| content, |
| } |
| } |
| } |
| |
| #[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 |
| )] |
| pub(crate) trait Signable<'a> { |
| const SIGN_LABEL: &'static str; |
| |
| type SigningContext: Send + Sync; |
| |
| fn signature(&self) -> &[u8]; |
| |
| fn signable_content( |
| &self, |
| context: &Self::SigningContext, |
| ) -> Result<Vec<u8>, mls_rs_codec::Error>; |
| |
| fn write_signature(&mut self, signature: Vec<u8>); |
| |
| async fn sign<P: CipherSuiteProvider>( |
| &mut self, |
| signature_provider: &P, |
| signer: &SignatureSecretKey, |
| context: &Self::SigningContext, |
| ) -> Result<(), MlsError> { |
| let sign_content = SignContent::new(Self::SIGN_LABEL, self.signable_content(context)?); |
| |
| let signature = signature_provider |
| .sign(signer, &sign_content.mls_encode_to_vec()?) |
| .await |
| .map_err(|e| MlsError::CryptoProviderError(e.into_any_error()))?; |
| |
| self.write_signature(signature); |
| |
| Ok(()) |
| } |
| |
| async fn verify<P: CipherSuiteProvider>( |
| &self, |
| signature_provider: &P, |
| public_key: &SignaturePublicKey, |
| context: &Self::SigningContext, |
| ) -> Result<(), MlsError> { |
| let sign_content = SignContent::new(Self::SIGN_LABEL, self.signable_content(context)?); |
| |
| signature_provider |
| .verify( |
| public_key, |
| self.signature(), |
| &sign_content.mls_encode_to_vec()?, |
| ) |
| .await |
| .map_err(|_| MlsError::InvalidSignature) |
| } |
| } |
| |
| #[cfg(test)] |
| pub(crate) mod test_utils { |
| use alloc::vec; |
| use alloc::{string::String, vec::Vec}; |
| use mls_rs_core::crypto::CipherSuiteProvider; |
| |
| use crate::crypto::test_utils::try_test_cipher_suite_provider; |
| |
| use super::Signable; |
| |
| #[derive(Debug, serde::Serialize, serde::Deserialize)] |
| pub struct SignatureInteropTestCase { |
| #[serde(with = "hex::serde", rename = "priv")] |
| secret: Vec<u8>, |
| #[serde(with = "hex::serde", rename = "pub")] |
| public: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| content: Vec<u8>, |
| label: String, |
| #[serde(with = "hex::serde")] |
| signature: Vec<u8>, |
| } |
| |
| #[derive(Debug, serde::Serialize, serde::Deserialize)] |
| pub struct InteropTestCase { |
| cipher_suite: u16, |
| sign_with_label: SignatureInteropTestCase, |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_basic_crypto_test_vectors() { |
| let test_cases: Vec<InteropTestCase> = |
| load_test_case_json!(basic_crypto, Vec::<InteropTestCase>::new()); |
| |
| for test_case in test_cases { |
| if let Some(cs) = try_test_cipher_suite_provider(test_case.cipher_suite) { |
| test_case.sign_with_label.verify(&cs).await; |
| } |
| } |
| } |
| |
| pub struct TestSignable { |
| pub content: Vec<u8>, |
| pub signature: Vec<u8>, |
| } |
| |
| impl<'a> Signable<'a> for TestSignable { |
| const SIGN_LABEL: &'static str = "SignWithLabel"; |
| |
| type SigningContext = Vec<u8>; |
| |
| fn signature(&self) -> &[u8] { |
| &self.signature |
| } |
| |
| fn signable_content( |
| &self, |
| context: &Self::SigningContext, |
| ) -> Result<Vec<u8>, mls_rs_codec::Error> { |
| Ok([context.as_slice(), self.content.as_slice()].concat()) |
| } |
| |
| fn write_signature(&mut self, signature: Vec<u8>) { |
| self.signature = signature |
| } |
| } |
| |
| impl SignatureInteropTestCase { |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| pub async fn verify<P: CipherSuiteProvider>(&self, cs: &P) { |
| let public = self.public.clone().into(); |
| |
| let signable = TestSignable { |
| content: self.content.clone(), |
| signature: self.signature.clone(), |
| }; |
| |
| signable.verify(cs, &public, &vec![]).await.unwrap(); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::{test_utils::TestSignable, *}; |
| use crate::{ |
| client::test_utils::TEST_CIPHER_SUITE, |
| crypto::test_utils::{ |
| test_cipher_suite_provider, try_test_cipher_suite_provider, TestCryptoProvider, |
| }, |
| group::test_utils::random_bytes, |
| }; |
| use alloc::vec; |
| use assert_matches::assert_matches; |
| |
| #[derive(Debug, serde::Serialize, serde::Deserialize)] |
| struct TestCase { |
| cipher_suite: u16, |
| #[serde(with = "hex::serde")] |
| content: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| context: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| signature: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| signer: Vec<u8>, |
| #[serde(with = "hex::serde")] |
| public: Vec<u8>, |
| } |
| |
| #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] |
| #[cfg_attr(coverage_nightly, coverage(off))] |
| async fn generate_test_cases() -> Vec<TestCase> { |
| let mut test_cases = Vec::new(); |
| |
| for cipher_suite in TestCryptoProvider::all_supported_cipher_suites() { |
| let provider = test_cipher_suite_provider(cipher_suite); |
| |
| let (signer, public) = provider.signature_key_generate().await.unwrap(); |
| |
| let content = random_bytes(32); |
| let context = random_bytes(32); |
| |
| let mut test_signable = TestSignable { |
| content: content.clone(), |
| signature: Vec::new(), |
| }; |
| |
| test_signable |
| .sign(&provider, &signer, &context) |
| .await |
| .unwrap(); |
| |
| test_cases.push(TestCase { |
| cipher_suite: cipher_suite.into(), |
| content, |
| context, |
| signature: test_signable.signature, |
| signer: signer.to_vec(), |
| public: public.to_vec(), |
| }); |
| } |
| |
| test_cases |
| } |
| |
| #[cfg(mls_build_async)] |
| async fn load_test_cases() -> Vec<TestCase> { |
| load_test_case_json!(signatures, generate_test_cases().await) |
| } |
| |
| #[cfg(not(mls_build_async))] |
| fn load_test_cases() -> Vec<TestCase> { |
| load_test_case_json!(signatures, generate_test_cases()) |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_signatures() { |
| let cases = load_test_cases().await; |
| |
| for one_case in cases { |
| let Some(cipher_suite_provider) = try_test_cipher_suite_provider(one_case.cipher_suite) |
| else { |
| continue; |
| }; |
| |
| let public_key = SignaturePublicKey::from(one_case.public); |
| |
| // Wasm uses incompatible signature secret key format |
| #[cfg(not(target_arch = "wasm32"))] |
| { |
| // Test signature generation |
| let mut test_signable = TestSignable { |
| content: one_case.content.clone(), |
| signature: Vec::new(), |
| }; |
| |
| let signature_key = SignatureSecretKey::from(one_case.signer); |
| |
| test_signable |
| .sign(&cipher_suite_provider, &signature_key, &one_case.context) |
| .await |
| .unwrap(); |
| |
| test_signable |
| .verify(&cipher_suite_provider, &public_key, &one_case.context) |
| .await |
| .unwrap(); |
| } |
| |
| // Test verifying an existing signature |
| let test_signable = TestSignable { |
| content: one_case.content, |
| signature: one_case.signature, |
| }; |
| |
| test_signable |
| .verify(&cipher_suite_provider, &public_key, &one_case.context) |
| .await |
| .unwrap(); |
| } |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_invalid_signature() { |
| let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| |
| let (correct_secret, _) = cipher_suite_provider |
| .signature_key_generate() |
| .await |
| .unwrap(); |
| let (_, incorrect_public) = cipher_suite_provider |
| .signature_key_generate() |
| .await |
| .unwrap(); |
| |
| let mut test_signable = TestSignable { |
| content: random_bytes(32), |
| signature: vec![], |
| }; |
| |
| test_signable |
| .sign(&cipher_suite_provider, &correct_secret, &vec![]) |
| .await |
| .unwrap(); |
| |
| let res = test_signable |
| .verify(&cipher_suite_provider, &incorrect_public, &vec![]) |
| .await; |
| |
| assert_matches!(res, Err(MlsError::InvalidSignature)); |
| } |
| |
| #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] |
| async fn test_invalid_context() { |
| let cipher_suite_provider = test_cipher_suite_provider(TEST_CIPHER_SUITE); |
| |
| let (secret, public) = cipher_suite_provider |
| .signature_key_generate() |
| .await |
| .unwrap(); |
| |
| let correct_context = random_bytes(32); |
| let incorrect_context = random_bytes(32); |
| |
| let mut test_signable = TestSignable { |
| content: random_bytes(32), |
| signature: vec![], |
| }; |
| |
| test_signable |
| .sign(&cipher_suite_provider, &secret, &correct_context) |
| .await |
| .unwrap(); |
| |
| let res = test_signable |
| .verify(&cipher_suite_provider, &public, &incorrect_context) |
| .await; |
| |
| assert_matches!(res, Err(MlsError::InvalidSignature)); |
| } |
| } |