1use core::fmt;
5
6use crate::binding::MAX_DEFLATE_RAW_DECODE_BYTES;
7use crate::constants::{
8 data_encryption_algorithm, key_encryption_algorithm, signature_algorithm, transform_algorithm,
9 MessageSignatureOrder,
10};
11use crate::error::{SamlError, TimeWindowField};
12use crate::xml::XmlLimits;
13
14#[non_exhaustive]
18#[derive(Clone)]
19pub struct EntitySetting {
20 pub entity_id: Option<String>,
22 pub request_signature_algorithm: String,
24 pub data_encryption_algorithm: String,
26 pub key_encryption_algorithm: String,
28 pub message_signing_order: MessageSignatureOrder,
30 pub allow_create: bool,
32 pub is_assertion_encrypted: bool,
34 pub allow_insecure_software_rsa_key_transport_decryption: bool,
45 pub relay_state: String,
47 pub authn_requests_signed: bool,
49 pub want_assertions_signed: bool,
51 pub validate_audience: bool,
53 pub(crate) require_audience_restriction: bool,
59 pub want_message_signed: bool,
61 pub(crate) strict_xml_signature_profile: bool,
64 pub(crate) want_encrypted_cbc_response_signed: bool,
66 pub want_authn_requests_signed: bool,
68 pub(crate) verify_authn_request_signature_if_present: bool,
71 pub want_logout_request_signed: bool,
73 pub want_logout_response_signed: bool,
75 pub name_id_format: Vec<String>,
77 pub private_key: Option<String>,
79 pub private_key_pass: Option<String>,
81 pub signing_cert: Option<String>,
83 pub encrypt_cert: Option<String>,
85 pub enc_private_key: Option<String>,
87 pub enc_private_key_pass: Option<String>,
89 pub clock_drifts: (i64, i64),
91 pub redirect_inflate_max_bytes: usize,
97 pub xml_limits: XmlLimits,
99 pub tag_prefix_protocol: String,
101 pub tag_prefix_assertion: String,
103 pub tag_prefix_encrypted_assertion: String,
105 pub login_response_template: Option<crate::template::LoginResponseTemplate>,
107 pub login_request_template: Option<String>,
109 pub logout_request_template: Option<String>,
115 pub logout_response_template: Option<String>,
124 pub signature_config: Option<SignatureConfig>,
126 pub transformation_algorithms: Vec<String>,
129}
130
131fn redacted_option(value: &Option<String>) -> Option<&'static str> {
132 value.as_ref().map(|_| "<redacted>")
133}
134
135impl fmt::Debug for EntitySetting {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("EntitySetting")
138 .field("entity_id", &self.entity_id)
139 .field(
140 "request_signature_algorithm",
141 &self.request_signature_algorithm,
142 )
143 .field("data_encryption_algorithm", &self.data_encryption_algorithm)
144 .field("key_encryption_algorithm", &self.key_encryption_algorithm)
145 .field("message_signing_order", &self.message_signing_order)
146 .field("allow_create", &self.allow_create)
147 .field("is_assertion_encrypted", &self.is_assertion_encrypted)
148 .field(
149 "allow_insecure_software_rsa_key_transport_decryption",
150 &self.allow_insecure_software_rsa_key_transport_decryption,
151 )
152 .field("relay_state", &self.relay_state)
153 .field("authn_requests_signed", &self.authn_requests_signed)
154 .field("want_assertions_signed", &self.want_assertions_signed)
155 .field("validate_audience", &self.validate_audience)
156 .field(
157 "require_audience_restriction",
158 &self.require_audience_restriction,
159 )
160 .field("want_message_signed", &self.want_message_signed)
161 .field(
162 "strict_xml_signature_profile",
163 &self.strict_xml_signature_profile,
164 )
165 .field(
166 "want_encrypted_cbc_response_signed",
167 &self.want_encrypted_cbc_response_signed,
168 )
169 .field(
170 "want_authn_requests_signed",
171 &self.want_authn_requests_signed,
172 )
173 .field(
174 "verify_authn_request_signature_if_present",
175 &self.verify_authn_request_signature_if_present,
176 )
177 .field(
178 "want_logout_request_signed",
179 &self.want_logout_request_signed,
180 )
181 .field(
182 "want_logout_response_signed",
183 &self.want_logout_response_signed,
184 )
185 .field("name_id_format", &self.name_id_format)
186 .field("private_key", &redacted_option(&self.private_key))
187 .field("private_key_pass", &redacted_option(&self.private_key_pass))
188 .field("signing_cert", &redacted_option(&self.signing_cert))
189 .field("encrypt_cert", &redacted_option(&self.encrypt_cert))
190 .field("enc_private_key", &redacted_option(&self.enc_private_key))
191 .field(
192 "enc_private_key_pass",
193 &redacted_option(&self.enc_private_key_pass),
194 )
195 .field("clock_drifts", &self.clock_drifts)
196 .field(
197 "redirect_inflate_max_bytes",
198 &self.redirect_inflate_max_bytes,
199 )
200 .field("xml_limits", &self.xml_limits)
201 .field("tag_prefix_protocol", &self.tag_prefix_protocol)
202 .field("tag_prefix_assertion", &self.tag_prefix_assertion)
203 .field(
204 "tag_prefix_encrypted_assertion",
205 &self.tag_prefix_encrypted_assertion,
206 )
207 .field("login_response_template", &self.login_response_template)
208 .field("login_request_template", &self.login_request_template)
209 .field("logout_request_template", &self.logout_request_template)
210 .field("logout_response_template", &self.logout_response_template)
211 .field("signature_config", &self.signature_config)
212 .field("transformation_algorithms", &self.transformation_algorithms)
213 .finish()
214 }
215}
216
217pub type CustomTagReplacement<'a> = &'a dyn Fn(&str) -> (String, String);
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum SignatureAction {
224 #[default]
226 After,
227 Before,
229 Prepend,
231 Append,
233}
234
235#[derive(Debug, Clone)]
237pub struct SignatureConfig {
238 pub prefix: String,
240 pub reference: Option<String>,
243 pub action: SignatureAction,
245}
246
247impl Default for SignatureConfig {
248 fn default() -> Self {
249 Self {
250 prefix: "ds".to_string(),
251 reference: None,
252 action: SignatureAction::After,
253 }
254 }
255}
256
257impl Default for EntitySetting {
258 fn default() -> Self {
259 Self {
260 entity_id: None,
261 request_signature_algorithm: signature_algorithm::RSA_SHA256.to_string(),
262 data_encryption_algorithm: data_encryption_algorithm::AES_256.to_string(),
263 key_encryption_algorithm: key_encryption_algorithm::RSA_OAEP_MGF1P.to_string(),
264 message_signing_order: MessageSignatureOrder::SignThenEncrypt,
265 allow_create: false,
266 is_assertion_encrypted: false,
267 allow_insecure_software_rsa_key_transport_decryption: false,
268 relay_state: String::new(),
269 authn_requests_signed: false,
270 want_assertions_signed: false,
271 validate_audience: true,
272 require_audience_restriction: true,
273 want_message_signed: false,
274 strict_xml_signature_profile: false,
275 want_encrypted_cbc_response_signed: false,
276 want_authn_requests_signed: false,
277 verify_authn_request_signature_if_present: false,
278 want_logout_request_signed: true,
279 want_logout_response_signed: true,
280 name_id_format: Vec::new(),
281 private_key: None,
282 private_key_pass: None,
283 signing_cert: None,
284 encrypt_cert: None,
285 enc_private_key: None,
286 enc_private_key_pass: None,
287 clock_drifts: (0, 0),
288 redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
289 xml_limits: XmlLimits::default(),
290 tag_prefix_protocol: "samlp".to_string(),
291 tag_prefix_assertion: "saml".to_string(),
292 tag_prefix_encrypted_assertion: "saml".to_string(),
293 login_response_template: None,
294 login_request_template: None,
295 logout_request_template: None,
296 logout_response_template: None,
297 signature_config: None,
298 transformation_algorithms: vec![
299 transform_algorithm::ENVELOPED_SIGNATURE.to_string(),
300 transform_algorithm::EXC_C14N.to_string(),
301 ],
302 }
303 }
304}
305
306pub fn generate_id() -> String {
308 format!("_{}", uuid::Uuid::new_v4())
309}
310
311#[derive(Debug, Clone, Default)]
313pub struct User {
314 pub name_id: String,
316 pub attributes: Vec<(String, String)>,
319 pub session_index: Option<String>,
321}
322
323impl User {
324 pub fn new(name_id: impl Into<String>) -> Self {
326 Self {
327 name_id: name_id.into(),
328 ..Default::default()
329 }
330 }
331}
332
333pub fn now_iso8601() -> String {
335 iso8601_offset(0)
336}
337
338pub fn iso8601_offset(seconds: i64) -> String {
340 let t = time::OffsetDateTime::now_utc() + time::Duration::seconds(seconds);
341 format_saml_utc_date_time(t)
342}
343
344#[derive(Debug, Clone)]
345pub(crate) struct IdpIssuanceWindow {
346 pub(crate) issue_instant: String,
347 pub(crate) expiration: String,
348}
349
350pub(crate) fn capture_idp_issuance_window(
351 lifetime: time::Duration,
352) -> Result<IdpIssuanceWindow, SamlError> {
353 let issue_instant = time::OffsetDateTime::now_utc();
354 let expiration = issue_instant
355 .checked_add(lifetime)
356 .ok_or(SamlError::TimeWindowInvalid {
357 field: TimeWindowField::IdpIssuanceExpiration,
358 })?;
359 let preserve_subseconds = lifetime.subsec_nanoseconds() != 0;
360 Ok(IdpIssuanceWindow {
361 issue_instant: format_idp_issuance_instant(issue_instant, preserve_subseconds),
362 expiration: format_idp_issuance_instant(expiration, preserve_subseconds),
363 })
364}
365
366fn format_idp_issuance_instant(t: time::OffsetDateTime, preserve_subseconds: bool) -> String {
367 if preserve_subseconds {
368 format!(
369 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}Z",
370 t.year(),
371 u8::from(t.month()),
372 t.day(),
373 t.hour(),
374 t.minute(),
375 t.second(),
376 t.nanosecond(),
377 )
378 } else {
379 format_saml_utc_date_time(t)
380 }
381}
382
383pub(crate) fn format_saml_utc_date_time(t: time::OffsetDateTime) -> String {
386 format!(
387 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
388 t.year(),
389 u8::from(t.month()),
390 t.day(),
391 t.hour(),
392 t.minute(),
393 t.second(),
394 )
395}
396
397#[cfg(test)]
398mod tests {
399 use super::format_saml_utc_date_time;
400
401 #[test]
402 fn outbound_saml_instants_do_not_generate_leap_seconds(
403 ) -> Result<(), Box<dyn std::error::Error>> {
404 let last_second = time::OffsetDateTime::from_unix_timestamp(1_483_228_799)?;
406
407 assert_eq!(
408 format_saml_utc_date_time(last_second),
409 "2016-12-31T23:59:59Z"
410 );
411 Ok(())
412 }
413}
414
415#[derive(Debug, Clone)]
417pub struct BindingContext {
418 pub id: String,
420 pub context: String,
422 pub relay_state: Option<String>,
424 pub entity_endpoint: String,
426 pub binding: crate::constants::Binding,
428 pub request_type: &'static str,
430 pub signature: Option<String>,
432 pub sig_alg: Option<String>,
434}
435
436impl BindingContext {
437 pub fn post_form(&self) -> String {
443 crate::binding::saml_post_binding_form_with_signature(
444 &self.entity_endpoint,
445 self.request_type,
446 &self.context,
447 self.relay_state.as_deref(),
448 self.sig_alg.as_deref(),
449 self.signature.as_deref(),
450 )
451 }
452
453 pub fn try_post_form(&self) -> Result<String, crate::error::SamlError> {
461 crate::binding::try_saml_post_binding_form_with_signature(
462 &self.entity_endpoint,
463 self.request_type,
464 &self.context,
465 self.relay_state.as_deref(),
466 self.sig_alg.as_deref(),
467 self.signature.as_deref(),
468 )
469 }
470}