Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/tests/presentation.rs

48.9 KiB, 1 run

created by r1870400018:61160, 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//! `presentation` from outside: a fixture signed by a real WebCrypto Ed25519
2//! key in headless Chromium (`tools/presentation_fixture.cjs`), the refusal
3//! each attack earns, and the module's silence about any network by name.
4//!
5//! The verifier's guards are proven load-bearing inside the module, where a
6//! test can switch one off; see `src/presentation/verify.rs`.
7
8use oxedyne_fe2o3_core::prelude::*;
9use oxedyne_fe2o3_crypto::{
10 linkring::{
11 self,
12 Ring,
13 SecretKey,
14 },
15 sign::SignatureScheme,
16};
17use oxedyne_fe2o3_hash::sha256;
18use oxedyne_fe2o3_iop_crypto::{
19 keys::KeyManager,
20 sign::Signer,
21};
22use oxedyne_fe2o3_jdat::prelude::*;
23use oxedyne_fe2o3_net::presentation::{
24 shape::{
25 self,
26 Accept,
27 Head,
28 Invoice,
29 Presentation,
30 Proof,
31 Request,
32 Settlement,
33 Status,
34 Subject,
35 HEAD_LEAD,
36 T_G,
37 },
38 verify::{
39 Lookup,
40 Refusal,
41 Verdict,
42 Verified,
43 Verifier,
44 },
45};
46use oxedyne_fe2o3_text::base64;
47
48use std::{
49 cell::RefCell,
50 collections::HashMap,
51 sync::Arc,
52};
53
54const NOW: u64 = 1_800_000_000;
55const APP: &str = "https://app.example";
56const OTHER:&str = "https://other.example";
57
58// ── Fixtures ────────────────────────────────────────────────────────────────
59
60struct Ed {
61 scheme: SignatureScheme,
62 public: [u8; 32],
63}
64
65impl Ed {
66 fn new() -> Outcome<Self> {
67 let scheme = SignatureScheme::new_ed25519();
68 let mut public = [0u8; 32];
69 match res!(scheme.get_public_key()) {
70 Some(pk) => public.copy_from_slice(pk),
71 None => return Err(err!("A new Ed25519 key has no public half."; Test, Missing)),
72 }
73 Ok(Self { scheme, public })
74 }
75
76 fn sign(&self, msg: &[u8]) -> Outcome<[u8; 64]> {
77 let sig = res!(self.scheme.sign(msg));
78 let mut out = [0u8; 64];
79 out.copy_from_slice(&sig);
80 Ok(out)
81 }
82}
83
84#[derive(Default)]
85struct Book {
86 heads: RefCell<HashMap<[u8; 32], Head>>,
87 rings: RefCell<HashMap<[u8; 32], Arc<Ring>>>, // by head id
88 statuses: RefCell<HashMap<String, Status>>,
89}
90
91impl Lookup for Book {
92 fn head(&self, id: &[u8; 32]) -> Outcome<Option<Head>> {
93 Ok(self.heads.borrow().get(id).cloned())
94 }
95 fn ring(&self, head: &Head) -> Outcome<Option<Arc<Ring>>> {
96 Ok(self.rings.borrow().get(&head.id).cloned())
97 }
98 fn status(&self, id: &str) -> Outcome<Option<Status>> {
99 Ok(self.statuses.borrow().get(id).cloned())
100 }
101}
102
103fn ring_of(keys: &[SecretKey]) -> Outcome<Arc<Ring>> {
104 let mut list: Vec<[u8; 32]> = keys.iter().map(|k| k.public_key()).collect();
105 list.sort(); // The ring list is sorted ascending by its keys' bytes.
106 Ok(Arc::new(res!(Ring::from_keys(&list))))
107}
108
109/// A head at `ts` over `ring`, signed by `signer` and filed in `book`.
110fn mint(book: &Book, signer: &Ed, ring: &Arc<Ring>, ts: u64) -> Outcome<Head> {
111 let mut head = Head {
112 id: [0u8; 32],
113 epoch: 1,
114 prev: None,
115 ts,
116 salt: [9u8; 16],
117 members: ring.len() as u64,
118 ring_n: ring.len() as u64,
119 ring_digest: *ring.digest(),
120 signer: signer.public,
121 sig: [0u8; 64],
122 };
123 head.sig = res!(signer.sign(&res!(head.signed_bytes())));
124 head.id = res!(head.compute_id());
125 book.rings.borrow_mut().insert(head.id, ring.clone());
126 book.heads.borrow_mut().insert(head.id, head.clone());
127 Ok(head)
128}
129
130struct World {
131 peer: Ed,
132 member: Ed, // a name's key
133 keys: Vec<SecretKey>,
134 ring: Arc<Ring>,
135 head: Head,
136}
137
138fn world() -> Outcome<(World, Book)> {
139 let book = Book::default();
140 let peer = res!(Ed::new());
141 let member = res!(Ed::new());
142 let mut keys = Vec::new();
143 for _ in 0..7 {
144 keys.push(res!(SecretKey::random()));
145 }
146 let ring = res!(ring_of(&keys));
147 let head = res!(mint(&book, &peer, &ring, NOW - 20));
148 book.statuses.borrow_mut().insert(
149 shape::key_id(&member.public), Status { key: member.public, live: true });
150 Ok((World { peer, member, keys, ring, head }, book))
151}
152
153fn verifier(w: &World, book: Book) -> Outcome<Verifier<Book>> {
154 Verifier::new(APP, vec![w.peer.public], book, 2)
155}
156
157/// A named presentation answering `req`, signed by `member`.
158fn named(req: &Request, member: &Ed, head: &Head) -> Outcome<Presentation> {
159 let mut p = Presentation {
160 rp_id: req.rp_id.clone(),
161 nonce: req.nonce,
162 subject: Subject::Named { id: shape::key_id(&member.public), key: member.public },
163 predicates: req.predicates.clone(),
164 head: head.id,
165 ts: NOW,
166 sig: [0u8; 64],
167 };
168 p.sig = res!(member.sign(&res!(p.signed_bytes())));
169 Ok(p)
170}
171
172/// A pairwise presentation answering `req`: pseudonym key `sub`, ring key
173/// `key`, proof over `ring` under `rp_id`'s scope.
174fn pairwise(
175 req: &Request,
176 sub: &Ed,
177 key: &SecretKey,
178 ring: &Ring,
179 head: &Head,
180 rp_id: &str,
181)
182 -> Outcome<Presentation>
183{
184 let scope = shape::scope(rp_id);
185 let mut p = Presentation {
186 rp_id: req.rp_id.clone(),
187 nonce: req.nonce,
188 subject: Subject::Pairwise {
189 key: sub.public,
190 tag: res!(linkring::tag(key, &scope)),
191 proof: Proof { alg: linkring::ALG.to_string(), body: Vec::new() },
192 },
193 predicates: req.predicates.clone(),
194 head: head.id,
195 ts: NOW,
196 sig: [0u8; 64],
197 };
198 res!(prove(&mut p, sub, key, ring, &scope));
199 Ok(p)
200}
201
202/// Makes the ring proof and the signature afresh over `p` as it now stands.
203fn prove(p: &mut Presentation, sub: &Ed, key: &SecretKey, ring: &Ring, scope: &[u8]) -> Outcome<()> {
204 let msg = res!(p.signed_bytes());
205 let (tag, body) = res!(linkring::sign(ring, key, scope, &msg));
206 if let Subject::Pairwise { tag: t, proof, .. } = &mut p.subject {
207 req!(*t, tag, "the proof's tag is the tag the body carries");
208 proof.body = body;
209 }
210 p.sig = res!(sub.sign(&msg));
211 Ok(())
212}
213
214/// Signs `p` again with `key`, after a change to what the signature covers.
215fn resign(p: &mut Presentation, key: &Ed) -> Outcome<()> {
216 p.sig = res!(key.sign(&res!(p.signed_bytes())));
217 Ok(())
218}
219
220fn body(p: &Presentation) -> Outcome<Vec<u8>> {
221 Ok(res!(p.to_json()).into_bytes())
222}
223
224fn refusal(v: Outcome<Verdict>) -> Outcome<Option<Refusal>> {
225 Ok(res!(v).refusal())
226}
227
228// ── Acceptance ──────────────────────────────────────────────────────────────
229
230#[test]
231fn a_named_presentation_verifies() -> Outcome<()> {
232 let (w, book) = res!(world());
233 let v = res!(verifier(&w, book));
234 let req = res!(v.issue(b"session", Accept::PairwiseOrNamed, &["adult"], None, None, NOW));
235 let p = res!(named(&req, &w.member, &w.head));
236 match res!(v.verify(b"session", &res!(body(&p)), NOW)) {
237 Verdict::Accepted(Verified::Named { id, key, predicates }) => {
238 req!(id, shape::key_id(&w.member.public));
239 req!(key, w.member.public);
240 req!(predicates, vec!["adult".to_string()]);
241 },
242 other => return Err(err!("A sound named presentation earned {:?}.", other; Test)),
243 }
244 Ok(())
245}
246
247/// Chromium made both keys, signed the head and the presentation over its own
248/// canonical JSON, and serialised the presentation with its members in the
249/// order it built them. The verifier accepts it only by rebuilding the
250/// browser's bytes and verifying the browser's Ed25519 signatures.
251#[test]
252fn a_named_presentation_signed_in_webcrypto_verifies() -> Outcome<()> {
253 let fx = res!(Dat::decode_string(include_str!("data/presentation_webcrypto.json")));
254 let now = res!(fx.map_get_u64(&dat!("now")));
255 let req = res!(Request::from_dat(res!(fx.map_get_must(&dat!("request")))));
256 let head = res!(Head::from_dat(res!(fx.map_get_must(&dat!("head")))));
257 let status = res!(Status::from_dat(res!(fx.map_get_must(&dat!("status")))));
258 let text = res!(fx.map_get_string(&dat!("presentation")));
259 let p = res!(Presentation::parse(&text));
260 req!(res!(fx.map_get_string(&dat!("rp_id"))), APP.to_string());
261 req!(head.ring_n, 0, "a named presentation needs no ring");
262
263 let book = Book::default();
264 book.heads.borrow_mut().insert(head.id, head.clone());
265 let id = match &p.subject {
266 Subject::Named { id, .. } => id.clone(),
267 other => return Err(err!("The fixture is named, not {:?}.", other; Test)),
268 };
269 book.statuses.borrow_mut().insert(id.clone(), status);
270 let v = res!(Verifier::new(APP, vec![head.signer], book, 1));
271 match res!(v.check_issued(&req, &p, now)) {
272 Verdict::Accepted(Verified::Named { id: got, .. }) => req!(got, id),
273 other => return Err(err!("The WebCrypto presentation earned {:?}.", other; Test)),
274 }
275
276 // One byte of the browser's signature changed, and it no longer verifies.
277 let mut bad = p.clone();
278 bad.sig[10] ^= 0x01;
279 let got = res!(refusal(v.check_issued(&req, &bad, now)));
280 req!(got, Some(Refusal::BadSignature));
281 Ok(())
282}
283
284/// A pairwise presentation over the whole ring at the head verifies, and its
285/// tag is the member's `linkring` tag under this origin's scope, whatever
286/// pseudonym key presents it.
287#[test]
288fn a_pairwise_presentation_over_the_whole_ring_verifies() -> Outcome<()> {
289 let (w, book) = res!(world());
290 let v = res!(verifier(&w, book));
291 let want = res!(linkring::tag(&w.keys[3], &shape::scope(APP)));
292 for _ in 0..2 {
293 let sub = res!(Ed::new());
294 let req = res!(v.issue(b"session", Accept::Pairwise, &[], None, None, NOW));
295 let p = res!(pairwise(&req, &sub, &w.keys[3], &w.ring, &w.head, APP));
296 match res!(v.verify(b"session", &res!(body(&p)), NOW)) {
297 Verdict::Accepted(Verified::Pairwise { key, tag, .. }) => {
298 req!(key, sub.public);
299 req!(tag, want, "one human, one tag at one relying party");
300 },
301 other => return Err(err!("A sound pairwise presentation earned {:?}.", other; Test)),
302 }
303 }
304 Ok(())
305}
306
307// ── Audience, nonce and freshness ───────────────────────────────────────────
308
309/// A presentation made for one relying party is refused at another, whether
310/// it arrives as it was made or with its origin rewritten.
311#[test]
312fn a_cross_origin_replay_is_refused() -> Outcome<()> {
313 let (w, book) = res!(world());
314 let other_book = Book::default();
315 other_book.heads.borrow_mut().insert(w.head.id, w.head.clone());
316 other_book.rings.borrow_mut().insert(w.head.id, w.ring.clone());
317 other_book.statuses.borrow_mut().insert(
318 shape::key_id(&w.member.public), Status { key: w.member.public, live: true });
319 let a = res!(verifier(&w, book));
320 let b = res!(Verifier::new(OTHER, vec![w.peer.public], other_book, 1));
321
322 let req = res!(a.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
323 let p = res!(named(&req, &w.member, &w.head));
324 let got = res!(refusal(b.verify(b"s", &res!(body(&p)), NOW)));
325 req!(got, Some(Refusal::WrongAudience));
326 let mut moved = p.clone();
327 moved.rp_id = OTHER.to_string();
328 let got = res!(refusal(b.verify(b"s", &res!(body(&moved)), NOW)));
329 req!(got, Some(Refusal::UnknownNonce));
330
331 // The same for a pairwise presentation, which carries A's tag.
332 let sub = res!(Ed::new());
333 let req = res!(a.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
334 let p = res!(pairwise(&req, &sub, &w.keys[0], &w.ring, &w.head, APP));
335 let got = res!(refusal(b.verify(b"s", &res!(body(&p)), NOW)));
336 req!(got, Some(Refusal::WrongAudience));
337
338 // And A still accepts what was made for it.
339 let got = res!(refusal(a.verify(b"s", &res!(body(&p)), NOW)));
340 req!(got, None::<Refusal>);
341 Ok(())
342}
343
344#[test]
345fn an_unknown_nonce_is_refused() -> Outcome<()> {
346 let (w, book) = res!(world());
347 let v = res!(verifier(&w, book));
348 // Never issued.
349 let fake = Request {
350 rp_id: APP.to_string(),
351 nonce: [5u8; 32],
352 accept: Accept::PairwiseOrNamed,
353 predicates: vec![],
354 invoice: None,
355 return_to: None,
356 exp: NOW + T_G,
357 };
358 let p = res!(named(&fake, &w.member, &w.head));
359 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
360 req!(got, Some(Refusal::UnknownNonce));
361 // Issued to another session.
362 let req = res!(v.issue(b"alice", Accept::PairwiseOrNamed, &[], None, None, NOW));
363 let p = res!(named(&req, &w.member, &w.head));
364 let got = res!(refusal(v.verify(b"mallory", &res!(body(&p)), NOW)));
365 req!(got, Some(Refusal::UnknownNonce));
366 // Past its request's expiry, though the presentation itself is fresh.
367 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW - T_G - 1));
368 let p = res!(named(&req, &w.member, &w.head));
369 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
370 req!(got, Some(Refusal::UnknownNonce));
371 Ok(())
372}
373
374/// A nonce is spent by the first presentation to reach it, even one that
375/// then fails, so nothing presented a second time is accepted.
376#[test]
377fn a_nonce_spent_twice_is_refused() -> Outcome<()> {
378 let (w, book) = res!(world());
379 let v = res!(verifier(&w, book));
380 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
381 let p = res!(named(&req, &w.member, &w.head));
382 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
383 req!(got, None::<Refusal>);
384 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
385 req!(got, Some(Refusal::Replayed));
386 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW + 100)));
387 req!(got, Some(Refusal::Replayed));
388
389 // Spent by a first attempt that failed on its signature.
390 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
391 let p = res!(named(&req, &w.member, &w.head));
392 let mut spoilt = p.clone();
393 spoilt.sig[0] ^= 0x01;
394 let got = res!(refusal(v.verify(b"s", &res!(body(&spoilt)), NOW)));
395 req!(got, Some(Refusal::BadSignature));
396 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
397 req!(got, Some(Refusal::Replayed));
398 Ok(())
399}
400
401/// A nonce stays spent for as long as its challenge stands, whatever the
402/// caller's clock does in between. Here the clock steps back 100 s between the
403/// issue and the first showing, and the replay comes 250 s after the issue,
404/// while the challenge still stands.
405#[test]
406fn a_replay_is_refused_after_the_clock_steps_back() -> Outcome<()> {
407 let (w, book) = res!(world());
408 let v = res!(verifier(&w, book));
409 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
410 let p = res!(named(&req, &w.member, &w.head));
411 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW - 100)));
412 req!(got, None::<Refusal>, "the first showing passes");
413 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW + 250)));
414 req!(got, Some(Refusal::Replayed));
415 Ok(())
416}
417
418/// A presentation shown in the wrong session is refused without spending the
419/// nonce, so whoever intercepts one cannot spoil it for the session it was
420/// made for.
421#[test]
422fn a_showing_in_another_session_spends_nothing() -> Outcome<()> {
423 let (w, book) = res!(world());
424 let v = res!(verifier(&w, book));
425 let req = res!(v.issue(b"alice", Accept::PairwiseOrNamed, &[], None, None, NOW));
426 let p = res!(named(&req, &w.member, &w.head));
427 let got = res!(refusal(v.verify(b"mallory", &res!(body(&p)), NOW)));
428 req!(got, Some(Refusal::UnknownNonce));
429 let got = res!(refusal(v.verify(b"alice", &res!(body(&p)), NOW)));
430 req!(got, None::<Refusal>);
431 Ok(())
432}
433
434/// An empty session is refused at issue and at verification, since it would
435/// bind a challenge to every browser that sends none. The refusal spends
436/// nothing.
437#[test]
438fn an_empty_session_is_refused() -> Outcome<()> {
439 let (w, book) = res!(world());
440 let v = res!(verifier(&w, book));
441 let issued = v.issue(b"", Accept::PairwiseOrNamed, &[], None, None, NOW).is_err();
442 req!(issued, true, "an issue to an empty session");
443 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
444 let p = res!(named(&req, &w.member, &w.head));
445 let verified = v.verify(b"", &res!(body(&p)), NOW).is_err();
446 req!(verified, true, "a verification in an empty session");
447 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
448 req!(got, None::<Refusal>, "the session it was issued to");
449 Ok(())
450}
451
452/// A session that fills the challenge store does not shut another out: the
453/// challenge nearest its lapse makes room, and it alone is lost.
454#[test]
455fn a_full_challenge_store_still_serves_another_session() -> Outcome<()> {
456 let (w, book) = res!(world());
457 let v = res!(verifier(&w, book)).with_max_issued(4);
458 let mut flood = Vec::new();
459 for i in 0..4 {
460 flood.push(res!(v.issue(b"flood", Accept::PairwiseOrNamed, &[], None, None, NOW - 4 + i)));
461 }
462 let req = res!(v.issue(b"alice", Accept::PairwiseOrNamed, &[], None, None, NOW));
463 let p = res!(named(&req, &w.member, &w.head));
464 let got = res!(refusal(v.verify(b"alice", &res!(body(&p)), NOW)));
465 req!(got, None::<Refusal>, "the other session");
466 let p = res!(named(&flood[0], &w.member, &w.head));
467 let got = res!(refusal(v.verify(b"flood", &res!(body(&p)), NOW)));
468 req!(got, Some(Refusal::UnknownNonce), "the challenge that made room");
469 let p = res!(named(&flood[1], &w.member, &w.head));
470 let got = res!(refusal(v.verify(b"flood", &res!(body(&p)), NOW)));
471 req!(got, None::<Refusal>, "the flood's later challenges still stand");
472 Ok(())
473}
474
475/// One session holds a bounded number of challenges: another beyond the bound
476/// lets that session's own soonest to lapse go, and no other session's.
477#[test]
478fn a_session_holds_a_bounded_number_of_challenges() -> Outcome<()> {
479 let (w, book) = res!(world());
480 let v = res!(verifier(&w, book)).with_max_per_session(2);
481 let other = res!(v.issue(b"other", Accept::PairwiseOrNamed, &[], None, None, NOW - 5));
482 let mut own = Vec::new();
483 for i in 0..3 {
484 own.push(res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW - 2 + i)));
485 }
486 for (what, session, req, want) in [
487 ("the session's first, let go", b"s".as_slice(), &own[0], Some(Refusal::UnknownNonce)),
488 ("the session's second", b"s".as_slice(), &own[1], None),
489 ("the session's third", b"s".as_slice(), &own[2], None),
490 ("the other session's, older", b"other".as_slice(), &other, None),
491 ] {
492 let p = res!(named(req, &w.member, &w.head));
493 let got = res!(refusal(v.verify(session, &res!(body(&p)), NOW)));
494 req!(got, want, "{}", what);
495 }
496 Ok(())
497}
498
499/// A caller that keeps its own store of challenges still has the lapse of the
500/// request asserted, as well as its audience and nonce.
501#[test]
502fn check_issued_refuses_a_lapsed_request() -> Outcome<()> {
503 let (w, book) = res!(world());
504 let v = res!(verifier(&w, book));
505 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW - T_G - 1));
506 let p = res!(named(&req, &w.member, &w.head));
507 let got = res!(refusal(v.check_issued(&req, &p, NOW)));
508 req!(got, Some(Refusal::UnknownNonce), "lapsed at {}", req.exp);
509 let got = res!(refusal(v.check_issued(&req, &p, req.exp)));
510 req!(got, None::<Refusal>, "still standing at {}", req.exp);
511 Ok(())
512}
513
514/// A presentation whose `ts` is more than T_G from now is stale, on either
515/// side, and both bounds are exact. Each is made against a head no later than
516/// HEAD_LEAD after its `ts`, so only the presentation's own clock is at issue.
517#[test]
518fn a_stale_presentation_is_refused() -> Outcome<()> {
519 let (w, book) = res!(world());
520 let v = res!(verifier(&w, book));
521 for (ts, want) in [
522 (NOW - T_G - 1, Some(Refusal::Stale)),
523 (NOW - T_G, None),
524 (NOW + T_G, None),
525 (NOW + T_G + 1, Some(Refusal::Stale)),
526 ] {
527 let head = res!(mint(v.lookup(), &w.peer, &w.ring, ts.min(NOW)));
528 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
529 let mut p = res!(named(&req, &w.member, &head));
530 p.ts = ts;
531 res!(resign(&mut p, &w.member));
532 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
533 req!(got, want, "ts {}", ts);
534 }
535 Ok(())
536}
537
538/// A head older than T_G is stale, and so is one minted more than HEAD_LEAD
539/// after the presentation that names it; both bounds are exact.
540#[test]
541fn a_stale_head_is_refused() -> Outcome<()> {
542 let (w, book) = res!(world());
543 let v = res!(verifier(&w, book));
544 for (head_ts, want) in [
545 (NOW - T_G - 1, Some(Refusal::StaleHead)),
546 (NOW - T_G, None),
547 (NOW + HEAD_LEAD + 1, Some(Refusal::StaleHead)),
548 (NOW + HEAD_LEAD, None),
549 ] {
550 let head = res!(mint(v.lookup(), &w.peer, &w.ring, head_ts));
551 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
552 let p = res!(named(&req, &w.member, &head));
553 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
554 req!(got, want, "head at {}", head_ts);
555 }
556 Ok(())
557}
558
559// ── Modes and predicates ────────────────────────────────────────────────────
560
561#[test]
562fn the_request_decides_mode_and_predicates() -> Outcome<()> {
563 let (w, book) = res!(world());
564 let v = res!(verifier(&w, book));
565 // A request that accepts pairwise only.
566 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
567 let p = res!(named(&req, &w.member, &w.head));
568 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
569 req!(got, Some(Refusal::ModeNotAccepted));
570 // A predicate the request did not ask for.
571 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &["adult"], None, None, NOW));
572 let mut p = res!(named(&req, &w.member, &w.head));
573 p.predicates = vec!["adult".to_string(), "established".to_string()];
574 res!(resign(&mut p, &w.member));
575 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
576 req!(got, Some(Refusal::PredicateNotOffered));
577 // Fewer than were asked is the member's choice.
578 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &["adult"], None, None, NOW));
579 let mut p = res!(named(&req, &w.member, &w.head));
580 p.predicates = vec![];
581 res!(resign(&mut p, &w.member));
582 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
583 req!(got, None::<Refusal>);
584 Ok(())
585}
586
587// ── Tampering ───────────────────────────────────────────────────────────────
588
589/// Each part of a pairwise presentation changed, by a member who can re-sign
590/// with the pseudonym key but cannot remake another's ring proof, is refused.
591#[test]
592fn each_pairwise_tamper_is_refused() -> Outcome<()> {
593 let (w, book) = res!(world());
594 let v = res!(verifier(&w, book));
595 let sub = res!(Ed::new());
596 let fresh = |v: &Verifier<Book>| v.issue(b"s", Accept::Pairwise, &[], None, None, NOW);
597
598 // Another member's tag, re-signed.
599 let req = res!(fresh(&v));
600 let mut p = res!(pairwise(&req, &sub, &w.keys[1], &w.ring, &w.head, APP));
601 if let Subject::Pairwise { tag, .. } = &mut p.subject {
602 *tag = res!(linkring::tag(&w.keys[2], &shape::scope(APP)));
603 }
604 res!(resign(&mut p, &sub));
605 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
606 req!(got, Some(Refusal::BadProof), "tag");
607
608 // One bit of the proof body.
609 let req = res!(fresh(&v));
610 let mut p = res!(pairwise(&req, &sub, &w.keys[1], &w.ring, &w.head, APP));
611 if let Subject::Pairwise { proof, .. } = &mut p.subject {
612 proof.body[40] ^= 0x01;
613 }
614 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
615 req!(got, Some(Refusal::BadProof), "body");
616
617 // An algorithm this verifier does not hold.
618 let req = res!(fresh(&v));
619 let mut p = res!(pairwise(&req, &sub, &w.keys[1], &w.ring, &w.head, APP));
620 if let Subject::Pairwise { proof, .. } = &mut p.subject {
621 proof.alg = "linkring/2".to_string();
622 }
623 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
624 req!(got, Some(Refusal::BadProof), "alg");
625
626 // The message: a proof over one body, the signature over another.
627 let req = res!(fresh(&v));
628 let mut p = res!(pairwise(&req, &sub, &w.keys[1], &w.ring, &w.head, APP));
629 p.ts = NOW - 1;
630 res!(resign(&mut p, &sub));
631 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
632 req!(got, Some(Refusal::BadProof), "message");
633
634 // The scope: a proof and tag made under another origin, presented here.
635 let req = res!(fresh(&v));
636 let p = res!(pairwise(&req, &sub, &w.keys[1], &w.ring, &w.head, OTHER));
637 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
638 req!(got, Some(Refusal::BadProof), "scope");
639
640 // The ring: a proof over a ring with one key dropped, which is not the
641 // whole ring at the head.
642 let req = res!(fresh(&v));
643 let short = res!(ring_of(&w.keys[..6]));
644 let p = res!(pairwise(&req, &sub, &w.keys[1], &short, &w.head, APP));
645 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
646 req!(got, Some(Refusal::BadProof), "ring");
647
648 // A ring served for the head that is not the head's: the attacker's own
649 // two keys, with a sound proof over them. Only the check of the ring
650 // against the head's length and digest stands in the way.
651 let req = res!(fresh(&v));
652 let own = vec![res!(SecretKey::random()), res!(SecretKey::random())];
653 let own_ring = res!(ring_of(&own));
654 let p = res!(pairwise(&req, &sub, &own[0], &own_ring, &w.head, APP));
655 v.lookup().rings.borrow_mut().insert(w.head.id, own_ring.clone());
656 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
657 req!(got, Some(Refusal::BadProof), "served ring");
658 v.lookup().rings.borrow_mut().insert(w.head.id, w.ring.clone());
659
660 // No ring at all for the head.
661 let req = res!(fresh(&v));
662 let p = res!(pairwise(&req, &sub, &w.keys[1], &w.ring, &w.head, APP));
663 v.lookup().rings.borrow_mut().remove(&w.head.id);
664 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
665 req!(got, Some(Refusal::BadProof), "no ring");
666 Ok(())
667}
668
669/// A pseudonym key of small order, whose "signature" anyone can make, is
670/// refused however sound the ring proof beside it. Accepting it would let
671/// anyone answer a later challenge made to that key alone.
672#[test]
673fn a_small_order_pseudonym_key_is_refused() -> Outcome<()> {
674 let (w, book) = res!(world());
675 let v = res!(verifier(&w, book));
676 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
677 let sub = res!(Ed::new());
678 let mut p = res!(pairwise(&req, &sub, &w.keys[4], &w.ring, &w.head, APP));
679 let mut identity = [0u8; 32];
680 identity[0] = 0x01;
681 if let Subject::Pairwise { key, .. } = &mut p.subject {
682 *key = identity;
683 }
684 // Remake the proof over the new body, then forge the Ed25519 half.
685 let scope = shape::scope(APP);
686 let msg = res!(p.signed_bytes());
687 let (_, proof_body) = res!(linkring::sign(&w.ring, &w.keys[4], &scope, &msg));
688 if let Subject::Pairwise { proof, .. } = &mut p.subject {
689 proof.body = proof_body;
690 }
691 let mut forged = [0u8; 64];
692 forged[0] = 0x01;
693 p.sig = forged;
694 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
695 req!(got, Some(Refusal::BadSignature));
696 Ok(())
697}
698
699#[test]
700fn each_named_tamper_is_refused() -> Outcome<()> {
701 let (w, book) = res!(world());
702 let v = res!(verifier(&w, book));
703 let fresh = |v: &Verifier<Book>| v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW);
704
705 // A name that is not its key's id, re-signed.
706 let req = res!(fresh(&v));
707 let mut p = res!(named(&req, &w.member, &w.head));
708 if let Subject::Named { id, .. } = &mut p.subject {
709 *id = "0123456789".to_string();
710 }
711 res!(resign(&mut p, &w.member));
712 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
713 req!(got, Some(Refusal::BadSignature), "sub");
714
715 // Signed by a key other than the one presented.
716 let req = res!(fresh(&v));
717 let mut p = res!(named(&req, &w.member, &w.head));
718 let other = res!(Ed::new());
719 res!(resign(&mut p, &other));
720 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
721 req!(got, Some(Refusal::BadSignature), "key");
722
723 // A name no longer live, one that holds another key now, and one unknown.
724 let id = shape::key_id(&w.member.public);
725 for status in [
726 Some(Status { key: w.member.public, live: false }),
727 Some(Status { key: other.public, live: true }),
728 None,
729 ] {
730 match &status {
731 Some(s) => { v.lookup().statuses.borrow_mut().insert(id.clone(), s.clone()); },
732 None => { v.lookup().statuses.borrow_mut().remove(&id); },
733 }
734 let req = res!(fresh(&v));
735 let p = res!(named(&req, &w.member, &w.head));
736 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
737 req!(got, Some(Refusal::NotLive),
738 "status {:?}", status);
739 }
740 Ok(())
741}
742
743#[test]
744fn each_head_tamper_is_refused() -> Outcome<()> {
745 let (w, book) = res!(world());
746 let v = res!(verifier(&w, book));
747 let fresh = |v: &Verifier<Book>| v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW);
748
749 // A head whose content no longer hashes to its id.
750 let mut altered = w.head.clone();
751 altered.members += 1;
752 v.lookup().heads.borrow_mut().insert(w.head.id, altered);
753 let req = res!(fresh(&v));
754 let p = res!(named(&req, &w.member, &w.head));
755 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
756 req!(got, Some(Refusal::UnknownHead), "content");
757
758 // A head whose signature fails.
759 let mut spoilt = w.head.clone();
760 spoilt.sig[5] ^= 0x01;
761 v.lookup().heads.borrow_mut().insert(w.head.id, spoilt);
762 let req = res!(fresh(&v));
763 let p = res!(named(&req, &w.member, &w.head));
764 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
765 req!(got, Some(Refusal::UnknownHead), "sig");
766
767 // A head minted by a key outside the issuers.
768 let forger = res!(Ed::new());
769 let forged = res!(mint(v.lookup(), &forger, &w.ring, NOW - 20));
770 let req = res!(fresh(&v));
771 let p = res!(named(&req, &w.member, &forged));
772 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
773 req!(got, Some(Refusal::UnknownHead), "issuer");
774
775 // A head nobody can fetch.
776 v.lookup().heads.borrow_mut().remove(&w.head.id);
777 let req = res!(fresh(&v));
778 let p = res!(named(&req, &w.member, &w.head));
779 let got = res!(refusal(v.verify(b"s", &res!(body(&p)), NOW)));
780 req!(got, Some(Refusal::UnknownHead), "missing");
781 Ok(())
782}
783
784// ── Shapes ──────────────────────────────────────────────────────────────────
785
786/// Every departure from the shape is `malformed`, whatever else is right.
787#[test]
788fn malformed_presentations_are_refused() -> Outcome<()> {
789 let (w, book) = res!(world());
790 let v = res!(verifier(&w, book));
791 let sub = res!(Ed::new());
792 let named_req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
793 let named_text = res!(res!(named(&named_req, &w.member, &w.head)).to_json());
794 let pair_req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
795 let pair_text = res!(res!(pairwise(&pair_req, &sub, &w.keys[0], &w.ring, &w.head, APP)).to_json());
796 let pub_b64 = base64::encode_url(&w.member.public);
797
798 let edits: Vec<(&str, String)> = vec![
799 ("not JSON", "{\"v\":".to_string()),
800 ("not an object", "[1,2,3]".to_string()),
801 ("another version", named_text.replace("\"present/1\"", "\"present/2\"")),
802 ("another mode", named_text.replace("\"named\"", "\"public\"")),
803 ("an unknown member", named_text.replace("{", "{\"extra\":1,")),
804 ("a missing member", named_text.replace(&fmt!("\"pub\":\"{}\",", pub_b64), "")),
805 ("padded base64url", named_text.replace(&fmt!("\"{}\"", pub_b64), &fmt!("\"{}=\"", pub_b64))),
806 ("a short key", named_text.replace(&fmt!("\"{}\"", pub_b64), "\"AAAA\"")),
807 ("a float ts", named_text.replace(&fmt!("\"ts\":{}", NOW), &fmt!("\"ts\":{}.5", NOW))),
808 ("a negative ts", named_text.replace(&fmt!("\"ts\":{}", NOW), "\"ts\":-1")),
809 ("a ts beyond 2^53", named_text.replace(&fmt!("\"ts\":{}", NOW), "\"ts\":9007199254740992")),
810 ("a string ts", named_text.replace(&fmt!("\"ts\":{}", NOW), &fmt!("\"ts\":\"{}\"", NOW))),
811 ("a repeated word", named_text.replace("\"predicates\":[]", "\"predicates\":[\"adult\",\"adult\"]")),
812 ("a word not a word", named_text.replace("\"predicates\":[]", "\"predicates\":[\"Adult\"]")),
813 ("a named sub in hex caps",
814 named_text.replace(&shape::key_id(&w.member.public), &shape::key_id(&w.member.public).to_uppercase())),
815 ("a named proof", named_text.replace("{", "{\"proof\":{\"alg\":\"linkring/1\",\"body\":\"AA\"},")),
816 ("a pairwise pub", pair_text.replace("{", &fmt!("{{\"pub\":\"{}\",", pub_b64))),
817 ("a proof member extra", pair_text.replace("\"alg\":", "\"x\":1,\"alg\":")),
818 ("a duplicate member", named_text.replace("{", &fmt!("{{\"ts\":{},", NOW))),
819 // The forms JDAT reads and JSON does not.
820 ("a typed integer", named_text.replace(&fmt!("\"ts\":{}", NOW), &fmt!("\"ts\":(u64|{})", NOW))),
821 ("a hex integer", named_text.replace(&fmt!("\"ts\":{}", NOW), &fmt!("\"ts\":0x{:x}", NOW))),
822 ("a typed string", named_text.replace("\"v\":\"present/1\"", "\"v\":(str|\"present/1\")")),
823 ("an unquoted word", named_text.replace("\"mode\":\"named\"", "\"mode\":named")),
824 ("a single-quoted string", named_text.replace("\"mode\":\"named\"", "\"mode\":'named'")),
825 ("digits split by a space", named_text.replace(&fmt!("\"ts\":{}", NOW), "\"ts\":1800 000000")),
826 ("a leading zero", named_text.replace(&fmt!("\"ts\":{}", NOW), &fmt!("\"ts\":0{}", NOW))),
827 ("a trailing comma", fmt!("{},}}", &named_text[..named_text.len() - 1])),
828 ("a second value after", fmt!("{} {{}}", named_text)),
829 ];
830 let mut wrong = Vec::new();
831 for (what, text) in edits {
832 let got = res!(refusal(v.verify(b"s", text.as_bytes(), NOW)));
833 if got != Some(Refusal::Malformed) {
834 wrong.push(fmt!("{} ({:?})", what, got));
835 }
836 }
837 req!(wrong, Vec::<String>::new(), "every case must be malformed");
838 let got = res!(refusal(v.verify(b"s", &[0xff, 0xfe, 0x00], NOW)));
839 req!(got, Some(Refusal::Malformed), "not UTF-8");
840 Ok(())
841}
842
843/// The signed bytes are the canonical JSON the contract describes, written out
844/// here by hand rather than taken from the encoder that makes them.
845#[test]
846fn the_signed_bytes_are_the_canonical_json() -> Outcome<()> {
847 let p = Presentation {
848 rp_id: APP.to_string(),
849 nonce: [0u8; 32],
850 subject: Subject::Named { id: "0123456789".to_string(), key: [0xffu8; 32] },
851 predicates: vec!["adult".to_string()],
852 head: [0xfbu8; 32],
853 ts: 1_700_000_000,
854 sig: [0u8; 64],
855 };
856 let want = "{\"head\":\"-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_s\",\"mode\":\"named\",\
857 \"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"predicates\":[\"adult\"],\
858 \"pub\":\"__________________________________________8\",\
859 \"rp_id\":\"https://app.example\",\"sub\":\"0123456789\",\"ts\":1700000000,\
860 \"v\":\"present/1\"}";
861 req!(String::from_utf8_lossy(&res!(p.signed_bytes())).to_string(), want.to_string());
862
863 // A head's id is SHA-256 over exactly its signed bytes, prev a present null.
864 let head = Head {
865 id: [0u8; 32],
866 epoch: 3,
867 prev: None,
868 ts: 5,
869 salt: [0u8; 16],
870 members: 2,
871 ring_n: 1,
872 ring_digest: [0u8; 32],
873 signer: [0u8; 32],
874 sig: [0u8; 64],
875 };
876 let want = "{\"epoch\":3,\"members\":2,\"prev\":null,\
877 \"ring_digest\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"ring_n\":1,\
878 \"salt\":\"AAAAAAAAAAAAAAAAAAAAAA\",\
879 \"signer\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"ts\":5,\"v\":\"head/1\"}";
880 req!(String::from_utf8_lossy(&res!(head.signed_bytes())).to_string(), want.to_string());
881 req!(res!(head.compute_id()), sha256::digest(want.as_bytes()));
882 Ok(())
883}
884
885/// The other signed bodies, each written out from the contract by hand: a
886/// pairwise presentation, which signs `sub` and `tag` and neither `proof` nor
887/// `sig`; a settlement; and a head with `prev` set, whose id is checked against
888/// a SHA-256 taken outside this crate.
889#[test]
890fn the_other_signed_bytes_are_the_canonical_json() -> Outcome<()> {
891 let p = Presentation {
892 rp_id: APP.to_string(),
893 nonce: [0x01u8; 32],
894 subject: Subject::Pairwise {
895 key: [0xffu8; 32],
896 tag: [0xfbu8; 32],
897 proof: Proof { alg: linkring::ALG.to_string(), body: vec![1, 2, 3] },
898 },
899 predicates: vec!["adult".to_string()],
900 head: [0u8; 32],
901 ts: 1_700_000_000,
902 sig: [7u8; 64],
903 };
904 let want = "{\"head\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",\"mode\":\"pairwise\",\
905 \"nonce\":\"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\",\"predicates\":[\"adult\"],\
906 \"rp_id\":\"https://app.example\",\"sub\":\"__________________________________________8\",\
907 \"tag\":\"-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_v7-_s\",\"ts\":1700000000,\
908 \"v\":\"present/1\"}";
909 req!(String::from_utf8_lossy(&res!(p.signed_bytes())).to_string(), want.to_string(), "pairwise");
910
911 let settlement = Settlement {
912 invoice: [0x11u8; 32],
913 entry: [0x22u8; 32],
914 amount: 500,
915 ts: 1_700_000_060,
916 signer: [0x33u8; 32],
917 sig: [7u8; 64],
918 };
919 let want = "{\"entry\":\"IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI\",\
920 \"invoice\":\"ERERERERERERERERERERERERERERERERERERERERERE\",\"oxes\":500,\
921 \"signer\":\"MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM\",\"ts\":1700000060,\
922 \"v\":\"settle/1\"}";
923 req!(String::from_utf8_lossy(&res!(settlement.signed_bytes())).to_string(), want.to_string(),
924 "settlement");
925
926 let head = Head {
927 id: [0u8; 32],
928 epoch: 4,
929 prev: Some([0x44u8; 32]),
930 ts: 6,
931 salt: [0u8; 16],
932 members: 3,
933 ring_n: 2,
934 ring_digest: [0x11u8; 32],
935 signer: [0x33u8; 32],
936 sig: [7u8; 64],
937 };
938 let want = "{\"epoch\":4,\"members\":3,\"prev\":\"REREREREREREREREREREREREREREREREREREREREREQ\",\
939 \"ring_digest\":\"ERERERERERERERERERERERERERERERERERERERERERE\",\"ring_n\":2,\
940 \"salt\":\"AAAAAAAAAAAAAAAAAAAAAA\",\
941 \"signer\":\"MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM\",\"ts\":6,\"v\":\"head/1\"}";
942 req!(String::from_utf8_lossy(&res!(head.signed_bytes())).to_string(), want.to_string(), "head");
943 // sha256sum of those bytes, taken outside this crate.
944 let id: [u8; 32] = [
945 0x5f, 0x01, 0x5f, 0x92, 0x74, 0x49, 0x63, 0x3e, 0x07, 0x9e, 0xae, 0x70, 0x29, 0xda, 0x00, 0x6b,
946 0xe4, 0x99, 0x35, 0xb3, 0x0e, 0x6c, 0x9d, 0x15, 0x90, 0xa7, 0x89, 0x99, 0x17, 0xc0, 0x94, 0x21,
947 ];
948 req!(res!(head.compute_id()), id, "head id");
949 Ok(())
950}
951
952#[test]
953fn a_key_id_is_the_head_of_its_sha256() -> Outcome<()> {
954 // SHA-256("abc") begins ba7816bf8f (FIPS 180-4).
955 req!(shape::key_id(b"abc"), "ba7816bf8f".to_string());
956 req!(shape::is_key_id("ba7816bf8f"), true);
957 for bad in ["BA7816BF8F", "ba7816bf8", "ba7816bf8f0", "ba7816bf8g"] {
958 req!(shape::is_key_id(bad), false, "{}", bad);
959 }
960 Ok(())
961}
962
963#[test]
964fn origins_are_read_as_serialised() -> Outcome<()> {
965 for good in [
966 "https://app.example",
967 "https://a.b-c.example:8443",
968 "https://127.0.0.1",
969 "http://localhost",
970 "http://localhost:8080",
971 "http://127.0.0.1:3000",
972 "https://xn--bcher-kva.example",
973 "https://192.168.0.1", // a dotted quad
974 "https://0.0.0.0",
975 "https://example.1a", // a last label that is not a number
976 "https://1a.example",
977 ] {
978 if shape::check_origin(good).is_err() {
979 return Err(err!("'{}' was refused.", good; Test));
980 }
981 }
982 for bad in [
983 "https://App.example", // upper case
984 "https://app.example/", // trailing slash
985 "https://app.example/path", // path
986 "https://app.example:443", // default port
987 "http://app.example", // plain http off localhost
988 "http://localhost:80", // default port
989 "ftp://app.example", // scheme
990 "https://user@app.example", // user information
991 "https://app.example:0", // port zero
992 "https://app.example:08443", // leading zero
993 "https://app.example:65536", // port too large
994 "https://app..example", // empty label
995 "https://-app.example", // hyphen at a label's edge
996 "https://[::1]", // IPv6 literal
997 "https://app.example?x=1", // query
998 "https://1234", // an IPv4 host not written as a dotted quad
999 "https://127.1",
1000 "https://0x7f.1",
1001 "https://0x7f.0.0.1",
1002 "https://1.2.3",
1003 "https://01.2.3.4", // a leading zero, read as octal
1004 "https://256.1.1.1", // a part beyond 255
1005 "https://1.2.3.4.5",
1006 "https://app.1", // a numeric last label on a name
1007 "https://app.0x1f",
1008 "app.example", // no scheme
1009 "https://", // no host
1010 ] {
1011 if shape::check_origin(bad).is_ok() {
1012 return Err(err!("'{}' was accepted.", bad; Test));
1013 }
1014 }
1015 Ok(())
1016}
1017
1018/// A request goes out and comes back as itself; the reserved members are
1019/// ignored and any other is refused.
1020#[test]
1021fn requests_round_trip_and_read_strictly() -> Outcome<()> {
1022 let (w, book) = res!(world());
1023 let v = res!(verifier(&w, book));
1024 let invoice = Invoice {
1025 rp_id: APP.to_string(),
1026 payee: "0123456789".to_string(),
1027 account: "abcdef0123".to_string(),
1028 amount: 4_294_967_296,
1029 memo: "One book".to_string(),
1030 nonce: [3u8; 16],
1031 expires: NOW + 600,
1032 ts: NOW,
1033 };
1034 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &["adult"], Some(invoice.clone()),
1035 Some("https://app.example/back?x=1"), NOW));
1036 let text = res!(req.to_json());
1037 req!(res!(Request::parse(&text)), req.clone());
1038
1039 let reserved = text.replacen("{", "{\"org\":\"a\",\"osig\":\"b\",", 1);
1040 req!(res!(Request::parse(&reserved)), req.clone(), "org and osig are ignored");
1041 for (what, bad) in [
1042 ("an unknown member", text.replacen("{", "{\"x\":1,", 1)),
1043 ("modes without pairwise", text.replace("[\"pairwise\",\"named\"]", "[\"named\"]")),
1044 ("modes out of order", text.replace("[\"pairwise\",\"named\"]", "[\"named\",\"pairwise\"]")),
1045 ("a return URL elsewhere", text.replace("https://app.example/back", "https://evil.example/back")),
1046 ] {
1047 req!(Request::parse(&bad).is_err(), true, "{}", what);
1048 }
1049 // Issuing checks the same rules.
1050 let bad = v.issue(b"s", Accept::Pairwise, &["Adult"], None, None, NOW).is_err();
1051 req!(bad, true, "a bad word");
1052 let bad = v.issue(b"s", Accept::Pairwise, &[], None, Some("https://app.example#frag"), NOW).is_err();
1053 req!(bad,
1054 true, "a return URL with a fragment");
1055 let mut foreign = invoice.clone();
1056 foreign.rp_id = OTHER.to_string();
1057 let bad = v.issue(b"s", Accept::Pairwise, &[], Some(foreign), None, NOW).is_err();
1058 req!(bad, true,
1059 "another origin's invoice");
1060 Ok(())
1061}
1062
1063#[test]
1064fn invoices_are_checked_and_named_by_their_hash() -> Outcome<()> {
1065 let invoice = Invoice {
1066 rp_id: APP.to_string(),
1067 payee: "0123456789".to_string(),
1068 account: "abcdef0123".to_string(),
1069 amount: 10,
1070 memo: "Tea".to_string(),
1071 nonce: [0u8; 16],
1072 expires: 1_000 + 3_600,
1073 ts: 1_000,
1074 };
1075 res!(invoice.check());
1076 let want = "{\"account\":\"abcdef0123\",\"expires\":4600,\"memo\":\"Tea\",\
1077 \"nonce\":\"AAAAAAAAAAAAAAAAAAAAAA\",\"oxes\":10,\"payee\":\"0123456789\",\
1078 \"rp_id\":\"https://app.example\",\"ts\":1000,\"v\":\"invoice/1\"}";
1079 req!(res!(invoice.to_json()), want.to_string());
1080 req!(res!(invoice.id()), sha256::digest(want.as_bytes()));
1081 req!(res!(Invoice::parse(want)), invoice.clone());
1082
1083 let cases: Vec<(&str, Invoice)> = vec![
1084 ("no amount", Invoice { amount: 0, ..invoice.clone() }),
1085 ("a long memo", Invoice { memo: "m".repeat(65), ..invoice.clone() }),
1086 ("a control in the memo", Invoice { memo: "a\tb".to_string(), ..invoice.clone() }),
1087 ("a payee not an id", Invoice { payee: "someone".to_string(), ..invoice.clone() }),
1088 ("too long a life", Invoice { expires: 1_000 + 3_601, ..invoice.clone() }),
1089 ("expiring before issue", Invoice { expires: 999, ..invoice.clone() }),
1090 ("a bad origin", Invoice { rp_id: "https://app.example/".to_string(), ..invoice.clone() }),
1091 ];
1092 for (what, bad) in cases {
1093 req!(bad.check().is_err(), true, "{}", what);
1094 }
1095 // A memo of 64 characters after trimming is within the limit.
1096 res!(Invoice { memo: fmt!(" {} ", "m".repeat(64)), ..invoice.clone() }.check());
1097 Ok(())
1098}
1099
1100#[test]
1101fn a_settlement_verifies_and_each_fault_is_refused() -> Outcome<()> {
1102 let (w, book) = res!(world());
1103 let v = res!(verifier(&w, book));
1104 let invoice = Invoice {
1105 rp_id: APP.to_string(),
1106 payee: "0123456789".to_string(),
1107 account: "abcdef0123".to_string(),
1108 amount: 500,
1109 memo: String::new(),
1110 nonce: [1u8; 16],
1111 expires: NOW + 3_600,
1112 ts: NOW,
1113 };
1114 let settle = |signer: &Ed, invoice_id: [u8; 32], amount: u64, ts: u64| -> Outcome<Settlement> {
1115 let mut s = Settlement {
1116 invoice: invoice_id,
1117 entry: [2u8; 32],
1118 amount,
1119 ts,
1120 signer: signer.public,
1121 sig: [0u8; 64],
1122 };
1123 s.sig = res!(signer.sign(&res!(s.signed_bytes())));
1124 Ok(s)
1125 };
1126 let id = res!(invoice.id());
1127 let good = res!(settle(&w.peer, id, 500, NOW + 60));
1128 res!(v.verify_settlement(&good, &invoice));
1129 req!(res!(Settlement::parse(&res!(good.to_json()))), good.clone());
1130
1131 let forger = res!(Ed::new());
1132 let mut spoilt = good.clone();
1133 spoilt.sig[3] ^= 0x01;
1134 let mut other_invoice = invoice.clone();
1135 other_invoice.rp_id = OTHER.to_string();
1136 for (what, s, inv) in [
1137 ("an outside signer", res!(settle(&forger, id, 500, NOW + 60)), &invoice),
1138 ("a bad signature", spoilt, &invoice),
1139 ("another invoice", res!(settle(&w.peer, [9u8; 32], 500, NOW + 60)), &invoice),
1140 ("another amount", res!(settle(&w.peer, id, 499, NOW + 60)), &invoice),
1141 ("after expiry", res!(settle(&w.peer, id, 500, NOW + 3_601)), &invoice),
1142 ("before issue", res!(settle(&w.peer, id, 500, NOW - 1)), &invoice),
1143 ("another's invoice", good.clone(), &other_invoice),
1144 ] {
1145 req!(v.verify_settlement(&s, inv).is_err(), true, "{}", what);
1146 }
1147 Ok(())
1148}
1149
1150// ── Vendor neutrality ───────────────────────────────────────────────────────
1151
1152/// The module serves any network of confirmed humans, so no source file in it
1153/// names one, or one's vocabulary.
1154#[test]
1155fn the_presentation_module_names_no_network() -> Outcome<()> {
1156 let sources = [
1157 ("mod.rs", include_str!("../src/presentation/mod.rs")),
1158 ("shape.rs", include_str!("../src/presentation/shape.rs")),
1159 ("verify.rs", include_str!("../src/presentation/verify.rs")),
1160 ];
1161 let banned = ["oxe".to_string() + "gen", "oxe".to_string() + "nym", "ox".to_string() + "id",
1162 "hu".to_string() + "ser"];
1163 for (name, text) in sources {
1164 let lower = text.to_lowercase();
1165 for word in banned.iter() {
1166 req!(lower.contains(word.as_str()), false, "src/presentation/{} says '{}'", name, word);
1167 }
1168 }
1169 Ok(())
1170}