Skip to main content

ic_auth/
canonical.rs

1//! Canonical signed bytes for Root delegations, proofs and role identities.
2//! Domains and tags are protocol identities, not package branding.
3//! This module does not verify signatures, authority, validity or caller binding.
4
5use 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// Domain byte assigned to one delegated-auth canonical payload family.
17#[repr(u8)]
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19enum CanonicalDomain {
20    DelegationCert = 1,
21    DelegationProof = 3,
22    RoleHash = 4,
23}
24
25///
26/// CanonicalAuthError
27///
28/// Typed failure surface for delegated auth canonicalization.
29///
30
31#[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
43/// Hash the canonical delegation certificate; this does not authenticate it.
44pub fn cert_hash(cert: &DelegationCert) -> Result<[u8; 32], CanonicalAuthError> {
45    Ok(hash_bytes(&cert_bytes(cert)?))
46}
47
48/// Hash a certificate together with its complete root proof.
49pub fn proof_hash(proof: &DelegationProof) -> Result<[u8; 32], CanonicalAuthError> {
50    Ok(hash_bytes(&proof_bytes(proof)?))
51}
52
53/// Hash a chain-key batch header under its existing signed domain.
54pub 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
62/// Hash an issuer leaf under the existing chain-key Merkle leaf domain.
63pub 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
71/// Hash the exact ordered derivation path under its existing Canic domain.
72/// This binds path bytes; it does not derive a key or establish its authority.
73pub 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
81/// Bind an issuer principal, proof algorithm and seed hash.
82pub 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
95/// Hash the exact validated role label under the existing role domain.
96pub 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
102/// Encode a certificate without changing grant order or normalizing labels.
103pub 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
169// ADMITTED is private same-call shape evidence: portable encoding validates the
170// immutable grants first. Standalone canonical encoders retain their own checks.
171pub(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
347/// Validate label grammar and strictly increasing, unique UTF-8 byte order.
348///
349/// An empty list is valid. Cardinality, normalization and application authority
350/// remain caller-owned; use the bounded grant validator for nonempty grants.
351pub 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
365/// Check the existing lowercase, colon-separated application-scope grammar.
366pub 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}