oxedyne/fe2o3/fe2o3_sbj/src/card.rs
18.3 KiB, 1 run
created by r1870400018:22242, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! `daimond/card/0` — a self-signed identity card, and the two renderings a person compares. |
| 2 | //! |
| 3 | //! A card is what a QR code carries and what a paste carries. A bare public key is not: it says |
| 4 | //! nothing about which key is for signing and which for sealing, carries no display name, and |
| 5 | //! gives a reader no way to tell a first key from one that replaced another. |
| 6 | //! |
| 7 | //! Most of the card is the envelope's already, which is the point of putting it in this container: |
| 8 | //! the signing key is `author`, the signature is `sig`, the algorithm is `sig_scheme`, and the |
| 9 | //! creation time is `time`. What remains, and what this schema defines, is the part a key cannot |
| 10 | //! say about itself — a display label, the separate encryption subkey, the role, and the key this |
| 11 | //! one supersedes. |
| 12 | //! |
| 13 | //! **Self-signed means exactly what it says, and it is worth being blunt about what it does not |
| 14 | //! buy.** A card verifies under the key it carries, so it proves the holder of that key composed |
| 15 | //! it. It proves nothing whatever about who that holder is. A card fetched from a server is |
| 16 | //! therefore Unverified no matter how well it verifies: an intermediary that substituted its own |
| 17 | //! key would produce a card that verifies perfectly. Only an out-of-band act — a QR read in |
| 18 | //! person, or a safety number compared aloud — raises it, and that act is the user's, never the |
| 19 | //! software's. |
| 20 | //! |
| 21 | //! The label is **advisory display text and never an identity**. Equality is always the full |
| 22 | //! 32-byte key. Two people may choose one label and neither is lying. |
| 23 | |
| 24 | use crate::{ |
| 25 | canon, |
| 26 | limit as sbj_limit, |
| 27 | }; |
| 28 | |
| 29 | use oxedyne_fe2o3_core::prelude::*; |
| 30 | use oxedyne_fe2o3_hash::sha256; |
| 31 | use oxedyne_fe2o3_jdat::{ |
| 32 | prelude::*, |
| 33 | bdat::DecodeLimits, |
| 34 | }; |
| 35 | use oxedyne_fe2o3_text::base2x::CROCKFORD32; |
| 36 | |
| 37 | |
| 38 | /// The display name the holder chose. Advisory. |
| 39 | pub const KEY_LABEL: &'static str = "label"; |
| 40 | /// The holder's encryption subkey. |
| 41 | pub const KEY_ENC: &'static str = "enc"; |
| 42 | /// What this key is for. |
| 43 | pub const KEY_ROLE: &'static str = "role"; |
| 44 | /// The key this one supersedes, if it supersedes one. |
| 45 | pub const KEY_PREV: &'static str = "prev"; |
| 46 | |
| 47 | /// Domain separator for a fingerprint. See [`fingerprint`]. |
| 48 | pub const FINGERPRINT_DOMAIN: &'static [u8] = b"daimond-id-v1"; |
| 49 | /// Domain separator for a safety number. See [`safety_number`]. |
| 50 | pub const SAFETY_DOMAIN: &'static [u8] = b"daimond-safety-v1"; |
| 51 | |
| 52 | /// Limits this schema enforces. |
| 53 | pub mod limit { |
| 54 | /// The most a display label may carry, in bytes of UTF-8. |
| 55 | pub const LABEL_BYTES: usize = 64; |
| 56 | /// The exact width of a public key. |
| 57 | pub const KEY_BYTES: usize = 32; |
| 58 | /// Decoding depth for a card, which is a flat record and reaches two. |
| 59 | pub const DEPTH: usize = 4; |
| 60 | /// Bytes of the fingerprint digest that are rendered. |
| 61 | /// |
| 62 | /// Ten, giving eighty bits, which is sixteen base-32 characters exactly and so needs no |
| 63 | /// padding. A fingerprint is a display convenience and decides nothing, so its width is chosen |
| 64 | /// for the eye rather than for a security margin — the margin lives in the full key, which is |
| 65 | /// what every comparison actually uses. |
| 66 | pub const FINGERPRINT_BYTES: usize = 10; |
| 67 | /// Decimal digits in a safety number. |
| 68 | /// |
| 69 | /// Sixty, which is the whole 256-bit digest and not a prefix of it. Truncation is refused: the |
| 70 | /// attack on a safety number is a meet-in-the-middle, which costs about 2^(n/2), so halving the |
| 71 | /// width to 120 bits would leave a 60-bit search rather than a 120-bit one. |
| 72 | pub const SAFETY_DIGITS: usize = 60; |
| 73 | } |
| 74 | |
| 75 | |
| 76 | /// What a key is for. |
| 77 | /// |
| 78 | /// An enum because the set is closed and a reader must be able to refuse a role it does not |
| 79 | /// implement rather than treat an unknown string as harmless. Only one role exists in v0; the type |
| 80 | /// is here so that adding a second is a versioned act rather than a new string appearing on the |
| 81 | /// wire. |
| 82 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 83 | pub enum Role { |
| 84 | /// The account's own long-lived identity key. |
| 85 | Root, |
| 86 | } |
| 87 | |
| 88 | impl Role { |
| 89 | /// The spelling on the wire. |
| 90 | pub fn as_str(&self) -> &'static str { |
| 91 | match self { |
| 92 | Self::Root => "root", |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | /// Reads a role, refusing any spelling this version does not know. |
| 97 | pub fn from_str(s: &str) -> Outcome<Self> { |
| 98 | match s { |
| 99 | "root" => Ok(Self::Root), |
| 100 | other => Err(err!( |
| 101 | "\"{}\" is not a role this version admits. The only role in v0 is \"{}\". An \ |
| 102 | unknown role is refused rather than ignored: a reader that skipped it would be \ |
| 103 | treating a key it does not understand as though it were an ordinary one.", |
| 104 | other, Self::Root.as_str(); |
| 105 | Invalid, Input, Unknown)), |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | |
| 111 | /// A `daimond/card/0` payload. |
| 112 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 113 | pub struct Card { |
| 114 | /// The display name the holder chose. Advisory, and never an identity. |
| 115 | pub label: String, |
| 116 | /// The holder's encryption subkey, which is not the signing key. |
| 117 | /// |
| 118 | /// Separate because the two do different jobs and have different lifetimes: a signature is |
| 119 | /// checked once and discarded, so a signing scheme may be replaced freely, while anything |
| 120 | /// sealed to an encryption key must remain openable. Carrying the sealing key inside a |
| 121 | /// signed card is what lets a reply be sealed to a key the recipient PROVED they hold, rather |
| 122 | /// than to one a server asserted on their behalf. |
| 123 | pub enc: Vec<u8>, |
| 124 | /// What this key is for. |
| 125 | pub role: Role, |
| 126 | /// The key this one supersedes, if any. |
| 127 | /// |
| 128 | /// Present when a holder has rotated. A reader that knows the previous key can see that the |
| 129 | /// new card claims to replace it — and that claim is signed by the NEW key only, so it is a |
| 130 | /// statement of intent and not a proof of succession. Treating it as proof would let anybody |
| 131 | /// claim to supersede anybody. |
| 132 | pub prev: Option<Vec<u8>>, |
| 133 | } |
| 134 | |
| 135 | impl Card { |
| 136 | /// Encodes this card as a canonical daticle. |
| 137 | pub fn to_dat(&self) -> Outcome<Dat> { |
| 138 | let mut map = DaticleMap::new(); |
| 139 | map.insert(dat!(KEY_ENC), Dat::BU8(self.enc.clone())); |
| 140 | map.insert(dat!(KEY_LABEL), Dat::Str(self.label.clone())); |
| 141 | // Absent means omitted, never `none`: SPEC.md §3 rule 4. |
| 142 | if let Some(p) = &self.prev { |
| 143 | map.insert(dat!(KEY_PREV), Dat::BU8(p.clone())); |
| 144 | } |
| 145 | map.insert(dat!(KEY_ROLE), Dat::Str(fmt!("{}", self.role.as_str()))); |
| 146 | Ok(Dat::Map(map)) |
| 147 | } |
| 148 | |
| 149 | /// Reads a card, enforcing every rule this schema declares. |
| 150 | pub fn from_dat(d: &Dat) -> Outcome<Self> { |
| 151 | let map = match d { |
| 152 | Dat::Map(m) => m, |
| 153 | Dat::OrdMap(_) => return Err(err!( |
| 154 | "SPEC.md §3 rule 2: a card payload is a Dat::Map, never a Dat::OrdMap."; |
| 155 | Invalid, Input, Mismatch)), |
| 156 | other => return Err(err!( |
| 157 | "A card payload must be a Dat::Map, found a {:?}.", other.kind(); |
| 158 | Invalid, Input, Mismatch)), |
| 159 | }; |
| 160 | let allowed: Vec<&str> = { |
| 161 | let mut v = vec![KEY_ENC, KEY_LABEL, KEY_ROLE]; |
| 162 | if map.contains_key(&dat!(KEY_PREV)) { v.push(KEY_PREV); } |
| 163 | v |
| 164 | }; |
| 165 | for k in &allowed { |
| 166 | if !map.contains_key(&dat!(*k)) { |
| 167 | return Err(err!( |
| 168 | "The card is missing the required key \"{}\".", k; |
| 169 | Invalid, Input, Missing)); |
| 170 | } |
| 171 | } |
| 172 | for k in map.keys() { |
| 173 | let name = match k { |
| 174 | Dat::Str(s) => s.clone(), |
| 175 | other => return Err(err!( |
| 176 | "SPEC.md §3 rule 3: a map key must be a string, found a {:?}.", other.kind(); |
| 177 | Invalid, Input, Mismatch)), |
| 178 | }; |
| 179 | res!(canon::check_key_string(&name)); |
| 180 | if !allowed.iter().any(|a| *a == name.as_str()) { |
| 181 | return Err(err!( |
| 182 | "The card carries the key \"{}\", which this schema does not admit. The \ |
| 183 | admitted keys are: {}.", name, allowed.join(", "); |
| 184 | Invalid, Input, Unknown)); |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | let label = match map.get(&dat!(KEY_LABEL)) { |
| 189 | Some(Dat::Str(s)) => s.clone(), |
| 190 | Some(other) => return Err(err!( |
| 191 | "The card key \"{}\" must be a string, found a {:?}.", KEY_LABEL, other.kind(); |
| 192 | Invalid, Input, Mismatch)), |
| 193 | None => return Err(err!( |
| 194 | "The card is missing \"{}\".", KEY_LABEL; Invalid, Input, Missing)), |
| 195 | }; |
| 196 | if label.len() > limit::LABEL_BYTES { |
| 197 | return Err(err!( |
| 198 | "The card label is {} bytes, exceeding the limit of {}.", |
| 199 | label.len(), limit::LABEL_BYTES; |
| 200 | Invalid, Input, LimitReached)); |
| 201 | } |
| 202 | res!(canon::check_string(&label)); |
| 203 | |
| 204 | let enc = res!(exact_bytes(map, KEY_ENC, limit::KEY_BYTES)); |
| 205 | let prev = match map.get(&dat!(KEY_PREV)) { |
| 206 | Some(_) => Some(res!(exact_bytes(map, KEY_PREV, limit::KEY_BYTES))), |
| 207 | None => None, |
| 208 | }; |
| 209 | let role = match map.get(&dat!(KEY_ROLE)) { |
| 210 | Some(Dat::Str(s)) => res!(Role::from_str(s)), |
| 211 | Some(other) => return Err(err!( |
| 212 | "The card key \"{}\" must be a string, found a {:?}.", KEY_ROLE, other.kind(); |
| 213 | Invalid, Input, Mismatch)), |
| 214 | None => return Err(err!( |
| 215 | "The card is missing \"{}\".", KEY_ROLE; Invalid, Input, Missing)), |
| 216 | }; |
| 217 | |
| 218 | Ok(Self { label, enc, role, prev }) |
| 219 | } |
| 220 | |
| 221 | /// Encodes this card to the canonical bytes that become the tree region. |
| 222 | pub fn encode(&self) -> Outcome<Vec<u8>> { |
| 223 | let d = res!(self.to_dat()); |
| 224 | // Read straight back, so a card that cannot be decoded can never be signed. |
| 225 | res!(Self::from_dat(&d)); |
| 226 | Ok(res!(d.to_bytes(Vec::new()))) |
| 227 | } |
| 228 | |
| 229 | /// Decodes a card from the bytes of a tree region, which must be consumed exactly. |
| 230 | pub fn decode(buf: &[u8]) -> Outcome<Self> { |
| 231 | let lims = DecodeLimits::new(limit::DEPTH, sbj_limit::TREE_BYTES); |
| 232 | let (d, n) = res!(Dat::from_bytes_limited(buf, &lims)); |
| 233 | if n != buf.len() { |
| 234 | return Err(err!( |
| 235 | "The card occupies {} of the {} bytes supplied, leaving {} trailing.", |
| 236 | n, buf.len(), buf.len() - n; |
| 237 | Invalid, Input, Decode)); |
| 238 | } |
| 239 | let re = res!(d.to_bytes(Vec::new())); |
| 240 | if re != buf { |
| 241 | return Err(err!( |
| 242 | "The card is not in canonical form: it re-encodes to {} bytes against the {} \ |
| 243 | supplied. See SPEC.md §3.", re.len(), buf.len(); |
| 244 | Invalid, Input, Decode)); |
| 245 | } |
| 246 | Self::from_dat(&d) |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | /// Reads a `BU8` key of an exact width. |
| 251 | fn exact_bytes(map: &DaticleMap, key: &str, width: usize) -> Outcome<Vec<u8>> { |
| 252 | let b = match map.get(&dat!(key)) { |
| 253 | Some(Dat::BU8(b)) => b.clone(), |
| 254 | Some(other) => return Err(err!( |
| 255 | "The card key \"{}\" must carry a BU8, found a {:?}.", key, other.kind(); |
| 256 | Invalid, Input, Mismatch)), |
| 257 | None => return Err(err!( |
| 258 | "The card is missing the required key \"{}\".", key; |
| 259 | Invalid, Input, Missing)), |
| 260 | }; |
| 261 | if b.len() != width { |
| 262 | return Err(err!( |
| 263 | "The card key \"{}\" carries {} bytes and must carry exactly {}. A key of the wrong \ |
| 264 | width is not a shorter key; it is a different thing.", key, b.len(), width; |
| 265 | Invalid, Input, Mismatch)); |
| 266 | } |
| 267 | Ok(b) |
| 268 | } |
| 269 | |
| 270 | |
| 271 | // ┌───────────────────────────────────────────────────────────────────────────┐ |
| 272 | // │ WHAT A PERSON READS │ |
| 273 | // └───────────────────────────────────────────────────────────────────────────┘ |
| 274 | |
| 275 | /// A short rendering of a key, for a person's eye. **It decides nothing.** |
| 276 | /// |
| 277 | /// Eighty bits of `SHA-256(domain ‖ key)`, in Crockford base 32, in four groups of four: |
| 278 | /// `K7Q2-9F3M-XR4A-8WVN`. The grouping is for reading aloud and copying by hand, and the alphabet |
| 279 | /// leaves out `I`, `L`, `O` and `U` for the same reason. |
| 280 | /// |
| 281 | /// **Equality is always the full 32-byte key, everywhere, without exception.** A fingerprint is a |
| 282 | /// display convenience, and eighty bits is comfortably within reach of somebody who wants two keys |
| 283 | /// to look alike in a list. Anything that compares fingerprints to decide whether two keys are the |
| 284 | /// same is a defect, and this function exists so that there is one implementation of the rendering |
| 285 | /// rather than two that can disagree about it. |
| 286 | /// |
| 287 | /// One function, deliberately: the same rendering is shown by the client and by the account |
| 288 | /// lookup, and two implementations of it would eventually differ on a key nobody had tested, which |
| 289 | /// a user would read as their correspondent's key having changed. |
| 290 | pub fn fingerprint(key: &[u8]) -> String { |
| 291 | let mut msg = Vec::with_capacity(FINGERPRINT_DOMAIN.len() + key.len()); |
| 292 | msg.extend_from_slice(FINGERPRINT_DOMAIN); |
| 293 | msg.extend_from_slice(key); |
| 294 | let digest = sha256::digest(&msg); |
| 295 | // Eighty bits is sixteen base-32 characters exactly, so nothing is padded and the rendering has |
| 296 | // one form. |
| 297 | let s = CROCKFORD32.to_string(&digest[..limit::FINGERPRINT_BYTES]); |
| 298 | let chars: Vec<char> = s.chars().collect(); |
| 299 | let mut out = String::with_capacity(19); |
| 300 | for (i, c) in chars.iter().enumerate() { |
| 301 | if i > 0 && i % 4 == 0 { |
| 302 | out.push('-'); |
| 303 | } |
| 304 | out.push(*c); |
| 305 | } |
| 306 | out |
| 307 | } |
| 308 | |
| 309 | /// The number two people read to each other to check they hold each other's real keys. |
| 310 | /// |
| 311 | /// `SHA-256(domain ‖ min(a, b) ‖ max(a, b))`, rendered as sixty decimal digits in twelve groups of |
| 312 | /// five. Sorting the two keys is what makes it symmetric: both parties compute the same number |
| 313 | /// without having to agree who is first, and a protocol that needed them to agree would be one |
| 314 | /// more thing for an intermediary to influence. |
| 315 | /// |
| 316 | /// **The whole digest, never a prefix.** The attack is a meet-in-the-middle, costing about |
| 317 | /// 2^(n/2), so a number truncated to 120 bits would face a 60-bit search rather than a 120-bit |
| 318 | /// one. A shorter number is easier to read aloud and that is not a reason. |
| 319 | /// |
| 320 | /// It is read over a channel an attacker cannot silently rewrite — a voice call, or in person. |
| 321 | /// Reading it over the same channel the keys arrived on proves nothing, since whatever substituted |
| 322 | /// the keys can substitute the number. |
| 323 | pub fn safety_number(a: &[u8], b: &[u8]) -> String { |
| 324 | let (first, second) = if a <= b { (a, b) } else { (b, a) }; |
| 325 | let mut msg = Vec::with_capacity(SAFETY_DOMAIN.len() + first.len() + second.len()); |
| 326 | msg.extend_from_slice(SAFETY_DOMAIN); |
| 327 | msg.extend_from_slice(first); |
| 328 | msg.extend_from_slice(second); |
| 329 | let digest = sha256::digest(&msg); |
| 330 | |
| 331 | // Five decimal digits per five bytes, taken big-endian and reduced modulo 100000, which is the |
| 332 | // same construction Signal's safety numbers use. Twelve groups of five covers the whole digest: |
| 333 | // 32 bytes does not divide by 5, so the last group is taken from the remaining two bytes and |
| 334 | // the digest's first byte, which is why the loop walks a rotated window rather than a slice. |
| 335 | let mut out = String::with_capacity(limit::SAFETY_DIGITS + 11); |
| 336 | for g in 0..12 { |
| 337 | let mut acc: u64 = 0; |
| 338 | for i in 0..5 { |
| 339 | acc = (acc << 8) | digest[(g * 5 + i) % digest.len()] as u64; |
| 340 | } |
| 341 | if g > 0 { |
| 342 | out.push(' '); |
| 343 | } |
| 344 | out.push_str(&fmt!("{:05}", acc % 100_000)); |
| 345 | } |
| 346 | out |
| 347 | } |
| 348 | |
| 349 | |
| 350 | #[cfg(test)] |
| 351 | mod tests { |
| 352 | use super::*; |
| 353 | |
| 354 | fn sample() -> Card { |
| 355 | Card { |
| 356 | label: fmt!("Jason"), |
| 357 | enc: vec![0xE1; limit::KEY_BYTES], |
| 358 | role: Role::Root, |
| 359 | prev: None, |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | #[test] |
| 364 | fn test_round_trip() -> Outcome<()> { |
| 365 | let c = sample(); |
| 366 | let bytes = res!(c.encode()); |
| 367 | assert_eq!(res!(Card::decode(&bytes)), c); |
| 368 | Ok(()) |
| 369 | } |
| 370 | |
| 371 | #[test] |
| 372 | fn test_round_trip_with_prev() -> Outcome<()> { |
| 373 | let mut c = sample(); |
| 374 | c.prev = Some(vec![0xD4; limit::KEY_BYTES]); |
| 375 | let bytes = res!(c.encode()); |
| 376 | assert_eq!(res!(Card::decode(&bytes)), c); |
| 377 | // A rotated card and a first card must not encode alike. |
| 378 | assert_ne!(bytes, res!(sample().encode())); |
| 379 | Ok(()) |
| 380 | } |
| 381 | |
| 382 | #[test] |
| 383 | fn test_unknown_role_refused() -> Outcome<()> { |
| 384 | match Role::from_str("admin") { |
| 385 | Ok(_) => Err(err!("An unknown role was accepted."; Test, Invalid)), |
| 386 | Err(_) => Ok(()), |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | #[test] |
| 391 | fn test_encryption_key_must_be_exact_width() -> Outcome<()> { |
| 392 | let mut c = sample(); |
| 393 | c.enc = vec![0xE1; limit::KEY_BYTES - 1]; |
| 394 | match c.encode() { |
| 395 | Ok(_) => Err(err!("A short encryption key was accepted."; Test, Invalid)), |
| 396 | Err(_) => Ok(()), |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | #[test] |
| 401 | fn test_unknown_card_key_refused() -> Outcome<()> { |
| 402 | let mut map = DaticleMap::new(); |
| 403 | map.insert(dat!(KEY_ENC), Dat::BU8(vec![0xE1; limit::KEY_BYTES])); |
| 404 | map.insert(dat!(KEY_LABEL), Dat::Str(fmt!("Jason"))); |
| 405 | map.insert(dat!(KEY_ROLE), Dat::Str(fmt!("root"))); |
| 406 | map.insert(dat!("verified"), Dat::Bool(true)); |
| 407 | match Card::from_dat(&Dat::Map(map)) { |
| 408 | Ok(_) => Err(err!("A card claiming to be verified was accepted."; Test, Invalid)), |
| 409 | Err(_) => Ok(()), |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | #[test] |
| 414 | fn test_label_not_nfc_refused() -> Outcome<()> { |
| 415 | let mut c = sample(); |
| 416 | c.label = fmt!("Jaso\u{0301}n"); |
| 417 | match c.encode() { |
| 418 | Ok(_) => Err(err!("A label that is not in NFC was accepted."; Test, Invalid)), |
| 419 | Err(_) => Ok(()), |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | /// The rendering must have one shape, and it must not be the key. |
| 424 | #[test] |
| 425 | fn test_fingerprint_shape() -> Outcome<()> { |
| 426 | let f = fingerprint(&[0xAA; 32]); |
| 427 | assert_eq!(f.len(), 19, "sixteen characters and three separators: {}", f); |
| 428 | assert_eq!(f.matches('-').count(), 3); |
| 429 | for part in f.split('-') { |
| 430 | assert_eq!(part.len(), 4); |
| 431 | for ch in part.chars() { |
| 432 | assert!(CROCKFORD32_CHARS.contains(&ch), "'{}' is outside the alphabet", ch); |
| 433 | } |
| 434 | } |
| 435 | Ok(()) |
| 436 | } |
| 437 | |
| 438 | /// The alphabet a fingerprint may use, restated here so the test is not checking the code |
| 439 | /// against itself. |
| 440 | const CROCKFORD32_CHARS: [char; 32] = [ |
| 441 | '0', '1', '2', '3', '4', '5', '6', '7', |
| 442 | '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', |
| 443 | 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', |
| 444 | 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', |
| 445 | ]; |
| 446 | |
| 447 | #[test] |
| 448 | fn test_fingerprint_differs_by_key() -> Outcome<()> { |
| 449 | assert_ne!(fingerprint(&[0xAA; 32]), fingerprint(&[0xAB; 32])); |
| 450 | Ok(()) |
| 451 | } |
| 452 | |
| 453 | /// Sorting the keys is what makes both parties compute the same number. |
| 454 | #[test] |
| 455 | fn test_safety_number_is_symmetric() -> Outcome<()> { |
| 456 | let a = [0x01; 32]; |
| 457 | let b = [0x02; 32]; |
| 458 | assert_eq!(safety_number(&a, &b), safety_number(&b, &a)); |
| 459 | Ok(()) |
| 460 | } |
| 461 | |
| 462 | #[test] |
| 463 | fn test_safety_number_shape() -> Outcome<()> { |
| 464 | let s = safety_number(&[0x01; 32], &[0x02; 32]); |
| 465 | let groups: Vec<&str> = s.split(' ').collect(); |
| 466 | assert_eq!(groups.len(), 12, "twelve groups: {}", s); |
| 467 | for g in &groups { |
| 468 | assert_eq!(g.len(), 5); |
| 469 | assert!(g.chars().all(|c| c.is_ascii_digit())); |
| 470 | } |
| 471 | assert_eq!(groups.iter().map(|g| g.len()).sum::<usize>(), limit::SAFETY_DIGITS); |
| 472 | Ok(()) |
| 473 | } |
| 474 | |
| 475 | /// A different pair of keys must give a different number, or it is measuring nothing. |
| 476 | #[test] |
| 477 | fn test_safety_number_differs_by_pair() -> Outcome<()> { |
| 478 | let a = [0x01; 32]; |
| 479 | let b = [0x02; 32]; |
| 480 | let c = [0x03; 32]; |
| 481 | assert_ne!(safety_number(&a, &b), safety_number(&a, &c)); |
| 482 | Ok(()) |
| 483 | } |
| 484 | |
| 485 | /// The domain separators must actually separate: the same bytes under the two constructions |
| 486 | /// must not collide. |
| 487 | #[test] |
| 488 | fn test_domains_are_separated() -> Outcome<()> { |
| 489 | let k = [0x07; 32]; |
| 490 | let mut plain = Vec::new(); |
| 491 | plain.extend_from_slice(&k); |
| 492 | assert_ne!( |
| 493 | fmt!("{:?}", sha256::digest(&plain)), |
| 494 | fmt!("{:?}", { |
| 495 | let mut m = Vec::new(); |
| 496 | m.extend_from_slice(FINGERPRINT_DOMAIN); |
| 497 | m.extend_from_slice(&k); |
| 498 | sha256::digest(&m) |
| 499 | }), |
| 500 | ); |
| 501 | Ok(()) |
| 502 | } |
| 503 | } |