diff --git a/CHANGELOG b/CHANGELOG index ed2be13..b5fe024 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ # Changelog -### Unreleased +### 0.0.16 + - Add support for basic `RequestedAuthnContext` de-/serialization in `AuthnRequest` +- Add support for Elliptic-curve cryptography ### 0.0.15 diff --git a/flake.nix b/flake.nix index d6793a2..cc02bee 100644 --- a/flake.nix +++ b/flake.nix @@ -156,6 +156,8 @@ # the tests to run twice samael-nextest = craneLib.cargoNextest (commonArgs // { inherit cargoArtifacts; + cargoExtraArgs = ""; + cargoNextestExtraArgs = "--features xmlsec"; partitions = 1; partitionType = "count"; }); diff --git a/src/crypto.rs b/src/crypto.rs index 5815d2c..b24eced 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -490,6 +490,7 @@ pub fn gen_saml_assertion_id() -> String { enum SigAlg { Unimplemented, RsaSha256, + EcdsaSha256, } impl FromStr for SigAlg { @@ -497,6 +498,7 @@ impl FromStr for SigAlg { fn from_str(s: &str) -> Result { match s { "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Ok(SigAlg::RsaSha256), + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Ok(SigAlg::EcdsaSha256), _ => Ok(SigAlg::Unimplemented), } } @@ -509,33 +511,45 @@ pub enum UrlVerifierError { } pub struct UrlVerifier { - keypair: openssl::pkey::PKey, + public_key: openssl::pkey::PKey, } impl UrlVerifier { pub fn from_rsa_pem(public_key_pem: &[u8]) -> Result> { let public = openssl::rsa::Rsa::public_key_from_pem(public_key_pem)?; - let keypair = openssl::pkey::PKey::from_rsa(public)?; - Ok(Self { keypair }) + let public_key = openssl::pkey::PKey::from_rsa(public)?; + Ok(Self { public_key }) } pub fn from_rsa_der(public_key_der: &[u8]) -> Result> { let public = openssl::rsa::Rsa::public_key_from_der(public_key_der)?; - let keypair = openssl::pkey::PKey::from_rsa(public)?; - Ok(Self { keypair }) + let public_key = openssl::pkey::PKey::from_rsa(public)?; + Ok(Self { public_key }) + } + + pub fn from_ec_pem(public_key_pem: &[u8]) -> Result> { + let public = openssl::ec::EcKey::public_key_from_pem(public_key_pem)?; + let public_key = openssl::pkey::PKey::from_ec_key(public)?; + Ok(Self { public_key }) + } + + pub fn from_ec_der(public_key_der: &[u8]) -> Result> { + let public = openssl::ec::EcKey::public_key_from_der(public_key_der)?; + let public_key = openssl::pkey::PKey::from_ec_key(public)?; + Ok(Self { public_key }) } pub fn from_x509_cert_pem(public_cert_pem: &str) -> Result> { let x509 = openssl::x509::X509::from_pem(public_cert_pem.as_bytes())?; - let keypair = x509.public_key()?; - Ok(Self { keypair }) + let public_key = x509.public_key()?; + Ok(Self { public_key }) } pub fn from_x509( public_cert: &openssl::x509::X509, ) -> Result> { - let keypair = public_cert.public_key()?; - Ok(Self { keypair }) + let public_key = public_cert.public_key()?; + Ok(Self { public_key }) } // Signed url should look like: @@ -660,9 +674,10 @@ impl UrlVerifier { let mut verifier = openssl::sign::Verifier::new( match sig_alg { SigAlg::RsaSha256 => openssl::hash::MessageDigest::sha256(), + SigAlg::EcdsaSha256 => openssl::hash::MessageDigest::sha256(), _ => panic!("sig_alg is bad!"), }, - &self.keypair, + &self.public_key, )?; verifier.update(data)?; @@ -704,6 +719,61 @@ mod test { .make_authentication_request("http://dummy.fake/saml") .unwrap(); + let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap(); + let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap(); + + let signed_request_url = authn_request + .signed_redirect("", private_key) + .unwrap() + .unwrap(); + + // percent encoeded URL: + // http://dummy.fake/saml?SAMLRequest=..&SigAlg=..&Signature=.. + // + // percent encoded URI: + // /saml?SAMLRequest=..&SigAlg=..&Signature=.. + // + let uri_string: &String = &signed_request_url[url::Position::BeforePath..].to_string(); + assert!(uri_string.starts_with("/saml?SAMLRequest=")); + + let url_verifier = + UrlVerifier::from_x509(&sp.idp_signing_certs().unwrap().unwrap()[0]).unwrap(); + + assert!(url_verifier + .verify_percent_encoded_request_uri_string(uri_string) + .unwrap(),); + } + + #[test] + fn test_verify_uri_ec() { + let private_key = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/ec_private.pem" + )); + + let idp_metadata_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/idp_ecdsa_metadata.xml" + )); + + let response_instant = "2014-07-17T01:01:48Z".parse::>().unwrap(); + let max_issue_delay = Utc::now() - response_instant + chrono::Duration::seconds(60); + + let sp = ServiceProvider { + metadata_url: Some("http://test_accept_signed_with_correct_key.test".into()), + acs_url: Some("http://sp.example.com/demo1/index.php?acs".into()), + idp_metadata: idp_metadata_xml.parse().unwrap(), + max_issue_delay, + ..Default::default() + }; + + let authn_request = sp + .make_authentication_request("http://dummy.fake/saml") + .unwrap(); + + let private_key = openssl::ec::EcKey::private_key_from_pem(private_key).unwrap(); + let private_key = openssl::pkey::PKey::from_ec_key(private_key).unwrap(); + let signed_request_url = authn_request .signed_redirect("", private_key) .unwrap() diff --git a/src/idp/tests.rs b/src/idp/tests.rs index 792a418..c9119bd 100644 --- a/src/idp/tests.rs +++ b/src/idp/tests.rs @@ -272,13 +272,13 @@ fn test_accept_signed_with_correct_key_idp() { ..Default::default() }; - let wrong_cert_signed_response_xml = include_str!(concat!( + let correct_cert_signed_response_xml = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/test_vectors/response_signed.xml", )); let resp = sp.parse_xml_response( - wrong_cert_signed_response_xml, + correct_cert_signed_response_xml, Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]), ); @@ -303,13 +303,44 @@ fn test_accept_signed_with_correct_key_idp_2() { ..Default::default() }; - let wrong_cert_signed_response_xml = include_str!(concat!( + let correct_cert_signed_response_xml = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/test_vectors/response_signed_by_idp_2.xml", )); let resp = sp.parse_xml_response( - wrong_cert_signed_response_xml, + correct_cert_signed_response_xml, + Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]), + ); + + assert!(resp.is_ok()); +} + +#[test] +fn test_accept_signed_with_correct_key_idp_3() { + let idp_metadata_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/idp_ecdsa_metadata.xml" + )); + + let response_instant = "2014-07-17T01:01:48Z".parse::>().unwrap(); + let max_issue_delay = Utc::now() - response_instant + chrono::Duration::seconds(60); + + let sp = ServiceProvider { + metadata_url: Some("http://test_accept_signed_with_correct_key.test".into()), + acs_url: Some("http://sp.example.com/demo1/index.php?acs".into()), + idp_metadata: idp_metadata_xml.parse().unwrap(), + max_issue_delay, + ..Default::default() + }; + + let correct_cert_signed_response_xml = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_vectors/response_signed_by_idp_ecdsa.xml", + )); + + let resp = sp.parse_xml_response( + correct_cert_signed_response_xml, Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]), ); diff --git a/src/schema/authn_request.rs b/src/schema/authn_request.rs index 38d6a3b..83e6134 100644 --- a/src/schema/authn_request.rs +++ b/src/schema/authn_request.rs @@ -290,6 +290,9 @@ mod test { "/test_vectors/authn_request_sign_template.xml" )); + let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap(); + let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap(); + let signed_authn_redirect_url = authn_request_sign_template .parse::()? .signed_redirect("", private_key)? @@ -318,6 +321,9 @@ mod test { "/test_vectors/authn_request_sign_template.xml" )); + let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap(); + let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap(); + let signed_authn_redirect_url = authn_request_sign_template .parse::()? .signed_redirect("some_relay_state_here", private_key)? @@ -347,6 +353,9 @@ mod test { "/test_vectors/authn_request_sign_template.xml" )); + let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap(); + let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap(); + let signed_authn_redirect_url = authn_request_sign_template .parse::()? .signed_redirect("some_relay_state_here", private_key)? diff --git a/src/schema/mod.rs b/src/schema/mod.rs index cbf04cb..597447c 100644 --- a/src/schema/mod.rs +++ b/src/schema/mod.rs @@ -723,9 +723,7 @@ impl LogoutResponse { #[cfg(test)] mod test { - use super::issuer::Issuer; - use super::{LogoutRequest, LogoutResponse, NameID, Status, StatusCode}; - use chrono::TimeZone; + use super::{LogoutRequest, LogoutResponse}; #[test] fn test_deserialize_serialize_logout_request() { diff --git a/src/service_provider/mod.rs b/src/service_provider/mod.rs index 2522a3c..561f19b 100644 --- a/src/service_provider/mod.rs +++ b/src/service_provider/mod.rs @@ -12,7 +12,7 @@ use chrono::prelude::*; use chrono::Duration; use flate2::{write::DeflateEncoder, Compression}; use openssl::pkey::Private; -use openssl::{rsa, x509}; +use openssl::x509; use std::fmt::Debug; use std::io::Write; use thiserror::Error; @@ -88,6 +88,8 @@ pub enum Error { FailedToParseCert { cert: String }, #[error("Unexpected Error Occurred!")] UnexpectedError, + #[error("Tried to use an unsupported key format")] + UnsupportedKey, #[error("Failed to parse SAMLResponse")] FailedToParseSamlResponse, @@ -103,7 +105,7 @@ pub enum Error { #[builder(default, setter(into))] pub struct ServiceProvider { pub entity_id: Option, - pub key: Option>, + pub key: Option>, pub certificate: Option, pub intermediates: Option>, pub metadata_url: Option, @@ -553,7 +555,7 @@ impl AuthnRequest { pub fn signed_redirect( &self, relay_state: &str, - private_key_der: &[u8], + private_key: openssl::pkey::PKey, ) -> Result, Box> { let unsigned_url = self.redirect(relay_state)?; @@ -570,11 +572,20 @@ impl AuthnRequest { // Note: the spec says to remove the Signature related XML elements // from the document but leaving them in usually works too. - // Use rsa-sha256 when signing (see RFC 4051 for choices) - unsigned_url.query_pairs_mut().append_pair( - "SigAlg", - "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", - ); + // see RFC 4051 for choices + if private_key.ec_key().is_ok() { + unsigned_url.query_pairs_mut().append_pair( + "SigAlg", + "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", + ); + } else if private_key.rsa().is_ok() { + unsigned_url.query_pairs_mut().append_pair( + "SigAlg", + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", + ); + } else { + return Err(Error::UnsupportedKey)?; + } // Sign *only* the existing url's encoded query parameters: // @@ -587,9 +598,7 @@ impl AuthnRequest { .ok_or(Error::UnexpectedError)? .to_string(); - // Use openssl's bindings to sign - let pkey = openssl::rsa::Rsa::private_key_from_der(private_key_der)?; - let pkey = openssl::pkey::PKey::from_rsa(pkey)?; + let pkey = private_key; let mut signer = openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), pkey.as_ref())?; diff --git a/src/xmlsec/mod.rs b/src/xmlsec/mod.rs index b28d644..448bfb9 100644 --- a/src/xmlsec/mod.rs +++ b/src/xmlsec/mod.rs @@ -12,6 +12,7 @@ #[doc(hidden)] pub use libxml::tree::document::Document as XmlDocument; #[doc(hidden)] +#[allow(unused)] pub use libxml::tree::node::Node as XmlNode; mod backend; diff --git a/test_vectors/README.md b/test_vectors/README.md index 8f511c1..8ca7d60 100644 --- a/test_vectors/README.md +++ b/test_vectors/README.md @@ -20,3 +20,29 @@ xmlsec1 --verify --trusted-der public.der --id-attr:ID Response response_signed_ ``` 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. + +To generate `response_signed_by_idp_ecdsa.xml`: + +```bash +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 +``` + +How the EC stuff was generated: + +```bash +# Step 1: Generate ECDSA Private Key +openssl ecparam -genkey -name prime256v1 -out ec_private.pem + +# Step 2: Create a Certificate Signing Request (CSR) +openssl req -new -key ec_private.pem -out ec_csr.pem + +# Step 3: Self-Sign the CSR to Create an X.509 Certificate +openssl x509 -req -in ec_csr.pem -signkey ec_private.pem -out ec_cert.pem -days 365000 + +# Step 4: Convert the Private Key and Certificate to DER Format +openssl pkcs8 -topk8 -inform PEM -outform DER -in ec_private.pem -out ec_private.der -nocrypt +openssl x509 -in ec_cert.pem -outform DER -out ec_cert.der + +# Step 5: Use the Private Key and Certificate with xmlsec1 +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 +``` diff --git a/test_vectors/ec_cert.der b/test_vectors/ec_cert.der new file mode 100644 index 0000000..e25a809 Binary files /dev/null and b/test_vectors/ec_cert.der differ diff --git a/test_vectors/ec_cert.pem b/test_vectors/ec_cert.pem new file mode 100644 index 0000000..ac3ed27 --- /dev/null +++ b/test_vectors/ec_cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ +BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 +NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc +W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI +qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I +UevJyxGd/2RkolE= +-----END CERTIFICATE----- diff --git a/test_vectors/ec_csr.pem b/test_vectors/ec_csr.pem new file mode 100644 index 0000000..541f8e3 --- /dev/null +++ b/test_vectors/ec_csr.pem @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBADCBpwIBADBFMQswCQYDVQQGEwJDQTETMBEGA1UECAwKU29tZS1TdGF0ZTEh +MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnwlzJd31gHv5qBg74j1kKS +aQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxqAAMAoGCCqGSM49BAMCA0gAMEUC +ICoacEPDHVz2FLWvcCi1feiH42+vXBd6Jy+Z99UE2TpZAiEA/B7qvdfLXGATSNMD +sM9Yp2o6woh3hOmXHN5BW2SAaj0= +-----END CERTIFICATE REQUEST----- diff --git a/test_vectors/ec_private.der b/test_vectors/ec_private.der new file mode 100644 index 0000000..b68fc33 Binary files /dev/null and b/test_vectors/ec_private.der differ diff --git a/test_vectors/ec_private.pem b/test_vectors/ec_private.pem new file mode 100644 index 0000000..96ac745 --- /dev/null +++ b/test_vectors/ec_private.pem @@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDWhjR2oP3FQMlxkD8qE8/CP+HTOe/KwOziEwnibBblKoAoGCCqGSM49 +AwEHoUQDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnwlzJd31gHv5qBg74j1kKSaQWD +ZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg== +-----END EC PRIVATE KEY----- diff --git a/test_vectors/ec_public.pem b/test_vectors/ec_public.pem new file mode 100644 index 0000000..dd22360 --- /dev/null +++ b/test_vectors/ec_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnw +lzJd31gHv5qBg74j1kKSaQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg== +-----END PUBLIC KEY----- diff --git a/test_vectors/idp_ecdsa_metadata.xml b/test_vectors/idp_ecdsa_metadata.xml new file mode 100644 index 0000000..882f52d --- /dev/null +++ b/test_vectors/idp_ecdsa_metadata.xml @@ -0,0 +1,29 @@ + + + + + + + + MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ + BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l + dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 + NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK + DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB + BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc + W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI + qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I + UevJyxGd/2RkolE= + + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + diff --git a/test_vectors/response_signed__ecdsa-template.xml b/test_vectors/response_signed__ecdsa-template.xml new file mode 100644 index 0000000..a5731dc --- /dev/null +++ b/test_vectors/response_signed__ecdsa-template.xml @@ -0,0 +1,69 @@ + + + https://fujifish.github.io/samling/samling.html + + + + + + + + + + + gciSu0u9H5QMP776LBbSg8ai9BM= + + + + + + + + + + + + https://fujifish.github.io/samling/samling.html + + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://test_accept_signed_with_correct_key.test + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + + diff --git a/test_vectors/response_signed_by_idp_ecdsa.xml b/test_vectors/response_signed_by_idp_ecdsa.xml new file mode 100644 index 0000000..e80f1db --- /dev/null +++ b/test_vectors/response_signed_by_idp_ecdsa.xml @@ -0,0 +1,68 @@ + + + https://fujifish.github.io/samling/samling.html + + + + + + + + + + + W7iYqYBNLg7dS+ueqLf04nO5V+c= + + + THCZWgdX01bDRNyUHHS+u3U7URTI4c3+1cuXKeWFQDjX/yjrC6V/6wCwXtD4VyjU +aUxevxscW8FBCRTkwDR78A== + + +MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ +BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1 +NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK +DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc +W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI +qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I +UevJyxGd/2RkolE= + + + + + + + + https://fujifish.github.io/samling/samling.html + + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://test_accept_signed_with_correct_key.test + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + +