feat: verify KeyPackages published by a device #1
@@ -0,0 +1,21 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: rustup toolchain install
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo fmt --all --check
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-deny
|
||||
- run: cargo llvm-cov --fail-under-lines 80 --ignore-filename-regex 'main\.rs$'
|
||||
- run: cargo deny check advisories
|
||||
Generated
+1996
File diff suppressed because it is too large
Load Diff
+14
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "whispr-mls"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
openmls = "0.9"
|
||||
openmls_rust_crypto = "0.6"
|
||||
thiserror = "2.0.21"
|
||||
|
||||
[dev-dependencies]
|
||||
openmls_basic_credential = "0.6"
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.98.1"
|
||||
components = ["clippy", "rustfmt", "llvm-tools-preview"]
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
//! MLS for Whispr, shared by the mobile app and the servers (ADR 0007 of `whispr-architecture`).
|
||||
|
||||
use openmls::prelude::{tls_codec::Deserialize, *};
|
||||
use openmls_rust_crypto::OpenMlsRustCrypto;
|
||||
|
||||
/// The only ciphersuite Whispr accepts.
|
||||
pub const CIPHERSUITE: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
|
||||
|
||||
/// What the Key directory keeps from a KeyPackage it accepted.
|
||||
#[derive(Debug)]
|
||||
pub struct VerifiedKeyPackage {
|
||||
pub key_package_ref: Vec<u8>,
|
||||
pub signature_key: Vec<u8>,
|
||||
pub last_resort: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KeyPackageError {
|
||||
#[error("not a valid KeyPackage")]
|
||||
Invalid,
|
||||
#[error("the KeyPackage credential names another Device")]
|
||||
ForeignCredential,
|
||||
}
|
||||
|
||||
/// Checks a KeyPackage published by `device_id` (invariant K4).
|
||||
pub fn verify_key_package(
|
||||
bytes: &[u8],
|
||||
device_id: &[u8],
|
||||
) -> Result<VerifiedKeyPackage, KeyPackageError> {
|
||||
let provider = OpenMlsRustCrypto::default();
|
||||
let key_package = KeyPackageIn::tls_deserialize_exact(bytes)
|
||||
.map_err(|_| KeyPackageError::Invalid)?
|
||||
.validate(provider.crypto(), ProtocolVersion::Mls10)
|
||||
.map_err(|_| KeyPackageError::Invalid)?;
|
||||
if key_package.ciphersuite() != CIPHERSUITE {
|
||||
return Err(KeyPackageError::Invalid);
|
||||
}
|
||||
let credential = BasicCredential::try_from(key_package.leaf_node().credential().clone())
|
||||
.map_err(|_| KeyPackageError::Invalid)?;
|
||||
if credential.identity() != device_id {
|
||||
return Err(KeyPackageError::ForeignCredential);
|
||||
}
|
||||
Ok(VerifiedKeyPackage {
|
||||
key_package_ref: key_package
|
||||
.hash_ref(provider.crypto())
|
||||
.map_err(|_| KeyPackageError::Invalid)?
|
||||
.as_slice()
|
||||
.to_vec(),
|
||||
signature_key: key_package.leaf_node().signature_key().as_slice().to_vec(),
|
||||
last_resort: key_package.last_resort(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use openmls::prelude::tls_codec::Serialize;
|
||||
use openmls_basic_credential::SignatureKeyPair;
|
||||
use openmls_rust_crypto::OpenMlsRustCrypto;
|
||||
|
||||
/// Serialized KeyPackage whose credential names `device_id`, as a Device would publish it.
|
||||
fn key_package(device_id: &[u8], last_resort: bool) -> (Vec<u8>, Vec<u8>) {
|
||||
key_package_with(CIPHERSUITE, device_id, last_resort)
|
||||
}
|
||||
|
||||
fn key_package_with(
|
||||
ciphersuite: Ciphersuite,
|
||||
device_id: &[u8],
|
||||
last_resort: bool,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let provider = OpenMlsRustCrypto::default();
|
||||
let signer = SignatureKeyPair::new(ciphersuite.signature_algorithm()).unwrap();
|
||||
let credential = CredentialWithKey {
|
||||
credential: BasicCredential::new(device_id.to_vec()).into(),
|
||||
signature_key: signer.to_public_vec().into(),
|
||||
};
|
||||
let mut builder = KeyPackage::builder().leaf_node_capabilities(Capabilities::new(
|
||||
None,
|
||||
None,
|
||||
Some(&[ExtensionType::LastResort]),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
if last_resort {
|
||||
builder = builder.mark_as_last_resort();
|
||||
}
|
||||
let bytes = builder
|
||||
.build(ciphersuite, &provider, &signer, credential)
|
||||
.unwrap()
|
||||
.key_package()
|
||||
.tls_serialize_detached()
|
||||
.unwrap();
|
||||
(bytes, signer.to_public_vec())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_a_key_package_of_its_device() {
|
||||
let (bytes, signature_key) = key_package(b"device-1", false);
|
||||
|
||||
let verified = verify_key_package(&bytes, b"device-1").unwrap();
|
||||
|
||||
assert_eq!(verified.signature_key, signature_key);
|
||||
assert!(!verified.last_resort);
|
||||
assert!(!verified.key_package_ref.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn k4_rejects_a_key_package_naming_another_device() {
|
||||
let (bytes, _) = key_package(b"device-2", false);
|
||||
|
||||
let error = verify_key_package(&bytes, b"device-1").unwrap_err();
|
||||
|
||||
assert!(matches!(error, KeyPackageError::ForeignCredential));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognises_a_last_resort_key_package() {
|
||||
let (bytes, _) = key_package(b"device-1", true);
|
||||
|
||||
assert!(verify_key_package(&bytes, b"device-1").unwrap().last_resort);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_tampered_key_package() {
|
||||
let (mut bytes, _) = key_package(b"device-1", false);
|
||||
*bytes.last_mut().unwrap() ^= 1;
|
||||
|
||||
let error = verify_key_package(&bytes, b"device-1").unwrap_err();
|
||||
|
||||
assert!(matches!(error, KeyPackageError::Invalid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_another_ciphersuite() {
|
||||
let (bytes, _) = key_package_with(
|
||||
Ciphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
|
||||
b"device-1",
|
||||
false,
|
||||
);
|
||||
|
||||
let error = verify_key_package(&bytes, b"device-1").unwrap_err();
|
||||
|
||||
assert!(matches!(error, KeyPackageError::Invalid));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user