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:
parent
188772365a
commit
8b1d543598
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "jsonwebkey"
|
name = "jsonwebkey"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
authors = ["Nick Hynes <nhynes@nhynes.com>"]
|
authors = ["Nick Hynes <nhynes@nhynes.com>"]
|
||||||
description = "JSON Web Key (JWK) (de)serialization, generation, and conversion."
|
description = "JSON Web Key (JWK) (de)serialization, generation, and conversion."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@ -17,4 +17,8 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
syn = { version = "1.0", features = ["full"] } # required to parse const generics
|
syn = { version = "1.0", features = ["full"] } # required to parse const generics
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
yasna = { version = "0.3", optional = true }
|
||||||
zeroize = { version = "1.1", features = ["zeroize_derive"] }
|
zeroize = { version = "1.1", features = ["zeroize_derive"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
conversion = ["yasna"]
|
||||||
|
35
README.md
35
README.md
@ -1,10 +1,37 @@
|
|||||||
# jsonwebkey
|
# 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)
|
- [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)
|
- [ ] 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();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -9,7 +9,7 @@ use zeroize::{Zeroize, Zeroizing};
|
|||||||
|
|
||||||
use crate::utils::{deserialize_base64, serialize_base64};
|
use crate::utils::{deserialize_base64, serialize_base64};
|
||||||
|
|
||||||
#[derive(Zeroize, Deref, AsRef, From)]
|
#[derive(Clone, Zeroize, Deref, AsRef, From)]
|
||||||
#[zeroize(drop)]
|
#[zeroize(drop)]
|
||||||
pub struct ByteArray<const N: usize>(pub [u8; N]);
|
pub struct ByteArray<const N: usize>(pub [u8; N]);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ use zeroize::Zeroize;
|
|||||||
|
|
||||||
use crate::utils::{deserialize_base64, serialize_base64};
|
use crate::utils::{deserialize_base64, serialize_base64};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Zeroize, Deref, AsRef, From)]
|
#[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)]
|
||||||
#[zeroize(drop)]
|
#[zeroize(drop)]
|
||||||
pub struct ByteVec(pub Vec<u8>);
|
pub struct ByteVec(pub Vec<u8>);
|
||||||
|
|
||||||
|
199
src/lib.rs
199
src/lib.rs
@ -18,7 +18,7 @@ pub use key_ops::KeyOps;
|
|||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct JsonWebKey {
|
pub struct JsonWebKey {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub key_type: Box<KeyType>,
|
pub key: Box<Key>,
|
||||||
|
|
||||||
#[serde(default, rename = "use", skip_serializing_if = "Option::is_none")]
|
#[serde(default, rename = "use", skip_serializing_if = "Option::is_none")]
|
||||||
pub key_use: Option<KeyUse>,
|
pub key_use: Option<KeyUse>,
|
||||||
@ -46,12 +46,12 @@ impl std::str::FromStr for JsonWebKey {
|
|||||||
|
|
||||||
// Validate alg.
|
// Validate alg.
|
||||||
use JsonWebAlgorithm::*;
|
use JsonWebAlgorithm::*;
|
||||||
use KeyType::*;
|
use Key::*;
|
||||||
let alg = match &jwk.algorithm {
|
let alg = match &jwk.algorithm {
|
||||||
Some(alg) => alg,
|
Some(alg) => alg,
|
||||||
None => return Ok(jwk),
|
None => return Ok(jwk),
|
||||||
};
|
};
|
||||||
match (alg, &*jwk.key_type) {
|
match (alg, &*jwk.key) {
|
||||||
(
|
(
|
||||||
ES256,
|
ES256,
|
||||||
EC {
|
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")]
|
#[serde(tag = "kty")]
|
||||||
pub enum KeyType {
|
pub enum Key {
|
||||||
EC {
|
EC {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
params: Curve,
|
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")]
|
#[serde(tag = "crv")]
|
||||||
pub enum Curve {
|
pub enum Curve {
|
||||||
/// prime256v1
|
/// prime256v1
|
||||||
@ -109,7 +290,7 @@ pub enum Curve {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RsaPublic {
|
pub struct RsaPublic {
|
||||||
/// Public exponent. Must be 65537.
|
/// Public exponent. Must be 65537.
|
||||||
pub e: PublicExponent,
|
pub e: PublicExponent,
|
||||||
@ -120,7 +301,7 @@ pub struct RsaPublic {
|
|||||||
const PUBLIC_EXPONENT: u32 = 65537;
|
const PUBLIC_EXPONENT: u32 = 65537;
|
||||||
const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros
|
const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros
|
||||||
const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA==";
|
const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA==";
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct PublicExponent;
|
pub struct PublicExponent;
|
||||||
|
|
||||||
impl Serialize for 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 {
|
pub struct RsaPrivate {
|
||||||
/// Private exponent.
|
/// Private exponent.
|
||||||
pub d: ByteVec,
|
pub d: ByteVec,
|
||||||
|
34
src/tests.rs
34
src/tests.rs
@ -19,7 +19,7 @@ fn deserialize_es256() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
jwk,
|
jwk,
|
||||||
JsonWebKey {
|
JsonWebKey {
|
||||||
key_type: box KeyType::EC {
|
key: box Key::EC {
|
||||||
// The parameters were decoded using a 10-liner Rust script.
|
// The parameters were decoded using a 10-liner Rust script.
|
||||||
params: Curve::P256 {
|
params: Curve::P256 {
|
||||||
d: Some(
|
d: Some(
|
||||||
@ -53,7 +53,7 @@ fn deserialize_es256() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn serialize_es256() {
|
fn serialize_es256() {
|
||||||
let jwk = JsonWebKey {
|
let jwk = JsonWebKey {
|
||||||
key_type: box KeyType::EC {
|
key: box Key::EC {
|
||||||
params: Curve::P256 {
|
params: Curve::P256 {
|
||||||
d: None,
|
d: None,
|
||||||
x: [1u8; 32].into(),
|
x: [1u8; 32].into(),
|
||||||
@ -83,7 +83,7 @@ fn deserialize_hs256() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
jwk,
|
jwk,
|
||||||
JsonWebKey {
|
JsonWebKey {
|
||||||
key_type: box KeyType::Symmetric {
|
key: box Key::Symmetric {
|
||||||
// The parameters were decoded using a 10-liner Rust script.
|
// The parameters were decoded using a 10-liner Rust script.
|
||||||
key: vec![180, 3, 141, 233].into(),
|
key: vec![180, 3, 141, 233].into(),
|
||||||
},
|
},
|
||||||
@ -98,7 +98,7 @@ fn deserialize_hs256() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn serialize_hs256() {
|
fn serialize_hs256() {
|
||||||
let jwk = JsonWebKey {
|
let jwk = JsonWebKey {
|
||||||
key_type: box KeyType::Symmetric {
|
key: box Key::Symmetric {
|
||||||
key: vec![42; 16].into(),
|
key: vec![42; 16].into(),
|
||||||
},
|
},
|
||||||
key_id: None,
|
key_id: None,
|
||||||
@ -130,7 +130,7 @@ fn deserialize_rs256() {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
jwk,
|
jwk,
|
||||||
JsonWebKey {
|
JsonWebKey {
|
||||||
key_type: box KeyType::RSA {
|
key: box Key::RSA {
|
||||||
public: RsaPublic {
|
public: RsaPublic {
|
||||||
e: PublicExponent,
|
e: PublicExponent,
|
||||||
n: vec![
|
n: vec![
|
||||||
@ -201,7 +201,7 @@ fn deserialize_rs256() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn serialize_rs256() {
|
fn serialize_rs256() {
|
||||||
let jwk = JsonWebKey {
|
let jwk = JsonWebKey {
|
||||||
key_type: box KeyType::RSA {
|
key: box Key::RSA {
|
||||||
public: RsaPublic {
|
public: RsaPublic {
|
||||||
e: PublicExponent,
|
e: PublicExponent,
|
||||||
n: vec![105, 183, 62].into(),
|
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-----"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user