ic_auth/
session_request.rs1use crate::canonical::CanonicalAuthError;
7use ic_auth_protocol_types::{AuthRole, DelegatedRoleGrant, ScopeLabel};
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<ScopeLabel>,
76 ttl_secs: Option<u64>,
77}
78impl SessionRequest {
79 pub fn new(scopes: Vec<String>, ttl_secs: Option<u64>) -> Result<Self, SessionRequestError> {
80 check_scope_count(scopes.len())?;
81 let mut scopes = ScopeLabel::from_strings(scopes).map_err(CanonicalAuthError::from)?;
82 scopes.sort_unstable();
83 if scopes.windows(2).any(|pair| pair[0] == pair[1]) || ttl_secs == Some(0) {
84 return Err(SessionRequestError::InvalidRequest);
85 }
86 Ok(Self { scopes, ttl_secs })
87 }
88 pub const fn ttl_secs(&self) -> Option<u64> {
90 self.ttl_secs
91 }
92 pub fn scopes(&self) -> &[ScopeLabel] {
93 &self.scopes
94 }
95 #[cfg(all(
96 feature = "sessions",
97 any(
98 feature = "portable-signature-verification",
99 feature = "certified-proof-verification"
100 )
101 ))]
102 pub(crate) fn into_scopes(self) -> Vec<String> {
103 self.scopes
104 .into_iter()
105 .map(ScopeLabel::into_string)
106 .collect()
107 }
108 pub fn expires_at_ns(
112 &self,
113 now_ns: u64,
114 policy: SessionTtlPolicy,
115 ) -> Result<u64, SessionRequestError> {
116 let ttl = self.ttl_secs.unwrap_or(policy.default_secs());
117 if ttl > policy.maximum().seconds() {
118 return Err(SessionRequestError::InvalidLifetime);
119 }
120 now_ns
121 .checked_add(ttl * crate::limits::NS_PER_SECOND)
122 .ok_or(SessionRequestError::InvalidLifetime)
123 }
124 fn check_granted_scopes(&self, granted: &[String]) -> Result<(), SessionRequestError> {
125 if !crate::certificate_issuance::scopes_are_subset(&self.scopes, granted) {
126 return Err(SessionRequestError::ScopeRejected);
127 }
128 Ok(())
129 }
130 pub(crate) fn evaluate(
134 &self,
135 grants: &[DelegatedRoleGrant],
136 role: &AuthRole,
137 now_ns: u64,
138 policy: SessionTtlPolicy,
139 ) -> Result<u64, SessionRequestError> {
140 let grant = crate::certificate_issuance::grant_for_role(grants, role)
141 .ok_or(SessionRequestError::ScopeRejected)?;
142 self.check_granted_scopes(&grant.scopes)?;
143 self.expires_at_ns(now_ns, policy)
144 }
145 pub fn hash(&self) -> [u8; 32] {
147 let mut hash = Sha256::new();
148 hash.update(b"canic-application-session-request-v1");
149 hash.update((self.scopes.len() as u64).to_be_bytes());
150 for scope in &self.scopes {
151 hash.update((scope.as_str().len() as u64).to_be_bytes());
152 hash.update(scope.as_str().as_bytes());
153 }
154 match self.ttl_secs {
155 None => hash.update([0]),
156 Some(ttl) => {
157 hash.update([1]);
158 hash.update(ttl.to_be_bytes());
159 }
160 }
161 hash.finalize().into()
162 }
163}
164
165#[cfg(feature = "session-restore")]
166pub(crate) fn check_scopes(scopes: &[String]) -> Result<(), SessionRequestError> {
167 check_scope_count(scopes.len())?;
168 for scope in scopes {
170 crate::canonical::validate_scope_label(scope)?;
171 }
172 Ok(())
173}
174
175fn check_scope_count(count: usize) -> Result<(), SessionRequestError> {
176 if count == 0 || count > crate::limits::MAX_SESSION_SCOPES {
177 return Err(SessionRequestError::InvalidRequest);
178 }
179 Ok(())
180}