OXYGENHUB

Coverage / crates/spectra-core/src/authn.rs 97.6%

41 of 42 lines run, 2 of 6 functions · run #30 on main · run missed

1
//! Authenticating a request (spec §8.5): a signature by a known key over the
2
//! method, the body, a time within five minutes and a nonce not seen in
3
//! that window, from a key the owner delegates to now, and, when the agent
4
//! is bound to a Unix user, from that user. What it returns becomes the
5
//! `auth` of every event the request causes.
6
7
use crate::event::{Auth, Method};
8
use crate::principals::{Principal, Principals};
9
use crate::proof::{Proof, WINDOW_MS};
10
use std::collections::HashMap;
11
use std::sync::Mutex;
12
13
/// Why a request was refused.
14
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
15
pub enum Refusal {
16
    #[error("the request is not signed")]
17
    Unsigned,
18
    #[error("the signature is for {0}, not this request")]
19
    WrongMethod(String),
20
    #[error("the request's time is {0} s from the server's: check the clock")]
21
    Stale(i64),
22
    #[error("the request's nonce was used before: a replay")]
23
    Replay,
24
    #[error("{0}")]
25
    NotAPrincipal(String),
26
    #[error("{0}")]
27
    BadSignature(String),
28
    /// The key is bound to another Unix user (logged as `guard.impersonation`).
29
    #[error("agent {agent}'s key is bound to uid {bound}, used from uid {from}")]
30
    WrongUid { agent: String, bound: u32, from: u32 },
31
    /// The key is bound to a Unix user but came over TCP, where there is no
32
    /// peer uid to check (spec §8.6; logged as `guard.impersonation`).
33
    #[error("agent {agent}'s key is bound to uid {bound} and must come over the Unix socket, not the network")]
34
    BoundOverNetwork { agent: String, bound: u32 },
35
}
36
37
/// The outcome: who, and the `auth` for their events.
38
#[derive(Clone, Debug)]
39
pub struct Caller {
40
    pub principal: Principal,
41
    pub auth: Auth,
42
    /// The agent asked for its uid to be learnt and has none yet: the
43
    /// caller records `agent.uid_bound` with this uid.
44
    pub learn_uid: Option<u32>,
45
}
46
47
/// Remembers nonces for the window.
48
#[derive(Debug, Default)]
49
pub struct Authenticator {
50
    nonces: Mutex<HashMap<[u8; 16], u64>>,
51
}
52
53
impl Authenticator {
54139×
    pub fn authenticate(
55139×
        &self,
56139×
        principals: &Principals,
57139×
        proof: Option<&Proof>,
58139×
        method: &str,
59139×
        body: &[u8],
60139×
        now_ms: u64,
61139×
        peer_uid: Option<u32>,
62139×
    ) -> Result<Caller, Refusal> {
63139×
        let proof = proof.ok_or(Refusal::Unsigned)?;
64136×
        if proof.method != method {
651×
            return Err(Refusal::WrongMethod(proof.method.clone()));
66135×
        }
67135×
        let skew = proof.time_ms as i64 - now_ms as i64;
68135×
        if skew.unsigned_abs() > WINDOW_MS {
691×
            return Err(Refusal::Stale(skew / 1000));
70134×
        }
71134×
        let key = crate::identity::KeyId(proof.key());
72134×
        let principal = principals.principal(&key, now_ms).map_err(Refusal::NotAPrincipal)?;
73132×
        let identity = principals.identity(&key).ok_or_else(|| Refusal::NotAPrincipal("an unknown key".into()))?;
74132×
        proof.verify(identity, Some(body)).map_err(Refusal::BadSignature)?;
75131×
        let mut learn_uid = None;
76131×
        if let Principal::Agent { name, uid, learn_uid: learn, .. } = &principal {
7777×
            match (uid, peer_uid) {
784×
                (Some(bound), Some(from)) if *bound != from => return Err(Refusal::WrongUid { agent: name.clone(), bound: *bound, from }),
791×
                (Some(bound), None) => return Err(Refusal::BoundOverNetwork { agent: name.clone(), bound: *bound }),
801×
                (None, Some(from)) if *learn => learn_uid = Some(from),
8173×
                _ => {}
82
            }
8354×
        }
84
        {
85128×
            let mut seen = self.nonces.lock().unwrap();
86877×
            seen.retain(|_, t| now_ms.saturating_sub(*t) <= 2 * WINDOW_MS);
87128×
            if seen.insert(proof.nonce, now_ms).is_some() {
881×
                return Err(Refusal::Replay);
89127×
            }
90
        }
91127×
        Ok(Caller {
92127×
            principal,
93127×
            auth: Auth { key: Some(proof.key()), method: Method::Signature, assurance: 2, proof: Some(proof.encode()) },
94127×
            learn_uid,
95127×
        })
96139×
    }
97
}