ic_auth/
session_request.rs1use crate::canonical::{CanonicalAuthError, validate_scope_label};
7use sha2::{Digest, Sha256};
8use thiserror::Error;
9
10#[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("invalid session TTL policy, requested lifetime or expiry overflow")]
16 InvalidLifetime,
17 #[error("requested scopes are not granted for the protected receiver role")]
18 ScopeRejected,
19 #[error(transparent)]
20 Canonical(#[from] CanonicalAuthError),
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct SessionRequest {
27 scopes: Vec<String>,
28 ttl_secs: Option<u64>,
29}
30impl SessionRequest {
31 pub fn new(
32 mut scopes: Vec<String>,
33 ttl_secs: Option<u64>,
34 ) -> Result<Self, SessionRequestError> {
35 check_scopes(&scopes)?;
36 scopes.sort_unstable();
37 if scopes.windows(2).any(|pair| pair[0] == pair[1]) || ttl_secs == Some(0) {
38 return Err(SessionRequestError::InvalidRequest);
39 }
40 Ok(Self { scopes, ttl_secs })
41 }
42 pub const fn ttl_secs(&self) -> Option<u64> {
44 self.ttl_secs
45 }
46 pub fn scopes(&self) -> &[String] {
47 &self.scopes
48 }
49 pub fn expires_at_ns(
53 &self,
54 now_ns: u64,
55 default_ttl_secs: u64,
56 max_ttl_secs: u64,
57 ) -> Result<u64, SessionRequestError> {
58 validate_ttl_policy(default_ttl_secs, max_ttl_secs)?;
59 let ttl = self.ttl_secs.unwrap_or(default_ttl_secs);
60 if ttl == 0 || ttl > max_ttl_secs {
61 return Err(SessionRequestError::InvalidLifetime);
62 }
63 now_ns
64 .checked_add(ttl * crate::limits::NS_PER_SECOND)
65 .ok_or(SessionRequestError::InvalidLifetime)
66 }
67 pub(crate) fn check_granted_scopes(
68 &self,
69 granted: &[String],
70 ) -> Result<(), SessionRequestError> {
71 if self.scopes.iter().any(|scope| !granted.contains(scope)) {
72 return Err(SessionRequestError::ScopeRejected);
73 }
74 Ok(())
75 }
76 pub fn hash(&self) -> [u8; 32] {
78 let mut hash = Sha256::new();
79 hash.update(b"canic-application-session-request-v1");
80 hash.update((self.scopes.len() as u64).to_be_bytes());
81 for scope in &self.scopes {
82 hash.update((scope.len() as u64).to_be_bytes());
83 hash.update(scope.as_bytes());
84 }
85 match self.ttl_secs {
86 None => hash.update([0]),
87 Some(ttl) => {
88 hash.update([1]);
89 hash.update(ttl.to_be_bytes());
90 }
91 }
92 hash.finalize().into()
93 }
94}
95
96pub(crate) fn validate_ttl_policy(default: u64, maximum: u64) -> Result<(), SessionRequestError> {
97 if default == 0 || default > maximum || maximum > crate::limits::MAX_SESSION_SECS {
98 return Err(SessionRequestError::InvalidLifetime);
99 }
100 Ok(())
101}
102
103pub(crate) fn check_scopes(scopes: &[String]) -> Result<(), SessionRequestError> {
104 if scopes.is_empty() || scopes.len() > 16 {
105 return Err(SessionRequestError::InvalidRequest);
106 }
107 for scope in scopes {
109 validate_scope_label(scope)?;
110 }
111 Ok(())
112}