From 2178cc75061f4a46a4029a78c55947dc8e6a723d Mon Sep 17 00:00:00 2001 From: Charles Lehner Date: Tue, 17 Nov 2020 08:15:17 -0500 Subject: [PATCH] Add EdDSA (Ed25519) (#154) --- src/algorithms.rs | 6 +++ src/crypto/eddsa.rs | 23 ++++++++++ src/crypto/mod.rs | 9 ++++ src/decoding.rs | 18 ++++++++ src/encoding.rs | 13 ++++++ src/pem/decoder.rs | 38 +++++++++++++++- tests/eddsa/mod.rs | 67 ++++++++++++++++++++++++++++ tests/eddsa/private_ed25519_key.pem | 3 ++ tests/eddsa/private_ed25519_key.pk8 | Bin 0 -> 48 bytes tests/eddsa/public_ed25519_key.pem | 3 ++ tests/eddsa/public_ed25519_key.pk8 | 2 + tests/lib.rs | 1 + 12 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/crypto/eddsa.rs create mode 100644 tests/eddsa/mod.rs create mode 100644 tests/eddsa/private_ed25519_key.pem create mode 100644 tests/eddsa/private_ed25519_key.pk8 create mode 100644 tests/eddsa/public_ed25519_key.pem create mode 100644 tests/eddsa/public_ed25519_key.pk8 diff --git a/src/algorithms.rs b/src/algorithms.rs index 924a41e..3948711 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -7,6 +7,7 @@ pub(crate) enum AlgorithmFamily { Hmac, Rsa, Ec, + Ed, } /// The algorithms supported for signing/verifying JWTs @@ -37,6 +38,9 @@ pub enum Algorithm { PS384, /// RSASSA-PSS using SHA-512 PS512, + + /// Edwards-curve Digital Signature Algorithm (EdDSA) + EdDSA, } impl Default for Algorithm { @@ -60,6 +64,7 @@ impl FromStr for Algorithm { "PS384" => Ok(Algorithm::PS384), "PS512" => Ok(Algorithm::PS512), "RS512" => Ok(Algorithm::RS512), + "EdDSA" => Ok(Algorithm::EdDSA), _ => Err(ErrorKind::InvalidAlgorithmName.into()), } } @@ -76,6 +81,7 @@ impl Algorithm { | Algorithm::PS384 | Algorithm::PS512 => AlgorithmFamily::Rsa, Algorithm::ES256 | Algorithm::ES384 => AlgorithmFamily::Ec, + Algorithm::EdDSA => AlgorithmFamily::Ed, } } } diff --git a/src/crypto/eddsa.rs b/src/crypto/eddsa.rs new file mode 100644 index 0000000..7a47ed2 --- /dev/null +++ b/src/crypto/eddsa.rs @@ -0,0 +1,23 @@ +use ring::signature; + +use crate::algorithms::Algorithm; +use crate::errors::Result; +use crate::serialization::b64_encode; + +/// Only used internally when signing or validating EdDSA, to map from our enum to the Ring EdDSAParameters structs. +pub(crate) fn alg_to_ec_verification(alg: Algorithm) -> &'static signature::EdDSAParameters { + // To support additional key subtypes, like Ed448, we would need to match on the JWK's ("crv") + // parameter. + match alg { + Algorithm::EdDSA => &signature::ED25519, + _ => unreachable!("Tried to get EdDSA alg for a non-EdDSA algorithm"), + } +} + +/// The actual EdDSA signing + encoding +/// The key needs to be in PKCS8 format +pub fn sign(key: &[u8], message: &str) -> Result { + let signing_key = signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(key)?; + let out = signing_key.sign(message.as_bytes()); + Ok(b64_encode(out.as_ref())) +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index fffeff5..b4c3e91 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -8,6 +8,7 @@ use crate::errors::Result; use crate::serialization::{b64_decode, b64_encode}; pub(crate) mod ecdsa; +pub(crate) mod eddsa; pub(crate) mod rsa; /// The actual HS signing + encoding @@ -31,6 +32,8 @@ pub fn sign(message: &str, key: &EncodingKey, algorithm: Algorithm) -> Result eddsa::sign(key.inner(), message), + Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 @@ -80,6 +83,12 @@ pub fn verify( message, key.as_bytes(), ), + Algorithm::EdDSA => verify_ring( + eddsa::alg_to_ec_verification(algorithm), + signature, + message, + key.as_bytes(), + ), Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 diff --git a/src/decoding.rs b/src/decoding.rs index 810c973..33462cf 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -94,6 +94,16 @@ impl<'a> DecodingKey<'a> { }) } + /// If you have a EdDSA public key in PEM format, use this. + pub fn from_ed_pem(key: &'a [u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_ed_public_key()?; + Ok(DecodingKey { + family: AlgorithmFamily::Ed, + kind: DecodingKeyKind::SecretOrDer(Cow::Owned(content.to_vec())), + }) + } + /// If you know what you're doing and have a RSA DER encoded public key, use this. pub fn from_rsa_der(der: &'a [u8]) -> Self { DecodingKey { @@ -110,6 +120,14 @@ impl<'a> DecodingKey<'a> { } } + /// If you know what you're doing and have a Ed DER encoded public key, use this. + pub fn from_ed_der(der: &'a [u8]) -> Self { + DecodingKey { + family: AlgorithmFamily::Ed, + kind: DecodingKeyKind::SecretOrDer(Cow::Borrowed(der)), + } + } + /// Convert self to `DecodingKey<'static>`. pub fn into_static(self) -> DecodingKey<'static> { use DecodingKeyKind::*; diff --git a/src/encoding.rs b/src/encoding.rs index 49564ae..cfb96e6 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -60,6 +60,14 @@ impl EncodingKey { Ok(EncodingKey { family: AlgorithmFamily::Ec, content: content.to_vec() }) } + /// If you are loading a EdDSA key from a .pem file + /// This errors if the key is not a valid private Ed key + pub fn from_ed_pem(key: &[u8]) -> Result { + let pem_key = PemEncodedKey::new(key)?; + let content = pem_key.as_ed_private_key()?; + Ok(EncodingKey { family: AlgorithmFamily::Ed, content: content.to_vec() }) + } + /// If you know what you're doing and have the DER-encoded key, for RSA only pub fn from_rsa_der(der: &[u8]) -> Self { EncodingKey { family: AlgorithmFamily::Rsa, content: der.to_vec() } @@ -70,6 +78,11 @@ impl EncodingKey { EncodingKey { family: AlgorithmFamily::Ec, content: der.to_vec() } } + /// If you know what you're doing and have the DER-encoded key, for EdDSA + pub fn from_ed_der(der: &[u8]) -> Self { + EncodingKey { family: AlgorithmFamily::Ed, content: der.to_vec() } + } + pub(crate) fn inner(&self) -> &[u8] { &self.content } diff --git a/src/pem/decoder.rs b/src/pem/decoder.rs index ded5ac5..f96beca 100644 --- a/src/pem/decoder.rs +++ b/src/pem/decoder.rs @@ -9,6 +9,8 @@ enum PemType { EcPrivate, RsaPublic, RsaPrivate, + EdPublic, + EdPrivate, } #[derive(Debug, PartialEq)] @@ -22,6 +24,7 @@ enum Standard { #[derive(Debug, PartialEq)] enum Classification { Ec, + Ed, Rsa, } @@ -89,6 +92,13 @@ impl PemEncodedKey { PemType::EcPublic } } + Classification::Ed => { + if is_private { + PemType::EdPrivate + } else { + PemType::EdPublic + } + } Classification::Rsa => { if is_private { PemType::RsaPrivate @@ -137,6 +147,28 @@ impl PemEncodedKey { } } + /// Can only be PKCS8 + pub fn as_ed_private_key(&self) -> Result<&[u8]> { + match self.standard { + Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()), + Standard::Pkcs8 => match self.pem_type { + PemType::EdPrivate => Ok(self.content.as_slice()), + _ => Err(ErrorKind::InvalidKeyFormat.into()), + }, + } + } + + /// Can only be PKCS8 + pub fn as_ed_public_key(&self) -> Result<&[u8]> { + match self.standard { + Standard::Pkcs1 => Err(ErrorKind::InvalidKeyFormat.into()), + Standard::Pkcs8 => match self.pem_type { + PemType::EdPublic => extract_first_bitstring(&self.asn1), + _ => Err(ErrorKind::InvalidKeyFormat.into()), + }, + } + } + /// Can be PKCS1 or PKCS8 pub fn as_rsa_key(&self) -> Result<&[u8]> { match self.standard { @@ -176,12 +208,13 @@ fn extract_first_bitstring(asn1: &[simple_asn1::ASN1Block]) -> Result<&[u8]> { Err(ErrorKind::InvalidEcdsaKey.into()) } -/// Find whether this is EC or RSA +/// Find whether this is EC, RSA, or Ed fn classify_pem(asn1: &[simple_asn1::ASN1Block]) -> Option { // These should be constant but the macro requires // #![feature(const_vec_new)] let ec_public_key_oid = simple_asn1::oid!(1, 2, 840, 10_045, 2, 1); let rsa_public_key_oid = simple_asn1::oid!(1, 2, 840, 113_549, 1, 1, 1); + let ed25519_oid = simple_asn1::oid!(1, 3, 101, 112); for asn1_entry in asn1.iter() { match asn1_entry { @@ -197,6 +230,9 @@ fn classify_pem(asn1: &[simple_asn1::ASN1Block]) -> Option { if oid == rsa_public_key_oid { return Some(Classification::Rsa); } + if oid == ed25519_oid { + return Some(Classification::Ed); + } } _ => {} } diff --git a/tests/eddsa/mod.rs b/tests/eddsa/mod.rs new file mode 100644 index 0000000..98cf532 --- /dev/null +++ b/tests/eddsa/mod.rs @@ -0,0 +1,67 @@ +use chrono::Utc; +use jsonwebtoken::{ + crypto::{sign, verify}, + decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Claims { + sub: String, + company: String, + exp: i64, +} + +#[test] +fn round_trip_sign_verification_pk8() { + let privkey = include_bytes!("private_ed25519_key.pk8"); + let pubkey = include_bytes!("public_ed25519_key.pk8"); + + let encrypted = + sign("hello world", &EncodingKey::from_ed_der(privkey), Algorithm::EdDSA).unwrap(); + let is_valid = + verify(&encrypted, "hello world", &DecodingKey::from_ed_der(pubkey), Algorithm::EdDSA) + .unwrap(); + assert!(is_valid); +} + +#[test] +fn round_trip_sign_verification_pem() { + let privkey_pem = include_bytes!("private_ed25519_key.pem"); + let pubkey_pem = include_bytes!("public_ed25519_key.pem"); + let encrypted = + sign("hello world", &EncodingKey::from_ed_pem(privkey_pem).unwrap(), Algorithm::EdDSA) + .unwrap(); + let is_valid = verify( + &encrypted, + "hello world", + &DecodingKey::from_ed_pem(pubkey_pem).unwrap(), + Algorithm::EdDSA, + ) + .unwrap(); + assert!(is_valid); +} + +#[test] +fn round_trip_claim() { + let privkey_pem = include_bytes!("private_ed25519_key.pem"); + let pubkey_pem = include_bytes!("public_ed25519_key.pem"); + let my_claims = Claims { + sub: "b@b.com".to_string(), + company: "ACME".to_string(), + exp: Utc::now().timestamp() + 10000, + }; + let token = encode( + &Header::new(Algorithm::EdDSA), + &my_claims, + &EncodingKey::from_ed_pem(privkey_pem).unwrap(), + ) + .unwrap(); + let token_data = decode::( + &token, + &DecodingKey::from_ed_pem(pubkey_pem).unwrap(), + &Validation::new(Algorithm::EdDSA), + ) + .unwrap(); + assert_eq!(my_claims, token_data.claims); +} diff --git a/tests/eddsa/private_ed25519_key.pem b/tests/eddsa/private_ed25519_key.pem new file mode 100644 index 0000000..84b5e06 --- /dev/null +++ b/tests/eddsa/private_ed25519_key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGrD/e7uKYqSY4twDEsRfMMuLSrODf14dpTiTK6K1YI0 +-----END PRIVATE KEY----- diff --git a/tests/eddsa/private_ed25519_key.pk8 b/tests/eddsa/private_ed25519_key.pk8 new file mode 100644 index 0000000000000000000000000000000000000000..3199873c6b94586fc85bd5cc97e3345e8ccb560f GIT binary patch literal 48 zcmXreV`5}5U}a<0PAy"Hé:ExJPV? \ No newline at end of file diff --git a/tests/lib.rs b/tests/lib.rs index 3760c89..9033881 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,2 +1,3 @@ mod ecdsa; +mod eddsa; mod rsa;