Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/presentation/shape.rs

36.5 KiB, 1 run

created by r1870400018:61154, 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

1use oxedyne_fe2o3_core::prelude::*;
2use oxedyne_fe2o3_crypto::linkring;
3use oxedyne_fe2o3_hash::sha256;
4use oxedyne_fe2o3_jdat::{
5 prelude::*,
6 bdat::DecodeLimits,
7};
8use oxedyne_fe2o3_text::base64;
9
10
11// Protocol words
12pub const V_REQUEST: &str = "present-req/1";
13pub const V_PRESENTATION: &str = "present/1";
14pub const V_HEAD: &str = "head/1";
15pub const V_INVOICE: &str = "invoice/1";
16pub const V_SETTLEMENT: &str = "settle/1";
17pub const SCOPE_PREFIX: &str = "present/1:";
18pub const ALG_RING: &str = linkring::ALG;
19
20// Clocks, in seconds
21pub const T_G: u64 = 300; // freshness of a head, a presentation and a nonce
22pub const HEAD_LEAD: u64 = 60; // how far a head may postdate its presentation
23pub const INVOICE_LIFE: u64 = 3_600; // longest an invoice may run
24
25// Sizes
26pub const NONCE_LEN: usize = 32;
27pub const KEY_LEN: usize = 32;
28pub const SIG_LEN: usize = 64;
29pub const SALT_LEN: usize = 16;
30pub const INVOICE_NONCE_LEN: usize = 16;
31pub const KEY_ID_CHARS: usize = 10;
32pub const MEMO_MAX_CHARS: usize = 64;
33pub const WORD_MAX_CHARS: usize = 32;
34pub const WORDS_MAX: usize = 16;
35pub const JSON_MAX_BYTES: usize = 64 * 1024;
36pub const MAX_INT: u64 = 9_007_199_254_740_991; // 2^53 - 1
37
38const JSON_MAX_DEPTH: usize = 16; // value nesting a parse descends to
39
40// Members, by shape
41const REQUEST_MEMBERS: [&str; 8] =
42 ["v", "rp_id", "nonce", "modes", "predicates", "invoice", "return_to", "exp"];
43const REQUEST_RESERVED: [&str; 2] = ["org", "osig"]; // ignored until origins are bound to names
44const NAMED_MEMBERS: [&str; 10] =
45 ["v", "mode", "rp_id", "nonce", "sub", "pub", "predicates", "head", "ts", "sig"];
46const PAIRWISE_MEMBERS: [&str; 11] =
47 ["v", "mode", "rp_id", "nonce", "sub", "tag", "predicates", "head", "ts", "proof", "sig"];
48const PROOF_MEMBERS: [&str; 2] = ["alg", "body"];
49const HEAD_MEMBERS: [&str; 11] = [
50 "v", "head", "epoch", "prev", "ts", "salt", "members", "ring_n", "ring_digest", "signer", "sig",
51];
52const INVOICE_MEMBERS: [&str; 9] =
53 ["v", "rp_id", "payee", "account", "oxes", "memo", "nonce", "expires", "ts"];
54const SETTLEMENT_MEMBERS: [&str; 7] = ["v", "invoice", "entry", "oxes", "ts", "signer", "sig"];
55
56
57// ── Rules ───────────────────────────────────────────────────────────────────
58
59/// Checks that `origin` is an origin in RFC 6454 ASCII serialisation, spelt the
60/// one way a verifier compares it, byte for byte: `scheme "://" host [":" port]`,
61/// scheme and host lowercase, the default port omitted, and no path, trailing
62/// slash or user information. The scheme is `https`, or `http` with a host of
63/// `localhost` or `127.0.0.1` for development. An IPv6 literal is not accepted,
64/// and an IPv4 address only as a dotted quad.
65pub fn check_origin(origin: &str) -> Outcome<()> {
66 let (scheme, rest) = match origin.split_once("://") {
67 Some(parts) => parts,
68 None => return Err(err!(
69 "The origin '{}' has no '://' after its scheme.", origin;
70 Invalid, Input)),
71 };
72 let default_port = match scheme {
73 "https" => "443",
74 "http" => "80",
75 _ => return Err(err!(
76 "The origin '{}' has scheme '{}', where 'https' is required ('http' only for \
77 localhost).", origin, scheme;
78 Invalid, Input)),
79 };
80 let (host, port) = match rest.rsplit_once(':') {
81 Some((host, port)) => (host, Some(port)),
82 None => (rest, None),
83 };
84 if host.is_empty() || host.len() > 253 {
85 return Err(err!(
86 "The origin '{}' has a host of {} characters, where 1 to 253 are allowed.",
87 origin, host.len();
88 Invalid, Input, Size));
89 }
90 // Lowercase letters, digits, hyphens and dots only, which also refuses a
91 // path, a slash, user information, a query and an upper case letter.
92 if let Some(c) = host.chars().find(|c| !(c.is_ascii_lowercase()
93 || c.is_ascii_digit() || *c == '-' || *c == '.'))
94 {
95 return Err(err!(
96 "The origin '{}' has '{}' in its host, which a serialised origin never \
97 carries there.", origin, c.escape_default();
98 Invalid, Input));
99 }
100 for label in host.split('.') {
101 if label.is_empty() || label.len() > 63 || label.starts_with('-') || label.ends_with('-') {
102 return Err(err!(
103 "The origin '{}' has the host label '{}', which is empty, longer than 63 \
104 characters, or begins or ends with a hyphen.", origin, label;
105 Invalid, Input));
106 }
107 }
108 // A browser reads a host whose last label is a number, decimal or hex, as
109 // an IPv4 address and serialises it as a dotted quad, so `https://127.1` is
110 // `https://127.0.0.1` there (WHATWG URL, "ends in a number").
111 if ends_in_a_number(host) && !is_dotted_quad(host) {
112 return Err(err!(
113 "The origin '{}' has a host that a browser reads as an IPv4 address, and \
114 serialises otherwise than as written; only a dotted quad is.", origin;
115 Invalid, Input));
116 }
117 if scheme == "http" && host != "localhost" && host != "127.0.0.1" {
118 return Err(err!(
119 "The origin '{}' is plain http on a host other than localhost or 127.0.0.1.",
120 origin;
121 Invalid, Input, Security));
122 }
123 if let Some(port) = port {
124 // Digits only, since `parse` would also take a leading '+'.
125 let digits = !port.is_empty() && port.len() <= 5 && port.bytes().all(|b| b.is_ascii_digit());
126 let number = match port.parse::<u32>() {
127 Ok(n) if digits => n,
128 _ => 0,
129 };
130 if port.starts_with('0') || number == 0 || number > 65_535 {
131 return Err(err!(
132 "The origin '{}' has the port '{}', which is not a number from 1 to 65535 \
133 without leading zeros.", origin, port;
134 Invalid, Input));
135 }
136 if port == default_port {
137 return Err(err!(
138 "The origin '{}' names its scheme's default port, which a serialised \
139 origin omits.", origin;
140 Invalid, Input));
141 }
142 }
143 Ok(())
144}
145
146/// Does the host's last label read as a number, all decimal digits or `0x` and
147/// hex digits?
148fn ends_in_a_number(host: &str) -> bool {
149 let last = match host.rsplit('.').next() {
150 Some(last) => last,
151 None => return false,
152 };
153 if !last.is_empty() && last.bytes().all(|b| b.is_ascii_digit()) {
154 return true;
155 }
156 match last.strip_prefix("0x") {
157 Some(hex) => hex.bytes().all(|b| b.is_ascii_hexdigit()),
158 None => false,
159 }
160}
161
162/// Is the host four decimal numbers from 0 to 255, without leading zeros?
163fn is_dotted_quad(host: &str) -> bool {
164 let parts: Vec<&str> = host.split('.').collect();
165 parts.len() == 4 && parts.iter().all(|p| {
166 !p.is_empty()
167 && p.bytes().all(|b| b.is_ascii_digit())
168 && (p.len() == 1 || !p.starts_with('0'))
169 && matches!(p.parse::<u16>(), Ok(n) if n <= 255)
170 })
171}
172
173/// The key id a public key earns: the first ten lowercase hex characters of
174/// SHA-256 over its raw bytes.
175pub fn key_id(public: &[u8]) -> String {
176 let digest = sha256::digest(public);
177 let mut id = String::with_capacity(KEY_ID_CHARS);
178 for b in &digest[..KEY_ID_CHARS / 2] {
179 id.push_str(&fmt!("{:02x}", b));
180 }
181 id
182}
183
184/// Is `s` a key id, ten lowercase hex characters?
185pub fn is_key_id(s: &str) -> bool {
186 s.len() == KEY_ID_CHARS && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
187}
188
189/// Is `s` a predicate word, 1 to 32 lowercase ASCII letters, digits and
190/// underscores?
191pub fn is_word(s: &str) -> bool {
192 !s.is_empty()
193 && s.len() <= WORD_MAX_CHARS
194 && s.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
195}
196
197/// The scope a pairwise proof for `rp_id` links under: `"present/1:"` and the
198/// origin, as UTF-8 bytes. One human has one tag per scope.
199pub fn scope(rp_id: &str) -> Vec<u8> {
200 let mut s = Vec::with_capacity(SCOPE_PREFIX.len() + rp_id.len());
201 s.extend_from_slice(SCOPE_PREFIX.as_bytes());
202 s.extend_from_slice(rp_id.as_bytes());
203 s
204}
205
206
207// ── Modes ───────────────────────────────────────────────────────────────────
208
209/// How a member presents: under a public name, or under a pseudonym and a tag
210/// that only this relying party sees.
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub enum Mode {
213 Named,
214 Pairwise,
215}
216
217impl Mode {
218 pub fn word(&self) -> &'static str {
219 match self {
220 Self::Named => "named",
221 Self::Pairwise => "pairwise",
222 }
223 }
224
225 pub fn from_word(s: &str) -> Option<Self> {
226 match s {
227 "named" => Some(Self::Named),
228 "pairwise" => Some(Self::Pairwise),
229 _ => None,
230 }
231 }
232}
233
234/// The modes a request accepts. Pairwise is always among them, so a relying
235/// party may accept a public name but cannot demand one.
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub enum Accept {
238 Pairwise, // wire ["pairwise"]
239 PairwiseOrNamed, // wire ["pairwise", "named"]
240}
241
242impl Accept {
243 /// Does a request carrying this accept a presentation in `mode`?
244 pub fn admits(&self, mode: Mode) -> bool {
245 match (self, mode) {
246 (_, Mode::Pairwise) => true,
247 (Self::PairwiseOrNamed, Mode::Named) => true,
248 (Self::Pairwise, Mode::Named) => false,
249 }
250 }
251
252 fn to_dat(&self) -> Dat {
253 match self {
254 Self::Pairwise => listdat![Mode::Pairwise.word()],
255 Self::PairwiseOrNamed => listdat![Mode::Pairwise.word(), Mode::Named.word()],
256 }
257 }
258
259 fn from_dat(dat: &Dat) -> Outcome<Self> {
260 let words: Vec<&str> = match dat {
261 Dat::List(list) => list.iter().filter_map(|d| match d {
262 Dat::Str(s) => Some(s.as_str()),
263 _ => None,
264 }).collect(),
265 _ => Vec::new(),
266 };
267 match (dat, words.as_slice()) {
268 (Dat::List(list), ["pairwise"]) if list.len() == 1 => Ok(Self::Pairwise),
269 (Dat::List(list), ["pairwise", "named"]) if list.len() == 2 => Ok(Self::PairwiseOrNamed),
270 _ => Err(err!(
271 "A request's modes are [\"pairwise\"] or [\"pairwise\", \"named\"], not {:?}.",
272 dat;
273 Invalid, Input)),
274 }
275 }
276}
277
278
279// ── Request ─────────────────────────────────────────────────────────────────
280
281/// A relying party's challenge to one browser session: its origin, a nonce,
282/// what it accepts and asks, and when the challenge lapses.
283#[derive(Clone, Debug, Eq, PartialEq)]
284pub struct Request {
285 pub rp_id: String,
286 pub nonce: [u8; NONCE_LEN],
287 pub accept: Accept,
288 pub predicates: Vec<String>,
289 pub invoice: Option<Invoice>,
290 pub return_to: Option<String>, // for the redirect route only, a URL on `rp_id`
291 pub exp: u64,
292}
293
294impl Request {
295
296 /// Checks what the shape alone cannot: the origin, the words, the return
297 /// URL and any invoice.
298 pub fn check(&self) -> Outcome<()> {
299 res!(check_origin(&self.rp_id));
300 res!(check_words(&self.predicates, "request"));
301 if let Some(url) = &self.return_to {
302 let on_origin = url.starts_with(&self.rp_id)
303 && url.as_bytes().get(self.rp_id.len()) == Some(&b'/');
304 if !on_origin || url.chars().any(|c| c == '#' || c.is_whitespace() || c.is_control()) {
305 return Err(err!(
306 "The return URL '{}' is not a URL on {} without a fragment.", url, self.rp_id;
307 Invalid, Input));
308 }
309 }
310 if let Some(invoice) = &self.invoice {
311 res!(invoice.check());
312 if invoice.rp_id != self.rp_id {
313 return Err(err!(
314 "The invoice was issued by {}, and the request by {}.",
315 invoice.rp_id, self.rp_id;
316 Invalid, Input, Mismatch));
317 }
318 }
319 Ok(())
320 }
321
322 pub fn to_dat(&self) -> Dat {
323 let mut m = DaticleMap::new();
324 m.insert(dat!("v"), dat!(V_REQUEST));
325 m.insert(dat!("rp_id"), dat!(self.rp_id.clone()));
326 m.insert(dat!("nonce"), dat!(base64::encode_url(&self.nonce)));
327 m.insert(dat!("modes"), self.accept.to_dat());
328 m.insert(dat!("predicates"), words_dat(&self.predicates));
329 if let Some(invoice) = &self.invoice {
330 m.insert(dat!("invoice"), invoice.to_dat());
331 }
332 if let Some(url) = &self.return_to {
333 m.insert(dat!("return_to"), dat!(url.clone()));
334 }
335 m.insert(dat!("exp"), Dat::U64(self.exp));
336 Dat::Map(m)
337 }
338
339 pub fn to_json(&self) -> Outcome<String> {
340 self.to_dat().json_canonical()
341 }
342
343 /// Reads a request strictly: an unknown member is refused, save `org` and
344 /// `osig`, which are reserved and ignored.
345 pub fn from_dat(dat: &Dat) -> Outcome<Self> {
346 let o = res!(Obj::of(dat, "request"));
347 res!(o.only(&REQUEST_MEMBERS, &REQUEST_RESERVED));
348 res!(o.version(V_REQUEST));
349 let invoice = match o.get("invoice") {
350 Some(d) => Some(res!(Invoice::from_dat(d))),
351 None => None,
352 };
353 let return_to = match o.get("return_to") {
354 Some(_) => Some(res!(o.text("return_to"))),
355 None => None,
356 };
357 let req = Self {
358 rp_id: res!(o.text("rp_id")),
359 nonce: res!(o.bytes::<NONCE_LEN>("nonce")),
360 accept: res!(Accept::from_dat(res!(o.must("modes")))),
361 predicates: res!(o.words("predicates")),
362 invoice,
363 return_to,
364 exp: res!(o.uint("exp")),
365 };
366 res!(req.check());
367 Ok(req)
368 }
369
370 pub fn parse(json: &str) -> Outcome<Self> {
371 Self::from_dat(&res!(decode_json(json, "request")))
372 }
373}
374
375
376// ── Presentation ────────────────────────────────────────────────────────────
377
378/// Who presents, with what the mode needs.
379#[derive(Clone, Debug, Eq, PartialEq)]
380pub enum Subject {
381 Named {
382 id: String, // the key id of `key`, on the wire as `sub`
383 key: [u8; KEY_LEN], // Ed25519, on the wire as `pub`
384 },
385 Pairwise {
386 key: [u8; KEY_LEN], // Ed25519 pseudonym for this relying party, `sub`
387 tag: [u8; KEY_LEN], // linking tag under this relying party's scope
388 proof: Proof,
389 },
390}
391
392/// A ring proof, named by its algorithm. Its bytes are the algorithm's own.
393#[derive(Clone, Debug, Eq, PartialEq)]
394pub struct Proof {
395 pub alg: String,
396 pub body: Vec<u8>,
397}
398
399/// A member's answer to a request.
400#[derive(Clone, Debug, Eq, PartialEq)]
401pub struct Presentation {
402 pub rp_id: String,
403 pub nonce: [u8; NONCE_LEN],
404 pub subject: Subject,
405 pub predicates: Vec<String>,
406 pub head: [u8; 32],
407 pub ts: u64,
408 pub sig: [u8; SIG_LEN],
409}
410
411impl Presentation {
412
413 pub fn mode(&self) -> Mode {
414 match self.subject {
415 Subject::Named { .. } => Mode::Named,
416 Subject::Pairwise { .. } => Mode::Pairwise,
417 }
418 }
419
420 fn signed_map(&self) -> DaticleMap {
421 let mut m = DaticleMap::new();
422 m.insert(dat!("v"), dat!(V_PRESENTATION));
423 m.insert(dat!("mode"), dat!(self.mode().word()));
424 m.insert(dat!("rp_id"), dat!(self.rp_id.clone()));
425 m.insert(dat!("nonce"), dat!(base64::encode_url(&self.nonce)));
426 match &self.subject {
427 Subject::Named { id, key } => {
428 m.insert(dat!("sub"), dat!(id.clone()));
429 m.insert(dat!("pub"), dat!(base64::encode_url(key)));
430 },
431 Subject::Pairwise { key, tag, .. } => {
432 m.insert(dat!("sub"), dat!(base64::encode_url(key)));
433 m.insert(dat!("tag"), dat!(base64::encode_url(tag)));
434 },
435 }
436 m.insert(dat!("predicates"), words_dat(&self.predicates));
437 m.insert(dat!("head"), dat!(base64::encode_url(&self.head)));
438 m.insert(dat!("ts"), Dat::U64(self.ts));
439 m
440 }
441
442 /// The bytes the signature and a pairwise proof both cover: the canonical
443 /// JSON of every member but `proof` and `sig`, rebuilt from the parsed
444 /// values, so the sender's own spelling of the object cannot move a byte.
445 pub fn signed_bytes(&self) -> Outcome<Vec<u8>> {
446 Ok(res!(Dat::Map(self.signed_map()).json_canonical()).into_bytes())
447 }
448
449 pub fn to_dat(&self) -> Dat {
450 let mut m = self.signed_map();
451 if let Subject::Pairwise { proof, .. } = &self.subject {
452 m.insert(dat!("proof"), mapdat!{
453 "alg" => proof.alg.clone(),
454 "body" => base64::encode_url(&proof.body),
455 });
456 }
457 m.insert(dat!("sig"), dat!(base64::encode_url(&self.sig)));
458 Dat::Map(m)
459 }
460
461 pub fn to_json(&self) -> Outcome<String> {
462 self.to_dat().json_canonical()
463 }
464
465 /// Reads a presentation strictly: exactly the members its mode carries, each
466 /// of its stated form.
467 pub fn from_dat(dat: &Dat) -> Outcome<Self> {
468 let o = res!(Obj::of(dat, "presentation"));
469 res!(o.version(V_PRESENTATION));
470 let mode = match Mode::from_word(&res!(o.text("mode"))) {
471 Some(mode) => mode,
472 None => return Err(err!(
473 "A presentation's mode is 'named' or 'pairwise'.";
474 Invalid, Input)),
475 };
476 let subject = match mode {
477 Mode::Named => {
478 res!(o.only(&NAMED_MEMBERS, &[]));
479 let id = res!(o.text("sub"));
480 if !is_key_id(&id) {
481 return Err(err!(
482 "A named presentation's sub is a key id of ten lowercase hex \
483 characters, not '{}'.", id;
484 Invalid, Input));
485 }
486 Subject::Named { id, key: res!(o.bytes::<KEY_LEN>("pub")) }
487 },
488 Mode::Pairwise => {
489 res!(o.only(&PAIRWISE_MEMBERS, &[]));
490 let p = res!(Obj::of(res!(o.must("proof")), "proof"));
491 res!(p.only(&PROOF_MEMBERS, &[]));
492 let body = res!(base64::decode_url(&res!(p.text("body"))));
493 if body.is_empty() {
494 return Err(err!("A proof's body is empty."; Invalid, Input, Missing));
495 }
496 Subject::Pairwise {
497 key: res!(o.bytes::<KEY_LEN>("sub")),
498 tag: res!(o.bytes::<KEY_LEN>("tag")),
499 proof: Proof { alg: res!(p.text("alg")), body },
500 }
501 },
502 };
503 Ok(Self {
504 rp_id: res!(o.text("rp_id")),
505 nonce: res!(o.bytes::<NONCE_LEN>("nonce")),
506 subject,
507 predicates: res!(o.words("predicates")),
508 head: res!(o.bytes::<32>("head")),
509 ts: res!(o.uint("ts")),
510 sig: res!(o.bytes::<SIG_LEN>("sig")),
511 })
512 }
513
514 pub fn parse(json: &str) -> Outcome<Self> {
515 Self::from_dat(&res!(decode_json(json, "presentation")))
516 }
517}
518
519
520// ── Head ────────────────────────────────────────────────────────────────────
521
522/// A signed point in a network's history that a presentation is made against:
523/// the member set's size and ring at a moment, under an issuer's key.
524#[derive(Clone, Debug, Eq, PartialEq)]
525pub struct Head {
526 pub id: [u8; 32], // SHA-256 of the signed bytes, on the wire as `head`
527 pub epoch: u64,
528 pub prev: Option<[u8; 32]>,
529 pub ts: u64,
530 pub salt: [u8; SALT_LEN],
531 pub members: u64,
532 pub ring_n: u64,
533 pub ring_digest: [u8; 32], // SHA-256 of the ring list
534 pub signer: [u8; KEY_LEN], // Ed25519
535 pub sig: [u8; SIG_LEN],
536}
537
538impl Head {
539
540 fn signed_map(&self) -> DaticleMap {
541 let mut m = DaticleMap::new();
542 m.insert(dat!("v"), dat!(V_HEAD));
543 m.insert(dat!("epoch"), Dat::U64(self.epoch));
544 m.insert(dat!("prev"), match &self.prev {
545 Some(prev) => dat!(base64::encode_url(prev)),
546 None => Dat::Empty,
547 });
548 m.insert(dat!("ts"), Dat::U64(self.ts));
549 m.insert(dat!("salt"), dat!(base64::encode_url(&self.salt)));
550 m.insert(dat!("members"), Dat::U64(self.members));
551 m.insert(dat!("ring_n"), Dat::U64(self.ring_n));
552 m.insert(dat!("ring_digest"), dat!(base64::encode_url(&self.ring_digest)));
553 m.insert(dat!("signer"), dat!(base64::encode_url(&self.signer)));
554 m
555 }
556
557 /// The bytes the signature covers and the id hashes: the canonical JSON of
558 /// every member but `head` and `sig`, with `prev` a present null when there
559 /// is none.
560 pub fn signed_bytes(&self) -> Outcome<Vec<u8>> {
561 Ok(res!(Dat::Map(self.signed_map()).json_canonical()).into_bytes())
562 }
563
564 /// The id this head's content earns, which its `id` must equal.
565 pub fn compute_id(&self) -> Outcome<[u8; 32]> {
566 Ok(sha256::digest(&res!(self.signed_bytes())))
567 }
568
569 pub fn to_dat(&self) -> Dat {
570 let mut m = self.signed_map();
571 m.insert(dat!("head"), dat!(base64::encode_url(&self.id)));
572 m.insert(dat!("sig"), dat!(base64::encode_url(&self.sig)));
573 Dat::Map(m)
574 }
575
576 pub fn to_json(&self) -> Outcome<String> {
577 self.to_dat().json_canonical()
578 }
579
580 pub fn from_dat(dat: &Dat) -> Outcome<Self> {
581 let o = res!(Obj::of(dat, "head"));
582 res!(o.only(&HEAD_MEMBERS, &[]));
583 res!(o.version(V_HEAD));
584 Ok(Self {
585 id: res!(o.bytes::<32>("head")),
586 epoch: res!(o.uint("epoch")),
587 prev: res!(o.bytes_or_null::<32>("prev")),
588 ts: res!(o.uint("ts")),
589 salt: res!(o.bytes::<SALT_LEN>("salt")),
590 members: res!(o.uint("members")),
591 ring_n: res!(o.uint("ring_n")),
592 ring_digest: res!(o.bytes::<32>("ring_digest")),
593 signer: res!(o.bytes::<KEY_LEN>("signer")),
594 sig: res!(o.bytes::<SIG_LEN>("sig")),
595 })
596 }
597
598 pub fn parse(json: &str) -> Outcome<Self> {
599 Self::from_dat(&res!(decode_json(json, "head")))
600 }
601}
602
603
604// ── Invoice and settlement ──────────────────────────────────────────────────
605
606/// A relying party's bill to a member, paid once through the network's ledger.
607#[derive(Clone, Debug, Eq, PartialEq)]
608pub struct Invoice {
609 pub rp_id: String,
610 pub payee: String, // key id of the name being paid
611 pub account: String, // key id of the account credited
612 pub amount: u64, // on the wire as `oxes`, the ledger's unit
613 pub memo: String,
614 pub nonce: [u8; INVOICE_NONCE_LEN],
615 pub expires: u64,
616 pub ts: u64,
617}
618
619impl Invoice {
620
621 /// Checks what the shape alone cannot. The memo is at most 64 characters
622 /// once trimmed and carries no control character.
623 pub fn check(&self) -> Outcome<()> {
624 res!(check_origin(&self.rp_id));
625 if !is_key_id(&self.payee) || !is_key_id(&self.account) {
626 return Err(err!(
627 "An invoice's payee and account are key ids of ten lowercase hex \
628 characters, not '{}' and '{}'.", self.payee, self.account;
629 Invalid, Input));
630 }
631 if self.amount == 0 || self.amount > MAX_INT {
632 return Err(err!(
633 "An invoice's amount is from 1 to 2^53 - 1, not {}.", self.amount;
634 Invalid, Input));
635 }
636 let chars = self.memo.trim().chars().count();
637 if chars > MEMO_MAX_CHARS || self.memo.chars().any(|c| c.is_control()) {
638 return Err(err!(
639 "An invoice's memo is at most {} characters once trimmed, with no control \
640 character; this one has {} characters.", MEMO_MAX_CHARS, chars;
641 Invalid, Input));
642 }
643 if self.expires < self.ts || self.expires - self.ts > INVOICE_LIFE {
644 return Err(err!(
645 "An invoice expires no earlier than it was issued and at most {} s after, \
646 not {} s from {} to {}.", INVOICE_LIFE,
647 self.expires as i128 - self.ts as i128, self.ts, self.expires;
648 Invalid, Input));
649 }
650 Ok(())
651 }
652
653 pub fn to_dat(&self) -> Dat {
654 mapdat!{
655 "v" => V_INVOICE,
656 "rp_id" => self.rp_id.clone(),
657 "payee" => self.payee.clone(),
658 "account" => self.account.clone(),
659 "oxes" => Dat::U64(self.amount),
660 "memo" => self.memo.clone(),
661 "nonce" => base64::encode_url(&self.nonce),
662 "expires" => Dat::U64(self.expires),
663 "ts" => Dat::U64(self.ts),
664 }
665 }
666
667 pub fn to_json(&self) -> Outcome<String> {
668 self.to_dat().json_canonical()
669 }
670
671 /// SHA-256 over the canonical JSON: what a payment's reference and a
672 /// settlement name.
673 pub fn id(&self) -> Outcome<[u8; 32]> {
674 Ok(sha256::digest(res!(self.to_json()).as_bytes()))
675 }
676
677 pub fn from_dat(dat: &Dat) -> Outcome<Self> {
678 let o = res!(Obj::of(dat, "invoice"));
679 res!(o.only(&INVOICE_MEMBERS, &[]));
680 res!(o.version(V_INVOICE));
681 let invoice = Self {
682 rp_id: res!(o.text("rp_id")),
683 payee: res!(o.text("payee")),
684 account: res!(o.text("account")),
685 amount: res!(o.uint("oxes")),
686 memo: res!(o.text("memo")),
687 nonce: res!(o.bytes::<INVOICE_NONCE_LEN>("nonce")),
688 expires: res!(o.uint("expires")),
689 ts: res!(o.uint("ts")),
690 };
691 res!(invoice.check());
692 Ok(invoice)
693 }
694
695 pub fn parse(json: &str) -> Outcome<Self> {
696 Self::from_dat(&res!(decode_json(json, "invoice")))
697 }
698}
699
700/// An issuer's signed word that an invoice was paid, naming no payer.
701#[derive(Clone, Debug, Eq, PartialEq)]
702pub struct Settlement {
703 pub invoice: [u8; 32], // the invoice's id
704 pub entry: [u8; 32], // the ledger entry that paid it
705 pub amount: u64, // on the wire as `oxes`
706 pub ts: u64,
707 pub signer: [u8; KEY_LEN], // Ed25519
708 pub sig: [u8; SIG_LEN],
709}
710
711impl Settlement {
712
713 fn signed_map(&self) -> DaticleMap {
714 let mut m = DaticleMap::new();
715 m.insert(dat!("v"), dat!(V_SETTLEMENT));
716 m.insert(dat!("invoice"), dat!(base64::encode_url(&self.invoice)));
717 m.insert(dat!("entry"), dat!(base64::encode_url(&self.entry)));
718 m.insert(dat!("oxes"), Dat::U64(self.amount));
719 m.insert(dat!("ts"), Dat::U64(self.ts));
720 m.insert(dat!("signer"), dat!(base64::encode_url(&self.signer)));
721 m
722 }
723
724 /// The bytes the signature covers: the canonical JSON of every member but
725 /// `sig`.
726 pub fn signed_bytes(&self) -> Outcome<Vec<u8>> {
727 Ok(res!(Dat::Map(self.signed_map()).json_canonical()).into_bytes())
728 }
729
730 pub fn to_dat(&self) -> Dat {
731 let mut m = self.signed_map();
732 m.insert(dat!("sig"), dat!(base64::encode_url(&self.sig)));
733 Dat::Map(m)
734 }
735
736 pub fn to_json(&self) -> Outcome<String> {
737 self.to_dat().json_canonical()
738 }
739
740 pub fn from_dat(dat: &Dat) -> Outcome<Self> {
741 let o = res!(Obj::of(dat, "settlement"));
742 res!(o.only(&SETTLEMENT_MEMBERS, &[]));
743 res!(o.version(V_SETTLEMENT));
744 Ok(Self {
745 invoice: res!(o.bytes::<32>("invoice")),
746 entry: res!(o.bytes::<32>("entry")),
747 amount: res!(o.uint("oxes")),
748 ts: res!(o.uint("ts")),
749 signer: res!(o.bytes::<KEY_LEN>("signer")),
750 sig: res!(o.bytes::<SIG_LEN>("sig")),
751 })
752 }
753
754 pub fn parse(json: &str) -> Outcome<Self> {
755 Self::from_dat(&res!(decode_json(json, "settlement")))
756 }
757}
758
759
760// ── Status ──────────────────────────────────────────────────────────────────
761
762/// What a named-mode status lookup reports: the key a name holds now, and
763/// whether it is live.
764#[derive(Clone, Debug, Eq, PartialEq)]
765pub struct Status {
766 pub key: [u8; KEY_LEN],
767 pub live: bool,
768}
769
770impl Status {
771
772 /// Reads `pub` and `live` from a status document and nothing else, since
773 /// the document carries display fields a verifier has no use for.
774 pub fn from_dat(dat: &Dat) -> Outcome<Self> {
775 let o = res!(Obj::of(dat, "status"));
776 Ok(Self {
777 key: res!(o.bytes::<KEY_LEN>("pub")),
778 live: res!(o.flag("live")),
779 })
780 }
781
782 pub fn parse(json: &str) -> Outcome<Self> {
783 Self::from_dat(&res!(decode_json(json, "status")))
784 }
785}
786
787
788// ── Reading ─────────────────────────────────────────────────────────────────
789
790/// Decodes a JSON document within the size and nesting these shapes need, as
791/// RFC 8259 JSON and nothing else: none of JDAT's typed, hex or unquoted forms.
792fn decode_json(json: &str, what: &str) -> Outcome<Dat> {
793 if json.len() > JSON_MAX_BYTES {
794 return Err(err!(
795 "A {} of {} bytes exceeds the {} bytes one may take.", what, json.len(),
796 JSON_MAX_BYTES;
797 Invalid, Input, Size));
798 }
799 Ok(res!(Dat::decode_json_strict(json, &DecodeLimits::new(JSON_MAX_DEPTH, JSON_MAX_BYTES))))
800}
801
802fn words_dat(words: &[String]) -> Dat {
803 Dat::List(words.iter().map(|w| dat!(w.clone())).collect())
804}
805
806fn check_words(words: &[String], what: &str) -> Outcome<()> {
807 if words.len() > WORDS_MAX {
808 return Err(err!(
809 "A {} carries {} predicates, and at most {} are allowed.", what, words.len(), WORDS_MAX;
810 Invalid, Input, Size));
811 }
812 for (i, w) in words.iter().enumerate() {
813 if !is_word(w) {
814 return Err(err!(
815 "A {}'s predicate '{}' is not a word of 1 to {} lowercase letters, digits \
816 and underscores.", what, w, WORD_MAX_CHARS;
817 Invalid, Input));
818 }
819 if words[..i].contains(w) {
820 return Err(err!(
821 "A {} names the predicate '{}' twice.", what, w;
822 Invalid, Input, Duplicate));
823 }
824 }
825 Ok(())
826}
827
828/// The members of one JSON object, read strictly.
829struct Obj<'a> {
830 map: &'a DaticleMap,
831 what: &'static str, // what the object is, for errors
832}
833
834impl<'a> Obj<'a> {
835
836 fn of(dat: &'a Dat, what: &'static str) -> Outcome<Self> {
837 match dat {
838 Dat::Map(map) => Ok(Self { map, what }),
839 other => Err(err!(
840 "A {} is a JSON object, not a {:?}.", what, other.kind();
841 Invalid, Input, Decode)),
842 }
843 }
844
845 /// Refuses any member outside `allowed`, save those in `ignored`.
846 fn only(&self, allowed: &[&str], ignored: &[&str]) -> Outcome<()> {
847 for key in self.map.keys() {
848 match key {
849 Dat::Str(s) if allowed.contains(&s.as_str()) || ignored.contains(&s.as_str()) => (),
850 other => return Err(err!(
851 "A {} has no member {:?}.", self.what, other;
852 Invalid, Input, Unknown)),
853 }
854 }
855 Ok(())
856 }
857
858 fn version(&self, v: &str) -> Outcome<()> {
859 let got = res!(self.text("v"));
860 if got != v {
861 return Err(err!(
862 "A {} carries v '{}', and this reads only '{}'.", self.what, got, v;
863 Invalid, Input, Version));
864 }
865 Ok(())
866 }
867
868 fn get(&self, key: &str) -> Option<&'a Dat> {
869 self.map.get(&dat!(key))
870 }
871
872 fn must(&self, key: &str) -> Outcome<&'a Dat> {
873 match self.get(key) {
874 Some(d) => Ok(d),
875 None => Err(err!(
876 "A {} lacks its '{}' member.", self.what, key;
877 Invalid, Input, Missing)),
878 }
879 }
880
881 fn text(&self, key: &str) -> Outcome<String> {
882 match res!(self.must(key)) {
883 Dat::Str(s) => Ok(s.clone()),
884 other => Err(err!(
885 "The '{}' of a {} is a string, not a {:?}.", key, self.what, other.kind();
886 Invalid, Input, Mismatch)),
887 }
888 }
889
890 fn flag(&self, key: &str) -> Outcome<bool> {
891 match res!(self.must(key)) {
892 Dat::Bool(b) => Ok(*b),
893 other => Err(err!(
894 "The '{}' of a {} is true or false, not a {:?}.", key, self.what, other.kind();
895 Invalid, Input, Mismatch)),
896 }
897 }
898
899 /// A non-negative integer no larger than 2^53 - 1, the largest a JSON
900 /// number carries exactly.
901 fn uint(&self, key: &str) -> Outcome<u64> {
902 let d = res!(self.must(key));
903 let n = match d {
904 Dat::U8(n) => *n as u64,
905 Dat::U16(n) => *n as u64,
906 Dat::U32(n) => *n as u64,
907 Dat::U64(n) => *n,
908 Dat::I8(n) if *n >= 0 => *n as u64,
909 Dat::I16(n) if *n >= 0 => *n as u64,
910 Dat::I32(n) if *n >= 0 => *n as u64,
911 Dat::I64(n) if *n >= 0 => *n as u64,
912 other => return Err(err!(
913 "The '{}' of a {} is a non-negative integer, not {:?}.", key, self.what, other;
914 Invalid, Input, Mismatch)),
915 };
916 if n > MAX_INT {
917 return Err(err!(
918 "The '{}' of a {} is {}, beyond 2^53 - 1, the largest integer JSON carries \
919 exactly.", key, self.what, n;
920 Invalid, Input, TooBig));
921 }
922 Ok(n)
923 }
924
925 /// Unpadded base64url of exactly `N` bytes.
926 fn bytes<const N: usize>(&self, key: &str) -> Outcome<[u8; N]> {
927 fixed::<N>(&res!(self.text(key)), key, self.what)
928 }
929
930 fn bytes_or_null<const N: usize>(&self, key: &str) -> Outcome<Option<[u8; N]>> {
931 match res!(self.must(key)) {
932 Dat::Empty => Ok(None),
933 Dat::Opt(inner) if inner.is_none() => Ok(None),
934 Dat::Str(s) => Ok(Some(res!(fixed::<N>(s, key, self.what)))),
935 other => Err(err!(
936 "The '{}' of a {} is base64url or null, not a {:?}.", key, self.what, other.kind();
937 Invalid, Input, Mismatch)),
938 }
939 }
940
941 fn words(&self, key: &str) -> Outcome<Vec<String>> {
942 let list = match res!(self.must(key)) {
943 Dat::List(list) => list,
944 other => return Err(err!(
945 "The '{}' of a {} is a list of words, not a {:?}.", key, self.what, other.kind();
946 Invalid, Input, Mismatch)),
947 };
948 let mut words = Vec::with_capacity(list.len());
949 for d in list {
950 match d {
951 Dat::Str(s) => words.push(s.clone()),
952 other => return Err(err!(
953 "The '{}' of a {} holds a {:?} where a word belongs.", key, self.what,
954 other.kind();
955 Invalid, Input, Mismatch)),
956 }
957 }
958 res!(check_words(&words, self.what));
959 Ok(words)
960 }
961}
962
963fn fixed<const N: usize>(b64: &str, key: &str, what: &str) -> Outcome<[u8; N]> {
964 let v = res!(base64::decode_url(b64));
965 if v.len() != N {
966 return Err(err!(
967 "The '{}' of a {} decodes to {} bytes, not {}.", key, what, v.len(), N;
968 Invalid, Input, Size));
969 }
970 let mut out = [0u8; N];
971 out.copy_from_slice(&v);
972 Ok(out)
973}