Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ECC #58

Merged
merged 1 commit into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 2 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
# the tests to run twice
samael-nextest = craneLib.cargoNextest (commonArgs // {
inherit cargoArtifacts;
cargoExtraArgs = "";
cargoNextestExtraArgs = "--features xmlsec";
partitions = 1;
partitionType = "count";
});
Expand Down
90 changes: 80 additions & 10 deletions src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,13 +490,15 @@ pub fn gen_saml_assertion_id() -> String {
enum SigAlg {
Unimplemented,
RsaSha256,
EcdsaSha256,
}

impl FromStr for SigAlg {
type Err = Box<dyn std::error::Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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),
}
}
Expand All @@ -509,33 +511,45 @@ pub enum UrlVerifierError {
}

pub struct UrlVerifier {
keypair: openssl::pkey::PKey<openssl::pkey::Public>,
public_key: openssl::pkey::PKey<openssl::pkey::Public>,
}

impl UrlVerifier {
pub fn from_rsa_pem(public_key_pem: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
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<Self, Box<dyn std::error::Error>> {
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<Self, Box<dyn std::error::Error>> {
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<Self, Box<dyn std::error::Error>> {
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<Self, Box<dyn std::error::Error>> {
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<Self, Box<dyn std::error::Error>> {
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:
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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::<DateTime<Utc>>().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()
Expand Down
39 changes: 35 additions & 4 deletions src/idp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
);

Expand All @@ -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::<DateTime<Utc>>().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"]),
);

Expand Down
9 changes: 9 additions & 0 deletions src/schema/authn_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<AuthnRequest>()?
.signed_redirect("", private_key)?
Expand Down Expand Up @@ -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::<AuthnRequest>()?
.signed_redirect("some_relay_state_here", private_key)?
Expand Down Expand Up @@ -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::<AuthnRequest>()?
.signed_redirect("some_relay_state_here", private_key)?
Expand Down
4 changes: 1 addition & 3 deletions src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
31 changes: 20 additions & 11 deletions src/service_provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -103,7 +105,7 @@ pub enum Error {
#[builder(default, setter(into))]
pub struct ServiceProvider {
pub entity_id: Option<String>,
pub key: Option<rsa::Rsa<Private>>,
pub key: Option<openssl::pkey::PKey<Private>>,
pub certificate: Option<x509::X509>,
pub intermediates: Option<Vec<x509::X509>>,
pub metadata_url: Option<String>,
Expand Down Expand Up @@ -553,7 +555,7 @@ impl AuthnRequest {
pub fn signed_redirect(
&self,
relay_state: &str,
private_key_der: &[u8],
private_key: openssl::pkey::PKey<Private>,
) -> Result<Option<Url>, Box<dyn std::error::Error>> {
let unsigned_url = self.redirect(relay_state)?;

Expand All @@ -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:
//
Expand All @@ -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())?;
Expand Down
1 change: 1 addition & 0 deletions src/xmlsec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions test_vectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Binary file added test_vectors/ec_cert.der
Binary file not shown.
11 changes: 11 additions & 0 deletions test_vectors/ec_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBhzCCAS0CFGE3kR43hTxJz3hg+bsefDiZjTSiMAoGCCqGSM49BAMCMEUxCzAJ
BgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwNjIzMTc0NTQ5WhgPMzAyMzEwMjUxNzQ1
NDlaMEUxCzAJBgNVBAYTAkNBMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQK
DBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMB
BwNCAATKNT2CQbh99zdbDIsXZDiWZGUyafCXMl3fWAe/moGDviPWQpJpBYNkSRMc
W3iDsCoiVFGoO3+7167FU1rlEurGMAoGCCqGSM49BAMCA0gAMEUCIQCdW4SacWlI
qj04IXo5QNWgbIrG6MKcXbvWEXDmMkiIewIgHkDlDn8Aq4reI+4BvUN+ZDmvOs1I
UevJyxGd/2RkolE=
-----END CERTIFICATE-----
Loading
Loading