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