From 8b1d54359897bc0395505341bd320d4cff3df846 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Sun, 12 Jul 2020 21:23:06 +0000 Subject: [PATCH] Add PEM conversion --- Cargo.toml | 6 +- README.md | 35 +++++++- src/byte_array.rs | 2 +- src/byte_vec.rs | 2 +- src/lib.rs | 199 +++++++++++++++++++++++++++++++++++++++++++--- src/tests.rs | 34 ++++++-- 6 files changed, 256 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3062828..cf20f3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebkey" -version = "0.0.1" +version = "0.0.2" authors = ["Nick Hynes "] description = "JSON Web Key (JWK) (de)serialization, generation, and conversion." readme = "README.md" @@ -17,4 +17,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" syn = { version = "1.0", features = ["full"] } # required to parse const generics thiserror = "1.0" +yasna = { version = "0.3", optional = true } zeroize = { version = "1.1", features = ["zeroize_derive"] } + +[features] +conversion = ["yasna"] diff --git a/README.md b/README.md index 05903b5..22e0e80 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,37 @@ # jsonwebkey -**JSON Web Key (JWK) (de)serialization, generation, and conversion.** +*[JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517#section-4.3) (de)serialization, generation, and conversion.* -This library aims to be [spec](https://tools.ietf.org/html/rfc7517#section-4.3) compliant and secure. +Note: requires rustc nightly >= 1.45 for conveniences around fixed-size arrays. + +**Goals** + +tl;dr: get keys into a format that can be used by other crates; be as safe as possible while doing so. -Features: - [x] Serialization and deserialization of _Required_ and _Recommended_ key types (HS256, RS256, ES256) -- [ ] Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken)) +- [x] Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken)) - [ ] Key generation (particularly for testing) + +**Non-goals** + +* be a fully-featured JOSE framework + +## Example + +```rust +extern crate jsonwebtoken as jwt; +extern crate jsonwebkey as jwk; + +fn main() { + let jwk_str = r#"{ + "kty": "EC", + "d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", + "crv": "P-256", + "x": "QOMHmv96tVlJv-uNqprnDSKIj5AiLTXKRomXYnav0N0", + "y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k" + }"#; + let jwk = jwk::JsonWebKey::from_str(jwk_str).unwrap(); + let encoding_key = jwk::EncodingKey::from_ec_der(jwk.to_der().unwrap()); + let token = jwt::encode(&jwt::Header::default(), &() /* claims */, encoding_key).unwrap(); +} +``` diff --git a/src/byte_array.rs b/src/byte_array.rs index 708bda8..5cded5d 100644 --- a/src/byte_array.rs +++ b/src/byte_array.rs @@ -9,7 +9,7 @@ use zeroize::{Zeroize, Zeroizing}; use crate::utils::{deserialize_base64, serialize_base64}; -#[derive(Zeroize, Deref, AsRef, From)] +#[derive(Clone, Zeroize, Deref, AsRef, From)] #[zeroize(drop)] pub struct ByteArray(pub [u8; N]); diff --git a/src/byte_vec.rs b/src/byte_vec.rs index 056d13e..ba054ae 100644 --- a/src/byte_vec.rs +++ b/src/byte_vec.rs @@ -9,7 +9,7 @@ use zeroize::Zeroize; use crate::utils::{deserialize_base64, serialize_base64}; -#[derive(PartialEq, Eq, Zeroize, Deref, AsRef, From)] +#[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)] #[zeroize(drop)] pub struct ByteVec(pub Vec); diff --git a/src/lib.rs b/src/lib.rs index 0dcaf13..5e5ad4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ pub use key_ops::KeyOps; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct JsonWebKey { #[serde(flatten)] - pub key_type: Box, + pub key: Box, #[serde(default, rename = "use", skip_serializing_if = "Option::is_none")] pub key_use: Option, @@ -46,12 +46,12 @@ impl std::str::FromStr for JsonWebKey { // Validate alg. use JsonWebAlgorithm::*; - use KeyType::*; + use Key::*; let alg = match &jwk.algorithm { Some(alg) => alg, None => return Ok(jwk), }; - match (alg, &*jwk.key_type) { + match (alg, &*jwk.key) { ( ES256, EC { @@ -75,9 +75,9 @@ impl std::fmt::Display for JsonWebKey { } } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kty")] -pub enum KeyType { +pub enum Key { EC { #[serde(flatten)] params: Curve, @@ -95,7 +95,188 @@ pub enum KeyType { }, } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +impl Key { + /// Returns true iff this key only contains private components (i.e. a private asymmetric + /// key or a symmetric key). + fn is_private(&self) -> bool { + match self { + Self::Symmetric { .. } + | Self::EC { + params: Curve::P256 { d: Some(_), .. }, + .. + } + | Self::RSA { + private: Some(_), .. + } => true, + _ => false, + } + } + + /// Returns true iff this key only contains non-private components. + pub fn is_public(&self) -> bool { + !self.is_private() + } + + /// Returns the public part of this key, if it's symmetric. + pub fn to_public(&self) -> Option { + if self.is_public() { + return Some(self.clone()); + } + Some(match self { + Self::Symmetric { .. } => return None, + Self::EC { + params: Curve::P256 { x, y, .. }, + } => Self::EC { + params: Curve::P256 { + x: x.clone(), + y: y.clone(), + d: None, + }, + }, + Self::RSA { public, .. } => Self::RSA { + public: public.clone(), + private: None, + }, + }) + } + + /// If this key is asymmetric, encodes it as PKCS#8. + #[cfg(feature = "conversion")] + pub fn to_der(&self) -> Option> { + use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag}; + + if let Self::Symmetric { .. } = self { + return None; + } + Some(yasna::construct_der(|writer| match self { + Self::EC { + params: Curve::P256 { d, x, y }, + } => { + let write_curve_oid = |writer: DERWriter| { + writer.write_oid(&ObjectIdentifier::from_slice(&[ + 1, 2, 840, 10045, 3, 1, 7, // prime256v1 + ])); + }; + let write_public = |writer: DERWriter| { + let public_bytes: Vec = [0x04 /* uncompressed */] + .iter() + .chain(x.iter()) + .chain(y.iter()) + .copied() + .collect(); + writer.write_bitvec_bytes(&public_bytes, 8 * (32 * 2 + 1)); + }; + writer.write_sequence(|writer| { + match d { + Some(private_point) => { + writer.next().write_i8(1); // version + writer.next().write_bytes(private_point.as_ref()); + writer.next().write_tagged(Tag::context(0), |writer| { + write_curve_oid(writer); + }); + writer.next().write_tagged(Tag::context(1), |writer| { + write_public(writer); + }); + } + None => { + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(&[ + 1, 2, 840, 10045, 2, 1, // ecPublicKey + ])); + write_curve_oid(writer.next()); + }); + write_public(writer.next()); + } + }; + }); + } + Self::RSA { public, private } => { + let write_alg_id = |writer: &mut DERWriterSeq| { + writer.next().write_oid(&ObjectIdentifier::from_slice(&[ + 1, 2, 840, 113549, 1, 1, 1, // rsaEncryption + ])); + writer.next().write_null(); // parameters + }; + let write_public = |writer: &mut DERWriterSeq| { + writer.next().write_bytes(&*public.n); + writer.next().write_u32(PUBLIC_EXPONENT); + }; + writer.write_sequence(|writer| { + match private { + Some(private) => { + writer.next().write_i8(0); // version + writer.next().write_sequence(|writer| { + write_alg_id(writer); + }); + writer + .next() + .write_tagged(yasna::tags::TAG_OCTETSTRING, |writer| { + writer.write_sequence(|writer| { + writer.next().write_i8(0); // version + write_public(writer); + writer.next().write_bytes(&private.d); + if let Some(p) = &private.p { + writer.next().write_bytes(p); + } + if let Some(q) = &private.q { + writer.next().write_bytes(q); + } + if let Some(dp) = &private.dp { + writer.next().write_bytes(dp); + } + if let Some(dq) = &private.dq { + writer.next().write_bytes(dq); + } + if let Some(qi) = &private.qi { + writer.next().write_bytes(qi); + } + }); + }); + } + None => { + write_alg_id(writer); + writer + .next() + .write_tagged(yasna::tags::TAG_BITSTRING, |writer| { + writer.write_sequence(|writer| { + write_public(writer); + }) + }); + } + } + }); + } + Self::Symmetric { .. } => unreachable!("checked above"), + })) + } + + /// If this key is asymmetric, encodes it as PKCS#8 with PEM armoring. + #[cfg(feature = "conversion")] + pub fn to_pem(&self) -> Option { + use std::fmt::Write; + let der_b64 = base64::encode(self.to_der()?); + let key_ty = if self.is_private() { + "PRIVATE" + } else { + "PUBLIC" + }; + let mut pem = String::new(); + writeln!(&mut pem, "-----BEGIN {} KEY-----", key_ty).unwrap(); + const MAX_LINE_LEN: usize = 64; + for i in (0..der_b64.len()).step_by(MAX_LINE_LEN) { + writeln!( + &mut pem, + "{}", + &der_b64[i..std::cmp::min(i + MAX_LINE_LEN, der_b64.len())] + ) + .unwrap(); + } + writeln!(&mut pem, "-----END {} KEY-----", key_ty).unwrap(); + Some(pem) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "crv")] pub enum Curve { /// prime256v1 @@ -109,7 +290,7 @@ pub enum Curve { }, } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RsaPublic { /// Public exponent. Must be 65537. pub e: PublicExponent, @@ -120,7 +301,7 @@ pub struct RsaPublic { const PUBLIC_EXPONENT: u32 = 65537; const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA=="; -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PublicExponent; impl Serialize for PublicExponent { @@ -143,7 +324,7 @@ impl<'de> Deserialize<'de> for PublicExponent { } } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RsaPrivate { /// Private exponent. pub d: ByteVec, diff --git a/src/tests.rs b/src/tests.rs index 590c458..3a36800 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -19,7 +19,7 @@ fn deserialize_es256() { assert_eq!( jwk, JsonWebKey { - key_type: box KeyType::EC { + key: box Key::EC { // The parameters were decoded using a 10-liner Rust script. params: Curve::P256 { d: Some( @@ -53,7 +53,7 @@ fn deserialize_es256() { #[test] fn serialize_es256() { let jwk = JsonWebKey { - key_type: box KeyType::EC { + key: box Key::EC { params: Curve::P256 { d: None, x: [1u8; 32].into(), @@ -83,7 +83,7 @@ fn deserialize_hs256() { assert_eq!( jwk, JsonWebKey { - key_type: box KeyType::Symmetric { + key: box Key::Symmetric { // The parameters were decoded using a 10-liner Rust script. key: vec![180, 3, 141, 233].into(), }, @@ -98,7 +98,7 @@ fn deserialize_hs256() { #[test] fn serialize_hs256() { let jwk = JsonWebKey { - key_type: box KeyType::Symmetric { + key: box Key::Symmetric { key: vec![42; 16].into(), }, key_id: None, @@ -130,7 +130,7 @@ fn deserialize_rs256() { assert_eq!( jwk, JsonWebKey { - key_type: box KeyType::RSA { + key: box Key::RSA { public: RsaPublic { e: PublicExponent, n: vec![ @@ -201,7 +201,7 @@ fn deserialize_rs256() { #[test] fn serialize_rs256() { let jwk = JsonWebKey { - key_type: box KeyType::RSA { + key: box Key::RSA { public: RsaPublic { e: PublicExponent, n: vec![105, 183, 62].into(), @@ -261,3 +261,25 @@ fn mismatched_algorithm() { }"# ); } + +#[cfg(feature = "conversion")] +#[test] +fn es256_to_pem() { + let jwk_str = r#"{ + "kty": "EC", + "d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", + "crv": "P-256", + "x": "QOMHmv96tVlJv-uNqprnDSKIj5AiLTXKRomXYnav0N0", + "y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k" + }"#; + let jwk = JsonWebKey::from_str(jwk_str).unwrap(); + #[rustfmt::skip] + assert_eq!( + base64::encode(jwk.key.to_pem().unwrap()), +"-----BEGIN PRIVATE KEY----- +MHcCAQEEIGaCkPY+HYSAZTEVa7/kBvD/0/bLrb9//eXoqPTLaYCooAoGCCqGSM49 +AwEHoUQDQgAEQOMHmv96tVlJv+uNqprnDSKIj5AiLTXKRomXYnav0N1ONhmgedy1 +q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== +-----END PRIVATE KEY-----" + ); +}