hessra_token_core/
error.rs

1use thiserror::Error;
2
3/// Detailed error type for hessra-token operations with specific failure information
4#[derive(Error, Debug, Clone)]
5pub enum TokenError {
6    // ===== Signature and Format Errors =====
7    /// Token signature verification failed
8    #[error("Invalid token signature: {details}")]
9    InvalidSignature { details: String },
10
11    /// The root public key was not recognized
12    #[error("Unknown public key - token was not signed by a recognized authority")]
13    UnknownPublicKey,
14
15    /// Invalid key format provided
16    #[error("Invalid key format: {reason}")]
17    InvalidKeyFormat { reason: String },
18
19    /// Token deserialization failed
20    #[error("Failed to deserialize token: {reason}")]
21    DeserializationError { reason: String },
22
23    /// Token serialization failed
24    #[error("Failed to serialize token: {reason}")]
25    SerializationError { reason: String },
26
27    /// Base64 decoding failed
28    #[error("Failed to decode base64 token: {reason}")]
29    Base64DecodingError { reason: String },
30
31    /// Token format version is not supported
32    #[error("Unsupported token version: expected {maximum}-{minimum}, got {actual}")]
33    UnsupportedVersion {
34        maximum: u32,
35        minimum: u32,
36        actual: u32,
37    },
38
39    // ===== Verification Errors (Common) =====
40    /// Token has expired
41    #[error("Token expired at {expired_at}, current time is {current_time}")]
42    Expired {
43        /// When the token expired (Unix timestamp)
44        expired_at: i64,
45        /// Current time when verification was attempted (Unix timestamp)
46        current_time: i64,
47        /// Block ID where the expiration check failed
48        block_id: u32,
49        /// Check ID within the block
50        check_id: u32,
51    },
52
53    /// Domain restriction check failed
54    #[error("Domain mismatch: expected '{expected}', {}", match provided {
55        Some(p) => format!("got '{p}'"),
56        None => "no domain provided".to_string(),
57    })]
58    DomainMismatch {
59        /// Expected domain from token
60        expected: String,
61        /// Domain provided during verification (if any)
62        provided: Option<String>,
63        /// Block ID where the check failed
64        block_id: u32,
65        /// Check ID within the block
66        check_id: u32,
67    },
68
69    /// A generic check failed (couldn't parse semantic meaning)
70    #[error("Verification check failed in block {block_id}, check {check_id}: {rule}")]
71    CheckFailed {
72        /// Block ID where the check failed
73        block_id: u32,
74        /// Check ID within the block
75        check_id: u32,
76        /// The Datalog rule that failed
77        rule: String,
78    },
79
80    // ===== Identity Token Errors =====
81    /// Identity/actor mismatch
82    #[error("Identity mismatch: expected '{expected}', got '{actual}'")]
83    IdentityMismatch {
84        /// Expected identity
85        expected: String,
86        /// Actual identity provided
87        actual: String,
88    },
89
90    /// Identity hierarchy violation (delegation not allowed)
91    #[error("Identity hierarchy violation: actor '{actual}' is not authorized for identity '{expected}' (delegatable: {delegatable})")]
92    HierarchyViolation {
93        /// Base identity that issued the token
94        expected: String,
95        /// Actor attempting to use the token
96        actual: String,
97        /// Whether delegation was allowed
98        delegatable: bool,
99        /// Block ID where the check failed
100        block_id: u32,
101        /// Check ID within the block
102        check_id: u32,
103    },
104
105    /// Token attenuation failed
106    #[error("Token attenuation failed: {reason}")]
107    AttenuationFailed { reason: String },
108
109    /// Bearer token not allowed in this context
110    #[error("Bearer token not allowed: {reason}")]
111    BearerNotAllowed { reason: String },
112
113    // ===== Authorization Token Errors =====
114    /// Authorization rights denied
115    #[error("Authorization denied: subject '{subject}' does not have permission to perform '{operation}' on '{resource}'")]
116    RightsDenied {
117        /// Subject requesting the action
118        subject: String,
119        /// Resource being accessed
120        resource: String,
121        /// Operation being performed
122        operation: String,
123    },
124
125    /// Service chain attestation failed
126    #[error("Service chain verification failed for component '{component}': {reason}")]
127    ServiceChainFailed {
128        /// Component name that failed
129        component: String,
130        /// Specific reason for failure
131        reason: ServiceChainFailure,
132    },
133
134    /// Multi-party attestation missing
135    #[error("Multi-party attestation missing for component '{component}' (expected key: {expected_key})")]
136    MultiPartyAttestationMissing {
137        /// Component requiring attestation
138        component: String,
139        /// Expected public key
140        expected_key: String,
141    },
142
143    /// No authorization policy matched and checks failed
144    #[error("No matching authorization policy found. Failed checks: {}", format_check_failures(.failed_checks))]
145    NoMatchingPolicy {
146        /// List of checks that failed
147        failed_checks: Vec<CheckFailure>,
148    },
149
150    /// Authorization policy matched but checks failed
151    #[error("Authorization policy {policy_type} matched (index {policy_index}), but the following checks failed: {}", format_check_failures(.failed_checks))]
152    PolicyMatchedButChecksFailed {
153        /// Type of policy that matched (Allow/Deny)
154        policy_type: String,
155        /// Index of the policy
156        policy_index: usize,
157        /// List of checks that failed
158        failed_checks: Vec<CheckFailure>,
159    },
160
161    // ===== Execution Errors =====
162    /// Datalog execution limit reached
163    #[error("Token verification exceeded execution limits: {reason}")]
164    ExecutionLimitReached { reason: String },
165
166    /// Datalog expression evaluation failed
167    #[error("Token verification expression error: {reason}")]
168    ExpressionError { reason: String },
169
170    /// Invalid block rule
171    #[error("Invalid rule in block {block_id}: {rule}")]
172    InvalidBlockRule { block_id: u32, rule: String },
173
174    // ===== Generic Errors =====
175    /// Internal error
176    #[error("Internal error: {0}")]
177    Internal(String),
178
179    /// Generic error with message
180    #[error("{0}")]
181    Generic(String),
182}
183
184/// Details about a failed check from biscuit verification
185#[derive(Debug, Clone)]
186pub struct CheckFailure {
187    /// Block ID (None if from authorizer)
188    pub block_id: Option<u32>,
189    /// Check ID
190    pub check_id: u32,
191    /// The Datalog rule that failed
192    pub rule: String,
193}
194
195impl std::fmt::Display for CheckFailure {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        if let Some(block_id) = self.block_id {
198            write!(
199                f,
200                "Check #{} in block #{}: {}",
201                self.check_id, block_id, self.rule
202            )
203        } else {
204            write!(f, "Check #{} in authorizer: {}", self.check_id, self.rule)
205        }
206    }
207}
208
209fn format_check_failures(checks: &[CheckFailure]) -> String {
210    checks
211        .iter()
212        .map(|c| c.to_string())
213        .collect::<Vec<_>>()
214        .join("; ")
215}
216
217/// Reasons a service chain verification can fail
218#[derive(Debug, Clone)]
219pub enum ServiceChainFailure {
220    /// Component not found in service chain
221    ComponentNotFound,
222    /// Attestation missing for component
223    MissingAttestation,
224    /// Attestation signature invalid
225    InvalidAttestation,
226    /// Component key mismatch
227    KeyMismatch { expected: String, actual: String },
228    /// Other failure reason
229    Other(String),
230}
231
232impl std::fmt::Display for ServiceChainFailure {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            ServiceChainFailure::ComponentNotFound => write!(f, "component not found in chain"),
236            ServiceChainFailure::MissingAttestation => write!(f, "attestation missing"),
237            ServiceChainFailure::InvalidAttestation => write!(f, "attestation signature invalid"),
238            ServiceChainFailure::KeyMismatch { expected, actual } => {
239                write!(f, "key mismatch (expected: {expected}, actual: {actual})")
240            }
241            ServiceChainFailure::Other(reason) => write!(f, "{reason}"),
242        }
243    }
244}
245
246impl TokenError {
247    // ===== Helper Methods for Common Error Checks =====
248
249    /// Check if this error is due to token expiration
250    pub fn is_expired(&self) -> bool {
251        matches!(self, TokenError::Expired { .. })
252    }
253
254    /// Check if this error is due to domain mismatch
255    pub fn is_domain_mismatch(&self) -> bool {
256        matches!(self, TokenError::DomainMismatch { .. })
257    }
258
259    /// Check if this error is due to identity mismatch
260    pub fn is_identity_mismatch(&self) -> bool {
261        matches!(
262            self,
263            TokenError::IdentityMismatch { .. } | TokenError::HierarchyViolation { .. }
264        )
265    }
266
267    /// Check if this error is due to authorization rights denial
268    pub fn is_rights_denied(&self) -> bool {
269        matches!(self, TokenError::RightsDenied { .. })
270    }
271
272    /// Check if this error is due to service chain failure
273    pub fn is_service_chain_failure(&self) -> bool {
274        matches!(self, TokenError::ServiceChainFailed { .. })
275    }
276
277    /// Check if this error is a signature/format error
278    pub fn is_signature_error(&self) -> bool {
279        matches!(
280            self,
281            TokenError::InvalidSignature { .. }
282                | TokenError::UnknownPublicKey
283                | TokenError::DeserializationError { .. }
284                | TokenError::Base64DecodingError { .. }
285        )
286    }
287
288    /// Get the expiration time if this is an expiration error
289    pub fn get_expiration_time(&self) -> Option<i64> {
290        match self {
291            TokenError::Expired { expired_at, .. } => Some(*expired_at),
292            _ => None,
293        }
294    }
295
296    /// Get the expected domain if this is a domain mismatch error
297    pub fn get_expected_domain(&self) -> Option<&str> {
298        match self {
299            TokenError::DomainMismatch { expected, .. } => Some(expected.as_str()),
300            _ => None,
301        }
302    }
303
304    /// Get the missing rights if this is a rights denied error
305    pub fn get_denied_access(&self) -> Option<(&str, &str, &str)> {
306        match self {
307            TokenError::RightsDenied {
308                subject,
309                resource,
310                operation,
311            } => Some((subject.as_str(), resource.as_str(), operation.as_str())),
312            _ => None,
313        }
314    }
315
316    // ===== Constructor Helper Methods =====
317
318    /// Create a generic error
319    pub fn generic<S: Into<String>>(msg: S) -> Self {
320        TokenError::Generic(msg.into())
321    }
322
323    /// Create an internal error
324    pub fn internal<S: Into<String>>(msg: S) -> Self {
325        TokenError::Internal(msg.into())
326    }
327
328    /// Create an invalid key format error
329    pub fn invalid_key_format<S: Into<String>>(reason: S) -> Self {
330        TokenError::InvalidKeyFormat {
331            reason: reason.into(),
332        }
333    }
334}
335
336// ===== Conversions from biscuit-auth errors =====
337
338impl From<biscuit_auth::error::Token> for TokenError {
339    fn from(err: biscuit_auth::error::Token) -> Self {
340        use biscuit_auth::error::{Logic, MatchedPolicy, Token};
341
342        match err {
343            Token::Format(format_err) => {
344                use biscuit_auth::error::Format;
345                match format_err {
346                    Format::Signature(sig_err) => TokenError::InvalidSignature {
347                        details: sig_err.to_string(),
348                    },
349                    Format::UnknownPublicKey => TokenError::UnknownPublicKey,
350                    Format::DeserializationError(msg) | Format::BlockDeserializationError(msg) => {
351                        TokenError::DeserializationError { reason: msg }
352                    }
353                    Format::SerializationError(msg) | Format::BlockSerializationError(msg) => {
354                        TokenError::SerializationError { reason: msg }
355                    }
356                    Format::Version {
357                        maximum,
358                        minimum,
359                        actual,
360                    } => TokenError::UnsupportedVersion {
361                        maximum,
362                        minimum,
363                        actual,
364                    },
365                    Format::InvalidKey(msg) => TokenError::InvalidKeyFormat { reason: msg },
366                    other => TokenError::Generic(other.to_string()),
367                }
368            }
369            Token::Base64(base64_err) => TokenError::Base64DecodingError {
370                reason: base64_err.to_string(),
371            },
372            Token::FailedLogic(logic_err) => match logic_err {
373                Logic::Unauthorized { policy, checks } => {
374                    let failed_checks = convert_failed_checks(checks);
375                    let (policy_type, policy_index) = match policy {
376                        MatchedPolicy::Allow(idx) => ("Allow", idx),
377                        MatchedPolicy::Deny(idx) => ("Deny", idx),
378                    };
379                    TokenError::PolicyMatchedButChecksFailed {
380                        policy_type: policy_type.to_string(),
381                        policy_index,
382                        failed_checks,
383                    }
384                }
385                Logic::NoMatchingPolicy { checks } => {
386                    let failed_checks = convert_failed_checks(checks);
387                    TokenError::NoMatchingPolicy { failed_checks }
388                }
389                Logic::InvalidBlockRule(block_id, rule) => {
390                    TokenError::InvalidBlockRule { block_id, rule }
391                }
392                Logic::AuthorizerNotEmpty => {
393                    TokenError::Internal("Authorizer already contains a token".to_string())
394                }
395            },
396            Token::RunLimit(limit) => TokenError::ExecutionLimitReached {
397                reason: limit.to_string(),
398            },
399            Token::Execution(expr) => TokenError::ExpressionError {
400                reason: expr.to_string(),
401            },
402            Token::Language(lang_err) => TokenError::Generic(lang_err.to_string()),
403            other => TokenError::Generic(other.to_string()),
404        }
405    }
406}
407
408/// Convert biscuit FailedCheck to our CheckFailure type
409fn convert_failed_checks(checks: Vec<biscuit_auth::error::FailedCheck>) -> Vec<CheckFailure> {
410    checks
411        .into_iter()
412        .map(|check| match check {
413            biscuit_auth::error::FailedCheck::Block(block_check) => CheckFailure {
414                block_id: Some(block_check.block_id),
415                check_id: block_check.check_id,
416                rule: block_check.rule,
417            },
418            biscuit_auth::error::FailedCheck::Authorizer(auth_check) => CheckFailure {
419                block_id: None,
420                check_id: auth_check.check_id,
421                rule: auth_check.rule,
422            },
423        })
424        .collect()
425}
426
427impl From<hex::FromHexError> for TokenError {
428    fn from(err: hex::FromHexError) -> Self {
429        TokenError::InvalidKeyFormat {
430            reason: err.to_string(),
431        }
432    }
433}
434
435impl From<&str> for TokenError {
436    fn from(err: &str) -> Self {
437        TokenError::Generic(err.to_string())
438    }
439}
440
441impl From<String> for TokenError {
442    fn from(err: String) -> Self {
443        TokenError::Generic(err)
444    }
445}