diff --git a/Cargo.toml b/Cargo.toml index 4865ea2f..1065710e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ license = "MIT" edition = "2018" [package.metadata.docs.rs] -features = ["openssl"] +features = ["openssl", "ed25519-dalek"] [dependencies] base64 = "0.13" @@ -27,5 +27,17 @@ serde_json = "1.0" version = "0.10" optional = true +[dependencies.ring] +version = "0.17" +optional = true + +[dependencies.ed25519-dalek] +version = "2.0" +optional = true + [dev-dependencies] doc-comment = "0.3" + +[dev-dependencies.ed25519-dalek] +version = "2.0" +features = ["pkcs8","pem"] diff --git a/src/algorithm/ed25519_dalek.rs b/src/algorithm/ed25519_dalek.rs new file mode 100644 index 00000000..7fed3186 --- /dev/null +++ b/src/algorithm/ed25519_dalek.rs @@ -0,0 +1,82 @@ +use crate::algorithm::{AlgorithmType, SigningAlgorithm, VerifyingAlgorithm}; +use crate::error::Error; + +use ed25519_dalek::{SigningKey, VerifyingKey, Signer, Verifier}; + +impl SigningAlgorithm for SigningKey { + fn algorithm_type(&self) -> AlgorithmType { + AlgorithmType::EdDSA + } + + fn sign(&self, header: &str, claims: &str) -> Result { + Ok(base64::encode_config(Signer::sign(self, super::make_body(header, claims).as_slice()).to_bytes(), base64::URL_SAFE_NO_PAD)) + } +} + +impl VerifyingAlgorithm for VerifyingKey { + fn algorithm_type(&self) -> AlgorithmType { + AlgorithmType::EdDSA + } + + fn verify_bytes(&self, header: &str, claims: &str, signature: &[u8]) -> Result { + let signature = ed25519_dalek::Signature::from_slice(signature).map_err(|_| Error::InvalidSignature)?; + Ok(Verifier::verify(self, super::make_body(header, claims).as_slice(), &signature).is_ok()) + } +} + +#[cfg(test)] +mod test { + use crate::{header::PrecomputedAlgorithmOnlyHeader as AlgOnly, ToBase64, Error}; + use super::{SigningAlgorithm,VerifyingAlgorithm}; + + use ed25519_dalek::pkcs8::{DecodePrivateKey,DecodePublicKey}; + + // {"sub":"1234567890","name":"John Doe","admin":true} + const CLAIMS: &'static str = + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"; + + #[test] + fn roundtrip() -> Result<(), Error> { + + let private_key_pem = include_str!("../../test/eddsa-private.pem"); + let private_key = ed25519_dalek::SigningKey::from_pkcs8_pem(private_key_pem).expect("couldn't load private key"); + + let signature = private_key.sign(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS)?; + + let public_key_pem = include_str!("../../test/eddsa-public.pem"); + let public_key = ed25519_dalek::VerifyingKey::from_public_key_pem(public_key_pem).expect("couldn't load public key"); + + let verification_result = public_key.verify(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS, &*signature)?; + assert!(verification_result); + + Ok(()) + } + + #[cfg(feature = "openssl")] + #[test] + fn cross_verify_openssl() -> Result<(), Error> { + let private_key_pem = include_str!("../../test/eddsa-private.pem"); + let dalek_private_key = ed25519_dalek::SigningKey::from_pkcs8_pem(private_key_pem).expect("couldn't load private key"); + let openssl_private_key = crate::algorithm::openssl::PKeyWithDigest { + digest: openssl::hash::MessageDigest::null(), + key: openssl::pkey::PKey::private_key_from_pem(private_key_pem.as_bytes())?, + }; + + let public_key_pem = include_str!("../../test/eddsa-public.pem"); + + let dalek_public_key = ed25519_dalek::VerifyingKey::from_public_key_pem(public_key_pem).expect("couldn't load public key"); + let openssl_public_key = crate::algorithm::openssl::PKeyWithDigest { + digest: openssl::hash::MessageDigest::null(), + key: openssl::pkey::PKey::public_key_from_pem(public_key_pem.as_bytes())?, + }; + + let dalek_signature = dalek_private_key.sign(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS)?; + let openssl_signature = openssl_private_key.sign(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS)?; + + assert!(dalek_public_key.verify(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS, &*dalek_signature)?); + assert!(openssl_public_key.verify(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS, &*dalek_signature)?); + assert!(dalek_public_key.verify(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS, &*openssl_signature)?); + assert!(openssl_public_key.verify(&AlgOnly(super::AlgorithmType::EdDSA).to_base64()?, CLAIMS, &*openssl_signature)?); + Ok(()) + } +} diff --git a/src/algorithm/mod.rs b/src/algorithm/mod.rs index 3e98a832..dd258288 100644 --- a/src/algorithm/mod.rs +++ b/src/algorithm/mod.rs @@ -16,6 +16,8 @@ use crate::error::Error; #[cfg(feature = "openssl")] pub mod openssl; +#[cfg(feature = "ed25519-dalek")] +pub mod ed25519_dalek; pub mod rust_crypto; pub mod store; @@ -36,6 +38,7 @@ pub enum AlgorithmType { Ps256, Ps384, Ps512, + EdDSA, #[serde(rename = "none")] None, } @@ -86,3 +89,12 @@ impl> SigningAlgorithm for T { self.as_ref().sign(header, claims) } } + + +fn make_body(header: &str, claims: &str) -> Vec { + let mut body = vec![]; + body.extend(header.as_bytes()); + body.extend(crate::SEPARATOR.as_bytes()); + body.extend(claims.as_bytes()); + body +} diff --git a/src/algorithm/openssl.rs b/src/algorithm/openssl.rs index fce0daaa..c852f13d 100644 --- a/src/algorithm/openssl.rs +++ b/src/algorithm/openssl.rs @@ -41,6 +41,8 @@ impl PKeyWithDigest { (Id::EC, Nid::SHA256) => AlgorithmType::Es256, (Id::EC, Nid::SHA384) => AlgorithmType::Es384, (Id::EC, Nid::SHA512) => AlgorithmType::Es512, + (Id::ED25519, Nid::UNDEF) => AlgorithmType::EdDSA, + (Id::ED448, Nid::UNDEF) => AlgorithmType::EdDSA, _ => panic!("Invalid algorithm type"), } } @@ -52,12 +54,24 @@ impl SigningAlgorithm for PKeyWithDigest { } fn sign(&self, header: &str, claims: &str) -> Result { - let mut signer = Signer::new(self.digest.clone(), &self.key)?; - signer.update(header.as_bytes())?; - signer.update(SEPARATOR.as_bytes())?; - signer.update(claims.as_bytes())?; - let signer_signature = signer.sign_to_vec()?; + let signer_signature = match self.algorithm_type() { + // for EdDSA, openssl needs to be told that no digest type is in use, as passing NULL + // is not enough. + AlgorithmType::EdDSA => { + let mut signer = Signer::new_without_digest(&self.key)?; + + signer.sign_oneshot_to_vec(super::make_body(header, claims).as_slice())? + }, + _ => { + let mut signer = Signer::new(self.digest.clone(), &self.key)?; + signer.update(header.as_bytes())?; + signer.update(SEPARATOR.as_bytes())?; + signer.update(claims.as_bytes())?; + signer.sign_to_vec()? + } + }; + // note that Ed25519 signatures do not need to be converted to/from a DER format let signature = if self.key.id() == Id::EC { der_to_jose(&signer_signature)? } else { @@ -74,19 +88,33 @@ impl VerifyingAlgorithm for PKeyWithDigest { } fn verify_bytes(&self, header: &str, claims: &str, signature: &[u8]) -> Result { - let mut verifier = Verifier::new(self.digest.clone(), &self.key)?; - verifier.update(header.as_bytes())?; - verifier.update(SEPARATOR.as_bytes())?; - verifier.update(claims.as_bytes())?; - - let verified = if self.key.id() == Id::EC { - let der = jose_to_der(signature)?; - verifier.verify(&der)? - } else { - verifier.verify(signature)? - }; - - Ok(verified) + match self.algorithm_type() { + // for EdDSA, openssl needs to be told that no digest type is in use, as passing NULL + // is not enough. + AlgorithmType::EdDSA => { + let mut verifier = Verifier::new_without_digest(&self.key)?; + + // note that Ed25519 signatures do not need to be converted to/from a DER format + let verified = verifier.verify_oneshot(signature, super::make_body(header, claims).as_slice())?; + + Ok(verified) + }, + _ => { + let mut verifier = Verifier::new(self.digest.clone(), &self.key)?; + verifier.update(header.as_bytes())?; + verifier.update(SEPARATOR.as_bytes())?; + verifier.update(claims.as_bytes())?; + + let verified = if self.key.id() == Id::EC { + let der = jose_to_der(signature)?; + verifier.verify(&der)? + } else { + verifier.verify(signature)? + }; + + Ok(verified) + } + } } } @@ -176,4 +204,28 @@ mod tests { assert!(verification_result); Ok(()) } + + #[test] + fn eddsa() -> Result<(), Error> { + let private_pem = include_bytes!("../../test/eddsa-private.pem"); + + let private_key = PKeyWithDigest { + digest: MessageDigest::null(), + key: PKey::private_key_from_pem(private_pem)?, + }; + + let signature = private_key.sign(&AlgOnly(EdDSA).to_base64()?, CLAIMS)?; + + let public_pem = include_bytes!("../../test/eddsa-public.pem"); + + let public_key = PKeyWithDigest { + digest: MessageDigest::null(), + key: PKey::public_key_from_pem(public_pem)?, + }; + + let verification_result = + public_key.verify(&AlgOnly(EdDSA).to_base64()?, CLAIMS, &*signature)?; + assert!(verification_result); + Ok(()) + } } diff --git a/src/header.rs b/src/header.rs index 2f75a7d1..eb7c4df5 100644 --- a/src/header.rs +++ b/src/header.rs @@ -100,6 +100,7 @@ impl ToBase64 for PrecomputedAlgorithmOnlyHeader { AlgorithmType::Ps256 => "eyJhbGciOiAiUFMyNTYifQ", AlgorithmType::Ps384 => "eyJhbGciOiAiUFMzODQifQ", AlgorithmType::Ps512 => "eyJhbGciOiAiUFM1MTIifQ", + AlgorithmType::EdDSA => "eyJhbGciOiAiRWREU0EifQ", AlgorithmType::None => "eyJhbGciOiAibm9uZSJ9Cg", }; diff --git a/test/eddsa-private.pem b/test/eddsa-private.pem new file mode 100644 index 00000000..3894de1e --- /dev/null +++ b/test/eddsa-private.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICpu7WI5foPbL4HZoO/ohmZR8DtktkuxadwXzUtiJQPq +-----END PRIVATE KEY----- diff --git a/test/eddsa-public.pem b/test/eddsa-public.pem new file mode 100644 index 00000000..03507dfd --- /dev/null +++ b/test/eddsa-public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAdie/nMgwk8iPLafbWMN6wM18fTjrPGo1ulDoPfX6obc= +-----END PUBLIC KEY-----