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
| 1 | use 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 | |
| 20 | use oxedyne_fe2o3_core::{ |
| 21 | prelude::*, |
| 22 | rand::Rand, |
| 23 | }; |
| 24 | use oxedyne_fe2o3_crypto::{ |
| 25 | linkring::{ |
| 26 | self, |
| 27 | Ring, |
| 28 | }, |
| 29 | sign::verify_ed25519, |
| 30 | }; |
| 31 | use oxedyne_fe2o3_hash::sha256; |
| 32 | |
| 33 | use 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. |
| 55 | pub 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)] |
| 63 | pub 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 | |
| 78 | impl 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)] |
| 100 | pub 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)] |
| 114 | pub enum Verdict { |
| 115 | Accepted(Verified), |
| 116 | Refused(Refusal), |
| 117 | } |
| 118 | |
| 119 | impl 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. |
| 134 | const G_AUDIENCE: u32 = 1; // rp_id is this verifier's own |
| 135 | const G_SESSION: u32 = 2; // the nonce was issued to this session |
| 136 | const G_REPLAY: u32 = 4; // the nonce is spent once |
| 137 | const G_ISSUER: u32 = 8; // the head is signed by a trusted issuer |
| 138 | const G_RING: u32 = 16; // the ring is the head's, by length and digest |
| 139 | const G_STATUS: u32 = 32; // a name is live with the presented key |
| 140 | |
| 141 | #[cfg(test)] |
| 142 | thread_local! { |
| 143 | static SKIP: std::cell::Cell<u32> = const { std::cell::Cell::new(0) }; |
| 144 | } |
| 145 | |
| 146 | #[cfg(test)] |
| 147 | fn on(g: u32) -> bool { SKIP.with(|s| s.get() & g == 0) } |
| 148 | |
| 149 | #[cfg(not(test))] |
| 150 | #[inline(always)] |
| 151 | fn on(_g: u32) -> bool { true } |
| 152 | |
| 153 | // ── Verifier ──────────────────────────────────────────────────────────────── |
| 154 | |
| 155 | struct 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 | |
| 161 | type 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. |
| 165 | struct Challenges { |
| 166 | issued: HashMap<[u8; NONCE_LEN], Issued>, |
| 167 | by_exp: BTreeSet<Slot>, |
| 168 | by_session: HashMap<[u8; 32], BTreeSet<Slot>>, |
| 169 | } |
| 170 | |
| 171 | impl 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. |
| 256 | pub 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 | |
| 266 | impl<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)] |
| 580 | mod 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 | } |