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

Use stable Rust

This commit is contained in:
Nick Hynes 2021-01-25 20:11:01 +03:00
parent 8014d18bf0
commit 9592cad01c
No known key found for this signature in database
GPG Key ID: 5B3463E9F1D73C83
8 changed files with 136 additions and 147 deletions

View File

@ -11,7 +11,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
toolchain: stable
override: true
components: rustfmt, clippy
@ -41,7 +41,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
toolchain: stable
override: true
- name: Build (release)
@ -58,7 +58,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
toolchain: stable
override: true
- uses: actions-rs/tarpaulin@v0.1

View File

@ -1,6 +1,6 @@
[package]
name = "jsonwebkey"
version = "0.2.0"
version = "0.3.0"
authors = ["Nick Hynes <nhynes@nhynes.com>"]
description = "JSON Web Key (JWK) (de)serialization, generation, and conversion."
readme = "README.md"
@ -9,24 +9,28 @@ license = "MIT"
edition = "2018"
[dependencies]
base64 = "0.12"
bitflags = "1.2"
derive_more = "0.99"
jsonwebtoken = { version = "7.2", optional = true }
num-bigint = { version = "0.2", optional = true }
p256 = { version = "0.3", optional = true }
rand = { version = "0.7", optional = true }
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, features = ["num-bigint"] }
zeroize = { version = "1.1", features = ["zeroize_derive"] }
base64 = "0.12"
bitflags = "1.2"
derive_more = "0.99"
generic-array = "0.14"
jsonwebtoken = { version = "7.2", optional = true }
num-bigint = { version = "0.2", optional = true }
p256 = { version = "0.3", optional = true }
rand = { version = "0.7", optional = true }
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, features = ["num-bigint"] }
zeroize = { version = "1.1", features = ["zeroize_derive"] }
[features]
pkcs-convert = ["num-bigint", "yasna"]
jwt-convert = ["pkcs-convert", "jsonwebtoken"]
generate = ["p256", "rand"]
jwt-convert = ["pkcs-convert", "jsonwebtoken"]
generate = ["p256", "rand"]
[dev-dependencies]
jsonwebtoken = "7.2"
[package.metadata.docs.rs]
all-features = true

View File

@ -6,8 +6,6 @@
*[JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517#section-4.3) (de)serialization, generation, and conversion.*
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.

View File

@ -1,61 +1,55 @@
use std::{array::FixedSizeArray, fmt};
use derive_more::{AsRef, Deref, From};
use serde::{
de::{self, Deserialize, Deserializer},
ser::{Serialize, Serializer},
};
use derive_more::{AsRef, Deref};
use generic_array::{ArrayLength, GenericArray};
use serde::de::{self, Deserialize, Deserializer};
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]);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deref, AsRef)]
#[serde(transparent)]
pub struct ByteArray<N: ArrayLength<u8>>(
#[serde(serialize_with = "crate::utils::serde_base64::serialize")] pub GenericArray<u8, N>,
);
impl<const N: usize> fmt::Debug for ByteArray<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if cfg!(debug_assertions) {
write!(f, "{}", base64::encode(self.0.as_slice()))
} else {
write!(f, "ByteArray<{}>", N)
}
impl<N: ArrayLength<u8>, T: Into<GenericArray<u8, N>>> From<T> for ByteArray<N> {
fn from(arr: T) -> Self {
Self(arr.into())
}
}
impl<const N: usize> PartialEq for ByteArray<N> {
fn eq(&self, other: &Self) -> bool {
self.0.as_slice() == other.0.as_slice()
impl<N: ArrayLength<u8>> Drop for ByteArray<N> {
fn drop(&mut self) {
Zeroize::zeroize(self.0.as_mut_slice())
}
}
impl<const N: usize> Eq for ByteArray<N> {}
impl<N: ArrayLength<u8>> ByteArray<N> {
/// An unwrapping version of `try_from_slice`.
pub fn from_slice(bytes: impl AsRef<[u8]>) -> Self {
Self::try_from_slice(bytes).unwrap()
}
impl<const N: usize> ByteArray<N> {
pub fn try_from_slice(bytes: impl AsRef<[u8]>) -> Result<Self, String> {
let mut arr = Self([0u8; N]);
let bytes = bytes.as_ref();
if bytes.len() != N {
Err(format!("expected {} bytes but got {}", N, bytes.len()))
if bytes.len() != N::USIZE {
Err(format!(
"expected {} bytes but got {}",
N::USIZE,
bytes.len()
))
} else {
arr.0.copy_from_slice(bytes);
Ok(arr)
Ok(Self(GenericArray::clone_from_slice(bytes)))
}
}
}
impl<const N: usize> Serialize for ByteArray<N> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
serialize_base64(self.0.as_slice(), s)
}
}
impl<'de, const N: usize> Deserialize<'de> for ByteArray<N> {
impl<'de, N: ArrayLength<u8>> Deserialize<'de> for ByteArray<N> {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let bytes = Zeroizing::new(deserialize_base64(d)?);
let bytes = Zeroizing::new(crate::utils::serde_base64::deserialize(d)?);
Self::try_from_slice(&*bytes).map_err(|_| {
de::Error::invalid_length(bytes.len(), &format!("{} base64-encoded bytes", N).as_str())
de::Error::invalid_length(
bytes.len(),
&format!("{} base64-encoded bytes", N::USIZE).as_str(),
)
})
}
}
@ -64,6 +58,8 @@ impl<'de, const N: usize> Deserialize<'de> for ByteArray<N> {
mod tests {
use super::*;
use generic_array::typenum::*;
static BYTES: &[u8] = &[1, 2, 3, 4, 5, 6, 7];
static BASE64_JSON: &str = "\"AQIDBAUGBw==\"";
@ -73,26 +69,26 @@ mod tests {
#[test]
fn test_serde_byte_array_good() {
let arr = ByteArray::<7>::try_from_slice(BYTES).unwrap();
let arr = ByteArray::<U7>::try_from_slice(BYTES).unwrap();
let b64 = serde_json::to_string(&arr).unwrap();
assert_eq!(b64, BASE64_JSON);
let bytes: ByteArray<7> = serde_json::from_str(&b64).unwrap();
assert_eq!(bytes.as_ref(), BYTES);
let bytes: ByteArray<U7> = serde_json::from_str(&b64).unwrap();
assert_eq!(bytes.as_slice(), BYTES);
}
#[test]
fn test_serde_deserialize_byte_array_invalid() {
let mut de = serde_json::Deserializer::from_str("\"Z\"");
ByteArray::<0>::deserialize(&mut de).unwrap_err();
ByteArray::<U0>::deserialize(&mut de).unwrap_err();
}
#[test]
fn test_serde_base64_deserialize_array_long() {
ByteArray::<6>::deserialize(&mut get_de()).unwrap_err();
ByteArray::<U6>::deserialize(&mut get_de()).unwrap_err();
}
#[test]
fn test_serde_base64_deserialize_array_short() {
ByteArray::<8>::deserialize(&mut get_de()).unwrap_err();
ByteArray::<U8>::deserialize(&mut get_de()).unwrap_err();
}
}

View File

@ -1,18 +1,13 @@
use std::fmt;
use derive_more::{AsRef, Deref, From};
use serde::{
de::{Deserialize, Deserializer},
ser::{Serialize, Serializer},
};
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)]
#[derive(Clone, PartialEq, Eq, Zeroize, Serialize, Deserialize, Deref, AsRef, From)]
#[zeroize(drop)]
pub struct ByteVec(pub Vec<u8>);
#[serde(transparent)]
pub struct ByteVec(#[serde(with = "crate::utils::serde_base64")] pub Vec<u8>);
impl fmt::Debug for ByteVec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -24,18 +19,6 @@ impl fmt::Debug for ByteVec {
}
}
impl Serialize for ByteVec {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
serialize_base64(&self.0, s)
}
}
impl<'de> Deserialize<'de> for ByteVec {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Ok(Self(deserialize_base64(d)?))
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,5 +1,5 @@
#![allow(incomplete_features)]
#![feature(box_syntax, const_generics, fixed_size_array)]
#![deny(rust_2018_idioms, unreachable_pub)]
#![forbid(unsafe_code)]
//! *[JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517#section-4.3) (de)serialization, generation, and conversion.*
//!
@ -58,6 +58,9 @@
//! 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.
#[macro_use]
extern crate serde;
mod byte_array;
mod byte_vec;
mod key_ops;
@ -67,6 +70,7 @@ mod utils;
use std::borrow::Cow;
use generic_array::typenum::U32;
use serde::{Deserialize, Serialize};
pub use byte_array::ByteArray;
@ -94,7 +98,7 @@ pub struct JsonWebKey {
impl JsonWebKey {
pub fn new(key: Key) -> Self {
Self {
key: box key,
key: Box::new(key),
key_use: None,
key_ops: KeyOps::empty(),
key_id: None,
@ -143,7 +147,7 @@ impl std::str::FromStr for JsonWebKey {
}
impl std::fmt::Display for JsonWebKey {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
} else {
@ -179,7 +183,7 @@ pub enum Key {
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 {
pub fn is_private(&self) -> bool {
match self {
Self::Symmetric { .. }
| Self::EC {
@ -194,7 +198,7 @@ impl Key {
}
/// Returns the public part of this key (symmetric keys have no public parts).
pub fn to_public(&self) -> Option<Cow<Self>> {
pub fn to_public(&self) -> Option<Cow<'_, Self>> {
if !self.is_private() {
return Some(Cow::Borrowed(self));
}
@ -236,7 +240,7 @@ impl Key {
let prime256v1_oid = ObjectIdentifier::from_slice(&[1, 2, 840, 10045, 3, 1, 7]);
let oids = &[Some(&ec_public_oid), Some(&prime256v1_oid)];
let write_public = |writer: DERWriter| {
let write_public = |writer: DERWriter<'_>| {
let public_bytes: Vec<u8> = [0x04 /* uncompressed */]
.iter()
.chain(x.iter())
@ -248,7 +252,7 @@ impl Key {
match d {
Some(private_point) => {
pkcs8::write_private(oids, |writer: &mut DERWriterSeq| {
pkcs8::write_private(oids, |writer: &mut DERWriterSeq<'_>| {
writer.next().write_i8(1); // version
writer.next().write_bytes(&**private_point);
// The following tagged value is optional. OpenSSL produces it,
@ -268,17 +272,17 @@ impl Key {
1, 2, 840, 113549, 1, 1, 1, // rsaEncryption
]);
let oids = &[Some(&rsa_encryption_oid), None];
let write_bytevec = |writer: DERWriter, vec: &ByteVec| {
let write_bytevec = |writer: DERWriter<'_>, vec: &ByteVec| {
let bigint = BigUint::from_bytes_be(vec.as_slice());
writer.write_biguint(&bigint);
};
let write_public = |writer: &mut DERWriterSeq| {
let write_public = |writer: &mut DERWriterSeq<'_>| {
write_bytevec(writer.next(), &public.n);
writer.next().write_u32(PUBLIC_EXPONENT);
};
let write_private = |writer: &mut DERWriterSeq, private: &RsaPrivate| {
let write_private = |writer: &mut DERWriterSeq<'_>, private: &RsaPrivate| {
// https://tools.ietf.org/html/rfc3447#appendix-A.1.2
writer.next().write_i8(0); // version (two-prime)
write_public(writer);
@ -389,8 +393,8 @@ impl Key {
Self::EC {
curve: Curve::P256 {
d: Some(sk_scalar.to_bytes().into()),
x: ByteArray::try_from_slice(x_bytes).unwrap(),
y: ByteArray::try_from_slice(y_bytes).unwrap(),
x: ByteArray::from_slice(x_bytes),
y: ByteArray::from_slice(y_bytes),
},
}
}
@ -404,11 +408,11 @@ pub enum Curve {
P256 {
/// The private scalar.
#[serde(skip_serializing_if = "Option::is_none")]
d: Option<ByteArray<32>>,
d: Option<ByteArray<U32>>,
/// The curve point x coordinate.
x: ByteArray<32>,
x: ByteArray<U32>,
/// The curve point y coordinate.
y: ByteArray<32>,
y: ByteArray<U32>,
},
}

View File

@ -2,6 +2,8 @@ use super::*;
use std::str::FromStr;
use crate::byte_array::ByteArray;
// Generated using https://mkjwk.org
static P256_JWK_FIXTURE: &str = r#"{
"kty": "EC",
@ -40,29 +42,24 @@ fn deserialize_es256() {
assert_eq!(
jwk,
JsonWebKey {
key: box Key::EC {
key: Box::new(Key::EC {
// The parameters were decoded using a 10-liner Rust script.
curve: Curve::P256 {
d: Some(
[
102, 130, 144, 246, 62, 29, 132, 128, 101, 49, 21, 107, 191, 228, 6,
240, 255, 211, 246, 203, 173, 191, 127, 253, 229, 232, 168, 244, 203,
105, 128, 168
]
.into()
),
x: [
d: Some(ByteArray::from_slice(&[
102, 130, 144, 246, 62, 29, 132, 128, 101, 49, 21, 107, 191, 228, 6, 240,
255, 211, 246, 203, 173, 191, 127, 253, 229, 232, 168, 244, 203, 105, 128,
168
])),
x: ByteArray::from_slice(&[
64, 227, 7, 154, 255, 122, 181, 89, 73, 191, 235, 141, 170, 154, 231, 13,
34, 136, 143, 144, 34, 45, 53, 202, 70, 137, 151, 98, 118, 175, 208, 221
]
.into(),
y: [
]),
y: ByteArray::from_slice(&[
78, 54, 25, 160, 121, 220, 181, 171, 68, 19, 163, 66, 172, 169, 151, 65,
210, 73, 62, 115, 115, 100, 69, 252, 156, 25, 153, 117, 237, 192, 99, 137
]
.into(),
])
},
},
}),
algorithm: Some(Algorithm::ES256),
key_id: Some("a key".into()),
key_ops: KeyOps::empty(),
@ -74,13 +71,13 @@ fn deserialize_es256() {
#[test]
fn serialize_es256() {
let jwk = JsonWebKey {
key: box Key::EC {
key: Box::new(Key::EC {
curve: Curve::P256 {
d: None,
x: [1u8; 32].into(),
y: [2u8; 32].into(),
x: ByteArray::from_slice(&[1u8; 32]),
y: ByteArray::from_slice(&[2u8; 32]),
},
},
}),
key_id: None,
algorithm: None,
key_ops: KeyOps::empty(),
@ -130,10 +127,10 @@ fn deserialize_hs256() {
assert_eq!(
jwk,
JsonWebKey {
key: box Key::Symmetric {
key: Box::new(Key::Symmetric {
// The parameters were decoded using a 10-liner Rust script.
key: vec![180, 3, 141, 233].into(),
},
}),
algorithm: Some(Algorithm::HS256),
key_id: None,
key_ops: KeyOps::SIGN | KeyOps::VERIFY,
@ -145,9 +142,9 @@ fn deserialize_hs256() {
#[test]
fn serialize_hs256() {
let jwk = JsonWebKey {
key: box Key::Symmetric {
key: Box::new(Key::Symmetric {
key: vec![42; 16].into(),
},
}),
key_id: None,
algorithm: None,
key_ops: KeyOps::empty(),
@ -165,7 +162,7 @@ fn deserialize_rs256() {
assert_eq!(
jwk,
JsonWebKey {
key: box Key::RSA {
key: Box::new(Key::RSA {
public: RsaPublic {
e: PublicExponent,
n: vec![
@ -223,7 +220,7 @@ fn deserialize_rs256() {
.into()
)
})
},
}),
algorithm: None,
key_id: None,
key_ops: KeyOps::WRAP_KEY,
@ -235,7 +232,7 @@ fn deserialize_rs256() {
#[test]
fn serialize_rs256() {
let jwk = JsonWebKey {
key: box Key::RSA {
key: Box::new(Key::RSA {
public: RsaPublic {
e: PublicExponent,
n: vec![105, 183, 62].into(),
@ -248,7 +245,7 @@ fn serialize_rs256() {
dq: None,
qi: None,
}),
},
}),
key_id: None,
algorithm: None,
key_ops: KeyOps::empty(),

View File

@ -16,29 +16,36 @@ fn base64_decode(b64: impl AsRef<[u8]>) -> Result<Vec<u8>, base64::DecodeError>
base64::decode_config(b64, base64_config())
}
pub fn serialize_base64<S: Serializer>(bytes: impl AsRef<[u8]>, s: S) -> Result<S::Ok, S::Error> {
base64_encode(bytes).serialize(s)
}
pub(crate) mod serde_base64 {
use super::*;
pub fn deserialize_base64<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64_str = Zeroizing::new(String::deserialize(d)?);
base64_decode(&*base64_str).map_err(|e| {
#[cfg(debug_assertions)]
let err_msg = e.to_string().to_lowercase();
#[cfg(not(debug_assertions))]
let err_msg = "invalid base64";
de::Error::custom(err_msg.strip_suffix(".").unwrap_or(&err_msg))
})
pub(crate) fn serialize<S: Serializer>(
bytes: impl AsRef<[u8]>,
s: S,
) -> Result<S::Ok, S::Error> {
base64_encode(bytes).serialize(s)
}
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64_str = Zeroizing::new(String::deserialize(d)?);
base64_decode(&*base64_str).map_err(|e| {
#[cfg(debug_assertions)]
let err_msg = e.to_string().to_lowercase();
#[cfg(not(debug_assertions))]
let err_msg = "invalid base64";
de::Error::custom(err_msg.strip_suffix(".").unwrap_or(&err_msg))
})
}
}
#[cfg(feature = "pkcs-convert")]
pub mod pkcs8 {
pub(crate) mod pkcs8 {
use yasna::{
models::{ObjectIdentifier, TaggedDerValue},
DERWriter, DERWriterSeq,
};
fn write_oids(writer: &mut DERWriterSeq, oids: &[Option<&ObjectIdentifier>]) {
fn write_oids(writer: &mut DERWriterSeq<'_>, oids: &[Option<&ObjectIdentifier>]) {
for oid in oids {
match oid {
Some(oid) => writer.next().write_oid(oid),
@ -47,9 +54,9 @@ pub mod pkcs8 {
}
}
pub fn write_private(
pub(crate) fn write_private(
oids: &[Option<&ObjectIdentifier>],
body_writer: impl FnOnce(&mut DERWriterSeq),
body_writer: impl FnOnce(&mut DERWriterSeq<'_>),
) -> Vec<u8> {
yasna::construct_der(|writer| {
writer.write_sequence(|writer| {
@ -66,9 +73,9 @@ pub mod pkcs8 {
})
}
pub fn write_public(
pub(crate) fn write_public(
oids: &[Option<&ObjectIdentifier>],
body_writer: impl FnOnce(DERWriter),
body_writer: impl FnOnce(DERWriter<'_>),
) -> Vec<u8> {
yasna::construct_der(|writer| {
writer.write_sequence(|writer| {