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 want_message_signed: bool,
55 pub(crate) want_encrypted_cbc_response_signed: bool,
57 pub want_authn_requests_signed: bool,
59 pub want_logout_request_signed: bool,
61 pub want_logout_response_signed: bool,
63 pub name_id_format: Vec<String>,
65 pub private_key: Option<String>,
67 pub private_key_pass: Option<String>,
69 pub signing_cert: Option<String>,
71 pub encrypt_cert: Option<String>,
73 pub enc_private_key: Option<String>,
75 pub enc_private_key_pass: Option<String>,
77 pub clock_drifts: (i64, i64),
79 pub redirect_inflate_max_bytes: usize,
85 pub xml_limits: XmlLimits,
87 pub tag_prefix_protocol: String,
89 pub tag_prefix_assertion: String,
91 pub tag_prefix_encrypted_assertion: String,
93 pub login_response_template: Option<crate::template::LoginResponseTemplate>,
95 pub login_request_template: Option<String>,
97 pub logout_request_template: Option<String>,
103 pub logout_response_template: Option<String>,
112 pub signature_config: Option<SignatureConfig>,
114 pub transformation_algorithms: Vec<String>,
117}
118
119fn redacted_option(value: &Option<String>) -> Option<&'static str> {
120 value.as_ref().map(|_| "<redacted>")
121}
122
123impl fmt::Debug for EntitySetting {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_struct("EntitySetting")
126 .field("entity_id", &self.entity_id)
127 .field(
128 "request_signature_algorithm",
129 &self.request_signature_algorithm,
130 )
131 .field("data_encryption_algorithm", &self.data_encryption_algorithm)
132 .field("key_encryption_algorithm", &self.key_encryption_algorithm)
133 .field("message_signing_order", &self.message_signing_order)
134 .field("allow_create", &self.allow_create)
135 .field("is_assertion_encrypted", &self.is_assertion_encrypted)
136 .field(
137 "allow_insecure_software_rsa_key_transport_decryption",
138 &self.allow_insecure_software_rsa_key_transport_decryption,
139 )
140 .field("relay_state", &self.relay_state)
141 .field("authn_requests_signed", &self.authn_requests_signed)
142 .field("want_assertions_signed", &self.want_assertions_signed)
143 .field("validate_audience", &self.validate_audience)
144 .field("want_message_signed", &self.want_message_signed)
145 .field(
146 "want_encrypted_cbc_response_signed",
147 &self.want_encrypted_cbc_response_signed,
148 )
149 .field(
150 "want_authn_requests_signed",
151 &self.want_authn_requests_signed,
152 )
153 .field(
154 "want_logout_request_signed",
155 &self.want_logout_request_signed,
156 )
157 .field(
158 "want_logout_response_signed",
159 &self.want_logout_response_signed,
160 )
161 .field("name_id_format", &self.name_id_format)
162 .field("private_key", &redacted_option(&self.private_key))
163 .field("private_key_pass", &redacted_option(&self.private_key_pass))
164 .field("signing_cert", &redacted_option(&self.signing_cert))
165 .field("encrypt_cert", &redacted_option(&self.encrypt_cert))
166 .field("enc_private_key", &redacted_option(&self.enc_private_key))
167 .field(
168 "enc_private_key_pass",
169 &redacted_option(&self.enc_private_key_pass),
170 )
171 .field("clock_drifts", &self.clock_drifts)
172 .field(
173 "redirect_inflate_max_bytes",
174 &self.redirect_inflate_max_bytes,
175 )
176 .field("xml_limits", &self.xml_limits)
177 .field("tag_prefix_protocol", &self.tag_prefix_protocol)
178 .field("tag_prefix_assertion", &self.tag_prefix_assertion)
179 .field(
180 "tag_prefix_encrypted_assertion",
181 &self.tag_prefix_encrypted_assertion,
182 )
183 .field("login_response_template", &self.login_response_template)
184 .field("login_request_template", &self.login_request_template)
185 .field("logout_request_template", &self.logout_request_template)
186 .field("logout_response_template", &self.logout_response_template)
187 .field("signature_config", &self.signature_config)
188 .field("transformation_algorithms", &self.transformation_algorithms)
189 .finish()
190 }
191}
192
193pub type CustomTagReplacement<'a> = &'a dyn Fn(&str) -> (String, String);
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
199pub enum SignatureAction {
200 #[default]
202 After,
203 Before,
205 Prepend,
207 Append,
209}
210
211#[derive(Debug, Clone)]
213pub struct SignatureConfig {
214 pub prefix: String,
216 pub reference: Option<String>,
219 pub action: SignatureAction,
221}
222
223impl Default for SignatureConfig {
224 fn default() -> Self {
225 Self {
226 prefix: "ds".to_string(),
227 reference: None,
228 action: SignatureAction::After,
229 }
230 }
231}
232
233impl Default for EntitySetting {
234 fn default() -> Self {
235 Self {
236 entity_id: None,
237 request_signature_algorithm: signature_algorithm::RSA_SHA256.to_string(),
238 data_encryption_algorithm: data_encryption_algorithm::AES_256.to_string(),
239 key_encryption_algorithm: key_encryption_algorithm::RSA_OAEP_MGF1P.to_string(),
240 message_signing_order: MessageSignatureOrder::SignThenEncrypt,
241 allow_create: false,
242 is_assertion_encrypted: false,
243 allow_insecure_software_rsa_key_transport_decryption: false,
244 relay_state: String::new(),
245 authn_requests_signed: false,
246 want_assertions_signed: false,
247 validate_audience: true,
248 want_message_signed: false,
249 want_encrypted_cbc_response_signed: false,
250 want_authn_requests_signed: false,
251 want_logout_request_signed: true,
252 want_logout_response_signed: true,
253 name_id_format: Vec::new(),
254 private_key: None,
255 private_key_pass: None,
256 signing_cert: None,
257 encrypt_cert: None,
258 enc_private_key: None,
259 enc_private_key_pass: None,
260 clock_drifts: (0, 0),
261 redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
262 xml_limits: XmlLimits::default(),
263 tag_prefix_protocol: "samlp".to_string(),
264 tag_prefix_assertion: "saml".to_string(),
265 tag_prefix_encrypted_assertion: "saml".to_string(),
266 login_response_template: None,
267 login_request_template: None,
268 logout_request_template: None,
269 logout_response_template: None,
270 signature_config: None,
271 transformation_algorithms: vec![
272 transform_algorithm::ENVELOPED_SIGNATURE.to_string(),
273 transform_algorithm::EXC_C14N.to_string(),
274 ],
275 }
276 }
277}
278
279pub fn generate_id() -> String {
281 format!("_{}", uuid::Uuid::new_v4())
282}
283
284#[derive(Debug, Clone, Default)]
286pub struct User {
287 pub name_id: String,
289 pub attributes: Vec<(String, String)>,
292 pub session_index: Option<String>,
294}
295
296impl User {
297 pub fn new(name_id: impl Into<String>) -> Self {
299 Self {
300 name_id: name_id.into(),
301 ..Default::default()
302 }
303 }
304}
305
306pub fn now_iso8601() -> String {
308 iso8601_offset(0)
309}
310
311pub fn iso8601_offset(seconds: i64) -> String {
313 let t = time::OffsetDateTime::now_utc() + time::Duration::seconds(seconds);
314 format_saml_utc_date_time(t)
315}
316
317#[derive(Debug, Clone)]
318pub(crate) struct IdpIssuanceWindow {
319 pub(crate) issue_instant: String,
320 pub(crate) expiration: String,
321}
322
323pub(crate) fn capture_idp_issuance_window(
324 lifetime: time::Duration,
325) -> Result<IdpIssuanceWindow, SamlError> {
326 let issue_instant = time::OffsetDateTime::now_utc();
327 let expiration = issue_instant
328 .checked_add(lifetime)
329 .ok_or(SamlError::TimeWindowInvalid {
330 field: TimeWindowField::IdpIssuanceExpiration,
331 })?;
332 let preserve_subseconds = lifetime.subsec_nanoseconds() != 0;
333 Ok(IdpIssuanceWindow {
334 issue_instant: format_idp_issuance_instant(issue_instant, preserve_subseconds),
335 expiration: format_idp_issuance_instant(expiration, preserve_subseconds),
336 })
337}
338
339fn format_idp_issuance_instant(t: time::OffsetDateTime, preserve_subseconds: bool) -> String {
340 if preserve_subseconds {
341 format!(
342 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}Z",
343 t.year(),
344 u8::from(t.month()),
345 t.day(),
346 t.hour(),
347 t.minute(),
348 t.second(),
349 t.nanosecond(),
350 )
351 } else {
352 format_saml_utc_date_time(t)
353 }
354}
355
356pub(crate) fn format_saml_utc_date_time(t: time::OffsetDateTime) -> String {
359 format!(
360 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
361 t.year(),
362 u8::from(t.month()),
363 t.day(),
364 t.hour(),
365 t.minute(),
366 t.second(),
367 )
368}
369
370#[cfg(test)]
371mod tests {
372 use super::format_saml_utc_date_time;
373
374 #[test]
375 fn outbound_saml_instants_do_not_generate_leap_seconds(
376 ) -> Result<(), Box<dyn std::error::Error>> {
377 let last_second = time::OffsetDateTime::from_unix_timestamp(1_483_228_799)?;
379
380 assert_eq!(
381 format_saml_utc_date_time(last_second),
382 "2016-12-31T23:59:59Z"
383 );
384 Ok(())
385 }
386}
387
388#[derive(Debug, Clone)]
390pub struct BindingContext {
391 pub id: String,
393 pub context: String,
395 pub relay_state: Option<String>,
397 pub entity_endpoint: String,
399 pub binding: crate::constants::Binding,
401 pub request_type: &'static str,
403 pub signature: Option<String>,
405 pub sig_alg: Option<String>,
407}
408
409impl BindingContext {
410 pub fn post_form(&self) -> String {
416 crate::binding::saml_post_binding_form_with_signature(
417 &self.entity_endpoint,
418 self.request_type,
419 &self.context,
420 self.relay_state.as_deref(),
421 self.sig_alg.as_deref(),
422 self.signature.as_deref(),
423 )
424 }
425
426 pub fn try_post_form(&self) -> Result<String, crate::error::SamlError> {
434 crate::binding::try_saml_post_binding_form_with_signature(
435 &self.entity_endpoint,
436 self.request_type,
437 &self.context,
438 self.relay_state.as_deref(),
439 self.sig_alg.as_deref(),
440 self.signature.as_deref(),
441 )
442 }
443}