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 = Vec::with_capacity(128);
119 encode_cert(&mut out, cert)?;
120 Ok(out)
121}
122
123fn encode_cert(out: &mut Vec<u8>, cert: &DelegationCert) -> Result<(), CanonicalAuthError> {
124 out.extend_from_slice(DOMAIN_SEPARATOR);
125 out.push(CanonicalDomain::DelegationCert as u8);
126
127 encode_principal(out, cert.root_pid)?;
128 encode_principal(out, cert.issuer_pid)?;
129 encode_issuer_proof_algorithm(out, cert.issuer_proof_alg);
130 encode_fixed_32(out, cert.issuer_proof_binding_hash);
131 encode_issuer_proof_binding(out, cert.issuer_proof_binding);
132 encode_u64(out, cert.issued_at_ns);
133 encode_u64(out, cert.not_before_ns);
134 encode_u64(out, cert.expires_at_ns);
135 encode_u64(out, cert.max_token_ttl_ns);
136 encode_audience(out, &cert.aud);
137 encode_role_grants(out, &cert.grants)?;
138
139 Ok(())
140}
141
142pub fn claims_bytes(claims: &DelegatedTokenClaims) -> Result<Vec<u8>, CanonicalAuthError> {
144 let mut out = domain_bytes(CanonicalDomain::DelegatedTokenClaims);
145
146 encode_principal(&mut out, claims.presenter)?;
147 encode_principal(&mut out, claims.subject)?;
148 encode_principal(&mut out, claims.issuer_pid)?;
149 encode_fixed_32(&mut out, claims.cert_hash);
150 encode_u64(&mut out, claims.issued_at_ns);
151 encode_u64(&mut out, claims.expires_at_ns);
152 encode_audience(&mut out, &claims.aud);
153 encode_role_grants(&mut out, &claims.grants)?;
154 out.extend_from_slice(&claims.nonce);
155 encode_token_ext(&mut out, claims.ext.as_deref())?;
156
157 Ok(out)
158}
159
160fn proof_bytes(proof: &DelegationProof) -> Result<Vec<u8>, CanonicalAuthError> {
161 let mut out = domain_bytes(CanonicalDomain::DelegationProof);
162
163 encode_cert(&mut out, &proof.cert)?;
164 encode_root_proof(&mut out, &proof.root_proof)?;
165
166 Ok(out)
167}
168
169fn issuer_proof_bytes(proof: &IssuerProof) -> Result<Vec<u8>, CanonicalAuthError> {
170 let mut out = domain_bytes(CanonicalDomain::IssuerProof);
171 encode_issuer_proof(&mut out, proof)?;
172 Ok(out)
173}
174
175fn domain_bytes(domain: CanonicalDomain) -> Vec<u8> {
176 let mut out = Vec::with_capacity(128);
177 out.extend_from_slice(DOMAIN_SEPARATOR);
178 out.push(domain as u8);
179 out
180}
181
182fn hash_bytes(bytes: &[u8]) -> [u8; 32] {
183 Sha256::digest(bytes).into()
184}
185
186fn encode_issuer_proof_algorithm(out: &mut Vec<u8>, alg: IssuerProofAlgorithm) {
187 let tag = match alg {
188 IssuerProofAlgorithm::IcCanisterSignatureV1 => 1,
189 };
190 out.push(tag);
191}
192
193fn encode_audience(out: &mut Vec<u8>, audience: &DelegationAudience) {
194 match audience {
195 DelegationAudience::Fleet(fleet) => {
196 out.push(1);
197 encode_fleet_key(out, *fleet);
198 }
199 }
200}
201
202fn encode_fleet_key(out: &mut Vec<u8>, fleet: AudienceId) {
203 encode_fixed_32(out, *fleet.canonical_network_id.as_bytes());
204 encode_fixed_32(out, *fleet.fleet_id.as_bytes());
205}
206
207pub(crate) fn encode_role_grants(
208 out: &mut Vec<u8>,
209 grants: &[DelegatedRoleGrant],
210) -> Result<(), CanonicalAuthError> {
211 encode_len(out, grants.len())?;
212 let mut previous = None;
213 for grant in grants {
214 let current = grant.target.as_str().as_bytes();
215 if previous.is_some_and(|previous| previous >= current) {
216 return Err(CanonicalAuthError::NonCanonicalRoles);
217 }
218 previous = Some(current);
219 encode_role(out, &grant.target)?;
220 encode_scopes(out, &grant.scopes)?;
221 }
222 Ok(())
223}
224
225fn hash_chain_key_header_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
226 let mut out = Vec::with_capacity(CHAIN_KEY_BATCH_HEADER_DOMAIN.len() + 4 + payload.len());
227 out.extend_from_slice(CHAIN_KEY_BATCH_HEADER_DOMAIN);
228 encode_bytes(&mut out, payload)?;
229 Ok(hash_bytes(&out))
230}
231
232fn hash_chain_key_leaf_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
233 let mut out =
234 Vec::with_capacity(1 + CHAIN_KEY_DELEGATION_CERT_DOMAIN.len() + 4 + payload.len());
235 out.push(0);
236 out.extend_from_slice(CHAIN_KEY_DELEGATION_CERT_DOMAIN);
237 encode_bytes(&mut out, payload)?;
238 Ok(hash_bytes(&out))
239}
240
241fn chain_key_batch_header_bytes(
242 header: &ChainKeyBatchHeaderV1,
243) -> Result<Vec<u8>, CanonicalAuthError> {
244 let mut out = Vec::with_capacity(256);
245 encode_u16(&mut out, header.schema_version);
246 encode_principal(&mut out, header.root_canister_id)?;
247 encode_fixed_32(&mut out, header.batch_id);
248 encode_u64(&mut out, header.proof_epoch);
249 encode_u64(&mut out, header.registry_epoch);
250 encode_fixed_32(&mut out, header.registry_hash);
251 encode_fixed_32(&mut out, header.tree_root);
252 encode_u64(&mut out, header.not_before_ns);
253 encode_u64(&mut out, header.expires_at_ns);
254 encode_chain_key_algorithm(&mut out, header.algorithm);
255 encode_chain_key_key_id(&mut out, &header.key_id)?;
256 encode_fixed_32(&mut out, header.derivation_path_hash);
257 encode_u64(&mut out, header.key_version);
258 Ok(out)
259}
260
261fn chain_key_delegation_cert_bytes(
262 cert: &ChainKeyDelegationCertV1,
263) -> Result<Vec<u8>, CanonicalAuthError> {
264 let mut out = Vec::with_capacity(256);
265 encode_principal(&mut out, cert.root_canister_id)?;
266 encode_principal(&mut out, cert.issuer_canister_id)?;
267 encode_u64(&mut out, cert.proof_epoch);
268 encode_issuer_proof_algorithm(&mut out, cert.issuer_proof_algorithm);
269 encode_fixed_32(&mut out, cert.issuer_proof_binding_hash);
270 encode_issuer_proof_binding(&mut out, cert.issuer_proof_binding);
271 encode_u64(&mut out, cert.max_token_ttl_ns);
272 encode_audience(&mut out, &cert.audience);
273 encode_role_grants(&mut out, &cert.grants)?;
274 encode_u64(&mut out, cert.not_before_ns);
275 encode_u64(&mut out, cert.expires_at_ns);
276 encode_u64(&mut out, cert.registry_epoch);
277 encode_fixed_32(&mut out, cert.registry_hash);
278 Ok(out)
279}
280
281fn encode_root_proof(out: &mut Vec<u8>, proof: &RootProof) -> Result<(), CanonicalAuthError> {
282 match proof {
283 RootProof::IcChainKeyBatchSignatureV1(proof) => {
284 out.push(2);
285 encode_chain_key_proof(out, proof)?;
286 }
287 }
288 Ok(())
289}
290
291fn encode_chain_key_proof(
292 out: &mut Vec<u8>,
293 proof: &IcChainKeyBatchSignatureProofV1,
294) -> Result<(), CanonicalAuthError> {
295 out.extend_from_slice(&chain_key_batch_header_bytes(&proof.header)?);
296 out.extend_from_slice(&chain_key_delegation_cert_bytes(&proof.delegation_cert)?);
297 encode_chain_key_witness(out, &proof.issuer_witness)?;
298 encode_chain_key_signature(out, &proof.signature)?;
299 Ok(())
300}
301
302fn encode_chain_key_witness(
303 out: &mut Vec<u8>,
304 witness: &ChainKeyBatchWitnessV1,
305) -> Result<(), CanonicalAuthError> {
306 encode_len(out, witness.steps.len())?;
307 for step in &witness.steps {
308 match step {
309 ChainKeyBatchWitnessStepV1::LeftSibling(hash) => {
310 out.push(1);
311 encode_fixed_32(out, *hash);
312 }
313 ChainKeyBatchWitnessStepV1::RightSibling(hash) => {
314 out.push(2);
315 encode_fixed_32(out, *hash);
316 }
317 }
318 }
319 Ok(())
320}
321
322fn encode_chain_key_signature(
323 out: &mut Vec<u8>,
324 signature: &ChainKeyRootSignatureV1,
325) -> Result<(), CanonicalAuthError> {
326 encode_chain_key_algorithm(out, signature.algorithm);
327 encode_chain_key_key_id(out, &signature.key_id)?;
328 encode_chain_key_derivation_path(out, &signature.derivation_path)?;
329 encode_bytes(out, &signature.public_key)?;
330 encode_bytes(out, &signature.signature)?;
331 Ok(())
332}
333
334fn encode_chain_key_derivation_path(
335 out: &mut Vec<u8>,
336 derivation_path: &[Vec<u8>],
337) -> Result<(), CanonicalAuthError> {
338 encode_len(out, derivation_path.len())?;
339 for path_component in derivation_path {
340 encode_bytes(out, path_component)?;
341 }
342 Ok(())
343}
344
345fn encode_chain_key_algorithm(out: &mut Vec<u8>, algorithm: ChainKeyAlgorithm) {
346 let tag = match algorithm {
347 ChainKeyAlgorithm::EcdsaSecp256k1 => 1,
348 };
349 out.push(tag);
350}
351
352fn encode_chain_key_key_id(
353 out: &mut Vec<u8>,
354 key_id: &ChainKeyKeyId,
355) -> Result<(), CanonicalAuthError> {
356 encode_string(out, &key_id.name)?;
357 Ok(())
358}
359
360fn encode_issuer_proof(out: &mut Vec<u8>, proof: &IssuerProof) -> Result<(), CanonicalAuthError> {
361 match proof {
362 IssuerProof::IcCanisterSignatureV1(proof) => {
363 out.push(1);
364 encode_bytes(out, &proof.signature_cbor)?;
365 encode_bytes(out, &proof.public_key_der)?;
366 }
367 }
368 Ok(())
369}
370
371fn encode_issuer_proof_binding(out: &mut Vec<u8>, binding: IssuerProofBinding) {
372 match binding {
373 IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } => {
374 out.push(1);
375 encode_fixed_32(out, seed_hash);
376 }
377 }
378}
379
380fn encode_token_ext(out: &mut Vec<u8>, ext: Option<&[u8]>) -> Result<(), CanonicalAuthError> {
381 match ext {
382 Some(ext) => {
383 if ext.len() > MAX_TOKEN_EXT_BYTES {
384 return Err(CanonicalAuthError::TokenExtTooLarge {
385 len: ext.len(),
386 max: MAX_TOKEN_EXT_BYTES,
387 });
388 }
389 out.push(1);
390 encode_bytes(out, ext)?;
391 }
392 None => out.push(0),
393 }
394 Ok(())
395}
396
397fn encode_role(out: &mut Vec<u8>, role: &AuthRole) -> Result<(), CanonicalAuthError> {
398 encode_bytes(out, role.as_str().as_bytes())?;
399 Ok(())
400}
401
402fn encode_scopes(out: &mut Vec<u8>, scopes: &[String]) -> Result<(), CanonicalAuthError> {
403 validate_scopes(scopes)?;
404 encode_len(out, scopes.len())?;
405 for scope in scopes {
406 encode_bytes(out, scope.as_bytes())?;
407 }
408 Ok(())
409}
410
411pub fn validate_scopes(scopes: &[String]) -> Result<(), CanonicalAuthError> {
416 let mut previous = None;
417 for scope in scopes {
418 validate_scope_label(scope)?;
419 let current = scope.as_bytes();
420 if previous.is_some_and(|previous| previous >= current) {
421 return Err(CanonicalAuthError::NonCanonicalScopes);
422 }
423 previous = Some(current);
424 }
425
426 Ok(())
427}
428
429#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
431pub enum ScopeLabelError {
432 #[error("scope is empty")]
433 Empty,
434 #[error("scope has {bytes} bytes; maximum is {max}")]
435 TooLong { bytes: usize, max: usize },
436 #[error("scope contains an empty segment")]
437 EmptySegment,
438 #[error("invalid scope segment start at byte {offset}")]
439 InvalidSegmentStart { offset: usize },
440 #[error("invalid scope byte at offset {offset}")]
441 InvalidByte { offset: usize },
442}
443
444pub const fn validate_scope_label_detailed(scope: &str) -> Result<(), ScopeLabelError> {
450 let bytes = scope.as_bytes();
451 if bytes.is_empty() {
452 return Err(ScopeLabelError::Empty);
453 }
454 if bytes.len() > 64 {
455 return Err(ScopeLabelError::TooLong {
456 bytes: bytes.len(),
457 max: 64,
458 });
459 }
460 let mut segment_start = true;
461 let mut offset = 0;
462 while offset < bytes.len() {
463 let byte = bytes[offset];
464 if byte == b':' {
465 if segment_start {
466 return Err(ScopeLabelError::EmptySegment);
467 }
468 segment_start = true;
469 } else if segment_start {
470 if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() {
471 return Err(ScopeLabelError::InvalidSegmentStart { offset });
472 }
473 segment_start = false;
474 } else if !byte.is_ascii_lowercase()
475 && !byte.is_ascii_digit()
476 && byte != b'_'
477 && byte != b'-'
478 {
479 return Err(ScopeLabelError::InvalidByte { offset });
480 }
481 offset += 1;
482 }
483 if segment_start {
484 return Err(ScopeLabelError::EmptySegment);
485 }
486 Ok(())
487}
488
489pub fn validate_scope_label(scope: &str) -> Result<(), CanonicalAuthError> {
491 validate_scope_label_detailed(scope).map_err(|error| match error {
492 ScopeLabelError::Empty => CanonicalAuthError::EmptyScope,
493 _ => CanonicalAuthError::InvalidScope {
494 scope: scope.to_string(),
495 },
496 })
497}
498
499fn encode_string(out: &mut Vec<u8>, value: &str) -> Result<(), CanonicalAuthError> {
500 encode_bytes(out, value.as_bytes())?;
501 Ok(())
502}
503
504pub(crate) fn encode_principal(
505 out: &mut Vec<u8>,
506 principal: Principal,
507) -> Result<(), CanonicalAuthError> {
508 encode_bytes(out, principal.as_slice())?;
509 Ok(())
510}
511
512fn encode_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), CanonicalAuthError> {
513 encode_len(out, bytes.len())?;
514 out.extend_from_slice(bytes);
515 Ok(())
516}
517
518fn encode_fixed_32(out: &mut Vec<u8>, bytes: [u8; 32]) {
519 out.extend_from_slice(&bytes);
520}
521
522fn encode_u64(out: &mut Vec<u8>, value: u64) {
523 out.extend_from_slice(&value.to_be_bytes());
524}
525
526fn encode_u16(out: &mut Vec<u8>, value: u16) {
527 out.extend_from_slice(&value.to_be_bytes());
528}
529
530fn encode_len(out: &mut Vec<u8>, len: usize) -> Result<(), CanonicalAuthError> {
531 let len = u32::try_from(len).map_err(|_| CanonicalAuthError::LengthOverflow)?;
532 out.extend_from_slice(&len.to_be_bytes());
533 Ok(())
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 #[cfg(target_pointer_width = "64")]
542 fn length_overflow_is_fallible_before_writing() {
543 let mut out = vec![7];
544 assert_eq!(
545 encode_len(&mut out, u32::MAX as usize + 1),
546 Err(CanonicalAuthError::LengthOverflow)
547 );
548 assert_eq!(out, [7]);
549 }
550}