1use crate::canonical::{CanonicalAuthError, cert_hash, claims_hash, validate_scopes};
8use ic_auth_protocol_types::{
9 AuthRole, DelegatedRoleGrant, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
10 DelegationProof, IssuerProof, Principal,
11};
12use sha2::{Digest, Sha256};
13use thiserror::Error;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct TokenPreparationLimits {
18 pub max_grants: usize,
19 pub max_scopes_per_grant: usize,
20}
21
22pub struct PrepareDelegatedTokenInput<'a> {
24 pub proof: &'a DelegationProof,
27 pub authority_expires_at_ns: u64,
29 pub operation_id: [u8; 32],
32 pub prepared_by: Principal,
34 pub audience: DelegationAudience,
35 pub grants: Vec<DelegatedRoleGrant>,
36 pub ttl_ns: u64,
37 pub ext: Option<Vec<u8>>,
38 pub now_ns: u64,
39 pub limits: TokenPreparationLimits,
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct PreparedDelegatedToken {
46 claims: DelegatedTokenClaims,
47 claims_hash: [u8; 32],
48 proof: DelegationProof,
49}
50
51impl PreparedDelegatedToken {
52 pub const fn claims(&self) -> &DelegatedTokenClaims {
53 &self.claims
54 }
55
56 pub const fn claims_hash(&self) -> [u8; 32] {
58 self.claims_hash
59 }
60
61 pub const fn proof(&self) -> &DelegationProof {
62 &self.proof
63 }
64}
65
66#[derive(Debug, Eq, Error, PartialEq)]
69pub enum PrepareDelegatedTokenError {
70 #[error("delegation certificate is not yet valid")]
71 CertNotYetValid,
72 #[error("delegation certificate or local authority expired")]
73 CertExpired,
74 #[error("token TTL must be greater than zero")]
75 TokenTtlZero,
76 #[error("token TTL {ttl_ns} exceeds certificate maximum {max_ttl_ns}")]
77 TokenTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
78 #[error("token expiry overflows")]
79 TokenExpiresAtOverflow,
80 #[error("token outlives certificate or local authority")]
81 TokenOutlivesCert,
82 #[error("token grants must not be empty")]
83 GrantsEmpty,
84 #[error("token grants {found} exceed host maximum {max}")]
85 TooManyGrants { found: usize, max: usize },
86 #[error("grant for {role:?} has no scopes")]
87 EmptyGrantScopes { role: AuthRole },
88 #[error("grant for {role:?} has {found} scopes exceeding host maximum {max}")]
89 TooManyGrantScopes {
90 role: AuthRole,
91 found: usize,
92 max: usize,
93 },
94 #[error("token audience is not a subset of certificate audience")]
95 AudienceNotSubset,
96 #[error("token grants are not a subset of certificate grants")]
97 GrantsNotSubset,
98 #[error(transparent)]
99 Canonical(#[from] CanonicalAuthError),
100}
101
102pub fn prepare_delegated_token(
106 input: PrepareDelegatedTokenInput<'_>,
107) -> Result<PreparedDelegatedToken, PrepareDelegatedTokenError> {
108 use PrepareDelegatedTokenError as Error;
109 let cert = &input.proof.cert;
110 let deadline = cert.expires_at_ns.min(input.authority_expires_at_ns);
111 if input.now_ns < cert.not_before_ns {
112 return Err(Error::CertNotYetValid);
113 }
114 if input.now_ns >= deadline {
115 return Err(Error::CertExpired);
116 }
117 if input.ttl_ns == 0 {
118 return Err(Error::TokenTtlZero);
119 }
120 if input.ttl_ns > cert.max_token_ttl_ns {
121 return Err(Error::TokenTtlExceeded {
122 ttl_ns: input.ttl_ns,
123 max_ttl_ns: cert.max_token_ttl_ns,
124 });
125 }
126 let expires_at_ns = input
127 .now_ns
128 .checked_add(input.ttl_ns)
129 .ok_or(Error::TokenExpiresAtOverflow)?;
130 if expires_at_ns > deadline {
131 return Err(Error::TokenOutlivesCert);
132 }
133 if input.grants.is_empty() {
134 return Err(Error::GrantsEmpty);
135 }
136 if input.grants.len() > input.limits.max_grants {
137 return Err(Error::TooManyGrants {
138 found: input.grants.len(),
139 max: input.limits.max_grants,
140 });
141 }
142 let mut previous = None;
143 for grant in &input.grants {
144 let role = grant.target.as_str().as_bytes();
145 if previous.is_some_and(|previous| previous >= role) {
146 return Err(CanonicalAuthError::NonCanonicalRoles.into());
147 }
148 previous = Some(role);
149 if grant.scopes.is_empty() {
150 return Err(Error::EmptyGrantScopes {
151 role: grant.target.clone(),
152 });
153 }
154 if grant.scopes.len() > input.limits.max_scopes_per_grant {
155 return Err(Error::TooManyGrantScopes {
156 role: grant.target.clone(),
157 found: grant.scopes.len(),
158 max: input.limits.max_scopes_per_grant,
159 });
160 }
161 validate_scopes(&grant.scopes)?;
162 }
163 if input.audience != cert.aud {
164 return Err(Error::AudienceNotSubset);
165 }
166 if !input.grants.iter().all(|child| {
167 cert.grants
168 .iter()
169 .find(|parent| parent.target == child.target)
170 .is_some_and(|parent| {
171 child
172 .scopes
173 .iter()
174 .all(|scope| parent.scopes.contains(scope))
175 })
176 }) {
177 return Err(Error::GrantsNotSubset);
178 }
179 let cert_hash = cert_hash(cert)?;
180 let nonce = delegated_token_nonce(
181 input.prepared_by,
182 input.operation_id,
183 cert.issuer_pid,
184 cert_hash,
185 );
186 let claims = DelegatedTokenClaims {
187 presenter: input.prepared_by,
188 subject: input.prepared_by,
189 issuer_pid: cert.issuer_pid,
190 cert_hash,
191 issued_at_ns: input.now_ns,
192 expires_at_ns,
193 aud: input.audience,
194 grants: input.grants,
195 nonce,
196 ext: input.ext,
197 };
198 Ok(PreparedDelegatedToken {
199 claims_hash: claims_hash(&claims)?,
200 claims,
201 proof: input.proof.clone(),
202 })
203}
204
205fn delegated_token_nonce(
206 presenter: Principal,
207 operation_id: [u8; 32],
208 issuer: Principal,
209 cert_hash: [u8; 32],
210) -> [u8; 16] {
211 let mut hash = Sha256::new();
213 hash.update(b"canic-token-nonce-v1");
214 hash.update(presenter.as_slice());
215 hash.update(operation_id);
216 hash.update(issuer.as_slice());
217 hash.update(cert_hash);
218 let digest: [u8; 32] = hash.finalize().into();
219 let mut nonce = [0; 16];
220 nonce.copy_from_slice(&digest[..16]);
221 nonce
222}
223
224pub fn finish_delegated_token(
228 prepared: PreparedDelegatedToken,
229 issuer_proof: IssuerProof,
230) -> DelegatedToken {
231 DelegatedToken {
232 claims: prepared.claims,
233 proof: prepared.proof,
234 issuer_proof,
235 }
236}