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