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(transparent)]
36 Scope(#[from] ScopeLabelError),
37 #[error("delegated auth scopes must be strictly sorted and unique")]
38 NonCanonicalScopes,
39 #[error("delegated auth role grants must be strictly sorted and unique")]
40 NonCanonicalRoles,
41}
42
43pub fn cert_hash(cert: &DelegationCert) -> Result<[u8; 32], CanonicalAuthError> {
45 Ok(hash_bytes(&cert_bytes(cert)?))
46}
47
48pub fn proof_hash(proof: &DelegationProof) -> Result<[u8; 32], CanonicalAuthError> {
50 Ok(hash_bytes(&proof_bytes(proof)?))
51}
52
53pub fn chain_key_batch_header_hash(
55 header: &ChainKeyBatchHeaderV1,
56) -> Result<[u8; 32], CanonicalAuthError> {
57 let mut payload = Vec::with_capacity(256);
58 encode_chain_key_batch_header(&mut payload, header)?;
59 hash_chain_key_header_payload(&payload)
60}
61
62pub fn chain_key_delegation_cert_hash(
64 cert: &ChainKeyDelegationCertV1,
65) -> Result<[u8; 32], CanonicalAuthError> {
66 let mut payload = Vec::with_capacity(256);
67 encode_chain_key_delegation_cert(&mut payload, cert)?;
68 hash_chain_key_leaf_payload(&payload)
69}
70
71pub fn chain_key_derivation_path_hash(
74 derivation_path: &[Vec<u8>],
75) -> Result<[u8; 32], CanonicalAuthError> {
76 let mut out = CHAIN_KEY_DERIVATION_PATH_DOMAIN.to_vec();
77 encode_chain_key_derivation_path(&mut out, derivation_path)?;
78 Ok(hash_bytes(&out))
79}
80
81pub fn issuer_proof_binding_hash(
83 issuer_pid: Principal,
84 issuer_proof_alg: IssuerProofAlgorithm,
85 issuer_proof_binding: IssuerProofBinding,
86) -> Result<[u8; 32], CanonicalAuthError> {
87 let mut out = Vec::with_capacity(128);
88 out.extend_from_slice(ISSUER_PROOF_BINDING_HASH_DOMAIN);
89 encode_principal(&mut out, issuer_pid)?;
90 encode_issuer_proof_algorithm(&mut out, issuer_proof_alg);
91 encode_issuer_proof_binding(&mut out, issuer_proof_binding);
92 Ok(hash_bytes(&out))
93}
94
95pub fn role_hash(role: &AuthRole) -> Result<[u8; 32], CanonicalAuthError> {
97 let mut out = domain_bytes(CanonicalDomain::RoleHash);
98 encode_string(&mut out, role.as_str())?;
99 Ok(hash_bytes(&out))
100}
101
102pub fn cert_bytes(cert: &DelegationCert) -> Result<Vec<u8>, CanonicalAuthError> {
104 let mut out = Vec::with_capacity(128);
105 encode_cert(&mut out, cert)?;
106 Ok(out)
107}
108
109fn encode_cert(out: &mut Vec<u8>, cert: &DelegationCert) -> Result<(), CanonicalAuthError> {
110 out.extend_from_slice(DOMAIN_SEPARATOR);
111 out.push(CanonicalDomain::DelegationCert as u8);
112
113 encode_principal(out, cert.root_pid)?;
114 encode_principal(out, cert.issuer_pid)?;
115 encode_issuer_proof_algorithm(out, cert.issuer_proof_alg);
116 encode_fixed_32(out, cert.issuer_proof_binding_hash);
117 encode_issuer_proof_binding(out, cert.issuer_proof_binding);
118 encode_u64(out, cert.issued_at_ns);
119 encode_u64(out, cert.not_before_ns);
120 encode_u64(out, cert.expires_at_ns);
121 encode_u64(out, cert.max_token_ttl_ns);
122 encode_audience(out, &cert.aud);
123 encode_role_grants::<false>(out, &cert.grants)?;
124
125 Ok(())
126}
127
128fn proof_bytes(proof: &DelegationProof) -> Result<Vec<u8>, CanonicalAuthError> {
129 let mut out = domain_bytes(CanonicalDomain::DelegationProof);
130
131 encode_cert(&mut out, &proof.cert)?;
132 encode_root_proof(&mut out, &proof.root_proof)?;
133
134 Ok(out)
135}
136
137fn domain_bytes(domain: CanonicalDomain) -> Vec<u8> {
138 let mut out = Vec::with_capacity(128);
139 out.extend_from_slice(DOMAIN_SEPARATOR);
140 out.push(domain as u8);
141 out
142}
143
144fn hash_bytes(bytes: &[u8]) -> [u8; 32] {
145 Sha256::digest(bytes).into()
146}
147
148fn encode_issuer_proof_algorithm(out: &mut Vec<u8>, alg: IssuerProofAlgorithm) {
149 let tag = match alg {
150 IssuerProofAlgorithm::IcCanisterSignatureV1 => 1,
151 };
152 out.push(tag);
153}
154
155fn encode_audience(out: &mut Vec<u8>, audience: &DelegationAudience) {
156 match audience {
157 DelegationAudience::Fleet(fleet) => {
158 out.push(1);
159 encode_fleet_key(out, *fleet);
160 }
161 }
162}
163
164fn encode_fleet_key(out: &mut Vec<u8>, fleet: AudienceId) {
165 encode_fixed_32(out, *fleet.canonical_network_id.as_bytes());
166 encode_fixed_32(out, *fleet.fleet_id.as_bytes());
167}
168
169pub(crate) fn encode_role_grants<const ADMITTED: bool>(
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 !ADMITTED && previous.is_some_and(|previous| previous >= current) {
180 return Err(CanonicalAuthError::NonCanonicalRoles);
181 }
182 previous = Some(current);
183 encode_role(out, &grant.target)?;
184 if !ADMITTED {
185 validate_scopes(&grant.scopes)?;
186 }
187 encode_len(out, grant.scopes.len())?;
188 for scope in &grant.scopes {
189 encode_bytes(out, scope.as_bytes())?;
190 }
191 }
192 Ok(())
193}
194
195fn hash_chain_key_header_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
196 let len = u32::try_from(payload.len()).map_err(|_| CanonicalAuthError::LengthOverflow)?;
197 let mut hash = Sha256::new();
198 hash.update(CHAIN_KEY_BATCH_HEADER_DOMAIN);
199 hash.update(len.to_be_bytes());
200 hash.update(payload);
201 Ok(hash.finalize().into())
202}
203
204fn hash_chain_key_leaf_payload(payload: &[u8]) -> Result<[u8; 32], CanonicalAuthError> {
205 let len = u32::try_from(payload.len()).map_err(|_| CanonicalAuthError::LengthOverflow)?;
206 let mut hash = Sha256::new();
207 hash.update([0]);
208 hash.update(CHAIN_KEY_DELEGATION_CERT_DOMAIN);
209 hash.update(len.to_be_bytes());
210 hash.update(payload);
211 Ok(hash.finalize().into())
212}
213
214fn encode_chain_key_batch_header(
215 out: &mut Vec<u8>,
216 header: &ChainKeyBatchHeaderV1,
217) -> Result<(), CanonicalAuthError> {
218 encode_u16(out, header.schema_version);
219 encode_principal(out, header.root_canister_id)?;
220 encode_fixed_32(out, header.batch_id);
221 encode_u64(out, header.proof_epoch);
222 encode_u64(out, header.registry_epoch);
223 encode_fixed_32(out, header.registry_hash);
224 encode_fixed_32(out, header.tree_root);
225 encode_u64(out, header.not_before_ns);
226 encode_u64(out, header.expires_at_ns);
227 encode_chain_key_algorithm(out, header.algorithm);
228 encode_chain_key_key_id(out, &header.key_id)?;
229 encode_fixed_32(out, header.derivation_path_hash);
230 encode_u64(out, header.key_version);
231 Ok(())
232}
233
234fn encode_chain_key_delegation_cert(
235 out: &mut Vec<u8>,
236 cert: &ChainKeyDelegationCertV1,
237) -> Result<(), CanonicalAuthError> {
238 encode_principal(out, cert.root_canister_id)?;
239 encode_principal(out, cert.issuer_canister_id)?;
240 encode_u64(out, cert.proof_epoch);
241 encode_issuer_proof_algorithm(out, cert.issuer_proof_algorithm);
242 encode_fixed_32(out, cert.issuer_proof_binding_hash);
243 encode_issuer_proof_binding(out, cert.issuer_proof_binding);
244 encode_u64(out, cert.max_token_ttl_ns);
245 encode_audience(out, &cert.audience);
246 encode_role_grants::<false>(out, &cert.grants)?;
247 encode_u64(out, cert.not_before_ns);
248 encode_u64(out, cert.expires_at_ns);
249 encode_u64(out, cert.registry_epoch);
250 encode_fixed_32(out, cert.registry_hash);
251 Ok(())
252}
253
254fn encode_root_proof(out: &mut Vec<u8>, proof: &RootProof) -> Result<(), CanonicalAuthError> {
255 match proof {
256 RootProof::IcChainKeyBatchSignatureV1(proof) => {
257 out.push(2);
258 encode_chain_key_proof(out, proof)?;
259 }
260 }
261 Ok(())
262}
263
264fn encode_chain_key_proof(
265 out: &mut Vec<u8>,
266 proof: &IcChainKeyBatchSignatureProofV1,
267) -> Result<(), CanonicalAuthError> {
268 encode_chain_key_batch_header(out, &proof.header)?;
269 encode_chain_key_delegation_cert(out, &proof.delegation_cert)?;
270 encode_chain_key_witness(out, &proof.issuer_witness)?;
271 encode_chain_key_signature(out, &proof.signature)?;
272 Ok(())
273}
274
275fn encode_chain_key_witness(
276 out: &mut Vec<u8>,
277 witness: &ChainKeyBatchWitnessV1,
278) -> Result<(), CanonicalAuthError> {
279 encode_len(out, witness.steps.len())?;
280 for step in &witness.steps {
281 match step {
282 ChainKeyBatchWitnessStepV1::LeftSibling(hash) => {
283 out.push(1);
284 encode_fixed_32(out, *hash);
285 }
286 ChainKeyBatchWitnessStepV1::RightSibling(hash) => {
287 out.push(2);
288 encode_fixed_32(out, *hash);
289 }
290 }
291 }
292 Ok(())
293}
294
295fn encode_chain_key_signature(
296 out: &mut Vec<u8>,
297 signature: &ChainKeyRootSignatureV1,
298) -> Result<(), CanonicalAuthError> {
299 encode_chain_key_algorithm(out, signature.algorithm);
300 encode_chain_key_key_id(out, &signature.key_id)?;
301 encode_chain_key_derivation_path(out, &signature.derivation_path)?;
302 encode_bytes(out, &signature.public_key)?;
303 encode_bytes(out, &signature.signature)?;
304 Ok(())
305}
306
307fn encode_chain_key_derivation_path(
308 out: &mut Vec<u8>,
309 derivation_path: &[Vec<u8>],
310) -> Result<(), CanonicalAuthError> {
311 encode_len(out, derivation_path.len())?;
312 for path_component in derivation_path {
313 encode_bytes(out, path_component)?;
314 }
315 Ok(())
316}
317
318fn encode_chain_key_algorithm(out: &mut Vec<u8>, algorithm: ChainKeyAlgorithm) {
319 let tag = match algorithm {
320 ChainKeyAlgorithm::EcdsaSecp256k1 => 1,
321 };
322 out.push(tag);
323}
324
325fn encode_chain_key_key_id(
326 out: &mut Vec<u8>,
327 key_id: &ChainKeyKeyId,
328) -> Result<(), CanonicalAuthError> {
329 encode_string(out, &key_id.name)?;
330 Ok(())
331}
332
333fn encode_issuer_proof_binding(out: &mut Vec<u8>, binding: IssuerProofBinding) {
334 match binding {
335 IssuerProofBinding::IcCanisterSignatureV1 { seed_hash } => {
336 out.push(1);
337 encode_fixed_32(out, seed_hash);
338 }
339 }
340}
341
342fn encode_role(out: &mut Vec<u8>, role: &AuthRole) -> Result<(), CanonicalAuthError> {
343 encode_bytes(out, role.as_str().as_bytes())?;
344 Ok(())
345}
346
347pub fn validate_scopes(scopes: &[String]) -> Result<(), CanonicalAuthError> {
352 let mut previous = None;
353 for scope in scopes {
354 validate_scope_label(scope)?;
355 let current = scope.as_bytes();
356 if previous.is_some_and(|previous| previous >= current) {
357 return Err(CanonicalAuthError::NonCanonicalScopes);
358 }
359 previous = Some(current);
360 }
361
362 Ok(())
363}
364
365pub fn validate_scope_label(scope: &str) -> Result<(), CanonicalAuthError> {
367 ScopeLabel::validate(scope).map_err(Into::into)
368}
369
370fn encode_string(out: &mut Vec<u8>, value: &str) -> Result<(), CanonicalAuthError> {
371 encode_bytes(out, value.as_bytes())?;
372 Ok(())
373}
374
375pub(crate) fn encode_principal(
376 out: &mut Vec<u8>,
377 principal: Principal,
378) -> Result<(), CanonicalAuthError> {
379 encode_bytes(out, principal.as_slice())?;
380 Ok(())
381}
382
383fn encode_bytes(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), CanonicalAuthError> {
384 encode_len(out, bytes.len())?;
385 out.extend_from_slice(bytes);
386 Ok(())
387}
388
389fn encode_fixed_32(out: &mut Vec<u8>, bytes: [u8; 32]) {
390 out.extend_from_slice(&bytes);
391}
392
393fn encode_u64(out: &mut Vec<u8>, value: u64) {
394 out.extend_from_slice(&value.to_be_bytes());
395}
396
397fn encode_u16(out: &mut Vec<u8>, value: u16) {
398 out.extend_from_slice(&value.to_be_bytes());
399}
400
401fn encode_len(out: &mut Vec<u8>, len: usize) -> Result<(), CanonicalAuthError> {
402 let len = u32::try_from(len).map_err(|_| CanonicalAuthError::LengthOverflow)?;
403 out.extend_from_slice(&len.to_be_bytes());
404 Ok(())
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn chain_key_payload_hashes_preserve_framing() {
413 for length in [0, 1, 255, 256, 4096] {
414 let payload: Vec<u8> = (0..length).map(|i| (i % 256) as u8).collect();
415 let len = (length as u32).to_be_bytes();
416 let header = [CHAIN_KEY_BATCH_HEADER_DOMAIN, &len, &payload].concat();
417 let leaf = [&[0], CHAIN_KEY_DELEGATION_CERT_DOMAIN, &len, &payload].concat();
418 assert_eq!(
419 hash_chain_key_header_payload(&payload).unwrap(),
420 <[u8; 32]>::from(Sha256::digest(header)),
421 );
422 assert_eq!(
423 hash_chain_key_leaf_payload(&payload).unwrap(),
424 <[u8; 32]>::from(Sha256::digest(leaf)),
425 );
426 }
427 }
428
429 #[test]
430 #[cfg(target_pointer_width = "64")]
431 fn length_overflow_is_fallible_before_writing() {
432 let mut out = vec![7];
433 assert_eq!(
434 encode_len(&mut out, u32::MAX as usize + 1),
435 Err(CanonicalAuthError::LengthOverflow)
436 );
437 assert_eq!(out, [7]);
438 }
439}