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
347
348
349
350
351
352
353
354
355
356
357
358
//! Entity base settings shared by [`crate::sp::ServiceProvider`] and
//! [`crate::idp::IdentityProvider`].
use core::fmt;
use crate::binding::MAX_DEFLATE_RAW_DECODE_BYTES;
use crate::constants::{
data_encryption_algorithm, key_encryption_algorithm, signature_algorithm, transform_algorithm,
MessageSignatureOrder,
};
use crate::xml::XmlLimits;
/// Runtime configuration for an entity (keys, algorithms, flags).
///
/// Use [`EntitySetting::default`] and tweak the fields you need.
#[non_exhaustive]
#[derive(Clone)]
pub struct EntitySetting {
/// Override entity ID (otherwise taken from metadata).
pub entity_id: Option<String>,
/// Signature algorithm URI for outgoing signatures.
pub request_signature_algorithm: String,
/// Data encryption algorithm URI.
pub data_encryption_algorithm: String,
/// Key encryption algorithm URI.
pub key_encryption_algorithm: String,
/// Sign-then-encrypt vs encrypt-then-sign.
pub message_signing_order: MessageSignatureOrder,
/// `AllowCreate` for the NameIDPolicy.
pub allow_create: bool,
/// Whether assertions are encrypted.
pub is_assertion_encrypted: bool,
/// Allow XML-Enc RSA key-transport decryption with the bundled software RSA backend.
///
/// This is disabled by default because `bergshamra` currently reaches the
/// RustCrypto `rsa` crate, which is affected by `RUSTSEC-2023-0071` when an
/// attacker can observe timing. Prefer an external/HSM decryptor once one is
/// exposed through the public API; enable this only as an explicit
/// compatibility exception for deployments that accept that risk.
pub allow_insecure_software_rsa_key_transport_decryption: bool,
/// Default RelayState.
pub relay_state: String,
/// SP: signs its AuthnRequests.
pub authn_requests_signed: bool,
/// SP: requires signed assertions.
pub want_assertions_signed: bool,
/// SP: reject a `<Response>` whose `<Audience>` is not this entity (default `true`).
pub validate_audience: bool,
/// SP: requires signed messages.
pub want_message_signed: bool,
/// IdP: requires signed AuthnRequests.
pub want_authn_requests_signed: bool,
/// Requires signed LogoutRequest (default `true`).
pub want_logout_request_signed: bool,
/// Requires signed LogoutResponse.
pub want_logout_response_signed: bool,
/// Supported NameID formats.
pub name_id_format: Vec<String>,
/// Signing private key (PEM).
pub private_key: Option<String>,
/// Passphrase for `private_key`.
pub private_key_pass: Option<String>,
/// Signing certificate (PEM/base64).
pub signing_cert: Option<String>,
/// Encryption certificate (PEM/base64).
pub encrypt_cert: Option<String>,
/// Decryption private key (PEM).
pub enc_private_key: Option<String>,
/// Passphrase for `enc_private_key`.
pub enc_private_key_pass: Option<String>,
/// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
pub clock_drifts: (i64, i64),
/// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
/// HTTP-Redirect input.
///
/// SAML does not define this limit; the default is a conservative
/// resource-exhaustion guard for unauthenticated Redirect messages.
pub redirect_inflate_max_bytes: usize,
/// XML parser resource limits for inbound messages and metadata parsing.
pub xml_limits: XmlLimits,
/// IdP: protocol tag prefix for generated IdP messages (default `samlp`).
pub tag_prefix_protocol: String,
/// IdP: assertion tag prefix for generated IdP messages (default `saml`).
pub tag_prefix_assertion: String,
/// IdP: tag prefix for the `<EncryptedAssertion>` element (default `saml`).
pub tag_prefix_encrypted_assertion: String,
/// IdP: login `<Response>` template + attribute configuration.
pub login_response_template: Option<crate::template::LoginResponseTemplate>,
/// SP: custom `<AuthnRequest>` template (`None` uses the default).
pub login_request_template: Option<String>,
/// Custom `<LogoutRequest>` template (`None` uses the default).
pub logout_request_template: Option<String>,
/// Custom `<LogoutResponse>` template (`None` uses the default).
pub logout_response_template: Option<String>,
/// Custom embedded-signature placement/prefix (`None` uses the default).
pub signature_config: Option<SignatureConfig>,
/// XML-DSig transforms applied to signed references (default
/// enveloped-signature + exclusive C14N).
pub transformation_algorithms: Vec<String>,
}
fn redacted_option(value: &Option<String>) -> Option<&'static str> {
value.as_ref().map(|_| "<redacted>")
}
impl fmt::Debug for EntitySetting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EntitySetting")
.field("entity_id", &self.entity_id)
.field(
"request_signature_algorithm",
&self.request_signature_algorithm,
)
.field("data_encryption_algorithm", &self.data_encryption_algorithm)
.field("key_encryption_algorithm", &self.key_encryption_algorithm)
.field("message_signing_order", &self.message_signing_order)
.field("allow_create", &self.allow_create)
.field("is_assertion_encrypted", &self.is_assertion_encrypted)
.field(
"allow_insecure_software_rsa_key_transport_decryption",
&self.allow_insecure_software_rsa_key_transport_decryption,
)
.field("relay_state", &self.relay_state)
.field("authn_requests_signed", &self.authn_requests_signed)
.field("want_assertions_signed", &self.want_assertions_signed)
.field("validate_audience", &self.validate_audience)
.field("want_message_signed", &self.want_message_signed)
.field(
"want_authn_requests_signed",
&self.want_authn_requests_signed,
)
.field(
"want_logout_request_signed",
&self.want_logout_request_signed,
)
.field(
"want_logout_response_signed",
&self.want_logout_response_signed,
)
.field("name_id_format", &self.name_id_format)
.field("private_key", &redacted_option(&self.private_key))
.field("private_key_pass", &redacted_option(&self.private_key_pass))
.field("signing_cert", &redacted_option(&self.signing_cert))
.field("encrypt_cert", &redacted_option(&self.encrypt_cert))
.field("enc_private_key", &redacted_option(&self.enc_private_key))
.field(
"enc_private_key_pass",
&redacted_option(&self.enc_private_key_pass),
)
.field("clock_drifts", &self.clock_drifts)
.field(
"redirect_inflate_max_bytes",
&self.redirect_inflate_max_bytes,
)
.field("xml_limits", &self.xml_limits)
.field("tag_prefix_protocol", &self.tag_prefix_protocol)
.field("tag_prefix_assertion", &self.tag_prefix_assertion)
.field(
"tag_prefix_encrypted_assertion",
&self.tag_prefix_encrypted_assertion,
)
.field("login_response_template", &self.login_response_template)
.field("login_request_template", &self.login_request_template)
.field("logout_request_template", &self.logout_request_template)
.field("logout_response_template", &self.logout_response_template)
.field("signature_config", &self.signature_config)
.field("transformation_algorithms", &self.transformation_algorithms)
.finish()
}
}
/// Custom message rendering hook: given the resolved template, returns
/// `(id, rendered_xml)`.
pub type CustomTagReplacement<'a> = &'a dyn Fn(&str) -> (String, String);
/// Where to place the `<Signature>` relative to the reference element.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SignatureAction {
/// Insert as the reference's next sibling.
#[default]
After,
/// Insert as the reference's previous sibling.
Before,
/// Insert as the reference's first child.
Prepend,
/// Insert as the reference's last child.
Append,
}
/// Customizes the embedded XML-DSig signature.
#[derive(Debug, Clone)]
pub struct SignatureConfig {
/// Element prefix for the signature (default `ds`).
pub prefix: String,
/// `local-name()` XPath of the reference element; `None` keeps the default
/// (after the signed target's `<Issuer>`).
pub reference: Option<String>,
/// Placement relative to `reference`.
pub action: SignatureAction,
}
impl Default for SignatureConfig {
fn default() -> Self {
Self {
prefix: "ds".to_string(),
reference: None,
action: SignatureAction::After,
}
}
}
impl Default for EntitySetting {
fn default() -> Self {
Self {
entity_id: None,
request_signature_algorithm: signature_algorithm::RSA_SHA256.to_string(),
data_encryption_algorithm: data_encryption_algorithm::AES_256.to_string(),
key_encryption_algorithm: key_encryption_algorithm::RSA_OAEP_MGF1P.to_string(),
message_signing_order: MessageSignatureOrder::SignThenEncrypt,
allow_create: false,
is_assertion_encrypted: false,
allow_insecure_software_rsa_key_transport_decryption: false,
relay_state: String::new(),
authn_requests_signed: false,
want_assertions_signed: false,
validate_audience: true,
want_message_signed: false,
want_authn_requests_signed: false,
want_logout_request_signed: true,
want_logout_response_signed: true,
name_id_format: Vec::new(),
private_key: None,
private_key_pass: None,
signing_cert: None,
encrypt_cert: None,
enc_private_key: None,
enc_private_key_pass: None,
clock_drifts: (0, 0),
redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
xml_limits: XmlLimits::default(),
tag_prefix_protocol: "samlp".to_string(),
tag_prefix_assertion: "saml".to_string(),
tag_prefix_encrypted_assertion: "saml".to_string(),
login_response_template: None,
login_request_template: None,
logout_request_template: None,
logout_response_template: None,
signature_config: None,
transformation_algorithms: vec![
transform_algorithm::ENVELOPED_SIGNATURE.to_string(),
transform_algorithm::EXC_C14N.to_string(),
],
}
}
}
/// Generate a SAML message ID (`_` + UUIDv4).
pub fn generate_id() -> String {
format!("_{}", uuid::Uuid::new_v4())
}
/// The authenticated subject an IdP issues a response for.
#[derive(Debug, Clone, Default)]
pub struct User {
/// `<NameID>` value.
pub name_id: String,
/// Attribute values keyed by their `LoginResponseAttribute.value_tag`;
/// each fills the `{attr<Tag>}` placeholder produced for that attribute.
pub attributes: Vec<(String, String)>,
/// `SessionIndex` for Single Logout requests.
pub session_index: Option<String>,
}
impl User {
/// A subject with just a NameID and no attributes.
pub fn new(name_id: impl Into<String>) -> Self {
Self {
name_id: name_id.into(),
..Default::default()
}
}
}
/// Current UTC time as an ISO-8601 `IssueInstant` (`YYYY-MM-DDTHH:MM:SSZ`).
pub fn now_iso8601() -> String {
iso8601_offset(0)
}
/// UTC time `seconds` from now as ISO-8601 (`YYYY-MM-DDTHH:MM:SSZ`).
pub fn iso8601_offset(seconds: i64) -> String {
let t = time::OffsetDateTime::now_utc() + time::Duration::seconds(seconds);
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
t.year(),
u8::from(t.month()),
t.day(),
t.hour(),
t.minute(),
t.second(),
)
}
/// The product of building an outbound message for a binding.
#[derive(Debug, Clone)]
pub struct BindingContext {
/// Generated message ID.
pub id: String,
/// Redirect: the full URL. POST/SimpleSign: the base64 message.
pub context: String,
/// RelayState, if any.
pub relay_state: Option<String>,
/// Destination endpoint.
pub entity_endpoint: String,
/// Binding used.
pub binding: crate::constants::Binding,
/// `SAMLRequest` or `SAMLResponse`.
pub request_type: &'static str,
/// Detached signature (redirect/SimpleSign signed messages), if computed.
pub signature: Option<String>,
/// Signature algorithm URI accompanying `signature`.
pub sig_alg: Option<String>,
}
impl BindingContext {
/// Build the POST/SimpleSign auto-submit form (the `context` must be base64).
///
/// If exactly one of `sig_alg` or `signature` is present, this infallible
/// helper omits the detached SimpleSign fields. Use [`Self::try_post_form`]
/// to reject partial detached signature state.
pub fn post_form(&self) -> String {
crate::binding::saml_post_binding_form_with_signature(
&self.entity_endpoint,
self.request_type,
&self.context,
self.relay_state.as_deref(),
self.sig_alg.as_deref(),
self.signature.as_deref(),
)
}
/// Build the POST/SimpleSign auto-submit form after validating the endpoint.
///
/// # Errors
///
/// Returns [`crate::error::SamlError::Invalid`] when `entity_endpoint`
/// is not an absolute HTTP(S) URL, or when detached SimpleSign state has
/// only one of `sig_alg` or `signature`.
pub fn try_post_form(&self) -> Result<String, crate::error::SamlError> {
crate::binding::try_saml_post_binding_form_with_signature(
&self.entity_endpoint,
self.request_type,
&self.context,
self.relay_state.as_deref(),
self.sig_alg.as_deref(),
self.signature.as_deref(),
)
}
}