ic_auth/
session_request.rs1use crate::canonical::{CanonicalAuthError, validate_scope_label};
7use ic_auth_protocol_types::{AuthRole, DelegatedRoleGrant};
8use sha2::{Digest, Sha256};
9use thiserror::Error;
10
11#[derive(Debug, Error, Eq, PartialEq)]
13pub enum SessionRequestError {
14 #[error("session request has invalid scope bounds, duplicate scopes or zero TTL")]
15 InvalidRequest,
16 #[error("invalid session TTL policy, requested lifetime or expiry overflow")]
17 InvalidLifetime,
18 #[error("requested scopes are not granted for the protected receiver role")]
19 ScopeRejected,
20 #[error(transparent)]
21 Canonical(#[from] CanonicalAuthError),
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub struct SessionTtlLimit(u64);
33impl SessionTtlLimit {
34 pub fn new(seconds: u64) -> Result<Self, SessionRequestError> {
35 if seconds == 0 || seconds > crate::limits::MAX_SESSION_SECS {
36 return Err(SessionRequestError::InvalidLifetime);
37 }
38 Ok(Self(seconds))
39 }
40 pub const fn seconds(self) -> u64 {
41 self.0
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48pub struct SessionTtlPolicy {
49 default_secs: u64,
50 maximum: SessionTtlLimit,
51}
52impl SessionTtlPolicy {
53 pub fn new(default_secs: u64, maximum_secs: u64) -> Result<Self, SessionRequestError> {
54 let maximum = SessionTtlLimit::new(maximum_secs)?;
55 if default_secs == 0 || default_secs > maximum_secs {
56 return Err(SessionRequestError::InvalidLifetime);
57 }
58 Ok(Self {
59 default_secs,
60 maximum,
61 })
62 }
63 pub const fn default_secs(self) -> u64 {
64 self.default_secs
65 }
66 pub const fn maximum(self) -> SessionTtlLimit {
67 self.maximum
68 }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct SessionRequest {
75 scopes: Vec<String>,
76 ttl_secs: Option<u64>,
77}
78impl SessionRequest {
79 pub fn new(
80 mut scopes: Vec<String>,
81 ttl_secs: Option<u64>,
82 ) -> Result<Self, SessionRequestError> {
83 check_scopes(&scopes)?;
84 scopes.sort_unstable();
85 if scopes.windows(2).any(|pair| pair[0] == pair[1]) || ttl_secs == Some(0) {
86 return Err(SessionRequestError::InvalidRequest);
87 }
88 Ok(Self { scopes, ttl_secs })
89 }
90 pub const fn ttl_secs(&self) -> Option<u64> {
92 self.ttl_secs
93 }
94 pub fn scopes(&self) -> &[String] {
95 &self.scopes
96 }
97 #[cfg(all(
98 feature = "sessions",
99 any(
100 feature = "portable-signature-verification",
101 feature = "certified-proof-verification"
102 )
103 ))]
104 pub(crate) fn into_scopes(self) -> Vec<String> {
105 self.scopes
106 }
107 pub fn expires_at_ns(
111 &self,
112 now_ns: u64,
113 policy: SessionTtlPolicy,
114 ) -> Result<u64, SessionRequestError> {
115 let ttl = self.ttl_secs.unwrap_or(policy.default_secs());
116 if ttl > policy.maximum().seconds() {
117 return Err(SessionRequestError::InvalidLifetime);
118 }
119 now_ns
120 .checked_add(ttl * crate::limits::NS_PER_SECOND)
121 .ok_or(SessionRequestError::InvalidLifetime)
122 }
123 fn check_granted_scopes(&self, granted: &[String]) -> Result<(), SessionRequestError> {
124 if !crate::certificate_issuance::scopes_are_subset(&self.scopes, granted) {
125 return Err(SessionRequestError::ScopeRejected);
126 }
127 Ok(())
128 }
129 pub(crate) fn evaluate(
133 &self,
134 grants: &[DelegatedRoleGrant],
135 role: &AuthRole,
136 now_ns: u64,
137 policy: SessionTtlPolicy,
138 ) -> Result<u64, SessionRequestError> {
139 let grant = crate::certificate_issuance::grant_for_role(grants, role)
140 .ok_or(SessionRequestError::ScopeRejected)?;
141 self.check_granted_scopes(&grant.scopes)?;
142 self.expires_at_ns(now_ns, policy)
143 }
144 pub fn hash(&self) -> [u8; 32] {
146 let mut hash = Sha256::new();
147 hash.update(b"canic-application-session-request-v1");
148 hash.update((self.scopes.len() as u64).to_be_bytes());
149 for scope in &self.scopes {
150 hash.update((scope.len() as u64).to_be_bytes());
151 hash.update(scope.as_bytes());
152 }
153 match self.ttl_secs {
154 None => hash.update([0]),
155 Some(ttl) => {
156 hash.update([1]);
157 hash.update(ttl.to_be_bytes());
158 }
159 }
160 hash.finalize().into()
161 }
162}
163
164pub(crate) fn check_scopes(scopes: &[String]) -> Result<(), SessionRequestError> {
165 if scopes.is_empty() || scopes.len() > crate::limits::MAX_SESSION_SCOPES {
166 return Err(SessionRequestError::InvalidRequest);
167 }
168 for scope in scopes {
170 validate_scope_label(scope)?;
171 }
172 Ok(())
173}