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("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
45/// Hash the canonical delegation certificate; this does not authenticate it.
46pub fn cert_hash(cert: &DelegationCert) -> Result<[u8; 32], CanonicalAuthError> {
47    Ok(hash_bytes(&cert_bytes(cert)?))
48}
49
50/// Hash a certificate together with its complete root proof.
51pub fn proof_hash(proof: &DelegationProof) -> Result<[u8; 32], CanonicalAuthError> {
52    Ok(hash_bytes(&proof_bytes(proof)?))
53}
54
55/// Hash a chain-key batch header under its existing signed domain.
56pub 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
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    hash_chain_key_leaf_payload(&chain_key_delegation_cert_bytes(cert)?)
67}
68
69/// Hash the exact ordered derivation path under its existing Canic domain.
70/// This binds path bytes; it does not derive a key or establish its authority.
71pub 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
79/// Bind an issuer principal, proof algorithm and seed hash.
80pub 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
93/// Hash the exact validated role label under the existing role domain.
94pub 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
100/// Encode a certificate without changing grant order or normalizing labels.
101pub 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
343/// Validate label grammar and strictly increasing, unique UTF-8 byte order.
344///
345/// An empty list is valid. Cardinality, normalization and application authority
346/// remain caller-owned; use the bounded grant validator for nonempty grants.
347pub 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/// A scope label's grammar failure. Offsets and lengths count UTF-8 bytes.
362#[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
376/// Check the 64-byte, lowercase ASCII/digit, colon-separated scope grammar.
377///
378/// Every segment starts with a lowercase letter or digit; subsequent bytes may
379/// also be `_` or `-`. Length is checked first, then bytes from left to right.
380/// This allocation-free check supports static construction and grants no authority.
381pub 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
421/// Check the existing lowercase, colon-separated application-scope grammar.
422pub 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}