1pub mod generate;
4pub mod idp;
5pub mod sp;
6mod write;
7
8#[cfg(feature = "crypto-bergshamra")]
9pub use crate::crypto::MetadataSignatureVerification;
10pub use generate::{
11 generate_idp_metadata, generate_sp_metadata, try_generate_idp_metadata, Endpoint,
12 IdpMetadataConfig, SpMetadataConfig,
13};
14pub use idp::IdpMetadata;
15pub use sp::SpMetadata;
16
17use crate::constants::{Binding, CertUse};
18use crate::error::SamlError;
19use crate::util::Value;
20use crate::xml::{dom, extract_with_limits, ExtractorField, XmlLimits};
21
22fn base_fields() -> Vec<ExtractorField> {
23 vec![
24 ExtractorField::new("entityID", &["EntityDescriptor"]).attrs(&["entityID"]),
25 ExtractorField::new(
26 "sharedCertificate",
27 &[
28 "EntityDescriptor",
29 "~SSODescriptor",
30 "KeyDescriptor",
31 "KeyInfo",
32 "X509Data",
33 "X509Certificate",
34 ],
35 ),
36 ExtractorField::new(
37 "certificate",
38 &["EntityDescriptor", "~SSODescriptor", "KeyDescriptor"],
39 )
40 .aggregate(&["use"], &["KeyInfo", "X509Data", "X509Certificate"]),
41 ExtractorField::new(
42 "singleLogoutService",
43 &["EntityDescriptor", "~SSODescriptor", "SingleLogoutService"],
44 )
45 .attrs(&["Binding", "Location"]),
46 ExtractorField::new(
47 "nameIDFormat",
48 &["EntityDescriptor", "~SSODescriptor", "NameIDFormat"],
49 ),
50 ]
51}
52
53pub(crate) fn as_object_list(value: &Value) -> Vec<&Value> {
55 match value {
56 Value::Array(items) => items.iter().collect(),
57 Value::Object(_) => vec![value],
58 _ => Vec::new(),
59 }
60}
61
62fn location_for_binding(value: Option<&Value>, binding: Binding) -> Option<String> {
63 let value = value?;
64 for obj in as_object_list(value) {
65 if obj.get_str("binding") == Some(binding.urn()) {
66 return obj.get_str("location").map(str::to_string);
67 }
68 }
69 None
70}
71
72#[derive(Debug, Clone)]
74pub struct Metadata {
75 xml: String,
76 pub(crate) meta: Value,
77}
78
79impl Metadata {
80 pub fn parse(xml: &str, extra: Vec<ExtractorField>) -> Result<Self, SamlError> {
89 Self::parse_with_limits(xml, extra, XmlLimits::default())
90 }
91
92 pub fn parse_with_limits(
99 xml: &str,
100 extra: Vec<ExtractorField>,
101 limits: XmlLimits,
102 ) -> Result<Self, SamlError> {
103 let roots = dom::parse_roots_with_limits(xml, limits)?;
104 if roots
105 .iter()
106 .filter(|n| n.local_name == "EntityDescriptor")
107 .count()
108 > 1
109 {
110 return Err(SamlError::Xml(
111 "ERR_MULTIPLE_METADATA_ENTITYDESCRIPTOR".into(),
112 ));
113 }
114
115 let mut fields = base_fields();
116 fields.extend(extra);
117 let mut meta = extract_with_limits(xml, &fields, limits)?;
118
119 if let Some(shared) = meta.get_str("sharedCertificate") {
121 let shared = shared.to_string();
122 meta.insert(
123 "certificate",
124 Value::Object(vec![
125 ("signing".into(), Value::Str(shared.clone())),
126 ("encryption".into(), Value::Str(shared)),
127 ]),
128 );
129 }
130
131 Ok(Self {
132 xml: xml.to_string(),
133 meta,
134 })
135 }
136
137 pub fn get_metadata(&self) -> &str {
139 &self.xml
140 }
141
142 pub fn get_entity_id(&self) -> Option<&str> {
144 self.meta.get_str("entityID")
145 }
146
147 pub fn get_name_id_format(&self) -> Vec<String> {
149 match self.meta.get("nameIDFormat") {
150 Some(Value::Array(items)) => items
151 .iter()
152 .filter_map(Value::as_str)
153 .map(str::to_string)
154 .collect(),
155 Some(Value::Str(s)) => vec![s.clone()],
156 _ => Vec::new(),
157 }
158 }
159
160 pub fn x509_certificates(&self, use_: CertUse) -> Vec<String> {
162 match self
163 .meta
164 .get("certificate")
165 .and_then(|c| c.get_key(use_.as_str()))
166 {
167 Some(Value::Str(s)) => vec![s.clone()],
168 Some(Value::Array(items)) => items
169 .iter()
170 .filter_map(Value::as_str)
171 .map(str::to_string)
172 .collect(),
173 _ => Vec::new(),
174 }
175 }
176
177 pub fn get_x509_certificate(&self, use_: CertUse) -> Option<String> {
179 self.x509_certificates(use_).into_iter().next()
180 }
181
182 pub fn get_single_logout_service(&self, binding: Binding) -> Option<String> {
184 location_for_binding(self.meta.get("singleLogoutService"), binding)
185 }
186
187 pub fn export_metadata(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
193 std::fs::write(path, &self.xml)
194 }
195
196 pub fn get_support_bindings(&self) -> Vec<Binding> {
198 [Binding::Redirect, Binding::Post, Binding::SimpleSign]
199 .into_iter()
200 .filter(|b| self.get_single_logout_service(*b).is_some())
201 .collect()
202 }
203
204 #[cfg(feature = "crypto-bergshamra")]
213 pub fn verify_signature(&self, trusted_certificates: &[String]) -> Result<bool, SamlError> {
214 self.verify_signature_with_limits(trusted_certificates, XmlLimits::default())
215 }
216
217 #[cfg(feature = "crypto-bergshamra")]
225 pub fn verify_signature_with_limits(
226 &self,
227 trusted_certificates: &[String],
228 limits: XmlLimits,
229 ) -> Result<bool, SamlError> {
230 crate::crypto::verify_metadata_signature_with_limits(
231 &self.xml,
232 trusted_certificates,
233 limits,
234 )
235 }
236
237 #[cfg(feature = "crypto-bergshamra")]
246 pub fn verify_signature_detailed(
247 &self,
248 trusted_certificates: &[String],
249 ) -> Result<crate::crypto::MetadataSignatureVerification, SamlError> {
250 crate::crypto::verify_metadata_signature_detailed(&self.xml, trusted_certificates)
251 }
252
253 #[cfg(feature = "crypto-bergshamra")]
262 pub fn verify_signature_detailed_with_limits(
263 &self,
264 trusted_certificates: &[String],
265 limits: XmlLimits,
266 ) -> Result<MetadataSignatureVerification, SamlError> {
267 crate::crypto::verify_metadata_signature_detailed_with_limits(
268 &self.xml,
269 trusted_certificates,
270 limits,
271 )
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 const IDPMETA: &str = include_str!("../../tests/fixtures/idpmeta.xml");
280 const SPMETA: &str = include_str!("../../tests/fixtures/spmeta.xml");
281 const MULTIPLE: &str = include_str!("../../tests/fixtures/multiple_entitydescriptor.xml");
282
283 #[test]
284 fn rejects_multiple_entity_descriptors() {
285 assert!(Metadata::parse(MULTIPLE, Vec::new()).is_err());
286 }
287
288 #[test]
289 fn parses_idp_metadata() -> Result<(), Box<dyn std::error::Error>> {
290 let idp = IdpMetadata::from_xml(IDPMETA)?;
291 assert_eq!(
292 idp.get_entity_id(),
293 Some("https://idp.example.com/metadata")
294 );
295 assert!(idp.is_want_authn_requests_signed());
296 assert_eq!(
297 idp.get_single_sign_on_service(Binding::Redirect).as_deref(),
298 Some("https://idp.example.org/sso/SingleSignOnService")
299 );
300 assert!(idp.get_x509_certificate(CertUse::Signing).is_some());
301 assert!(idp
302 .get_name_id_format()
303 .iter()
304 .any(|f| f.contains("persistent")));
305 Ok(())
306 }
307
308 #[test]
309 fn parses_sp_metadata() -> Result<(), Box<dyn std::error::Error>> {
310 let sp = SpMetadata::from_xml(SPMETA)?;
311 assert_eq!(sp.get_entity_id(), Some("https://sp.example.org/metadata"));
312 assert!(sp.is_want_assertions_signed());
313 assert!(sp.is_authn_request_signed());
314 assert_eq!(
315 sp.get_assertion_consumer_service(Binding::Post).as_deref(),
316 Some("https://sp.example.org/sp/sso")
317 );
318 assert_eq!(
319 sp.get_single_logout_service(Binding::Redirect).as_deref(),
320 Some("https://sp.example.org/sp/slo")
321 );
322 assert!(sp.get_x509_certificate(CertUse::Encryption).is_some());
323 Ok(())
324 }
325
326 #[test]
327 fn support_bindings_and_export() -> Result<(), Box<dyn std::error::Error>> {
328 let sp = SpMetadata::from_xml(SPMETA)?;
329 assert!(sp.get_support_bindings().contains(&Binding::Redirect));
330 let mut path = std::env::temp_dir();
331 path.push(format!("saml_rs_md_{}.xml", std::process::id()));
332 sp.export_metadata(&path)?;
333 assert_eq!(std::fs::read_to_string(&path)?, sp.get_metadata());
334 std::fs::remove_file(&path)?;
335 Ok(())
336 }
337}