Skip to main content

ic_auth/
canonical.rs

1//! Canonical signed bytes for the existing Canic application-token protocol.
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";
13/// Largest extension admitted by the existing application-token protocol.
14pub const MAX_TOKEN_EXT_BYTES: usize = 4096;
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    DelegatedTokenClaims = 2,
22    DelegationProof = 3,
23    RoleHash = 4,
24    IssuerProof = 6,
25}
26
27///
28/// CanonicalAuthError
29///
30/// Typed failure surface for delegated auth canonicalization.
31///
32
33#[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
49/// Hash the canonical delegation certificate; this does not authenticate it.
50pub fn cert_hash(cert: &DelegationCert) -> Result<[u8; 32], CanonicalAuthError> {
51    Ok(hash_bytes(&cert_bytes(cert)?))
52}
53
54/// Hash canonical claims, rejecting noncanonical grants and oversized extensions.
55pub fn claims_hash(claims: &DelegatedTokenClaims) -> Result<[u8; 32], CanonicalAuthError> {
56    Ok(hash_bytes(&claims_bytes(claims)?))
57}
58
59/// Hash a certificate together with its complete root proof.
60pub fn proof_hash(proof: &DelegationProof) -> Result<[u8; 32], CanonicalAuthError> {
61    Ok(hash_bytes(&proof_bytes(proof)?))
62}
63
64/// Hash issuer signature bytes together with their public key.
65pub fn issuer_proof_hash(proof: &IssuerProof) -> Result<[u8; 32], CanonicalAuthError> {
66    Ok(hash_bytes(&issuer_proof_bytes(proof)?))
67}
68
69/// Hash a chain-key batch header under its existing signed domain.
70pub 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
76/// Hash an issuer leaf under the existing chain-key Merkle leaf domain.
77pub 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
83/// Bind an issuer principal, proof algorithm and seed hash.
84pub 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
97/// Hash the exact validated role label under the existing role domain.
98pub 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
104/// Encode a certificate without changing grant order or normalizing labels.
105pub 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
123/// Encode claims exactly, including presenter, audience, nonce and extension presence.
124pub 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
402/// Check the existing lowercase, colon-separated application-scope grammar.
403pub 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
449// Existing application-scope grammar, including its 64-byte bound.
450fn 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}