Skip to main content

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 SPs require assertion-level signatures.
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub enum AssertionSignaturePolicy {
15    /// Reject unsigned assertions.
16    RequireSigned,
17    /// Accept unsigned assertions for legacy interoperability.
18    #[default]
19    AllowUnsignedForCompatibility,
20}
21
22/// Whether SPs require trusted authentication of the SAML Response.
23///
24/// The Web Browser SSO profile permits HTTP-POST assertions to be protected by
25/// signing either each Assertion or the enclosing Response. These variants are
26/// library policy choices layered on top of that profile rule. When Response
27/// authentication is required, HTTP-POST requires trusted XML-DSig coverage of
28/// the Response root; supported detached bindings use their binding-defined
29/// message signatures.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub enum ResponseSignaturePolicy {
32    /// Allow CBC-encrypted assertions without outer integrity protection for
33    /// legacy interoperability.
34    ///
35    /// This explicitly relaxes the recommendation in SAML V2.0 Approved
36    /// Errata 05 E93.
37    #[default]
38    AllowUnsignedEncryptedCbcForCompatibility,
39    /// Require Response authentication when an `<EncryptedAssertion>` uses a
40    /// known CBC-mode data-encryption algorithm.
41    RequireForEncryptedCbc,
42    /// Require trusted Response authentication through root-covering XML-DSig
43    /// or a supported binding-defined detached signature.
44    RequireSigned,
45}
46
47/// Inbound embedded XML-signature algorithm and reference profile.
48///
49/// The selected profile applies to every embedded signature that authenticates
50/// an accepted SSO or Single Logout message. [`Self::StrictRsaSha2`] is an
51/// explicit library policy. SAML does not require a receiver to reject other
52/// provider-supported algorithms, and [`SpValidationPolicy::strict`] keeps the
53/// provider-supported profile.
54#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
55pub enum XmlSignatureProfile {
56    /// Require one local `#id` reference, RSA-SHA256/384/512, a SHA-256/384/512
57    /// digest, and exclusive canonicalization. Each transform element, when
58    /// present, must belong to the XML-DSig namespace and use enveloped-signature
59    /// or exclusive canonicalization.
60    StrictRsaSha2,
61    /// Accept every algorithm and same-document reference shape supported by
62    /// the selected cryptographic provider.
63    #[default]
64    AllowProviderSupportedForCompatibility,
65}
66
67/// Whether an SP signs outgoing AuthnRequests.
68#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
69pub enum AuthnRequestSigningPolicy {
70    /// Sign outgoing AuthnRequests.
71    Sign,
72    /// Send unsigned AuthnRequests for legacy interoperability.
73    #[default]
74    DoNotSignForCompatibility,
75}
76
77/// Whether an IdP requires signed inbound AuthnRequests.
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub enum AuthnRequestValidationPolicy {
80    /// Reject unsigned AuthnRequests.
81    RequireSigned,
82    /// Accept unsigned AuthnRequests for legacy interoperability.
83    #[default]
84    AllowUnsignedForCompatibility,
85}
86
87/// Whether logout messages require signatures.
88#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
89pub enum LogoutSignaturePolicy {
90    /// Reject unsigned logout messages.
91    #[default]
92    RequireSigned,
93    /// Accept unsigned logout messages for legacy interoperability.
94    AllowUnsignedForCompatibility,
95}
96
97/// Whether an SP validates assertion audience restrictions.
98#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
99pub enum AudienceValidationPolicy {
100    /// Require this SP's entity ID in assertion audiences.
101    #[default]
102    Validate,
103    /// Skip audience validation for legacy interoperability.
104    SkipForCompatibility,
105}
106
107/// Whether SP AuthnRequests allow IdPs to create a new identifier.
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
109pub enum NameIdCreationPolicy {
110    /// Set `AllowCreate="true"` in AuthnRequests.
111    AllowCreate,
112    /// Set `AllowCreate="false"` in AuthnRequests.
113    #[default]
114    DoNotAllowCreate,
115}
116
117/// SP-side validation and outbound signing policy.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct SpValidationPolicy {
120    /// Assertion signature requirement.
121    pub assertions: AssertionSignaturePolicy,
122    /// Top-level SAML Response signature requirement.
123    pub responses: ResponseSignaturePolicy,
124    /// Embedded XML-signature algorithm and reference requirements.
125    pub xml_signatures: XmlSignatureProfile,
126    /// Outbound AuthnRequest signing behavior.
127    pub authn_requests: AuthnRequestSigningPolicy,
128    /// Audience validation behavior.
129    pub audience: AudienceValidationPolicy,
130    /// NameID creation behavior for AuthnRequests.
131    pub name_id_creation: NameIdCreationPolicy,
132    /// Logout signature validation behavior.
133    pub logout: LogoutPolicy,
134}
135
136impl SpValidationPolicy {
137    /// Strict SP validation and outbound signing defaults.
138    pub fn strict() -> Self {
139        Self {
140            assertions: AssertionSignaturePolicy::RequireSigned,
141            responses: ResponseSignaturePolicy::RequireForEncryptedCbc,
142            xml_signatures: XmlSignatureProfile::AllowProviderSupportedForCompatibility,
143            authn_requests: AuthnRequestSigningPolicy::Sign,
144            audience: AudienceValidationPolicy::Validate,
145            name_id_creation: NameIdCreationPolicy::DoNotAllowCreate,
146            logout: LogoutPolicy::strict(),
147        }
148    }
149
150    /// Legacy interoperability policy with unsigned behavior made explicit.
151    pub fn compatibility() -> Self {
152        Self {
153            assertions: AssertionSignaturePolicy::AllowUnsignedForCompatibility,
154            responses: ResponseSignaturePolicy::AllowUnsignedEncryptedCbcForCompatibility,
155            xml_signatures: XmlSignatureProfile::AllowProviderSupportedForCompatibility,
156            authn_requests: AuthnRequestSigningPolicy::DoNotSignForCompatibility,
157            audience: AudienceValidationPolicy::SkipForCompatibility,
158            name_id_creation: NameIdCreationPolicy::DoNotAllowCreate,
159            logout: LogoutPolicy::compatibility(),
160        }
161    }
162}
163
164impl Default for SpValidationPolicy {
165    fn default() -> Self {
166        Self::compatibility()
167    }
168}
169
170/// IdP-side validation policy.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct IdpValidationPolicy {
173    /// Inbound AuthnRequest signature requirement.
174    pub authn_requests: AuthnRequestValidationPolicy,
175    /// Logout signature validation behavior.
176    pub logout: LogoutPolicy,
177}
178
179impl IdpValidationPolicy {
180    /// Strict IdP validation defaults.
181    pub fn strict() -> Self {
182        Self {
183            authn_requests: AuthnRequestValidationPolicy::RequireSigned,
184            logout: LogoutPolicy::strict(),
185        }
186    }
187
188    /// Legacy interoperability policy with unsigned behavior made explicit.
189    pub fn compatibility() -> Self {
190        Self {
191            authn_requests: AuthnRequestValidationPolicy::AllowUnsignedForCompatibility,
192            logout: LogoutPolicy::compatibility(),
193        }
194    }
195}
196
197impl Default for IdpValidationPolicy {
198    fn default() -> Self {
199        Self::compatibility()
200    }
201}
202
203/// Logout request and response signature policy.
204#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
205pub struct LogoutPolicy {
206    /// LogoutRequest signature behavior.
207    pub requests: LogoutSignaturePolicy,
208    /// LogoutResponse signature behavior.
209    pub responses: LogoutSignaturePolicy,
210}
211
212impl LogoutPolicy {
213    /// Require signed logout requests and responses.
214    pub fn strict() -> Self {
215        Self {
216            requests: LogoutSignaturePolicy::RequireSigned,
217            responses: LogoutSignaturePolicy::RequireSigned,
218        }
219    }
220
221    /// Accept unsigned logout requests and responses for legacy interoperability.
222    pub fn compatibility() -> Self {
223        Self {
224            requests: LogoutSignaturePolicy::AllowUnsignedForCompatibility,
225            responses: LogoutSignaturePolicy::AllowUnsignedForCompatibility,
226        }
227    }
228}
229
230/// Whether assertions are encrypted in generated responses.
231#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
232pub enum AssertionEncryptionPolicy {
233    /// Do not encrypt assertions.
234    #[default]
235    PlaintextAssertions,
236    /// Encrypt assertions.
237    EncryptAssertions,
238}
239
240/// XML encryption policy.
241///
242/// # Examples
243///
244/// Use typed configuration to request encrypted assertions in generated
245/// responses. This only configures policy; actual encryption uses the crate's
246/// XML-Enc backend and deployment credentials.
247///
248/// ```
249/// use saml_rs::{EntityId, IdpConfig, SsoEndpoint, XmlEncryptionPolicy, XmlPolicy};
250///
251/// let xml = XmlPolicy {
252///     encryption: XmlEncryptionPolicy::encrypt_assertions(),
253///     ..XmlPolicy::default()
254/// };
255/// let idp_builder = IdpConfig::builder(EntityId::try_new("https://idp.example.com/metadata")?)
256///     .sso_endpoint(SsoEndpoint::post("https://idp.example.com/sso")?)
257///     .xml(xml);
258/// # let _ = idp_builder;
259/// # Ok::<(), saml_rs::SamlError>(())
260/// ```
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
262pub struct XmlEncryptionPolicy {
263    /// Assertion encryption behavior.
264    pub assertions: AssertionEncryptionPolicy,
265    allow_insecure_software_rsa_key_transport_decryption: bool,
266}
267
268impl XmlEncryptionPolicy {
269    /// Enable assertion encryption.
270    pub fn encrypt_assertions() -> Self {
271        Self {
272            assertions: AssertionEncryptionPolicy::EncryptAssertions,
273            ..Self::default()
274        }
275    }
276
277    /// Explicitly allow RustCrypto software RSA key-transport decryption despite
278    /// `RUSTSEC-2023-0071` timing-risk concerns in that backend.
279    pub fn allow_insecure_software_rsa_key_transport_decryption() -> Self {
280        Self {
281            allow_insecure_software_rsa_key_transport_decryption: true,
282            ..Self::default()
283        }
284    }
285
286    /// Return a copy with the software RSA key-transport risk explicitly allowed.
287    pub fn with_insecure_software_rsa_key_transport_decryption_allowed(mut self) -> Self {
288        self.allow_insecure_software_rsa_key_transport_decryption = true;
289        self
290    }
291
292    pub(super) fn allows_insecure_software_rsa_key_transport_decryption(self) -> bool {
293        self.allow_insecure_software_rsa_key_transport_decryption
294    }
295}
296
297/// XML parser, redirect decompression, clock, and XML encryption policy.
298///
299/// # Examples
300///
301/// Software RSA key-transport decryption is disabled by default on the
302/// RustCrypto provider because that backend, reached through `bergshamra` /
303/// `kryptering`, is affected by `RUSTSEC-2023-0071`. Enable it only as an
304/// explicit compatibility exception for a RustCrypto deployment that accepts
305/// that risk. AWS-LC and FIPS ignore this opt-in.
306///
307/// ```
308/// use saml_rs::{AcsEndpoint, EntityId, SpConfig, XmlEncryptionPolicy, XmlPolicy};
309///
310/// let xml = XmlPolicy {
311///     encryption: XmlEncryptionPolicy::default()
312///         .with_insecure_software_rsa_key_transport_decryption_allowed(),
313///     ..XmlPolicy::default()
314/// };
315/// let sp_builder = SpConfig::builder(EntityId::try_new("https://sp.example.com/metadata")?)
316///     .acs_endpoint(AcsEndpoint::post("https://sp.example.com/acs")?)
317///     .xml(xml);
318/// # let _ = sp_builder;
319/// # Ok::<(), saml_rs::SamlError>(())
320/// ```
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub struct XmlPolicy {
323    /// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
324    pub clock_drifts: (i64, i64),
325    /// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
326    /// HTTP-Redirect input.
327    pub redirect_inflate_max_bytes: usize,
328    /// XML parser resource limits.
329    pub limits: XmlLimits,
330    /// XML encryption behavior.
331    pub encryption: XmlEncryptionPolicy,
332}
333
334impl Default for XmlPolicy {
335    fn default() -> Self {
336        Self {
337            clock_drifts: (0, 0),
338            redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
339            limits: XmlLimits::default(),
340            encryption: XmlEncryptionPolicy::default(),
341        }
342    }
343}
344
345/// Algorithm choices used by outgoing SAML messages.
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct AlgorithmPolicy {
348    /// Signature algorithm URI.
349    pub signature: SignatureAlgorithm,
350    /// Data encryption algorithm URI.
351    pub data_encryption: DataEncryptionAlgorithm,
352    /// Key encryption algorithm URI.
353    pub key_encryption: KeyEncryptionAlgorithm,
354    /// Sign/encrypt operation order for messages that do both.
355    pub message_signing_order: MessageSignatureOrder,
356    /// XML-DSig reference transforms.
357    pub signed_reference_transforms: Vec<TransformAlgorithm>,
358}
359
360impl Default for AlgorithmPolicy {
361    fn default() -> Self {
362        Self {
363            signature: SignatureAlgorithm::default(),
364            data_encryption: DataEncryptionAlgorithm::default(),
365            key_encryption: KeyEncryptionAlgorithm::default(),
366            message_signing_order: MessageSignatureOrder::SignThenEncrypt,
367            signed_reference_transforms: vec![
368                TransformAlgorithm::EnvelopedSignature,
369                TransformAlgorithm::ExclusiveCanonicalization,
370            ],
371        }
372    }
373}
374
375/// Template and XML tag-prefix customization.
376#[derive(Debug, Clone)]
377pub struct TemplatePolicy {
378    /// Default RelayState.
379    pub relay_state: String,
380    /// IdP protocol tag prefix for generated messages.
381    pub tag_prefix_protocol: String,
382    /// IdP assertion tag prefix for generated messages.
383    pub tag_prefix_assertion: String,
384    /// IdP tag prefix for generated `<EncryptedAssertion>` elements.
385    pub tag_prefix_encrypted_assertion: String,
386    /// IdP login response template and attributes.
387    pub login_response_template: Option<LoginResponseTemplate>,
388    /// SP login request template.
389    pub login_request_template: Option<String>,
390    /// Logout request template.
391    ///
392    /// Typed Session Authority generation requires a complete unqualified
393    /// `NotOnOrAfter="{NotOnOrAfter}"` root attribute. Typed SP and raw
394    /// compatibility generation do not synthesize the attribute.
395    pub logout_request_template: Option<String>,
396    /// Logout response template.
397    ///
398    /// After prefix and placeholder substitution, the final outbound XML must
399    /// satisfy the enforced LogoutResponse structure, issuer, destination, and
400    /// request-correlation requirements. A root `<ds:Signature>` is rejected
401    /// before signing so the library owns signature construction. When
402    /// `InResponseTo` is `None`, an attribute whose complete value is the
403    /// `{InResponseTo}` placeholder is omitted.
404    pub logout_response_template: Option<String>,
405    /// Embedded-signature placement and prefix.
406    pub signature_config: Option<SignatureConfig>,
407}
408
409impl Default for TemplatePolicy {
410    fn default() -> Self {
411        Self {
412            relay_state: String::new(),
413            tag_prefix_protocol: "samlp".to_string(),
414            tag_prefix_assertion: "saml".to_string(),
415            tag_prefix_encrypted_assertion: "saml".to_string(),
416            login_response_template: None,
417            login_request_template: None,
418            logout_request_template: None,
419            logout_response_template: None,
420            signature_config: None,
421        }
422    }
423}
424pub(super) fn authn_request_signing_enabled(policy: AuthnRequestSigningPolicy) -> bool {
425    matches!(policy, AuthnRequestSigningPolicy::Sign)
426}
427
428pub(super) fn authn_request_signature_required(policy: AuthnRequestValidationPolicy) -> bool {
429    matches!(policy, AuthnRequestValidationPolicy::RequireSigned)
430}
431
432pub(super) fn assertion_signature_required(policy: AssertionSignaturePolicy) -> bool {
433    matches!(policy, AssertionSignaturePolicy::RequireSigned)
434}
435
436pub(super) fn response_signature_required(policy: ResponseSignaturePolicy) -> bool {
437    matches!(policy, ResponseSignaturePolicy::RequireSigned)
438}
439
440pub(super) fn encrypted_cbc_response_signature_required(policy: ResponseSignaturePolicy) -> bool {
441    matches!(policy, ResponseSignaturePolicy::RequireForEncryptedCbc)
442}
443
444pub(super) fn logout_signature_required(policy: LogoutSignaturePolicy) -> Result<bool, SamlError> {
445    match policy {
446        LogoutSignaturePolicy::RequireSigned => Ok(true),
447        LogoutSignaturePolicy::AllowUnsignedForCompatibility => Ok(false),
448    }
449}
450pub(super) fn name_id_creation_allowed(policy: NameIdCreationPolicy) -> bool {
451    matches!(policy, NameIdCreationPolicy::AllowCreate)
452}
453
454pub(super) fn audience_validation_enabled(policy: AudienceValidationPolicy) -> bool {
455    matches!(policy, AudienceValidationPolicy::Validate)
456}