Skip to main content

ic_auth/
token_preparation.rs

1//! Pure delegated-token issuance over a host-admitted delegation proof.
2//!
3//! Preparation and finishing do not authenticate any proof or authorize signing.
4//! The host owns issuer approval, independently enrolled authority, authenticated
5//! identity, clock, operation custody, signing and certification-root publication.
6
7use crate::canonical::{CanonicalAuthError, cert_hash, claims_hash};
8use crate::certificate_issuance::{GrantValidationError, grants_are_subset, validate_grants};
9use ic_auth_protocol_types::{
10    AuthRole, DelegatedRoleGrant, DelegatedToken, DelegatedTokenClaims, DelegationAudience,
11    DelegationProof, IssuerProof, Principal,
12};
13use sha2::{Digest, Sha256};
14use thiserror::Error;
15
16/// Explicit host-selected request bounds; there are no implicit fleet defaults.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct TokenPreparationLimits {
19    pub max_grants: usize,
20    pub max_scopes_per_grant: usize,
21}
22
23/// Inputs resolved by the host before preparing an issuer-local token.
24pub struct PrepareDelegatedTokenInput<'a> {
25    /// Already installed/verified proof belonging to the authorized local issuer.
26    /// Never resolve this authority from client-submitted proof fields.
27    pub proof: &'a DelegationProof,
28    /// Exclusive protected authority deadline, capped by enrolled root policy.
29    pub authority_expires_at_ns: u64,
30    /// Host-retained operation identity. Retry must retain the original inputs,
31    /// including time, rather than rebuilding a different token for this ID.
32    pub operation_id: [u8; 32],
33    /// Authenticated presenter, also used as the subject; not a request assertion.
34    pub prepared_by: Principal,
35    pub audience: DelegationAudience,
36    pub grants: Vec<DelegatedRoleGrant>,
37    pub ttl_ns: u64,
38    pub ext: Option<Vec<u8>>,
39    pub now_ns: u64,
40    pub limits: TokenPreparationLimits,
41}
42
43/// Immutable claims, signing hash and attached Root proof. This is prepared
44/// material, not a verified token, signature, resource grant or session.
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct PreparedDelegatedToken {
47    claims: DelegatedTokenClaims,
48    claims_hash: [u8; 32],
49    proof: DelegationProof,
50}
51
52impl PreparedDelegatedToken {
53    pub const fn claims(&self) -> &DelegatedTokenClaims {
54        &self.claims
55    }
56
57    /// Canonical hash to pass to the host's authorized issuer-signature path.
58    pub const fn claims_hash(&self) -> [u8; 32] {
59        self.claims_hash
60    }
61
62    pub const fn proof(&self) -> &DelegationProof {
63        &self.proof
64    }
65}
66
67/// Ordered request rejection. Neither certificate bytes nor these shape checks
68/// establish the provenance of the host's supplied authority.
69#[derive(Debug, Eq, Error, PartialEq)]
70pub enum PrepareDelegatedTokenError {
71    #[error("delegation certificate is not yet valid")]
72    CertNotYetValid,
73    #[error("delegation certificate or local authority expired")]
74    CertExpired,
75    #[error("token TTL must be greater than zero")]
76    TokenTtlZero,
77    #[error("token TTL {ttl_ns} exceeds certificate maximum {max_ttl_ns}")]
78    TokenTtlExceeded { ttl_ns: u64, max_ttl_ns: u64 },
79    #[error("token expiry overflows")]
80    TokenExpiresAtOverflow,
81    #[error("token outlives certificate or local authority")]
82    TokenOutlivesCert,
83    #[error("token grants must not be empty")]
84    GrantsEmpty,
85    #[error("token grants {found} exceed host maximum {max}")]
86    TooManyGrants { found: usize, max: usize },
87    #[error("grant for {role:?} has no scopes")]
88    EmptyGrantScopes { role: AuthRole },
89    #[error("grant for {role:?} has {found} scopes exceeding host maximum {max}")]
90    TooManyGrantScopes {
91        role: AuthRole,
92        found: usize,
93        max: usize,
94    },
95    #[error("token audience is not a subset of certificate audience")]
96    AudienceNotSubset,
97    #[error("token grants are not a subset of certificate grants")]
98    GrantsNotSubset,
99    #[error(transparent)]
100    Canonical(#[from] CanonicalAuthError),
101}
102
103impl From<GrantValidationError> for PrepareDelegatedTokenError {
104    fn from(error: GrantValidationError) -> Self {
105        use GrantValidationError as E;
106        match error {
107            E::Empty => Self::GrantsEmpty,
108            E::TooMany { found, max } => Self::TooManyGrants { found, max },
109            E::EmptyScopes { role } => Self::EmptyGrantScopes { role },
110            E::TooManyScopes { role, found, max } => Self::TooManyGrantScopes { role, found, max },
111            E::Canonical(error) => Self::Canonical(error),
112        }
113    }
114}
115
116/// Prepare exact canonical claims without reading runtime state or signing.
117/// Expiry is checked against both certificate and supplied authority: an
118/// overlong request rejects rather than silently shortening the requested TTL.
119pub fn prepare_delegated_token(
120    input: PrepareDelegatedTokenInput<'_>,
121) -> Result<PreparedDelegatedToken, PrepareDelegatedTokenError> {
122    use PrepareDelegatedTokenError as Error;
123    let cert = &input.proof.cert;
124    let deadline = cert.expires_at_ns.min(input.authority_expires_at_ns);
125    if input.now_ns < cert.not_before_ns {
126        return Err(Error::CertNotYetValid);
127    }
128    if input.now_ns >= deadline {
129        return Err(Error::CertExpired);
130    }
131    if input.ttl_ns == 0 {
132        return Err(Error::TokenTtlZero);
133    }
134    if input.ttl_ns > cert.max_token_ttl_ns {
135        return Err(Error::TokenTtlExceeded {
136            ttl_ns: input.ttl_ns,
137            max_ttl_ns: cert.max_token_ttl_ns,
138        });
139    }
140    let expires_at_ns = input
141        .now_ns
142        .checked_add(input.ttl_ns)
143        .ok_or(Error::TokenExpiresAtOverflow)?;
144    if expires_at_ns > deadline {
145        return Err(Error::TokenOutlivesCert);
146    }
147    validate_grants(
148        &input.grants,
149        input.limits.max_grants,
150        input.limits.max_scopes_per_grant,
151    )
152    .map_err(Error::from)?;
153    if input.audience != cert.aud {
154        return Err(Error::AudienceNotSubset);
155    }
156    if !grants_are_subset(&input.grants, &cert.grants) {
157        return Err(Error::GrantsNotSubset);
158    }
159    let cert_hash = cert_hash(cert)?;
160    let nonce = delegated_token_nonce(
161        input.prepared_by,
162        input.operation_id,
163        cert.issuer_pid,
164        cert_hash,
165    );
166    let claims = DelegatedTokenClaims {
167        presenter: input.prepared_by,
168        subject: input.prepared_by,
169        issuer_pid: cert.issuer_pid,
170        cert_hash,
171        issued_at_ns: input.now_ns,
172        expires_at_ns,
173        aud: input.audience,
174        grants: input.grants,
175        nonce,
176        ext: input.ext,
177    };
178    Ok(PreparedDelegatedToken {
179        claims_hash: claims_hash(&claims)?,
180        claims,
181        proof: input.proof.clone(),
182    })
183}
184
185fn delegated_token_nonce(
186    presenter: Principal,
187    operation_id: [u8; 32],
188    issuer: Principal,
189    cert_hash: [u8; 32],
190) -> [u8; 16] {
191    // Frozen Canic domain and raw principal framing; do not rebrand signed bytes.
192    let mut hash = Sha256::new();
193    hash.update(b"canic-token-nonce-v1");
194    hash.update(presenter.as_slice());
195    hash.update(operation_id);
196    hash.update(issuer.as_slice());
197    hash.update(cert_hash);
198    let digest: [u8; 32] = hash.finalize().into();
199    let mut nonce = [0; 16];
200    nonce.copy_from_slice(&digest[..16]);
201    nonce
202}
203
204/// Attach host-provided issuer material verbatim, without proof verification.
205/// Obtain and authorize that material through the host's signature service;
206/// recipients still need complete verification before admitting the token.
207pub fn finish_delegated_token(
208    prepared: PreparedDelegatedToken,
209    issuer_proof: IssuerProof,
210) -> DelegatedToken {
211    DelegatedToken {
212        claims: prepared.claims,
213        proof: prepared.proof,
214        issuer_proof,
215    }
216}