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, 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 !input.grants.iter().all(|child| {
157        cert.grants
158            .iter()
159            .find(|parent| parent.target == child.target)
160            .is_some_and(|parent| {
161                child
162                    .scopes
163                    .iter()
164                    .all(|scope| parent.scopes.contains(scope))
165            })
166    }) {
167        return Err(Error::GrantsNotSubset);
168    }
169    let cert_hash = cert_hash(cert)?;
170    let nonce = delegated_token_nonce(
171        input.prepared_by,
172        input.operation_id,
173        cert.issuer_pid,
174        cert_hash,
175    );
176    let claims = DelegatedTokenClaims {
177        presenter: input.prepared_by,
178        subject: input.prepared_by,
179        issuer_pid: cert.issuer_pid,
180        cert_hash,
181        issued_at_ns: input.now_ns,
182        expires_at_ns,
183        aud: input.audience,
184        grants: input.grants,
185        nonce,
186        ext: input.ext,
187    };
188    Ok(PreparedDelegatedToken {
189        claims_hash: claims_hash(&claims)?,
190        claims,
191        proof: input.proof.clone(),
192    })
193}
194
195fn delegated_token_nonce(
196    presenter: Principal,
197    operation_id: [u8; 32],
198    issuer: Principal,
199    cert_hash: [u8; 32],
200) -> [u8; 16] {
201    // Frozen Canic domain and raw principal framing; do not rebrand signed bytes.
202    let mut hash = Sha256::new();
203    hash.update(b"canic-token-nonce-v1");
204    hash.update(presenter.as_slice());
205    hash.update(operation_id);
206    hash.update(issuer.as_slice());
207    hash.update(cert_hash);
208    let digest: [u8; 32] = hash.finalize().into();
209    let mut nonce = [0; 16];
210    nonce.copy_from_slice(&digest[..16]);
211    nonce
212}
213
214/// Attach host-provided issuer material verbatim, without proof verification.
215/// Obtain and authorize that material through the host's signature service;
216/// recipients still need complete verification before admitting the token.
217pub fn finish_delegated_token(
218    prepared: PreparedDelegatedToken,
219    issuer_proof: IssuerProof,
220) -> DelegatedToken {
221    DelegatedToken {
222        claims: prepared.claims,
223        proof: prepared.proof,
224        issuer_proof,
225    }
226}