Oregami
Repositories/oxedyne/fe2o3

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
24use crate::{
25 canon,
26 limit as sbj_limit,
27};
28
29use oxedyne_fe2o3_core::prelude::*;
30use oxedyne_fe2o3_hash::sha256;
31use oxedyne_fe2o3_jdat::{
32 prelude::*,
33 bdat::DecodeLimits,
34};
35use oxedyne_fe2o3_text::base2x::CROCKFORD32;
36
37
38/// The display name the holder chose. Advisory.
39pub const KEY_LABEL: &'static str = "label";
40/// The holder's encryption subkey.
41pub const KEY_ENC: &'static str = "enc";
42/// What this key is for.
43pub const KEY_ROLE: &'static str = "role";
44/// The key this one supersedes, if it supersedes one.
45pub const KEY_PREV: &'static str = "prev";
46
47/// Domain separator for a fingerprint. See [`fingerprint`].
48pub const FINGERPRINT_DOMAIN: &'static [u8] = b"daimond-id-v1";
49/// Domain separator for a safety number. See [`safety_number`].
50pub const SAFETY_DOMAIN: &'static [u8] = b"daimond-safety-v1";
51
52/// Limits this schema enforces.
53pub 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)]
83pub enum Role {
84 /// The account's own long-lived identity key.
85 Root,
86}
87
88impl 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)]
113pub 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
135impl 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.
251fn 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.
290pub 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.
323pub 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)]
351mod 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}