use offline_protocol_core::Address;
use offline_protocol_sealed::{encode_identity_assertion, parse_identity_assertion};
use crate::error::{MlsError, Result};
use crate::manager::MlsManager;
pub fn verify_identity_assertion(bytes: &[u8]) -> Result<Address> {
use ed25519_dalek::{Signature, VerifyingKey};
let parts = parse_identity_assertion(bytes)?;
let key = VerifyingKey::try_from(parts.public_key)
.map_err(|e| MlsError::InvalidPublicKey(format!("Invalid Ed25519 public key: {e}")))?;
let signature = Signature::try_from(parts.signature)
.map_err(|e| MlsError::VerificationFailed(format!("Invalid signature format: {e}")))?;
key.verify_strict(parts.signed_data, &signature)
.map_err(|_| {
MlsError::VerificationFailed(
"Identity assertion signature does not verify under its public key".to_string(),
)
})?;
MlsManager::derive_address(parts.public_key)
}
impl MlsManager {
pub fn identity_assertion(&self, signed_data: &[u8]) -> Result<Vec<u8>> {
let public_key = self.get_identity_public_key()?;
let signature = self.sign_data(signed_data)?;
encode_identity_assertion(&public_key, &signature, signed_data).map_err(MlsError::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::InMemoryStorage;
use ed25519_dalek::{Signer, SigningKey};
use std::sync::Arc;
fn assertion_from(seed: u8, signed_data: &[u8]) -> (Vec<u8>, Address) {
let signing = SigningKey::from_bytes(&[seed; 32]);
let public = signing.verifying_key().to_bytes();
let signature = signing.sign(signed_data).to_bytes();
let address = MlsManager::derive_address(&public).expect("derive");
(
encode_identity_assertion(&public, &signature, signed_data).expect("encode"),
address,
)
}
#[test]
fn a_valid_assertion_yields_the_address_its_key_derives_to() {
let (bytes, address) = assertion_from(1, b"mesh advertisement");
assert_eq!(
verify_identity_assertion(&bytes).expect("verifies"),
address
);
}
#[test]
fn an_empty_signed_data_verifies_at_the_floor() {
let (bytes, address) = assertion_from(2, b"");
assert_eq!(bytes.len(), 96);
assert_eq!(
verify_identity_assertion(&bytes).expect("verifies"),
address
);
}
#[test]
fn a_short_assertion_is_refused_before_any_cryptography() {
for len in [0usize, 32, 95] {
let err = verify_identity_assertion(&vec![0u8; len]).unwrap_err();
assert!(matches!(err, MlsError::Deserialization(_)), "{len}: {err}");
}
}
#[test]
fn a_signature_by_another_key_is_refused() {
let (mut bytes, _) = assertion_from(3, b"data");
let foreign = SigningKey::from_bytes(&[4u8; 32]);
bytes[32..96].copy_from_slice(&foreign.sign(b"data").to_bytes());
assert!(matches!(
verify_identity_assertion(&bytes),
Err(MlsError::VerificationFailed(_))
));
}
#[test]
fn a_signature_over_other_data_is_refused() {
let (mut bytes, _) = assertion_from(5, b"data");
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
assert!(matches!(
verify_identity_assertion(&bytes),
Err(MlsError::VerificationFailed(_))
));
}
#[test]
fn every_flipped_bit_of_the_signature_is_refused() {
let (bytes, _) = assertion_from(6, b"");
for byte in 32..96 {
let mut tampered = bytes.clone();
tampered[byte] ^= 0x80;
assert!(
verify_identity_assertion(&tampered).is_err(),
"flipping signature byte {byte} must not verify"
);
}
}
#[test]
fn a_key_that_is_not_on_the_curve_is_refused() {
let mut bytes = vec![0xffu8; 96];
bytes[32..].fill(0);
assert!(verify_identity_assertion(&bytes).is_err());
}
#[test]
fn a_small_order_key_with_a_trivial_signature_is_refused() {
let mut identity_point = [0u8; 32];
identity_point[0] = 1;
let mut signature = [0u8; 64];
signature[..32].copy_from_slice(&identity_point);
let data = b"any message at all";
assert!(
MlsManager::verify_signature(&identity_point, data, &signature).expect("parses"),
"the permissive check accepts this; if it stops, this test no longer \
proves the verifier is strict"
);
let bytes = encode_identity_assertion(&identity_point, &signature, data).expect("encode");
assert!(matches!(
verify_identity_assertion(&bytes),
Err(MlsError::VerificationFailed(_))
));
}
#[test]
fn a_manager_builds_an_assertion_the_verifier_accepts() {
let storage: Arc<dyn crate::MlsStorage> = Arc::new(InMemoryStorage::new());
let manager = MlsManager::new("device", storage).expect("manager");
let own = manager.get_identity_public_key().expect("key");
let own_address = MlsManager::derive_address(&own).expect("derive");
for signed_data in [&b""[..], b"advertisement"] {
let bytes = manager.identity_assertion(signed_data).expect("assertion");
assert_eq!(bytes.len(), 96 + signed_data.len());
assert_eq!(&bytes[96..], signed_data);
assert_eq!(
verify_identity_assertion(&bytes).expect("verifies"),
own_address
);
}
}
#[test]
fn the_conformance_assertions_verify_to_their_pinned_addresses() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../offline-protocol-sealed/tests/data/identity-assertion-v1.vectors.json");
let Ok(text) = std::fs::read_to_string(&path) else {
eprintln!("sealed vector file not present, skipping");
return;
};
let v: serde_json::Value = serde_json::from_str(&text).expect("JSON");
let unhex = |s: &str| -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex"))
.collect()
};
let cases = v["assertions"].as_array().expect("assertions");
assert_eq!(cases.len(), 3);
for case in cases {
let name = case["name"].as_str().expect("name");
let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
let got = verify_identity_assertion(&bytes).unwrap_or_else(|e| panic!("[{name}]: {e}"));
assert_eq!(
got.to_string(),
case["address"].as_str().expect("address"),
"[{name}]"
);
}
for case in v["refusals"].as_array().expect("refusals") {
let name = case["name"].as_str().expect("name");
let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
assert!(
verify_identity_assertion(&bytes).is_err(),
"[{name}] must be refused"
);
}
}
}