diff options
Diffstat (limited to 'src/access')
| -rw-r--r-- | src/access/challenge.rs | 114 |
1 files changed, 91 insertions, 23 deletions
diff --git a/src/access/challenge.rs b/src/access/challenge.rs index 64cafac..e61ea85 100644 --- a/src/access/challenge.rs +++ b/src/access/challenge.rs @@ -23,9 +23,8 @@ use crate::identity::NodeKey; pub(crate) const NAMESPACE: &str = "continuum-login"; /// What every challenge starts with. const PREFIX: &str = "cc1"; -/// How long a challenge is valid (ms). -pub(crate) const LIFETIME_MS: u64 = 5 * 60 * 1000; -/// Used challenges kept at most: past it, logins by key wait for some to expire. +/// Used challenges kept at most, of all users: past it, logins by key wait for some to +/// expire. A user's own are fewer (`--key-logins-per-user`). const MAX_USED: usize = 10_000; #[derive(Debug, Serialize, Deserialize)] @@ -41,13 +40,13 @@ fn message(payload: &[u8]) -> Vec<u8> { message } -/// A new challenge by `key`'s node, valid from `now` for `LIFETIME_MS`; with when it +/// A new challenge by `key`'s node, valid from `now` for `lifetime_ms`; with when it /// expires. -pub(crate) fn issue(key: &NodeKey, now: u64) -> (String, u64) { +pub(crate) fn issue(key: &NodeKey, now: u64, lifetime_ms: u64) -> (String, u64) { let payload = Payload { node: key.id(), nonce: registry::new_id(), - exp: now.saturating_add(LIFETIME_MS), + exp: now.saturating_add(lifetime_ms), }; let bytes = serde_json::to_vec(&payload).expect("a challenge serializes"); let signature = key.sign(&message(&bytes)); @@ -61,13 +60,15 @@ pub(crate) fn issue(key: &NodeKey, now: u64) -> (String, u64) { ) } -/// The challenges used, by nonce, with when each expires. +/// The challenges used, by nonce, with when each expires and the user it logged in. #[derive(Debug, Default)] -pub(crate) struct Used(HashMap<String, u64>); +pub(crate) struct Used(HashMap<String, (u64, String)>); /// Takes `signature` (armored) over `challenge` as a login of `user`, whose registry /// `doc` holds: a challenge of `key`'s node, unexpired at `now`, not used before (then -/// used, in `used`), signed by one of `user`'s keys, `user` enabled. Why not, if not. +/// used, in `used`), signed by one of `user`'s keys, `user` enabled, with fewer than +/// `per_user` of its own still remembered. Why not, if not. +#[allow(clippy::too_many_arguments)] pub(crate) fn take( doc: &impl ReadDoc, key: &NodeKey, @@ -76,6 +77,7 @@ pub(crate) fn take( user: &str, now: u64, used: &mut Used, + per_user: usize, ) -> Result<(), String> { let mut parts = challenge.split('.'); let (Some(PREFIX), Some(payload), Some(signed), None) = @@ -102,10 +104,15 @@ pub(crate) fn take( if payload.exp <= now { return Err("the challenge expired: ask for another".to_owned()); } - used.0.retain(|_, exp| *exp > now); + used.0.retain(|_, (exp, _)| *exp > now); if used.0.contains_key(&payload.nonce) { return Err("the challenge was used: ask for another".to_owned()); } + if used.0.values().filter(|(_, by)| by == user).count() >= per_user { + return Err(format!( + "{user} logged in by key too often of late: try again in a while" + )); + } if used.0.len() >= MAX_USED { return Err("too many logins by key at once: try again in a while".to_owned()); } @@ -117,7 +124,7 @@ pub(crate) fn take( if !entry.keys.values().any(|known| *known == signer) { return Err(format!("{user} has no key {}", signer.fingerprint())); } - used.0.insert(payload.nonce, payload.exp); + used.0.insert(payload.nonce, (payload.exp, user.to_owned())); Ok(()) } @@ -163,20 +170,40 @@ mod tests { let stranger = SigningKey::from_bytes(&[6; 32]); let doc = registry_with(&key); let mut used = Used::default(); - let (challenge, exp) = issue(&node, 1000); - assert_eq!(exp, 1000 + LIFETIME_MS); + let (challenge, exp) = issue(&node, 1000, 300_000); + assert_eq!(exp, 301_000); let signed = sshsig::sign(&key, NAMESPACE, challenge.as_bytes()); // Another user, a disabled one, an unknown one, another key: refused. - assert!(take(&doc, &node, &challenge, &signed, "bob", 1001, &mut used).is_err()); - assert!(take(&doc, &node, &challenge, &signed, "carol", 1001, &mut used).is_err()); - assert!(take(&doc, &node, &challenge, &signed, "dave", 1001, &mut used).is_err()); + assert!( + take( + &doc, &node, &challenge, &signed, "bob", 1001, &mut used, 100 + ) + .is_err() + ); + assert!( + take( + &doc, &node, &challenge, &signed, "carol", 1001, &mut used, 100 + ) + .is_err() + ); + assert!( + take( + &doc, &node, &challenge, &signed, "dave", 1001, &mut used, 100 + ) + .is_err() + ); let foreign = sshsig::sign(&stranger, NAMESPACE, challenge.as_bytes()); - assert!(take(&doc, &node, &challenge, &foreign, "alice", 1001, &mut used).is_err()); + assert!( + take( + &doc, &node, &challenge, &foreign, "alice", 1001, &mut used, 100 + ) + .is_err() + ); // For another purpose: refused. let elsewhere = sshsig::sign(&key, "continuum-value", challenge.as_bytes()); assert!( take( - &doc, &node, &challenge, &elsewhere, "alice", 1001, &mut used + &doc, &node, &challenge, &elsewhere, "alice", 1001, &mut used, 100 ) .is_err() ); @@ -189,17 +216,58 @@ mod tests { &signed, "alice", 1001, - &mut used + &mut used, + 100 + ) + .is_err() + ); + assert!( + take( + &doc, &node, &challenge, &signed, "alice", exp, &mut used, 100 ) .is_err() ); - assert!(take(&doc, &node, &challenge, &signed, "alice", exp, &mut used).is_err()); // Taken, once. - assert!(take(&doc, &node, &challenge, &signed, "alice", 1001, &mut used).is_ok()); - assert!(take(&doc, &node, &challenge, &signed, "alice", 1002, &mut used).is_err()); + assert!( + take( + &doc, &node, &challenge, &signed, "alice", 1001, &mut used, 100 + ) + .is_ok() + ); + assert!( + take( + &doc, &node, &challenge, &signed, "alice", 1002, &mut used, 100 + ) + .is_err() + ); // Tampered with. let tampered = challenge.replacen("cc1.", "cc1.e30", 1); let resigned = sshsig::sign(&key, NAMESPACE, tampered.as_bytes()); - assert!(take(&doc, &node, &tampered, &resigned, "alice", 1001, &mut used).is_err()); + assert!( + take( + &doc, &node, &tampered, &resigned, "alice", 1001, &mut used, 100 + ) + .is_err() + ); + } + + /// A user's logins by key, as many as it may within a challenge's lifetime; past + /// them it waits, until those remembered expire. + #[test] + fn logins_by_key_per_user() { + let node = NodeKey::from_seed([1; 32]); + let key = SigningKey::from_bytes(&[5; 32]); + let doc = registry_with(&key); + let mut used = Used::default(); + let login = |now: u64, used: &mut Used| { + let (challenge, _) = issue(&node, now, 1_000); + let signed = sshsig::sign(&key, NAMESPACE, challenge.as_bytes()); + take(&doc, &node, &challenge, &signed, "alice", now, used, 2) + }; + assert!(login(10, &mut used).is_ok()); + assert!(login(20, &mut used).is_ok()); + assert!(login(30, &mut used).unwrap_err().contains("too often")); + // Those remembered expired: in again. + assert!(login(1_015, &mut used).is_ok()); } } |
