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}