aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/challenge.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/access/challenge.rs')
-rw-r--r--src/access/challenge.rs114
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());
}
}