Skip to content

Commit

Permalink
Implement ECC
Browse files Browse the repository at this point in the history
  • Loading branch information
njaremko committed Jun 23, 2024
1 parent 466c443 commit 47462c6
Show file tree
Hide file tree
Showing 18 changed files with 371 additions and 26 deletions.
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
33 changes: 32 additions & 1 deletion src/idp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,42 @@ 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(
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 wrong_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(
wrong_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-----
8 changes: 8 additions & 0 deletions test_vectors/ec_csr.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBADCBpwIBADBFMQswCQYDVQQGEwJDQTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnwlzJd31gHv5qBg74j1kKS
aQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxqAAMAoGCCqGSM49BAMCA0gAMEUC
ICoacEPDHVz2FLWvcCi1feiH42+vXBd6Jy+Z99UE2TpZAiEA/B7qvdfLXGATSNMD
sM9Yp2o6woh3hOmXHN5BW2SAaj0=
-----END CERTIFICATE REQUEST-----
Binary file added test_vectors/ec_private.der
Binary file not shown.
8 changes: 8 additions & 0 deletions test_vectors/ec_private.pem
Original file line number Diff line number Diff line change
@@ -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-----
4 changes: 4 additions & 0 deletions test_vectors/ec_public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyjU9gkG4ffc3WwyLF2Q4lmRlMmnw
lzJd31gHv5qBg74j1kKSaQWDZEkTHFt4g7AqIlRRqDt/u9euxVNa5RLqxg==
-----END PUBLIC KEY-----
Loading

0 comments on commit 47462c6

Please sign in to comment.