Skip to main content

ic_auth/
session_request.rs

1//! Canonical session request identity, independent of proof verification or storage.
2//!
3//! Hashing a request does not authenticate a token, select host TTL policy or
4//! grant scopes. Session admission and replay remain separate engine obligations.
5
6use crate::canonical::{CanonicalAuthError, validate_scope_label};
7use sha2::{Digest, Sha256};
8use thiserror::Error;
9
10/// Request shape rejection, without cryptographic or session-policy causes.
11#[derive(Debug, Error, Eq, PartialEq)]
12pub enum SessionRequestError {
13    #[error("session request has invalid scope bounds, duplicate scopes or zero TTL")]
14    InvalidRequest,
15    #[error(transparent)]
16    Canonical(#[from] CanonicalAuthError),
17}
18
19/// Canonical narrowing request: 1..=16 unique scopes, at most 1024 scope bytes.
20/// Input order is normalized before hashing; duplicates are rejected.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct SessionRequest {
23    scopes: Vec<String>,
24    ttl_secs: Option<u64>,
25}
26impl SessionRequest {
27    pub fn new(
28        mut scopes: Vec<String>,
29        ttl_secs: Option<u64>,
30    ) -> Result<Self, SessionRequestError> {
31        check_scopes(&scopes)?;
32        scopes.sort_unstable();
33        if scopes.windows(2).any(|pair| pair[0] == pair[1]) || ttl_secs == Some(0) {
34            return Err(SessionRequestError::InvalidRequest);
35        }
36        Ok(Self { scopes, ttl_secs })
37    }
38    /// Exact requested TTL in seconds; None leaves selection to protected host policy.
39    pub const fn ttl_secs(&self) -> Option<u64> {
40        self.ttl_secs
41    }
42    pub fn scopes(&self) -> &[String] {
43        &self.scopes
44    }
45    /// Exact Canic V1 request hash; None and an explicit default TTL differ.
46    pub fn hash(&self) -> [u8; 32] {
47        let mut hash = Sha256::new();
48        hash.update(b"canic-application-session-request-v1");
49        hash.update((self.scopes.len() as u64).to_be_bytes());
50        for scope in &self.scopes {
51            hash.update((scope.len() as u64).to_be_bytes());
52            hash.update(scope.as_bytes());
53        }
54        match self.ttl_secs {
55            None => hash.update([0]),
56            Some(ttl) => {
57                hash.update([1]);
58                hash.update(ttl.to_be_bytes());
59            }
60        }
61        hash.finalize().into()
62    }
63}
64
65pub(crate) fn check_scopes(scopes: &[String]) -> Result<(), SessionRequestError> {
66    if scopes.is_empty() || scopes.len() > 16 {
67        return Err(SessionRequestError::InvalidRequest);
68    }
69    let mut bytes = 0usize;
70    for scope in scopes {
71        validate_scope_label(scope)?;
72        bytes += scope.len(); // Each validated scope is <=64 bytes, count <=16.
73    }
74    if bytes > 1024 {
75        return Err(SessionRequestError::InvalidRequest);
76    }
77    Ok(())
78}