diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1873790..304258e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 14077ad..f3f163d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonwebkey" -version = "0.2.0" +version = "0.3.0" authors = ["Nick Hynes "] 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 diff --git a/README.md b/README.md index 7ab01e5..96f5bfb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/byte_array.rs b/src/byte_array.rs index 8c81573..74d5a9e 100644 --- a/src/byte_array.rs +++ b/src/byte_array.rs @@ -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(pub [u8; N]); +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deref, AsRef)] +#[serde(transparent)] +pub struct ByteArray>( + #[serde(serialize_with = "crate::utils::serde_base64::serialize")] pub GenericArray, +); -impl fmt::Debug for ByteArray { - 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, T: Into>> From for ByteArray { + fn from(arr: T) -> Self { + Self(arr.into()) } } -impl PartialEq for ByteArray { - fn eq(&self, other: &Self) -> bool { - self.0.as_slice() == other.0.as_slice() +impl> Drop for ByteArray { + fn drop(&mut self) { + Zeroize::zeroize(self.0.as_mut_slice()) } } -impl Eq for ByteArray {} +impl> ByteArray { + /// An unwrapping version of `try_from_slice`. + pub fn from_slice(bytes: impl AsRef<[u8]>) -> Self { + Self::try_from_slice(bytes).unwrap() + } -impl ByteArray { pub fn try_from_slice(bytes: impl AsRef<[u8]>) -> Result { - 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 Serialize for ByteArray { - fn serialize(&self, s: S) -> Result { - serialize_base64(self.0.as_slice(), s) - } -} - -impl<'de, const N: usize> Deserialize<'de> for ByteArray { +impl<'de, N: ArrayLength> Deserialize<'de> for ByteArray { fn deserialize>(d: D) -> Result { - 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 { 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::::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 = 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::::deserialize(&mut de).unwrap_err(); } #[test] fn test_serde_base64_deserialize_array_long() { - ByteArray::<6>::deserialize(&mut get_de()).unwrap_err(); + ByteArray::::deserialize(&mut get_de()).unwrap_err(); } #[test] fn test_serde_base64_deserialize_array_short() { - ByteArray::<8>::deserialize(&mut get_de()).unwrap_err(); + ByteArray::::deserialize(&mut get_de()).unwrap_err(); } } diff --git a/src/byte_vec.rs b/src/byte_vec.rs index fa86cd5..2451881 100644 --- a/src/byte_vec.rs +++ b/src/byte_vec.rs @@ -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` 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); +#[serde(transparent)] +pub struct ByteVec(#[serde(with = "crate::utils::serde_base64")] pub Vec); 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(&self, s: S) -> Result { - serialize_base64(&self.0, s) - } -} - -impl<'de> Deserialize<'de> for ByteVec { - fn deserialize>(d: D) -> Result { - Ok(Self(deserialize_base64(d)?)) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index e1dbf29..df96dc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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> { + pub fn to_public(&self) -> Option> { 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 = [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>, + d: Option>, /// The curve point x coordinate. - x: ByteArray<32>, + x: ByteArray, /// The curve point y coordinate. - y: ByteArray<32>, + y: ByteArray, }, } diff --git a/src/tests.rs b/src/tests.rs index acf850e..ca2a2cf 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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(), diff --git a/src/utils.rs b/src/utils.rs index 440f5d4..2a75c17 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,29 +16,36 @@ fn base64_decode(b64: impl AsRef<[u8]>) -> Result, base64::DecodeError> base64::decode_config(b64, base64_config()) } -pub fn serialize_base64(bytes: impl AsRef<[u8]>, s: S) -> Result { - base64_encode(bytes).serialize(s) -} +pub(crate) mod serde_base64 { + use super::*; -pub fn deserialize_base64<'de, D: Deserializer<'de>>(d: D) -> Result, 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( + bytes: impl AsRef<[u8]>, + s: S, + ) -> Result { + base64_encode(bytes).serialize(s) + } + + pub(crate) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, 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 { 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 { yasna::construct_der(|writer| { writer.write_sequence(|writer| {