diff --git a/Cargo.toml b/Cargo.toml index cf20f3d..5ae407a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,13 +12,17 @@ edition = "2018" base64 = "0.12" bitflags = "1.2" derive_more = "0.99" +num-bigint = { version = "0.2", optional = true } paste = "0.1" 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 } +yasna = { version = "0.3", optional = true, features = ["num-bigint"] } zeroize = { version = "1.1", features = ["zeroize_derive"] } [features] -conversion = ["yasna"] +convert = ["num-bigint", "yasna"] + +[dev-dependencies] +jsonwebtoken = "7.2" diff --git a/README.md b/README.md index 22e0e80..1106d1b 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,8 @@ fn main() { let token = jwt::encode(&jwt::Header::default(), &() /* claims */, encoding_key).unwrap(); } ``` + +## Features + +* `convert` - enables `Key::{to_der, to_pem}`. + This pulls in the [yasna](https://crates.io/crates/yasna) crate. diff --git a/src/lib.rs b/src/lib.rs index 5e5ad4a..7ab5695 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ mod key_ops; mod tests; mod utils; +use std::array::FixedSizeArray; + use serde::{Deserialize, Serialize}; use zeroize::Zeroize; @@ -55,7 +57,7 @@ impl std::str::FromStr for JsonWebKey { ( ES256, EC { - params: Curve::P256 { .. }, + curve: Curve::P256 { .. }, }, ) | (RS256, RSA { .. }) @@ -78,16 +80,20 @@ impl std::fmt::Display for JsonWebKey { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kty")] pub enum Key { + /// An elliptic curve, as per [RFC 7518 §6.2](https://tools.ietf.org/html/rfc7518#section-6.2). EC { #[serde(flatten)] - params: Curve, + curve: Curve, }, + /// An elliptic curve, as per [RFC 7518 §6.3](https://tools.ietf.org/html/rfc7518#section-6.3). + /// See also: [RFC 3447](https://tools.ietf.org/html/rfc3447). RSA { #[serde(flatten)] public: RsaPublic, #[serde(flatten, default, skip_serializing_if = "Option::is_none")] private: Option, }, + /// A symmetric key, as per [RFC 7518 §6.4](https://tools.ietf.org/html/rfc7518#section-6.4). #[serde(rename = "oct")] Symmetric { #[serde(rename = "k")] @@ -102,7 +108,7 @@ impl Key { match self { Self::Symmetric { .. } | Self::EC { - params: Curve::P256 { d: Some(_), .. }, + curve: Curve::P256 { d: Some(_), .. }, .. } | Self::RSA { @@ -125,9 +131,9 @@ impl Key { Some(match self { Self::Symmetric { .. } => return None, Self::EC { - params: Curve::P256 { x, y, .. }, + curve: Curve::P256 { x, y, .. }, } => Self::EC { - params: Curve::P256 { + curve: Curve::P256 { x: x.clone(), y: y.clone(), d: None, @@ -141,22 +147,25 @@ impl Key { } /// If this key is asymmetric, encodes it as PKCS#8. - #[cfg(feature = "conversion")] - pub fn to_der(&self) -> Option> { + #[cfg(feature = "convert")] + pub fn to_der(&self) -> Result, PkcsConvertError> { + use num_bigint::BigUint; use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag}; + use crate::utils::pkcs8; + if let Self::Symmetric { .. } = self { - return None; + return Err(PkcsConvertError::NotAsymmetric); } - Some(yasna::construct_der(|writer| match self { + + Ok(match self { Self::EC { - params: Curve::P256 { d, x, y }, + curve: 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 ec_public_oid = ObjectIdentifier::from_slice(&[1, 2, 840, 10045, 2, 1]); + 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 public_bytes: Vec = [0x04 /* uncompressed */] .iter() @@ -166,93 +175,77 @@ impl Key { .collect(); writer.write_bitvec_bytes(&public_bytes, 8 * (32 * 2 + 1)); }; - writer.write_sequence(|writer| { - match d { - Some(private_point) => { + + match d { + Some(private_point) => { + pkcs8::write_private(oids, |writer: &mut DERWriterSeq| { writer.next().write_i8(1); // version - writer.next().write_bytes(private_point.as_ref()); + writer.next().write_bytes(private_point.as_slice()); writer.next().write_tagged(Tag::context(0), |writer| { - write_curve_oid(writer); + writer.write_oid(&prime256v1_oid) }); - 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()); - } - }; - }); + writer.next().write_tagged(Tag::context(1), write_public); + }) + } + None => pkcs8::write_public(oids, write_public), + } } 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 rsa_encryption_oid = ObjectIdentifier::from_slice(&[ + 1, 2, 840, 113549, 1, 1, 1, // rsaEncryption + ]); + let oids = &[Some(&rsa_encryption_oid), None]; + 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| { - writer.next().write_bytes(&*public.n); + write_bytevec(writer.next(), &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); - } - }); - }); + + 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); + write_bytevec(writer.next(), &private.d); + macro_rules! write_opt_bytevecs { + ($($param:ident),+) => {{ + $(write_bytevec(writer.next(), private.$param.as_ref().unwrap());)+ + }}; } - None => { - write_alg_id(writer); - writer - .next() - .write_tagged(yasna::tags::TAG_BITSTRING, |writer| { - writer.write_sequence(|writer| { - write_public(writer); - }) - }); - } - } - }); + write_opt_bytevecs!(p, q, dp, dq, qi); + }; + + match private { + Some( + private + @ + RsaPrivate { + d: _, + p: Some(_), + q: Some(_), + dp: Some(_), + dq: Some(_), + qi: Some(_), + }, + ) => pkcs8::write_private(oids, |writer| write_private(writer, private)), + Some(_) => return Err(PkcsConvertError::MissingRsaParams), + None => pkcs8::write_public(oids, |writer| { + let body = + yasna::construct_der(|writer| writer.write_sequence(write_public)); + writer.write_bitvec_bytes(&body, body.len() * 8); + }), + } } 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 { + #[cfg(feature = "convert")] + pub fn to_pem(&self) -> Result { use std::fmt::Write; let der_b64 = base64::encode(self.to_der()?); let key_ty = if self.is_private() { @@ -272,7 +265,7 @@ impl Key { .unwrap(); } writeln!(&mut pem, "-----END {} KEY-----", key_ty).unwrap(); - Some(pem) + Ok(pem) } } @@ -360,8 +353,7 @@ pub enum JsonWebAlgorithm { ES256, } -#[derive(thiserror::Error)] -#[cfg_attr(debug_assertions, derive(Debug))] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Serde(#[from] serde_json::Error), @@ -372,3 +364,12 @@ pub enum Error { #[error("mismatched algorithm for key type")] MismatchedAlgorithm, } + +#[derive(Debug, thiserror::Error)] +pub enum PkcsConvertError { + #[error("encoding RSA JWK as PKCS#8 requires specifing all of p, q, dp, dq, qi")] + MissingRsaParams, + + #[error("a symmetric key can not be encoded using PKCS#8")] + NotAsymmetric, +} diff --git a/src/tests.rs b/src/tests.rs index 3a36800..dcc8e71 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,10 +2,8 @@ use super::*; use std::str::FromStr; -#[test] -fn deserialize_es256() { - // Generated using https://mkjwk.org - let jwk_str = r#"{ +// Generated using https://mkjwk.org +static P256_JWK_FIXTURE: &str = r#"{ "kty": "EC", "d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", "use": "enc", @@ -15,13 +13,29 @@ fn deserialize_es256() { "y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k", "alg": "ES256" }"#; - let jwk = JsonWebKey::from_str(jwk_str).unwrap(); + +static RSA_JWK_FIXTURE: &str = r#"{ + "p": "6AQ4yHef17an_i5LQPHNIxzpH65xWOSf_qCB7q-lXyM", + "kty": "RSA", + "q": "tSVfpefCsf1iWmAs1zYvxdEsUiv0VMEuQBtbTijj_OE", + "d": "Qdp8a8Df5TlMaaloXApNF_3eu8sLHNWbXdg70e5YVTAs0WUfaIf5c3n96RrDDAzmMEwgKnJ7A1NJ9Nlzz4Z0AQ", + "e": "AQAB", + "use": "enc", + "qi": "adhQHH8IGXFfLEMnZ5t_TeCp5zgSwQktJ2lmylxUG0M", + "dp": "qVnLiKeoSG_Olz17OGBGd4a2sqVFnrjh_51wuaQDdTk", + "dq": "GL_Ec6xYg2z1FRfyyGyU1lgf0BJFTZcfNI8ISIN5ssE", + "n": "pCzbcd9kjvg5rfGHdEMWnXo49zbB6FLQ-m0B0BvVp0aojVWYa0xujC-ZP7ZhxByPxyc2PazwFJJi9ivZ_ggRww" + }"#; + +#[test] +fn deserialize_es256() { + let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); assert_eq!( jwk, JsonWebKey { key: box Key::EC { // The parameters were decoded using a 10-liner Rust script. - params: Curve::P256 { + curve: Curve::P256 { d: Some( [ 102, 130, 144, 246, 62, 29, 132, 128, 101, 49, 21, 107, 191, 228, 6, @@ -54,7 +68,7 @@ fn deserialize_es256() { fn serialize_es256() { let jwk = JsonWebKey { key: box Key::EC { - params: Curve::P256 { + curve: Curve::P256 { d: None, x: [1u8; 32].into(), y: [2u8; 32].into(), @@ -114,19 +128,7 @@ fn serialize_hs256() { #[test] fn deserialize_rs256() { - let jwk_str = r#"{ - "p": "_LSip5o4eaGf25uvwyUq9ubRtKemrCaoCxumoj63Au0", - "kty": "RSA", - "q": "l20iLpicEW3uja0Zg2xP6DjZa86bD4IQ3wFXCcKCf1c", - "d": "Xo0VAHtfV38HwJbAI6X-Fu7vuyoQjnuiSlQhcSjxn0BZfLP_DKxdJ2ANgTGVE0x243YHqhWRHLobbmDcnUuMOQ", - "e": "AQAB", - "qi": "2mzAaSr7I1D3vDtOhbWKS9-9ELRHKbAHz4dhn4DSCBo", - "dp": "-kyswxeVEpyM6wdU2xRobu-HDMn145PSZFY6AX_e460", - "alg": "RS256", - "dq": "OqMWE3khJlatg8s-D_hHUSOCfg65WN4C7ng0XiEmK20", - "n": "lXpGmBoIxj56TpptApaac6V19_7WWbq0a14a5UHBBlkc54NwIUa2X4p9OeK2sy6rLQ_1g1AcSwfsVUy8MP-Riw" - }"#; - let jwk = JsonWebKey::from_str(jwk_str).unwrap(); + let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); assert_eq!( jwk, JsonWebKey { @@ -262,24 +264,71 @@ fn mismatched_algorithm() { ); } -#[cfg(feature = "conversion")] +#[cfg(feature = "convert")] #[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(); +fn p256_private_to_pem() { + // generated using mkjwk, converted using node-jwk-to-pem, verified using openssl + let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); #[rustfmt::skip] assert_eq!( - base64::encode(jwk.key.to_pem().unwrap()), + jwk.key.to_pem().unwrap(), "-----BEGIN PRIVATE KEY----- -MHcCAQEEIGaCkPY+HYSAZTEVa7/kBvD/0/bLrb9//eXoqPTLaYCooAoGCCqGSM49 -AwEHoUQDQgAEQOMHmv96tVlJv+uNqprnDSKIj5AiLTXKRomXYnav0N1ONhmgedy1 -q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== ------END PRIVATE KEY-----" +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgZoKQ9j4dhIBlMRVr +v+QG8P/T9sutv3/95eio9MtpgKigCgYIKoZIzj0DAQehRANCAARA4wea/3q1WUm/ +642qmucNIoiPkCItNcpGiZdidq/Q3U42GaB53LWrRBOjQqypl0HSST5zc2RF/JwZ +mXXtwGOJ +-----END PRIVATE KEY----- +" + ); +} + +#[cfg(feature = "convert")] +#[test] +fn p256_public_to_pem() { + let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap(); + #[rustfmt::skip] + assert_eq!( + jwk.key.to_public().unwrap().to_pem().unwrap(), +"-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQOMHmv96tVlJv+uNqprnDSKIj5Ai +LTXKRomXYnav0N1ONhmgedy1q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== +-----END PUBLIC KEY----- +" + ); +} + +#[cfg(feature = "convert")] +#[test] +fn rsa_private_to_pem() { + let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); + #[rustfmt::skip] + assert_eq!( + jwk.key.to_pem().unwrap(), +"-----BEGIN PRIVATE KEY----- +MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEApCzbcd9kjvg5rfGH +dEMWnXo49zbB6FLQ+m0B0BvVp0aojVWYa0xujC+ZP7ZhxByPxyc2PazwFJJi9ivZ +/ggRwwIDAQABAkBB2nxrwN/lOUxpqWhcCk0X/d67ywsc1Ztd2DvR7lhVMCzRZR9o +h/lzef3pGsMMDOYwTCAqcnsDU0n02XPPhnQBAiEA6AQ4yHef17an/i5LQPHNIxzp +H65xWOSf/qCB7q+lXyMCIQC1JV+l58Kx/WJaYCzXNi/F0SxSK/RUwS5AG1tOKOP8 +4QIhAKlZy4inqEhvzpc9ezhgRneGtrKlRZ644f+dcLmkA3U5AiAYv8RzrFiDbPUV +F/LIbJTWWB/QEkVNlx80jwhIg3mywQIgadhQHH8IGXFfLEMnZ5t/TeCp5zgSwQkt +J2lmylxUG0M= +-----END PRIVATE KEY----- +" + ); +} + +#[cfg(feature = "convert")] +#[test] +fn rsa_public_to_pem() { + let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap(); + #[rustfmt::skip] + assert_eq!( + jwk.key.to_public().unwrap().to_pem().unwrap(), +"-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKQs23HfZI74Oa3xh3RDFp16OPc2wehS +0PptAdAb1adGqI1VmGtMbowvmT+2YcQcj8cnNj2s8BSSYvYr2f4IEcMCAwEAAQ== +-----END PUBLIC KEY----- +" ); } diff --git a/src/utils.rs b/src/utils.rs index aaa5797..d03b778 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,3 +30,53 @@ pub fn deserialize_base64<'de, D: Deserializer<'de>>(d: D) -> Result, D: de::Error::custom(err_msg.strip_suffix(".").unwrap_or(&err_msg)) }) } + +#[cfg(feature = "convert")] +pub mod pkcs8 { + use yasna::{ + models::{ObjectIdentifier, TaggedDerValue}, + DERWriter, DERWriterSeq, + }; + + fn write_oids(writer: &mut DERWriterSeq, oids: &[Option<&ObjectIdentifier>]) { + for oid in oids { + match oid { + Some(oid) => writer.next().write_oid(oid), + None => writer.next().write_null(), + } + } + } + + pub fn write_private( + oids: &[Option<&ObjectIdentifier>], + body_writer: impl FnOnce(&mut DERWriterSeq), + ) -> Vec { + yasna::construct_der(|writer| { + writer.write_sequence(|writer| { + writer.next().write_i8(0); // version + writer + .next() + .write_sequence(|writer| write_oids(writer, oids)); + + let body = yasna::construct_der(|writer| writer.write_sequence(body_writer)); + writer + .next() + .write_tagged_der(&TaggedDerValue::from_octetstring(body)); + }) + }) + } + + pub fn write_public( + oids: &[Option<&ObjectIdentifier>], + body_writer: impl FnOnce(DERWriter), + ) -> Vec { + yasna::construct_der(|writer| { + writer.write_sequence(|writer| { + writer + .next() + .write_sequence(|writer| write_oids(writer, oids)); + body_writer(writer.next()); + }) + }) + } +}