offline_protocol_sealed/
identity_assertion.rs1use crate::{Result, SealedError};
37use alloc::format;
38use alloc::vec::Vec;
39
40use crate::derive::ED25519_PUBLIC_KEY_LEN;
41
42pub const ED25519_SIGNATURE_LEN: usize = 64;
44
45pub const IDENTITY_ASSERTION_MIN_LEN: usize = ED25519_PUBLIC_KEY_LEN + ED25519_SIGNATURE_LEN;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct IdentityAssertion<'a> {
57 pub public_key: &'a [u8],
59 pub signature: &'a [u8],
61 pub signed_data: &'a [u8],
63}
64
65pub fn encode_identity_assertion(
74 public_key: &[u8],
75 signature: &[u8],
76 signed_data: &[u8],
77) -> Result<Vec<u8>> {
78 if public_key.len() != ED25519_PUBLIC_KEY_LEN {
79 return Err(SealedError::InvalidPublicKey(format!(
80 "Ed25519 public key must be {} bytes, got {}",
81 ED25519_PUBLIC_KEY_LEN,
82 public_key.len()
83 )));
84 }
85 if signature.len() != ED25519_SIGNATURE_LEN {
86 return Err(SealedError::Serialization(format!(
87 "Identity assertion signature must be {} bytes, got {}",
88 ED25519_SIGNATURE_LEN,
89 signature.len()
90 )));
91 }
92
93 let mut out = Vec::with_capacity(IDENTITY_ASSERTION_MIN_LEN + signed_data.len());
94 out.extend_from_slice(public_key);
95 out.extend_from_slice(signature);
96 out.extend_from_slice(signed_data);
97 Ok(out)
98}
99
100pub fn parse_identity_assertion(bytes: &[u8]) -> Result<IdentityAssertion<'_>> {
110 if bytes.len() < IDENTITY_ASSERTION_MIN_LEN {
111 return Err(SealedError::Deserialization(format!(
112 "Identity assertion is {} bytes, minimum is {}",
113 bytes.len(),
114 IDENTITY_ASSERTION_MIN_LEN
115 )));
116 }
117 let (public_key, rest) = bytes.split_at(ED25519_PUBLIC_KEY_LEN);
118 let (signature, signed_data) = rest.split_at(ED25519_SIGNATURE_LEN);
119 Ok(IdentityAssertion {
120 public_key,
121 signature,
122 signed_data,
123 })
124}
125
126#[cfg(all(test, feature = "std"))]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn encode_then_parse_returns_the_fields() {
132 let key = [1u8; 32];
133 let sig = [2u8; 64];
134 let data = b"whatever follows";
135 let bytes = encode_identity_assertion(&key, &sig, data).expect("encodes");
136 assert_eq!(bytes.len(), 96 + data.len());
137 let parsed = parse_identity_assertion(&bytes).expect("parses");
138 assert_eq!(parsed.public_key, &key);
139 assert_eq!(parsed.signature, &sig);
140 assert_eq!(parsed.signed_data, data);
141 }
142
143 #[test]
144 fn an_empty_signed_data_is_the_floor_and_is_accepted() {
145 let bytes = encode_identity_assertion(&[0u8; 32], &[0u8; 64], &[]).expect("encodes");
146 assert_eq!(bytes.len(), IDENTITY_ASSERTION_MIN_LEN);
147 let parsed = parse_identity_assertion(&bytes).expect("96 bytes is a whole assertion");
148 assert!(parsed.signed_data.is_empty());
149 }
150
151 #[test]
152 fn encode_refuses_wrong_field_sizes() {
153 for key_len in [0usize, 31, 33] {
154 let err = encode_identity_assertion(&vec![0u8; key_len], &[0u8; 64], &[]).unwrap_err();
155 assert!(
156 matches!(err, SealedError::InvalidPublicKey(_)),
157 "{key_len}: {err}"
158 );
159 }
160 for sig_len in [0usize, 63, 65] {
161 let err = encode_identity_assertion(&[0u8; 32], &vec![0u8; sig_len], &[]).unwrap_err();
162 assert!(
163 matches!(err, SealedError::Serialization(_)),
164 "{sig_len}: {err}"
165 );
166 }
167 }
168
169 #[test]
170 fn parse_refuses_everything_under_the_floor() {
171 for len in [0usize, 1, 32, 64, 95] {
172 let err = parse_identity_assertion(&vec![0u8; len]).unwrap_err();
173 assert!(
174 matches!(err, SealedError::Deserialization(_)),
175 "{len}: {err}"
176 );
177 }
178 }
179}
180
181#[cfg(all(test, feature = "std"))]
189mod spec_vectors {
190 use super::*;
191
192 const VECTORS: &str = include_str!("../tests/data/identity-assertion-v1.vectors.json");
193
194 fn vectors() -> serde_json::Value {
195 serde_json::from_str(VECTORS).expect("the vector file is JSON")
196 }
197
198 fn unhex(s: &str) -> Vec<u8> {
199 (0..s.len())
200 .step_by(2)
201 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex digit"))
202 .collect()
203 }
204
205 #[test]
208 fn the_vector_file_is_the_size_it_was() {
209 let v = vectors();
210 assert_eq!(v["assertions"].as_array().expect("assertions").len(), 3);
211 assert_eq!(v["refusals"].as_array().expect("refusals").len(), 3);
212 assert_eq!(v["minimum_length"], IDENTITY_ASSERTION_MIN_LEN);
213 assert!(
214 v["assertions"]
215 .as_array()
216 .expect("assertions")
217 .iter()
218 .any(|a| a["signed_data_hex"] == ""),
219 "one assertion must sit exactly on the floor, or the file cannot catch \
220 a reader that refuses 96 bytes"
221 );
222 }
223
224 #[test]
225 fn every_assertion_parses_into_the_fields_it_was_built_from() {
226 for case in vectors()["assertions"].as_array().expect("assertions") {
227 let name = case["name"].as_str().expect("a name");
228 let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
229 let parsed =
230 parse_identity_assertion(&bytes).unwrap_or_else(|e| panic!("[{name}]: {e}"));
231 assert_eq!(
232 parsed.public_key,
233 unhex(case["public_key_hex"].as_str().expect("key")),
234 "[{name}] public key"
235 );
236 assert_eq!(
237 parsed.signature,
238 unhex(case["signature_hex"].as_str().expect("signature")),
239 "[{name}] signature"
240 );
241 assert_eq!(
242 parsed.signed_data,
243 unhex(case["signed_data_hex"].as_str().expect("signed data")),
244 "[{name}] signed data"
245 );
246 assert_eq!(
249 crate::derive_address(parsed.public_key)
250 .expect("32-byte key")
251 .to_string(),
252 case["address"].as_str().expect("address"),
253 "[{name}] the address the chapter's derivation gives this key"
254 );
255 }
256 }
257
258 #[test]
259 fn every_assertion_re_encodes_to_the_same_bytes() {
260 for case in vectors()["assertions"].as_array().expect("assertions") {
261 let name = case["name"].as_str().expect("a name");
262 let want = unhex(case["assertion_hex"].as_str().expect("hex"));
263 let got = encode_identity_assertion(
264 &unhex(case["public_key_hex"].as_str().expect("key")),
265 &unhex(case["signature_hex"].as_str().expect("signature")),
266 &unhex(case["signed_data_hex"].as_str().expect("signed data")),
267 )
268 .unwrap_or_else(|e| panic!("[{name}]: {e}"));
269 assert_eq!(got, want, "[{name}] encoded bytes");
270 }
271 }
272
273 #[test]
274 fn every_refusal_is_refused() {
275 for case in vectors()["refusals"].as_array().expect("refusals") {
276 let name = case["name"].as_str().expect("a name");
277 let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
278 assert!(
279 parse_identity_assertion(&bytes).is_err(),
280 "[{name}] {} bytes must be refused",
281 bytes.len()
282 );
283 }
284 }
285
286 fn chapter() -> Option<String> {
289 let path =
290 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/ble-framing.md");
291 std::fs::read_to_string(&path).ok().or_else(|| {
292 eprintln!("spec tree not present, skipping the BLE framing chapter drift checks");
293 None
294 })
295 }
296
297 #[test]
298 fn the_chapter_states_the_layout_the_code_performs() {
299 let Some(text) = chapter() else { return };
300
301 assert!(
302 text.contains("| `public_key` | 32 |") && text.contains("| `signature` | 64 |"),
303 "the chapter no longer states the two fixed field sizes"
304 );
305 assert!(
306 text.contains(&format!("shorter than {IDENTITY_ASSERTION_MIN_LEN} bytes")),
307 "the chapter no longer states the {IDENTITY_ASSERTION_MIN_LEN}-byte floor"
308 );
309 assert!(
310 text.contains("identity-assertion-v1.vectors.json"),
311 "the chapter no longer names the vector file that pins it"
312 );
313 }
314}