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(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(
172 out: &mut Vec<u8>,
173 grants: &[DelegatedRoleGrant],
174) -> Result<(), CanonicalAuthError> {
175 encode_len(out, grants.len())?;
176 let mut previous = None;
177 for grant in grants {
178 let current = grant.target.as_str().as_bytes();
179 if previous.is_some_and(|previous| previous >= current) {
180 return Err(CanonicalAuthError::NonCanonicalRoles);
181 }
182 previous = Some(current);
183 encode_role(out, &grant.target)?;
184 encode_scopes(out, &grant.scopes)?;
185 }
186 Ok(())
187}
188
189fn hash_chain_key_header_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
190 let len = u32::try_from(payload.len()).map_err(|_| CanonicalAuthError::LengthOverflow)?;
191 let mut hash = Sha256::new();
192 hash.update(CHAIN_KEY_BATCH_HEADER_DOMAIN);
193 hash.update(len.to_be_bytes());
194 hash.update(payload);
195 Ok(hash.finalize().into())
196}
197
198fn hash_chain_key_leaf_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
199 let len = u32::try_from(payload.len()).map_err(|_| CanonicalAuthError::LengthOverflow)?;
200 let mut hash = Sha256::new();
201 hash.update([0]);
202 hash.update(CHAIN_KEY_DELEGATION_CERT_DOMAIN);
203 hash.update(len.to_be_bytes());
204 hash.update(payload);
205 Ok(hash.finalize().into())
206}
207
208fn encode_chain_key_batch_header(
209 out: &mut Vec<u8>,
210 header: &ChainKeyBatchHeaderV1,
211) -> Result<(), CanonicalAuthError> {
212 encode_u16(out, header.schema_version);
213 encode_principal(out, header.root_canister_id)?;
214 encode_fixed_32(out, header.batch_id);
215 encode_u64(out, header.proof_epoch);
216 encode_u64(out, header.registry_epoch);
217 encode_fixed_32(out, header.registry_hash);
218 encode_fixed_32(out, header.tree_root);
219 encode_u64(out, header.not_before_ns);
220 encode_u64(out, header.expires_at_ns);
221 encode_chain_key_algorithm(out, header.algorithm);
222 encode_chain_key_key_id(out, &header.key_id)?;
223 encode_fixed_32(out, header.derivation_path_hash);
224 encode_u64(out, header.key_version);
225 Ok(())
226}
227
228fn encode_chain_key_delegation_cert(
229 out: &mut Vec<u8>,
230 cert: &ChainKeyDelegationCertV1,
231) -> Result<(), CanonicalAuthError> {
232 encode_principal(out, cert.root_canister_id)?;
233 encode_principal(out, cert.issuer_canister_id)?;
234 encode_u64(out, cert.proof_epoch);
235 encode_issuer_proof_algorithm(out, cert.issuer_proof_algorithm);
236 encode_fixed_32(out, cert.issuer_proof_binding_hash);
237 encode_issuer_proof_binding(out, cert.issuer_proof_binding);
238 encode_u64(out, cert.max_token_ttl_ns);
239 encode_audience(out, &cert.audience);
240 encode_role_grants(out, &cert.grants)?;
241 encode_u64(out, cert.not_before_ns);
242 encode_u64(out, cert.expires_at_ns);
243 encode_u64(out, cert.registry_epoch);
244 encode_fixed_32(out, cert.registry_hash);
245 Ok(())
246}
247
248fn encode_root_proof(out: &mut Vec<u8>, proof: &RootProof) -> Result<(), CanonicalAuthError> {
249 match proof {
250 RootProof::IcChainKeyBatchSignatureV1(proof) => {
251 out.push(2);
252 encode_chain_key_proof(out, proof)?;
253 }
254 }
255 Ok(())
256}
257
258fn encode_chain_key_proof(
259 out: &mut Vec<u8>,
260 proof: &IcChainKeyBatchSignatureProofV1,
261) -> Result<(), CanonicalAuthError> {
262 encode_chain_key_batch_header(out, &proof.header)?;
263 encode_chain_key_delegation_cert(out, &proof.delegation_cert)?;
264 encode_chain_key_witness(out, &proof.issuer_witness)?;
265 encode_chain_key_signature(out, &proof.signature)?;
266 Ok(())
267}
268
269fn encode_chain_key_witness(
270 out: &mut Vec<u8>,
271 witness: &ChainKeyBatchWitnessV1,
272) -> Result<(), CanonicalAuthError> {
273 encode_len(out, witness.steps.len())?;
274 for step in &witness.steps {
275 match step {
276 ChainKeyBatchWitnessStepV1::LeftSibling(hash) => {
277 out.push(1);
278 encode_fixed_32(out, *hash);
279 }
280 ChainKeyBatchWitnessStepV1::RightSibling(hash) => {
281 out.push(2);
282 encode_fixed_32(out, *hash);
283 }
284 }
285 }
286 Ok(())
287}
288
289fn encode_chain_key_signature(
290 out: &mut Vec<u8>,
291 signature: &ChainKeyRootSignatureV1,
292) -> Result<(), CanonicalAuthError> {
293 encode_chain_key_algorithm(out, signature.algorithm);
294 encode_chain_key_key_id(out, &signature.key_id)?;
295 encode_chain_key_derivation_path(out, &signature.derivation_path)?;
296 encode_bytes(out, &signature.public_key)?;
297 encode_bytes(out, &signature.signature)?;
298 Ok(())
299}
300
301fn encode_chain_key_derivation_path(
302 out: &mut Vec<u8>,
303 derivation_path: &[Vec<u8>],
304) -> Result<(), CanonicalAuthError> {
305 encode_len(out, derivation_path.len())?;
306 for path_component in derivation_path {
307 encode_bytes(out, path_component)?;
308 }
309 Ok(())
310}
311
312fn encode_chain_key_algorithm(out: &mut Vec<u8>, algorithm: ChainKeyAlgorithm) {
313 let tag = match algorithm {
314 ChainKeyAlgorithm::EcdsaSecp256k1 => 1,
315 };
316 out.push(tag);
317}
318
319fn encode_chain_key_key_id(
320 out: &mut Vec<u8>,
321 key_id: &ChainKeyKeyId,
322) -> Result<(), CanonicalAuthError> {
323 encode_string(out, &key_id.name)?;
324 Ok(())
325}
326
327fn encode_issuer_proof_binding(out: &mut Vec<u8>, binding: IssuerProofBinding) {
328 match binding {
329 IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } => {
330 out.push(1);
331 encode_fixed_32(out, seed_hash);
332 }
333 }
334}
335
336fn encode_role(out: &mut Vec<u8>, role: &AuthRole) -> Result<(), CanonicalAuthError> {
337 encode_bytes(out, role.as_str().as_bytes())?;
338 Ok(())
339}
340
341fn encode_scopes(out: &mut Vec<u8>, scopes: &[String]) -> Result<(), CanonicalAuthError> {
342 validate_scopes(scopes)?;
343 encode_len(out, scopes.len())?;
344 for scope in scopes {
345 encode_bytes(out, scope.as_bytes())?;
346 }
347 Ok(())
348}
349
350pub fn validate_scopes(scopes: &[String]) -> Result<(), CanonicalAuthError> {
355 let mut previous = None;
356 for scope in scopes {
357 validate_scope_label(scope)?;
358 let current = scope.as_bytes();
359 if previous.is_some_and(|previous| previous >= current) {
360 return Err(CanonicalAuthError::NonCanonicalScopes);
361 }
362 previous = Some(current);
363 }
364
365 Ok(())
366}
367
368#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
370pub enum ScopeLabelError {
371 #[error("scope is empty")]
372 Empty,
373 #[error("scope has {bytes} bytes; maximum is {max}")]
374 TooLong { bytes: usize, max: usize },
375 #[error("scope contains an empty segment")]
376 EmptySegment,
377 #[error("invalid scope segment start at byte {offset}")]
378 InvalidSegmentStart { offset: usize },
379 #[error("invalid scope byte at offset {offset}")]
380 InvalidByte { offset: usize },
381}
382
383pub const fn validate_scope_label_detailed(scope: &str) -> Result<(), ScopeLabelError> {
389 let bytes = scope.as_bytes();
390 if bytes.is_empty() {
391 return Err(ScopeLabelError::Empty);
392 }
393 if bytes.len() > crate::limits::MAX_LABEL_BYTES {
394 return Err(ScopeLabelError::TooLong {
395 bytes: bytes.len(),
396 max: crate::limits::MAX_LABEL_BYTES,
397 });
398 }
399 let mut segment_start = true;
400 let mut offset = 0;
401 while offset < bytes.len() {
402 let byte = bytes[offset];
403 if byte == b':' {
404 if segment_start {
405 return Err(ScopeLabelError::EmptySegment);
406 }
407 segment_start = true;
408 } else if segment_start {
409 if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() {
410 return Err(ScopeLabelError::InvalidSegmentStart { offset });
411 }
412 segment_start = false;
413 } else if !byte.is_ascii_lowercase()
414 && !byte.is_ascii_digit()
415 && byte != b'_'
416 && byte != b'-'
417 {
418 return Err(ScopeLabelError::InvalidByte { offset });
419 }
420 offset += 1;
421 }
422 if segment_start {
423 return Err(ScopeLabelError::EmptySegment);
424 }
425 Ok(())
426}
427
428pub fn validate_scope_label(scope: &str) -> Result<(), CanonicalAuthError> {
430 validate_scope_label_detailed(scope).map_err(|error| match error {
431 ScopeLabelError::Empty => CanonicalAuthError::EmptyScope,
432 _ => CanonicalAuthError::InvalidScope {
433 scope: scope.to_string(),
434 },
435 })
436}
437
438fn encode_string(out: &mut Vec<u8>, value: &str) -> Result<(), CanonicalAuthError> {
439 encode_bytes(out, value.as_bytes())?;
440 Ok(())
441}
442
443pub(crate) fn encode_principal(
444 out: &mut Vec<u8>,
445 principal: Principal,
446) -> Result<(), CanonicalAuthError> {
447 encode_bytes(out, principal.as_slice())?;
448 Ok(())
449}
450
451fn encode_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), CanonicalAuthError> {
452 encode_len(out, bytes.len())?;
453 out.extend_from_slice(bytes);
454 Ok(())
455}
456
457fn encode_fixed_32(out: &mut Vec<u8>, bytes: [u8; 32]) {
458 out.extend_from_slice(&bytes);
459}
460
461fn encode_u64(out: &mut Vec<u8>, value: u64) {
462 out.extend_from_slice(&value.to_be_bytes());
463}
464
465fn encode_u16(out: &mut Vec<u8>, value: u16) {
466 out.extend_from_slice(&value.to_be_bytes());
467}
468
469fn encode_len(out: &mut Vec<u8>, len: usize) -> Result<(), CanonicalAuthError> {
470 let len = u32::try_from(len).map_err(|_| CanonicalAuthError::LengthOverflow)?;
471 out.extend_from_slice(&len.to_be_bytes());
472 Ok(())
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn chain_key_payload_hashes_preserve_framing() {
481 for length in [0, 1, 255, 256, 4096] {
482 let payload: Vec<u8> = (0..length).map(|i| (i % 256) as u8).collect();
483 let len = (length as u32).to_be_bytes();
484 let header = [CHAIN_KEY_BATCH_HEADER_DOMAIN, &len, &payload].concat();
485 let leaf = [&[0], CHAIN_KEY_DELEGATION_CERT_DOMAIN, &len, &payload].concat();
486 assert_eq!(
487 hash_chain_key_header_payload(&payload).unwrap(),
488 <[u8; 32]>::from(Sha256::digest(header)),
489 );
490 assert_eq!(
491 hash_chain_key_leaf_payload(&payload).unwrap(),
492 <[u8; 32]>::from(Sha256::digest(leaf)),
493 );
494 }
495 }
496
497 #[test]
498 #[cfg(target_pointer_width = "64")]
499 fn length_overflow_is_fallible_before_writing() {
500 let mut out = vec![7];
501 assert_eq!(
502 encode_len(&mut out, u32::MAX as usize + 1),
503 Err(CanonicalAuthError::LengthOverflow)
504 );
505 assert_eq!(out, [7]);
506 }
507}