saml_rs/config/policies.rs
1use crate::binding::MAX_DEFLATE_RAW_DECODE_BYTES;
2use crate::constants::MessageSignatureOrder;
3use crate::entity::SignatureConfig;
4use crate::error::SamlError;
5use crate::template::LoginResponseTemplate;
6use crate::xml::XmlLimits;
7
8use super::algorithms::{
9 DataEncryptionAlgorithm, KeyEncryptionAlgorithm, SignatureAlgorithm, TransformAlgorithm,
10};
11
12/// Whether an accepting service provider requires a signature on the Assertion
13/// itself.
14///
15/// [`Self::RequireSigned`] is library hardening and stays off unless the caller
16/// sets it. A response signed only on the `<Response>` remains acceptable
17/// until then. The accept combination is classified in
18/// `docs/conformance/web-browser-sso-acceptance.md`.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20pub enum AssertionSignaturePolicy {
21 /// Reject an assertion that is not directly signed.
22 ///
23 /// Library hardening for the accepting service provider.
24 RequireSigned,
25 /// Do not require a signature directly on the Assertion.
26 ///
27 /// HTTP POST still requires the assertion to be protected by an Assertion
28 /// signature or a Response signature.
29 #[default]
30 AllowUnsignedForCompatibility,
31}
32
33/// Whether an accepting service provider requires a signature on the
34/// `<Response>`.
35///
36/// [`Self::RequireForEncryptedCbc`] follows the recommendation for a
37/// CBC-encrypted assertion. [`Self::AllowUnsignedEncryptedCbc`] relaxes that
38/// recommendation alone and is not the legacy permissive preset.
39/// [`Self::RequireSigned`] requires a signature on every response and is
40/// library hardening. The accept combination is classified in
41/// `docs/conformance/web-browser-sso-acceptance.md`.
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum ResponseSignaturePolicy {
44 /// Accept a CBC `EncryptedAssertion` without a Response signature.
45 #[default]
46 AllowUnsignedEncryptedCbc,
47 /// Require a signed Response when an `<EncryptedAssertion>` uses CBC.
48 ///
49 /// Plaintext responses are unchanged.
50 RequireForEncryptedCbc,
51 /// Require a Response signature on every response.
52 ///
53 /// Library hardening. Also rejects a response signed only on the Assertion.
54 RequireSigned,
55}
56
57/// Inbound embedded XML-signature algorithm profile for accepted SSO and
58/// Single Logout messages.
59///
60/// [`Self::StrictRsaSha2`] is library hardening. Selecting it does not require
61/// a signature directly on the Assertion. The accept combination is classified
62/// in `docs/conformance/web-browser-sso-acceptance.md`.
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
64pub enum XmlSignatureProfile {
65 /// Require one local `#id` reference, RSA-SHA256/384/512, a SHA-256/384/512
66 /// digest, and exclusive canonicalization. Each transform element, when
67 /// present, must belong to the XML-DSig namespace and use enveloped-signature
68 /// or exclusive canonicalization.
69 StrictRsaSha2,
70 /// Accept every algorithm and same-document reference shape supported by
71 /// the selected cryptographic provider.
72 #[default]
73 AllowProviderSupportedForCompatibility,
74}
75
76/// Whether an SP signs outgoing AuthnRequests.
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub enum AuthnRequestSigningPolicy {
79 /// Sign outgoing AuthnRequests.
80 Sign,
81 /// Send unsigned AuthnRequests, preserving the samlify port.
82 #[default]
83 DoNotSignForCompatibility,
84}
85
86/// Whether an identity provider requires a signed inbound `<AuthnRequest>`.
87///
88/// [`Self::RequireSigned`] is optional and stays off.
89/// [`Self::AllowUnsignedVerifyIfPresent`] verifies a signature that is present
90/// and still accepts an unsigned request.
91/// [`Self::AllowUnsignedForCompatibility`] does not verify a present signature.
92/// The accept combination is classified in
93/// `docs/conformance/web-browser-sso-acceptance.md`.
94#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
95pub enum AuthnRequestValidationPolicy {
96 /// Reject unsigned AuthnRequests.
97 RequireSigned,
98 /// Accept an unsigned AuthnRequest and verify a signature that is present.
99 AllowUnsignedVerifyIfPresent,
100 /// Accept unsigned AuthnRequests without verifying a signature that is present.
101 #[default]
102 AllowUnsignedForCompatibility,
103}
104
105/// Whether logout messages require signatures.
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
107pub enum LogoutSignaturePolicy {
108 /// Reject unsigned logout messages.
109 #[default]
110 RequireSigned,
111 /// Accept unsigned logout messages, preserving the samlify port.
112 AllowUnsignedForCompatibility,
113}
114
115/// Whether an accepting service provider evaluates `<AudienceRestriction>`.
116///
117/// [`Self::EvaluatePresentRestrictions`] checks restrictions that are present.
118/// [`Self::Validate`] also rejects a bearer assertion that omits one.
119/// [`Self::SkipForCompatibility`] skips the check. The accept combination is
120/// classified in `docs/conformance/web-browser-sso-acceptance.md`.
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
122pub enum AudienceValidationPolicy {
123 /// Evaluate audience restrictions and reject a bearer assertion that omits one.
124 ///
125 /// Rejecting a missing restriction is library hardening. `strict()` keeps
126 /// this variant.
127 #[default]
128 Validate,
129 /// Evaluate `<AudienceRestriction>` elements that are present.
130 ///
131 /// A bearer assertion that omits the element is not rejected.
132 EvaluatePresentRestrictions,
133 /// Skip audience evaluation, preserving the samlify port.
134 SkipForCompatibility,
135}
136
137/// Whether SP AuthnRequests allow IdPs to create a new identifier.
138#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
139pub enum NameIdCreationPolicy {
140 /// Set `AllowCreate="true"` in AuthnRequests.
141 AllowCreate,
142 /// Set `AllowCreate="false"` in AuthnRequests.
143 #[default]
144 DoNotAllowCreate,
145}
146
147/// SP-side validation and outbound signing policy.
148///
149/// [`Self::recommended`] is the preset for claimed Web Browser SSO and Single
150/// Logout features. [`Self::compatibility`] is the legacy permissive preset.
151/// `strict()` is deprecated and keeps its current bundle. Field classification
152/// is in `docs/conformance/`.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct SpValidationPolicy {
155 /// Assertion signature requirement.
156 pub assertions: AssertionSignaturePolicy,
157 /// Top-level SAML Response signature requirement.
158 pub responses: ResponseSignaturePolicy,
159 /// Embedded XML-signature algorithm and reference requirements.
160 pub xml_signatures: XmlSignatureProfile,
161 /// Outbound AuthnRequest signing behavior.
162 pub authn_requests: AuthnRequestSigningPolicy,
163 /// Audience validation behavior.
164 pub audience: AudienceValidationPolicy,
165 /// NameID creation behavior for AuthnRequests.
166 pub name_id_creation: NameIdCreationPolicy,
167 /// Logout signature validation behavior.
168 pub logout: LogoutPolicy,
169}
170
171impl SpValidationPolicy {
172 /// Validation preset for claimed Web Browser SSO and Single Logout features.
173 ///
174 /// Recommendations start enabled. Optional capabilities and library
175 /// hardening stay off until a caller replaces one field.
176 ///
177 /// # Examples
178 ///
179 /// ```
180 /// use saml_rs::{AssertionSignaturePolicy, SpValidationPolicy, XmlSignatureProfile};
181 ///
182 /// let with_assertion_signature = SpValidationPolicy {
183 /// assertions: AssertionSignaturePolicy::RequireSigned,
184 /// ..SpValidationPolicy::recommended()
185 /// };
186 /// assert_eq!(
187 /// with_assertion_signature.xml_signatures,
188 /// XmlSignatureProfile::AllowProviderSupportedForCompatibility
189 /// );
190 /// ```
191 pub fn recommended() -> Self {
192 Self {
193 assertions: AssertionSignaturePolicy::AllowUnsignedForCompatibility,
194 responses: ResponseSignaturePolicy::RequireForEncryptedCbc,
195 xml_signatures: XmlSignatureProfile::AllowProviderSupportedForCompatibility,
196 authn_requests: AuthnRequestSigningPolicy::DoNotSignForCompatibility,
197 audience: AudienceValidationPolicy::EvaluatePresentRestrictions,
198 name_id_creation: NameIdCreationPolicy::DoNotAllowCreate,
199 logout: LogoutPolicy::recommended(),
200 }
201 }
202
203 /// Deprecated bundle of SP validation choices.
204 ///
205 /// The returned fields are unchanged from earlier releases.
206 #[deprecated(note = "start from recommended() and add only the named hardenings you want")]
207 #[expect(
208 deprecated,
209 reason = "the deprecated strict preset keeps its current logout bundle"
210 )]
211 pub fn strict() -> Self {
212 Self {
213 assertions: AssertionSignaturePolicy::RequireSigned,
214 responses: ResponseSignaturePolicy::RequireForEncryptedCbc,
215 xml_signatures: XmlSignatureProfile::AllowProviderSupportedForCompatibility,
216 authn_requests: AuthnRequestSigningPolicy::Sign,
217 audience: AudienceValidationPolicy::Validate,
218 name_id_creation: NameIdCreationPolicy::DoNotAllowCreate,
219 logout: LogoutPolicy::strict(),
220 }
221 }
222
223 /// Legacy permissive preset: samlify-port behavior for a caller leaving
224 /// the raw API.
225 pub fn compatibility() -> Self {
226 Self {
227 assertions: AssertionSignaturePolicy::AllowUnsignedForCompatibility,
228 responses: ResponseSignaturePolicy::AllowUnsignedEncryptedCbc,
229 xml_signatures: XmlSignatureProfile::AllowProviderSupportedForCompatibility,
230 authn_requests: AuthnRequestSigningPolicy::DoNotSignForCompatibility,
231 audience: AudienceValidationPolicy::SkipForCompatibility,
232 name_id_creation: NameIdCreationPolicy::DoNotAllowCreate,
233 logout: LogoutPolicy::compatibility(),
234 }
235 }
236}
237
238impl Default for SpValidationPolicy {
239 fn default() -> Self {
240 Self::compatibility()
241 }
242}
243
244/// IdP-side validation policy.
245///
246/// [`Self::recommended`] is the preset for claimed Web Browser SSO and Single
247/// Logout features. [`Self::compatibility`] is the legacy permissive preset.
248/// `strict()` is deprecated and keeps its current bundle. Field classification
249/// is in `docs/conformance/`.
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct IdpValidationPolicy {
252 /// Inbound AuthnRequest signature requirement.
253 pub authn_requests: AuthnRequestValidationPolicy,
254 /// Logout signature validation behavior.
255 pub logout: LogoutPolicy,
256}
257
258impl IdpValidationPolicy {
259 /// Validation preset for claimed Web Browser SSO and Single Logout features.
260 ///
261 /// An inbound `AuthnRequest` may be unsigned. A signature that is present
262 /// is verified. Logout requests and responses require a signature.
263 pub fn recommended() -> Self {
264 Self {
265 authn_requests: AuthnRequestValidationPolicy::AllowUnsignedVerifyIfPresent,
266 logout: LogoutPolicy::recommended(),
267 }
268 }
269
270 /// Deprecated bundle of IdP validation choices.
271 ///
272 /// Inbound `AuthnRequest` and logout messages must be signed. The returned
273 /// fields are unchanged from earlier releases.
274 #[deprecated(note = "start from recommended() and add only the named hardenings you want")]
275 #[expect(
276 deprecated,
277 reason = "the deprecated strict preset keeps its current logout bundle"
278 )]
279 pub fn strict() -> Self {
280 Self {
281 authn_requests: AuthnRequestValidationPolicy::RequireSigned,
282 logout: LogoutPolicy::strict(),
283 }
284 }
285
286 /// Legacy permissive preset.
287 ///
288 /// Samlify-port behavior for a caller leaving the raw API. A signature
289 /// present on an `AuthnRequest` is not verified.
290 pub fn compatibility() -> Self {
291 Self {
292 authn_requests: AuthnRequestValidationPolicy::AllowUnsignedForCompatibility,
293 logout: LogoutPolicy::compatibility(),
294 }
295 }
296}
297
298impl Default for IdpValidationPolicy {
299 fn default() -> Self {
300 Self::compatibility()
301 }
302}
303
304/// Logout request and response signature policy.
305///
306/// [`Self::recommended`] requires a signature on requests and responses.
307/// [`Self::compatibility`] is the legacy permissive preset. `strict()` is
308/// deprecated and unchanged.
309///
310/// `finish_slo` reads `responses`. `receive_slo` reads `requests`. With
311/// [`crate::LogoutSigning::FollowLocalPolicy`], `requests` also decides
312/// whether an outbound `LogoutRequest` is signed. See
313/// `docs/conformance/single-logout.md`.
314#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
315pub struct LogoutPolicy {
316 /// LogoutRequest signature behavior.
317 pub requests: LogoutSignaturePolicy,
318 /// LogoutResponse signature behavior.
319 pub responses: LogoutSignaturePolicy,
320}
321
322impl LogoutPolicy {
323 /// Require a signature on logout requests and responses.
324 pub fn recommended() -> Self {
325 Self {
326 requests: LogoutSignaturePolicy::RequireSigned,
327 responses: LogoutSignaturePolicy::RequireSigned,
328 }
329 }
330
331 /// Deprecated logout signature bundle.
332 ///
333 /// Logout requests and responses must still be signed.
334 #[deprecated(note = "start from recommended() and add only the named hardenings you want")]
335 pub fn strict() -> Self {
336 Self {
337 requests: LogoutSignaturePolicy::RequireSigned,
338 responses: LogoutSignaturePolicy::RequireSigned,
339 }
340 }
341
342 /// Legacy permissive preset.
343 ///
344 /// Unsigned logout requests and responses are accepted. This is the
345 /// samlify-port hatch.
346 pub fn compatibility() -> Self {
347 Self {
348 requests: LogoutSignaturePolicy::AllowUnsignedForCompatibility,
349 responses: LogoutSignaturePolicy::AllowUnsignedForCompatibility,
350 }
351 }
352}
353
354/// Whether assertions are encrypted in generated responses.
355#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
356pub enum AssertionEncryptionPolicy {
357 /// Do not encrypt assertions.
358 #[default]
359 PlaintextAssertions,
360 /// Encrypt assertions.
361 EncryptAssertions,
362}
363
364/// XML encryption policy.
365///
366/// # Examples
367///
368/// Use typed configuration to request encrypted assertions in generated
369/// responses. This only configures policy; actual encryption uses the crate's
370/// XML-Enc backend and deployment credentials.
371///
372/// ```
373/// use saml_rs::{EntityId, IdpConfig, SsoEndpoint, XmlEncryptionPolicy, XmlPolicy};
374///
375/// let xml = XmlPolicy {
376/// encryption: XmlEncryptionPolicy::encrypt_assertions(),
377/// ..XmlPolicy::default()
378/// };
379/// let idp_builder = IdpConfig::builder(EntityId::try_new("https://idp.example.com/metadata")?)
380/// .sso_endpoint(SsoEndpoint::post("https://idp.example.com/sso")?)
381/// .xml(xml);
382/// # let _ = idp_builder;
383/// # Ok::<(), saml_rs::SamlError>(())
384/// ```
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
386pub struct XmlEncryptionPolicy {
387 /// Assertion encryption behavior.
388 pub assertions: AssertionEncryptionPolicy,
389 allow_insecure_software_rsa_key_transport_decryption: bool,
390}
391
392impl XmlEncryptionPolicy {
393 /// Enable assertion encryption.
394 pub fn encrypt_assertions() -> Self {
395 Self {
396 assertions: AssertionEncryptionPolicy::EncryptAssertions,
397 ..Self::default()
398 }
399 }
400
401 /// Explicitly allow RustCrypto software RSA key-transport decryption despite
402 /// `RUSTSEC-2023-0071` timing-risk concerns in that backend.
403 pub fn allow_insecure_software_rsa_key_transport_decryption() -> Self {
404 Self {
405 allow_insecure_software_rsa_key_transport_decryption: true,
406 ..Self::default()
407 }
408 }
409
410 /// Return a copy with the software RSA key-transport risk explicitly allowed.
411 pub fn with_insecure_software_rsa_key_transport_decryption_allowed(mut self) -> Self {
412 self.allow_insecure_software_rsa_key_transport_decryption = true;
413 self
414 }
415
416 pub(super) fn allows_insecure_software_rsa_key_transport_decryption(self) -> bool {
417 self.allow_insecure_software_rsa_key_transport_decryption
418 }
419}
420
421/// XML parser, redirect decompression, clock, and XML encryption policy.
422///
423/// # Examples
424///
425/// Software RSA key-transport decryption is disabled by default on the
426/// RustCrypto provider because that backend, reached through `bergshamra` /
427/// `kryptering`, is affected by `RUSTSEC-2023-0071`. Enable it only as an
428/// explicit RustCrypto exception for a deployment that accepts
429/// that risk. AWS-LC and FIPS ignore this opt-in.
430///
431/// ```
432/// use saml_rs::{AcsEndpoint, EntityId, SpConfig, XmlEncryptionPolicy, XmlPolicy};
433///
434/// let xml = XmlPolicy {
435/// encryption: XmlEncryptionPolicy::default()
436/// .with_insecure_software_rsa_key_transport_decryption_allowed(),
437/// ..XmlPolicy::default()
438/// };
439/// let sp_builder = SpConfig::builder(EntityId::try_new("https://sp.example.com/metadata")?)
440/// .acs_endpoint(AcsEndpoint::post("https://sp.example.com/acs")?)
441/// .xml(xml);
442/// # let _ = sp_builder;
443/// # Ok::<(), saml_rs::SamlError>(())
444/// ```
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub struct XmlPolicy {
447 /// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
448 pub clock_drifts: (i64, i64),
449 /// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
450 /// HTTP-Redirect input.
451 pub redirect_inflate_max_bytes: usize,
452 /// XML parser resource limits.
453 pub limits: XmlLimits,
454 /// XML encryption behavior.
455 pub encryption: XmlEncryptionPolicy,
456}
457
458impl Default for XmlPolicy {
459 fn default() -> Self {
460 Self {
461 clock_drifts: (0, 0),
462 redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
463 limits: XmlLimits::default(),
464 encryption: XmlEncryptionPolicy::default(),
465 }
466 }
467}
468
469/// Algorithm choices used by outgoing SAML messages.
470#[derive(Debug, Clone, PartialEq, Eq)]
471pub struct AlgorithmPolicy {
472 /// Signature algorithm URI.
473 pub signature: SignatureAlgorithm,
474 /// Data encryption algorithm URI.
475 pub data_encryption: DataEncryptionAlgorithm,
476 /// Key encryption algorithm URI.
477 pub key_encryption: KeyEncryptionAlgorithm,
478 /// Sign/encrypt operation order for messages that do both.
479 pub message_signing_order: MessageSignatureOrder,
480 /// XML-DSig reference transforms.
481 pub signed_reference_transforms: Vec<TransformAlgorithm>,
482}
483
484impl Default for AlgorithmPolicy {
485 fn default() -> Self {
486 Self {
487 signature: SignatureAlgorithm::default(),
488 data_encryption: DataEncryptionAlgorithm::default(),
489 key_encryption: KeyEncryptionAlgorithm::default(),
490 message_signing_order: MessageSignatureOrder::SignThenEncrypt,
491 signed_reference_transforms: vec![
492 TransformAlgorithm::EnvelopedSignature,
493 TransformAlgorithm::ExclusiveCanonicalization,
494 ],
495 }
496 }
497}
498
499/// Template and XML tag-prefix customization.
500#[derive(Debug, Clone)]
501pub struct TemplatePolicy {
502 /// Default RelayState.
503 pub relay_state: String,
504 /// IdP protocol tag prefix for generated messages.
505 pub tag_prefix_protocol: String,
506 /// IdP assertion tag prefix for generated messages.
507 pub tag_prefix_assertion: String,
508 /// IdP tag prefix for generated `<EncryptedAssertion>` elements.
509 pub tag_prefix_encrypted_assertion: String,
510 /// IdP login response template and attributes.
511 pub login_response_template: Option<LoginResponseTemplate>,
512 /// SP login request template.
513 pub login_request_template: Option<String>,
514 /// Logout request template.
515 ///
516 /// Typed Session Authority generation requires a complete unqualified
517 /// `NotOnOrAfter="{NotOnOrAfter}"` root attribute. Typed SP and raw
518 /// samlify-port generation do not synthesize the attribute.
519 pub logout_request_template: Option<String>,
520 /// Logout response template.
521 ///
522 /// After prefix and placeholder substitution, the final outbound XML must
523 /// satisfy the enforced LogoutResponse structure, issuer, destination, and
524 /// request-correlation requirements. A root `<ds:Signature>` is rejected
525 /// before signing so the library owns signature construction. When
526 /// `InResponseTo` is `None`, an attribute whose complete value is the
527 /// `{InResponseTo}` placeholder is omitted.
528 pub logout_response_template: Option<String>,
529 /// Embedded-signature placement and prefix.
530 pub signature_config: Option<SignatureConfig>,
531}
532
533impl Default for TemplatePolicy {
534 fn default() -> Self {
535 Self {
536 relay_state: String::new(),
537 tag_prefix_protocol: "samlp".to_string(),
538 tag_prefix_assertion: "saml".to_string(),
539 tag_prefix_encrypted_assertion: "saml".to_string(),
540 login_response_template: None,
541 login_request_template: None,
542 logout_request_template: None,
543 logout_response_template: None,
544 signature_config: None,
545 }
546 }
547}
548pub(super) fn authn_request_signing_enabled(policy: AuthnRequestSigningPolicy) -> bool {
549 matches!(policy, AuthnRequestSigningPolicy::Sign)
550}
551
552pub(super) fn authn_request_signature_required(policy: AuthnRequestValidationPolicy) -> bool {
553 matches!(policy, AuthnRequestValidationPolicy::RequireSigned)
554}
555
556pub(super) fn verify_present_authn_request_signature(policy: AuthnRequestValidationPolicy) -> bool {
557 matches!(
558 policy,
559 AuthnRequestValidationPolicy::AllowUnsignedVerifyIfPresent
560 )
561}
562
563pub(super) fn assertion_signature_required(policy: AssertionSignaturePolicy) -> bool {
564 matches!(policy, AssertionSignaturePolicy::RequireSigned)
565}
566
567pub(super) fn response_signature_required(policy: ResponseSignaturePolicy) -> bool {
568 matches!(policy, ResponseSignaturePolicy::RequireSigned)
569}
570
571pub(super) fn encrypted_cbc_response_signature_required(policy: ResponseSignaturePolicy) -> bool {
572 matches!(policy, ResponseSignaturePolicy::RequireForEncryptedCbc)
573}
574
575pub(super) fn logout_signature_required(policy: LogoutSignaturePolicy) -> Result<bool, SamlError> {
576 match policy {
577 LogoutSignaturePolicy::RequireSigned => Ok(true),
578 LogoutSignaturePolicy::AllowUnsignedForCompatibility => Ok(false),
579 }
580}
581pub(super) fn name_id_creation_allowed(policy: NameIdCreationPolicy) -> bool {
582 matches!(policy, NameIdCreationPolicy::AllowCreate)
583}
584
585pub(super) fn audience_validation_enabled(policy: AudienceValidationPolicy) -> bool {
586 matches!(
587 policy,
588 AudienceValidationPolicy::Validate | AudienceValidationPolicy::EvaluatePresentRestrictions
589 )
590}
591
592pub(super) fn audience_restriction_required(policy: AudienceValidationPolicy) -> bool {
593 matches!(policy, AudienceValidationPolicy::Validate)
594}