saml_rs/error.rs
1//! Error types for `saml-rs`.
2//!
3//! [`SamlError`] is non-exhaustive. Callers should include a fallback match arm
4//! so new semantic SAML validation failures can be added without breaking
5//! source compatibility.
6
7use crate::constants::Binding;
8use crate::model::RelayStateParam;
9use std::fmt;
10
11/// Reason a required SAML signature verification failed.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum SignatureVerificationReason {
15 /// Enveloped XML-DSig verification failed.
16 XmlSignature,
17 /// HTTP-Redirect or SimpleSign detached message signature verification failed.
18 DetachedMessageSignature,
19 /// Detached signature input could not be correlated with consumed RelayState.
20 RelayStateCorrelation,
21 /// Signed reference digest verification failed.
22 ReferenceDigest,
23}
24
25impl fmt::Display for SignatureVerificationReason {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::XmlSignature => f.write_str("xml signature"),
29 Self::DetachedMessageSignature => f.write_str("detached message signature"),
30 Self::RelayStateCorrelation => f.write_str("relay state correlation"),
31 Self::ReferenceDigest => f.write_str("reference digest"),
32 }
33 }
34}
35
36/// Reason a signed XML reference could not be resolved safely.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum ReferenceResolutionReason {
40 /// Reference URI points outside the same XML document.
41 ExternalReference,
42 /// Reference URI syntax is not supported by the SAML verifier.
43 UnsupportedReferenceUri,
44 /// Signature contained no references to validate.
45 MissingSignatureReference,
46 /// Same-document reference did not resolve to an XML node.
47 UnresolvedReference,
48}
49
50impl fmt::Display for ReferenceResolutionReason {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::ExternalReference => f.write_str("external reference"),
54 Self::UnsupportedReferenceUri => f.write_str("unsupported reference URI"),
55 Self::MissingSignatureReference => f.write_str("missing signature reference"),
56 Self::UnresolvedReference => f.write_str("unresolved reference"),
57 }
58 }
59}
60
61/// Bearer subject confirmation validation failure reason.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63#[non_exhaustive]
64pub enum SubjectConfirmationReason {
65 /// SubjectConfirmation `Method` was missing or was not bearer.
66 InvalidMethod,
67 /// SubjectConfirmationData omitted `NotOnOrAfter`.
68 MissingNotOnOrAfter,
69 /// SubjectConfirmationData `NotOnOrAfter` time bounds were not satisfied.
70 TimeWindowInvalid,
71 /// SubjectConfirmationData `Recipient` did not match the ACS URL.
72 RecipientMismatch,
73 /// SubjectConfirmationData `InResponseTo` did not match the request ID.
74 InResponseToMismatch,
75 /// No bearer SubjectConfirmation satisfied the validation requirements.
76 MissingBearerConfirmation,
77}
78
79impl fmt::Display for SubjectConfirmationReason {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::InvalidMethod => f.write_str("method"),
83 Self::MissingNotOnOrAfter => f.write_str("missing NotOnOrAfter"),
84 Self::TimeWindowInvalid => f.write_str("time window"),
85 Self::RecipientMismatch => f.write_str("recipient"),
86 Self::InResponseToMismatch => f.write_str("InResponseTo"),
87 Self::MissingBearerConfirmation => f.write_str("missing bearer confirmation"),
88 }
89 }
90}
91
92/// SAML time-bound field whose validation failed.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub enum TimeWindowField {
96 /// Expiration derived from the typed IdP issuance lifetime could not be
97 /// represented for the current issue instant.
98 IdpIssuanceExpiration,
99 /// LogoutRequest `NotOnOrAfter`.
100 LogoutRequestNotOnOrAfter,
101 /// Assertion session `SessionNotOnOrAfter`.
102 SessionNotOnOrAfter,
103 /// Assertion `Conditions` NotBefore/NotOnOrAfter window.
104 Conditions,
105 /// Replay cache retention window could not be computed or has elapsed.
106 ReplayExpiration,
107 /// AuthnRequest `IssueInstant` age window.
108 AuthnRequestIssueInstant,
109}
110
111impl fmt::Display for TimeWindowField {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 Self::IdpIssuanceExpiration => f.write_str("IdP issuance expiration"),
115 Self::LogoutRequestNotOnOrAfter => f.write_str("LogoutRequest@NotOnOrAfter"),
116 Self::SessionNotOnOrAfter => f.write_str("SessionNotOnOrAfter"),
117 Self::Conditions => f.write_str("Conditions"),
118 Self::ReplayExpiration => f.write_str("ReplayExpiration"),
119 Self::AuthnRequestIssueInstant => f.write_str("AuthnRequest@IssueInstant"),
120 }
121 }
122}
123
124/// Errors produced by the SAML-RS library.
125///
126/// Variants are grouped by validation category:
127///
128/// - wire and XML decoding failures;
129/// - SAML protocol validation failures;
130/// - signature, signed-reference, and delegated crypto failures;
131/// - metadata and trust selection failures;
132/// - configuration, unsupported binding, and compatibility failures.
133///
134/// This enum is `#[non_exhaustive]`; downstream callers should include a
135/// fallback arm when matching.
136#[derive(Debug, thiserror::Error)]
137#[non_exhaustive]
138pub enum SamlError {
139 // Wire / XML errors.
140 /// Raw DEFLATE (de)compression failed.
141 #[error("deflate error: {0}")]
142 Deflate(#[from] std::io::Error),
143 /// Base64 decoding failed.
144 #[error("base64 decode error: {0}")]
145 Base64(#[from] base64::DecodeError),
146 /// Malformed or unexpected XML.
147 #[error("xml error: {0}")]
148 Xml(String),
149 /// Input failed validation.
150 #[error("invalid input: {0}")]
151 Invalid(String),
152 /// XML violates SAML protocol requirements such as QName, version, required
153 /// attributes, or their lexical forms.
154 #[error("SAML protocol profile violation: {0}")]
155 ProtocolProfile(String),
156
157 // Unsupported profiles and bindings.
158 /// Functionality not yet implemented in the current milestone.
159 #[error("unsupported: {0}")]
160 Unsupported(String),
161 /// Requested binding is not supported by this API path.
162 #[error("unsupported binding: {binding:?}")]
163 UnsupportedBinding {
164 /// Binding that reached an unsupported profile path.
165 binding: Binding,
166 },
167
168 // SAML protocol validation.
169 /// Issuer in the message does not match the expected peer entity ID.
170 #[error("issuer mismatch: expected {expected}, got {actual:?}")]
171 IssuerMismatch {
172 /// Expected peer entity ID.
173 expected: String,
174 /// Actual issuer value extracted from the message, when available.
175 actual: Option<String>,
176 },
177 /// Response `Destination` does not match the expected recipient URL.
178 #[error("destination mismatch: expected {expected}, got {actual:?}")]
179 DestinationMismatch {
180 /// Expected recipient URL.
181 expected: String,
182 /// Actual destination value extracted from the message, when available.
183 actual: Option<String>,
184 },
185 /// `InResponseTo` does not match the expected request ID.
186 #[error("inResponseTo mismatch: expected {expected:?}, got {actual:?}")]
187 InResponseToMismatch {
188 /// Expected request ID. `None` means no request correlation was expected.
189 expected: Option<String>,
190 /// Actual `InResponseTo` value extracted from the message, when available.
191 actual: Option<String>,
192 },
193 /// RelayState does not match the pending message correlation state.
194 #[error("relay state mismatch")]
195 RelayStateMismatch {
196 /// Expected RelayState presence and value.
197 expected: RelayStateParam,
198 /// Actual RelayState presence and value.
199 actual: RelayStateParam,
200 },
201 /// `<Audience>` does not include the expected Service Provider entity ID.
202 #[error("audience restriction not satisfied: expected {expected}")]
203 AudienceMismatch {
204 /// Expected SP entity ID.
205 expected: String,
206 },
207 /// A `<Conditions>` child is not one this service provider can evaluate.
208 #[error("assertion condition is not understood: {element}")]
209 UnrecognizedCondition {
210 /// Local name of the unrecognized condition element.
211 element: String,
212 },
213 /// Assertions in one response do not share one principal.
214 ///
215 /// The comparison is `NameID` value, `Format`, `NameQualifier`, and
216 /// `SPNameQualifier`. An omitted `Format` is unspecified.
217 #[error("assertions do not refer to one principal")]
218 PrincipalMismatch,
219 /// Response carried a non-success SAML status code.
220 #[error("status not success: top={top}, second={second:?}")]
221 StatusNotSuccess {
222 /// Top-tier status code.
223 top: String,
224 /// Optional second-tier status code.
225 second: Option<String>,
226 },
227 /// A SAML message, assertion, session, or replay time bound is not satisfied.
228 #[error("SAML time window is invalid for {field}")]
229 TimeWindowInvalid {
230 /// SAML field or validation scope whose time window failed.
231 field: TimeWindowField,
232 },
233 /// Bearer subject confirmation requirements are not satisfied.
234 #[error("subject confirmation is not satisfied: {reason}")]
235 SubjectConfirmationInvalid {
236 /// Stable validation reason for callers and logs.
237 reason: SubjectConfirmationReason,
238 },
239 /// A later assertion matches an outstanding logout that is still before
240 /// `NotOnOrAfter`.
241 #[error("assertion matches an outstanding logout")]
242 AssertionMatchesOutstandingLogout,
243 /// A duplicate SAML message or assertion key was detected.
244 #[error("replayed SAML message or assertion: {key}")]
245 ReplayDetected {
246 /// Replay cache key or duplicate identifier.
247 key: String,
248 },
249
250 // Signature / crypto validation.
251 /// Required signature is absent.
252 #[error("signature missing where required")]
253 SignatureMissing,
254 /// Required binding parameter is absent.
255 #[error("required binding parameter missing: {name}")]
256 MissingBindingParameter {
257 /// Binding parameter name.
258 name: &'static str,
259 },
260 /// Signature verification failed for a semantic SAML validation reason.
261 #[error("signature verification failed: {reason}")]
262 SignatureVerification {
263 /// Stable verification reason for callers and logs.
264 reason: SignatureVerificationReason,
265 },
266 /// Signed reference could not be resolved safely.
267 #[error("signed reference could not be resolved: {reason}")]
268 ReferenceResolution {
269 /// Stable reference-resolution reason for callers and logs.
270 reason: ReferenceResolutionReason,
271 },
272 /// Verified signed reference does not cover the consumed payload.
273 #[error("signed reference does not cover consumed payload")]
274 SignedReferenceMismatch,
275 /// Assertion-signature policy requires direct coverage of the consumed assertion.
276 #[error("consumed assertion is not directly covered by a trusted XML signature")]
277 AssertionSignatureRequired,
278
279 /// An embedded XML signature selected an algorithm outside the strict SAML profile.
280 #[error("embedded XML signature algorithm is not allowed")]
281 AlgorithmUnsupported,
282
283 // Metadata / trust validation.
284 /// No trusted certificate could be selected for verification.
285 #[error("no trusted certificate could be selected for verification")]
286 NoTrustedCertificate,
287 /// Certificate embedded in the message does not match configured metadata.
288 #[error("certificate mismatch")]
289 CertificateMismatch,
290
291 // Legacy compatibility and lower-level operational variants.
292 /// Compatibility variant for issuer validation; prefer [`Self::IssuerMismatch`].
293 #[error("ERR_UNMATCH_ISSUER")]
294 UnmatchIssuer,
295 /// Compatibility variant for audience validation; prefer [`Self::AudienceMismatch`].
296 #[error("ERR_UNMATCH_AUDIENCE")]
297 UnmatchAudience,
298 /// Compatibility variant for destination validation; prefer [`Self::DestinationMismatch`].
299 #[error("ERR_UNMATCH_DESTINATION")]
300 UnmatchDestination,
301 /// Caller supplied a malformed request ID for response correlation.
302 ///
303 /// Prefer [`Self::InResponseToMismatch`] when a message value fails request
304 /// correlation.
305 #[error("ERR_INVALID_IN_RESPONSE_TO")]
306 InvalidInResponseTo,
307 /// Response status code is missing, empty, or could not be extracted.
308 #[error("ERR_UNDEFINED_STATUS")]
309 UndefinedStatus,
310 /// Compatibility variant for non-success status; prefer [`Self::StatusNotSuccess`].
311 #[error("ERR_FAILED_STATUS with top tier code: {top}, second tier code: {second}")]
312 FailedStatus {
313 /// Top-tier status code.
314 top: String,
315 /// Second-tier status code (empty when absent).
316 second: String,
317 },
318 /// Compatibility variant for elapsed session bounds; prefer [`Self::TimeWindowInvalid`].
319 #[error("ERR_EXPIRED_SESSION")]
320 ExpiredSession,
321 /// Compatibility variant for subject confirmation failures; prefer
322 /// [`Self::SubjectConfirmationInvalid`].
323 #[error("ERR_SUBJECT_UNCONFIRMED")]
324 SubjectUnconfirmed,
325 /// A signature-wrapping (XSW) attempt was detected.
326 #[error("ERR_POTENTIAL_WRAPPING_ATTACK")]
327 PotentialWrappingAttack,
328 /// Compatibility variant for missing binding signature parameters; prefer
329 /// [`Self::SignatureMissing`] or [`Self::MissingBindingParameter`].
330 #[error("ERR_MISSING_SIG_ALG")]
331 MissingSigAlg,
332 /// Compatibility variant for detached signature failures; prefer
333 /// [`Self::SignatureVerification`].
334 #[error("ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION")]
335 FailedMessageSignatureVerification,
336 /// Compatibility variant for XML-DSig failures; prefer [`Self::SignatureVerification`].
337 #[error("FAILED_TO_VERIFY_SIGNATURE")]
338 FailedToVerifySignature,
339 /// Compatibility variant for certificate mismatch; prefer [`Self::CertificateMismatch`].
340 #[error("ERROR_UNMATCH_CERTIFICATE_DECLARATION_IN_METADATA")]
341 UnmatchCertificate,
342 /// Compatibility variant for unsupported bindings; prefer [`Self::UnsupportedBinding`].
343 #[error("ERR_UNDEFINED_BINDING")]
344 UndefinedBinding,
345 /// Required metadata (endpoint/certificate) was missing.
346 #[error("missing metadata: {0}")]
347 MissingMetadata(String),
348 /// A required cryptographic key was missing.
349 #[error("missing key: {0}")]
350 MissingKey(String),
351 /// A delegated cryptographic operation failed.
352 #[error("crypto error: {0}")]
353 Crypto(String),
354}
355
356impl SamlError {
357 pub(crate) fn issuer_mismatch(expected: &str, actual: Option<&str>) -> Self {
358 Self::IssuerMismatch {
359 expected: expected.to_string(),
360 actual: actual.map(str::to_string),
361 }
362 }
363
364 pub(crate) fn destination_mismatch(expected: &str, actual: Option<&str>) -> Self {
365 Self::DestinationMismatch {
366 expected: expected.to_string(),
367 actual: actual.map(str::to_string),
368 }
369 }
370
371 pub(crate) fn in_response_to_mismatch(expected: Option<&str>, actual: Option<&str>) -> Self {
372 Self::InResponseToMismatch {
373 expected: expected.map(str::to_string),
374 actual: actual.map(str::to_string),
375 }
376 }
377}