1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3pub enum SsoDeprecatedAlgorithmBehavior {
4 Reject,
5 #[default]
6 Warn,
7 Allow,
8}
9#[derive(Clone, Debug, Default)]
11pub struct SsoSamlSigningPolicy {
12 pub on_deprecated: SsoDeprecatedAlgorithmBehavior,
13 pub allowed_signature_algorithms: Option<Vec<String>>,
14 pub allowed_digest_algorithms: Option<Vec<String>>,
15}
16
17use super::{AuthError, AuthResult, Value};
18const SIGNATURES: &[(&str, &str)] = &[
19 ("sha1", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
20 (
21 "sha256",
22 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
23 ),
24 (
25 "sha384",
26 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
27 ),
28 (
29 "sha512",
30 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
31 ),
32 ("rsa-sha1", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
33 (
34 "rsa-sha256",
35 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
36 ),
37 (
38 "rsa-sha384",
39 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
40 ),
41 (
42 "rsa-sha512",
43 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
44 ),
45 (
46 "ecdsa-sha256",
47 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
48 ),
49 (
50 "ecdsa-sha384",
51 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
52 ),
53 (
54 "ecdsa-sha512",
55 "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
56 ),
57];
58const DIGESTS: &[(&str, &str)] = &[
59 ("sha1", "http://www.w3.org/2000/09/xmldsig#sha1"),
60 ("sha256", "http://www.w3.org/2001/04/xmlenc#sha256"),
61 ("sha384", "http://www.w3.org/2001/04/xmldsig-more#sha384"),
62 ("sha512", "http://www.w3.org/2001/04/xmlenc#sha512"),
63];
64fn normalize<'a>(algorithm: &'a str, names: &[(&str, &'static str)]) -> &'a str {
65 names
66 .iter()
67 .find(|(name, _)| name.eq_ignore_ascii_case(algorithm))
68 .map_or(algorithm, |(_, uri)| *uri)
69}
70fn failure(code: &str, message: String) -> AuthError {
71 AuthError::Api {
72 status: 400,
73 code: Some(code.into()),
74 message,
75 }
76}
77pub(super) fn validate(config: &Value, policy: &SsoSamlSigningPolicy) -> AuthResult<()> {
78 for (field, label, names, allowed) in [
79 (
80 "signatureAlgorithm",
81 "signature",
82 SIGNATURES,
83 &policy.allowed_signature_algorithms,
84 ),
85 (
86 "digestAlgorithm",
87 "digest",
88 DIGESTS,
89 &policy.allowed_digest_algorithms,
90 ),
91 ] {
92 let Some(algorithm) = config
93 .get(field)
94 .and_then(Value::as_str)
95 .filter(|value| !value.is_empty())
96 else {
97 continue;
98 };
99 let normalized = normalize(algorithm, names);
100 if let Some(allowed) = allowed {
101 if !allowed
102 .iter()
103 .any(|value| normalize(value, names) == normalized)
104 {
105 return Err(failure(
106 "SAML_ALGORITHM_NOT_ALLOWED",
107 format!("SAML {label} algorithm not in allow-list: {algorithm}"),
108 ));
109 }
110 continue;
111 }
112 if names.first().is_some_and(|(_, uri)| normalized == *uri) {
113 let message = format!(
114 "SAML config uses deprecated {label} algorithm: {algorithm}. Consider using SHA-256 or stronger."
115 );
116 match policy.on_deprecated {
117 SsoDeprecatedAlgorithmBehavior::Reject => {
118 return Err(failure("SAML_DEPRECATED_CONFIG_ALGORITHM", message));
119 }
120 SsoDeprecatedAlgorithmBehavior::Warn => tracing::warn!("{message}"),
121 SsoDeprecatedAlgorithmBehavior::Allow => {}
122 }
123 } else if !names.iter().any(|(_, uri)| *uri == normalized) {
124 return Err(failure(
125 "SAML_UNKNOWN_ALGORITHM",
126 format!("SAML {label} algorithm not recognized: {algorithm}"),
127 ));
128 }
129 }
130 Ok(())
131}