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