Add EdDSA (Ed25519) (#154)

This commit is contained in:
Charles Lehner 2020-11-17 08:15:17 -05:00 committed by Vincent Prouillet
parent 445dfe037e
commit 2178cc7506
12 changed files with 182 additions and 1 deletions

View File

@ -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,
}
}
}

23
src/crypto/eddsa.rs Normal file
View File

@ -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<String> {
let signing_key = signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(key)?;
let out = signing_key.sign(message.as_bytes());
Ok(b64_encode(out.as_ref()))
}

View File

@ -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<St
ecdsa::sign(ecdsa::alg_to_ec_signing(algorithm), key.inner(), message)
}
Algorithm::EdDSA => 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

View File

@ -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<Self> {
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::*;

View File

@ -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<Self> {
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
}

View File

@ -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<Classification> {
// 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<Classification> {
if oid == rsa_public_key_oid {
return Some(Classification::Rsa);
}
if oid == ed25519_oid {
return Some(Classification::Ed);
}
}
_ => {}
}

67
tests/eddsa/mod.rs Normal file
View File

@ -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::<Claims>(
&token,
&DecodingKey::from_ed_pem(pubkey_pem).unwrap(),
&Validation::new(Algorithm::EdDSA),
)
.unwrap();
assert_eq!(my_claims, token_data.claims);
}

View File

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIGrD/e7uKYqSY4twDEsRfMMuLSrODf14dpTiTK6K1YI0
-----END PRIVATE KEY-----

Binary file not shown.

View File

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA2+Jj2UvNCvQiUPNYRgSi0cJSPiJI6Rs6D0UTeEpQVj8=
-----END PUBLIC KEY-----

View File

@ -0,0 +1,2 @@
ÛâcÙKÍ
ô"PóXF¢ÑÂR>"Hé:ExJPV?

View File

@ -1,2 +1,3 @@
mod ecdsa;
mod eddsa;
mod rsa;