1use ic_auth_protocol_types::*;
6use sha2::{Digest, Sha256};
7use thiserror::Error;
8
9const DOMAIN_SEPARATOR: &[u8] = b"CANIC-AUTH\0";
10const ISSUER_PROOF_BINDING_HASH_DOMAIN: &[u8] = b"canic-issuer-proof-binding-v1";
11const CHAIN_KEY_BATCH_HEADER_DOMAIN: &[u8] = b"CANIC_ROOT_DELEGATION_CHAIN_KEY_BATCH_V1";
12const CHAIN_KEY_DELEGATION_CERT_DOMAIN: &[u8] = b"CANIC_ROOT_DELEGATION_CHAIN_KEY_ISSUER_LEAF_V1";
13const CHAIN_KEY_DERIVATION_PATH_DOMAIN: &[u8] =
14 b"CANIC_ROOT_DELEGATION_CHAIN_KEY_DERIVATION_PATH_V1";
15pub const MAX_TOKEN_EXT_BYTES: usize = 4096;
17
18#[repr(u8)]
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21enum CanonicalDomain {
22 DelegationCert = 1,
23 DelegatedTokenClaims = 2,
24 DelegationProof = 3,
25 RoleHash = 4,
26 IssuerProof = 6,
27}
28
29#[derive(Debug, Eq, Error, PartialEq)]
36pub enum CanonicalAuthError {
37 #[error("canonical vector length exceeds u32")]
38 LengthOverflow,
39 #[error("delegated auth scope is empty")]
40 EmptyScope,
41 #[error("delegated auth scope contains invalid characters: {scope}")]
42 InvalidScope { scope: String },
43 #[error("delegated auth scopes must be strictly sorted and unique")]
44 NonCanonicalScopes,
45 #[error("delegated auth role grants must be strictly sorted and unique")]
46 NonCanonicalRoles,
47 #[error("delegated auth token ext is {len} bytes and exceeds max {max} bytes")]
48 TokenExtTooLarge { len: usize, max: usize },
49}
50
51pub fn cert_hash(cert: &DelegationCert) -> Result<[u8; 32], CanonicalAuthError> {
53 Ok(hash_bytes(&cert_bytes(cert)?))
54}
55
56pub fn claims_hash(claims: &DelegatedTokenClaims) -> Result<[u8; 32], CanonicalAuthError> {
58 Ok(hash_bytes(&claims_bytes(claims)?))
59}
60
61pub fn proof_hash(proof: &DelegationProof) -> Result<[u8; 32], CanonicalAuthError> {
63 Ok(hash_bytes(&proof_bytes(proof)?))
64}
65
66pub fn issuer_proof_hash(proof: &IssuerProof) -> Result<[u8; 32], CanonicalAuthError> {
68 Ok(hash_bytes(&issuer_proof_bytes(proof)?))
69}
70
71pub fn chain_key_batch_header_hash(
73 header: &ChainKeyBatchHeaderV1,
74) -> Result<[u8; 32], CanonicalAuthError> {
75 hash_chain_key_header_payload(&chain_key_batch_header_bytes(header)?)
76}
77
78pub fn chain_key_delegation_cert_hash(
80 cert: &ChainKeyDelegationCertV1,
81) -> Result<[u8; 32], CanonicalAuthError> {
82 hash_chain_key_leaf_payload(&chain_key_delegation_cert_bytes(cert)?)
83}
84
85pub fn chain_key_derivation_path_hash(
88 derivation_path: &[Vec<u8>],
89) -> Result<[u8; 32], CanonicalAuthError> {
90 let mut out = CHAIN_KEY_DERIVATION_PATH_DOMAIN.to_vec();
91 encode_chain_key_derivation_path(&mut out, derivation_path)?;
92 Ok(hash_bytes(&out))
93}
94
95pub fn issuer_proof_binding_hash(
97 issuer_pid: Principal,
98 issuer_proof_alg: IssuerProofAlgorithm,
99 issuer_proof_binding: IssuerProofBinding,
100) -> Result<[u8; 32], CanonicalAuthError> {
101 let mut out = Vec::with_capacity(128);
102 out.extend_from_slice(ISSUER_PROOF_BINDING_HASH_DOMAIN);
103 encode_principal(&mut out, issuer_pid)?;
104 encode_issuer_proof_algorithm(&mut out, issuer_proof_alg);
105 encode_issuer_proof_binding(&mut out, issuer_proof_binding);
106 Ok(hash_bytes(&out))
107}
108
109pub fn role_hash(role: &AuthRole) -> Result<[u8; 32], CanonicalAuthError> {
111 let mut out = domain_bytes(CanonicalDomain::RoleHash);
112 encode_string(&mut out, role.as_str())?;
113 Ok(hash_bytes(&out))
114}
115
116pub fn cert_bytes(cert: &DelegationCert) -> Result<Vec<u8>, CanonicalAuthError> {
118 let mut out = domain_bytes(CanonicalDomain::DelegationCert);
119
120 encode_principal(&mut out, cert.root_pid)?;
121 encode_principal(&mut out, cert.issuer_pid)?;
122 encode_issuer_proof_algorithm(&mut out, cert.issuer_proof_alg);
123 encode_fixed_32(&mut out, cert.issuer_proof_binding_hash);
124 encode_issuer_proof_binding(&mut out, cert.issuer_proof_binding);
125 encode_u64(&mut out, cert.issued_at_ns);
126 encode_u64(&mut out, cert.not_before_ns);
127 encode_u64(&mut out, cert.expires_at_ns);
128 encode_u64(&mut out, cert.max_token_ttl_ns);
129 encode_audience(&mut out, &cert.aud);
130 encode_role_grants(&mut out, &cert.grants)?;
131
132 Ok(out)
133}
134
135pub fn claims_bytes(claims: &DelegatedTokenClaims) -> Result<Vec<u8>, CanonicalAuthError> {
137 let mut out = domain_bytes(CanonicalDomain::DelegatedTokenClaims);
138
139 encode_principal(&mut out, claims.presenter)?;
140 encode_principal(&mut out, claims.subject)?;
141 encode_principal(&mut out, claims.issuer_pid)?;
142 encode_fixed_32(&mut out, claims.cert_hash);
143 encode_u64(&mut out, claims.issued_at_ns);
144 encode_u64(&mut out, claims.expires_at_ns);
145 encode_audience(&mut out, &claims.aud);
146 encode_role_grants(&mut out, &claims.grants)?;
147 out.extend_from_slice(&claims.nonce);
148 encode_token_ext(&mut out, claims.ext.as_deref())?;
149
150 Ok(out)
151}
152
153fn proof_bytes(proof: &DelegationProof) -> Result<Vec<u8>, CanonicalAuthError> {
154 let mut out = domain_bytes(CanonicalDomain::DelegationProof);
155
156 out.extend_from_slice(&cert_bytes(&proof.cert)?);
157 encode_root_proof(&mut out, &proof.root_proof)?;
158
159 Ok(out)
160}
161
162fn issuer_proof_bytes(proof: &IssuerProof) -> Result<Vec<u8>, CanonicalAuthError> {
163 let mut out = domain_bytes(CanonicalDomain::IssuerProof);
164 encode_issuer_proof(&mut out, proof)?;
165 Ok(out)
166}
167
168fn domain_bytes(domain: CanonicalDomain) -> Vec<u8> {
169 let mut out = Vec::with_capacity(128);
170 out.extend_from_slice(DOMAIN_SEPARATOR);
171 out.push(domain as u8);
172 out
173}
174
175fn hash_bytes(bytes: &[u8]) -> [u8; 32] {
176 Sha256::digest(bytes).into()
177}
178
179fn encode_issuer_proof_algorithm(out: &mut Vec<u8>, alg: IssuerProofAlgorithm) {
180 let tag = match alg {
181 IssuerProofAlgorithm::IcCanisterSignatureV1 => 1,
182 };
183 out.push(tag);
184}
185
186fn encode_audience(out: &mut Vec<u8>, audience: &DelegationAudience) {
187 match audience {
188 DelegationAudience::Fleet(fleet) => {
189 out.push(1);
190 encode_fleet_key(out, *fleet);
191 }
192 }
193}
194
195fn encode_fleet_key(out: &mut Vec<u8>, fleet: AudienceId) {
196 encode_fixed_32(out, *fleet.canonical_network_id.as_bytes());
197 encode_fixed_32(out, *fleet.fleet_id.as_bytes());
198}
199
200fn encode_role_grants(
201 out: &mut Vec<u8>,
202 grants: &[DelegatedRoleGrant],
203) -> Result<(), CanonicalAuthError> {
204 encode_len(out, grants.len())?;
205 let mut previous = None;
206 for grant in grants {
207 let current = grant.target.as_str().as_bytes();
208 if previous.is_some_and(|previous| previous >= current) {
209 return Err(CanonicalAuthError::NonCanonicalRoles);
210 }
211 previous = Some(current);
212 encode_role(out, &grant.target)?;
213 encode_scopes(out, &grant.scopes)?;
214 }
215 Ok(())
216}
217
218fn hash_chain_key_header_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
219 let mut out = Vec::with_capacity(CHAIN_KEY_BATCH_HEADER_DOMAIN.len() + 4 + payload.len());
220 out.extend_from_slice(CHAIN_KEY_BATCH_HEADER_DOMAIN);
221 encode_bytes(&mut out, payload)?;
222 Ok(hash_bytes(&out))
223}
224
225fn hash_chain_key_leaf_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
226 let mut out =
227 Vec::with_capacity(1 + CHAIN_KEY_DELEGATION_CERT_DOMAIN.len() + 4 + payload.len());
228 out.push(0);
229 out.extend_from_slice(CHAIN_KEY_DELEGATION_CERT_DOMAIN);
230 encode_bytes(&mut out, payload)?;
231 Ok(hash_bytes(&out))
232}
233
234fn chain_key_batch_header_bytes(
235 header: &ChainKeyBatchHeaderV1,
236) -> Result<Vec<u8>, CanonicalAuthError> {
237 let mut out = Vec::with_capacity(256);
238 encode_u16(&mut out, header.schema_version);
239 encode_principal(&mut out, header.root_canister_id)?;
240 encode_fixed_32(&mut out, header.batch_id);
241 encode_u64(&mut out, header.proof_epoch);
242 encode_u64(&mut out, header.registry_epoch);
243 encode_fixed_32(&mut out, header.registry_hash);
244 encode_fixed_32(&mut out, header.tree_root);
245 encode_u64(&mut out, header.not_before_ns);
246 encode_u64(&mut out, header.expires_at_ns);
247 encode_chain_key_algorithm(&mut out, header.algorithm);
248 encode_chain_key_key_id(&mut out, &header.key_id)?;
249 encode_fixed_32(&mut out, header.derivation_path_hash);
250 encode_u64(&mut out, header.key_version);
251 Ok(out)
252}
253
254fn chain_key_delegation_cert_bytes(
255 cert: &ChainKeyDelegationCertV1,
256) -> Result<Vec<u8>, CanonicalAuthError> {
257 let mut out = Vec::with_capacity(256);
258 encode_principal(&mut out, cert.root_canister_id)?;
259 encode_principal(&mut out, cert.issuer_canister_id)?;
260 encode_u64(&mut out, cert.proof_epoch);
261 encode_issuer_proof_algorithm(&mut out, cert.issuer_proof_algorithm);
262 encode_fixed_32(&mut out, cert.issuer_proof_binding_hash);
263 encode_issuer_proof_binding(&mut out, cert.issuer_proof_binding);
264 encode_u64(&mut out, cert.max_token_ttl_ns);
265 encode_audience(&mut out, &cert.audience);
266 encode_role_grants(&mut out, &cert.grants)?;
267 encode_u64(&mut out, cert.not_before_ns);
268 encode_u64(&mut out, cert.expires_at_ns);
269 encode_u64(&mut out, cert.registry_epoch);
270 encode_fixed_32(&mut out, cert.registry_hash);
271 Ok(out)
272}
273
274fn encode_root_proof(out: &mut Vec<u8>, proof: &RootProof) -> Result<(), CanonicalAuthError> {
275 match proof {
276 RootProof::IcChainKeyBatchSignatureV1(proof) => {
277 out.push(2);
278 encode_chain_key_proof(out, proof)?;
279 }
280 }
281 Ok(())
282}
283
284fn encode_chain_key_proof(
285 out: &mut Vec<u8>,
286 proof: &IcChainKeyBatchSignatureProofV1,
287) -> Result<(), CanonicalAuthError> {
288 out.extend_from_slice(&chain_key_batch_header_bytes(&proof.header)?);
289 out.extend_from_slice(&chain_key_delegation_cert_bytes(&proof.delegation_cert)?);
290 encode_chain_key_witness(out, &proof.issuer_witness)?;
291 encode_chain_key_signature(out, &proof.signature)?;
292 Ok(())
293}
294
295fn encode_chain_key_witness(
296 out: &mut Vec<u8>,
297 witness: &ChainKeyBatchWitnessV1,
298) -> Result<(), CanonicalAuthError> {
299 encode_len(out, witness.steps.len())?;
300 for step in &witness.steps {
301 match step {
302 ChainKeyBatchWitnessStepV1::LeftSibling(hash) => {
303 out.push(1);
304 encode_fixed_32(out, *hash);
305 }
306 ChainKeyBatchWitnessStepV1::RightSibling(hash) => {
307 out.push(2);
308 encode_fixed_32(out, *hash);
309 }
310 }
311 }
312 Ok(())
313}
314
315fn encode_chain_key_signature(
316 out: &mut Vec<u8>,
317 signature: &ChainKeyRootSignatureV1,
318) -> Result<(), CanonicalAuthError> {
319 encode_chain_key_algorithm(out, signature.algorithm);
320 encode_chain_key_key_id(out, &signature.key_id)?;
321 encode_chain_key_derivation_path(out, &signature.derivation_path)?;
322 encode_bytes(out, &signature.public_key)?;
323 encode_bytes(out, &signature.signature)?;
324 Ok(())
325}
326
327fn encode_chain_key_derivation_path(
328 out: &mut Vec<u8>,
329 derivation_path: &[Vec<u8>],
330) -> Result<(), CanonicalAuthError> {
331 encode_len(out, derivation_path.len())?;
332 for path_component in derivation_path {
333 encode_bytes(out, path_component)?;
334 }
335 Ok(())
336}
337
338fn encode_chain_key_algorithm(out: &mut Vec<u8>, algorithm: ChainKeyAlgorithm) {
339 let tag = match algorithm {
340 ChainKeyAlgorithm::EcdsaSecp256k1 => 1,
341 };
342 out.push(tag);
343}
344
345fn encode_chain_key_key_id(
346 out: &mut Vec<u8>,
347 key_id: &ChainKeyKeyId,
348) -> Result<(), CanonicalAuthError> {
349 encode_string(out, &key_id.name)?;
350 Ok(())
351}
352
353fn encode_issuer_proof(out: &mut Vec<u8>, proof: &IssuerProof) -> Result<(), CanonicalAuthError> {
354 match proof {
355 IssuerProof::IcCanisterSignatureV1(proof) => {
356 out.push(1);
357 encode_bytes(out, &proof.signature_cbor)?;
358 encode_bytes(out, &proof.public_key_der)?;
359 }
360 }
361 Ok(())
362}
363
364fn encode_issuer_proof_binding(out: &mut Vec<u8>, binding: IssuerProofBinding) {
365 match binding {
366 IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } => {
367 out.push(1);
368 encode_fixed_32(out, seed_hash);
369 }
370 }
371}
372
373fn encode_token_ext(out: &mut Vec<u8>, ext: Option<&[u8]>) -> Result<(), CanonicalAuthError> {
374 match ext {
375 Some(ext) => {
376 if ext.len() > MAX_TOKEN_EXT_BYTES {
377 return Err(CanonicalAuthError::TokenExtTooLarge {
378 len: ext.len(),
379 max: MAX_TOKEN_EXT_BYTES,
380 });
381 }
382 out.push(1);
383 encode_bytes(out, ext)?;
384 }
385 None => out.push(0),
386 }
387 Ok(())
388}
389
390fn encode_role(out: &mut Vec<u8>, role: &AuthRole) -> Result<(), CanonicalAuthError> {
391 encode_bytes(out, role.as_str().as_bytes())?;
392 Ok(())
393}
394
395fn encode_scopes(out: &mut Vec<u8>, scopes: &[String]) -> Result<(), CanonicalAuthError> {
396 let mut previous = None;
397 for scope in scopes {
398 validate_scope_label(scope)?;
399 let current = scope.as_bytes();
400 if previous.is_some_and(|previous| previous >= current) {
401 return Err(CanonicalAuthError::NonCanonicalScopes);
402 }
403 previous = Some(current);
404 }
405
406 encode_len(out, scopes.len())?;
407 for scope in scopes {
408 encode_bytes(out, scope.as_bytes())?;
409 }
410
411 Ok(())
412}
413
414pub fn validate_scope_label(scope: &str) -> Result<(), CanonicalAuthError> {
416 if scope.is_empty() {
417 return Err(CanonicalAuthError::EmptyScope);
418 }
419 if !is_valid_scope(scope) {
420 return Err(CanonicalAuthError::InvalidScope {
421 scope: scope.to_string(),
422 });
423 }
424 Ok(())
425}
426
427fn encode_string(out: &mut Vec<u8>, value: &str) -> Result<(), CanonicalAuthError> {
428 encode_bytes(out, value.as_bytes())?;
429 Ok(())
430}
431
432fn encode_principal(out: &mut Vec<u8>, principal: Principal) -> Result<(), CanonicalAuthError> {
433 encode_bytes(out, principal.as_slice())?;
434 Ok(())
435}
436
437fn encode_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), CanonicalAuthError> {
438 encode_len(out, bytes.len())?;
439 out.extend_from_slice(bytes);
440 Ok(())
441}
442
443fn encode_fixed_32(out: &mut Vec<u8>, bytes: [u8; 32]) {
444 out.extend_from_slice(&bytes);
445}
446
447fn encode_u64(out: &mut Vec<u8>, value: u64) {
448 out.extend_from_slice(&value.to_be_bytes());
449}
450
451fn encode_u16(out: &mut Vec<u8>, value: u16) {
452 out.extend_from_slice(&value.to_be_bytes());
453}
454
455fn encode_len(out: &mut Vec<u8>, len: usize) -> Result<(), CanonicalAuthError> {
456 let len = u32::try_from(len).map_err(|_| CanonicalAuthError::LengthOverflow)?;
457 out.extend_from_slice(&len.to_be_bytes());
458 Ok(())
459}
460
461fn is_valid_scope(scope: &str) -> bool {
463 if scope.is_empty() || scope.len() > 64 {
464 return false;
465 }
466 scope.split(':').all(|segment| {
467 let mut bytes = segment.bytes();
468 bytes
469 .next()
470 .is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
471 && bytes
472 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-'))
473 })
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 #[cfg(target_pointer_width = "64")]
482 fn length_overflow_is_fallible_before_writing() {
483 let mut out = vec![7];
484 assert_eq!(
485 encode_len(&mut out, u32::MAX as usize + 1),
486 Err(CanonicalAuthError::LengthOverflow)
487 );
488 assert_eq!(out, [7]);
489 }
490}