1
0
mirror of https://github.com/BitskiCo/jwk-rs synced 2024-11-24 12:59:22 +00:00
This commit is contained in:
Nick Hynes 2020-07-13 23:51:09 +00:00
parent 30ceb84639
commit bca7f0a3ca
No known key found for this signature in database
GPG Key ID: 5B3463E9F1D73C83
7 changed files with 140 additions and 55 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "jsonwebkey"
version = "0.0.2"
version = "0.0.3"
authors = ["Nick Hynes <nhynes@nhynes.com>"]
description = "JSON Web Key (JWK) (de)serialization, generation, and conversion."
readme = "README.md"
@ -15,7 +15,6 @@ derive_more = "0.99"
jsonwebtoken = { version = "7.2", optional = true }
num-bigint = { version = "0.2", optional = true }
p256 = { version = "0.3", optional = true }
paste = "0.1"
rand = { version = "0.7", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@ -8,32 +8,56 @@ Note: requires rustc nightly >= 1.45 for conveniences around fixed-size arrays.
tl;dr: get keys into a format that can be used by other crates; be as safe as possible while doing so.
- [x] Serialization and deserialization of _Required_ and _Recommended_ key types (HS256, RS256, ES256)
- [x] Conversion to PEM for interop with existing JWT libraries (e.g., [jsonwebtoken](https://crates.io/crates/jsonwebtoken))
- [ ] Key generation (particularly for testing)
- 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))
- Key generation (particularly useful for testing)
**Non-goals**
* be a fully-featured JOSE framework
- be a fully-featured JOSE framework
## Example
## Examples
### Deserializing from JSON
```rust
extern crate jsonwebkey as jwk;
// Generated using https://mkjwk.org/.
let jwt_str = r#"{
"kty": "oct",
"use": "sig",
"kid": "my signing key",
"k": "Wpj30SfkzM_m0Sa_B2NqNw",
"alg": "HS256"
}"#;
let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap();
println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields.
```
### Using with other crates
```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();
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TokenClaims {}
let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256());
my_jwk.set_algorithm(jwk::Algorithm::ES256);
let encoding_key = jwt::EncodingKey::from_ec_der(&my_jwk.key.to_der().unwrap());
let token = jwt::encode(
&jwt::Header::new(my_jwk.algorithm.unwrap().into()),
&TokenClaims {},
&encoding_key,
).unwrap();
let public_pem = my_jwk.key.to_public().unwrap().to_pem().unwrap();
let decoding_key = jwt::DecodingKey::from_ec_pem(public_pem.as_bytes()).unwrap();
let mut validation = jwt::Validation::new(my_jwk.algorithm.unwrap().into());
validation.validate_exp = false;
jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap();
```
## Features

View File

@ -9,6 +9,7 @@ use zeroize::{Zeroize, Zeroizing};
use crate::utils::{deserialize_base64, serialize_base64};
/// A zeroizing-on-drop container for a `[u8; N]` that deserializes from base64.
#[derive(Clone, Zeroize, Deref, AsRef, From)]
#[zeroize(drop)]
pub struct ByteArray<const N: usize>(pub [u8; N]);

View File

@ -9,6 +9,7 @@ use zeroize::Zeroize;
use crate::utils::{deserialize_base64, serialize_base64};
/// A zeroizing-on-drop container for a `Vec<u8>` that deserializes from base64.
#[derive(Clone, PartialEq, Eq, Zeroize, Deref, AsRef, From)]
#[zeroize(drop)]
pub struct ByteVec(pub Vec<u8>);

View File

@ -4,13 +4,11 @@ use serde::{
};
macro_rules! impl_key_ops {
($(($key_op:ident, $i:literal)),+,) => {
paste::item! {
bitflags::bitflags! {
#[derive(Default)]
pub struct KeyOps: u16 {
$(const [<$key_op:upper>] = $i;)*
}
($(($key_op:ident, $const_name:ident, $i:literal)),+,) => {
bitflags::bitflags! {
#[derive(Default)]
pub struct KeyOps: u16 {
$(const $const_name = $i;)*
}
}
@ -18,7 +16,7 @@ macro_rules! impl_key_ops {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut seq = s.serialize_seq(Some(self.bits().count_ones() as usize))?;
$(
if self.contains(paste::expr! { KeyOps::[<$key_op:upper>] }) {
if self.contains(KeyOps::$const_name) {
seq.serialize_element(stringify!($key_op))?;
}
)+
@ -33,7 +31,7 @@ macro_rules! impl_key_ops {
for op_str in op_strs {
$(
if op_str == stringify!($key_op) {
ops |= paste::expr! { KeyOps::[<$key_op:upper>] };
ops |= KeyOps::$const_name;
continue;
}
)+
@ -47,12 +45,12 @@ macro_rules! impl_key_ops {
#[rustfmt::skip]
impl_key_ops!(
(sign, 0b00000001),
(verify, 0b00000010),
(encrypt, 0b00000100),
(decrypt, 0b00001000),
(wrapKey, 0b00010000),
(unwrapKey, 0b00100000),
(deriveKey, 0b01000000),
(deriveBits, 0b10000000),
(sign, SIGN, 0b00000001),
(verify, VERIFY, 0b00000010),
(encrypt, ENCRYPT, 0b00000100),
(decrypt, DECRYPT, 0b00001000),
(wrapKey, WRAP_KEY, 0b00010000),
(unwrapKey, UNWRAP_KEY, 0b00100000),
(deriveKey, DERIVE_KEY, 0b01000000),
(deriveBits, DERIVE_BITS, 0b10000000),
);

View File

@ -1,6 +1,65 @@
#![allow(incomplete_features)]
#![feature(box_syntax, const_generics, fixed_size_array)]
//! # jsonwebkey
//!
//! *[JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517#section-4.3) (de)serialization, generation, and conversion.*
//!
//! **Note**: this crate requires Rust nightly >= 1.45 because it uses
//! `feature(const_generics, fixed_size_array)` to enable statically-checked key lengths.
//!
//! ## Examples
//!
//! ### Deserializing from JSON
//!
//! ```
//! extern crate jsonwebkey as jwk;
//! // Generated using https://mkjwk.org/.
//! let jwt_str = r#"{
//! "kty": "oct",
//! "use": "sig",
//! "kid": "my signing key",
//! "k": "Wpj30SfkzM_m0Sa_B2NqNw",
//! "alg": "HS256"
//! }"#;
//! let jwk: jwk::JsonWebKey = jwt_str.parse().unwrap();
//! println!("{:#?}", jwk); // looks like `jwt_str` but with reordered fields.
//! ```
//!
//! ### Using with other crates
//!
//! ```
//! extern crate jsonwebtoken as jwt;
//! extern crate jsonwebkey as jwk;
//!
//! #[derive(serde::Serialize, serde::Deserialize)]
//! struct TokenClaims {}
//!
//! let mut my_jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256());
//! my_jwk.set_algorithm(jwk::Algorithm::ES256);
//!
//! let encoding_key = jwt::EncodingKey::from_ec_der(&my_jwk.key.to_der().unwrap());
//! let token = jwt::encode(
//! &jwt::Header::new(my_jwk.algorithm.unwrap().into()),
//! &TokenClaims {},
//! &encoding_key,
//! ).unwrap();
//!
//! let public_pem = my_jwk.key.to_public().unwrap().to_pem().unwrap();
//! let decoding_key = jwt::DecodingKey::from_ec_pem(public_pem.as_bytes()).unwrap();
//! let mut validation = jwt::Validation::new(my_jwk.algorithm.unwrap().into());
//! validation.validate_exp = false;
//! jwt::decode::<TokenClaims>(&token, &decoding_key, &validation).unwrap();
//! ```
//!
//! ## Features
//!
//! * `convert` - enables `Key::{to_der, to_pem}`.
//! This pulls in the [yasna](https://crates.io/crates/yasna) crate.
//! * `generate` - enables `Key::{generate_p256, generate_symmetric}`.
//! This pulls in the [p256](https://crates.io/crates/p256) and [rand](https://crates.io/crates/rand) crates.
//! * `jsonwebtoken` - enables conversions to types in the [jsonwebtoken](https://crates.io/crates/jsonwebtoken) crate.
mod byte_array;
mod byte_vec;
mod key_ops;
@ -8,10 +67,7 @@ mod key_ops;
mod tests;
mod utils;
use std::array::FixedSizeArray;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
pub use byte_array::ByteArray;
pub use byte_vec::ByteVec;
@ -32,7 +88,7 @@ pub struct JsonWebKey {
pub key_id: Option<String>,
#[serde(default, rename = "alg", skip_serializing_if = "Option::is_none")]
pub algorithm: Option<JsonWebAlgorithm>,
pub algorithm: Option<Algorithm>,
}
impl JsonWebKey {
@ -46,7 +102,7 @@ impl JsonWebKey {
}
}
pub fn set_algorithm(&mut self, alg: JsonWebAlgorithm) -> Result<(), Error> {
pub fn set_algorithm(&mut self, alg: Algorithm) -> Result<(), Error> {
Self::validate_algorithm(alg, &*self.key)?;
self.algorithm = Some(alg);
Ok(())
@ -56,8 +112,8 @@ impl JsonWebKey {
Ok(serde_json::from_slice(bytes.as_ref())?)
}
fn validate_algorithm(alg: JsonWebAlgorithm, key: &Key) -> Result<(), Error> {
use JsonWebAlgorithm::*;
fn validate_algorithm(alg: Algorithm, key: &Key) -> Result<(), Error> {
use Algorithm::*;
use Key::*;
match (alg, key) {
(
@ -199,6 +255,7 @@ impl Key {
Some(private_point) => {
pkcs8::write_private(oids, |writer: &mut DERWriterSeq| {
writer.next().write_i8(1); // version
use std::array::FixedSizeArray;
writer.next().write_bytes(private_point.as_slice());
// The following tagged value is optional. OpenSSL produces it,
// but many tools, including jwt.io and `jsonwebtoken`, don't like it,
@ -333,28 +390,32 @@ impl Key {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "crv")]
pub enum Curve {
/// prime256v1
/// Parameters of the prime256v1 (P256) curve.
#[serde(rename = "P-256")]
P256 {
/// Private point.
/// The private scalar.
#[serde(skip_serializing_if = "Option::is_none")]
d: Option<ByteArray<32>>,
/// The curve point x coordinate.
x: ByteArray<32>,
/// The curve point y coordinate.
y: ByteArray<32>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RsaPublic {
/// Public exponent. Must be 65537.
/// The standard public exponent, 65537.
pub e: PublicExponent,
/// Modulus, p*q.
/// The modulus, p*q.
pub n: ByteVec,
}
const PUBLIC_EXPONENT: u32 = 65537;
const PUBLIC_EXPONENT_B64: &str = "AQAB"; // little-endian, strip zeros
const PUBLIC_EXPONENT_B64_PADDED: &str = "AQABAA==";
/// The standard RSA public exponent, 65537.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PublicExponent;
@ -391,7 +452,7 @@ pub struct RsaPrivate {
/// First factor Chinese Remainder Theorem (CRT) exponent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dp: Option<ByteVec>,
/// Second factor Chinese Remainder Theorem (CRT) exponent.
/// Second factor CRT exponent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dq: Option<ByteVec>,
/// First CRT coefficient.
@ -407,15 +468,15 @@ pub enum KeyUse {
Encryption,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)]
pub enum JsonWebAlgorithm {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Algorithm {
HS256,
RS256,
ES256,
}
#[cfg(any(test, feature = "jsonwebtoken"))]
impl Into<jsonwebtoken::Algorithm> for JsonWebAlgorithm {
impl Into<jsonwebtoken::Algorithm> for Algorithm {
fn into(self) -> jsonwebtoken::Algorithm {
match self {
Self::HS256 => jsonwebtoken::Algorithm::HS256,

View File

@ -24,6 +24,7 @@ static RSA_JWK_FIXTURE: &str = r#"{
"qi": "adhQHH8IGXFfLEMnZ5t_TeCp5zgSwQktJ2lmylxUG0M",
"dp": "qVnLiKeoSG_Olz17OGBGd4a2sqVFnrjh_51wuaQDdTk",
"dq": "GL_Ec6xYg2z1FRfyyGyU1lgf0BJFTZcfNI8ISIN5ssE",
"key_ops": ["wrapKey"],
"n": "pCzbcd9kjvg5rfGHdEMWnXo49zbB6FLQ-m0B0BvVp0aojVWYa0xujC-ZP7ZhxByPxyc2PazwFJJi9ivZ_ggRww"
}"#;
@ -56,7 +57,7 @@ fn deserialize_es256() {
.into(),
},
},
algorithm: Some(JsonWebAlgorithm::ES256),
algorithm: Some(Algorithm::ES256),
key_id: Some("a key".into()),
key_ops: KeyOps::empty(),
key_use: Some(KeyUse::Encryption),
@ -94,7 +95,7 @@ fn generate_p256() {
struct TokenClaims {}
let mut the_jwk = JsonWebKey::new(Key::generate_p256());
the_jwk.set_algorithm(JsonWebAlgorithm::ES256).unwrap();
the_jwk.set_algorithm(Algorithm::ES256).unwrap();
let encoding_key = jwt::EncodingKey::from_ec_der(&the_jwk.key.to_der().unwrap());
let token = jwt::encode(
@ -127,7 +128,7 @@ fn deserialize_hs256() {
// The parameters were decoded using a 10-liner Rust script.
key: vec![180, 3, 141, 233].into(),
},
algorithm: Some(JsonWebAlgorithm::HS256),
algorithm: Some(Algorithm::HS256),
key_id: None,
key_ops: KeyOps::SIGN | KeyOps::VERIFY,
key_use: None,
@ -219,7 +220,7 @@ fn deserialize_rs256() {
},
algorithm: None,
key_id: None,
key_ops: KeyOps::empty(),
key_ops: KeyOps::WRAP_KEY,
key_use: Some(KeyUse::Encryption),
}
);