Skip to main content

saml_rs/
entity.rs

1//! Entity base settings shared by [`crate::sp::ServiceProvider`] and
2//! [`crate::idp::IdentityProvider`].
3
4use 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::xml::XmlLimits;
12
13/// Runtime configuration for an entity (keys, algorithms, flags).
14///
15/// Use [`EntitySetting::default`] and tweak the fields you need.
16#[non_exhaustive]
17#[derive(Clone)]
18pub struct EntitySetting {
19    /// Override entity ID (otherwise taken from metadata).
20    pub entity_id: Option<String>,
21    /// Signature algorithm URI for outgoing signatures.
22    pub request_signature_algorithm: String,
23    /// Data encryption algorithm URI.
24    pub data_encryption_algorithm: String,
25    /// Key encryption algorithm URI.
26    pub key_encryption_algorithm: String,
27    /// Sign-then-encrypt vs encrypt-then-sign.
28    pub message_signing_order: MessageSignatureOrder,
29    /// `AllowCreate` for the NameIDPolicy.
30    pub allow_create: bool,
31    /// Whether assertions are encrypted.
32    pub is_assertion_encrypted: bool,
33    /// Allow XML-Enc RSA key-transport decryption with the bundled software RSA backend.
34    ///
35    /// This is disabled by default because `bergshamra` currently reaches the
36    /// RustCrypto `rsa` crate, which is affected by `RUSTSEC-2023-0071` when an
37    /// attacker can observe timing. Prefer an external/HSM decryptor once one is
38    /// exposed through the public API; enable this only as an explicit
39    /// compatibility exception for deployments that accept that risk.
40    pub allow_insecure_software_rsa_key_transport_decryption: bool,
41    /// Default RelayState.
42    pub relay_state: String,
43    /// SP: signs its AuthnRequests.
44    pub authn_requests_signed: bool,
45    /// SP: requires signed assertions.
46    pub want_assertions_signed: bool,
47    /// SP: reject a `<Response>` whose `<Audience>` is not this entity (default `true`).
48    pub validate_audience: bool,
49    /// SP: requires signed messages.
50    pub want_message_signed: bool,
51    /// IdP: requires signed AuthnRequests.
52    pub want_authn_requests_signed: bool,
53    /// Requires signed LogoutRequest (default `true`).
54    pub want_logout_request_signed: bool,
55    /// Requires signed LogoutResponse.
56    pub want_logout_response_signed: bool,
57    /// Supported NameID formats.
58    pub name_id_format: Vec<String>,
59    /// Signing private key (PEM).
60    pub private_key: Option<String>,
61    /// Passphrase for `private_key`.
62    pub private_key_pass: Option<String>,
63    /// Signing certificate (PEM/base64).
64    pub signing_cert: Option<String>,
65    /// Encryption certificate (PEM/base64).
66    pub encrypt_cert: Option<String>,
67    /// Decryption private key (PEM).
68    pub enc_private_key: Option<String>,
69    /// Passphrase for `enc_private_key`.
70    pub enc_private_key_pass: Option<String>,
71    /// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
72    pub clock_drifts: (i64, i64),
73    /// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
74    /// HTTP-Redirect input.
75    ///
76    /// SAML does not define this limit; the default is a conservative
77    /// resource-exhaustion guard for unauthenticated Redirect messages.
78    pub redirect_inflate_max_bytes: usize,
79    /// XML parser resource limits for inbound messages and metadata parsing.
80    pub xml_limits: XmlLimits,
81    /// IdP: protocol tag prefix for generated IdP messages (default `samlp`).
82    pub tag_prefix_protocol: String,
83    /// IdP: assertion tag prefix for generated IdP messages (default `saml`).
84    pub tag_prefix_assertion: String,
85    /// IdP: tag prefix for the `<EncryptedAssertion>` element (default `saml`).
86    pub tag_prefix_encrypted_assertion: String,
87    /// IdP: login `<Response>` template + attribute configuration.
88    pub login_response_template: Option<crate::template::LoginResponseTemplate>,
89    /// SP: custom `<AuthnRequest>` template (`None` uses the default).
90    pub login_request_template: Option<String>,
91    /// Custom `<LogoutRequest>` template (`None` uses the default).
92    pub logout_request_template: Option<String>,
93    /// Custom `<LogoutResponse>` template (`None` uses the default).
94    pub logout_response_template: Option<String>,
95    /// Custom embedded-signature placement/prefix (`None` uses the default).
96    pub signature_config: Option<SignatureConfig>,
97    /// XML-DSig transforms applied to signed references (default
98    /// enveloped-signature + exclusive C14N).
99    pub transformation_algorithms: Vec<String>,
100}
101
102fn redacted_option(value: &Option<String>) -> Option<&'static str> {
103    value.as_ref().map(|_| "<redacted>")
104}
105
106impl fmt::Debug for EntitySetting {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.debug_struct("EntitySetting")
109            .field("entity_id", &self.entity_id)
110            .field(
111                "request_signature_algorithm",
112                &self.request_signature_algorithm,
113            )
114            .field("data_encryption_algorithm", &self.data_encryption_algorithm)
115            .field("key_encryption_algorithm", &self.key_encryption_algorithm)
116            .field("message_signing_order", &self.message_signing_order)
117            .field("allow_create", &self.allow_create)
118            .field("is_assertion_encrypted", &self.is_assertion_encrypted)
119            .field(
120                "allow_insecure_software_rsa_key_transport_decryption",
121                &self.allow_insecure_software_rsa_key_transport_decryption,
122            )
123            .field("relay_state", &self.relay_state)
124            .field("authn_requests_signed", &self.authn_requests_signed)
125            .field("want_assertions_signed", &self.want_assertions_signed)
126            .field("validate_audience", &self.validate_audience)
127            .field("want_message_signed", &self.want_message_signed)
128            .field(
129                "want_authn_requests_signed",
130                &self.want_authn_requests_signed,
131            )
132            .field(
133                "want_logout_request_signed",
134                &self.want_logout_request_signed,
135            )
136            .field(
137                "want_logout_response_signed",
138                &self.want_logout_response_signed,
139            )
140            .field("name_id_format", &self.name_id_format)
141            .field("private_key", &redacted_option(&self.private_key))
142            .field("private_key_pass", &redacted_option(&self.private_key_pass))
143            .field("signing_cert", &redacted_option(&self.signing_cert))
144            .field("encrypt_cert", &redacted_option(&self.encrypt_cert))
145            .field("enc_private_key", &redacted_option(&self.enc_private_key))
146            .field(
147                "enc_private_key_pass",
148                &redacted_option(&self.enc_private_key_pass),
149            )
150            .field("clock_drifts", &self.clock_drifts)
151            .field(
152                "redirect_inflate_max_bytes",
153                &self.redirect_inflate_max_bytes,
154            )
155            .field("xml_limits", &self.xml_limits)
156            .field("tag_prefix_protocol", &self.tag_prefix_protocol)
157            .field("tag_prefix_assertion", &self.tag_prefix_assertion)
158            .field(
159                "tag_prefix_encrypted_assertion",
160                &self.tag_prefix_encrypted_assertion,
161            )
162            .field("login_response_template", &self.login_response_template)
163            .field("login_request_template", &self.login_request_template)
164            .field("logout_request_template", &self.logout_request_template)
165            .field("logout_response_template", &self.logout_response_template)
166            .field("signature_config", &self.signature_config)
167            .field("transformation_algorithms", &self.transformation_algorithms)
168            .finish()
169    }
170}
171
172/// Custom message rendering hook: given the resolved template, returns
173/// `(id, rendered_xml)`.
174pub type CustomTagReplacement<'a> = &'a dyn Fn(&str) -> (String, String);
175
176/// Where to place the `<Signature>` relative to the reference element.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
178pub enum SignatureAction {
179    /// Insert as the reference's next sibling.
180    #[default]
181    After,
182    /// Insert as the reference's previous sibling.
183    Before,
184    /// Insert as the reference's first child.
185    Prepend,
186    /// Insert as the reference's last child.
187    Append,
188}
189
190/// Customizes the embedded XML-DSig signature.
191#[derive(Debug, Clone)]
192pub struct SignatureConfig {
193    /// Element prefix for the signature (default `ds`).
194    pub prefix: String,
195    /// `local-name()` XPath of the reference element; `None` keeps the default
196    /// (after the signed target's `<Issuer>`).
197    pub reference: Option<String>,
198    /// Placement relative to `reference`.
199    pub action: SignatureAction,
200}
201
202impl Default for SignatureConfig {
203    fn default() -> Self {
204        Self {
205            prefix: "ds".to_string(),
206            reference: None,
207            action: SignatureAction::After,
208        }
209    }
210}
211
212impl Default for EntitySetting {
213    fn default() -> Self {
214        Self {
215            entity_id: None,
216            request_signature_algorithm: signature_algorithm::RSA_SHA256.to_string(),
217            data_encryption_algorithm: data_encryption_algorithm::AES_256.to_string(),
218            key_encryption_algorithm: key_encryption_algorithm::RSA_OAEP_MGF1P.to_string(),
219            message_signing_order: MessageSignatureOrder::SignThenEncrypt,
220            allow_create: false,
221            is_assertion_encrypted: false,
222            allow_insecure_software_rsa_key_transport_decryption: false,
223            relay_state: String::new(),
224            authn_requests_signed: false,
225            want_assertions_signed: false,
226            validate_audience: true,
227            want_message_signed: false,
228            want_authn_requests_signed: false,
229            want_logout_request_signed: true,
230            want_logout_response_signed: true,
231            name_id_format: Vec::new(),
232            private_key: None,
233            private_key_pass: None,
234            signing_cert: None,
235            encrypt_cert: None,
236            enc_private_key: None,
237            enc_private_key_pass: None,
238            clock_drifts: (0, 0),
239            redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
240            xml_limits: XmlLimits::default(),
241            tag_prefix_protocol: "samlp".to_string(),
242            tag_prefix_assertion: "saml".to_string(),
243            tag_prefix_encrypted_assertion: "saml".to_string(),
244            login_response_template: None,
245            login_request_template: None,
246            logout_request_template: None,
247            logout_response_template: None,
248            signature_config: None,
249            transformation_algorithms: vec![
250                transform_algorithm::ENVELOPED_SIGNATURE.to_string(),
251                transform_algorithm::EXC_C14N.to_string(),
252            ],
253        }
254    }
255}
256
257/// Generate a SAML message ID (`_` + UUIDv4).
258pub fn generate_id() -> String {
259    format!("_{}", uuid::Uuid::new_v4())
260}
261
262/// The authenticated subject an IdP issues a response for.
263#[derive(Debug, Clone, Default)]
264pub struct User {
265    /// `<NameID>` value.
266    pub name_id: String,
267    /// Attribute values keyed by their `LoginResponseAttribute.value_tag`;
268    /// each fills the `{attr<Tag>}` placeholder produced for that attribute.
269    pub attributes: Vec<(String, String)>,
270    /// `SessionIndex` for Single Logout requests.
271    pub session_index: Option<String>,
272}
273
274impl User {
275    /// A subject with just a NameID and no attributes.
276    pub fn new(name_id: impl Into<String>) -> Self {
277        Self {
278            name_id: name_id.into(),
279            ..Default::default()
280        }
281    }
282}
283
284/// Current UTC time as an ISO-8601 `IssueInstant` (`YYYY-MM-DDTHH:MM:SSZ`).
285pub fn now_iso8601() -> String {
286    iso8601_offset(0)
287}
288
289/// UTC time `seconds` from now as ISO-8601 (`YYYY-MM-DDTHH:MM:SSZ`).
290pub fn iso8601_offset(seconds: i64) -> String {
291    let t = time::OffsetDateTime::now_utc() + time::Duration::seconds(seconds);
292    format!(
293        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
294        t.year(),
295        u8::from(t.month()),
296        t.day(),
297        t.hour(),
298        t.minute(),
299        t.second(),
300    )
301}
302
303/// The product of building an outbound message for a binding.
304#[derive(Debug, Clone)]
305pub struct BindingContext {
306    /// Generated message ID.
307    pub id: String,
308    /// Redirect: the full URL. POST/SimpleSign: the base64 message.
309    pub context: String,
310    /// RelayState, if any.
311    pub relay_state: Option<String>,
312    /// Destination endpoint.
313    pub entity_endpoint: String,
314    /// Binding used.
315    pub binding: crate::constants::Binding,
316    /// `SAMLRequest` or `SAMLResponse`.
317    pub request_type: &'static str,
318    /// Detached signature (redirect/SimpleSign signed messages), if computed.
319    pub signature: Option<String>,
320    /// Signature algorithm URI accompanying `signature`.
321    pub sig_alg: Option<String>,
322}
323
324impl BindingContext {
325    /// Build the POST/SimpleSign auto-submit form (the `context` must be base64).
326    ///
327    /// If exactly one of `sig_alg` or `signature` is present, this infallible
328    /// helper omits the detached SimpleSign fields. Use [`Self::try_post_form`]
329    /// to reject partial detached signature state.
330    pub fn post_form(&self) -> String {
331        crate::binding::saml_post_binding_form_with_signature(
332            &self.entity_endpoint,
333            self.request_type,
334            &self.context,
335            self.relay_state.as_deref(),
336            self.sig_alg.as_deref(),
337            self.signature.as_deref(),
338        )
339    }
340
341    /// Build the POST/SimpleSign auto-submit form after validating the endpoint.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`crate::error::SamlError::Invalid`] when `entity_endpoint`
346    /// is not an absolute HTTP(S) URL, or when detached SimpleSign state has
347    /// only one of `sig_alg` or `signature`.
348    pub fn try_post_form(&self) -> Result<String, crate::error::SamlError> {
349        crate::binding::try_saml_post_binding_form_with_signature(
350            &self.entity_endpoint,
351            self.request_type,
352            &self.context,
353            self.relay_state.as_deref(),
354            self.sig_alg.as_deref(),
355            self.signature.as_deref(),
356        )
357    }
358}