mirror of
https://github.com/BitskiCo/jwk-rs
synced 2024-12-29 12:08:50 +00:00
Add docs
This commit is contained in:
parent
30ceb84639
commit
bca7f0a3ca
@ -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"
|
||||
|
58
README.md
58
README.md
@ -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
|
||||
|
@ -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]);
|
||||
|
@ -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>);
|
||||
|
@ -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),
|
||||
);
|
||||
|
91
src/lib.rs
91
src/lib.rs
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user