1
0
mirror of https://github.com/BitskiCo/jwk-rs synced 2024-11-22 03:49:22 +00:00

Add PEM conversion

This commit is contained in:
Nick Hynes 2020-07-12 21:23:06 +00:00
parent 188772365a
commit 8b1d543598
No known key found for this signature in database
GPG Key ID: 5B3463E9F1D73C83
6 changed files with 256 additions and 22 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "jsonwebkey"
version = "0.0.1"
version = "0.0.2"
authors = ["Nick Hynes <nhynes@nhynes.com>"]
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"]

View File

@ -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();
}
```

View File

@ -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<const N: usize>(pub [u8; N]);

View File

@ -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<u8>);

View File

@ -18,7 +18,7 @@ pub use key_ops::KeyOps;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonWebKey {
#[serde(flatten)]
pub key_type: Box<KeyType>,
pub key: Box<Key>,
#[serde(default, rename = "use", skip_serializing_if = "Option::is_none")]
pub key_use: Option<KeyUse>,
@ -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<Self> {
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<Vec<u8>> {
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<u8> = [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<String> {
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,

View File

@ -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-----"
);
}