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