Skip to content

Commit b120682

Browse files
committed
Implement ECC
1 parent 466c443 commit b120682

18 files changed

+374
-29
lines changed

CHANGELOG

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Changelog
22

3-
### Unreleased
3+
### 0.0.16
4+
45
- Add support for basic `RequestedAuthnContext` de-/serialization in `AuthnRequest`
6+
- Add support for Elliptic-curve cryptography
57

68
### 0.0.15
79

flake.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@
156156
# the tests to run twice
157157
samael-nextest = craneLib.cargoNextest (commonArgs // {
158158
inherit cargoArtifacts;
159+
cargoExtraArgs = "";
160+
cargoNextestExtraArgs = "--features xmlsec";
159161
partitions = 1;
160162
partitionType = "count";
161163
});

src/crypto.rs

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -490,13 +490,15 @@ pub fn gen_saml_assertion_id() -> String {
490490
enum SigAlg {
491491
Unimplemented,
492492
RsaSha256,
493+
EcdsaSha256,
493494
}
494495

495496
impl FromStr for SigAlg {
496497
type Err = Box<dyn std::error::Error>;
497498
fn from_str(s: &str) -> Result<Self, Self::Err> {
498499
match s {
499500
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => Ok(SigAlg::RsaSha256),
501+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => Ok(SigAlg::EcdsaSha256),
500502
_ => Ok(SigAlg::Unimplemented),
501503
}
502504
}
@@ -509,33 +511,45 @@ pub enum UrlVerifierError {
509511
}
510512

511513
pub struct UrlVerifier {
512-
keypair: openssl::pkey::PKey<openssl::pkey::Public>,
514+
public_key: openssl::pkey::PKey<openssl::pkey::Public>,
513515
}
514516

515517
impl UrlVerifier {
516518
pub fn from_rsa_pem(public_key_pem: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
517519
let public = openssl::rsa::Rsa::public_key_from_pem(public_key_pem)?;
518-
let keypair = openssl::pkey::PKey::from_rsa(public)?;
519-
Ok(Self { keypair })
520+
let public_key = openssl::pkey::PKey::from_rsa(public)?;
521+
Ok(Self { public_key })
520522
}
521523

522524
pub fn from_rsa_der(public_key_der: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
523525
let public = openssl::rsa::Rsa::public_key_from_der(public_key_der)?;
524-
let keypair = openssl::pkey::PKey::from_rsa(public)?;
525-
Ok(Self { keypair })
526+
let public_key = openssl::pkey::PKey::from_rsa(public)?;
527+
Ok(Self { public_key })
528+
}
529+
530+
pub fn from_ec_pem(public_key_pem: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
531+
let public = openssl::ec::EcKey::public_key_from_pem(public_key_pem)?;
532+
let public_key = openssl::pkey::PKey::from_ec_key(public)?;
533+
Ok(Self { public_key })
534+
}
535+
536+
pub fn from_ec_der(public_key_der: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
537+
let public = openssl::ec::EcKey::public_key_from_der(public_key_der)?;
538+
let public_key = openssl::pkey::PKey::from_ec_key(public)?;
539+
Ok(Self { public_key })
526540
}
527541

528542
pub fn from_x509_cert_pem(public_cert_pem: &str) -> Result<Self, Box<dyn std::error::Error>> {
529543
let x509 = openssl::x509::X509::from_pem(public_cert_pem.as_bytes())?;
530-
let keypair = x509.public_key()?;
531-
Ok(Self { keypair })
544+
let public_key = x509.public_key()?;
545+
Ok(Self { public_key })
532546
}
533547

534548
pub fn from_x509(
535549
public_cert: &openssl::x509::X509,
536550
) -> Result<Self, Box<dyn std::error::Error>> {
537-
let keypair = public_cert.public_key()?;
538-
Ok(Self { keypair })
551+
let public_key = public_cert.public_key()?;
552+
Ok(Self { public_key })
539553
}
540554

541555
// Signed url should look like:
@@ -660,9 +674,10 @@ impl UrlVerifier {
660674
let mut verifier = openssl::sign::Verifier::new(
661675
match sig_alg {
662676
SigAlg::RsaSha256 => openssl::hash::MessageDigest::sha256(),
677+
SigAlg::EcdsaSha256 => openssl::hash::MessageDigest::sha256(),
663678
_ => panic!("sig_alg is bad!"),
664679
},
665-
&self.keypair,
680+
&self.public_key,
666681
)?;
667682

668683
verifier.update(data)?;
@@ -704,6 +719,61 @@ mod test {
704719
.make_authentication_request("http://dummy.fake/saml")
705720
.unwrap();
706721

722+
let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap();
723+
let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap();
724+
725+
let signed_request_url = authn_request
726+
.signed_redirect("", private_key)
727+
.unwrap()
728+
.unwrap();
729+
730+
// percent encoeded URL:
731+
// http://dummy.fake/saml?SAMLRequest=..&SigAlg=..&Signature=..
732+
//
733+
// percent encoded URI:
734+
// /saml?SAMLRequest=..&SigAlg=..&Signature=..
735+
//
736+
let uri_string: &String = &signed_request_url[url::Position::BeforePath..].to_string();
737+
assert!(uri_string.starts_with("/saml?SAMLRequest="));
738+
739+
let url_verifier =
740+
UrlVerifier::from_x509(&sp.idp_signing_certs().unwrap().unwrap()[0]).unwrap();
741+
742+
assert!(url_verifier
743+
.verify_percent_encoded_request_uri_string(uri_string)
744+
.unwrap(),);
745+
}
746+
747+
#[test]
748+
fn test_verify_uri_ec() {
749+
let private_key = include_bytes!(concat!(
750+
env!("CARGO_MANIFEST_DIR"),
751+
"/test_vectors/ec_private.pem"
752+
));
753+
754+
let idp_metadata_xml = include_str!(concat!(
755+
env!("CARGO_MANIFEST_DIR"),
756+
"/test_vectors/idp_ecdsa_metadata.xml"
757+
));
758+
759+
let response_instant = "2014-07-17T01:01:48Z".parse::<DateTime<Utc>>().unwrap();
760+
let max_issue_delay = Utc::now() - response_instant + chrono::Duration::seconds(60);
761+
762+
let sp = ServiceProvider {
763+
metadata_url: Some("http://test_accept_signed_with_correct_key.test".into()),
764+
acs_url: Some("http://sp.example.com/demo1/index.php?acs".into()),
765+
idp_metadata: idp_metadata_xml.parse().unwrap(),
766+
max_issue_delay,
767+
..Default::default()
768+
};
769+
770+
let authn_request = sp
771+
.make_authentication_request("http://dummy.fake/saml")
772+
.unwrap();
773+
774+
let private_key = openssl::ec::EcKey::private_key_from_pem(private_key).unwrap();
775+
let private_key = openssl::pkey::PKey::from_ec_key(private_key).unwrap();
776+
707777
let signed_request_url = authn_request
708778
.signed_redirect("", private_key)
709779
.unwrap()

src/idp/tests.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,13 @@ fn test_accept_signed_with_correct_key_idp() {
272272
..Default::default()
273273
};
274274

275-
let wrong_cert_signed_response_xml = include_str!(concat!(
275+
let correct_cert_signed_response_xml = include_str!(concat!(
276276
env!("CARGO_MANIFEST_DIR"),
277277
"/test_vectors/response_signed.xml",
278278
));
279279

280280
let resp = sp.parse_xml_response(
281-
wrong_cert_signed_response_xml,
281+
correct_cert_signed_response_xml,
282282
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
283283
);
284284

@@ -303,13 +303,44 @@ fn test_accept_signed_with_correct_key_idp_2() {
303303
..Default::default()
304304
};
305305

306-
let wrong_cert_signed_response_xml = include_str!(concat!(
306+
let correct_cert_signed_response_xml = include_str!(concat!(
307307
env!("CARGO_MANIFEST_DIR"),
308308
"/test_vectors/response_signed_by_idp_2.xml",
309309
));
310310

311311
let resp = sp.parse_xml_response(
312-
wrong_cert_signed_response_xml,
312+
correct_cert_signed_response_xml,
313+
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
314+
);
315+
316+
assert!(resp.is_ok());
317+
}
318+
319+
#[test]
320+
fn test_accept_signed_with_correct_key_idp_3() {
321+
let idp_metadata_xml = include_str!(concat!(
322+
env!("CARGO_MANIFEST_DIR"),
323+
"/test_vectors/idp_ecdsa_metadata.xml"
324+
));
325+
326+
let response_instant = "2014-07-17T01:01:48Z".parse::<DateTime<Utc>>().unwrap();
327+
let max_issue_delay = Utc::now() - response_instant + chrono::Duration::seconds(60);
328+
329+
let sp = ServiceProvider {
330+
metadata_url: Some("http://test_accept_signed_with_correct_key.test".into()),
331+
acs_url: Some("http://sp.example.com/demo1/index.php?acs".into()),
332+
idp_metadata: idp_metadata_xml.parse().unwrap(),
333+
max_issue_delay,
334+
..Default::default()
335+
};
336+
337+
let correct_cert_signed_response_xml = include_str!(concat!(
338+
env!("CARGO_MANIFEST_DIR"),
339+
"/test_vectors/response_signed_by_idp_ecdsa.xml",
340+
));
341+
342+
let resp = sp.parse_xml_response(
343+
correct_cert_signed_response_xml,
313344
Some(&["ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"]),
314345
);
315346

src/schema/authn_request.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ mod test {
290290
"/test_vectors/authn_request_sign_template.xml"
291291
));
292292

293+
let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap();
294+
let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap();
295+
293296
let signed_authn_redirect_url = authn_request_sign_template
294297
.parse::<AuthnRequest>()?
295298
.signed_redirect("", private_key)?
@@ -318,6 +321,9 @@ mod test {
318321
"/test_vectors/authn_request_sign_template.xml"
319322
));
320323

324+
let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap();
325+
let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap();
326+
321327
let signed_authn_redirect_url = authn_request_sign_template
322328
.parse::<AuthnRequest>()?
323329
.signed_redirect("some_relay_state_here", private_key)?
@@ -347,6 +353,9 @@ mod test {
347353
"/test_vectors/authn_request_sign_template.xml"
348354
));
349355

356+
let private_key = openssl::rsa::Rsa::private_key_from_der(private_key).unwrap();
357+
let private_key = openssl::pkey::PKey::from_rsa(private_key).unwrap();
358+
350359
let signed_authn_redirect_url = authn_request_sign_template
351360
.parse::<AuthnRequest>()?
352361
.signed_redirect("some_relay_state_here", private_key)?

src/schema/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -723,9 +723,7 @@ impl LogoutResponse {
723723

724724
#[cfg(test)]
725725
mod test {
726-
use super::issuer::Issuer;
727-
use super::{LogoutRequest, LogoutResponse, NameID, Status, StatusCode};
728-
use chrono::TimeZone;
726+
use super::{LogoutRequest, LogoutResponse};
729727

730728
#[test]
731729
fn test_deserialize_serialize_logout_request() {

src/service_provider/mod.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use chrono::prelude::*;
1212
use chrono::Duration;
1313
use flate2::{write::DeflateEncoder, Compression};
1414
use openssl::pkey::Private;
15-
use openssl::{rsa, x509};
15+
use openssl::x509;
1616
use std::fmt::Debug;
1717
use std::io::Write;
1818
use thiserror::Error;
@@ -88,6 +88,8 @@ pub enum Error {
8888
FailedToParseCert { cert: String },
8989
#[error("Unexpected Error Occurred!")]
9090
UnexpectedError,
91+
#[error("Tried to use an unsupported key format")]
92+
UnsupportedKey,
9193

9294
#[error("Failed to parse SAMLResponse")]
9395
FailedToParseSamlResponse,
@@ -103,7 +105,7 @@ pub enum Error {
103105
#[builder(default, setter(into))]
104106
pub struct ServiceProvider {
105107
pub entity_id: Option<String>,
106-
pub key: Option<rsa::Rsa<Private>>,
108+
pub key: Option<openssl::pkey::PKey<Private>>,
107109
pub certificate: Option<x509::X509>,
108110
pub intermediates: Option<Vec<x509::X509>>,
109111
pub metadata_url: Option<String>,
@@ -553,7 +555,7 @@ impl AuthnRequest {
553555
pub fn signed_redirect(
554556
&self,
555557
relay_state: &str,
556-
private_key_der: &[u8],
558+
private_key: openssl::pkey::PKey<Private>,
557559
) -> Result<Option<Url>, Box<dyn std::error::Error>> {
558560
let unsigned_url = self.redirect(relay_state)?;
559561

@@ -570,11 +572,20 @@ impl AuthnRequest {
570572
// Note: the spec says to remove the Signature related XML elements
571573
// from the document but leaving them in usually works too.
572574

573-
// Use rsa-sha256 when signing (see RFC 4051 for choices)
574-
unsigned_url.query_pairs_mut().append_pair(
575-
"SigAlg",
576-
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
577-
);
575+
// see RFC 4051 for choices
576+
if private_key.ec_key().is_ok() {
577+
unsigned_url.query_pairs_mut().append_pair(
578+
"SigAlg",
579+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
580+
);
581+
} else if private_key.rsa().is_ok() {
582+
unsigned_url.query_pairs_mut().append_pair(
583+
"SigAlg",
584+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
585+
);
586+
} else {
587+
return Err(Error::UnsupportedKey)?;
588+
}
578589

579590
// Sign *only* the existing url's encoded query parameters:
580591
//
@@ -587,9 +598,7 @@ impl AuthnRequest {
587598
.ok_or(Error::UnexpectedError)?
588599
.to_string();
589600

590-
// Use openssl's bindings to sign
591-
let pkey = openssl::rsa::Rsa::private_key_from_der(private_key_der)?;
592-
let pkey = openssl::pkey::PKey::from_rsa(pkey)?;
601+
let pkey = private_key;
593602

594603
let mut signer =
595604
openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), pkey.as_ref())?;

src/xmlsec/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#[doc(hidden)]
1313
pub use libxml::tree::document::Document as XmlDocument;
1414
#[doc(hidden)]
15+
#[allow(unused)]
1516
pub use libxml::tree::node::Node as XmlNode;
1617

1718
mod backend;

test_vectors/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,29 @@ xmlsec1 --verify --trusted-der public.der --id-attr:ID Response response_signed_
2020
```
2121

2222
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.
23+
24+
To generate `response_signed_by_idp_ecdsa.xml`:
25+
26+
```bash
27+
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
28+
```
29+
30+
How the EC stuff was generated:
31+
32+
```bash
33+
# Step 1: Generate ECDSA Private Key
34+
openssl ecparam -genkey -name prime256v1 -out ec_private.pem
35+
36+
# Step 2: Create a Certificate Signing Request (CSR)
37+
openssl req -new -key ec_private.pem -out ec_csr.pem
38+
39+
# Step 3: Self-Sign the CSR to Create an X.509 Certificate
40+
openssl x509 -req -in ec_csr.pem -signkey ec_private.pem -out ec_cert.pem -days 365000
41+
42+
# Step 4: Convert the Private Key and Certificate to DER Format
43+
openssl pkcs8 -topk8 -inform PEM -outform DER -in ec_private.pem -out ec_private.der -nocrypt
44+
openssl x509 -in ec_cert.pem -outform DER -out ec_cert.der
45+
46+
# Step 5: Use the Private Key and Certificate with xmlsec1
47+
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
48+
```

test_vectors/ec_cert.der

395 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)