Skip to main content

offline_protocol_mls/
identity_assertion.rs

1//! Verifying the identity assertion a peer proves its address with.
2//!
3//! The layout is `public_key(32) || signature(64) || signed_data`, and it has
4//! one home, [`offline_protocol_sealed::identity_assertion`]. This module is
5//! the one verifier of it: it parses, checks the signature under the key, and
6//! derives the address from the key, in that order, and returns the derived
7//! address or an error. It is what the `verify_identity_assertion` FFI
8//! function calls, so a Bluetooth LE central on any platform and a peer-stream
9//! transport on any host run the same four lines against the same bytes.
10//!
11//! # What verification proves, and what it does not
12//!
13//! A returned address means the peer that served these bytes holds, or once
14//! held, the private key that address derives from. It does not mean the peer
15//! is live or recent: the assertion is static and replayable, as
16//! [BLE framing](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/spec/ble-framing.md#the-identity-assertion)
17//! states, and a device in range can serve a value it copied. What it cannot
18//! do is sign the frames that address must sign, which is where authenticity
19//! is established. A caller MUST NOT treat a verified assertion as proof of
20//! liveness or of sole ownership.
21//!
22//! # The fourth step is the caller's
23//!
24//! The chapter's verification has four steps. The first three are here. The
25//! fourth, comparing the derived address to the one the peer claimed over
26//! some other channel (the Device id characteristic, a DNS-SD TXT record),
27//! belongs to whoever read that channel, because this function never sees it.
28//! The comparison is exact, and the announced address is always the derived
29//! one, never the claimed string: the two are equal whenever verification
30//! succeeds, so the derived value costs nothing, and it means no code path
31//! announces a name that arrived unauthenticated.
32
33use offline_protocol_core::Address;
34use offline_protocol_sealed::{encode_identity_assertion, parse_identity_assertion};
35
36use crate::error::{MlsError, Result};
37use crate::manager::MlsManager;
38
39/// Verifies an identity assertion and returns the address it proves.
40///
41/// In order, cheap before expensive:
42///
43/// 1. the bytes are at least 96 long and split into key, signature and data;
44/// 2. the key parses as an Ed25519 verifying key;
45/// 3. the signature verifies over `signed_data` under it;
46/// 4. the address is derived from the key by the SDK's one derivation.
47///
48/// `signed_data` is not interpreted. Nothing domain-separates it, so any
49/// signature the key ever produced over any message verifies here; that is
50/// harmless only while the bytes mean nothing, and the chapter says so.
51///
52/// # Errors
53///
54/// [`MlsError::Deserialization`] for anything under 96 bytes,
55/// [`MlsError::InvalidPublicKey`] for a key that is not on the curve, and
56/// [`MlsError::VerificationFailed`] for a signature that does not verify
57/// under RFC 8032's strict rules, which also refuse a small-order key and a
58/// non-canonical signature.
59/// Every case means the peer is not surfaced at all, never surfaced with a
60/// caveat: what this returns becomes `Message.recipient` on every outbound
61/// frame and the transport-verified peer id the receiving core matches
62/// `Message.sender` against.
63pub fn verify_identity_assertion(bytes: &[u8]) -> Result<Address> {
64    use ed25519_dalek::{Signature, VerifyingKey};
65
66    let parts = parse_identity_assertion(bytes)?;
67    let key = VerifyingKey::try_from(parts.public_key)
68        .map_err(|e| MlsError::InvalidPublicKey(format!("Invalid Ed25519 public key: {e}")))?;
69    let signature = Signature::try_from(parts.signature)
70        .map_err(|e| MlsError::VerificationFailed(format!("Invalid signature format: {e}")))?;
71    // Strict, not `MlsManager::verify_signature`: the permissive check accepts
72    // a small-order key with a signature anyone can write down, over any
73    // message. That forges nothing a peer could not get by minting a key, but
74    // this is the one verifier for every link that proves an address, and an
75    // assertion no private key ever produced has no business verifying.
76    key.verify_strict(parts.signed_data, &signature)
77        .map_err(|_| {
78            MlsError::VerificationFailed(
79                "Identity assertion signature does not verify under its public key".to_string(),
80            )
81        })?;
82    MlsManager::derive_address(parts.public_key)
83}
84
85impl MlsManager {
86    /// Builds this identity's assertion over `signed_data`.
87    ///
88    /// The result is what a Bluetooth LE peripheral serves from its Identity
89    /// characteristic and what a peer-stream transport sends first. The key
90    /// never leaves the manager: the caller supplies the bytes to sign and
91    /// receives the whole layout, so no bridge assembles it.
92    ///
93    /// `signed_data` means nothing to a verifier and may be empty. The mobile
94    /// peripherals put their mesh advertisement there; a peripheral with
95    /// nothing to say should pass nothing.
96    ///
97    /// # Errors
98    ///
99    /// Whatever [`MlsManager::sign_data`] returns when there is no identity to
100    /// sign with.
101    pub fn identity_assertion(&self, signed_data: &[u8]) -> Result<Vec<u8>> {
102        let public_key = self.get_identity_public_key()?;
103        let signature = self.sign_data(signed_data)?;
104        encode_identity_assertion(&public_key, &signature, signed_data).map_err(MlsError::from)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::storage::InMemoryStorage;
112    use ed25519_dalek::{Signer, SigningKey};
113    use std::sync::Arc;
114
115    fn assertion_from(seed: u8, signed_data: &[u8]) -> (Vec<u8>, Address) {
116        let signing = SigningKey::from_bytes(&[seed; 32]);
117        let public = signing.verifying_key().to_bytes();
118        let signature = signing.sign(signed_data).to_bytes();
119        let address = MlsManager::derive_address(&public).expect("derive");
120        (
121            encode_identity_assertion(&public, &signature, signed_data).expect("encode"),
122            address,
123        )
124    }
125
126    #[test]
127    fn a_valid_assertion_yields_the_address_its_key_derives_to() {
128        let (bytes, address) = assertion_from(1, b"mesh advertisement");
129        assert_eq!(
130            verify_identity_assertion(&bytes).expect("verifies"),
131            address
132        );
133    }
134
135    #[test]
136    fn an_empty_signed_data_verifies_at_the_floor() {
137        let (bytes, address) = assertion_from(2, b"");
138        assert_eq!(bytes.len(), 96);
139        assert_eq!(
140            verify_identity_assertion(&bytes).expect("verifies"),
141            address
142        );
143    }
144
145    #[test]
146    fn a_short_assertion_is_refused_before_any_cryptography() {
147        for len in [0usize, 32, 95] {
148            let err = verify_identity_assertion(&vec![0u8; len]).unwrap_err();
149            assert!(matches!(err, MlsError::Deserialization(_)), "{len}: {err}");
150        }
151    }
152
153    /// The attack the signature exists for: a public key anyone can copy,
154    /// paired with a signature it did not make.
155    #[test]
156    fn a_signature_by_another_key_is_refused() {
157        let (mut bytes, _) = assertion_from(3, b"data");
158        let foreign = SigningKey::from_bytes(&[4u8; 32]);
159        bytes[32..96].copy_from_slice(&foreign.sign(b"data").to_bytes());
160        assert!(matches!(
161            verify_identity_assertion(&bytes),
162            Err(MlsError::VerificationFailed(_))
163        ));
164    }
165
166    #[test]
167    fn a_signature_over_other_data_is_refused() {
168        let (mut bytes, _) = assertion_from(5, b"data");
169        // Same key, same signature, one byte of the covered data changed.
170        let last = bytes.len() - 1;
171        bytes[last] ^= 0x01;
172        assert!(matches!(
173            verify_identity_assertion(&bytes),
174            Err(MlsError::VerificationFailed(_))
175        ));
176    }
177
178    #[test]
179    fn every_flipped_bit_of_the_signature_is_refused() {
180        let (bytes, _) = assertion_from(6, b"");
181        for byte in 32..96 {
182            let mut tampered = bytes.clone();
183            tampered[byte] ^= 0x80;
184            assert!(
185                verify_identity_assertion(&tampered).is_err(),
186                "flipping signature byte {byte} must not verify"
187            );
188        }
189    }
190
191    /// Which step refuses it is the signature library's business (one
192    /// version rejects the point at parse, another at verify), so this pins
193    /// the refusal and not the variant.
194    #[test]
195    fn a_key_that_is_not_on_the_curve_is_refused() {
196        // All-0xff is not a valid compressed Edwards point.
197        let mut bytes = vec![0xffu8; 96];
198        bytes[32..].fill(0);
199        assert!(verify_identity_assertion(&bytes).is_err());
200    }
201
202    /// The forgery the permissive check admits: the identity point as the
203    /// key, the identity point as `R` and a zero scalar verify over any
204    /// message, with no private key behind them. The permissive
205    /// `verify_signature` is asserted to accept it, so this test fails if the
206    /// verifier is ever routed back through it.
207    #[test]
208    fn a_small_order_key_with_a_trivial_signature_is_refused() {
209        let mut identity_point = [0u8; 32];
210        identity_point[0] = 1;
211        let mut signature = [0u8; 64];
212        signature[..32].copy_from_slice(&identity_point);
213        let data = b"any message at all";
214
215        assert!(
216            MlsManager::verify_signature(&identity_point, data, &signature).expect("parses"),
217            "the permissive check accepts this; if it stops, this test no longer \
218             proves the verifier is strict"
219        );
220        let bytes = encode_identity_assertion(&identity_point, &signature, data).expect("encode");
221        assert!(matches!(
222            verify_identity_assertion(&bytes),
223            Err(MlsError::VerificationFailed(_))
224        ));
225    }
226
227    /// The instance side: what a manager builds is what the verifier
228    /// accepts, and it names the manager's own address.
229    #[test]
230    fn a_manager_builds_an_assertion_the_verifier_accepts() {
231        let storage: Arc<dyn crate::MlsStorage> = Arc::new(InMemoryStorage::new());
232        let manager = MlsManager::new("device", storage).expect("manager");
233        let own = manager.get_identity_public_key().expect("key");
234        let own_address = MlsManager::derive_address(&own).expect("derive");
235
236        for signed_data in [&b""[..], b"advertisement"] {
237            let bytes = manager.identity_assertion(signed_data).expect("assertion");
238            assert_eq!(bytes.len(), 96 + signed_data.len());
239            assert_eq!(&bytes[96..], signed_data);
240            assert_eq!(
241                verify_identity_assertion(&bytes).expect("verifies"),
242                own_address
243            );
244        }
245    }
246
247    /// The published vectors, end to end: the signatures in the file were
248    /// pasted from RFC 8032, so a verifier that accepts them and returns the
249    /// pinned address has its whole pipeline checked against a source outside
250    /// this repository.
251    #[test]
252    fn the_conformance_assertions_verify_to_their_pinned_addresses() {
253        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
254            .join("../offline-protocol-sealed/tests/data/identity-assertion-v1.vectors.json");
255        // Absent in a published .crate archive, present in the repo.
256        let Ok(text) = std::fs::read_to_string(&path) else {
257            eprintln!("sealed vector file not present, skipping");
258            return;
259        };
260        let v: serde_json::Value = serde_json::from_str(&text).expect("JSON");
261        let unhex = |s: &str| -> Vec<u8> {
262            (0..s.len())
263                .step_by(2)
264                .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex"))
265                .collect()
266        };
267        let cases = v["assertions"].as_array().expect("assertions");
268        assert_eq!(cases.len(), 3);
269        for case in cases {
270            let name = case["name"].as_str().expect("name");
271            let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
272            let got = verify_identity_assertion(&bytes).unwrap_or_else(|e| panic!("[{name}]: {e}"));
273            assert_eq!(
274                got.to_string(),
275                case["address"].as_str().expect("address"),
276                "[{name}]"
277            );
278        }
279        for case in v["refusals"].as_array().expect("refusals") {
280            let name = case["name"].as_str().expect("name");
281            let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
282            assert!(
283                verify_identity_assertion(&bytes).is_err(),
284                "[{name}] must be refused"
285            );
286        }
287    }
288}