├── .gitignore ├── .cargo └── audit.toml ├── test_vectors ├── public.der ├── ec_cert.der ├── idp_cert.der ├── private.der ├── sp_cert.der ├── ec_private.der ├── public_key.der ├── idp_private_key.der ├── ec_public.pem ├── public_key.pem ├── ec_private.pem ├── ec_csr.pem ├── logout_response.xml ├── logout_request.xml ├── ec_cert.pem ├── sp_cert.pem ├── sp_private.pem ├── idp_2_metadata_public.pem ├── authn_request_sign_template.xml ├── idp_ecdsa_metadata.xml ├── idp_2_metadata.xml ├── README.md ├── response.xml ├── idp_metadata.xml ├── authn_request.xml ├── response_signed_template.xml ├── response_signed__ecdsa-template.xml ├── sp_metadata.xml ├── response_signed_by_idp_ecdsa.xml ├── response_signed_assertion.xml ├── response_signed.xml ├── response_signed_by_idp_2.xml ├── idp_metadata_nested.xml └── response_encrypted.xml ├── .vscode └── settings.json ├── .envrc ├── src ├── crypto │ ├── ids.rs │ ├── xmlsec │ │ └── wrapper │ │ │ ├── bindings.rs │ │ │ ├── backend.rs │ │ │ ├── mod.rs │ │ │ ├── error.rs │ │ │ ├── keys.rs │ │ │ ├── xmlsec_internal.rs │ │ │ └── xmldsig.rs │ ├── cert_encoding.rs │ ├── crypto_disabled.rs │ └── mod.rs ├── lib.rs ├── metadata │ ├── helpers.rs │ ├── affiliation_descriptor.rs │ ├── encryption_method.rs │ ├── organization.rs │ ├── key_descriptor.rs │ ├── localized.rs │ ├── endpoint.rs │ ├── contact_person.rs │ ├── attribute_consuming_service.rs │ └── sp_sso_descriptor.rs ├── idp │ ├── error.rs │ ├── sp_extractor.rs │ ├── verified_request.rs │ ├── mod.rs │ └── response_builder.rs ├── traits.rs ├── schema │ ├── name_id_policy.rs │ ├── issuer.rs │ ├── requested_authn_context.rs │ ├── conditions.rs │ ├── response.rs │ └── subject.rs ├── attribute.rs ├── service_provider │ └── tests.rs └── key_info.rs ├── CONTRIBUTORS ├── bindings.h ├── .github ├── workflows │ └── deploy.yml └── FUNDING.yml ├── CHANGELOG ├── LICENSE ├── Cargo.toml ├── bindings.rs ├── flake.lock ├── README.md └── flake.nix /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.direnv 3 | /result 4 | **/.DS_Store 5 | -------------------------------------------------------------------------------- /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | ignore = [ 3 | "RUSTSEC-2020-0071", 4 | ] 5 | -------------------------------------------------------------------------------- /test_vectors/public.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/public.der -------------------------------------------------------------------------------- /test_vectors/ec_cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/ec_cert.der -------------------------------------------------------------------------------- /test_vectors/idp_cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/idp_cert.der -------------------------------------------------------------------------------- /test_vectors/private.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/private.der -------------------------------------------------------------------------------- /test_vectors/sp_cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/sp_cert.der -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": [ 3 | "xmlsec" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test_vectors/ec_private.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/ec_private.der -------------------------------------------------------------------------------- /test_vectors/public_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/public_key.der -------------------------------------------------------------------------------- /test_vectors/idp_private_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njaremko/samael/HEAD/test_vectors/idp_private_key.der -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.5/direnvrc" "sha256-RuwIS+QKFj/T9M2TFXScjBsLR6V3A17YVoEW/Q6AZ1w=" 2 | 3 | nix_direnv_manual_reload 4 | 5 | use flake 6 | -------------------------------------------------------------------------------- /src/crypto/ids.rs: -------------------------------------------------------------------------------- 1 | pub fn gen_saml_response_id() -> String { 2 | format!("id{}", uuid::Uuid::new_v4()) 3 | } 4 | 5 | pub fn gen_saml_assertion_id() -> String { 6 | format!("_{}", uuid::Uuid::new_v4()) 7 | } 8 | -------------------------------------------------------------------------------- /test_vectors/ec_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnw 3 | lzJd31gHv5qBg74j1kKSaQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | - agrinman 2 | - ahl 3 | - brandonweeks 4 | - erikescher 5 | - Guiguiprim 6 | - iliana 7 | - janst97 8 | - jclulow 9 | - jmpesp 10 | - luqmana 11 | - RavuAlHemio 12 | - samueltardieu 13 | - ServiusHack 14 | - tiphaineruy 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod attribute; 2 | pub mod crypto; 3 | pub mod idp; 4 | pub mod key_info; 5 | pub mod metadata; 6 | pub mod schema; 7 | pub mod service_provider; 8 | pub mod signature; 9 | 10 | pub mod traits; 11 | 12 | #[macro_use] 13 | extern crate derive_builder; 14 | -------------------------------------------------------------------------------- /bindings.h: -------------------------------------------------------------------------------- 1 | // 2 | // xmlsec1 headers include 3 | // 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | -------------------------------------------------------------------------------- /test_vectors/public_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCRATQySh+Z169IWSnEjSGNZeB3 3 | mCzwRCiS18gJ6BU3YBq74uNGMiMfZR4dC2w1VgoMSadtQZipZdApIJnionbxpa9x 4 | VMLRE66OOC/koJo4iTW37bxcYTZIANujthg0LgaEMIEomoFWnVl8CSpKHR/LceKI 5 | wT2CtaQTvHx398Dp8QIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /test_vectors/ec_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIDWhjR2oP3FQMlxkD8qE8/CP+HTOe/KwOziEwnibBblKoAoGCCqGSM49 6 | AwEHoUQDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnwlzJd31gHv5qBg74j1kKSaQWD 7 | ZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/bindings.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Bindgen generated bindings 3 | //! 4 | #![allow(dead_code)] 5 | #![allow(non_snake_case)] 6 | #![allow(non_camel_case_types)] 7 | #![allow(non_upper_case_globals)] 8 | #![allow(improper_ctypes)] 9 | #![allow(clippy::upper_case_acronyms)] 10 | 11 | include!(concat!(env!("OUT_DIR"), "/xmlsec_bindings.rs")); 12 | -------------------------------------------------------------------------------- /test_vectors/ec_csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBADCBpwIBADBFMQswCQYDVQQGEwJDQTETMBEGA1UECAwKU29tZS1TdGF0ZTEh 3 | MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFkwEwYHKoZIzj0CAQYI 4 | KoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnwlzJd31gHv5qBg74j1kKS 5 | aQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxqAAMAoGCCqGSM49BAMCA0gAMEUC 6 | ICoacEPDHVz2FLWvcCi1feiH42+vXBd6Jy+Z99UE2TpZAiEA/B7qvdfLXGATSNMD 7 | sM9Yp2o6woh3hOmXHN5BW2SAaj0= 8 | -----END CERTIFICATE REQUEST----- 9 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: 3 | push: 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: DeterminateSystems/nix-installer-action@main 10 | - uses: DeterminateSystems/magic-nix-cache-action@main 11 | - uses: cachix/cachix-action@v14 12 | with: 13 | name: nix-community 14 | - run: | 15 | nix build 16 | nix flake check 17 | -------------------------------------------------------------------------------- /test_vectors/logout_response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://sp.example.com/demo1/metadata.php 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test_vectors/logout_request.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://sp.example.com/demo1/metadata.php 4 | session-index-1 5 | test@example.com 6 | -------------------------------------------------------------------------------- /src/metadata/helpers.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use std::io::Write; 4 | 5 | pub fn write_plain_element( 6 | writer: &mut Writer, 7 | element_name: &str, 8 | text: &str, 9 | ) -> Result<(), Box> { 10 | let element = BytesStart::new(element_name); 11 | writer.write_event(Event::Start(element))?; 12 | writer.write_event(Event::Text(BytesText::from_escaped(text)))?; 13 | writer.write_event(Event::End(BytesEnd::new(element_name)))?; 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /test_vectors/ec_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ 3 | BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l 4 | dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 5 | NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK 6 | DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB 7 | BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc 8 | W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI 9 | qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I 10 | UevJyxGd/2RkolE= 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [njaremko] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/crypto/cert_encoding.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, Engine as _}; 2 | use crate::crypto::CertificateDer; 3 | 4 | // strip out 76-width format and decode base64 5 | pub fn decode_x509_cert(x509_cert: &str) -> Result { 6 | let stripped = x509_cert 7 | .as_bytes() 8 | .iter() 9 | .copied() 10 | .filter(|b| !b" \n\t\r\x0b\x0c".contains(b)) 11 | .collect::>(); 12 | 13 | general_purpose::STANDARD.decode(stripped).map(|data| data.into()) 14 | } 15 | 16 | // 76-width base64 encoding (MIME) 17 | pub fn mime_encode_x509_cert(x509_cert_der: &CertificateDer) -> String { 18 | data_encoding::BASE64_MIME.encode(x509_cert_der.der_data()) 19 | } 20 | -------------------------------------------------------------------------------- /src/metadata/affiliation_descriptor.rs: -------------------------------------------------------------------------------- 1 | use crate::metadata::KeyDescriptor; 2 | use chrono::prelude::*; 3 | use serde::Deserialize; 4 | 5 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 6 | pub struct AffiliationDescriptor { 7 | #[serde(rename = "@affiliationOwnerID")] 8 | pub affiliation_descriptors: String, 9 | #[serde(rename = "@ID")] 10 | pub id: String, 11 | #[serde(rename = "@validUntil")] 12 | pub valid_until: Option>, 13 | #[serde(rename = "@cacheDuration")] 14 | pub cache_duration: String, 15 | #[serde(rename = "AffiliateMember", default)] 16 | pub affiliate_members: Vec, 17 | #[serde(rename = "KeyDescriptor", default)] 18 | pub key_descriptors: Vec, 19 | } 20 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/backend.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Abstraction over API differences between dynamic loading and static OpenSSL 3 | //! 4 | 5 | #[cfg(xmlsec_dynamic)] 6 | use crate::crypto::xmlsec::wrapper::bindings as backend_inner; 7 | 8 | #[cfg(xmlsec_static)] 9 | mod backend_inner { 10 | pub use crate::crypto::xmlsec::wrapper::bindings::{ 11 | xmlSecOpenSSLAppInit as xmlSecCryptoAppInit, 12 | xmlSecOpenSSLAppKeyCertLoad as xmlSecCryptoAppKeyCertLoad, 13 | xmlSecOpenSSLAppKeyCertLoadMemory as xmlSecCryptoAppKeyCertLoadMemory, 14 | xmlSecOpenSSLAppKeyLoadMemory as xmlSecCryptoAppKeyLoadMemory, 15 | xmlSecOpenSSLAppShutdown as xmlSecCryptoAppShutdown, xmlSecOpenSSLInit as xmlSecCryptoInit, 16 | xmlSecOpenSSLShutdown as xmlSecCryptoShutdown, 17 | }; 18 | } 19 | 20 | pub use backend_inner::*; 21 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/mod.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Source of wrapper adapted from a separate project: https://github.com/voipir/rust-xmlsec 3 | // MIT Licence (Voipir Group): https://github.com/voipir/rust-xmlsec/blob/master/LICENSE 4 | 5 | //! 6 | //! Bindings for XmlSec1 7 | //! 8 | //! Modules reflect the header names of the bound xmlsec1 library 9 | //! 10 | #![deny(missing_docs)] 11 | 12 | #[doc(hidden)] 13 | pub use libxml::tree::document::Document as XmlDocument; 14 | #[doc(hidden)] 15 | #[allow(unused)] 16 | pub use libxml::tree::node::Node as XmlNode; 17 | 18 | mod backend; 19 | mod error; 20 | mod keys; 21 | mod xmldsig; 22 | mod xmlsec_internal; 23 | mod bindings; 24 | 25 | // exports 26 | pub use self::error::XmlSecError; 27 | pub use self::error::XmlSecResult; 28 | pub use self::keys::XmlSecKey; 29 | pub use self::keys::XmlSecKeyFormat; 30 | pub use self::xmldsig::XmlSecSignatureContext; 31 | -------------------------------------------------------------------------------- /test_vectors/sp_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICYjCCAcugAwIBAgIBADANBgkqhkiG9w0BAQ0FADBOMQswCQYDVQQGEwJ1czEV 3 | MBMGA1UECAwMUGVubnN5bHZhbmlhMRIwEAYDVQQKDAlBQ01FIENvcnAxFDASBgNV 4 | BAMMC2V4YW1wbGUuY29tMB4XDTI1MDMwNDE0NDYzN1oXDTI2MDMwNDE0NDYzN1ow 5 | TjELMAkGA1UEBhMCdXMxFTATBgNVBAgMDFBlbm5zeWx2YW5pYTESMBAGA1UECgwJ 6 | QUNNRSBDb3JwMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEF 7 | AAOBjQAwgYkCgYEAzDiE5ekAwPlyDS3TkGOmmPEsBDBpllWo3mKT1jQm4jFw2uuZ 8 | kkTq7vCaxTW/605GtwMPpfWioh7G84+COhL/bPvPZdXRnzMzhZGXoQGRr65cA6da 9 | VrUQD/vAYtvk2wmdPH8syaAsEv7c7QxtrdeQwaWYI8zwpOyG4ZLy6kBhOlcCAwEA 10 | AaNQME4wHQYDVR0OBBYEFAa6vl34COSnZQmE2NTtwBVdAcWEMB8GA1UdIwQYMBaA 11 | FAa6vl34COSnZQmE2NTtwBVdAcWEMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEN 12 | BQADgYEAXhtyDK6qvHR8YmoJurZG0H9yj4W7O4TvweZMPOpATtVddnh1ZffgrAuq 13 | XzmxQV/Zg2+mceJ1Ez2GWlRE5kCKqcao8iQpPs4zd9CPll3u5+yqPXeeq1lAdsE1 14 | 9vODcZYoUBFgmhnRngCOOoliu+3pd6YDgSbvbG6Q390Uhhr14mA= 15 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test_vectors/sp_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMw4hOXpAMD5cg0t 3 | 05BjppjxLAQwaZZVqN5ik9Y0JuIxcNrrmZJE6u7wmsU1v+tORrcDD6X1oqIexvOP 4 | gjoS/2z7z2XV0Z8zM4WRl6EBka+uXAOnWla1EA/7wGLb5NsJnTx/LMmgLBL+3O0M 5 | ba3XkMGlmCPM8KTshuGS8upAYTpXAgMBAAECgYEAi21rDqzt3tJvk5/d+Y6Ph4vg 6 | yVtkO0dwa6RR2sTwZy3qJw0DZGG5JDkQ8eOojDZ9ASYN4Pi7eIQawN8RwiSGTeFU 7 | 9geqK8tDdoJa6GQyJ0IDzokEaPduAPI07mPfO9maiM9aK0eKc1PItCc7gfnpU6rh 8 | 3ZXfTgMe26CGNPiqDAECQQDxK5cXkvG+/C7sKtHbF3YaW5JrreIBf3iPFqQMNi14 9 | ZsaUEE+2BiPyeFNg1OPn7vjhe0XVQL8qf4Btpdk8m/iXAkEA2MdIvlXRce6NN+K/ 10 | Iqnx4EkIY4YWhwj36NpxXH0zWkAaxswh1yp+mRJNsKI1+Li6yBvnNwHCrt5UYQW5 11 | GBlEQQJAf3JCqUGNIRlRjppeRgKS9gDYUrEUEyiSxEL5tD5ZLxxY6lvoU8/Q5Uyy 12 | +yPlwGZn/XhQgg5yN3ojm04ei8n8xwJBALxjY6pVdjEm+P7KRQTg39zkWy/yhX1Q 13 | o/FudPnFrG0QLLT7DaWpvl9UcsPAFFtUXq4s4aECHKhPetDeYl65BoECQBftLkzG 14 | qSkWvUKDWs/LspUnROXasOtDhAtttZMqHGEuq5oYdHLmNDTHPGIpEptQIrdEN+V9 15 | LElOgInl0QEaR8w= 16 | -----END PRIVATE KEY----- -------------------------------------------------------------------------------- /test_vectors/idp_2_metadata_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICpzCCAhACCQDuFX0Db5iljDANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMC 3 | VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEQMA4G 4 | A1UECgwHU2FtbGluZzEPMA0GA1UECwwGU2FsaW5nMRQwEgYDVQQDDAtjYXByaXph 5 | LmNvbTEmMCQGCSqGSIb3DQEJARYXZW5naW5lZXJpbmdAY2Fwcml6YS5jb20wHhcN 6 | MTgwNTE1MTgxMTEwWhcNMjgwNTEyMTgxMTEwWjCBlzELMAkGA1UEBhMCVVMxEzAR 7 | BgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEQMA4GA1UECgwH 8 | U2FtbGluZzEPMA0GA1UECwwGU2FsaW5nMRQwEgYDVQQDDAtjYXByaXphLmNvbTEm 9 | MCQGCSqGSIb3DQEJARYXZW5naW5lZXJpbmdAY2Fwcml6YS5jb20wgZ8wDQYJKoZI 10 | hvcNAQEBBQADgY0AMIGJAoGBAJEBNDJKH5nXr0hZKcSNIY1l4HeYLPBEKJLXyAno 11 | FTdgGrvi40YyIx9lHh0LbDVWCgxJp21BmKll0CkgmeKidvGlr3FUwtETro44L+Sg 12 | mjiJNbftvFxhNkgA26O2GDQuBoQwgSiagVadWXwJKkodH8tx4ojBPYK1pBO8fHf3 13 | wOnxAgMBAAEwDQYJKoZIhvcNAQELBQADgYEACIylhvh6T758hcZjAQJiV7rMRg+O 14 | mb68iJI4L9f0cyBcJENR+1LQNgUGyFDMm9Wm9o81CuIKBnfpEE2Jfcs76YVWRJy5 15 | xJ11GFKJJ5T0NEB7txbUQPoJOeNoE736lF5vYw6YKp8fJqPW0L2PLWe9qTn8hxpd 16 | njo3k6r5gXyl8tk= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Unreleased 4 | 5 | - add `EncryptedAssertion` and ability to decrypt it 6 | 7 | ## 0.0.18 8 | 9 | - Fix typo in `AttributeConsumingServiceIndex` attribute name in AuthnRequest 10 | - Re-export `RequestedAttribute` from metadata module 11 | - Make `service_provider::Error` implement `Clone` 12 | 13 | ### 0.0.17 14 | 15 | - Expose the `ToXml` trait, and make it consistent with libraries like `serde_json` 16 | 17 | ### 0.0.16 18 | 19 | - Add support for basic `RequestedAuthnContext` de-/serialization in `AuthnRequest` 20 | - Add support for Elliptic-curve cryptography 21 | - Use enum for signature and digest algorithm 22 | - Add EntityDescriptorType enum to allow parsing of arbitrarily nested EntityDescriptors and EntitiesDescriptors 23 | 24 | ### 0.0.15 25 | 26 | - Updates dependencies 27 | - Removes bad openssl import that broke builds against recent openssl releases 28 | - Fix failing test due to expired certs 29 | 30 | ### 0.0.14 31 | 32 | - Check blocks against each cert individually (Thanks @janst97) 33 | 34 | ### 0.0.13 35 | 36 | - Ability to serialize logout requests 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nathan Jaremko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/idp/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum Error { 5 | #[error("")] 6 | NoSignature, 7 | #[error("")] 8 | NoKeyInfo, 9 | #[error("")] 10 | NoCertificate, 11 | #[error("")] 12 | NoSPSsoDescriptors, 13 | #[error("")] 14 | SignatureFailed, 15 | #[error("")] 16 | UnexpectedError, 17 | #[error("")] 18 | MismatchedCertificate, 19 | #[error("")] 20 | InvalidCertificateEncoding, 21 | 22 | #[error("")] 23 | MissingAudience, 24 | #[error("")] 25 | MissingAcsUrl, 26 | #[error("")] 27 | NonHttpPostBindingUnsupported, 28 | 29 | #[error("")] 30 | MissingAuthnRequestSubjectNameID, 31 | #[error("")] 32 | MissingAuthnRequestIssuer, 33 | 34 | #[error("Invalid AuthnRequest: {}", error)] 35 | InvalidAuthnRequest { 36 | #[from] 37 | error: crate::schema::authn_request::Error, 38 | }, 39 | 40 | #[error("OpenSSL Error: {}", stack)] 41 | OpenSSLError { 42 | #[from] 43 | stack: openssl::error::ErrorStack, 44 | }, 45 | 46 | #[error("Verification Error: {}", error)] 47 | VerificationError { 48 | #[from] 49 | error: crate::crypto::CryptoError, 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /src/metadata/encryption_method.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::io::Cursor; 5 | 6 | const NAME: &str = "md:EncryptionMethod"; 7 | 8 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 9 | pub struct EncryptionMethod { 10 | #[serde(rename = "@Algorithm")] 11 | pub algorithm: String, 12 | } 13 | 14 | impl TryFrom for Event<'_> { 15 | type Error = Box; 16 | 17 | fn try_from(value: EncryptionMethod) -> Result { 18 | (&value).try_into() 19 | } 20 | } 21 | 22 | impl TryFrom<&EncryptionMethod> for Event<'_> { 23 | type Error = Box; 24 | 25 | fn try_from(value: &EncryptionMethod) -> Result { 26 | let mut write_buf = Vec::new(); 27 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 28 | let mut root = BytesStart::new(NAME); 29 | 30 | root.push_attribute(("Algorithm", value.algorithm.as_ref())); 31 | 32 | writer.write_event(Event::Start(root))?; 33 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 34 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 35 | write_buf, 36 | )?))) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "samael" 3 | version = "0.0.19" 4 | authors = ["Nathan Jaremko "] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/njaremko/samael" 9 | description = "A SAML2 library for Rust" 10 | keywords = ["saml", "saml2", "xml", "sso"] 11 | categories = ["authentication"] 12 | build = "bindings.rs" 13 | 14 | [lib] 15 | # Disabling doctests because they cause nix build check phase to fail 16 | doctest = false 17 | crate-type = ["staticlib", "rlib"] 18 | 19 | [features] 20 | default = ["xmlsec"] 21 | xmlsec = ["libc", "lazy_static", "libxml"] 22 | 23 | [build-dependencies] 24 | pkg-config = "^0.3.17" 25 | bindgen = "^0.71.1" 26 | 27 | [dependencies] 28 | openssl = "^0.10.0" 29 | openssl-sys = "^0.9.0" 30 | openssl-probe = "^0.1.2" 31 | url = "^2.1.1" 32 | quick-xml = { version = "^0.37.2", features = ["serialize"] } 33 | serde = { version = "^1.0.0", features = ["derive"] } 34 | chrono = { version = "^0.4.0", features = ["serde"] } 35 | base64 = "^0.22.0" 36 | flate2 = "^1.0.0" 37 | rand = "^0.9.0" 38 | derive_builder = "^0.20.0" 39 | libxml = { version = "=0.3.3", optional = true } 40 | uuid = { version = "^1.3.0", features = ["v4"] } 41 | data-encoding = "2.9.0" 42 | libc = { version = "^0.2.66", optional = true } 43 | lazy_static = { version = "^1.4.0", optional = true } 44 | thiserror = "^2.0.11" 45 | -------------------------------------------------------------------------------- /src/crypto/crypto_disabled.rs: -------------------------------------------------------------------------------- 1 | //! This module provides the behaviour if no crypto is available. 2 | 3 | use crate::crypto::{CryptoError, CryptoProvider, CertificateDer}; 4 | use crate::schema::CipherValue; 5 | 6 | pub struct NoCrypto; 7 | 8 | impl CryptoProvider for NoCrypto { 9 | type PrivateKey = (); 10 | fn verify_signed_xml>( 11 | _xml: Bytes, 12 | _x509_cert_der: &CertificateDer, 13 | _id_attribute: Option<&str>, 14 | ) -> Result<(), CryptoError> { 15 | // todo: Should have a warning?? 16 | Ok(()) 17 | } 18 | 19 | fn reduce_xml_to_signed(_xml_str: &str, _certs: &[CertificateDer]) -> Result { 20 | // Since we cannot verify anything. Return empty. 21 | Ok(String::new()) 22 | } 23 | 24 | fn decrypt_assertion_key_info( 25 | _cipher_value: &CipherValue, 26 | _method: &str, 27 | _decryption_key: &Self::PrivateKey, 28 | ) -> Result, CryptoError> { 29 | Err(CryptoError::CryptoDisabled) 30 | } 31 | 32 | fn decrypt_assertion_value_info( 33 | _cipher_value: &CipherValue, 34 | _method: &str, 35 | _decryption_key: &[u8], 36 | ) -> Result, CryptoError> { 37 | Err(CryptoError::CryptoDisabled) 38 | } 39 | 40 | fn sign_xml>( 41 | _xml: Bytes, 42 | _private_key_der: &[u8], 43 | ) -> Result { 44 | Err(CryptoError::CryptoDisabled) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test_vectors/authn_request_sign_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://sp.example.com/demo1/metadata.php 4 | 5 | 6 | 7 | 8 | 9 | 10 | urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::{events::Event, Writer}; 2 | use std::{fmt::Debug, io::Cursor, str::Utf8Error}; 3 | 4 | pub trait ToXml<'a> { 5 | type Error; 6 | 7 | /// Serialize the data structure as a String of XML. 8 | fn to_string(&'a self) -> Result; 9 | 10 | /// Serialize the data structure as an XML byte vector. 11 | fn to_vec(&'a self) -> Result, Self::Error>; 12 | 13 | /// Serialize the data structure as XML into the I/O stream. 14 | fn to_writer(&'a self, writer: impl std::io::Write) -> Result<(), Self::Error>; 15 | } 16 | 17 | impl<'a, FromType> ToXml<'a> for FromType 18 | where 19 | &'a FromType: TryInto> + 'a, 20 | <&'a FromType as TryInto>>::Error: 21 | Debug + From + From + From, 22 | { 23 | type Error = <&'a FromType as TryInto>>::Error; 24 | 25 | fn to_string(&'a self) -> Result { 26 | let v = self.to_vec()?; 27 | let output = std::str::from_utf8(v.as_slice())?.to_string(); 28 | Ok(output) 29 | } 30 | 31 | fn to_vec(&'a self) -> Result, Self::Error> { 32 | let mut v = Vec::new(); 33 | self.to_writer(Cursor::new(&mut v))?; 34 | Ok(v) 35 | } 36 | 37 | fn to_writer(&'a self, writer: impl std::io::Write) -> Result<(), Self::Error> { 38 | let mut xml_writer = Writer::new(writer); 39 | let e: Event<'a> = self.try_into()?; 40 | xml_writer.write_event(e)?; 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test_vectors/idp_ecdsa_metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 10 | MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ 11 | BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l 12 | dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 13 | NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK 14 | DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB 15 | BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc 16 | W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI 17 | qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I 18 | UevJyxGd/2RkolE= 19 | 20 | 21 | 22 | 23 | 25 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 26 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/schema/name_id_policy.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::io::Cursor; 5 | 6 | const NAME: &str = "saml2p:NameIDPolicy"; 7 | 8 | #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] 9 | pub struct NameIdPolicy { 10 | #[serde(rename = "@Format")] 11 | pub format: Option, 12 | #[serde(rename = "@SPNameQualifier")] 13 | pub sp_name_qualifier: Option, 14 | #[serde(rename = "@AllowCreate")] 15 | pub allow_create: Option, 16 | } 17 | 18 | impl TryFrom for Event<'_> { 19 | type Error = Box; 20 | 21 | fn try_from(value: NameIdPolicy) -> Result { 22 | (&value).try_into() 23 | } 24 | } 25 | 26 | impl TryFrom<&NameIdPolicy> for Event<'_> { 27 | type Error = Box; 28 | 29 | fn try_from(value: &NameIdPolicy) -> Result { 30 | let mut write_buf = Vec::new(); 31 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 32 | let mut root = BytesStart::new(NAME); 33 | if let Some(format) = &value.format { 34 | root.push_attribute(("Format", format.as_ref())); 35 | } 36 | if let Some(sp_name_qualifier) = &value.sp_name_qualifier { 37 | root.push_attribute(("SPNameQualifier", sp_name_qualifier.as_ref())); 38 | } 39 | if let Some(allow_create) = &value.allow_create { 40 | root.push_attribute(("AllowCreate", allow_create.to_string().as_ref())); 41 | } 42 | writer.write_event(Event::Empty(root))?; 43 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 44 | write_buf, 45 | )?))) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test_vectors/idp_2_metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MIICpzCCAhACCQDuFX0Db5iljDANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEQMA4GA1UECgwHU2FtbGluZzEPMA0GA1UECwwGU2FsaW5nMRQwEgYDVQQDDAtjYXByaXphLmNvbTEmMCQGCSqGSIb3DQEJARYXZW5naW5lZXJpbmdAY2Fwcml6YS5jb20wHhcNMTgwNTE1MTgxMTEwWhcNMjgwNTEyMTgxMTEwWjCBlzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEQMA4GA1UECgwHU2FtbGluZzEPMA0GA1UECwwGU2FsaW5nMRQwEgYDVQQDDAtjYXByaXphLmNvbTEmMCQGCSqGSIb3DQEJARYXZW5naW5lZXJpbmdAY2Fwcml6YS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJEBNDJKH5nXr0hZKcSNIY1l4HeYLPBEKJLXyAnoFTdgGrvi40YyIx9lHh0LbDVWCgxJp21BmKll0CkgmeKidvGlr3FUwtETro44L+SgmjiJNbftvFxhNkgA26O2GDQuBoQwgSiagVadWXwJKkodH8tx4ojBPYK1pBO8fHf3wOnxAgMBAAEwDQYJKoZIhvcNAQELBQADgYEACIylhvh6T758hcZjAQJiV7rMRg+Omb68iJI4L9f0cyBcJENR+1LQNgUGyFDMm9Wm9o81CuIKBnfpEE2Jfcs76YVWRJy5xJ11GFKJJ5T0NEB7txbUQPoJOeNoE736lF5vYw6YKp8fJqPW0L2PLWe9qTn8hxpdnjo3k6r5gXyl8tk= 8 | 9 | 10 | 11 | 13 | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/metadata/organization.rs: -------------------------------------------------------------------------------- 1 | use crate::metadata::{LocalizedName, LocalizedUri}; 2 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 3 | use quick_xml::Writer; 4 | use serde::Deserialize; 5 | use std::io::Cursor; 6 | 7 | const NAME: &str = "md:Organization"; 8 | 9 | #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] 10 | pub struct Organization { 11 | #[serde(rename = "OrganizationName")] 12 | pub organization_names: Option>, 13 | #[serde(rename = "OrganizationDisplayName")] 14 | pub organization_display_names: Option>, 15 | #[serde(rename = "md:OrganizationURL")] 16 | pub organization_urls: Option>, 17 | } 18 | 19 | impl TryFrom<&Organization> for Event<'_> { 20 | type Error = Box; 21 | 22 | fn try_from(value: &Organization) -> Result { 23 | let mut write_buf = Vec::new(); 24 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 25 | let root = BytesStart::new(NAME); 26 | writer.write_event(Event::Start(root))?; 27 | if let Some(organization_names) = &value.organization_names { 28 | for name in organization_names { 29 | writer.write_event(name.to_xml("md:OrganizationName")?)?; 30 | } 31 | } 32 | if let Some(organization_display_names) = &value.organization_display_names { 33 | for name in organization_display_names { 34 | writer.write_event(name.to_xml("md:OrganizationDisplayName")?)?; 35 | } 36 | } 37 | if let Some(organization_urls) = &value.organization_urls { 38 | for url in organization_urls { 39 | writer.write_event(url.to_xml("md:OrganizationURL")?)?; 40 | } 41 | } 42 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 43 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 44 | write_buf, 45 | )?))) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/metadata/key_descriptor.rs: -------------------------------------------------------------------------------- 1 | use crate::key_info::KeyInfo; 2 | use crate::metadata::EncryptionMethod; 3 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 4 | use quick_xml::Writer; 5 | use serde::Deserialize; 6 | use std::io::Cursor; 7 | 8 | const NAME: &str = "md:KeyDescriptor"; 9 | 10 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 11 | pub struct KeyDescriptor { 12 | #[serde(rename = "@use")] 13 | pub key_use: Option, 14 | #[serde(rename = "KeyInfo")] 15 | pub key_info: KeyInfo, 16 | #[serde(rename = "EncryptionMethod")] 17 | pub encryption_methods: Option>, 18 | } 19 | 20 | impl KeyDescriptor { 21 | pub fn is_signing(&self) -> bool { 22 | self.key_use 23 | .as_ref() 24 | .map(|u| u == "signing") 25 | .unwrap_or(false) 26 | } 27 | } 28 | 29 | impl TryFrom for Event<'_> { 30 | type Error = Box; 31 | 32 | fn try_from(value: KeyDescriptor) -> Result { 33 | (&value).try_into() 34 | } 35 | } 36 | 37 | impl TryFrom<&KeyDescriptor> for Event<'_> { 38 | type Error = Box; 39 | 40 | fn try_from(value: &KeyDescriptor) -> Result { 41 | let mut write_buf = Vec::new(); 42 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 43 | let mut root = BytesStart::new(NAME); 44 | if let Some(key_use) = &value.key_use { 45 | root.push_attribute(("use", key_use.as_ref())); 46 | } 47 | writer.write_event(Event::Start(root))?; 48 | 49 | let event: Event<'_> = (&value.key_info).try_into()?; 50 | writer.write_event(event)?; 51 | 52 | if let Some(encryption_methods) = &value.encryption_methods { 53 | for method in encryption_methods { 54 | let event: Event<'_> = method.try_into()?; 55 | writer.write_event(event)?; 56 | } 57 | } 58 | 59 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 60 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 61 | write_buf, 62 | )?))) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/metadata/localized.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::io::Cursor; 5 | 6 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 7 | pub struct LocalizedName { 8 | #[serde(rename = "@xml:lang")] 9 | #[serde(alias = "@lang")] 10 | pub lang: Option, 11 | #[serde(rename = "$value")] 12 | pub value: String, 13 | } 14 | 15 | impl LocalizedName { 16 | pub fn to_xml(&self, element_name: &str) -> Result> { 17 | let mut write_buf = Vec::new(); 18 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 19 | let mut root = BytesStart::new(element_name); 20 | if let Some(x) = &self.lang { 21 | root.push_attribute(("xml:lang", x.as_ref())); 22 | } 23 | writer.write_event(Event::Start(root))?; 24 | writer.write_event(Event::Text(BytesText::from_escaped(&self.value)))?; 25 | writer.write_event(Event::End(BytesEnd::new(element_name)))?; 26 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 27 | write_buf, 28 | )?))) 29 | } 30 | } 31 | 32 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 33 | pub struct LocalizedUri { 34 | #[serde(rename = "@xml:lang")] 35 | #[serde(alias = "@lang")] 36 | pub lang: Option, 37 | #[serde(rename = "$value")] 38 | pub value: String, 39 | } 40 | 41 | impl LocalizedUri { 42 | pub fn to_xml(&self, element_name: &str) -> Result> { 43 | let mut write_buf = Vec::new(); 44 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 45 | let mut root = BytesStart::new(element_name); 46 | if let Some(x) = &self.lang { 47 | root.push_attribute(("xml:lang", x.as_ref())); 48 | } 49 | writer.write_event(Event::Start(root))?; 50 | writer.write_event(Event::Text(BytesText::from_escaped(&self.value)))?; 51 | writer.write_event(Event::End(BytesEnd::new(element_name)))?; 52 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 53 | write_buf, 54 | )?))) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/schema/issuer.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::io::Cursor; 5 | 6 | const NAME: &str = "saml2:Issuer"; 7 | const SCHEMA: (&str, &str) = ("xmlns:saml2", "urn:oasis:names:tc:SAML:2.0:assertion"); 8 | 9 | #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] 10 | pub struct Issuer { 11 | #[serde(rename = "@NameQualifier")] 12 | pub name_qualifier: Option, 13 | #[serde(rename = "@SPNameQualifier")] 14 | pub sp_name_qualifier: Option, 15 | #[serde(rename = "@Format")] 16 | pub format: Option, 17 | #[serde(rename = "@SPProvidedID")] 18 | pub sp_provided_id: Option, 19 | #[serde(rename = "$value")] 20 | pub value: Option, 21 | } 22 | 23 | impl TryFrom for Event<'_> { 24 | type Error = Box; 25 | 26 | fn try_from(value: Issuer) -> Result { 27 | (&value).try_into() 28 | } 29 | } 30 | 31 | impl TryFrom<&Issuer> for Event<'_> { 32 | type Error = Box; 33 | 34 | fn try_from(value: &Issuer) -> Result { 35 | let mut write_buf = Vec::new(); 36 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 37 | let mut root = BytesStart::new(NAME); 38 | root.push_attribute(SCHEMA); 39 | 40 | if let Some(name_qualifier) = &value.name_qualifier { 41 | root.push_attribute(("NameQualifier", name_qualifier.as_ref())); 42 | } 43 | if let Some(sp_name_qualifier) = &value.sp_name_qualifier { 44 | root.push_attribute(("SPNameQualifier", sp_name_qualifier.as_ref())); 45 | } 46 | if let Some(format) = &value.format { 47 | root.push_attribute(("Format", format.as_ref())); 48 | } 49 | if let Some(sp_provided_id) = &value.sp_provided_id { 50 | root.push_attribute(("SPProvidedID", sp_provided_id.as_ref())); 51 | } 52 | writer.write_event(Event::Start(root))?; 53 | if let Some(value) = &value.value { 54 | writer.write_event(Event::Text(BytesText::from_escaped(value)))?; 55 | } 56 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 57 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 58 | write_buf, 59 | )?))) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test_vectors/README.md: -------------------------------------------------------------------------------- 1 | `idp_cert.der` corresponds to `idp_metadata.xml`: 2 | 3 | $ grep "$(openssl x509 -inform der -in idp_cert.der | grep -v -- '-----' | tr -d '\n')" * 4 | idp_metadata.xml: .... 5 | 6 | TODO it is unknown what has signed `response_signed.xml`, it's not the certificate in `idp_metadata.xml`, see `idp::tests::test_accept_signed_with_correct_key_idp` 7 | 8 | `response_encrypted.xml` is an encrypted response, it is encrypted with the private key in `sp_private.pem`. `sp_private.pem` is the private key of `sp_cert.pem`. 9 | 10 | # Generating signed responses for tests 11 | 12 | `public.der` and `private.der` correspond to `idp_2_metadata.xml`, and are used to sign `response_signed_by_idp_2.xml`. Update `response_signed_template.xml` and then generate `response_signed_by_idp_2.xml` using: 13 | 14 | ```bash 15 | xmlsec1 --sign --privkey-der private.der,public.der --output response_signed_by_idp_2.xml --id-attr:ID Response response_signed_template.xml 16 | ``` 17 | 18 | Validate with: 19 | 20 | ```bash 21 | xmlsec1 --verify --trusted-der public.der --id-attr:ID Response response_signed_by_idp_2.xml 22 | ``` 23 | 24 | Both `response_signed_by_idp_2.xml` and `authn_request_sign_template.xml` are used in unit tests, where `authn_request_sign_template.xml` is signed in the test. 25 | 26 | To generate `response_signed_by_idp_ecdsa.xml`: 27 | 28 | ```bash 29 | xmlsec1 --sign --privkey-der ec_private.der,ec_cert.der --output response_signed_by_idp_ecdsa.xml --id-attr:ID Response response_signed__ecdsa-template.xml 30 | ``` 31 | 32 | How the EC stuff was generated: 33 | 34 | ```bash 35 | # Step 1: Generate ECDSA Private Key 36 | openssl ecparam -genkey -name prime256v1 -out ec_private.pem 37 | 38 | # Step 2: Create a Certificate Signing Request (CSR) 39 | openssl req -new -key ec_private.pem -out ec_csr.pem 40 | 41 | # Step 3: Self-Sign the CSR to Create an X.509 Certificate 42 | openssl x509 -req -in ec_csr.pem -signkey ec_private.pem -out ec_cert.pem -days 365000 43 | 44 | # Step 4: Convert the Private Key and Certificate to DER Format 45 | openssl pkcs8 -topk8 -inform PEM -outform DER -in ec_private.pem -out ec_private.der -nocrypt 46 | openssl x509 -in ec_cert.pem -outform DER -out ec_cert.der 47 | 48 | # Step 5: Use the Private Key and Certificate with xmlsec1 49 | xmlsec1 --sign --privkey-der ec_private.der,ec_cert.der --output response_signed_by_idp_ecdsa.xml --id-attr:ID Response response_signed_template.xml 50 | ``` 51 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/error.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! XmlSec High Level Error handling 3 | //! 4 | 5 | /// Wrapper project-wide Result typealias. 6 | pub type XmlSecResult = Result; 7 | 8 | /// Wrapper project-wide Errors enumeration. 9 | #[allow(missing_docs)] 10 | #[derive(Debug)] 11 | pub enum XmlSecError { 12 | XmlSecAbiMismatch, 13 | XmlSecInitError, 14 | ContextInitError, 15 | CryptoInitOpenSSLError, 16 | CryptoInitOpenSSLAppError, 17 | #[cfg(xmlsec_dynamic)] 18 | CryptoLoadLibraryError, 19 | 20 | InvalidInputString, 21 | 22 | SetKeyError, 23 | KeyNotLoaded, 24 | KeyLoadError, 25 | CertLoadError, 26 | 27 | RootNotFound, 28 | NodeNotFound, 29 | NotASignatureNode, 30 | 31 | SigningError, 32 | VerifyError, 33 | } 34 | 35 | impl std::fmt::Display for XmlSecError { 36 | fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { 37 | match self { 38 | Self::XmlSecInitError => write!(fmt, "Internal XmlSec Init Error"), 39 | Self::XmlSecAbiMismatch => write!(fmt, "XmlSec ABI version mismatch"), 40 | Self::CryptoInitOpenSSLError => { 41 | write!(fmt, "Internal XmlSec Crypto OpenSSL Init Error") 42 | } 43 | Self::CryptoInitOpenSSLAppError => { 44 | write!(fmt, "Internal XmlSec Crypto OpenSSLApp Init Error") 45 | } 46 | #[cfg(xmlsec_dynamic)] 47 | Self::CryptoLoadLibraryError => { 48 | write!(fmt, "XmlSec failed to load default crypto backend") 49 | } 50 | Self::ContextInitError => write!(fmt, "Internal XmlSec Context Error"), 51 | 52 | Self::InvalidInputString => write!(fmt, "Input value is not a valid string"), 53 | 54 | Self::SetKeyError => write!(fmt, "Key could not be set"), 55 | Self::KeyNotLoaded => write!(fmt, "Key has not yet been loaded and is required"), 56 | Self::KeyLoadError => write!(fmt, "Failed to load key"), 57 | Self::CertLoadError => write!(fmt, "Failed to load certificate"), 58 | 59 | Self::RootNotFound => write!(fmt, "Failed to find document root"), 60 | Self::NodeNotFound => write!(fmt, "Failed to find node"), 61 | Self::NotASignatureNode => write!(fmt, "Node is not a signature node"), 62 | 63 | Self::SigningError => { 64 | write!(fmt, "An error has ocurred while attemting to sign document") 65 | } 66 | Self::VerifyError => write!(fmt, "Verification failed"), 67 | } 68 | } 69 | } 70 | 71 | impl std::error::Error for XmlSecError { 72 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 73 | None 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test_vectors/response.xml: -------------------------------------------------------------------------------- 1 | 2 | http://idp.example.com/metadata.php 3 | 4 | 5 | 6 | 7 | http://idp.example.com/metadata.php 8 | 9 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 10 | 11 | 12 | 13 | 14 | 15 | 16 | http://sp.example.com/demo1/metadata.php 17 | 18 | 19 | 20 | 21 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 22 | 23 | 24 | 25 | 26 | test 27 | 28 | 29 | test@example.com 30 | 31 | 32 | users 33 | examplerole1 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/metadata/endpoint.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::io::Cursor; 5 | 6 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 7 | pub struct Endpoint { 8 | #[serde(rename = "@Binding")] 9 | pub binding: String, 10 | #[serde(rename = "@Location")] 11 | pub location: String, 12 | #[serde(rename = "@ResponseLocation")] 13 | pub response_location: Option, 14 | } 15 | 16 | impl Endpoint { 17 | pub fn to_xml(&self, element_name: &str) -> Result> { 18 | let mut write_buf = Vec::new(); 19 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 20 | let mut root = BytesStart::new(element_name); 21 | root.push_attribute(("Binding", self.binding.as_ref())); 22 | root.push_attribute(("Location", self.location.as_ref())); 23 | if let Some(response_location) = &self.response_location { 24 | root.push_attribute(("ResponseLocation", response_location.as_ref())); 25 | } 26 | writer.write_event(Event::Start(root))?; 27 | writer.write_event(Event::End(BytesEnd::new(element_name)))?; 28 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 29 | write_buf, 30 | )?))) 31 | } 32 | } 33 | 34 | #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] 35 | pub struct IndexedEndpoint { 36 | #[serde(rename = "@Binding")] 37 | pub binding: String, 38 | #[serde(rename = "@Location")] 39 | pub location: String, 40 | #[serde(rename = "@ResponseLocation")] 41 | pub response_location: Option, 42 | #[serde(rename = "@index")] 43 | pub index: usize, 44 | #[serde(rename = "@isDefault")] 45 | pub is_default: Option, 46 | } 47 | 48 | impl IndexedEndpoint { 49 | pub fn to_xml(&self, element_name: &str) -> Result> { 50 | let mut write_buf = Vec::new(); 51 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 52 | let mut root = BytesStart::new(element_name); 53 | root.push_attribute(("Binding", self.binding.as_ref())); 54 | root.push_attribute(("Location", self.location.as_ref())); 55 | root.push_attribute(("index", self.index.to_string().as_ref())); 56 | if let Some(response_location) = &self.response_location { 57 | root.push_attribute(("ResponseLocation", response_location.as_ref())); 58 | } 59 | if let Some(is_default) = &self.is_default { 60 | root.push_attribute(("isDefault", is_default.to_string().as_ref())); 61 | } 62 | 63 | writer.write_event(Event::Start(root))?; 64 | writer.write_event(Event::End(BytesEnd::new(element_name)))?; 65 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 66 | write_buf, 67 | )?))) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bindings.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! XmlSec Bindings Generation 3 | //! 4 | use bindgen::Builder as BindgenBuilder; 5 | 6 | use pkg_config::Config as PkgConfig; 7 | 8 | use std::env; 9 | use std::path::PathBuf; 10 | use std::process::Command; 11 | 12 | const BINDINGS: &str = "xmlsec_bindings.rs"; 13 | 14 | fn main() { 15 | // Tell the compiler about our custom cfg flags 16 | println!("cargo:rustc-check-cfg=cfg(xmlsec_dynamic)"); 17 | println!("cargo:rustc-check-cfg=cfg(xmlsec_static)"); 18 | 19 | if env::var_os("CARGO_FEATURE_XMLSEC").is_some() { 20 | let path_out = PathBuf::from(env::var("OUT_DIR").unwrap()); 21 | let path_bindings = path_out.join(BINDINGS); 22 | 23 | // Determine which API/ABI is available on this platform: 24 | let cflags = fetch_xmlsec_config_flags(); 25 | let dynamic = if cflags 26 | .iter() 27 | .any(|s| s == "-DXMLSEC_CRYPTO_DYNAMIC_LOADING=1") 28 | { 29 | println!("cargo:rustc-cfg=xmlsec_dynamic"); 30 | true 31 | } else { 32 | println!("cargo:rustc-cfg=xmlsec_static"); 33 | false 34 | }; 35 | 36 | if !dynamic { 37 | println!("cargo:rustc-link-lib=xmlsec1-openssl"); // -lxmlsec1-openssl 38 | } 39 | println!("cargo:rustc-link-lib=xmlsec1"); // -lxmlsec1 40 | println!("cargo:rustc-link-lib=xml2"); // -lxml2 41 | println!("cargo:rustc-link-lib=ssl"); // -lssl 42 | println!("cargo:rustc-link-lib=crypto"); // -lcrypto 43 | 44 | if !path_bindings.exists() { 45 | PkgConfig::new() 46 | .probe("xmlsec1") 47 | .expect("Could not find xmlsec1 using pkg-config"); 48 | 49 | let bindbuild = BindgenBuilder::default() 50 | .header("bindings.h") 51 | .clang_args(cflags) 52 | .clang_args(fetch_xmlsec_config_libs()) 53 | .layout_tests(true) 54 | .generate_comments(true); 55 | 56 | let bindings = bindbuild.generate().expect("Unable to generate bindings"); 57 | 58 | bindings 59 | .write_to_file(path_bindings) 60 | .expect("Couldn't write bindings!"); 61 | } 62 | } 63 | } 64 | 65 | fn fetch_xmlsec_config_flags() -> Vec { 66 | let out = Command::new("xmlsec1-config") 67 | .arg("--cflags") 68 | .output() 69 | .expect("Failed to get --cflags from xmlsec1-config. Is xmlsec1 installed?") 70 | .stdout; 71 | 72 | args_from_output(out) 73 | } 74 | 75 | fn fetch_xmlsec_config_libs() -> Vec { 76 | let out = Command::new("xmlsec1-config") 77 | .arg("--libs") 78 | .output() 79 | .expect("Failed to get --libs from xmlsec1-config. Is xmlsec1 installed?") 80 | .stdout; 81 | 82 | args_from_output(out) 83 | } 84 | 85 | fn args_from_output(args: Vec) -> Vec { 86 | let decoded = String::from_utf8(args).expect("Got invalid UTF8 from xmlsec1-config"); 87 | 88 | decoded.split_whitespace().map(|p| p.to_owned()).collect() 89 | } 90 | -------------------------------------------------------------------------------- /src/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | mod cert_encoding; 2 | mod crypto_disabled; 3 | mod ids; 4 | mod url_verification; 5 | #[cfg(feature = "xmlsec")] 6 | mod xmlsec; 7 | 8 | use crate::schema::CipherValue; 9 | pub use cert_encoding::*; 10 | pub use ids::*; 11 | use thiserror::Error; 12 | pub use url_verification::{UrlVerifier, UrlVerifierError, sign_url}; 13 | #[cfg(feature = "xmlsec")] 14 | pub use xmlsec::*; 15 | 16 | #[cfg(feature = "xmlsec")] 17 | pub type Crypto = XmlSec; 18 | #[cfg(not(feature = "xmlsec"))] 19 | pub type Crypto = crypto_disabled::NoCrypto; 20 | 21 | #[derive(Debug, Error)] 22 | pub enum CryptoError { 23 | #[error("Encountered an invalid signature")] 24 | InvalidSignature, 25 | 26 | #[error("base64 decoding Error: {}", error)] 27 | Base64Error { 28 | #[from] 29 | error: base64::DecodeError, 30 | }, 31 | 32 | #[error("The given XML is missing a root element")] 33 | XmlMissingRootElement, 34 | 35 | #[error("Crypto Provider Error")] 36 | CryptoProviderError(#[source] Box), 37 | 38 | #[error("The encryption method {method} is not supported for the assertion key")] 39 | EncryptedAssertionKeyMethodUnsupported { method: String }, 40 | 41 | #[error("The encryption method {method} is not supported for the assertion value")] 42 | EncryptedAssertionValueMethodUnsupported { method: String }, 43 | 44 | #[error("The crypto provider is not enabled so encryption and signing methods are disabled")] 45 | CryptoDisabled, 46 | } 47 | 48 | /// A certificate encoded in der format. 49 | #[derive(Debug, Clone, PartialEq, Eq)] 50 | pub struct CertificateDer(Vec); 51 | 52 | impl CertificateDer { 53 | pub fn der_data(&self) -> &[u8] { 54 | &self.0 55 | } 56 | } 57 | 58 | impl From> for CertificateDer { 59 | fn from(cert_der: Vec) -> Self { 60 | Self(cert_der) 61 | } 62 | } 63 | 64 | pub trait CryptoProvider { 65 | type PrivateKey; 66 | 67 | fn verify_signed_xml>( 68 | xml: Bytes, 69 | x509_cert_der: &CertificateDer, 70 | id_attribute: Option<&str>, 71 | ) -> Result<(), CryptoError>; 72 | 73 | /// Takes an XML document, parses it, verifies all XML digital signatures against the given 74 | /// certificates, and returns a derived version of the document where all elements that are not 75 | /// covered by a digital signature have been removed. 76 | fn reduce_xml_to_signed( 77 | xml_str: &str, 78 | certs_der: &[CertificateDer], 79 | ) -> Result; 80 | 81 | fn decrypt_assertion_key_info( 82 | cipher_value: &CipherValue, 83 | method: &str, 84 | decryption_key: &Self::PrivateKey, 85 | ) -> Result, CryptoError>; 86 | 87 | fn decrypt_assertion_value_info( 88 | cipher_value: &CipherValue, 89 | method: &str, 90 | decryption_key: &[u8], 91 | ) -> Result, CryptoError>; 92 | 93 | fn sign_xml>( 94 | xml: Bytes, 95 | private_key_der: &[u8], 96 | ) -> Result; 97 | } 98 | -------------------------------------------------------------------------------- /src/metadata/contact_person.rs: -------------------------------------------------------------------------------- 1 | use crate::metadata::helpers::write_plain_element; 2 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 3 | use quick_xml::Writer; 4 | use serde::Deserialize; 5 | use std::io::Cursor; 6 | 7 | const NAME: &str = "md:ContactPerson"; 8 | 9 | pub enum ContactType { 10 | Technical, 11 | Support, 12 | Administrative, 13 | Billing, 14 | Other, 15 | } 16 | 17 | impl ContactType { 18 | pub fn value(&self) -> &'static str { 19 | match self { 20 | ContactType::Technical => "technical", 21 | ContactType::Support => "support", 22 | ContactType::Administrative => "administrative", 23 | ContactType::Billing => "billing", 24 | ContactType::Other => "other", 25 | } 26 | } 27 | } 28 | 29 | #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] 30 | pub struct ContactPerson { 31 | #[serde(rename = "@contactType")] 32 | pub contact_type: Option, 33 | #[serde(rename = "Company")] 34 | pub company: Option, 35 | #[serde(rename = "GivenName")] 36 | pub given_name: Option, 37 | #[serde(rename = "SurName")] 38 | pub sur_name: Option, 39 | #[serde(rename = "EmailAddress")] 40 | pub email_addresses: Option>, 41 | #[serde(rename = "TelephoneNumber")] 42 | pub telephone_numbers: Option>, 43 | } 44 | 45 | impl TryFrom for Event<'_> { 46 | type Error = Box; 47 | 48 | fn try_from(value: ContactPerson) -> Result { 49 | (&value).try_into() 50 | } 51 | } 52 | 53 | impl TryFrom<&ContactPerson> for Event<'_> { 54 | type Error = Box; 55 | 56 | fn try_from(value: &ContactPerson) -> Result { 57 | let mut write_buf = Vec::new(); 58 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 59 | let mut root = BytesStart::new(NAME); 60 | if let Some(contact_type) = &value.contact_type { 61 | root.push_attribute(("contactType", contact_type.as_ref())) 62 | } 63 | writer.write_event(Event::Start(root))?; 64 | 65 | value 66 | .company 67 | .as_ref() 68 | .map(|company| write_plain_element(&mut writer, "md:Company", company)); 69 | value 70 | .sur_name 71 | .as_ref() 72 | .map(|sur_name| write_plain_element(&mut writer, "md:SurName", sur_name)); 73 | value 74 | .given_name 75 | .as_ref() 76 | .map(|given_name| write_plain_element(&mut writer, "md:GivenName", given_name)); 77 | 78 | if let Some(email_addresses) = &value.email_addresses { 79 | for email in email_addresses { 80 | write_plain_element(&mut writer, "md:EmailAddress", email)?; 81 | } 82 | } 83 | 84 | if let Some(telephone_numbers) = &value.telephone_numbers { 85 | for number in telephone_numbers { 86 | write_plain_element(&mut writer, "md:TelephoneNumber", number)?; 87 | } 88 | } 89 | 90 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 91 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 92 | write_buf, 93 | )?))) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test_vectors/idp_metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 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= 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Example.org Non-Profit Org 23 | Example.org 24 | https://www.example.org/ 25 | 26 | 27 | SAML Technical Support 28 | mailto:technical-support@example.org 29 | 30 | 31 | SAML Support 32 | mailto:support@example.org 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/idp/sp_extractor.rs: -------------------------------------------------------------------------------- 1 | use super::error::Error; 2 | use crate::crypto; 3 | use crate::crypto::CertificateDer; 4 | use crate::metadata::EntityDescriptor; 5 | 6 | pub struct SPMetadataExtractor(EntityDescriptor); 7 | 8 | pub struct RequiredAttribute { 9 | pub name: String, 10 | pub format: Option, 11 | } 12 | 13 | pub struct Acs { 14 | pub bind_type: BindType, 15 | pub url: String, 16 | } 17 | 18 | pub enum BindType { 19 | Post, 20 | } 21 | 22 | impl SPMetadataExtractor { 23 | pub fn try_from_xml(xml: &str) -> Result> { 24 | Ok(Self(xml.parse()?)) 25 | } 26 | 27 | pub fn issuer(&self) -> Result { 28 | self.0.entity_id.clone().ok_or(Error::MissingAudience) 29 | } 30 | 31 | pub fn acs(&self) -> Result { 32 | let (binding, location) = self 33 | .0 34 | .sp_sso_descriptors 35 | .as_ref() 36 | .and_then(|d| d.first()) 37 | .and_then(|sd| sd.assertion_consumer_services.first()) 38 | .map(|acs| (acs.binding.as_str(), acs.location.as_str())) 39 | .ok_or(Error::MissingAcsUrl)?; 40 | 41 | if binding != "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" { 42 | return Err(Error::NonHttpPostBindingUnsupported); 43 | } 44 | 45 | Ok(Acs { 46 | bind_type: BindType::Post, 47 | url: location.to_string(), 48 | }) 49 | } 50 | 51 | pub fn required_attributes(&self) -> Vec { 52 | self.0 53 | .sp_sso_descriptors 54 | .as_ref() 55 | .and_then(|d| d.first()) 56 | .and_then(|sd| sd.attribute_consuming_services.as_ref()) 57 | .and_then(|s| s.first()) 58 | .map(|acs| { 59 | acs.request_attributes 60 | .iter() 61 | .filter(|ra| ra.is_required == Some(true)) 62 | .map(|ra| RequiredAttribute { 63 | name: ra.name.clone(), 64 | format: ra.name_format.clone(), 65 | }) 66 | .collect() 67 | }) 68 | .unwrap_or_default() 69 | } 70 | 71 | pub fn verification_cert(&self) -> Result> { 72 | let sp_descriptors = self 73 | .0 74 | .sp_sso_descriptors 75 | .as_ref() 76 | .ok_or(Error::NoSPSsoDescriptors)?; 77 | 78 | for sp_descriptor in sp_descriptors { 79 | match sp_descriptor.key_descriptors.as_ref() { 80 | Some(kd) => { 81 | // grab the first signing key 82 | let data = kd 83 | .iter() 84 | .filter(|d| d.is_signing()) 85 | .flat_map(|d| { 86 | d.key_info 87 | .x509_data 88 | .iter() 89 | .flat_map(|d| d.certificates.iter()) 90 | }) 91 | .next() 92 | .ok_or(Error::NoCertificate)?; 93 | 94 | return Ok(crypto::decode_x509_cert(data.as_str())?); 95 | } 96 | None => continue, 97 | }; 98 | } 99 | 100 | Err(Error::NoCertificate.into()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test_vectors/authn_request.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://sp.example.com/demo1/metadata.php 4 | 5 | 6 | aGWxn+1lDhMOeR+JZrBE7Q84eOY=y/OSHEPXWYSNX7HkUmgm0/E/dcmgE9pSRR3hN/ZmM6PscTpnk5lFz11RsrsI8p4SgVC082nVFY3PlOV8izFowBcWwloQJbpunmw6iDsirNfWRcRUhFXOtmv7EBbjB9axoj35Gzq0INxSgXrEIcnyT4V3a6NFiKtPXLxk/FnldzqgB/BT9cB5cCKRfnxs2BIrJKH86iFXYAxBkB1ZLLYmYeTeexSJVYSRGTuHUXyAO+MgEJk6tpOOlBjpxnZhCY45MAoWvqoRt2g30fJccvy3hjsPuV0K3jhDGoCwl0fsFBXw/hcraLdbmGfsTdZZ0b8Ctc+JugeCPRQLAm+GytxZRjtzEDKS8TIAvvcGlm8rtkspQsYzPDS5dOKFBqaAPEoMXYBmFONsQ+Ac1N0ypPxXpH/sOaGNSGHy+HsSjkN8QBs5PwvhRAnrQ1gIEyQb89HN/WyHpM6mR6WFQ30chK66iD4t0ruZYtaxoJFOc19f2W2NRbkc02MIMBVb18mjsjtm 7 | MIIEQDCCAqgCCQDisA7Xfmj+5jANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGV0czEXMBUGA1UEAwwOc3AuZXhhbXBsZS5jb20wHhcNMjAwMzA4MjI1NjQ3WhcNMzAwMzA2MjI1NjQ3WjBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGV0czEXMBUGA1UEAwwOc3AuZXhhbXBsZS5jb20wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDYzvlEAZMce+J4j2YU++08SoTlKzrqKHOeV091yBfuemYlTi/Za01jRkP7fnSpaNx4R1hDtlAUP4thjjQQl3TEhFKrrug8NEzagPXphG6DELisDRE1/jXDeB/NFASrnflJ2IdkQzIUoNwMQfOmj27YKZ5eN06kjg1caTHSb7b8t7OMZYqf92PFwe8UmaUhBbyfGEFnHTwsLlLop7Qz6Ax8d/GSRagcz16Pl/5bW22pQS6EZ+XS5+71urjN90FiN+SnOnpxO3NMiwZLeQxVeeDW1s+Z7zp1uviiMDWcYp/GQroGGREsmXCS/tk/hYepmbKJGvjA4y2gIamvUwjuuuPvCIcnMiDur/KyQz5wPlUvc+FHXhJmjqOjyt+v3tbmlTRrd/OLU7kfWpN/KhKfUv/RtCxp8YWI0hor5FrAOie1xtTIMrXNSSrVSPBlO000BEQ3JMQytJWH/uHFv9KwGVMxBoN+PVGA2Si9oXxvBUTQ3G05V1cc2z8CwGDCWxo/TiMCAwEAATANBgkqhkiG9w0BAQsFAAOCAYEAxmOjjScYv8wGY5WOtVjtEa3fg9z8i4cDrWyJKOgsBbZLUj+DF4TtuEnQkAG4P6r953t1/L+BTUbA3E/7Mj+O2NJs33Er1McL1gw4uclk+gFtL5yEc5BLLgeXOGlnWyuRlTYZb40wDxS7QJoBM/rSXvGTuj4JhMuGTQTUFZ0P889OOPj8FxYOVHVfXLhWR9+17ip18ag/RCG6fJbd95OLV21F1E90yUIb8crEroLI+G0IAuvStqqWYHXqD/ONywvE/qun3CKqsoj+l5k6YCY/gwr936JZexviokhLS2o0gWppCtsxyQTGMJivK2sbvq60RNGvWHkOuVQ2XxGYgMFCgJFQwlD8UBiRdJ4T60i9N3DwwX3U2KKa5eskglvykQClHMrSlhYhyfoNdsDEt68ywyhltT+Q4Rqo09DRx7SbsSSqSpS0H2zxvYEY7NNeS/k993pRgQGscjfRYCGUbrfq4o5nS9531TKzsi7QluJNsaXpEYsdT9R+bmgyD26Ew7FD 8 | 9 | 10 | urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport 11 | 12 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/keys.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Wrapper for XmlSec Key and Certificate management Context 3 | //! 4 | use crate::crypto::xmlsec::wrapper::bindings; 5 | 6 | use super::backend; 7 | use super::error::XmlSecError; 8 | use super::error::XmlSecResult; 9 | use super::xmlsec_internal; 10 | 11 | use std::ptr::null; 12 | use std::ptr::null_mut; 13 | 14 | /// x509 key format. 15 | #[allow(dead_code)] 16 | #[allow(missing_docs)] 17 | #[repr(u32)] 18 | pub enum XmlSecKeyFormat { 19 | Unknown = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatUnknown, 20 | Binary = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatBinary, 21 | Pem = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatPem, 22 | Der = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatDer, 23 | Pkcs8Pem = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatPkcs8Pem, 24 | Pkcs8Der = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatPkcs8Der, 25 | Pkcs12 = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatPkcs12, 26 | CertPem = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatCertPem, 27 | CertDer = bindings::xmlSecKeyDataFormat_xmlSecKeyDataFormatCertDer, 28 | } 29 | 30 | /// Key with which we sign/verify signatures or encrypt data. Used by [`XmlSecSignatureContext`][sigctx]. 31 | /// 32 | /// [sigctx]: struct.XmlSecSignatureContext.html 33 | #[derive(Debug)] 34 | pub struct XmlSecKey(*mut bindings::xmlSecKey); 35 | 36 | impl XmlSecKey { 37 | /// Load key from buffer in memory, specifying format and optionally the password required to decrypt/unlock. 38 | pub fn from_memory(buffer: &[u8], format: XmlSecKeyFormat) -> XmlSecResult { 39 | xmlsec_internal::guarantee_xmlsec_init()?; 40 | 41 | // Load key from buffer 42 | let key = unsafe { 43 | backend::xmlSecCryptoAppKeyLoadMemory( 44 | buffer.as_ptr(), 45 | buffer.len().try_into().expect("Key buffer length overflow"), 46 | format as u32, 47 | null(), 48 | null_mut(), 49 | null_mut(), 50 | ) 51 | }; 52 | 53 | if key.is_null() { 54 | return Err(XmlSecError::KeyLoadError); 55 | } 56 | 57 | Ok(Self(key)) 58 | } 59 | 60 | /// Create from raw pointer to an underlying wrapper key structure. Henceforth its lifetime will be managed by this 61 | /// object. 62 | pub unsafe fn from_ptr(ptr: *mut bindings::xmlSecKey) -> Self { 63 | Self(ptr) 64 | } 65 | 66 | /// Leak the internal resource. This is needed by [`XmlSecSignatureContext`][sigctx], since wrapper takes over the 67 | /// lifetime management of the underlying resource when setting it as the active key for signature signing or 68 | /// verification. 69 | /// 70 | /// [sigctx]: struct.XmlSecSignatureContext.html 71 | pub unsafe fn leak(key: Self) -> *mut bindings::xmlSecKey { 72 | let ptr = key.0; 73 | 74 | std::mem::forget(key); 75 | 76 | ptr 77 | } 78 | } 79 | 80 | impl PartialEq for XmlSecKey { 81 | fn eq(&self, other: &Self) -> bool { 82 | self.0 == other.0 // compare pointer addresses 83 | } 84 | } 85 | 86 | impl Eq for XmlSecKey {} 87 | 88 | impl Clone for XmlSecKey { 89 | fn clone(&self) -> Self { 90 | let new = unsafe { bindings::xmlSecKeyDuplicate(self.0) }; 91 | 92 | Self(new) 93 | } 94 | } 95 | 96 | impl Drop for XmlSecKey { 97 | fn drop(&mut self) { 98 | unsafe { bindings::xmlSecKeyDestroy(self.0) }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/idp/verified_request.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::Event; 2 | 3 | use crate::crypto::{decode_x509_cert, Crypto, CryptoProvider, CertificateDer}; 4 | use crate::schema::AuthnRequest; 5 | 6 | use super::error::Error; 7 | 8 | pub struct UnverifiedAuthnRequest<'a> { 9 | pub request: AuthnRequest, 10 | xml: &'a str, 11 | } 12 | 13 | impl<'a> UnverifiedAuthnRequest<'a> { 14 | pub fn from_xml(xml: &str) -> Result { 15 | Ok(UnverifiedAuthnRequest { 16 | request: xml.parse()?, 17 | xml, 18 | }) 19 | } 20 | 21 | fn get_certs_der(&self) -> Result, Error> { 22 | let x509_certs = self 23 | .request 24 | .signature 25 | .as_ref() 26 | .ok_or(Error::NoSignature)? 27 | .key_info 28 | .as_ref() 29 | .map(|ki| ki.iter()) 30 | .ok_or(Error::NoKeyInfo)? 31 | .flat_map(|d| d.x509_data.as_ref()) 32 | .flat_map(|d| d.certificates.iter()) 33 | .map(|cert| decode_x509_cert(cert)) 34 | .collect::, _>>() 35 | .map_err(|_| Error::InvalidCertificateEncoding)?; 36 | 37 | if x509_certs.is_empty() { 38 | return Err(Error::NoCertificate); 39 | } 40 | 41 | Ok(x509_certs) 42 | } 43 | 44 | pub fn try_verify_self_signed(self) -> Result { 45 | let xml = self.xml.as_bytes(); 46 | self.get_certs_der()? 47 | .into_iter() 48 | .map(|der_cert| Ok(Crypto::verify_signed_xml(xml, &der_cert, Some("ID"))?)) 49 | .reduce(|a, b| a.or(b)) 50 | .ok_or(Error::UnexpectedError)? 51 | .map(|()| VerifiedAuthnRequest(self.request)) 52 | } 53 | 54 | pub fn try_verify_with_cert(self, der_cert: &CertificateDer) -> Result { 55 | Crypto::verify_signed_xml(self.xml.as_bytes(), der_cert, Some("ID"))?; 56 | Ok(VerifiedAuthnRequest(self.request)) 57 | } 58 | } 59 | 60 | pub struct VerifiedAuthnRequest(AuthnRequest); 61 | 62 | impl std::ops::Deref for VerifiedAuthnRequest { 63 | type Target = AuthnRequest; 64 | fn deref(&self) -> &AuthnRequest { 65 | &self.0 66 | } 67 | } 68 | 69 | impl TryFrom for Event<'_> { 70 | type Error = Box; 71 | 72 | fn try_from(value: VerifiedAuthnRequest) -> Result { 73 | (&value).try_into() 74 | } 75 | } 76 | 77 | impl TryFrom<&VerifiedAuthnRequest> for Event<'_> { 78 | type Error = Box; 79 | 80 | fn try_from(value: &VerifiedAuthnRequest) -> Result { 81 | value.0.clone().try_into() 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod test { 87 | use super::UnverifiedAuthnRequest; 88 | use crate::traits::ToXml; 89 | 90 | #[test] 91 | fn test_request_deserialize_and_serialize() { 92 | let authn_request_xml = include_str!("../../test_vectors/authn_request.xml"); 93 | let unverified = 94 | UnverifiedAuthnRequest::from_xml(authn_request_xml).expect("failed to parse"); 95 | let expected_verified = unverified 96 | .try_verify_self_signed() 97 | .expect("failed to verify self signed signature"); 98 | let verified_request_xml = expected_verified 99 | .to_string() 100 | .expect("Failed to serialize verified authn request"); 101 | let reparsed_unverified = 102 | UnverifiedAuthnRequest::from_xml(&verified_request_xml).expect("failed to parse"); 103 | assert_eq!(reparsed_unverified.request, expected_verified.0); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/xmlsec_internal.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Central XmlSec1 Context 3 | //! 4 | use crate::crypto::xmlsec::wrapper::bindings; 5 | 6 | use lazy_static::lazy_static; 7 | 8 | use super::backend; 9 | use super::error::XmlSecError; 10 | use super::XmlSecResult; 11 | use std::convert::TryInto; 12 | use std::ptr::null; 13 | use std::sync::Mutex; 14 | 15 | lazy_static! { 16 | static ref XMLSEC: Mutex> = Mutex::new(None); 17 | } 18 | 19 | pub fn guarantee_xmlsec_init() -> XmlSecResult<()> { 20 | let mut inner = XMLSEC 21 | .lock() 22 | .expect("Unable to lock global wrapper initalization wrapper"); 23 | 24 | if inner.is_none() { 25 | *inner = Some(XmlSecContext::new()?); 26 | } 27 | 28 | Ok(()) 29 | } 30 | 31 | /// XmlSec Global Context 32 | /// 33 | /// This object initializes the underlying wrapper global state and cleans it 34 | /// up once gone out of scope. It is checked by all objects in the library that 35 | /// require the context to be initialized. See [`globals`][globals]. 36 | /// 37 | /// [globals]: globals 38 | pub struct XmlSecContext {} 39 | 40 | impl XmlSecContext { 41 | /// Runs wrapper initialization and returns instance of itself. 42 | pub fn new() -> XmlSecResult { 43 | unsafe { 44 | libxml::bindings::xmlInitParser(); 45 | } 46 | 47 | init_xmlsec()?; 48 | init_crypto_app()?; 49 | init_crypto()?; 50 | 51 | Ok(Self {}) 52 | } 53 | } 54 | 55 | impl Drop for XmlSecContext { 56 | fn drop(&mut self) { 57 | cleanup_crypto(); 58 | cleanup_crypto_app(); 59 | cleanup_xmlsec(); 60 | } 61 | } 62 | 63 | /// Init wrapper library 64 | fn init_xmlsec() -> XmlSecResult<()> { 65 | let rc = unsafe { 66 | bindings::xmlSecCheckVersionExt( 67 | bindings::XMLSEC_VERSION_MAJOR.try_into().unwrap(), 68 | bindings::XMLSEC_VERSION_MINOR.try_into().unwrap(), 69 | bindings::XMLSEC_VERSION_SUBMINOR.try_into().unwrap(), 70 | bindings::xmlSecCheckVersionMode_xmlSecCheckVersionABICompatible, 71 | ) 72 | }; 73 | 74 | if rc < 0 { 75 | return Err(XmlSecError::XmlSecAbiMismatch); 76 | } 77 | 78 | let rc = unsafe { bindings::xmlSecInit() }; 79 | 80 | if rc < 0 { 81 | Err(XmlSecError::XmlSecInitError) 82 | } else { 83 | Ok(()) 84 | } 85 | } 86 | 87 | /// Load default crypto engine if we are supporting dynamic loading for 88 | /// wrapper-crypto libraries. Use the crypto library name ("openssl", 89 | /// "nss", etc.) to load corresponding wrapper-crypto library. 90 | fn init_crypto_app() -> XmlSecResult<()> { 91 | #[cfg(xmlsec_dynamic)] 92 | { 93 | let rc = unsafe { backend::xmlSecCryptoDLLoadLibrary(null()) }; 94 | if rc < 0 { 95 | return Err(XmlSecError::CryptoLoadLibraryError); 96 | } 97 | } 98 | 99 | let rc = unsafe { backend::xmlSecCryptoAppInit(null()) }; 100 | 101 | if rc < 0 { 102 | Err(XmlSecError::CryptoInitOpenSSLAppError) 103 | } else { 104 | Ok(()) 105 | } 106 | } 107 | 108 | /// Init wrapper-crypto library 109 | fn init_crypto() -> XmlSecResult<()> { 110 | let rc = unsafe { backend::xmlSecCryptoInit() }; 111 | 112 | if rc < 0 { 113 | Err(XmlSecError::CryptoInitOpenSSLError) 114 | } else { 115 | Ok(()) 116 | } 117 | } 118 | 119 | /// Shutdown wrapper-crypto library 120 | fn cleanup_crypto() { 121 | unsafe { backend::xmlSecCryptoShutdown() }; 122 | } 123 | 124 | /// Shutdown crypto library 125 | fn cleanup_crypto_app() { 126 | unsafe { backend::xmlSecCryptoAppShutdown() }; 127 | } 128 | 129 | /// Shutdown wrapper library 130 | fn cleanup_xmlsec() { 131 | unsafe { bindings::xmlSecShutdown() }; 132 | } 133 | -------------------------------------------------------------------------------- /test_vectors/response_signed_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | https://fujifish.github.io/samling/samling.html 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | gciSu0u9H5QMP776LBbSg8ai9BM= 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | https://fujifish.github.io/samling/samling.html 33 | 34 | 36 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 37 | 38 | 41 | 42 | 43 | 44 | 45 | http://test_accept_signed_with_correct_key.test 46 | 47 | 48 | 51 | 52 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 53 | 54 | 55 | 56 | 57 | test 58 | 59 | 60 | test@example.com 61 | 62 | 64 | users 65 | examplerole1 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test_vectors/response_signed__ecdsa-template.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | https://fujifish.github.io/samling/samling.html 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | gciSu0u9H5QMP776LBbSg8ai9BM= 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | https://fujifish.github.io/samling/samling.html 33 | 34 | 36 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 37 | 38 | 41 | 42 | 43 | 44 | 45 | http://test_accept_signed_with_correct_key.test 46 | 47 | 48 | 51 | 52 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 53 | 54 | 55 | 56 | 57 | test 58 | 59 | 60 | test@example.com 61 | 62 | 64 | users 65 | examplerole1 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test_vectors/sp_metadata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MIIEQDCCAqgCCQDisA7Xfmj+5jANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGV0czEXMBUGA1UEAwwOc3AuZXhhbXBsZS5jb20wHhcNMjAwMzA4MjI1NjQ3WhcNMzAwMzA2MjI1NjQ3WjBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGV0czEXMBUGA1UEAwwOc3AuZXhhbXBsZS5jb20wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDYzvlEAZMce+J4j2YU++08SoTlKzrqKHOeV091yBfuemYlTi/Za01jRkP7fnSpaNx4R1hDtlAUP4thjjQQl3TEhFKrrug8NEzagPXphG6DELisDRE1/jXDeB/NFASrnflJ2IdkQzIUoNwMQfOmj27YKZ5eN06kjg1caTHSb7b8t7OMZYqf92PFwe8UmaUhBbyfGEFnHTwsLlLop7Qz6Ax8d/GSRagcz16Pl/5bW22pQS6EZ+XS5+71urjN90FiN+SnOnpxO3NMiwZLeQxVeeDW1s+Z7zp1uviiMDWcYp/GQroGGREsmXCS/tk/hYepmbKJGvjA4y2gIamvUwjuuuPvCIcnMiDur/KyQz5wPlUvc+FHXhJmjqOjyt+v3tbmlTRrd/OLU7kfWpN/KhKfUv/RtCxp8YWI0hor5FrAOie1xtTIMrXNSSrVSPBlO000BEQ3JMQytJWH/uHFv9KwGVMxBoN+PVGA2Si9oXxvBUTQ3G05V1cc2z8CwGDCWxo/TiMCAwEAATANBgkqhkiG9w0BAQsFAAOCAYEAxmOjjScYv8wGY5WOtVjtEa3fg9z8i4cDrWyJKOgsBbZLUj+DF4TtuEnQkAG4P6r953t1/L+BTUbA3E/7Mj+O2NJs33Er1McL1gw4uclk+gFtL5yEc5BLLgeXOGlnWyuRlTYZb40wDxS7QJoBM/rSXvGTuj4JhMuGTQTUFZ0P889OOPj8FxYOVHVfXLhWR9+17ip18ag/RCG6fJbd95OLV21F1E90yUIb8crEroLI+G0IAuvStqqWYHXqD/ONywvE/qun3CKqsoj+l5k6YCY/gwr936JZexviokhLS2o0gWppCtsxyQTGMJivK2sbvq60RNGvWHkOuVQ2XxGYgMFCgJFQwlD8UBiRdJ4T60i9N3DwwX3U2KKa5eskglvykQClHMrSlhYhyfoNdsDEt68ywyhltT+Q4Rqo09DRx7SbsSSqSpS0H2zxvYEY7NNeS/k993pRgQGscjfRYCGUbrfq4o5nS9531TKzsi7QluJNsaXpEYsdT9R+bmgyD26Ew7FD 8 | 9 | 10 | 11 | 12 | 13 | 14 | MIIEQDCCAqgCCQDisA7Xfmj+5jANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGV0czEXMBUGA1UEAwwOc3AuZXhhbXBsZS5jb20wHhcNMjAwMzA4MjI1NjQ3WhcNMzAwMzA2MjI1NjQ3WjBiMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGV0czEXMBUGA1UEAwwOc3AuZXhhbXBsZS5jb20wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDYzvlEAZMce+J4j2YU++08SoTlKzrqKHOeV091yBfuemYlTi/Za01jRkP7fnSpaNx4R1hDtlAUP4thjjQQl3TEhFKrrug8NEzagPXphG6DELisDRE1/jXDeB/NFASrnflJ2IdkQzIUoNwMQfOmj27YKZ5eN06kjg1caTHSb7b8t7OMZYqf92PFwe8UmaUhBbyfGEFnHTwsLlLop7Qz6Ax8d/GSRagcz16Pl/5bW22pQS6EZ+XS5+71urjN90FiN+SnOnpxO3NMiwZLeQxVeeDW1s+Z7zp1uviiMDWcYp/GQroGGREsmXCS/tk/hYepmbKJGvjA4y2gIamvUwjuuuPvCIcnMiDur/KyQz5wPlUvc+FHXhJmjqOjyt+v3tbmlTRrd/OLU7kfWpN/KhKfUv/RtCxp8YWI0hor5FrAOie1xtTIMrXNSSrVSPBlO000BEQ3JMQytJWH/uHFv9KwGVMxBoN+PVGA2Si9oXxvBUTQ3G05V1cc2z8CwGDCWxo/TiMCAwEAATANBgkqhkiG9w0BAQsFAAOCAYEAxmOjjScYv8wGY5WOtVjtEa3fg9z8i4cDrWyJKOgsBbZLUj+DF4TtuEnQkAG4P6r953t1/L+BTUbA3E/7Mj+O2NJs33Er1McL1gw4uclk+gFtL5yEc5BLLgeXOGlnWyuRlTYZb40wDxS7QJoBM/rSXvGTuj4JhMuGTQTUFZ0P889OOPj8FxYOVHVfXLhWR9+17ip18ag/RCG6fJbd95OLV21F1E90yUIb8crEroLI+G0IAuvStqqWYHXqD/ONywvE/qun3CKqsoj+l5k6YCY/gwr936JZexviokhLS2o0gWppCtsxyQTGMJivK2sbvq60RNGvWHkOuVQ2XxGYgMFCgJFQwlD8UBiRdJ4T60i9N3DwwX3U2KKa5eskglvykQClHMrSlhYhyfoNdsDEt68ywyhltT+Q4Rqo09DRx7SbsSSqSpS0H2zxvYEY7NNeS/k993pRgQGscjfRYCGUbrfq4o5nS9531TKzsi7QluJNsaXpEYsdT9R+bmgyD26Ew7FD 15 | 16 | 17 | 18 | urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified 19 | 20 | 21 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "advisory-db": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1735928634, 7 | "narHash": "sha256-Qg1vJOuEohAbdRmTTOLrbbGsyK9KRB54r3+aBuOMctM=", 8 | "owner": "rustsec", 9 | "repo": "advisory-db", 10 | "rev": "63a2f39924f66ca89cf5761f299a8a244fe02543", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "rustsec", 15 | "repo": "advisory-db", 16 | "type": "github" 17 | } 18 | }, 19 | "crane": { 20 | "locked": { 21 | "lastModified": 1736898272, 22 | "narHash": "sha256-D10wlrU/HCpSRcb3a7yk+bU3ggpMD1kGbseKtO+7teo=", 23 | "owner": "ipetkov", 24 | "repo": "crane", 25 | "rev": "6a589f034202a7c6e10bce6c5d1d392d7bc0f340", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "ipetkov", 30 | "repo": "crane", 31 | "type": "github" 32 | } 33 | }, 34 | "flake-utils": { 35 | "inputs": { 36 | "systems": "systems" 37 | }, 38 | "locked": { 39 | "lastModified": 1731533236, 40 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 41 | "owner": "numtide", 42 | "repo": "flake-utils", 43 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "type": "github" 50 | } 51 | }, 52 | "nix-filter": { 53 | "locked": { 54 | "lastModified": 1731533336, 55 | "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=", 56 | "owner": "numtide", 57 | "repo": "nix-filter", 58 | "rev": "f7653272fd234696ae94229839a99b73c9ab7de0", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "numtide", 63 | "repo": "nix-filter", 64 | "type": "github" 65 | } 66 | }, 67 | "nixpkgs": { 68 | "locked": { 69 | "lastModified": 1736867362, 70 | "narHash": "sha256-i/UJ5I7HoqmFMwZEH6vAvBxOrjjOJNU739lnZnhUln8=", 71 | "owner": "NixOS", 72 | "repo": "nixpkgs", 73 | "rev": "9c6b49aeac36e2ed73a8c472f1546f6d9cf1addc", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "NixOS", 78 | "ref": "nixos-24.11", 79 | "repo": "nixpkgs", 80 | "type": "github" 81 | } 82 | }, 83 | "root": { 84 | "inputs": { 85 | "advisory-db": "advisory-db", 86 | "crane": "crane", 87 | "flake-utils": "flake-utils", 88 | "nix-filter": "nix-filter", 89 | "nixpkgs": "nixpkgs", 90 | "rust-overlay": "rust-overlay" 91 | } 92 | }, 93 | "rust-overlay": { 94 | "inputs": { 95 | "nixpkgs": [ 96 | "nixpkgs" 97 | ] 98 | }, 99 | "locked": { 100 | "lastModified": 1736907983, 101 | "narHash": "sha256-fw55wVwpJW36Md2HZBKuxX3YHGeqsGsspPLtCMVr1Y8=", 102 | "owner": "oxalica", 103 | "repo": "rust-overlay", 104 | "rev": "eaa365c911441e07e387ff6acc596619fc50b156", 105 | "type": "github" 106 | }, 107 | "original": { 108 | "owner": "oxalica", 109 | "repo": "rust-overlay", 110 | "type": "github" 111 | } 112 | }, 113 | "systems": { 114 | "locked": { 115 | "lastModified": 1681028828, 116 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 117 | "owner": "nix-systems", 118 | "repo": "default", 119 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 120 | "type": "github" 121 | }, 122 | "original": { 123 | "owner": "nix-systems", 124 | "repo": "default", 125 | "type": "github" 126 | } 127 | } 128 | }, 129 | "root": "root", 130 | "version": 7 131 | } 132 | -------------------------------------------------------------------------------- /src/attribute.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::fmt::Debug; 5 | use std::io::Cursor; 6 | 7 | const ATTRIBUTE_VALUE_NAME: &str = "saml2:AttributeValue"; 8 | 9 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 10 | pub struct AttributeValue { 11 | #[serde(rename = "@xsi:type")] 12 | #[serde(alias = "@type")] 13 | pub attribute_type: Option, 14 | #[serde(rename = "$value")] 15 | pub value: Option, 16 | } 17 | 18 | impl AttributeValue { 19 | fn schema() -> Vec<(&'static str, &'static str)> { 20 | vec![ 21 | ("xmlns:xs", "http://www.w3.org/2001/XMLSchema"), 22 | ("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"), 23 | ] 24 | } 25 | } 26 | 27 | impl TryFrom for Event<'_> { 28 | type Error = Box; 29 | 30 | fn try_from(value: AttributeValue) -> Result { 31 | (&value).try_into() 32 | } 33 | } 34 | 35 | impl TryFrom<&AttributeValue> for Event<'_> { 36 | type Error = Box; 37 | 38 | fn try_from(value: &AttributeValue) -> Result { 39 | let mut write_buf = Vec::new(); 40 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 41 | let mut root = BytesStart::new(ATTRIBUTE_VALUE_NAME); 42 | 43 | for attr in AttributeValue::schema() { 44 | root.push_attribute(attr); 45 | } 46 | 47 | if let Some(typ) = &value.attribute_type { 48 | root.push_attribute(("xsi:type", typ.as_ref())); 49 | } 50 | 51 | writer.write_event(Event::Start(root))?; 52 | 53 | if let Some(value) = &value.value { 54 | writer.write_event(Event::Text(BytesText::from_escaped(value)))?; 55 | } 56 | 57 | writer.write_event(Event::End(BytesEnd::new(ATTRIBUTE_VALUE_NAME)))?; 58 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 59 | write_buf, 60 | )?))) 61 | } 62 | } 63 | 64 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 65 | pub struct Attribute { 66 | #[serde(rename = "@FriendlyName")] 67 | pub friendly_name: Option, 68 | #[serde(rename = "@Name")] 69 | pub name: Option, 70 | #[serde(rename = "@NameFormat")] 71 | pub name_format: Option, 72 | #[serde(rename = "AttributeValue", default)] 73 | pub values: Vec, 74 | } 75 | 76 | impl Attribute { 77 | fn name() -> &'static str { 78 | "saml2:Attribute" 79 | } 80 | } 81 | 82 | impl TryFrom for Event<'_> { 83 | type Error = Box; 84 | 85 | fn try_from(value: Attribute) -> Result { 86 | (&value).try_into() 87 | } 88 | } 89 | 90 | impl TryFrom<&Attribute> for Event<'_> { 91 | type Error = Box; 92 | 93 | fn try_from(value: &Attribute) -> Result { 94 | let mut write_buf = Vec::new(); 95 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 96 | let mut root = BytesStart::new(Attribute::name()); 97 | 98 | if let Some(name) = &value.name { 99 | root.push_attribute(("Name", name.as_ref())); 100 | } 101 | 102 | if let Some(format) = &value.name_format { 103 | root.push_attribute(("NameFormat", format.as_ref())); 104 | } 105 | 106 | if let Some(name) = &value.friendly_name { 107 | root.push_attribute(("FriendlyName", name.as_ref())); 108 | } 109 | 110 | writer.write_event(Event::Start(root))?; 111 | 112 | for val in &value.values { 113 | let event: Event<'_> = val.try_into()?; 114 | writer.write_event(event)?; 115 | } 116 | 117 | writer.write_event(Event::End(BytesEnd::new(Attribute::name())))?; 118 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 119 | write_buf, 120 | )?))) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/service_provider/tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod encrypted_assertion_tests { 3 | use crate::metadata::EntityDescriptor; 4 | use crate::service_provider::{Error, ServiceProvider, ServiceProviderBuilder}; 5 | use chrono::{Duration, Utc}; 6 | use openssl::pkey::PKey; 7 | 8 | // Helper function to create a service provider with a private key 9 | fn create_sp_with_private_key(key: PKey) -> ServiceProvider { 10 | // Create a service provider with the private key 11 | ServiceProviderBuilder::default() 12 | .idp_metadata(create_mock_idp()) 13 | .allow_idp_initiated(true) 14 | .key(key) 15 | .entity_id(Some("example".to_string())) // Set entity_id to match the audience requirement 16 | .max_clock_skew(Duration::days(365)) 17 | .max_issue_delay(Duration::days(365)) 18 | .build() 19 | .unwrap() 20 | } 21 | 22 | fn create_mock_idp() -> EntityDescriptor { 23 | EntityDescriptor { 24 | entity_id: Some("saml-mock".to_string()), 25 | valid_until: Some(Utc::now() + Duration::days(365)), 26 | ..Default::default() 27 | } 28 | } 29 | 30 | #[test] 31 | fn test_missing_private_key() { 32 | // Create a service provider without a private key 33 | let sp = ServiceProviderBuilder::default() 34 | .idp_metadata(create_mock_idp()) 35 | .max_clock_skew(Duration::days(365)) 36 | .max_issue_delay(Duration::days(365)) 37 | .build() 38 | .unwrap(); 39 | 40 | // Sample response with an encrypted assertion 41 | let response_xml = include_str!(concat!( 42 | env!("CARGO_MANIFEST_DIR"), 43 | "/test_vectors/response_encrypted.xml" 44 | )); 45 | 46 | // Attempt to parse the response 47 | let result = sp.parse_xml_response(response_xml, Some(&["example"])); 48 | 49 | // Verify that the correct error is returned 50 | assert_eq!( 51 | result.err().unwrap().to_string(), 52 | Error::MissingPrivateKeySP.to_string() 53 | ); 54 | } 55 | 56 | #[test] 57 | fn test_decrypt_assertion() { 58 | // In this test, we're primarily testing the decryption functionality, 59 | // so we won't perform full validation 60 | 61 | let pkey = include_bytes!(concat!( 62 | env!("CARGO_MANIFEST_DIR"), 63 | "/test_vectors/sp_private.pem" 64 | )); 65 | let key = PKey::private_key_from_pem(pkey).unwrap(); 66 | 67 | // Create a service provider with the private key but don't validate assertion 68 | let sp = create_sp_with_private_key(key); 69 | 70 | let response_xml = include_str!(concat!( 71 | env!("CARGO_MANIFEST_DIR"), 72 | "/test_vectors/response_encrypted.xml" 73 | )); 74 | 75 | // Extract encrypted assertion directly to test decryption functionality 76 | // without validation 77 | let response: crate::schema::Response = response_xml.parse().unwrap(); 78 | 79 | // Verify that an encrypted assertion is present 80 | assert!(response.encrypted_assertion.is_some()); 81 | 82 | // Directly decrypt the assertion without validation 83 | let result = response 84 | .encrypted_assertion 85 | .unwrap() 86 | .decrypt(&sp.key.unwrap()); 87 | 88 | assert!(result.is_ok()); 89 | } 90 | 91 | #[test] 92 | fn test_decrypt_and_validate_assertion() { 93 | let pkey = include_bytes!(concat!( 94 | env!("CARGO_MANIFEST_DIR"), 95 | "/test_vectors/sp_private.pem" 96 | )); 97 | let key = PKey::private_key_from_pem(pkey).unwrap(); 98 | 99 | let sp = create_sp_with_private_key(key); 100 | 101 | let response_xml = include_str!(concat!( 102 | env!("CARGO_MANIFEST_DIR"), 103 | "/test_vectors/response_encrypted.xml" 104 | )); 105 | 106 | let result = sp.parse_xml_response(response_xml, Some(&["example"])); 107 | 108 | println!("Result: {:?}", result); 109 | 110 | assert!(result.is_ok()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test_vectors/response_signed_by_idp_ecdsa.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://fujifish.github.io/samling/samling.html 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | W7iYqYBNLg7dS+ueqLf04nO5V+c= 15 | 16 | 17 | THCZWgdX01bDRNyUHHS+u3U7URTI4c3+1cuXKeWFQDjX/yjrC6V/6wCwXtD4VyjU 18 | aUxevxscW8FBCRTkwDR78A== 19 | 20 | 21 | MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ 22 | BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l 23 | dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 24 | NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK 25 | DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB 26 | BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc 27 | W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI 28 | qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I 29 | UevJyxGd/2RkolE= 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | https://fujifish.github.io/samling/samling.html 38 | 39 | 40 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 41 | 42 | 43 | 44 | 45 | 46 | 47 | http://test_accept_signed_with_correct_key.test 48 | 49 | 50 | 51 | 52 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 53 | 54 | 55 | 56 | 57 | test 58 | 59 | 60 | test@example.com 61 | 62 | 63 | users 64 | examplerole1 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /test_vectors/response_signed_assertion.xml: -------------------------------------------------------------------------------- 1 | 2 | http://idp.example.com/metadata.php 3 | 4 | 5 | 6 | 7 | http://idp.example.com/metadata.php 8 | 9 | 10 | YmjY1aj24539ZQO/Evpm0IosNGM=uUsyh4JxV9ow9h3hxt5FOh0ah0aHOoIZPhelbjLC/zKw58o7tucSg5nf8ZfY48pao1+A6O/X4HTfi/IeYWrTtuxBdTR5Y/Lb8YAQIw8KUV/+8ijFS9E4HtlBjJSi0rmeMgRWBvdb90p+t9TP5PjKfdFI5USY6Bt6FvlrzybgTvk= 11 | MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== 12 | 13 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 14 | 15 | 16 | 17 | 18 | 19 | 20 | http://sp.example.com/demo1/metadata.php 21 | 22 | 23 | 24 | 25 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 26 | 27 | 28 | 29 | 30 | test 31 | 32 | 33 | test@example.com 34 | 35 | 36 | users 37 | examplerole1 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test_vectors/response_signed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://idp.example.com/metadata.php 4 | 5 | 6 | gciSu0u9H5QMP776LBbSg8ai9BM=d8fP51sHmS+BNEmlXVqkJ2ZMTdBeeYlfUL3IUVXKv4xgsHbmbYPUjXFFwNyUDt5SMK/lo70NVlVyfmLGCHByah1v4en10WPU+WOj6BypxOVi/BRZrY1Dj1CKL9ro1Q2fUFnHu33MikADpwxCbxjpsUxPcDjX8sNrVvswbab6cxc= 7 | MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg== 8 | 9 | 10 | 11 | 12 | http://idp.example.com/metadata.php 13 | 14 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 15 | 16 | 17 | 18 | 19 | 20 | 21 | http://sp.example.com/demo1/metadata.php 22 | 23 | 24 | 25 | 26 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 27 | 28 | 29 | 30 | 31 | test 32 | 33 | 34 | test@example.com 35 | 36 | 37 | users 38 | examplerole1 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/metadata/attribute_consuming_service.rs: -------------------------------------------------------------------------------- 1 | use crate::attribute::AttributeValue; 2 | use crate::metadata::LocalizedName; 3 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 4 | use quick_xml::Writer; 5 | use serde::Deserialize; 6 | use std::io::Cursor; 7 | 8 | const NAME: &str = "md:AttributeConsumingService"; 9 | 10 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 11 | pub struct AttributeConsumingService { 12 | #[serde(rename = "@index")] 13 | pub index: usize, 14 | #[serde(rename = "@isDefault")] 15 | pub is_default: Option, 16 | #[serde(rename = "ServiceName", default)] 17 | pub service_names: Vec, 18 | #[serde(rename = "ServiceDescription")] 19 | pub service_descriptions: Option>, 20 | #[serde(rename = "RequestedAttribute", default)] 21 | pub request_attributes: Vec, 22 | } 23 | 24 | impl TryFrom for Event<'_> { 25 | type Error = Box; 26 | 27 | fn try_from(value: AttributeConsumingService) -> Result { 28 | (&value).try_into() 29 | } 30 | } 31 | 32 | impl TryFrom<&AttributeConsumingService> for Event<'_> { 33 | type Error = Box; 34 | 35 | fn try_from(value: &AttributeConsumingService) -> Result { 36 | let mut write_buf = Vec::new(); 37 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 38 | let mut root = BytesStart::new(NAME); 39 | 40 | root.push_attribute(("index", value.index.to_string().as_ref())); 41 | if let Some(is_default) = &value.is_default { 42 | root.push_attribute(("isDefault", is_default.to_string().as_ref())); 43 | } 44 | writer.write_event(Event::Start(root))?; 45 | 46 | for name in &value.service_names { 47 | writer.write_event(name.to_xml("md:ServiceName")?)?; 48 | } 49 | 50 | if let Some(service_descriptions) = &value.service_descriptions { 51 | for name in service_descriptions { 52 | writer.write_event(name.to_xml("md:ServiceDescription")?)?; 53 | } 54 | } 55 | for request_attributes in &value.request_attributes { 56 | let event: Event<'_> = request_attributes.try_into()?; 57 | writer.write_event(event)?; 58 | } 59 | 60 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 61 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 62 | write_buf, 63 | )?))) 64 | } 65 | } 66 | 67 | const REQUESTED_ATTRIBUTE_NAME: &str = "md:RequestedAttribute"; 68 | 69 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 70 | pub struct RequestedAttribute { 71 | #[serde(rename = "@FriendlyName")] 72 | pub friendly_name: Option, 73 | #[serde(rename = "@Name")] 74 | pub name: String, 75 | #[serde(rename = "@NameFormat")] 76 | pub name_format: Option, 77 | #[serde(rename = "AttributeValue")] 78 | pub values: Option>, 79 | #[serde(rename = "@isRequired")] 80 | pub is_required: Option, 81 | } 82 | 83 | impl TryFrom for Event<'_> { 84 | type Error = Box; 85 | 86 | fn try_from(value: RequestedAttribute) -> Result { 87 | (&value).try_into() 88 | } 89 | } 90 | 91 | impl TryFrom<&RequestedAttribute> for Event<'_> { 92 | type Error = Box; 93 | 94 | fn try_from(value: &RequestedAttribute) -> Result { 95 | let mut write_buf = Vec::new(); 96 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 97 | let mut root = BytesStart::new(REQUESTED_ATTRIBUTE_NAME); 98 | root.push_attribute(("Name", value.name.as_ref())); 99 | if let Some(name_format) = &value.name_format { 100 | root.push_attribute(("NameFormat", name_format.as_ref())); 101 | } 102 | if let Some(friendly_name) = &value.friendly_name { 103 | root.push_attribute(("FriendlyName", friendly_name.as_ref())); 104 | } 105 | if let Some(is_required) = &value.is_required { 106 | root.push_attribute(("isRequired", is_required.to_string().as_ref())); 107 | } 108 | writer.write_event(Event::Start(root))?; 109 | 110 | if let Some(values) = &value.values { 111 | for value in values { 112 | let event: Event<'_> = value.try_into()?; 113 | writer.write_event(event)?; 114 | } 115 | } 116 | 117 | writer.write_event(Event::End(BytesEnd::new(REQUESTED_ATTRIBUTE_NAME)))?; 118 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 119 | write_buf, 120 | )?))) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/schema/requested_authn_context.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::{AuthnContextClassRef, AuthnContextDeclRef}; 2 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 3 | use quick_xml::Writer; 4 | use serde::Deserialize; 5 | use std::io::Cursor; 6 | use std::str::FromStr; 7 | 8 | const NAME: &str = "saml2p:RequestedAuthnContext"; 9 | const SCHEMA: (&str, &str) = ("xmlns:saml2", "urn:oasis:names:tc:SAML:2.0:assertion"); 10 | 11 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 12 | pub struct RequestedAuthnContext { 13 | #[serde(rename = "AuthnContextClassRef")] 14 | pub authn_context_class_refs: Option>, 15 | #[serde(rename = "AuthnContextDeclRef")] 16 | pub authn_context_decl_refs: Option>, 17 | #[serde(rename = "@Comparison")] 18 | pub comparison: Option, 19 | } 20 | 21 | impl TryFrom for Event<'_> { 22 | type Error = Box; 23 | 24 | fn try_from(value: RequestedAuthnContext) -> Result { 25 | (&value).try_into() 26 | } 27 | } 28 | 29 | impl TryFrom<&RequestedAuthnContext> for Event<'_> { 30 | type Error = Box; 31 | 32 | fn try_from(value: &RequestedAuthnContext) -> Result { 33 | let mut write_buf = Vec::new(); 34 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 35 | let mut root = BytesStart::from_content(NAME, NAME.len()); 36 | root.push_attribute(SCHEMA); 37 | 38 | if let Some(comparison) = &value.comparison { 39 | root.push_attribute(("Comparison", comparison.value())); 40 | } 41 | writer.write_event(Event::Start(root))?; 42 | 43 | if let Some(authn_context_class_refs) = &value.authn_context_class_refs { 44 | for authn_context_class_ref in authn_context_class_refs { 45 | let event: Event<'_> = authn_context_class_ref.try_into()?; 46 | writer.write_event(event)?; 47 | } 48 | } else if let Some(authn_context_decl_refs) = &value.authn_context_decl_refs { 49 | for authn_context_decl_ref in authn_context_decl_refs { 50 | let event: Event<'_> = authn_context_decl_ref.try_into()?; 51 | writer.write_event(event)?; 52 | } 53 | } 54 | 55 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 56 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 57 | write_buf, 58 | )?))) 59 | } 60 | } 61 | 62 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 63 | #[serde(rename_all = "lowercase")] 64 | pub enum AuthnContextComparison { 65 | Exact, 66 | Minimum, 67 | Maximum, 68 | Better, 69 | } 70 | 71 | impl AuthnContextComparison { 72 | pub fn value(&self) -> &'static str { 73 | match self { 74 | AuthnContextComparison::Exact => "exact", 75 | AuthnContextComparison::Minimum => "minimum", 76 | AuthnContextComparison::Maximum => "maximum", 77 | AuthnContextComparison::Better => "better", 78 | } 79 | } 80 | } 81 | 82 | impl FromStr for AuthnContextComparison { 83 | type Err = quick_xml::DeError; 84 | 85 | fn from_str(s: &str) -> Result { 86 | Ok(match s { 87 | "exact" => AuthnContextComparison::Exact, 88 | "minimum" => AuthnContextComparison::Minimum, 89 | "maximum" => AuthnContextComparison::Maximum, 90 | "better" => AuthnContextComparison::Better, 91 | _ => { 92 | return Err(quick_xml::DeError::Custom( 93 | "Illegal comparison! Must be one of `exact`, `minimum`, `maximum` or `better`" 94 | .to_string(), 95 | )); 96 | } 97 | }) 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod test { 103 | use crate::traits::ToXml; 104 | 105 | use super::*; 106 | 107 | #[test] 108 | pub fn test_deserialize_serialize_requested_authn_context() { 109 | let xml_context = r#"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"#; 110 | 111 | let expected_context: RequestedAuthnContext = 112 | quick_xml::de::from_str(xml_context).expect("failed to parse RequestedAuthnContext"); 113 | let serialized_context = expected_context 114 | .to_string() 115 | .expect("failed to convert RequestedAuthnContext to xml"); 116 | let actual_context: RequestedAuthnContext = quick_xml::de::from_str(&serialized_context) 117 | .expect("failed to re-parse RequestedAuthnContext"); 118 | 119 | assert_eq!(expected_context, actual_context); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test_vectors/response_signed_by_idp_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://fujifish.github.io/samling/samling.html 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | W7iYqYBNLg7dS+ueqLf04nO5V+c= 15 | 16 | 17 | HxXRgmgrGJxhp6K3Bsj9H0QnZEdJfz/idDGN02a7h4G32BpmXzJ11OVII5vR6tK5 18 | BrSn2COna//MaXte/hrcJrr4RO7FkwP++Z3If5dlOvrcZg4WF4S+MbwDlZY2w5AV 19 | wgdlJCl/Iay8YB0mmq177FcNi4GZg8/sIB11+y9hmVA= 20 | 21 | 22 | MIICpzCCAhACCQDuFX0Db5iljDANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMC 23 | VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEQMA4G 24 | A1UECgwHU2FtbGluZzEPMA0GA1UECwwGU2FsaW5nMRQwEgYDVQQDDAtjYXByaXph 25 | LmNvbTEmMCQGCSqGSIb3DQEJARYXZW5naW5lZXJpbmdAY2Fwcml6YS5jb20wHhcN 26 | MTgwNTE1MTgxMTEwWhcNMjgwNTEyMTgxMTEwWjCBlzELMAkGA1UEBhMCVVMxEzAR 27 | BgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0bzEQMA4GA1UECgwH 28 | U2FtbGluZzEPMA0GA1UECwwGU2FsaW5nMRQwEgYDVQQDDAtjYXByaXphLmNvbTEm 29 | MCQGCSqGSIb3DQEJARYXZW5naW5lZXJpbmdAY2Fwcml6YS5jb20wgZ8wDQYJKoZI 30 | hvcNAQEBBQADgY0AMIGJAoGBAJEBNDJKH5nXr0hZKcSNIY1l4HeYLPBEKJLXyAno 31 | FTdgGrvi40YyIx9lHh0LbDVWCgxJp21BmKll0CkgmeKidvGlr3FUwtETro44L+Sg 32 | mjiJNbftvFxhNkgA26O2GDQuBoQwgSiagVadWXwJKkodH8tx4ojBPYK1pBO8fHf3 33 | wOnxAgMBAAEwDQYJKoZIhvcNAQELBQADgYEACIylhvh6T758hcZjAQJiV7rMRg+O 34 | mb68iJI4L9f0cyBcJENR+1LQNgUGyFDMm9Wm9o81CuIKBnfpEE2Jfcs76YVWRJy5 35 | xJ11GFKJJ5T0NEB7txbUQPoJOeNoE736lF5vYw6YKp8fJqPW0L2PLWe9qTn8hxpd 36 | njo3k6r5gXyl8tk= 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | https://fujifish.github.io/samling/samling.html 45 | 46 | 47 | _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 48 | 49 | 50 | 51 | 52 | 53 | 54 | http://test_accept_signed_with_correct_key.test 55 | 56 | 57 | 58 | 59 | urn:oasis:names:tc:SAML:2.0:ac:classes:Password 60 | 61 | 62 | 63 | 64 | test 65 | 66 | 67 | test@example.com 68 | 69 | 70 | users 71 | examplerole1 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/idp/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | use self::error::Error; 3 | 4 | pub mod response_builder; 5 | pub mod sp_extractor; 6 | pub mod verified_request; 7 | 8 | #[cfg(test)] 9 | mod tests; 10 | 11 | use openssl::bn::{BigNum, MsbOption}; 12 | use openssl::ec::{EcGroup, EcKey}; 13 | use openssl::nid::Nid; 14 | use openssl::pkey::Private; 15 | use openssl::{asn1::Asn1Time, pkey, x509}; 16 | use std::str::FromStr; 17 | 18 | use crate::crypto::{CertificateDer, Crypto, CryptoProvider}; 19 | 20 | use crate::idp::response_builder::{build_response_template, ResponseAttribute}; 21 | use crate::schema::Response; 22 | use crate::traits::ToXml; 23 | 24 | pub struct IdentityProvider { 25 | private_key: pkey::PKey, 26 | } 27 | 28 | pub enum Rsa { 29 | Rsa2048, 30 | Rsa3072, 31 | Rsa4096, 32 | } 33 | 34 | impl Rsa { 35 | fn bit_length(&self) -> u32 { 36 | match &self { 37 | Rsa::Rsa2048 => 2048, 38 | Rsa::Rsa3072 => 3072, 39 | Rsa::Rsa4096 => 4096, 40 | } 41 | } 42 | } 43 | 44 | pub enum Elliptic { 45 | NISTP256, 46 | } 47 | 48 | pub enum KeyType { 49 | Rsa(Rsa), 50 | Elliptic(Elliptic), 51 | } 52 | 53 | pub struct CertificateParams<'a> { 54 | pub common_name: &'a str, 55 | pub issuer_name: &'a str, 56 | pub days_until_expiration: u32, 57 | } 58 | 59 | impl IdentityProvider { 60 | pub fn generate_new(key_type: KeyType) -> Result { 61 | let private_key = match key_type { 62 | KeyType::Rsa(rsa) => { 63 | let bit_length = rsa.bit_length(); 64 | let rsa = openssl::rsa::Rsa::generate(bit_length)?; 65 | pkey::PKey::from_rsa(rsa)? 66 | } 67 | KeyType::Elliptic(ecc) => { 68 | let nid = match ecc { 69 | Elliptic::NISTP256 => Nid::X9_62_PRIME256V1, 70 | }; 71 | let group = EcGroup::from_curve_name(nid)?; 72 | let private_key: EcKey = EcKey::generate(&group)?; 73 | pkey::PKey::from_ec_key(private_key)? 74 | } 75 | }; 76 | 77 | Ok(IdentityProvider { private_key }) 78 | } 79 | 80 | pub fn from_rsa_private_key_der(der_bytes: &[u8]) -> Result { 81 | let rsa = openssl::rsa::Rsa::private_key_from_der(der_bytes)?; 82 | let private_key = pkey::PKey::from_rsa(rsa)?; 83 | 84 | Ok(IdentityProvider { private_key }) 85 | } 86 | 87 | pub fn export_private_key_der(&self) -> Result, Error> { 88 | if let Ok(ec_key) = self.private_key.ec_key() { 89 | Ok(ec_key.private_key_to_der()?) 90 | } else if let Ok(rsa) = self.private_key.rsa() { 91 | Ok(rsa.private_key_to_der()?) 92 | } else { 93 | Err(Error::UnexpectedError)? 94 | } 95 | } 96 | 97 | pub fn create_certificate(&self, params: &CertificateParams) -> Result { 98 | let mut name = x509::X509Name::builder()?; 99 | name.append_entry_by_nid(Nid::COMMONNAME, params.common_name)?; 100 | let name = name.build(); 101 | 102 | let mut iss = x509::X509Name::builder()?; 103 | iss.append_entry_by_nid(Nid::COMMONNAME, params.issuer_name)?; 104 | let iss = iss.build(); 105 | 106 | let mut builder = x509::X509::builder()?; 107 | 108 | let serial_number = { 109 | let mut serial = BigNum::new()?; 110 | serial.rand(159, MsbOption::MAYBE_ZERO, false)?; 111 | serial.to_asn1_integer()? 112 | }; 113 | 114 | builder.set_serial_number(&serial_number)?; 115 | builder.set_version(2)?; 116 | builder.set_subject_name(&name)?; 117 | builder.set_issuer_name(&iss)?; 118 | builder.set_pubkey(&self.private_key)?; 119 | 120 | let starts = Asn1Time::days_from_now(0)?; // now 121 | builder.set_not_before(&starts)?; 122 | 123 | let expires = Asn1Time::days_from_now(params.days_until_expiration)?; 124 | builder.set_not_after(&expires)?; 125 | 126 | builder.sign(&self.private_key, openssl::hash::MessageDigest::sha256())?; 127 | 128 | let certificate: x509::X509 = builder.build(); 129 | Ok(certificate.to_der()?.into()) 130 | } 131 | 132 | pub fn sign_authn_response( 133 | &self, 134 | idp_x509_cert_der: &CertificateDer, 135 | subject_name_id: &str, 136 | audience: &str, 137 | acs_url: &str, 138 | issuer: &str, 139 | in_response_to_id: &str, 140 | attributes: &[ResponseAttribute], 141 | ) -> Result> { 142 | let response = build_response_template( 143 | idp_x509_cert_der, 144 | subject_name_id, 145 | audience, 146 | issuer, 147 | acs_url, 148 | in_response_to_id, 149 | attributes, 150 | ); 151 | 152 | let response_xml_unsigned = response.to_string()?; 153 | let signed_xml = Crypto::sign_xml( 154 | response_xml_unsigned.as_str(), 155 | self.export_private_key_der()?.as_slice(), 156 | )?; 157 | let signed_response = Response::from_str(signed_xml.as_str())?; 158 | Ok(signed_response) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/idp/response_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::attribute::{Attribute, AttributeValue}; 2 | use crate::schema::{ 3 | Assertion, AttributeStatement, AudienceRestriction, AuthnContext, AuthnContextClassRef, 4 | AuthnStatement, Conditions, Issuer, Response, Status, StatusCode, Subject, SubjectConfirmation, 5 | SubjectConfirmationData, SubjectNameID, 6 | }; 7 | use crate::signature::Signature; 8 | use chrono::Utc; 9 | 10 | use super::sp_extractor::RequiredAttribute; 11 | use crate::crypto; 12 | use crate::crypto::CertificateDer; 13 | 14 | fn build_conditions(audience: &str) -> Conditions { 15 | Conditions { 16 | not_before: None, 17 | not_on_or_after: None, 18 | audience_restrictions: Some(vec![AudienceRestriction { 19 | audience: vec![audience.to_string()], 20 | }]), 21 | one_time_use: None, 22 | proxy_restriction: None, 23 | } 24 | } 25 | 26 | fn build_authn_statement(class: &str) -> AuthnStatement { 27 | AuthnStatement { 28 | authn_instant: Some(Utc::now()), 29 | session_index: None, 30 | session_not_on_or_after: None, 31 | subject_locality: None, 32 | authn_context: Some(AuthnContext { 33 | value: Some(AuthnContextClassRef { 34 | value: Some(class.to_string()), 35 | }), 36 | }), 37 | } 38 | } 39 | 40 | pub struct ResponseAttribute<'a> { 41 | pub required_attribute: RequiredAttribute, 42 | pub value: &'a str, 43 | } 44 | 45 | fn build_attributes(formats_names_values: &[ResponseAttribute]) -> Vec { 46 | formats_names_values 47 | .iter() 48 | .map(|attr| Attribute { 49 | friendly_name: None, 50 | name: Some(attr.required_attribute.name.clone()), 51 | name_format: attr.required_attribute.format.clone(), 52 | values: vec![AttributeValue { 53 | attribute_type: Some("xs:string".to_string()), 54 | value: Some(attr.value.to_string()), 55 | }], 56 | }) 57 | .collect() 58 | } 59 | 60 | fn build_assertion( 61 | name_id: &str, 62 | request_id: &str, 63 | issuer: Issuer, 64 | recipient: &str, 65 | audience: &str, 66 | attributes: &[ResponseAttribute], 67 | ) -> Assertion { 68 | let assertion_id = crypto::gen_saml_assertion_id(); 69 | 70 | Assertion { 71 | id: assertion_id, 72 | issue_instant: Utc::now(), 73 | version: "2.0".to_string(), 74 | issuer, 75 | signature: None, 76 | subject: Some(Subject { 77 | name_id: Some(SubjectNameID { 78 | format: Some("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified".to_string()), 79 | value: name_id.to_owned(), 80 | }), 81 | subject_confirmations: Some(vec![SubjectConfirmation { 82 | method: Some("urn:oasis:names:tc:SAML:2.0:cm:bearer".to_string()), 83 | name_id: None, 84 | subject_confirmation_data: Some(SubjectConfirmationData { 85 | not_before: None, 86 | not_on_or_after: None, 87 | recipient: Some(recipient.to_owned()), 88 | in_response_to: Some(request_id.to_owned()), 89 | address: None, 90 | content: None, 91 | }), 92 | }]), 93 | }), 94 | conditions: Some(build_conditions(audience)), 95 | authn_statements: Some(vec![build_authn_statement( 96 | "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified", 97 | )]), 98 | attribute_statements: Some(vec![AttributeStatement { 99 | attributes: build_attributes(attributes), 100 | }]), 101 | } 102 | } 103 | 104 | fn build_response( 105 | name_id: &str, 106 | issuer: &str, 107 | request_id: &str, 108 | attributes: &[ResponseAttribute], 109 | destination: &str, 110 | audience: &str, 111 | x509_cert: &CertificateDer, 112 | ) -> Response { 113 | let issuer = Issuer { 114 | value: Some(issuer.to_string()), 115 | ..Default::default() 116 | }; 117 | 118 | let response_id = crypto::gen_saml_response_id(); 119 | 120 | Response { 121 | id: response_id.clone(), 122 | in_response_to: Some(request_id.to_owned()), 123 | version: "2.0".to_string(), 124 | issue_instant: Utc::now(), 125 | destination: Some(destination.to_string()), 126 | consent: None, 127 | issuer: Some(issuer.clone()), 128 | signature: Some(Signature::template(&response_id, x509_cert)), 129 | status: Some(Status { 130 | status_code: StatusCode { 131 | value: Some("urn:oasis:names:tc:SAML:2.0:status:Success".to_string()), 132 | }, 133 | status_message: None, 134 | status_detail: None, 135 | }), 136 | encrypted_assertion: None, 137 | assertion: Some(build_assertion( 138 | name_id, 139 | request_id, 140 | issuer, 141 | destination, 142 | audience, 143 | attributes, 144 | )), 145 | } 146 | } 147 | 148 | pub fn build_response_template( 149 | cert_der: &CertificateDer, 150 | name_id: &str, 151 | audience: &str, 152 | issuer: &str, 153 | acs_url: &str, 154 | request_id: &str, 155 | attributes: &[ResponseAttribute], 156 | ) -> Response { 157 | build_response( 158 | name_id, issuer, request_id, attributes, acs_url, audience, cert_der, 159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Samael 2 | 3 | [![Crates.io][crates-badge]][crates-url] 4 | [![MIT licensed][mit-badge]][mit-url] 5 | 6 | [crates-badge]: https://img.shields.io/crates/v/samael.svg 7 | [crates-url]: https://crates.io/crates/samael 8 | [mit-badge]: https://img.shields.io/crates/l/samael 9 | [mit-url]: https://github.com/njaremko/samael/blob/master/LICENSE 10 | 11 | This is a SAML2 library for rust. 12 | 13 | This is a work in progress. Pull Requests are welcome. 14 | 15 | Current Features: 16 | 17 | - Serializing and Deserializing SAML messages 18 | - IDP-initiated SSO 19 | - SP-initiated SSO Redirect-POST binding 20 | - Helpers for validating SAML assertions 21 | - Encrypted assertions only support: 22 | - **key info:** 23 | - `http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p` 24 | - `http://www.w3.org/2001/04/xmlenc#rsa-1_5` 25 | - **value info:** 26 | - `http://www.w3.org/2001/04/xmlenc#aes128-cbc` 27 | - `http://www.w3.org/2009/xmlenc11#aes128-gcm` 28 | - Verify SAMLRequest (AuthnRequest) message signatures 29 | - Create signed SAMLResponse (Response) messages 30 | 31 | The `"xmlsec"` feature flag adds basic support for verifying and signing SAML messages. We're using a modified copy of [rust-xmlsec](https://github.com/voipir/rust-xmlsec) library (bindings to xmlsec1 library). 32 | 33 | If you want to use the `"xmlsec"` feature, you'll need to install the following C libs: 34 | 35 | - libiconv 36 | - libtool 37 | - libxml2 38 | - libxslt 39 | - libclang 40 | - openssl 41 | - pkg-config 42 | - xmlsec1 43 | 44 | # Build instructions 45 | 46 | We use [nix](https://nixos.org) to faciliate reproducible builds of `samael`. 47 | It will ensure you have the required libraries installed in a way that won't cause any issues with the rest of your system. 48 | If you want to take advantage of this, you'll need to put in a little bit of work. 49 | 50 | 1. [Install nix](https://github.com/DeterminateSystems/nix-installer) 51 | 1. Install [direnv](https://direnv.net/) and [cachix](https://docs.cachix.org) 52 | ``` 53 | # Add ~/.nix-profile/bin to your path first 54 | nix profile install nixpkgs#direnv 55 | nix profile install nixpkgs#cachix 56 | ``` 57 | 1. Run `cachix use nix-community` to enable a binary cache for the rust toolchain (otherwise you'll build the rust toolchain from scratch) 58 | 1. `cd` into this repo and run `direnv allow` and `nix-direnv-reload` 59 | 1. Install the [direnv VS Code extension](https://marketplace.visualstudio.com/items?itemName=mkhl.direnv) 60 | 61 | ## Building the library 62 | 63 | Just run `nix build` 64 | 65 | ## Entering a dev environment 66 | 67 | If you followed the above instructions, just `cd`-ing into the directory will setup a reproducible dev environment, 68 | but if you don't want to install `direnv`, then just run `nix develop`. 69 | 70 | From their you can build as normal: 71 | 72 | ```sh 73 | cargo build --features xmlsec 74 | cargo test --features xmlsec 75 | ``` 76 | 77 | # How do I use this library? 78 | 79 | You'll need these dependencies for this example 80 | 81 | ```toml 82 | [dependencies] 83 | tokio = { version = "1.28.1", features = ["full"] } 84 | samael = { version = "0.0.12", features = ["xmlsec"] } 85 | warp = "0.3.5" 86 | reqwest = "0.11.18" 87 | openssl = "0.10.52" 88 | openssl-probe = "0.1.5" 89 | ``` 90 | 91 | Here is some sample code using this library: 92 | 93 | ```rust 94 | use samael::metadata::{ContactPerson, ContactType, EntityDescriptor}; 95 | use samael::service_provider::ServiceProviderBuilder; 96 | use std::collections::HashMap; 97 | use std::fs; 98 | use warp::Filter; 99 | 100 | #[tokio::main] 101 | async fn main() -> Result<(), Box> { 102 | openssl_probe::init_ssl_cert_env_vars(); 103 | 104 | let resp = reqwest::get("https://samltest.id/saml/idp") 105 | .await? 106 | .text() 107 | .await?; 108 | let idp_metadata: EntityDescriptor = samael::metadata::de::from_str(&resp)?; 109 | 110 | let pub_key = openssl::x509::X509::from_pem(&fs::read("./publickey.cer")?)?; 111 | let private_key = openssl::rsa::Rsa::private_key_from_pem(&fs::read("./privatekey.pem")?)?; 112 | 113 | let sp = ServiceProviderBuilder::default() 114 | .entity_id("".to_string()) 115 | .key(private_key) 116 | .certificate(pub_key) 117 | .allow_idp_initiated(true) 118 | .contact_person(ContactPerson { 119 | sur_name: Some("Bob".to_string()), 120 | contact_type: Some(ContactType::Technical.value().to_string()), 121 | ..ContactPerson::default() 122 | }) 123 | .idp_metadata(idp_metadata) 124 | .acs_url("http://localhost:8080/saml/acs".to_string()) 125 | .slo_url("http://localhost:8080/saml/slo".to_string()) 126 | .build()?; 127 | 128 | let metadata = sp.metadata()?.to_string()?; 129 | 130 | let metadata_route = warp::get() 131 | .and(warp::path("metadata")) 132 | .map(move || metadata.clone()); 133 | 134 | let acs_route = warp::post() 135 | .and(warp::path("acs")) 136 | .and(warp::body::form()) 137 | .map(move |s: HashMap| { 138 | if let Some(encoded_resp) = s.get("SAMLResponse") { 139 | let t = sp 140 | .parse_base64_response(encoded_resp, Some(&["a_possible_request_id"])) 141 | .unwrap(); 142 | return format!("{:?}", t); 143 | } 144 | format!("") 145 | }); 146 | 147 | let saml_routes = warp::path("saml").and(acs_route.or(metadata_route)); 148 | warp::serve(saml_routes).run(([127, 0, 0, 1], 8080)).await; 149 | Ok(()) 150 | } 151 | ``` 152 | -------------------------------------------------------------------------------- /src/schema/conditions.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 3 | use quick_xml::Writer; 4 | use serde::Deserialize; 5 | use std::io::Cursor; 6 | 7 | const NAME: &str = "saml2:Conditions"; 8 | 9 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 10 | pub struct Conditions { 11 | #[serde(rename = "@NotBefore")] 12 | pub not_before: Option>, 13 | #[serde(rename = "@NotOnOrAfter")] 14 | pub not_on_or_after: Option>, 15 | #[serde(rename = "AudienceRestriction", default)] 16 | pub audience_restrictions: Option>, 17 | #[serde(rename = "OneTimeUse")] 18 | pub one_time_use: Option, 19 | #[serde(rename = "ProxyRestriction")] 20 | pub proxy_restriction: Option, 21 | } 22 | 23 | impl TryFrom for Event<'_> { 24 | type Error = Box; 25 | 26 | fn try_from(value: Conditions) -> Result { 27 | (&value).try_into() 28 | } 29 | } 30 | 31 | impl TryFrom<&Conditions> for Event<'_> { 32 | type Error = Box; 33 | 34 | fn try_from(value: &Conditions) -> Result { 35 | let mut write_buf = Vec::new(); 36 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 37 | let mut root = BytesStart::new(NAME); 38 | if let Some(not_before) = &value.not_before { 39 | root.push_attribute(( 40 | "NotBefore", 41 | not_before 42 | .to_rfc3339_opts(SecondsFormat::Secs, true) 43 | .as_ref(), 44 | )); 45 | } 46 | if let Some(not_on_or_after) = &value.not_on_or_after { 47 | root.push_attribute(( 48 | "NotOnOrAfter", 49 | not_on_or_after 50 | .to_rfc3339_opts(SecondsFormat::Secs, true) 51 | .as_ref(), 52 | )); 53 | } 54 | writer.write_event(Event::Start(root))?; 55 | if let Some(audience_restrictions) = &value.audience_restrictions { 56 | for restriction in audience_restrictions { 57 | let event: Event<'_> = restriction.try_into()?; 58 | writer.write_event(event)?; 59 | } 60 | } 61 | if let Some(proxy_restriction) = &value.proxy_restriction { 62 | let event: Event<'_> = proxy_restriction.try_into()?; 63 | writer.write_event(event)?; 64 | } 65 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 66 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 67 | write_buf, 68 | )?))) 69 | } 70 | } 71 | 72 | const AUDIENCE_RESTRICTION_NAME: &str = "saml2:AudienceRestriction"; 73 | const AUDIENCE_NAME: &str = "saml2:Audience"; 74 | 75 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 76 | pub struct AudienceRestriction { 77 | #[serde(rename = "Audience")] 78 | pub audience: Vec, 79 | } 80 | 81 | impl TryFrom for Event<'_> { 82 | type Error = Box; 83 | 84 | fn try_from(value: AudienceRestriction) -> Result { 85 | (&value).try_into() 86 | } 87 | } 88 | 89 | impl TryFrom<&AudienceRestriction> for Event<'_> { 90 | type Error = Box; 91 | 92 | fn try_from(value: &AudienceRestriction) -> Result { 93 | let mut write_buf = Vec::new(); 94 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 95 | let root = BytesStart::new(AUDIENCE_RESTRICTION_NAME); 96 | writer.write_event(Event::Start(root))?; 97 | for aud in &value.audience { 98 | writer.write_event(Event::Start(BytesStart::new(AUDIENCE_NAME)))?; 99 | writer.write_event(Event::Text(BytesText::from_escaped(aud)))?; 100 | writer.write_event(Event::End(BytesEnd::new(AUDIENCE_NAME)))?; 101 | } 102 | writer.write_event(Event::End(BytesEnd::new(AUDIENCE_RESTRICTION_NAME)))?; 103 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 104 | write_buf, 105 | )?))) 106 | } 107 | } 108 | 109 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 110 | pub struct OneTimeUse {} 111 | 112 | const PROXY_RESTRICTION_NAME: &str = "saml2:ProxyRestriction"; 113 | 114 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 115 | pub struct ProxyRestriction { 116 | #[serde(rename = "@Count")] 117 | pub count: Option, 118 | #[serde(rename = "Audience")] 119 | pub audiences: Option>, 120 | } 121 | 122 | impl TryFrom for Event<'_> { 123 | type Error = Box; 124 | 125 | fn try_from(value: ProxyRestriction) -> Result { 126 | (&value).try_into() 127 | } 128 | } 129 | 130 | impl TryFrom<&ProxyRestriction> for Event<'_> { 131 | type Error = Box; 132 | 133 | fn try_from(value: &ProxyRestriction) -> Result { 134 | let mut write_buf = Vec::new(); 135 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 136 | let mut root = BytesStart::new(PROXY_RESTRICTION_NAME); 137 | if let Some(count) = &value.count { 138 | root.push_attribute(("Count", count.to_string().as_ref())); 139 | } 140 | writer.write_event(Event::Start(root))?; 141 | if let Some(audiences) = &value.audiences { 142 | for aud in audiences { 143 | writer.write_event(Event::Start(BytesStart::new(AUDIENCE_NAME)))?; 144 | writer.write_event(Event::Text(BytesText::from_escaped(aud)))?; 145 | writer.write_event(Event::End(BytesEnd::new(AUDIENCE_NAME)))?; 146 | } 147 | } 148 | writer.write_event(Event::End(BytesEnd::new(PROXY_RESTRICTION_NAME)))?; 149 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 150 | write_buf, 151 | )?))) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/key_info.rs: -------------------------------------------------------------------------------- 1 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 2 | use quick_xml::Writer; 3 | use serde::Deserialize; 4 | use std::io::Cursor; 5 | 6 | use crate::schema::EncryptedKey; 7 | 8 | const NAME: &str = "ds:KeyInfo"; 9 | const SCHEMA: &str = "xmlns:ds"; 10 | 11 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 12 | pub struct KeyInfo { 13 | #[serde(rename = "@Id")] 14 | pub id: Option, 15 | #[serde(rename = "X509Data")] 16 | pub x509_data: Option, 17 | } 18 | 19 | impl TryFrom for Event<'_> { 20 | type Error = Box; 21 | 22 | fn try_from(value: KeyInfo) -> Result { 23 | (&value).try_into() 24 | } 25 | } 26 | 27 | impl TryFrom<&KeyInfo> for Event<'_> { 28 | type Error = Box; 29 | 30 | fn try_from(value: &KeyInfo) -> Result { 31 | let mut write_buf = Vec::new(); 32 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 33 | let mut root = BytesStart::new(NAME); 34 | if let Some(id) = &value.id { 35 | root.push_attribute(("Id", id.as_ref())); 36 | } 37 | writer.write_event(Event::Start(root))?; 38 | 39 | if let Some(x509_data) = &value.x509_data { 40 | let event: Event<'_> = x509_data.try_into()?; 41 | writer.write_event(event)?; 42 | } 43 | 44 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 45 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 46 | write_buf, 47 | )?))) 48 | } 49 | } 50 | 51 | const X509_DATA_NAME: &str = "ds:X509Data"; 52 | 53 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 54 | pub struct X509Data { 55 | #[serde(rename = "X509Certificate")] 56 | pub certificates: Vec, 57 | } 58 | 59 | impl TryFrom for Event<'_> { 60 | type Error = Box; 61 | 62 | fn try_from(value: X509Data) -> Result { 63 | (&value).try_into() 64 | } 65 | } 66 | 67 | impl TryFrom<&X509Data> for Event<'_> { 68 | type Error = Box; 69 | 70 | fn try_from(value: &X509Data) -> Result { 71 | let mut write_buf = Vec::new(); 72 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 73 | let root = BytesStart::new(X509_DATA_NAME); 74 | writer.write_event(Event::Start(root))?; 75 | 76 | for certificate in &value.certificates { 77 | let name = "ds:X509Certificate"; 78 | writer.write_event(Event::Start(BytesStart::new(name)))?; 79 | writer.write_event(Event::Text(BytesText::from_escaped(certificate.as_str())))?; 80 | writer.write_event(Event::End(BytesEnd::new(name)))?; 81 | } 82 | 83 | writer.write_event(Event::End(BytesEnd::new(X509_DATA_NAME)))?; 84 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 85 | write_buf, 86 | )?))) 87 | } 88 | } 89 | 90 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 91 | pub struct EncryptedKeyInfo { 92 | #[serde(rename = "@Id")] 93 | pub id: Option, 94 | #[serde(alias = "@xmlns:dsig", alias = "@xmlns:ds")] 95 | pub ds: String, 96 | #[serde(rename = "EncryptedKey")] 97 | pub encrypted_key: Option, 98 | #[serde(rename = "RetrievalMethod")] 99 | pub retrieval_method: Option, 100 | } 101 | 102 | const RETRIEVAL_METHOD_NAME: &str = "ds:RetrievalMethod"; 103 | 104 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 105 | pub struct RetrievalMethod { 106 | #[serde(rename = "@URI")] 107 | pub uri: String, 108 | } 109 | 110 | impl TryFrom for Event<'_> { 111 | type Error = Box; 112 | 113 | fn try_from(value: RetrievalMethod) -> Result { 114 | (&value).try_into() 115 | } 116 | } 117 | 118 | impl TryFrom<&RetrievalMethod> for Event<'_> { 119 | type Error = Box; 120 | 121 | fn try_from(value: &RetrievalMethod) -> Result { 122 | let mut write_buf = Vec::new(); 123 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 124 | let mut root = BytesStart::new(RETRIEVAL_METHOD_NAME); 125 | root.push_attribute(("URI", value.uri.as_ref())); 126 | writer.write_event(Event::Start(root))?; 127 | writer.write_event(Event::End(BytesEnd::new(RETRIEVAL_METHOD_NAME)))?; 128 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 129 | write_buf, 130 | )?))) 131 | } 132 | } 133 | 134 | impl TryFrom for Event<'_> { 135 | type Error = Box; 136 | 137 | fn try_from(value: EncryptedKeyInfo) -> Result { 138 | (&value).try_into() 139 | } 140 | } 141 | 142 | impl TryFrom<&EncryptedKeyInfo> for Event<'_> { 143 | type Error = Box; 144 | 145 | fn try_from(value: &EncryptedKeyInfo) -> Result { 146 | let mut write_buf = Vec::new(); 147 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 148 | let mut root = BytesStart::new(NAME); 149 | if let Some(id) = &value.id { 150 | root.push_attribute(("Id", id.as_ref())); 151 | } 152 | root.push_attribute((SCHEMA, value.ds.as_ref())); 153 | 154 | writer.write_event(Event::Start(root))?; 155 | if let Some(encrypted_key) = &value.encrypted_key { 156 | let event: Event<'_> = encrypted_key.try_into()?; 157 | writer.write_event(event)?; 158 | } 159 | if let Some(retrieval_method) = &value.retrieval_method { 160 | let event: Event<'_> = retrieval_method.try_into()?; 161 | writer.write_event(event)?; 162 | } 163 | 164 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 165 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 166 | write_buf, 167 | )?))) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/metadata/sp_sso_descriptor.rs: -------------------------------------------------------------------------------- 1 | use crate::metadata::helpers::write_plain_element; 2 | use crate::metadata::{ 3 | AttributeConsumingService, ContactPerson, Endpoint, IndexedEndpoint, KeyDescriptor, 4 | Organization, 5 | }; 6 | use chrono::prelude::*; 7 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 8 | use quick_xml::Writer; 9 | use serde::Deserialize; 10 | use std::io::Cursor; 11 | 12 | const NAME: &str = "md:SPSSODescriptor"; 13 | 14 | #[derive(Clone, Debug, Deserialize, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] 15 | pub struct SpSsoDescriptor { 16 | #[serde(rename = "@ID")] 17 | pub id: Option, 18 | #[serde(rename = "@validUntil")] 19 | pub valid_until: Option>, 20 | #[serde(rename = "@cacheDuration")] 21 | pub cache_duration: Option, 22 | #[serde(rename = "@protocolSupportEnumeration")] 23 | pub protocol_support_enumeration: Option, 24 | #[serde(rename = "@errorURL")] 25 | pub error_url: Option, 26 | #[serde(rename = "KeyDescriptor")] 27 | pub key_descriptors: Option>, 28 | #[serde(rename = "Organization")] 29 | pub organization: Option, 30 | #[serde(rename = "ContactPerson")] 31 | pub contact_people: Option>, 32 | #[serde(rename = "ArtifactResolutionService")] 33 | pub artifact_resolution_service: Option>, 34 | #[serde(rename = "SingleLogoutService")] 35 | pub single_logout_services: Option>, 36 | #[serde(rename = "ManageNameIDService")] 37 | pub manage_name_id_services: Option>, 38 | #[serde(rename = "NameIDFormat")] 39 | pub name_id_formats: Option>, 40 | // ^-SSODescriptor 41 | #[serde(rename = "@AuthnRequestsSigned")] 42 | pub authn_requests_signed: Option, 43 | #[serde(rename = "@WantAssertionsSigned")] 44 | pub want_assertions_signed: Option, 45 | #[serde(rename = "AssertionConsumerService")] 46 | pub assertion_consumer_services: Vec, 47 | #[serde(rename = "AttributeConsumingService")] 48 | pub attribute_consuming_services: Option>, 49 | } 50 | 51 | impl TryFrom for Event<'_> { 52 | type Error = Box; 53 | 54 | fn try_from(value: SpSsoDescriptor) -> Result { 55 | (&value).try_into() 56 | } 57 | } 58 | 59 | impl TryFrom<&SpSsoDescriptor> for Event<'_> { 60 | type Error = Box; 61 | 62 | fn try_from(value: &SpSsoDescriptor) -> Result { 63 | let mut write_buf = Vec::new(); 64 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 65 | let mut root = BytesStart::new(NAME); 66 | 67 | if let Some(id) = &value.id { 68 | root.push_attribute(("ID", id.as_ref())); 69 | } 70 | 71 | if let Some(valid_until) = &value.valid_until { 72 | root.push_attribute(( 73 | "validUntil", 74 | valid_until 75 | .to_rfc3339_opts(SecondsFormat::Secs, true) 76 | .as_ref(), 77 | )); 78 | } 79 | 80 | if let Some(cache_duration) = &value.cache_duration { 81 | root.push_attribute(("cacheDuration", cache_duration.to_string().as_ref())); 82 | } 83 | 84 | if let Some(protocol_support_enumeration) = &value.protocol_support_enumeration { 85 | root.push_attribute(( 86 | "protocolSupportEnumeration", 87 | protocol_support_enumeration.as_ref(), 88 | )); 89 | } 90 | 91 | if let Some(error_url) = &value.error_url { 92 | root.push_attribute(("errorURL", error_url.as_ref())); 93 | } 94 | 95 | if let Some(want_assertions_signed) = &value.want_assertions_signed { 96 | root.push_attribute(( 97 | "WantAssertionsSigned", 98 | want_assertions_signed.to_string().as_ref(), 99 | )); 100 | } 101 | 102 | if let Some(authn_requests_signed) = &value.authn_requests_signed { 103 | root.push_attribute(( 104 | "AuthnRequestsSigned", 105 | authn_requests_signed.to_string().as_ref(), 106 | )); 107 | } 108 | 109 | writer.write_event(Event::Start(root))?; 110 | 111 | if let Some(key_descriptors) = &value.key_descriptors { 112 | for descriptor in key_descriptors { 113 | let event: Event<'_> = descriptor.try_into()?; 114 | writer.write_event(event)?; 115 | } 116 | } 117 | 118 | if let Some(organization) = &value.organization { 119 | let event: Event<'_> = organization.try_into()?; 120 | writer.write_event(event)?; 121 | } 122 | 123 | if let Some(contact_people) = &value.contact_people { 124 | for contact in contact_people { 125 | let event: Event<'_> = contact.try_into()?; 126 | writer.write_event(event)?; 127 | } 128 | } 129 | 130 | if let Some(artifact_resolution_service) = &value.artifact_resolution_service { 131 | for service in artifact_resolution_service { 132 | writer.write_event(service.to_xml("md:ArtifactResolutionService")?)?; 133 | } 134 | } 135 | 136 | if let Some(single_logout_services) = &value.single_logout_services { 137 | for service in single_logout_services { 138 | writer.write_event(service.to_xml("md:SingleLogoutService")?)?; 139 | } 140 | } 141 | 142 | if let Some(manage_name_id_services) = &value.manage_name_id_services { 143 | for service in manage_name_id_services { 144 | writer.write_event(service.to_xml("md:ManageNameIDService")?)?; 145 | } 146 | } 147 | 148 | if let Some(name_id_formats) = &value.name_id_formats { 149 | for format in name_id_formats { 150 | write_plain_element(&mut writer, "md:NameIDFormat", format.as_ref())?; 151 | } 152 | } 153 | 154 | for service in &value.assertion_consumer_services { 155 | writer.write_event(service.to_xml("md:AssertionConsumerService")?)?; 156 | } 157 | 158 | if let Some(attribute_consuming_services) = &value.attribute_consuming_services { 159 | for service in attribute_consuming_services { 160 | let event: Event<'_> = service.try_into()?; 161 | writer.write_event(event)?; 162 | } 163 | } 164 | 165 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 166 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 167 | write_buf, 168 | )?))) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /test_vectors/idp_metadata_nested.xml: -------------------------------------------------------------------------------- 1 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 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= 17 | 18 | 19 | 20 | 22 | 24 | 25 | 26 | Example.org Non-Profit Org 27 | Example.org 28 | https://www.example.org/ 29 | 30 | 31 | SAML Technical Support 32 | mailto:technical-support@example.org 33 | 34 | 35 | SAML Support 36 | mailto:support@example.org 37 | 38 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 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= 53 | 54 | 55 | 56 | 58 | 60 | 61 | 62 | Example.org Non-Profit Org 63 | Example.org 64 | https://www.example.org/ 65 | 66 | 67 | SAML Technical Support 68 | mailto:technical-support@example.org 69 | 70 | 71 | SAML Support 72 | mailto:support@example.org 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/schema/response.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::{Assertion, EncryptedAssertion, Issuer, Status}; 2 | use crate::signature::Signature; 3 | use chrono::prelude::*; 4 | use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; 5 | use quick_xml::Writer; 6 | use serde::Deserialize; 7 | use std::io::Cursor; 8 | use std::str::FromStr; 9 | 10 | const NAME: &str = "saml2p:Response"; 11 | const SCHEMA: (&str, &str) = ("xmlns:saml2p", "urn:oasis:names:tc:SAML:2.0:protocol"); 12 | 13 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 14 | pub struct Response { 15 | #[serde(rename = "@ID")] 16 | pub id: String, 17 | #[serde(rename = "@InResponseTo")] 18 | pub in_response_to: Option, 19 | #[serde(rename = "@Version")] 20 | pub version: String, 21 | #[serde(rename = "@IssueInstant")] 22 | pub issue_instant: DateTime, 23 | #[serde(rename = "@Destination")] 24 | pub destination: Option, 25 | #[serde(rename = "@Consent")] 26 | pub consent: Option, 27 | #[serde(rename = "Issuer")] 28 | pub issuer: Option, 29 | #[serde(rename = "Signature")] 30 | pub signature: Option, 31 | #[serde(rename = "Status")] 32 | pub status: Option, 33 | #[serde(rename = "EncryptedAssertion")] 34 | pub encrypted_assertion: Option, 35 | #[serde(rename = "Assertion")] 36 | pub assertion: Option, 37 | } 38 | 39 | impl FromStr for Response { 40 | type Err = quick_xml::DeError; 41 | 42 | fn from_str(s: &str) -> Result { 43 | quick_xml::de::from_str(s) 44 | } 45 | } 46 | 47 | impl TryFrom for Event<'_> { 48 | type Error = Box; 49 | 50 | fn try_from(value: Response) -> Result { 51 | (&value).try_into() 52 | } 53 | } 54 | 55 | impl TryFrom<&Response> for Event<'_> { 56 | type Error = Box; 57 | 58 | fn try_from(value: &Response) -> Result { 59 | let mut write_buf = Vec::new(); 60 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 61 | writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?; 62 | 63 | let mut root = BytesStart::new(NAME); 64 | root.push_attribute(SCHEMA); 65 | root.push_attribute(("ID", value.id.as_ref())); 66 | if let Some(resp_to) = &value.in_response_to { 67 | root.push_attribute(("InResponseTo", resp_to.as_ref())); 68 | } 69 | root.push_attribute(("Version", value.version.as_ref())); 70 | root.push_attribute(( 71 | "IssueInstant", 72 | value 73 | .issue_instant 74 | .to_rfc3339_opts(SecondsFormat::Millis, true) 75 | .as_ref(), 76 | )); 77 | if let Some(destination) = &value.destination { 78 | root.push_attribute(("Destination", destination.as_ref())); 79 | } 80 | if let Some(consent) = &value.consent { 81 | root.push_attribute(("Consent", consent.as_ref())); 82 | } 83 | 84 | writer.write_event(Event::Start(root))?; 85 | 86 | if let Some(issuer) = &value.issuer { 87 | let event: Event<'_> = issuer.try_into()?; 88 | writer.write_event(event)?; 89 | } 90 | if let Some(signature) = &value.signature { 91 | let event: Event<'_> = signature.try_into()?; 92 | writer.write_event(event)?; 93 | } 94 | if let Some(status) = &value.status { 95 | let event: Event<'_> = status.try_into()?; 96 | writer.write_event(event)?; 97 | } 98 | 99 | if let Some(assertion) = &value.assertion { 100 | let event: Event<'_> = assertion.try_into()?; 101 | writer.write_event(event)?; 102 | } 103 | 104 | if let Some(encrypted_assertion) = &value.encrypted_assertion { 105 | let event: Event<'_> = encrypted_assertion.try_into()?; 106 | writer.write_event(event)?; 107 | } 108 | 109 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 110 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 111 | write_buf, 112 | )?))) 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod test { 118 | use super::Response; 119 | use crate::traits::ToXml; 120 | 121 | #[test] 122 | fn test_deserialize_serialize_response() { 123 | let response_xml = include_str!(concat!( 124 | env!("CARGO_MANIFEST_DIR"), 125 | "/test_vectors/response.xml", 126 | )); 127 | let expected_response: Response = 128 | response_xml.parse().expect("failed to parse response.xml"); 129 | let serialized_response = expected_response 130 | .to_string() 131 | .expect("failed to convert response to xml"); 132 | let actual_response: Response = serialized_response 133 | .parse() 134 | .expect("failed to re-parse response"); 135 | 136 | assert_eq!(expected_response, actual_response); 137 | } 138 | 139 | #[test] 140 | fn test_deserialize_serialize_response_with_signed_assertion() { 141 | let response_xml = include_str!(concat!( 142 | env!("CARGO_MANIFEST_DIR"), 143 | "/test_vectors/response_signed_assertion.xml", 144 | )); 145 | let expected_response: Response = response_xml 146 | .parse() 147 | .expect("failed to parse response_signed_assertion.xml"); 148 | let serialized_response = expected_response 149 | .to_string() 150 | .expect("failed to convert response to xml"); 151 | let actual_response: Response = serialized_response 152 | .parse() 153 | .expect("failed to re-parse response"); 154 | 155 | assert_eq!(expected_response, actual_response); 156 | } 157 | 158 | #[test] 159 | fn test_deserialize_serialize_signed_response() { 160 | let response_xml = include_str!(concat!( 161 | env!("CARGO_MANIFEST_DIR"), 162 | "/test_vectors/response_signed.xml", 163 | )); 164 | let expected_response: Response = response_xml 165 | .parse() 166 | .expect("failed to parse response_signed.xml"); 167 | let serialized_response = expected_response 168 | .to_string() 169 | .expect("failed to convert response to xml"); 170 | let actual_response: Response = serialized_response 171 | .parse() 172 | .expect("failed to re-parse response"); 173 | 174 | assert_eq!(expected_response, actual_response); 175 | } 176 | 177 | #[test] 178 | fn test_deserialize_serialize_response_encrypted_assertion() { 179 | let response_xml = include_str!(concat!( 180 | env!("CARGO_MANIFEST_DIR"), 181 | "/test_vectors/response_encrypted.xml", 182 | )); 183 | let expected_response: Response = response_xml 184 | .parse() 185 | .expect("failed to parse response_encrypted.xml"); 186 | let serialized_response = expected_response 187 | .to_string() 188 | .expect("failed to convert response to xml"); 189 | let actual_response: Response = serialized_response 190 | .parse() 191 | .expect("failed to re-parse response"); 192 | 193 | assert_eq!(expected_response, actual_response); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | nix-filter.url = "github:numtide/nix-filter"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs = { 9 | nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | crane = { 13 | url = "github:ipetkov/crane"; 14 | }; 15 | advisory-db = { 16 | url = "github:rustsec/advisory-db"; 17 | flake = false; 18 | }; 19 | }; 20 | 21 | outputs = { self, nixpkgs, nix-filter, rust-overlay, crane, advisory-db, flake-utils }: 22 | flake-utils.lib.eachDefaultSystem 23 | (system: 24 | let 25 | overlays = [ 26 | (import rust-overlay) 27 | (final: prev: { 28 | nix-filter = nix-filter.lib; 29 | rust-toolchain = pkgs.rust-bin.stable.latest.default; 30 | rust-dev-toolchain = pkgs.rust-toolchain.override { 31 | extensions = [ "rust-src" ]; 32 | }; 33 | }) 34 | ]; 35 | pkgs = import nixpkgs { 36 | inherit system overlays; 37 | }; 38 | craneLib = 39 | (crane.mkLib pkgs).overrideToolchain pkgs.rust-toolchain; 40 | lib = pkgs.lib; 41 | stdenv = pkgs.stdenv; 42 | commonNativeBuildInputs = with pkgs; [ 43 | libiconv 44 | libtool 45 | libxml2 46 | libxslt 47 | llvmPackages.libclang 48 | openssl 49 | pkg-config 50 | xmlsec 51 | ]; 52 | fixtureFilter = path: _type: 53 | builtins.match ".*test_vectors.*" path != null || 54 | builtins.match ".*\.h" path != null; 55 | sourceAndFixtures = path: type: 56 | (fixtureFilter path type) || (craneLib.filterCargoSources path type); 57 | src = lib.cleanSourceWith { 58 | src = ./.; 59 | filter = sourceAndFixtures; 60 | }; 61 | cargoFile = builtins.fromTOML (builtins.readFile ./Cargo.toml); 62 | commonArgs = { 63 | pname = "samael"; 64 | inherit src; 65 | version = cargoFile.package.version; 66 | 67 | # Need to tell bindgen where to find libclang 68 | LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; 69 | 70 | # Set C flags for Rust's bindgen program. Unlike ordinary C 71 | # compilation, bindgen does not invoke $CC directly. Instead it 72 | # uses LLVM's libclang. To make sure all necessary flags are 73 | # included we need to look in a few places. 74 | # See https://web.archive.org/web/20220523141208/https://hoverbear.org/blog/rust-bindgen-in-nix/ 75 | BINDGEN_EXTRA_CLANG_ARGS = "${builtins.readFile "${stdenv.cc}/nix-support/libc-crt1-cflags"} \ 76 | ${builtins.readFile "${stdenv.cc}/nix-support/libc-cflags"} \ 77 | ${builtins.readFile "${stdenv.cc}/nix-support/cc-cflags"} \ 78 | ${builtins.readFile "${stdenv.cc}/nix-support/libcxx-cxxflags"} \ 79 | -idirafter ${pkgs.libiconv}/include \ 80 | ${lib.optionalString stdenv.cc.isClang "-idirafter ${stdenv.cc.cc}/lib/clang/${lib.getVersion stdenv.cc.cc}/include"} \ 81 | ${lib.optionalString stdenv.cc.isGNU "-isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc} -isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc}/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${lib.getVersion stdenv.cc.cc}/include"} \ 82 | "; 83 | 84 | nativeBuildInputs = commonNativeBuildInputs; 85 | cargoExtraArgs = "--features xmlsec"; 86 | cargoTestExtraArgs = "--features xmlsec"; 87 | }; 88 | # Build *just* the cargo dependencies, so we can reuse 89 | # all of that work (e.g. via cachix) when running in CI 90 | cargoArtifacts = craneLib.buildDepsOnly commonArgs; 91 | samael = craneLib.buildPackage (commonArgs // { 92 | inherit cargoArtifacts; 93 | }); 94 | in 95 | rec { 96 | # `nix build` 97 | packages.default = samael; 98 | 99 | # `nix develop` 100 | devShells.default = pkgs.mkShell { 101 | # Need to tell bindgen where to find libclang 102 | LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; 103 | 104 | # Set C flags for Rust's bindgen program. Unlike ordinary C 105 | # compilation, bindgen does not invoke $CC directly. Instead it 106 | # uses LLVM's libclang. To make sure all necessary flags are 107 | # included we need to look in a few places. 108 | # See https://web.archive.org/web/20220523141208/https://hoverbear.org/blog/rust-bindgen-in-nix/ 109 | BINDGEN_EXTRA_CLANG_ARGS = "${builtins.readFile "${stdenv.cc}/nix-support/libc-crt1-cflags"} \ 110 | ${builtins.readFile "${stdenv.cc}/nix-support/libc-cflags"} \ 111 | ${builtins.readFile "${stdenv.cc}/nix-support/cc-cflags"} \ 112 | ${builtins.readFile "${stdenv.cc}/nix-support/libcxx-cxxflags"} \ 113 | -idirafter ${pkgs.libiconv}/include \ 114 | ${lib.optionalString stdenv.cc.isClang "-idirafter ${stdenv.cc.cc}/lib/clang/${lib.getVersion stdenv.cc.cc}/include"} \ 115 | ${lib.optionalString stdenv.cc.isGNU "-isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc} -isystem ${stdenv.cc.cc}/include/c++/${lib.getVersion stdenv.cc.cc}/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${lib.getVersion stdenv.cc.cc}/include"} \ 116 | "; 117 | 118 | buildInputs = with pkgs; [ rust-dev-toolchain nixpkgs-fmt ]; 119 | nativeBuildInputs = commonNativeBuildInputs; 120 | }; 121 | 122 | checks = { 123 | # Build the crate as part of `nix flake check` for convenience 124 | inherit samael; 125 | 126 | # Run clippy (and deny all warnings) on the crate source, 127 | # again, resuing the dependency artifacts from above. 128 | # 129 | # Note that this is done as a separate derivation so that 130 | # we can block the CI if there are issues here, but not 131 | # prevent downstream consumers from building our crate by itself. 132 | samael-clippy = craneLib.cargoClippy (commonArgs // { 133 | inherit cargoArtifacts; 134 | cargoClippyExtraArgs = "--all-targets"; #-- --deny warnings 135 | }); 136 | 137 | samael-doc = craneLib.cargoDoc (commonArgs // { 138 | inherit cargoArtifacts; 139 | }); 140 | 141 | # Check formatting 142 | samael-fmt = craneLib.cargoFmt { 143 | inherit src; 144 | }; 145 | 146 | # Audit dependencies 147 | samael-audit = craneLib.cargoAudit { 148 | inherit src advisory-db; 149 | }; 150 | 151 | # Run tests with cargo-nextest 152 | # Consider setting `doCheck = false` on `samael` if you do not want 153 | # the tests to run twice 154 | samael-nextest = craneLib.cargoNextest (commonArgs // { 155 | inherit cargoArtifacts; 156 | cargoExtraArgs = ""; 157 | cargoNextestExtraArgs = "--features xmlsec"; 158 | partitions = 1; 159 | partitionType = "count"; 160 | }); 161 | }; 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /src/crypto/xmlsec/wrapper/xmldsig.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Wrapper for XmlSec Signature Context 3 | //! 4 | use crate::crypto::xmlsec::wrapper::bindings; 5 | 6 | use super::XmlSecKey; 7 | use super::XmlSecResult; 8 | use super::{XmlDocument, XmlSecError}; 9 | 10 | use std::os::raw::c_uchar; 11 | use std::ptr::{null, null_mut}; 12 | 13 | /// Signature signing/veryfying context 14 | pub struct XmlSecSignatureContext { 15 | ctx: *mut bindings::xmlSecDSigCtx, 16 | } 17 | 18 | impl XmlSecSignatureContext { 19 | /// Builds a context, ensuring wrapper is initialized. 20 | pub fn new() -> XmlSecResult { 21 | super::xmlsec_internal::guarantee_xmlsec_init()?; 22 | 23 | let ctx = unsafe { bindings::xmlSecDSigCtxCreate(null_mut()) }; 24 | 25 | if ctx.is_null() { 26 | return Err(XmlSecError::ContextInitError); 27 | } 28 | 29 | Ok(Self { ctx }) 30 | } 31 | 32 | /// Sets the key to use for signature or verification. In case a key had 33 | /// already been set, the latter one gets released in the optional return. 34 | pub fn insert_key(&mut self, key: XmlSecKey) -> Option { 35 | let mut old = None; 36 | 37 | unsafe { 38 | if !(*self.ctx).signKey.is_null() { 39 | old = Some(XmlSecKey::from_ptr((*self.ctx).signKey)); 40 | } 41 | 42 | (*self.ctx).signKey = XmlSecKey::leak(key); 43 | } 44 | 45 | old 46 | } 47 | 48 | /// Releases a currently set key returning `Some(key)` or None otherwise. 49 | #[allow(unused)] 50 | pub fn release_key(&mut self) -> Option { 51 | unsafe { 52 | if (*self.ctx).signKey.is_null() { 53 | None 54 | } else { 55 | let key = XmlSecKey::from_ptr((*self.ctx).signKey); 56 | 57 | (*self.ctx).signKey = null_mut(); 58 | 59 | Some(key) 60 | } 61 | } 62 | } 63 | 64 | /// Takes a [`XmlDocument`][xmldoc] and attempts to sign it. For this to work it has to have a properly structured 65 | /// `` node within, and a XmlSecKey must have been previously set with [`insert_key`][inskey]. 66 | /// 67 | /// # Errors 68 | /// 69 | /// If key has not been previously set or document is malformed. 70 | /// 71 | /// [xmldoc]: http://kwarc.github.io/rust-libxml/libxml/tree/document/struct.Document.html 72 | /// [inskey]: struct.XmlSecSignatureContext.html#method.insert_key 73 | pub fn sign_document(&self, doc: &XmlDocument, id_attr: Option<&str>) -> XmlSecResult<()> { 74 | self.key_is_set()?; 75 | 76 | let doc_ptr = doc.doc_ptr(); 77 | let root = if let Some(root) = doc.get_root_element() { 78 | root 79 | } else { 80 | return Err(XmlSecError::RootNotFound); 81 | }; 82 | 83 | let root_ptr = root.node_ptr() as *mut bindings::xmlNode; 84 | 85 | if let Some(id_attr) = id_attr { 86 | let cid = 87 | std::ffi::CString::new(id_attr).map_err(|_| XmlSecError::InvalidInputString)?; 88 | 89 | unsafe { 90 | let mut list = [cid.as_bytes().as_ptr(), null()]; 91 | bindings::xmlSecAddIDs( 92 | doc_ptr as *mut bindings::xmlDoc, 93 | root_ptr, 94 | list.as_mut_ptr(), 95 | ); 96 | } 97 | } 98 | 99 | let signode = find_signode(root_ptr)?; 100 | self.sign_node_raw(signode) 101 | } 102 | 103 | /// Takes a [`XmlDocument`][xmldoc] and attempts to verify its signature. For this to work it has to have a properly 104 | /// structured and signed `` node within, and a XmlSecKey must have been previously set with 105 | /// [`insert_key`][inskey]. 106 | /// 107 | /// # Errors 108 | /// 109 | /// If key has not been previously set or document is malformed. 110 | /// 111 | /// [xmldoc]: http://kwarc.github.io/rust-libxml/libxml/tree/document/struct.Document.html 112 | /// [inskey]: struct.XmlSecSignatureContext.html#method.insert_key 113 | pub fn verify_document(&self, doc: &XmlDocument, id_attr: Option<&str>) -> XmlSecResult { 114 | self.key_is_set()?; 115 | 116 | let doc_ptr = doc.doc_ptr(); 117 | let root = if let Some(root) = doc.get_root_element() { 118 | root 119 | } else { 120 | return Err(XmlSecError::RootNotFound); 121 | }; 122 | 123 | let root_ptr = root.node_ptr() as *mut bindings::xmlNode; 124 | 125 | if let Some(id_attr) = id_attr { 126 | let cid = 127 | std::ffi::CString::new(id_attr).map_err(|_| XmlSecError::InvalidInputString)?; 128 | 129 | unsafe { 130 | let mut list = [cid.as_bytes().as_ptr(), null()]; 131 | bindings::xmlSecAddIDs( 132 | doc_ptr as *mut bindings::xmlDoc, 133 | root_ptr, 134 | list.as_mut_ptr(), 135 | ); 136 | } 137 | } 138 | 139 | let signode = find_signode(root_ptr)?; 140 | self.verify_node_raw(signode) 141 | } 142 | 143 | /// Takes a `` [`Node`][xmlnode] and attempts to verify it. For this to work, a XmlSecKey must have 144 | /// been previously set with [`insert_key`][inskey]. 145 | /// 146 | /// # Errors 147 | /// 148 | /// If key has not been previously set, the node is not a signature node or the document is malformed. 149 | /// 150 | /// [xmlnode]: http://kwarc.github.io/rust-libxml/libxml/tree/document/struct.Node.html 151 | /// [inskey]: struct.XmlSecSignatureContext.html#method.insert_key 152 | pub fn verify_node(&self, sig_node: &libxml::tree::Node) -> XmlSecResult { 153 | self.key_is_set()?; 154 | if let Some(ns) = sig_node.get_namespace() { 155 | if ns.get_href() != "http://www.w3.org/2000/09/xmldsig#" 156 | || sig_node.get_name() != "Signature" 157 | { 158 | return Err(XmlSecError::NotASignatureNode); 159 | } 160 | } else { 161 | return Err(XmlSecError::NotASignatureNode); 162 | } 163 | 164 | let node_ptr = sig_node.node_ptr(); 165 | self.verify_node_raw(node_ptr as *mut bindings::xmlNode) 166 | } 167 | } 168 | 169 | impl XmlSecSignatureContext { 170 | fn key_is_set(&self) -> XmlSecResult<()> { 171 | unsafe { 172 | if !(*self.ctx).signKey.is_null() { 173 | Ok(()) 174 | } else { 175 | Err(XmlSecError::KeyNotLoaded) 176 | } 177 | } 178 | } 179 | 180 | fn sign_node_raw(&self, node: *mut bindings::xmlNode) -> XmlSecResult<()> { 181 | let rc = unsafe { bindings::xmlSecDSigCtxSign(self.ctx, node) }; 182 | 183 | if rc < 0 { 184 | Err(XmlSecError::SigningError) 185 | } else { 186 | Ok(()) 187 | } 188 | } 189 | 190 | fn verify_node_raw(&self, node: *mut bindings::xmlNode) -> XmlSecResult { 191 | let rc = unsafe { bindings::xmlSecDSigCtxVerify(self.ctx, node) }; 192 | 193 | if rc < 0 { 194 | return Err(XmlSecError::VerifyError); 195 | } 196 | 197 | match unsafe { (*self.ctx).status } { 198 | bindings::xmlSecDSigStatus_xmlSecDSigStatusSucceeded => Ok(true), 199 | _ => Ok(false), 200 | } 201 | } 202 | } 203 | 204 | impl Drop for XmlSecSignatureContext { 205 | fn drop(&mut self) { 206 | unsafe { bindings::xmlSecDSigCtxDestroy(self.ctx) }; 207 | } 208 | } 209 | 210 | fn find_signode(tree: *mut bindings::xmlNode) -> XmlSecResult<*mut bindings::xmlNode> { 211 | let signode = unsafe { 212 | bindings::xmlSecFindNode( 213 | tree, 214 | &bindings::xmlSecNodeSignature as *const c_uchar, 215 | &bindings::xmlSecDSigNs as *const c_uchar, 216 | ) 217 | }; 218 | 219 | if signode.is_null() { 220 | return Err(XmlSecError::NodeNotFound); 221 | } 222 | 223 | Ok(signode) 224 | } 225 | -------------------------------------------------------------------------------- /test_vectors/response_encrypted.xml: -------------------------------------------------------------------------------- 1 | 2 | saml-mock 3 | 4 | 5 | 6 | 7 | K9oaUFc6pchkIcxNaieObk3wzdKiX6ylQpxVnkJf7DsMeGmboY/EDzC0QswMldqAc5JQ/XrdnXJ7Z7HMUr9qg67xBmrXMm/OI+Dydlt0919G209tCgXKg4VFsvHT31XmAYdmz5YZlUXcKRXIw+sfiOdLcWShh4pabO1c5ZVXaBo= 8 | 9 | oznJ9crdMwXIgSQYIiqFQqcW16nu2/vvuLhmwr0XdSxMCTii6hxJnCeiBJKQD/OBppMgGJU90dBPvXe4pRGsnjD/aVqfLIxz1Jmyh8slkDv20IMR3Vd82v2A1bRwKnFQ+RtsV8pFrZaGn44wLRuPt/nFvEnlsZTJ0JiDznx0Z1oebuTGZVwDSJuKFkCDVgF64pya8gpI1jDvDPCrPf+12kNMhm+Ueuf3pRGytnI5L78mwOMA32C7Oi/GPNxEf4tlqL3DlWPTM74D6bjQFAYK3sRAUw4a60hxOTpRSyFC3Cqm5HIAGUMATUms+bpDi7ViVFMO85+oTcgrSgSsSC/C/ge8EnCe2ZegHxt/lZsruz/iJgZKaAJOzdL7ZGn8ppvhR/lN5QUi6WCyIwr1faolN3zFLqIXzmPZS77mlJkHcvq0giDs3os23at5ya+nEQXkgzlvUg8dt0JnwRTVWorzFFIWuyRQlAkSXmllpgNHzbtfgmN+8k2D5ZNhWz3YzTpT2FlDcf5tE4j5R5icD2uBbnao1yT4w4UFS3iBSMBn7/CpLG5nKHR4MZUXCOMg62egvD0aAAnSqWIpTfcVWp0w65LFdep1qit6jjX2d8nrN5nCdGNED6emDcAnB/pOgTPEcXR2jMNvJ720EEZ20TRZ5iMYK5am2tNonVHH3Wj8BjOOcO91d5snv8rGwPYP20dVI+0e0QxE98pYHBS0gbkc9J4RRE3KaNDn1SLw8QXt0VTLOrdBEYKt1h5kMak1m6tjNdXoFUIk3V5zlqd9kEUnKfFUd5Y/UbYzOUjWo8u9BD3tx4MWbhcLZcC+C1Gb/4oAR1Y1E9Jya7fc58N4bZ9R122hobEZ8iST2a4uzVJEWva55jdkNjU1VM+VqNpcOM5Y670WmEvTs6T00cfu/1WTGqPxR+awtWv27GqMRsHjdctm9b5OKIhjtKPwWuQpCQ6vK2RAO8K6YvsARq+oGXCYjIKWkv9pzF0NlrhrEDF/r01zHa42NWuUqAfjLrXd//5YsNg4G/D1ldOwc/55q8D1cOmqkn0oM3N0zpGewWfTGii8TU7zN7AjuH7W8xvw9oiGiZa2/dsfBhbuSnqryao2US75ZAkRNN+FzpM/rV3UHgQz+0Kz5hGmdw9hhm4KZcPqbWMl3Jm0EBtIPkyjECd8CScvvbGdbqriU96zoOBSQwK9uREIOw3xfjAsNPqT7+YJjRdybSAk1Gq2B1LTod38Z8+6NzIeA0VlrTFFk+1/+6Njj+A4USfBctfxw7/Wb6m7MEGldcy/mhzImTyZzs8laIezksxNGdwLaXyBTwR428iKDAQKIMvZjq5Knu+9qsfPm6u7nlD3gvBwcHMfKKEANXFpdMRakjkx/ua6xugUwgCmGjKMszosCMZSkBsB/TRYZqXbziWxIDeQNPbH/HpT/95nnDLNrR9s3GweKGBZL7hk+6nkXZvR87p7C+0OZ7PBwJmkde/wNzMiEtMWjmXIA2MbzkWiSN5YQw2NSN89Ao0frVSEPoH/a6T8y7/nv7qsC+k6ETAm8CbsHYNY/L5UqZgGDqBuxz593QiBBAQgBUopvN37QiL4UHsnojevOKwWQG9egwZBof7YlUXnWP4u6tZWBy25/4+ne+3oT1nKd1eccIi72/cxALXuU3yt84mkWvj4T3NXqHSp399Il8hyg4w3ObMorqGyjS9KDJYRtXiUwLH9XhudFOYcSA27W1Jb471QYPP4MAOh1m+CU67asmShEX3ihMHcRk/c6jo6HyrFP2lq4CpiOQ78Z69vJBUYULfaDLbRLQJCFVDQz06cQu3nsKHEJ8QjC5BO1+hGiNRRg/xc8j7jTWAj51vaB+JOYtgIGryrUcIFIfJIDrryMMoZxbivwOV/i5ABz/uCbAxyalCcOj5mu/MQmnlFrO/ORQ6upb2yAUjii0Iie+BBMRr65z7boqxBh5MbF3HIZXCe9IngM9X8h4JLlL1gPda0i2HGsEKpkbII/SfyPSknuJjybL5fpayYFLNvEKMi95n0lYXN3vaJKYGQ/qTlbq4WJAAlTsvuEg3J/t27GGsxAFHEywEi/dAPjRShEpDPwNajkY8FwHugmtqmgNFr9WuxL9dDXwY7pH5gRMvKBtfH6lO2E/kEUpbKBO18mp/ddL8kB2AKCvKE+t4c0v9LFjuLHRxg6cMGlHEPHb1RWFdIT1p6KDnJa+YtiZsEcLLnToh4J0U1qbU9rvFHA5LWBTp8k2YIkMnB+zuIdKyvHHFEai2eMpdZTjjFhheB0HgiKrC2TV3Kw2w6gQEFvHWlL39ItBNxl13p/iE9Rk3j6h6nUu1MqoCr5CFhf7mw9YZBjFlntPHJW0bCkiP5jSjTywjcee8SFLMZ5O621Z3LV1ffZnw/9XJW/LItAOhAKCtWyoEjcz7ZQuo3vDYpEcQ96n64hv5z4jbkxIzgoFZC7V69R0T0JlezJE5auEThqQe9zVAAeO9OV+gkmRtMcfggxuF67ooIiiZYQyqwL1Hy22R6ZY4JDYkWw42qQomo2WKIZH7Mu+B/erKTC/+aYK4euzrpM2w15YK0Ts0pd5oXqOG0ug0l/z3rTY4W8C3mKEg4UpU5dxAUcxGXsm8R6iN06/VC6KyZe815xH9VNEmX6K/uS9Xd8ZjhQ1YWNZi5YH0G9IxNMxZjCzPwDlzMiUMUtE5GDo7zxY5M+4He3TLy7sxmG66LhX1AnWh3VegEQYIZz0whLQeeXlgfnROcW5a5X54EVTJbLM5eK0L82g64KsvjRlgewxC6Y61B1Yr3UBuMFXKo7LcsYB4asf/v4tjJWCmzbzX07FDqJ9HwTdgmRed0tQWxddO+wk1l7qKw+pnfVeTpKeV0c7OH3lLZ2yRItJVUUoX8EodtC+4xp+QPk87c/M2tdNJdnIMu7gjhLoGmaj6v78Dmx139SyiRiCVpOLEGv5FQz4koEK6s/LIaKEPrMuwZRfcJyPaKJSXsVQNuEiMWHsZrNXpQ7ilN7IHPM6INdTwUpN3iAYyeQNtyUTvFcUIgM7a0oMmrV8zmLOmIY/JKvxXzRYyv/Q8wnZiwce1t+GfuxRxW88RvxAuDZW8YgA4u0fz64y557L/jqVWHdYJLHrsyr/5zamwNxjvFwwSLwekWgDUA+0lPEkIxFDhp0g2dpIvgargm43j6Qb6kOSljzcoVrEFZ84KVsnpK3CU1YFqSJl4KPHBEyHMVV+emeBTz8A0Glw1IvIdnXsiTd99XIuxDqmoaH5OQUlL6YfMozm1hkJwfXTIDcyH54rnm1tXDIg30T61sQH6QkSWA9wUORgOm39eXtUQnun7NE6b3cWqbshdck7MhJ7/6bjdIq/J6VW9belismQAVRrR7pwekFW7E8r8CdVam2hQzPth8rfVx0xkeWRRryagXOg9cmcPox00+G9rdDejLCNiUcbwZkD+aHurzFtQ4/77XY9MHKyIMmWy47u3qKE+X1x5jSjAb//R82AzgIY6x17AAMoqwkBvoMs685BArxWN8ImVtHCsSixhrKMU5RwttX74XhLoxlOSthvo1FZJDGT11RPOY7eAsfHTgLX7qGLfSXvDY208TE3jlUTCGQ6D5+GQb1Yhh0+wE64KxUfk4R/W4ltVqWLDROuTCpqK/KTMZVe1zjLRyUuPJr7XnI4gQ6ZCvjRL6HIzCwtxDG9UvZUwHa2MVpTctVwvwNdj2zMKBrRzj/QFwyskqMsdt77f+BSTRlHXQk1nJehVsbB0BsvR5k6NENEBesqVz3kjjtcmiRfeNQaEpkmo/jgvp55hVi8wHR5+oWhuHpcDtDfQ4ue/os+MnwXHB043/ZdNDeVxGsAg37ySKMORAjBtAV4fMIhoI+DDjPWx21cnxiY3x+IcHlqcWlMFsjrdzw/hPIjNAFLMThX2dvlMyoldlYpcFnPI7NOuUZXhAgc6Z+AB/9ew4M+0LUuYbKLjg0f+QUltc5W2teELeX2H7ysMhqhKEdnvw6xBNb8gWhCznN8j45THgXUY8c5+VXYV2gLlO6IPrcY3/m//QgH+HqHXAUDfl7FIO5MNVlreG9ly/GQgvCv6WVxFLLiB689cJFIkhGEUaas0T86iGptJfSW+Md/KVoPoyUaV0bffN8rfXEkcOpDejHJytL5Lie1vmeacOI67zy/blI2mYdqFLCcr7MuhHjNnI8NLAa3GhclSecGe5WyzGfZR8DEP2oxdtRRfHMvabTvQDEHZGQJv0Nyh/oB8lqCD3225L1BL3nFP/rs9bT6Meiu3pkruf9jTjQgfwnJHhOGnEeGs5sqg8Nb5OpDn8oiEbvjpIhXyEG7BuYN/sW4POHtFyUzyFIqWd0dzEDZRc902H45ugtJXSRJHEMNXbcJ3jTBACDuYMlVrnmqm5hCH62Nzku4uoq8XLgQO02olKMpfzaQfm/KMNuOCj4c8fNvUQk733HqgGk+wqP79381Bg66mZuOgtl343+tfv4N6UQr9nl3yhkU0d0zWHTKjVkZTgt+rBszg5AWevm1ShFwz727vpLmRTVpvIJumxU2AGw9XJWbb6oglQ+A6uGiuBqOEH6VuUAbSp+0Np989iNjs4z0mh3NamLkU7CFQPCi4EiKJRBA/C+I7qae58hjWG87VAph2BXBcdM6gk/ewbYXF5o5VeEm/Gp3eZqbU7l6z4t5nn1NMRwiqKv44tilkUr3GkTR/3jRgQst4Aqy0U3RpNhyAQ8URHBbx2Mx1Blb7peFVH03nP6cjP4pIl+3e00C/UcF/v9mqFR9d2+atppSpr5m6Anwc8F91FnM9qUBEObAdkJCJeIyx26Hwwpv9vddgCRpX7+EcX9uxvzF+id2HWK22XLhBsbd1Q2dxmMe8e+zFWZ/LIQd8TTUGKHkxwEaasFDZ54dUlx3qIJcMupgRKJqRSqEyGbx53r7HhqXOhLfUDmmzQel4ap4UvVOJOppdgu+nbKlor+UMUiGjdgTvBIblkckdadDAJvJH1I6ajpwJgkfhc3rQa6uo0UvUP6zoXiVEDZhbcXT/HIPXKIZ3SqW9DTdE2KqmkX2lfrW8Q9xozx4IrF7JzFd5V21o2lntUStzXGx/ZfBxGsUP8kBG7XIoY4xJkFn6lb2sYAiq2rN1z8165Tg53FRwJNpdrxp6MCAUyyO70M0vaxaugbFi0Z+7HxrG/aWi9Q6RLEofHHBW6oYE9yNhGR0/kkijDWlzZWCURuRkdTOrvqIN0/H8gtOiudGaLwtzOUkmV9jsWTp8uPFzRbXS+wJ+aBgmz8TxnBBBo2sHVwl/DL68bKsqOO1pWPjVaYWuQoZ8QuyzL4z6FOuqwpNdJMQFzU5F1ueLpJlkpPxarM3lA9LmWVqLd17kJF2R/5OVz1AUMpJiUkMLPM+kIOLSp1zUE06uHJawyIc+x2OM9SVO41al/g6QhN/TkLqjsN15FOsnwgu3OTK+9IiWE+94hkcvSa6hquBiHmoyYH+ac82n8vzY5iOHi+iv2Z9252p94u4FjOoamxKVlzBa0NRC20sVxetIByD60wAGVsaMhSnB8pxDgfnmhvup6v+lkTXtLUsm+8YjEEApXX6X4TOmIsBz1NCcBCsvm9c7nA/lLCswLxJb+bgSLVEyG2d30Qchh7UjzZ4K6B8ghuC+84WeP49rjpLr1duoUFvVvsoie/jVleo2EGeItjc0fBfqkbEcdAKn9dal6PmxQN+PB9bx1Y7m9LaHDou/R/b2xEXeNT8lOBhAVRwYkIoFWQWdbteOyNGyehCvAuVkAyDd62tTkgH7xzGn/gRN8bJCxpGFm4mtI0KX67JNSb8gwoD5JVM24wKLDcR+T/zbnXkWwxpDVyioS9FX95N6ANTUNceDxPKo8iGegTh2DwwoP8+IurCClziaFkqMNg2s24pbT+zPWeE8S7n+nmBNXSNY4UIXQQTWSeg0FX0l9U7fntb0mzpy5EveL6uJCq0bqbyqK+4q0qy0HEGU2HOIGQdin9XcG+XdO83yK/leXhrocvVsE2NRlW6AIS8iKa7X/OigeUB3D5YOZ3pZs5mUriI/socxmxBSqODA2up4RTaGl8m7bY7fEaDNWXtOoXVhwv7dDDawvc2zGq10UrOjnZBBzTwzXjFZL/xYlzbxsOxMvdr2RT5q1W2t+PTXMpb11NfYYacrjwb1bJdsf0swJU7fTEo6H4BQRzXlPf/97o9N5B9tiE/7o16NRdKg3R+kozrojMWRdxLZF2+dKEV00h/bPGzc7Fop/Ny5NanYwPbhW0Z2nJ30712+/KBzsuSm/SEeN4r/yaJbRmBcwIOCHg+dI1xYCBvAfUTLj/Ld779ysE/JTbgr38D8flIsEdRt8mFcjNkbPrbzW1KH6kphFiEu1vRHJlHniGhn2imMQjgGsYGELRQ0OnykurFIOCs/oOhRwtb8etWiuQ6kRY5RIZZ7Xh4JbsipHYS4116A4zywHLklYwjGbZKxVxncCxGvD4BxHe3RuZPBbmfVD3knS6/8W3M3Ca3uIOjt/eYLMTAPvdUl/KE/5OaR/yB+oLNHJ+1E4p8Itpg0l4AfFqxR/GykgPfPVusZ4Pl5E+yRAvbux/UzGsJ8iWhqGnlxbgMDDtCVxF6wBxRMEvHBP6OIqjzUpFGcy8EItL5bRqmiXnwfykeaj9eLPMINRkclOtsg2mIsOcULP578momqm2My2TexzSf0yaW6urL3Vz57qQ0/TqKBsnwwNpTt90lam761P40dIrLEZ9CUZjNd12ZuSwwoEFFhHUBu/7MeHFP+SWTbnNdwbRSQXVjDYBwJjs5sKQnMmkQ9d/518R0fjMhwsBdXmYmiSk1fTK46aurWlnDtzK6ib4TlEQFbUdULN5TBm/sl30oPtrQgQb8ERcpfZ8R4W5JZQje4iMznHzQ5i0ksm/CdAGx224RIYN5WyEt+NmX9caTmWfxYEbTe2D9SlsLQVP+GrUMWckzWpqBEAF6V445XfK8uvVAo5yHg501GDKCUQ7XH1keyjpiHa2yCBvX2SS8FpOUcj6tR30YFDwJyGx8pyWYBXPucbbixxNNQG0q7CAXe/cjdpFlpI3D9/qky3tcD9W/HO5xlJpzVBegni6vm7aO1DQPFBbFEsFNTMHqqBzgKktorUBdsoUtg+OaPKmKiLCpC1kahKVDge2jgz0ZhHmC5gHNJWyC6Gly9Vxt/lY7bQ1Xznl9x7GmQOMmoluLC53OziBbYjAp9M1ItGu5KH/5EoI7MW9h7rf/S2yHpXchzT4P2ZbTZkuulABQwooIz0EU+zjp8UC79s6SE/jXMLRHP8FV/+jQkpHPq7Js0R9LdtlQ/BEi6SBjrHsEyk9PJEooRSn50Y/verUdrmyOXr8ZPdkhEVWyL0y/0daWPqMDkiTt7vCv1JqbupHi3WaYCED3RuFc2LEEdMLl8YaayeX8rfTrRrkxkkSu+orxEiZjtBoNrSgkUWp4Cz2UljJthYp/2e4qYMSQ2k7AJSn+CT16Bc3zV9wBEzdKCMPs40c++hwrgMiPkZ3+irbDq6lFXfd77/s6zCTfcha/kG/hOZqe8N6Yx8GooZlv3x7dHgGHIAUeHcUDE6CV1IsPS1OzgONBxx/2y3CEF8/2ZcKZssVbdu+B6wR3kRTSzyuDMAOfJOOyJI2f45/MD/sAHmdJvhQuHNxgXJWNpobZU9teb9OiQb9uJ6lF8+kNI/p+1utu/JzTQob4R894yqCwAYls/zb0i+c/WH/SlUvvI3A5MZbMV0xMgGgOv6TIMNpHfjsKrE1SNunPKaS6TpMqzp6x7rsLyUC5uRoF6P8xg7YolVY7X+plLK5Ye8hJ0YULXfTQiDDEHEsuVDhLtEG5hUda7NoIeVhhkl2CbeK4PxeyaK2R1RyrwRmHtvz/fhHswAN0MNaHbFBSATlqCs+GdvNozGSHbRz/wVSE6WVkmIRCufq/I7SqMIaKChdxlskrD3QSk1S+O0AuKzWeKx/WbSlV86FFDBAQRe0pRig5GAE9TUdqhDcbtW0OvyPjH6 10 | 11 | -------------------------------------------------------------------------------- /src/schema/subject.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; 3 | use quick_xml::Writer; 4 | use serde::Deserialize; 5 | use std::io::Cursor; 6 | 7 | pub enum SubjectType<'a> { 8 | BaseId, 9 | NameId(&'a str), 10 | EncryptedId, 11 | } 12 | 13 | impl<'a> SubjectType<'a> { 14 | fn saml_element_name(&self) -> &'static str { 15 | match self { 16 | SubjectType::BaseId => "saml2:BaseID", 17 | SubjectType::NameId(_) => "saml2:NameID", 18 | SubjectType::EncryptedId => "saml2:EncryptedID", 19 | } 20 | } 21 | } 22 | 23 | impl<'a> TryFrom> for Event<'_> { 24 | type Error = Box; 25 | 26 | fn try_from(value: SubjectType) -> Result { 27 | (&value).try_into() 28 | } 29 | } 30 | 31 | impl<'a> TryFrom<&SubjectType<'a>> for Event<'_> { 32 | type Error = Box; 33 | 34 | fn try_from(value: &SubjectType) -> Result { 35 | let mut write_buf = Vec::new(); 36 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 37 | let elem_name = value.saml_element_name(); 38 | let root = BytesStart::new(elem_name); 39 | writer.write_event(Event::Start(root))?; 40 | if let SubjectType::NameId(content) = value { 41 | writer.write_event(Event::Text(BytesText::from_escaped(*content)))?; 42 | } 43 | writer.write_event(Event::End(BytesEnd::new(elem_name)))?; 44 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 45 | write_buf, 46 | )?))) 47 | } 48 | } 49 | 50 | const NAME: &str = "saml2:Subject"; 51 | const SCHEMA: (&str, &str) = ("xmlns:saml2", "urn:oasis:names:tc:SAML:2.0:assertion"); 52 | 53 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 54 | pub struct Subject { 55 | #[serde(rename = "NameID")] 56 | pub name_id: Option, 57 | #[serde(rename = "SubjectConfirmation")] 58 | pub subject_confirmations: Option>, 59 | } 60 | 61 | impl TryFrom for Event<'_> { 62 | type Error = Box; 63 | 64 | fn try_from(value: Subject) -> Result { 65 | (&value).try_into() 66 | } 67 | } 68 | 69 | impl TryFrom<&Subject> for Event<'_> { 70 | type Error = Box; 71 | 72 | fn try_from(value: &Subject) -> Result { 73 | let mut write_buf = Vec::new(); 74 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 75 | let mut root = BytesStart::new(NAME); 76 | root.push_attribute(SCHEMA); 77 | 78 | writer.write_event(Event::Start(root))?; 79 | if let Some(name_id) = &value.name_id { 80 | let event: Event<'_> = name_id.try_into()?; 81 | writer.write_event(event)?; 82 | } 83 | if let Some(subject_confirmations) = &value.subject_confirmations { 84 | for confirmation in subject_confirmations { 85 | let event: Event<'_> = confirmation.try_into()?; 86 | writer.write_event(event)?; 87 | } 88 | } 89 | writer.write_event(Event::End(BytesEnd::new(NAME)))?; 90 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 91 | write_buf, 92 | )?))) 93 | } 94 | } 95 | 96 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 97 | pub struct SubjectNameID { 98 | #[serde(rename = "@Format")] 99 | pub format: Option, 100 | 101 | #[serde(rename = "$value")] 102 | pub value: String, 103 | } 104 | 105 | impl SubjectNameID { 106 | fn name() -> &'static str { 107 | "saml2:NameID" 108 | } 109 | } 110 | 111 | impl TryFrom for Event<'_> { 112 | type Error = Box; 113 | 114 | fn try_from(value: SubjectNameID) -> Result { 115 | (&value).try_into() 116 | } 117 | } 118 | 119 | impl TryFrom<&SubjectNameID> for Event<'_> { 120 | type Error = Box; 121 | 122 | fn try_from(value: &SubjectNameID) -> Result { 123 | let mut write_buf = Vec::new(); 124 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 125 | let mut root = BytesStart::new(SubjectNameID::name()); 126 | 127 | if let Some(format) = &value.format { 128 | root.push_attribute(("Format", format.as_ref())); 129 | } 130 | 131 | writer.write_event(Event::Start(root))?; 132 | writer.write_event(Event::Text(BytesText::from_escaped(value.value.as_str())))?; 133 | writer.write_event(Event::End(BytesEnd::new(SubjectNameID::name())))?; 134 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 135 | write_buf, 136 | )?))) 137 | } 138 | } 139 | 140 | const SUBJECT_CONFIRMATION_NAME: &str = "saml2:SubjectConfirmation"; 141 | 142 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 143 | pub struct SubjectConfirmation { 144 | #[serde(rename = "@Method")] 145 | pub method: Option, 146 | #[serde(rename = "NameID")] 147 | pub name_id: Option, 148 | #[serde(rename = "SubjectConfirmationData")] 149 | pub subject_confirmation_data: Option, 150 | } 151 | 152 | impl TryFrom for Event<'_> { 153 | type Error = Box; 154 | 155 | fn try_from(value: SubjectConfirmation) -> Result { 156 | (&value).try_into() 157 | } 158 | } 159 | 160 | impl TryFrom<&SubjectConfirmation> for Event<'_> { 161 | type Error = Box; 162 | 163 | fn try_from(value: &SubjectConfirmation) -> Result { 164 | let mut write_buf = Vec::new(); 165 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 166 | let mut root = BytesStart::new(SUBJECT_CONFIRMATION_NAME); 167 | if let Some(method) = &value.method { 168 | root.push_attribute(("Method", method.as_ref())); 169 | } 170 | writer.write_event(Event::Start(root))?; 171 | if let Some(name_id) = &value.name_id { 172 | let event: Event<'_> = name_id.try_into()?; 173 | writer.write_event(event)?; 174 | } 175 | if let Some(subject_confirmation_data) = &value.subject_confirmation_data { 176 | let event: Event<'_> = subject_confirmation_data.try_into()?; 177 | writer.write_event(event)?; 178 | } 179 | writer.write_event(Event::End(BytesEnd::new(SUBJECT_CONFIRMATION_NAME)))?; 180 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 181 | write_buf, 182 | )?))) 183 | } 184 | } 185 | 186 | const SUBJECT_CONFIRMATION_DATA_NAME: &str = "saml2:SubjectConfirmationData"; 187 | 188 | #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)] 189 | pub struct SubjectConfirmationData { 190 | #[serde(rename = "@NotBefore")] 191 | pub not_before: Option>, 192 | #[serde(rename = "@NotOnOrAfter")] 193 | pub not_on_or_after: Option>, 194 | #[serde(rename = "@Recipient")] 195 | pub recipient: Option, 196 | #[serde(rename = "@InResponseTo")] 197 | pub in_response_to: Option, 198 | #[serde(rename = "@Address")] 199 | pub address: Option, 200 | #[serde(rename = "$value")] 201 | pub content: Option, 202 | } 203 | 204 | impl TryFrom for Event<'_> { 205 | type Error = Box; 206 | 207 | fn try_from(value: SubjectConfirmationData) -> Result { 208 | (&value).try_into() 209 | } 210 | } 211 | 212 | impl TryFrom<&SubjectConfirmationData> for Event<'_> { 213 | type Error = Box; 214 | 215 | fn try_from(value: &SubjectConfirmationData) -> Result { 216 | let mut write_buf = Vec::new(); 217 | let mut writer = Writer::new(Cursor::new(&mut write_buf)); 218 | let mut root = BytesStart::new(SUBJECT_CONFIRMATION_DATA_NAME); 219 | if let Some(not_before) = &value.not_before { 220 | root.push_attribute(( 221 | "NotBefore", 222 | not_before 223 | .to_rfc3339_opts(SecondsFormat::Millis, true) 224 | .as_ref(), 225 | )); 226 | } 227 | if let Some(not_on_or_after) = &value.not_on_or_after { 228 | root.push_attribute(( 229 | "NotOnOrAfter", 230 | not_on_or_after 231 | .to_rfc3339_opts(SecondsFormat::Millis, true) 232 | .as_ref(), 233 | )); 234 | } 235 | if let Some(recipient) = &value.recipient { 236 | root.push_attribute(("Recipient", recipient.as_ref())); 237 | } 238 | if let Some(in_response_to) = &value.in_response_to { 239 | root.push_attribute(("InResponseTo", in_response_to.as_ref())); 240 | } 241 | if let Some(address) = &value.address { 242 | root.push_attribute(("Address", address.as_ref())); 243 | } 244 | writer.write_event(Event::Start(root))?; 245 | if let Some(content) = &value.content { 246 | writer.write_event(Event::Text(BytesText::from_escaped(content)))?; 247 | } 248 | writer.write_event(Event::End(BytesEnd::new(SUBJECT_CONFIRMATION_DATA_NAME)))?; 249 | Ok(Event::Text(BytesText::from_escaped(String::from_utf8( 250 | write_buf, 251 | )?))) 252 | } 253 | } 254 | --------------------------------------------------------------------------------