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