From 91c9461ef0f5c55b9be7b72a40f3affb68fcc887 Mon Sep 17 00:00:00 2001 From: Nathan Jaremko Date: Sun, 23 Jun 2024 17:25:27 -0400 Subject: [PATCH] Refactors --- src/crypto.rs | 34 +++-------- src/idp/mod.rs | 52 ++++++++++++---- src/idp/tests.rs | 4 +- src/metadata/entity_descriptor.rs | 63 ++++++++++++++------ src/signature.rs | 89 +++++++++++++++++----------- test_vectors/idp_metadata_nested.xml | 47 +++++++++++++-- 6 files changed, 194 insertions(+), 95 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index b24eced..ee93fcd 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -5,6 +5,7 @@ use std::ffi::CString; use std::str::FromStr; use thiserror::Error; +use crate::signature::SignatureAlgorithm; #[cfg(feature = "xmlsec")] use crate::xmlsec::{self, XmlSecKey, XmlSecKeyFormat, XmlSecSignatureContext}; #[cfg(feature = "xmlsec")] @@ -223,6 +224,7 @@ fn get_elements_by_predicate bool>( /// Searches for and returns the element with the given value of the `ID` attribute from the subtree /// rooted at the given node. #[cfg(feature = "xmlsec")] +#[allow(unused)] fn get_element_by_id(elem: &libxml::tree::Node, id: &str) -> Option { let mut elems = get_elements_by_predicate(elem, |node| { node.get_attribute("ID") @@ -486,24 +488,6 @@ pub fn gen_saml_assertion_id() -> String { format!("_{}", uuid::Uuid::new_v4()) } -#[derive(Debug, PartialEq)] -enum SigAlg { - Unimplemented, - RsaSha256, - EcdsaSha256, -} - -impl FromStr for SigAlg { - type Err = Box; - fn from_str(s: &str) -> Result { - match s { - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Ok(SigAlg::RsaSha256), - "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Ok(SigAlg::EcdsaSha256), - _ => Ok(SigAlg::Unimplemented), - } - } -} - #[derive(Debug, Error, Clone)] pub enum UrlVerifierError { #[error("Unimplemented SigAlg: {:?}", sigalg)] @@ -621,11 +605,9 @@ impl UrlVerifier { .collect::>(); // Match against implemented SigAlg - let sig_alg: SigAlg = SigAlg::from_str(&query_params["SigAlg"])?; - if sig_alg == SigAlg::Unimplemented { - return Err(Box::new(UrlVerifierError::SigAlgUnimplemented { - sigalg: query_params["SigAlg"].clone(), - })); + let sig_alg = SignatureAlgorithm::from_str(&query_params["SigAlg"])?; + if let SignatureAlgorithm::Unsupported(sigalg) = sig_alg { + return Err(Box::new(UrlVerifierError::SigAlgUnimplemented { sigalg })); } // Construct a Url so that percent encoded query can be easily @@ -668,13 +650,13 @@ impl UrlVerifier { fn verify_signature( &self, data: &[u8], - sig_alg: SigAlg, + sig_alg: SignatureAlgorithm, signature: &[u8], ) -> Result> { let mut verifier = openssl::sign::Verifier::new( match sig_alg { - SigAlg::RsaSha256 => openssl::hash::MessageDigest::sha256(), - SigAlg::EcdsaSha256 => openssl::hash::MessageDigest::sha256(), + SignatureAlgorithm::RsaSha256 => openssl::hash::MessageDigest::sha256(), + SignatureAlgorithm::EcdsaSha256 => openssl::hash::MessageDigest::sha256(), _ => panic!("sig_alg is bad!"), }, &self.public_key, diff --git a/src/idp/mod.rs b/src/idp/mod.rs index c9835dd..e7b6a95 100644 --- a/src/idp/mod.rs +++ b/src/idp/mod.rs @@ -9,9 +9,10 @@ pub mod verified_request; mod tests; use openssl::bn::{BigNum, MsbOption}; +use openssl::ec::{EcGroup, EcKey}; use openssl::nid::Nid; use openssl::pkey::Private; -use openssl::{asn1::Asn1Time, pkey, rsa::Rsa, x509}; +use openssl::{asn1::Asn1Time, pkey, x509}; use std::str::FromStr; use crate::crypto::{self}; @@ -24,22 +25,31 @@ pub struct IdentityProvider { private_key: pkey::PKey, } -pub enum KeyType { +pub enum Rsa { Rsa2048, Rsa3072, Rsa4096, } -impl KeyType { +impl Rsa { fn bit_length(&self) -> u32 { match &self { - KeyType::Rsa2048 => 2048, - KeyType::Rsa3072 => 3072, - KeyType::Rsa4096 => 4096, + Rsa::Rsa2048 => 2048, + Rsa::Rsa3072 => 3072, + Rsa::Rsa4096 => 4096, } } } +pub enum Elliptic { + NISTP256, +} + +pub enum KeyType { + Rsa(Rsa), + Elliptic(Elliptic), +} + pub struct CertificateParams<'a> { pub common_name: &'a str, pub issuer_name: &'a str, @@ -48,22 +58,40 @@ pub struct CertificateParams<'a> { impl IdentityProvider { pub fn generate_new(key_type: KeyType) -> Result { - let rsa = Rsa::generate(key_type.bit_length())?; - let private_key = pkey::PKey::from_rsa(rsa)?; + let private_key = match key_type { + KeyType::Rsa(rsa) => { + let bit_length = rsa.bit_length(); + let rsa = openssl::rsa::Rsa::generate(bit_length)?; + pkey::PKey::from_rsa(rsa)? + } + KeyType::Elliptic(ecc) => { + let nid = match ecc { + Elliptic::NISTP256 => Nid::X9_62_PRIME256V1, + }; + let group = EcGroup::from_curve_name(nid)?; + let private_key: EcKey = EcKey::generate(&group)?; + pkey::PKey::from_ec_key(private_key)? + } + }; Ok(IdentityProvider { private_key }) } - pub fn from_private_key_der(der_bytes: &[u8]) -> Result { - let rsa = Rsa::private_key_from_der(der_bytes)?; + pub fn from_rsa_private_key_der(der_bytes: &[u8]) -> Result { + let rsa = openssl::rsa::Rsa::private_key_from_der(der_bytes)?; let private_key = pkey::PKey::from_rsa(rsa)?; Ok(IdentityProvider { private_key }) } pub fn export_private_key_der(&self) -> Result, Error> { - let rsa: Rsa = self.private_key.rsa()?; - Ok(rsa.private_key_to_der()?) + if let Ok(ec_key) = self.private_key.ec_key() { + Ok(ec_key.private_key_to_der()?) + } else if let Ok(rsa) = self.private_key.rsa() { + Ok(rsa.private_key_to_der()?) + } else { + Err(Error::UnexpectedError)? + } } pub fn create_certificate(&self, params: &CertificateParams) -> Result, Error> { diff --git a/src/idp/tests.rs b/src/idp/tests.rs index c9119bd..fb45287 100644 --- a/src/idp/tests.rs +++ b/src/idp/tests.rs @@ -37,7 +37,7 @@ fn test_extract_sp() { #[test] fn test_signed_response() { // init our IdP - let idp = IdentityProvider::from_private_key_der(include_bytes!( + let idp = IdentityProvider::from_rsa_private_key_der(include_bytes!( "../../test_vectors/idp_private_key.der" )) .expect("failed to create idp"); @@ -135,7 +135,7 @@ fn test_signed_response_threads() { #[test] fn test_signed_response_fingerprint() { - let idp = IdentityProvider::from_private_key_der(include_bytes!( + let idp = IdentityProvider::from_rsa_private_key_der(include_bytes!( "../../test_vectors/idp_private_key.der" )) .expect("failed to create idp"); diff --git a/src/metadata/entity_descriptor.rs b/src/metadata/entity_descriptor.rs index 466db83..39b320f 100644 --- a/src/metadata/entity_descriptor.rs +++ b/src/metadata/entity_descriptor.rs @@ -7,6 +7,7 @@ use chrono::prelude::*; use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Writer; use serde::Deserialize; +use std::collections::VecDeque; use std::io::Cursor; use std::str::FromStr; use thiserror::Error; @@ -29,18 +30,8 @@ pub enum EntityDescriptorType { } impl EntityDescriptorType { - pub fn take_first(self) -> Option { - match self { - EntityDescriptorType::EntitiesDescriptor(descriptor) => descriptor - .descriptors - .into_iter() - .next() - .and_then(|descriptor_type| match descriptor_type { - EntityDescriptorType::EntitiesDescriptor(_) => None, - EntityDescriptorType::EntityDescriptor(descriptor) => Some(descriptor), - }), - EntityDescriptorType::EntityDescriptor(descriptor) => Some(descriptor), - } + pub fn iter(&self) -> EntityDescriptorIterator { + EntityDescriptorIterator::new(self) } } @@ -284,6 +275,39 @@ impl TryFrom<&EntityDescriptor> for Event<'_> { } } +#[derive(Clone)] +pub struct EntityDescriptorIterator<'a> { + queue: VecDeque<&'a EntityDescriptorType>, +} + +impl<'a> EntityDescriptorIterator<'a> { + pub fn new(root: &'a EntityDescriptorType) -> Self { + let mut queue = VecDeque::new(); + queue.push_back(root); + EntityDescriptorIterator { queue } + } +} + +impl<'a> Iterator for EntityDescriptorIterator<'a> { + type Item = &'a EntityDescriptor; + + fn next(&mut self) -> Option { + while let Some(current) = self.queue.pop_front() { + match current { + EntityDescriptorType::EntitiesDescriptor(entities_descriptor) => { + for descriptor in &entities_descriptor.descriptors { + self.queue.push_back(descriptor); + } + } + EntityDescriptorType::EntityDescriptor(entity_descriptor) => { + return Some(entity_descriptor); + } + } + } + None + } +} + #[cfg(test)] mod test { use crate::traits::ToXml; @@ -345,6 +369,7 @@ mod test { .parse() .expect("Failed to parse EntitiesDescriptor"); + assert_eq!(2, reparsed_entities_descriptor.descriptors.len()); assert_eq!(reparsed_entities_descriptor, entities_descriptor); } @@ -369,11 +394,12 @@ mod test { let expected_entity_descriptor: EntityDescriptor = input_xml .parse() .expect("Failed to parse idp_metadata.xml into an EntityDescriptor"); - let entity_descriptor: EntityDescriptor = entity_descriptor_type - .take_first() + let entity_descriptor = entity_descriptor_type + .iter() + .next() .expect("Failed to take first EntityDescriptor from EntityDescriptorType"); - assert_eq!(expected_entity_descriptor, entity_descriptor); + assert_eq!(&expected_entity_descriptor, entity_descriptor); } #[test] @@ -401,11 +427,12 @@ mod test { let expected_entity_descriptor: EntityDescriptor = input_xml .parse() .expect("Failed to parse idp_metadata.xml into an EntityDescriptor"); - let entity_descriptor: EntityDescriptor = entity_descriptor_type - .take_first() + let entity_descriptor = entity_descriptor_type + .iter() + .next() .expect("Failed to take first EntityDescriptor from EntityDescriptorType"); println!("{entity_descriptor:#?}"); - assert_eq!(expected_entity_descriptor, entity_descriptor); + assert_eq!(&expected_entity_descriptor, entity_descriptor); } } diff --git a/src/signature.rs b/src/signature.rs index 0c4d4b6..10973b2 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -4,6 +4,7 @@ use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::Writer; use serde::Deserialize; use std::io::Cursor; +use std::str::FromStr; const NAME: &str = "ds:Signature"; const SCHEMA: (&str, &str) = ("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#"); @@ -33,32 +34,30 @@ impl Signature { algorithm: SignatureAlgorithm::RsaSha256, hmac_output_length: None, }, - reference: vec![ - Reference { - transforms: Some(Transforms { - transforms: vec![ - Transform { - algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature" - .to_string(), - xpath: None, - }, - Transform { - algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#".to_string(), - xpath: None, - }, - ], - }), - digest_method: DigestMethod { - algorithm: DigestAlgorithm::Sha1, - }, - digest_value: Some(DigestValue { - base64_content: Some("".to_string()), - }), - uri: Some(format!("#{}", ref_id)), - reference_type: None, - id: None, - } - ], + reference: vec![Reference { + transforms: Some(Transforms { + transforms: vec![ + Transform { + algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature" + .to_string(), + xpath: None, + }, + Transform { + algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#".to_string(), + xpath: None, + }, + ], + }), + digest_method: DigestMethod { + algorithm: DigestAlgorithm::Sha1, + }, + digest_value: Some(DigestValue { + base64_content: Some("".to_string()), + }), + uri: Some(format!("#{}", ref_id)), + reference_type: None, + id: None, + }], }, signature_value: SignatureValue { id: None, @@ -294,22 +293,43 @@ impl TryFrom<&SignatureMethod> for Event<'_> { #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum SignatureAlgorithm { - #[serde(rename="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")] + #[serde(rename = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")] RsaSha256, - #[serde(rename="http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1")] + #[serde(rename = "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1")] Sha256RsaMGF1, + #[serde(rename = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256")] + EcdsaSha256, #[serde(untagged)] Unsupported(String), } +impl FromStr for SignatureAlgorithm { + type Err = Box; + + fn from_str(s: &str) -> Result { + Ok(match s { + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => SignatureAlgorithm::RsaSha256, + "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1" => { + SignatureAlgorithm::Sha256RsaMGF1 + } + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => { + SignatureAlgorithm::EcdsaSha256 + } + i => SignatureAlgorithm::Unsupported(i.to_string()), + }) + } +} + impl SignatureAlgorithm { const RSA_SHA256: &'static str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; const SHA256_RSA_MGF1: &'static str = "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1"; + const SHA256_ECDSA: &'static str = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"; pub fn value(&self) -> &str { match self { SignatureAlgorithm::RsaSha256 => Self::RSA_SHA256, SignatureAlgorithm::Sha256RsaMGF1 => Self::SHA256_RSA_MGF1, + SignatureAlgorithm::EcdsaSha256 => Self::SHA256_ECDSA, SignatureAlgorithm::Unsupported(algo) => algo, } } @@ -430,9 +450,9 @@ impl TryFrom<&DigestMethod> for Event<'_> { #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum DigestAlgorithm { - #[serde(rename="http://www.w3.org/2000/09/xmldsig#sha1")] + #[serde(rename = "http://www.w3.org/2000/09/xmldsig#sha1")] Sha1, - #[serde(rename="http://www.w3.org/2001/04/xmlenc#sha256")] + #[serde(rename = "http://www.w3.org/2001/04/xmlenc#sha256")] Sha256, #[serde(untagged)] Unsupported(String), @@ -588,8 +608,10 @@ mod test { #[test] pub fn test_canonicalizationmethod_deserialization() -> Result<(), Box> { - let canonicalization_method = r#""#; - let deserialized: CanonicalizationMethod = quick_xml::de::from_str(canonicalization_method)?; + let canonicalization_method = + r#""#; + let deserialized: CanonicalizationMethod = + quick_xml::de::from_str(canonicalization_method)?; let serialized = deserialized.to_xml()?; let re_deserialized: CanonicalizationMethod = quick_xml::de::from_str(&serialized)?; assert_eq!(deserialized, re_deserialized); @@ -627,7 +649,8 @@ mod test { #[test] pub fn test_digestmethod_deserialization() -> Result<(), Box> { - let digest_method = r#""#; + let digest_method = + r#""#; let deserialized: DigestMethod = quick_xml::de::from_str(digest_method)?; let serialized = deserialized.to_xml()?; let re_deserialized: DigestMethod = quick_xml::de::from_str(&serialized)?; diff --git a/test_vectors/idp_metadata_nested.xml b/test_vectors/idp_metadata_nested.xml index b168bfc..43733e2 100644 --- a/test_vectors/idp_metadata_nested.xml +++ b/test_vectors/idp_metadata_nested.xml @@ -1,5 +1,41 @@ + + - + + + + + + MIIEQjCCAqoCCQCrSuOfmFjlRTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDEYMBYGA1UEAwwPaWRwLmV4YW1wbGUuY29tMB4XDTIwMDMwODIzMDM0NVoXDTMwMDMwNjIzMDM0NVowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxGDAWBgNVBAMMD2lkcC5leGFtcGxlLmNvbTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAL98URjbAoBa7kNxFrIr4WRQ2p82fclLCMWPGV8pgu982jSLePsGuopVCggTRJ9Rd8YdRdkXlK6S8jEa7cZUVaupXlanus48gIm5XxGtbVxr+hkWmLbvs2pZl6UbbCHxOqR4elycsU/NY+9r3R19bHFZxXbcUHWUhdrQanMopWsmT7Jw24ZEyaQjXZ/e9wo6jhbjpW7cRccP/7OmjJsNfDsmnuw6fgk2UFxEAnngUbOfbJ85ksZ0W4Lhs+tyS1sm6vD2vfLx+WYzEqRZDjmeaSEqlg8Atw29lkfXf5ja8GAx+I6lH7qB/Ex4PYU/miBPKUkCv9BkBC6Gklfmutt9kMlwkXDR+xb6Z4jMtUBhqGbsYz/1DzgQbm6B2sq8Q8vm3kkQpnBe3aOUr1KNmNnMQ3HAhG7HpO20UcuvH/AiawOkWA4oepDN03AdMkVSDFg4QhuCk69QAGF0Bwgfvx8BT1kFi6vHuZnhNfDX7PNKLvRceoOwIUa3wqiGsh56wcIjhQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBgQA03335pbzoghD6V4l2Ie1Sj/ffLLCCg6c2prQCX5PiK14sKah0Y8/UY0GattCKYrKPjh4SW1xG0gNFXnA1gyngTXCphlhGCS24lqg040IGIoyQaZNCptdrBRvBgrgONcxH1C9KVc5X+uMjulkW3m5S9nnBHBuU9sEKkF8foCaviY4pFiVsySKgBkfr1pTnXSduohalmfQCAJHKWU4ZZhHAMiJj0Fiy80ba0+40Wt6BTb92XZnyH/3sOmgQ5tazNv3rSoSYepPGLW7Ka6g+xDhl3+pqOS6KyUvA17xFvnakwzV5mLY+rSD2sIuf3qvobPEuq4aNdas7KPZRHDva+DqoMI4wU6woeTagulJV6+vG0YREmdfHmF2QL35yWxTK/vxAJoQzX2QVWk9bOV17Rmf77dDjrBMeLcQUQa9bS2Efg8BAehoDuG+XuqygdHMrAildlU+ZSLdV0YqmVrHsoqTXRrrbuzopEkKeqFblXVii3YBx/E7kpn6/wu84srY+394= + + + + + + + + Example.org Non-Profit Org + Example.org + https://www.example.org/ + + + SAML Technical Support + mailto:technical-support@example.org + + + SAML Support + mailto:support@example.org + + - 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 - - + + Example.org Non-Profit Org