1use crate::canonical::{CanonicalAuthError, cert_hash, claims_hash};
8use crate::certificate_issuance::{GrantValidationError, grants_are_subset, 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 !grants_are_subset(&input.grants, &cert.grants) {
157 return Err(Error::GrantsNotSubset);
158 }
159 let cert_hash = cert_hash(cert)?;
160 let nonce = delegated_token_nonce(
161 input.prepared_by,
162 input.operation_id,
163 cert.issuer_pid,
164 cert_hash,
165 );
166 let claims = DelegatedTokenClaims {
167 presenter: input.prepared_by,
168 subject: input.prepared_by,
169 issuer_pid: cert.issuer_pid,
170 cert_hash,
171 issued_at_ns: input.now_ns,
172 expires_at_ns,
173 aud: input.audience,
174 grants: input.grants,
175 nonce,
176 ext: input.ext,
177 };
178 Ok(PreparedDelegatedToken {
179 claims_hash: claims_hash(&claims)?,
180 claims,
181 proof: input.proof.clone(),
182 })
183}
184
185fn delegated_token_nonce(
186 presenter: Principal,
187 operation_id: [u8; 32],
188 issuer: Principal,
189 cert_hash: [u8; 32],
190) -> [u8; 16] {
191 let mut hash = Sha256::new();
193 hash.update(b"canic-token-nonce-v1");
194 hash.update(presenter.as_slice());
195 hash.update(operation_id);
196 hash.update(issuer.as_slice());
197 hash.update(cert_hash);
198 let digest: [u8; 32] = hash.finalize().into();
199 let mut nonce = [0; 16];
200 nonce.copy_from_slice(&digest[..16]);
201 nonce
202}
203
204pub fn finish_delegated_token(
208 prepared: PreparedDelegatedToken,
209 issuer_proof: IssuerProof,
210) -> DelegatedToken {
211 DelegatedToken {
212 claims: prepared.claims,
213 proof: prepared.proof,
214 issuer_proof,
215 }
216}