Oregami
Repositories/oxedyne/fe2o3

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

33.0 KiB, 1 run

created by r1870400018:61156, 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 crate::{
2 presentation::shape::{
3 Accept,
4 HEAD_LEAD,
5 Head,
6 Invoice,
7 NONCE_LEN,
8 Presentation,
9 Request,
10 Settlement,
11 Status,
12 Subject,
13 T_G,
14 check_origin,
15 key_id,
16 scope,
17 },
18};
19
20use oxedyne_fe2o3_core::{
21 prelude::*,
22 rand::Rand,
23};
24use oxedyne_fe2o3_crypto::{
25 linkring::{
26 self,
27 Ring,
28 },
29 sign::verify_ed25519,
30};
31use oxedyne_fe2o3_hash::sha256;
32
33use std::{
34 collections::{
35 BTreeSet,
36 HashMap,
37 },
38 sync::{
39 Arc,
40 Mutex,
41 },
42};
43
44
45/// What a verifier needs fetched: heads, the ring at a head, and the status of
46/// a name. Each answer is the caller's to fetch and cache. A head and its ring
47/// are content-addressed, so a ring decoded once serves every presentation made
48/// against it. An error and a `None` both refuse the step that asked.
49///
50/// A head is checked here against its own hash and an issuer's signature, and a
51/// ring against its head, so either may come from anywhere. A status carries no
52/// signature, so it must come from a source the relying party trusts, such as
53/// an issuer's own read API over TLS: whoever answers it decides which key a
54/// name holds.
55pub trait Lookup {
56 fn head(&self, id: &[u8; 32]) -> Outcome<Option<Head>>;
57 fn ring(&self, head: &Head) -> Outcome<Option<Arc<Ring>>>;
58 fn status(&self, id: &str) -> Outcome<Option<Status>>;
59}
60
61/// The first check a presentation failed, as one of a fixed list of words.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum Refusal {
64 Malformed, // not the shape, or not `present/1`
65 WrongAudience, // `rp_id` is not this verifier's origin
66 UnknownNonce, // not issued here to this session, or past its `exp`
67 Replayed, // the nonce was spent before
68 ModeNotAccepted, // a mode the request did not accept
69 PredicateNotOffered, // a predicate the request did not ask for
70 Stale, // `ts` more than T_G from now
71 UnknownHead, // not fetched, not its own hash, or not signed by an issuer
72 StaleHead, // older than T_G, or more than HEAD_LEAD after `ts`
73 BadSignature, // the signature fails, or a name is not its key's id
74 NotLive, // named: the name is not live with this key
75 BadProof, // pairwise: the ring proof fails over the head's whole ring
76}
77
78impl Refusal {
79 pub fn word(&self) -> &'static str {
80 match self {
81 Self::Malformed => "malformed",
82 Self::WrongAudience => "wrong_audience",
83 Self::UnknownNonce => "unknown_nonce",
84 Self::Replayed => "replayed",
85 Self::ModeNotAccepted => "mode_not_accepted",
86 Self::PredicateNotOffered => "predicate_not_offered",
87 Self::Stale => "stale",
88 Self::UnknownHead => "unknown_head",
89 Self::StaleHead => "stale_head",
90 Self::BadSignature => "bad_signature",
91 Self::NotLive => "not_live",
92 Self::BadProof => "bad_proof",
93 }
94 }
95}
96
97/// What an accepted presentation establishes. A relying party mints its own
98/// session from it; uniqueness and bans by tag are its policy.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum Verified {
101 Named {
102 id: String, // the name's key id, `sub`
103 key: [u8; 32], // its Ed25519 key, `pub`
104 predicates: Vec<String>,
105 },
106 Pairwise {
107 key: [u8; 32], // the pseudonym key, `sub`, to re-challenge later
108 tag: [u8; 32], // one per human at this relying party
109 predicates: Vec<String>,
110 },
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub enum Verdict {
115 Accepted(Verified),
116 Refused(Refusal),
117}
118
119impl Verdict {
120 /// The refusal, if this is one.
121 pub fn refusal(&self) -> Option<Refusal> {
122 match self {
123 Self::Accepted(_) => None,
124 Self::Refused(r) => Some(*r),
125 }
126 }
127}
128
129// ── Guards ──────────────────────────────────────────────────────────────────
130
131// Each refusal that stands between an attacker and an accepted presentation
132// has a bit here. The unit tests switch one off, on their own thread only, to
133// prove the attack it stops then succeeds.
134const G_AUDIENCE: u32 = 1; // rp_id is this verifier's own
135const G_SESSION: u32 = 2; // the nonce was issued to this session
136const G_REPLAY: u32 = 4; // the nonce is spent once
137const G_ISSUER: u32 = 8; // the head is signed by a trusted issuer
138const G_RING: u32 = 16; // the ring is the head's, by length and digest
139const G_STATUS: u32 = 32; // a name is live with the presented key
140
141#[cfg(test)]
142thread_local! {
143 static SKIP: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
144}
145
146#[cfg(test)]
147fn on(g: u32) -> bool { SKIP.with(|s| s.get() & g == 0) }
148
149#[cfg(not(test))]
150#[inline(always)]
151fn on(_g: u32) -> bool { true }
152
153// ── Verifier ────────────────────────────────────────────────────────────────
154
155struct Issued {
156 session: [u8; 32], // SHA-256 of the session it was issued to
157 req: Request,
158 spent: bool, // shown once already, pass or fail
159}
160
161type Slot = (u64, [u8; NONCE_LEN]); // (exp, nonce), so a set of slots lapses in order
162
163/// The challenges outstanding, indexed by nonce, and by lapse overall and per
164/// session, so nothing is found or dropped by a scan.
165struct Challenges {
166 issued: HashMap<[u8; NONCE_LEN], Issued>,
167 by_exp: BTreeSet<Slot>,
168 by_session: HashMap<[u8; 32], BTreeSet<Slot>>,
169}
170
171impl Challenges {
172
173 fn new() -> Self {
174 Self {
175 issued: HashMap::new(),
176 by_exp: BTreeSet::new(),
177 by_session: HashMap::new(),
178 }
179 }
180
181 /// Drops the challenges whose requests have lapsed by `now`, soonest first.
182 fn drop_lapsed(&mut self, now: u64) {
183 while let Some((exp, nonce)) = self.by_exp.first().copied() {
184 if exp >= now {
185 break;
186 }
187 self.remove(&nonce);
188 }
189 }
190
191 fn remove(&mut self, nonce: &[u8; NONCE_LEN]) {
192 if let Some(issued) = self.issued.remove(nonce) {
193 let slot = (issued.req.exp, *nonce);
194 self.by_exp.remove(&slot);
195 let emptied = match self.by_session.get_mut(&issued.session) {
196 Some(slots) => {
197 slots.remove(&slot);
198 slots.is_empty()
199 },
200 None => false,
201 };
202 if emptied {
203 self.by_session.remove(&issued.session);
204 }
205 }
206 }
207
208 /// Files a challenge, first letting the session's own soonest to lapse go
209 /// while it holds `per_session`, then the store's while it holds `max`, so
210 /// a flood from one session costs other sessions nothing and a flood from
211 /// many shortens the life of the oldest challenges rather than refusing new
212 /// ones.
213 fn insert(
214 &mut self,
215 nonce: [u8; NONCE_LEN],
216 issued: Issued,
217 max: usize,
218 per_session: usize,
219 ) {
220 // A nonce drawn twice would leave its first slot behind; keep the
221 // indexes whole however unlikely that is.
222 self.remove(&nonce);
223 loop {
224 let first = match self.by_session.get(&issued.session) {
225 Some(slots) if slots.len() >= per_session => slots.first().copied(),
226 _ => None,
227 };
228 match first {
229 Some((_, old)) => self.remove(&old),
230 None => break,
231 }
232 }
233 while self.issued.len() >= max {
234 match self.by_exp.first().copied() {
235 Some((_, old)) => self.remove(&old),
236 None => break,
237 }
238 }
239 let slot = (issued.req.exp, nonce);
240 self.by_exp.insert(slot);
241 self.by_session.entry(issued.session).or_default().insert(slot);
242 self.issued.insert(nonce, issued);
243 }
244}
245
246/// A relying party's presentation verifier, for one origin. Its methods take
247/// `&self`, and the challenge store's lock is held only while a nonce is looked
248/// up and spent, so a pass over a large ring never holds up another session.
249///
250/// It holds at most `MAX_ISSUED` challenges, and at most `MAX_PER_SESSION` for
251/// one session. A session's challenge beyond its bound lets that session's
252/// soonest to lapse go, and one beyond the store's lets the store's soonest go,
253/// so an issue is never refused for want of room. A challenge let go early
254/// reads `unknown_nonce`. Limiting how fast one address may ask is the
255/// caller's.
256pub struct Verifier<L: Lookup> {
257 rp_id: String,
258 issuers: Vec<[u8; 32]>,
259 lookup: L,
260 threads: usize,
261 max_issued: usize,
262 max_per_session: usize,
263 challenges: Mutex<Challenges>,
264}
265
266impl<L: Lookup> Verifier<L> {
267
268 pub const MAX_ISSUED: usize = 1 << 16;
269 pub const MAX_PER_SESSION: usize = 8;
270
271 /// A verifier for the relying party at `rp_id`, which trusts heads signed
272 /// by `issuers`, Ed25519 keys.
273 ///
274 /// # Arguments
275 ///
276 /// * `threads` - how many threads a pairwise proof's pass over the ring may
277 /// use; 1 on wasm32.
278 pub fn new(
279 rp_id: &str,
280 issuers: Vec<[u8; 32]>,
281 lookup: L,
282 threads: usize,
283 )
284 -> Outcome<Self>
285 {
286 res!(check_origin(rp_id));
287 if issuers.is_empty() {
288 return Err(err!(
289 "A presentation verifier for {} with no issuer keys could accept nothing.",
290 rp_id;
291 Invalid, Input, Missing));
292 }
293 Ok(Self {
294 rp_id: rp_id.to_string(),
295 issuers,
296 lookup,
297 threads: threads.max(1),
298 max_issued: Self::MAX_ISSUED,
299 max_per_session: Self::MAX_PER_SESSION,
300 challenges: Mutex::new(Challenges::new()),
301 })
302 }
303
304 /// Caps the challenges outstanding at once, at least one.
305 pub fn with_max_issued(mut self, max_issued: usize) -> Self {
306 self.max_issued = max_issued.max(1);
307 self
308 }
309
310 /// Caps the challenges one session holds at once, at least one.
311 pub fn with_max_per_session(mut self, max_per_session: usize) -> Self {
312 self.max_per_session = max_per_session.max(1);
313 self
314 }
315
316 pub fn rp_id(&self) -> &str { &self.rp_id }
317 pub fn lookup(&self) -> &L { &self.lookup }
318
319 /// Issues a challenge to one browser session: a fresh nonce, and the
320 /// request that carries it, which lapses T_G after `now`. The session is
321 /// whatever the caller binds a browser by, such as its session cookie, and
322 /// is refused when empty.
323 pub fn issue(
324 &self,
325 session: &[u8],
326 accept: Accept,
327 predicates: &[&str],
328 invoice: Option<Invoice>,
329 return_to: Option<&str>,
330 now: u64,
331 )
332 -> Outcome<Request>
333 {
334 res!(Self::bound(session));
335 let mut nonce = [0u8; NONCE_LEN];
336 Rand::fill_u8(&mut nonce);
337 let req = Request {
338 rp_id: self.rp_id.clone(),
339 nonce,
340 accept,
341 predicates: predicates.iter().map(|w| w.to_string()).collect(),
342 invoice,
343 return_to: return_to.map(|url| url.to_string()),
344 exp: now.saturating_add(T_G),
345 };
346 res!(req.check());
347 let mut ch = lock_mutex!(self.challenges);
348 ch.drop_lapsed(now);
349 ch.insert(nonce, Issued {
350 session: sha256::digest(session),
351 req: req.clone(),
352 spent: false,
353 }, self.max_issued, self.max_per_session);
354 Ok(req)
355 }
356
357 /// Refuses an empty session, which would bind a challenge to every browser
358 /// that sends none.
359 fn bound(session: &[u8]) -> Outcome<()> {
360 if session.is_empty() {
361 return Err(err!(
362 "A challenge is bound to a browser session, and the session given is empty.";
363 Invalid, Input, Missing));
364 }
365 Ok(())
366 }
367
368 /// Verifies a presentation that arrived in `session` at `now` (unix
369 /// seconds). The nonce is spent by the first presentation to reach that
370 /// check, whether or not it goes on to pass, so a presentation is accepted
371 /// once at most. An error is the fault of this verifier or its caller, such
372 /// as an empty session, never the presentation's.
373 pub fn verify(
374 &self,
375 session: &[u8],
376 body: &[u8],
377 now: u64,
378 )
379 -> Outcome<Verdict>
380 {
381 res!(Self::bound(session));
382 let p = match std::str::from_utf8(body) {
383 Ok(text) => match Presentation::parse(text) {
384 Ok(p) => p,
385 Err(_) => return Ok(Verdict::Refused(Refusal::Malformed)),
386 },
387 Err(_) => return Ok(Verdict::Refused(Refusal::Malformed)),
388 };
389 if on(G_AUDIENCE) && p.rp_id != self.rp_id {
390 return Ok(Verdict::Refused(Refusal::WrongAudience));
391 }
392 let req = {
393 let mut ch = lock_mutex!(self.challenges);
394 ch.drop_lapsed(now);
395 let issued = match ch.issued.get_mut(&p.nonce) {
396 Some(issued) => issued,
397 None => return Ok(Verdict::Refused(Refusal::UnknownNonce)),
398 };
399 if (on(G_SESSION) && issued.session != sha256::digest(session)) || now > issued.req.exp {
400 return Ok(Verdict::Refused(Refusal::UnknownNonce));
401 }
402 // The spent mark lives in the challenge, so it stands exactly as
403 // long as the challenge does and a second showing reads as a
404 // replay. A tracker windowed from the first showing could forget
405 // it while the challenge still stood, whenever the caller's clock
406 // stepped back between the issue and that showing.
407 if on(G_REPLAY) && issued.spent {
408 return Ok(Verdict::Refused(Refusal::Replayed));
409 }
410 issued.spent = true;
411 issued.req.clone()
412 };
413 self.check_issued(&req, &p, now)
414 }
415
416 /// The checks after the nonce's, for a caller that keeps its own store of
417 /// issued and spent nonces and has matched `p` to the request `req` it
418 /// issued. The audience, the nonce and the request's lapse are asserted
419 /// again here; spending the nonce once is the caller's.
420 pub fn check_issued(
421 &self,
422 req: &Request,
423 p: &Presentation,
424 now: u64,
425 )
426 -> Outcome<Verdict>
427 {
428 let refuse = |r| Ok(Verdict::Refused(r));
429 if on(G_AUDIENCE) && (p.rp_id != self.rp_id || req.rp_id != self.rp_id) {
430 return refuse(Refusal::WrongAudience);
431 }
432 if p.nonce != req.nonce || now > req.exp {
433 return refuse(Refusal::UnknownNonce);
434 }
435 if !req.accept.admits(p.mode()) {
436 return refuse(Refusal::ModeNotAccepted);
437 }
438 if p.predicates.iter().any(|w| !req.predicates.contains(w)) {
439 return refuse(Refusal::PredicateNotOffered);
440 }
441 if now.abs_diff(p.ts) > T_G {
442 return refuse(Refusal::Stale);
443 }
444
445 // The head: fetched, its own hash, signed by an issuer, and recent.
446 let head = match self.lookup.head(&p.head) {
447 Ok(Some(head)) => head,
448 _ => return refuse(Refusal::UnknownHead),
449 };
450 let head_bytes = match head.signed_bytes() {
451 Ok(bytes) => bytes,
452 Err(_) => return refuse(Refusal::UnknownHead),
453 };
454 if head.id != p.head || sha256::digest(&head_bytes) != head.id {
455 return refuse(Refusal::UnknownHead);
456 }
457 if on(G_ISSUER) && !self.issuers.contains(&head.signer) {
458 return refuse(Refusal::UnknownHead);
459 }
460 if !matches!(verify_ed25519(&head.signer, &head_bytes, &head.sig), Ok(true)) {
461 return refuse(Refusal::UnknownHead);
462 }
463 if head.ts.saturating_add(T_G) < now || head.ts > p.ts.saturating_add(HEAD_LEAD) {
464 return refuse(Refusal::StaleHead);
465 }
466
467 let msg = match p.signed_bytes() {
468 Ok(msg) => msg,
469 Err(_) => return refuse(Refusal::Malformed),
470 };
471 match &p.subject {
472 Subject::Named { id, key } => {
473 if key_id(key) != *id
474 || !matches!(verify_ed25519(key, &msg, &p.sig), Ok(true))
475 {
476 return refuse(Refusal::BadSignature);
477 }
478 let status = match self.lookup.status(id) {
479 Ok(Some(status)) => status,
480 _ => return refuse(Refusal::NotLive),
481 };
482 if on(G_STATUS) && (!status.live || status.key != *key) {
483 return refuse(Refusal::NotLive);
484 }
485 Ok(Verdict::Accepted(Verified::Named {
486 id: id.clone(),
487 key: *key,
488 predicates: p.predicates.clone(),
489 }))
490 },
491 Subject::Pairwise { key, tag, proof } => {
492 if !matches!(verify_ed25519(key, &msg, &p.sig), Ok(true)) {
493 return refuse(Refusal::BadSignature);
494 }
495 if proof.alg != linkring::ALG {
496 return refuse(Refusal::BadProof);
497 }
498 let ring = match self.lookup.ring(&head) {
499 Ok(Some(ring)) => ring,
500 _ => return refuse(Refusal::BadProof),
501 };
502 if on(G_RING)
503 && (ring.len() as u64 != head.ring_n || *ring.digest() != head.ring_digest)
504 {
505 return refuse(Refusal::BadProof);
506 }
507 // The scope is this verifier's own origin, so a proof made
508 // under any other audience fails here as well as above.
509 let proved = linkring::verify_par(
510 &ring, &scope(&self.rp_id), &msg, tag, &proof.body, self.threads);
511 if !matches!(proved, Ok(true)) {
512 return refuse(Refusal::BadProof);
513 }
514 Ok(Verdict::Accepted(Verified::Pairwise {
515 key: *key,
516 tag: *tag,
517 predicates: p.predicates.clone(),
518 }))
519 },
520 }
521 }
522
523 /// Checks that `settlement` shows `invoice` paid: signed by an issuer,
524 /// naming the invoice's id and amount, and made while the invoice ran, from
525 /// its issue to its expiry. `invoice` must come from this relying party's
526 /// own store of what it issued, never from the member's page, and the
527 /// caller credits it once per invoice id, since a settlement verifies again
528 /// every time it is shown.
529 pub fn verify_settlement(
530 &self,
531 settlement: &Settlement,
532 invoice: &Invoice,
533 )
534 -> Outcome<()>
535 {
536 if invoice.rp_id != self.rp_id {
537 return Err(err!(
538 "The invoice was issued by {}, not by {}.", invoice.rp_id, self.rp_id;
539 Invalid, Input, Mismatch));
540 }
541 if !self.issuers.contains(&settlement.signer) {
542 return Err(err!(
543 "The settlement is signed by a key outside {}'s issuers.", self.rp_id;
544 Invalid, Input, Security));
545 }
546 if !res!(verify_ed25519(&settlement.signer, &res!(settlement.signed_bytes()), &settlement.sig)) {
547 return Err(err!(
548 "The settlement's signature does not verify against its signer.";
549 Invalid, Input, Security));
550 }
551 if settlement.invoice != res!(invoice.id()) {
552 return Err(err!(
553 "The settlement names another invoice.";
554 Invalid, Input, Mismatch));
555 }
556 if settlement.amount != invoice.amount {
557 return Err(err!(
558 "The settlement pays {} where the invoice asks {}.",
559 settlement.amount, invoice.amount;
560 Invalid, Input, Mismatch));
561 }
562 if settlement.ts > invoice.expires {
563 return Err(err!(
564 "The settlement at {} is after the invoice expired at {}.",
565 settlement.ts, invoice.expires;
566 Invalid, Input));
567 }
568 if settlement.ts < invoice.ts {
569 return Err(err!(
570 "The settlement at {} is before the invoice was issued at {}.",
571 settlement.ts, invoice.ts;
572 Invalid, Input));
573 }
574 Ok(())
575 }
576}
577
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::presentation::shape::{
583 KEY_LEN,
584 Proof,
585 SALT_LEN,
586 SIG_LEN,
587 };
588
589 use oxedyne_fe2o3_crypto::{
590 linkring::SecretKey,
591 sign::SignatureScheme,
592 };
593 use oxedyne_fe2o3_iop_crypto::{
594 keys::KeyManager,
595 sign::Signer,
596 };
597
598 const NOW: u64 = 1_800_000_000;
599
600 /// Runs `f` with the guard `g` switched off on this thread.
601 fn without<T>(g: u32, f: impl FnOnce() -> T) -> T {
602 SKIP.with(|s| s.set(g));
603 let out = f();
604 SKIP.with(|s| s.set(0));
605 out
606 }
607
608 struct Ed {
609 scheme: SignatureScheme,
610 public: [u8; KEY_LEN],
611 }
612
613 impl Ed {
614 fn new() -> Outcome<Self> {
615 let scheme = SignatureScheme::new_ed25519();
616 let mut public = [0u8; KEY_LEN];
617 match res!(scheme.get_public_key()) {
618 Some(pk) => public.copy_from_slice(pk),
619 None => return Err(err!("A new Ed25519 key has no public half."; Bug, Missing)),
620 }
621 Ok(Self { scheme, public })
622 }
623
624 fn sign(&self, msg: &[u8]) -> Outcome<[u8; SIG_LEN]> {
625 let sig = res!(self.scheme.sign(msg));
626 let mut out = [0u8; SIG_LEN];
627 out.copy_from_slice(&sig);
628 Ok(out)
629 }
630 }
631
632 #[derive(Default)]
633 struct Book {
634 heads: HashMap<[u8; 32], Head>,
635 rings: HashMap<[u8; 32], Arc<Ring>>, // by head id
636 statuses: HashMap<String, Status>,
637 }
638
639 impl Lookup for Book {
640 fn head(&self, id: &[u8; 32]) -> Outcome<Option<Head>> {
641 Ok(self.heads.get(id).cloned())
642 }
643 fn ring(&self, head: &Head) -> Outcome<Option<Arc<Ring>>> {
644 Ok(self.rings.get(&head.id).cloned())
645 }
646 fn status(&self, id: &str) -> Outcome<Option<Status>> {
647 Ok(self.statuses.get(id).cloned())
648 }
649 }
650
651 /// A head at `ts` over `ring`, signed by `signer`, filed with its ring.
652 fn mint(book: &mut Book, signer: &Ed, ring: &Arc<Ring>, ts: u64) -> Outcome<[u8; 32]> {
653 let mut head = Head {
654 id: [0u8; 32],
655 epoch: 1,
656 prev: None,
657 ts,
658 salt: [7u8; SALT_LEN],
659 members: ring.len() as u64,
660 ring_n: ring.len() as u64,
661 ring_digest: *ring.digest(),
662 signer: signer.public,
663 sig: [0u8; SIG_LEN],
664 };
665 let bytes = res!(head.signed_bytes());
666 head.sig = res!(signer.sign(&bytes));
667 head.id = res!(head.compute_id());
668 book.rings.insert(head.id, ring.clone());
669 book.heads.insert(head.id, head.clone());
670 Ok(head.id)
671 }
672
673 fn ring_of(keys: &[SecretKey]) -> Outcome<Arc<Ring>> {
674 let mut list: Vec<[u8; 32]> = keys.iter().map(|k| k.public_key()).collect();
675 list.sort();
676 Ok(Arc::new(res!(Ring::from_keys(&list))))
677 }
678
679 fn named(req: &Request, member: &Ed, head: [u8; 32], rp_id: &str) -> Outcome<Vec<u8>> {
680 let mut p = Presentation {
681 rp_id: rp_id.to_string(),
682 nonce: req.nonce,
683 subject: Subject::Named { id: key_id(&member.public), key: member.public },
684 predicates: vec![],
685 head,
686 ts: NOW,
687 sig: [0u8; SIG_LEN],
688 };
689 p.sig = res!(member.sign(&res!(p.signed_bytes())));
690 Ok(res!(p.to_json()).into_bytes())
691 }
692
693 fn pairwise(
694 req: &Request,
695 sub: &Ed,
696 key: &SecretKey,
697 ring: &Ring,
698 head: [u8; 32],
699 rp_id: &str,
700 )
701 -> Outcome<Vec<u8>>
702 {
703 let scope = scope(rp_id);
704 let mut p = Presentation {
705 rp_id: rp_id.to_string(),
706 nonce: req.nonce,
707 subject: Subject::Pairwise {
708 key: sub.public,
709 tag: res!(linkring::tag(key, &scope)),
710 proof: Proof { alg: linkring::ALG.to_string(), body: Vec::new() },
711 },
712 predicates: vec![],
713 head,
714 ts: NOW,
715 sig: [0u8; SIG_LEN],
716 };
717 let msg = res!(p.signed_bytes());
718 let (_, body) = res!(linkring::sign(ring, key, &scope, &msg));
719 if let Subject::Pairwise { proof, .. } = &mut p.subject {
720 proof.body = body;
721 }
722 p.sig = res!(sub.sign(&msg));
723 Ok(res!(p.to_json()).into_bytes())
724 }
725
726 struct World {
727 peer: Ed,
728 member: Ed,
729 keys: Vec<SecretKey>,
730 ring: Arc<Ring>,
731 head: [u8; 32],
732 }
733
734 fn world() -> Outcome<(World, Book)> {
735 let mut book = Book::default();
736 let peer = res!(Ed::new());
737 let member = res!(Ed::new());
738 let mut keys = Vec::new();
739 for _ in 0..5 {
740 keys.push(res!(SecretKey::random()));
741 }
742 let ring = res!(ring_of(&keys));
743 let head = res!(mint(&mut book, &peer, &ring, NOW - 10));
744 book.statuses.insert(key_id(&member.public), Status { key: member.public, live: true });
745 Ok((World { peer, member, keys, ring, head }, book))
746 }
747
748 fn accepted(v: &Verdict) -> bool {
749 matches!(v, Verdict::Accepted(_))
750 }
751
752 /// The nonce is spent once. With the replay guard off the same
753 /// presentation is accepted a second time.
754 #[test]
755 fn replay_guard_is_load_bearing() -> Outcome<()> {
756 let (w, book) = res!(world());
757 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
758 let req = res!(v.issue(b"alice", Accept::PairwiseOrNamed, &[], None, None, NOW));
759 let body = res!(named(&req, &w.member, w.head, "https://app.example"));
760 assert!(accepted(&res!(v.verify(b"alice", &body, NOW))), "the first showing passes");
761 assert_eq!(res!(v.verify(b"alice", &body, NOW)).refusal(), Some(Refusal::Replayed));
762 let again = without(G_REPLAY, || v.verify(b"alice", &body, NOW));
763 assert!(accepted(&res!(again)), "without the guard the replay is accepted");
764 Ok(())
765 }
766
767 /// A relying party that relays another's challenge to a member, under its
768 /// own origin, and forwards the answer, is refused by the audience check.
769 /// Named mode is the case to try, since a pairwise proof's scope refuses
770 /// the foreign origin a second time.
771 #[test]
772 fn audience_guard_is_load_bearing() -> Outcome<()> {
773 let (w, book) = res!(world());
774 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
775 let req = res!(v.issue(b"alice", Accept::PairwiseOrNamed, &[], None, None, NOW));
776 let body = res!(named(&req, &w.member, w.head, "https://evil.example"));
777 assert_eq!(res!(v.verify(b"alice", &body, NOW)).refusal(), Some(Refusal::WrongAudience));
778 let relayed = without(G_AUDIENCE, || v.verify(b"alice", &body, NOW));
779 assert!(accepted(&res!(relayed)), "without the guard the relayed answer is accepted");
780 Ok(())
781 }
782
783 /// A presentation made for one session is refused in another.
784 #[test]
785 fn session_guard_is_load_bearing() -> Outcome<()> {
786 let (w, book) = res!(world());
787 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
788 let req = res!(v.issue(b"alice", Accept::PairwiseOrNamed, &[], None, None, NOW));
789 let body = res!(named(&req, &w.member, w.head, "https://app.example"));
790 assert_eq!(res!(v.verify(b"mallory", &body, NOW)).refusal(), Some(Refusal::UnknownNonce));
791 let moved = without(G_SESSION, || v.verify(b"mallory", &body, NOW));
792 assert!(accepted(&res!(moved)), "without the guard another session completes it");
793 Ok(())
794 }
795
796 /// A head the attacker mints over a ring of its own keys is refused for
797 /// want of an issuer's signature.
798 #[test]
799 fn issuer_guard_is_load_bearing() -> Outcome<()> {
800 let (w, mut book) = res!(world());
801 let forger = res!(Ed::new());
802 let own = vec![res!(SecretKey::random()), res!(SecretKey::random())];
803 let own_ring = res!(ring_of(&own));
804 let fake = res!(mint(&mut book, &forger, &own_ring, NOW - 10));
805 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
806 let sub = res!(Ed::new());
807 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
808 let body = res!(pairwise(&req, &sub, &own[0], &own_ring, fake, "https://app.example"));
809 assert_eq!(res!(v.verify(b"s", &body, NOW)).refusal(), Some(Refusal::UnknownHead));
810 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
811 let body = res!(pairwise(&req, &sub, &own[0], &own_ring, fake, "https://app.example"));
812 let forged = without(G_ISSUER, || v.verify(b"s", &body, NOW));
813 assert!(accepted(&res!(forged)), "without the guard a self-minted head is accepted");
814 Ok(())
815 }
816
817 /// A ring served for a head whose digest it does not match is refused,
818 /// even when the proof over it is sound.
819 #[test]
820 fn ring_guard_is_load_bearing() -> Outcome<()> {
821 let (w, mut book) = res!(world());
822 let own = vec![res!(SecretKey::random()), res!(SecretKey::random())];
823 let own_ring = res!(ring_of(&own));
824 // The issuer's head, with the attacker's ring filed against it.
825 book.rings.insert(w.head, own_ring.clone());
826 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
827 let sub = res!(Ed::new());
828 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
829 let body = res!(pairwise(&req, &sub, &own[1], &own_ring, w.head, "https://app.example"));
830 assert_eq!(res!(v.verify(b"s", &body, NOW)).refusal(), Some(Refusal::BadProof));
831 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
832 let body = res!(pairwise(&req, &sub, &own[1], &own_ring, w.head, "https://app.example"));
833 let swapped = without(G_RING, || v.verify(b"s", &body, NOW));
834 assert!(accepted(&res!(swapped)), "without the guard a foreign ring is accepted");
835 Ok(())
836 }
837
838 /// A name that is no longer live, or that now holds another key, is
839 /// refused.
840 #[test]
841 fn status_guard_is_load_bearing() -> Outcome<()> {
842 let (w, mut book) = res!(world());
843 book.statuses.insert(key_id(&w.member.public), Status { key: w.member.public, live: false });
844 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
845 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
846 let body = res!(named(&req, &w.member, w.head, "https://app.example"));
847 assert_eq!(res!(v.verify(b"s", &body, NOW)).refusal(), Some(Refusal::NotLive));
848 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
849 let body = res!(named(&req, &w.member, w.head, "https://app.example"));
850 let erased = without(G_STATUS, || v.verify(b"s", &body, NOW));
851 assert!(accepted(&res!(erased)), "without the guard an erased name is accepted");
852 Ok(())
853 }
854
855 /// Every guard on, the honest cases pass in both modes, so the refusals
856 /// above are about the attacks and not about the fixtures.
857 #[test]
858 fn the_honest_cases_pass_with_every_guard_on() -> Outcome<()> {
859 let (w, book) = res!(world());
860 let v = res!(Verifier::new("https://app.example", vec![w.peer.public], book, 1));
861 let req = res!(v.issue(b"s", Accept::PairwiseOrNamed, &[], None, None, NOW));
862 let body = res!(named(&req, &w.member, w.head, "https://app.example"));
863 assert!(accepted(&res!(v.verify(b"s", &body, NOW))), "named");
864 let sub = res!(Ed::new());
865 let req = res!(v.issue(b"s", Accept::Pairwise, &[], None, None, NOW));
866 let body = res!(pairwise(&req, &sub, &w.keys[2], &w.ring, w.head, "https://app.example"));
867 assert!(accepted(&res!(v.verify(b"s", &body, NOW))), "pairwise");
868 Ok(())
869 }
870}