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

Refactor PKCS#8 conversion

This commit is contained in:
Nick Hynes 2020-07-13 20:46:42 +00:00
parent 8b1d543598
commit 8aaf3d71c7
No known key found for this signature in database
GPG Key ID: 5B3463E9F1D73C83
5 changed files with 236 additions and 127 deletions

View File

@ -12,13 +12,17 @@ edition = "2018"
base64 = "0.12" base64 = "0.12"
bitflags = "1.2" bitflags = "1.2"
derive_more = "0.99" derive_more = "0.99"
num-bigint = { version = "0.2", optional = true }
paste = "0.1" paste = "0.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
syn = { version = "1.0", features = ["full"] } # required to parse const generics syn = { version = "1.0", features = ["full"] } # required to parse const generics
thiserror = "1.0" 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"] } zeroize = { version = "1.1", features = ["zeroize_derive"] }
[features] [features]
conversion = ["yasna"] convert = ["num-bigint", "yasna"]
[dev-dependencies]
jsonwebtoken = "7.2"

View File

@ -35,3 +35,8 @@ fn main() {
let token = jwt::encode(&jwt::Header::default(), &() /* claims */, encoding_key).unwrap(); 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.

View File

@ -8,6 +8,8 @@ mod key_ops;
mod tests; mod tests;
mod utils; mod utils;
use std::array::FixedSizeArray;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use zeroize::Zeroize; use zeroize::Zeroize;
@ -55,7 +57,7 @@ impl std::str::FromStr for JsonWebKey {
( (
ES256, ES256,
EC { EC {
params: Curve::P256 { .. }, curve: Curve::P256 { .. },
}, },
) )
| (RS256, RSA { .. }) | (RS256, RSA { .. })
@ -78,16 +80,20 @@ impl std::fmt::Display for JsonWebKey {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kty")] #[serde(tag = "kty")]
pub enum Key { pub enum Key {
/// An elliptic curve, as per [RFC 7518 §6.2](https://tools.ietf.org/html/rfc7518#section-6.2).
EC { EC {
#[serde(flatten)] #[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 { RSA {
#[serde(flatten)] #[serde(flatten)]
public: RsaPublic, public: RsaPublic,
#[serde(flatten, default, skip_serializing_if = "Option::is_none")] #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
private: Option<RsaPrivate>, private: Option<RsaPrivate>,
}, },
/// A symmetric key, as per [RFC 7518 §6.4](https://tools.ietf.org/html/rfc7518#section-6.4).
#[serde(rename = "oct")] #[serde(rename = "oct")]
Symmetric { Symmetric {
#[serde(rename = "k")] #[serde(rename = "k")]
@ -102,7 +108,7 @@ impl Key {
match self { match self {
Self::Symmetric { .. } Self::Symmetric { .. }
| Self::EC { | Self::EC {
params: Curve::P256 { d: Some(_), .. }, curve: Curve::P256 { d: Some(_), .. },
.. ..
} }
| Self::RSA { | Self::RSA {
@ -125,9 +131,9 @@ impl Key {
Some(match self { Some(match self {
Self::Symmetric { .. } => return None, Self::Symmetric { .. } => return None,
Self::EC { Self::EC {
params: Curve::P256 { x, y, .. }, curve: Curve::P256 { x, y, .. },
} => Self::EC { } => Self::EC {
params: Curve::P256 { curve: Curve::P256 {
x: x.clone(), x: x.clone(),
y: y.clone(), y: y.clone(),
d: None, d: None,
@ -141,22 +147,25 @@ impl Key {
} }
/// If this key is asymmetric, encodes it as PKCS#8. /// If this key is asymmetric, encodes it as PKCS#8.
#[cfg(feature = "conversion")] #[cfg(feature = "convert")]
pub fn to_der(&self) -> Option<Vec<u8>> { pub fn to_der(&self) -> Result<Vec<u8>, PkcsConvertError> {
use num_bigint::BigUint;
use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag}; use yasna::{models::ObjectIdentifier, DERWriter, DERWriterSeq, Tag};
use crate::utils::pkcs8;
if let Self::Symmetric { .. } = self { if let Self::Symmetric { .. } = self {
return None; return Err(PkcsConvertError::NotAsymmetric);
} }
Some(yasna::construct_der(|writer| match self {
Ok(match self {
Self::EC { Self::EC {
params: Curve::P256 { d, x, y }, curve: Curve::P256 { d, x, y },
} => { } => {
let write_curve_oid = |writer: DERWriter| { let ec_public_oid = ObjectIdentifier::from_slice(&[1, 2, 840, 10045, 2, 1]);
writer.write_oid(&ObjectIdentifier::from_slice(&[ let prime256v1_oid = ObjectIdentifier::from_slice(&[1, 2, 840, 10045, 3, 1, 7]);
1, 2, 840, 10045, 3, 1, 7, // prime256v1 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 */] let public_bytes: Vec<u8> = [0x04 /* uncompressed */]
.iter() .iter()
@ -166,93 +175,77 @@ impl Key {
.collect(); .collect();
writer.write_bitvec_bytes(&public_bytes, 8 * (32 * 2 + 1)); writer.write_bitvec_bytes(&public_bytes, 8 * (32 * 2 + 1));
}; };
writer.write_sequence(|writer| {
match d { match d {
Some(private_point) => { Some(private_point) => {
pkcs8::write_private(oids, |writer: &mut DERWriterSeq| {
writer.next().write_i8(1); // version 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| { 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);
}); });
writer.next().write_tagged(Tag::context(1), write_public);
})
} }
None => { None => pkcs8::write_public(oids, write_public),
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());
} }
};
});
} }
Self::RSA { public, private } => { Self::RSA { public, private } => {
let write_alg_id = |writer: &mut DERWriterSeq| { let rsa_encryption_oid = ObjectIdentifier::from_slice(&[
writer.next().write_oid(&ObjectIdentifier::from_slice(&[
1, 2, 840, 113549, 1, 1, 1, // rsaEncryption 1, 2, 840, 113549, 1, 1, 1, // rsaEncryption
])); ]);
writer.next().write_null(); // parameters 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| { 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.next().write_u32(PUBLIC_EXPONENT);
}; };
writer.write_sequence(|writer| {
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());)+
}};
}
write_opt_bytevecs!(p, q, dp, dq, qi);
};
match private { match private {
Some(private) => { Some(
writer.next().write_i8(0); // version private
writer.next().write_sequence(|writer| { @
write_alg_id(writer); RsaPrivate {
}); d: _,
writer p: Some(_),
.next() q: Some(_),
.write_tagged(yasna::tags::TAG_OCTETSTRING, |writer| { dp: Some(_),
writer.write_sequence(|writer| { dq: Some(_),
writer.next().write_i8(0); // version qi: Some(_),
write_public(writer); },
writer.next().write_bytes(&private.d); ) => pkcs8::write_private(oids, |writer| write_private(writer, private)),
if let Some(p) = &private.p { Some(_) => return Err(PkcsConvertError::MissingRsaParams),
writer.next().write_bytes(p); 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);
}),
} }
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);
}
});
});
}
None => {
write_alg_id(writer);
writer
.next()
.write_tagged(yasna::tags::TAG_BITSTRING, |writer| {
writer.write_sequence(|writer| {
write_public(writer);
})
});
}
}
});
} }
Self::Symmetric { .. } => unreachable!("checked above"), Self::Symmetric { .. } => unreachable!("checked above"),
})) })
} }
/// If this key is asymmetric, encodes it as PKCS#8 with PEM armoring. /// If this key is asymmetric, encodes it as PKCS#8 with PEM armoring.
#[cfg(feature = "conversion")] #[cfg(feature = "convert")]
pub fn to_pem(&self) -> Option<String> { pub fn to_pem(&self) -> Result<String, PkcsConvertError> {
use std::fmt::Write; use std::fmt::Write;
let der_b64 = base64::encode(self.to_der()?); let der_b64 = base64::encode(self.to_der()?);
let key_ty = if self.is_private() { let key_ty = if self.is_private() {
@ -272,7 +265,7 @@ impl Key {
.unwrap(); .unwrap();
} }
writeln!(&mut pem, "-----END {} KEY-----", key_ty).unwrap(); writeln!(&mut pem, "-----END {} KEY-----", key_ty).unwrap();
Some(pem) Ok(pem)
} }
} }
@ -360,8 +353,7 @@ pub enum JsonWebAlgorithm {
ES256, ES256,
} }
#[derive(thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Serde(#[from] serde_json::Error), Serde(#[from] serde_json::Error),
@ -372,3 +364,12 @@ pub enum Error {
#[error("mismatched algorithm for key type")] #[error("mismatched algorithm for key type")]
MismatchedAlgorithm, 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,
}

View File

@ -2,10 +2,8 @@ use super::*;
use std::str::FromStr; use std::str::FromStr;
#[test] // Generated using https://mkjwk.org
fn deserialize_es256() { static P256_JWK_FIXTURE: &str = r#"{
// Generated using https://mkjwk.org
let jwk_str = r#"{
"kty": "EC", "kty": "EC",
"d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg", "d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg",
"use": "enc", "use": "enc",
@ -15,13 +13,29 @@ fn deserialize_es256() {
"y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k", "y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k",
"alg": "ES256" "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!( assert_eq!(
jwk, jwk,
JsonWebKey { JsonWebKey {
key: box Key::EC { key: box Key::EC {
// The parameters were decoded using a 10-liner Rust script. // The parameters were decoded using a 10-liner Rust script.
params: Curve::P256 { curve: Curve::P256 {
d: Some( d: Some(
[ [
102, 130, 144, 246, 62, 29, 132, 128, 101, 49, 21, 107, 191, 228, 6, 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() { fn serialize_es256() {
let jwk = JsonWebKey { let jwk = JsonWebKey {
key: box Key::EC { key: box Key::EC {
params: Curve::P256 { curve: Curve::P256 {
d: None, d: None,
x: [1u8; 32].into(), x: [1u8; 32].into(),
y: [2u8; 32].into(), y: [2u8; 32].into(),
@ -114,19 +128,7 @@ fn serialize_hs256() {
#[test] #[test]
fn deserialize_rs256() { fn deserialize_rs256() {
let jwk_str = r#"{ let jwk = JsonWebKey::from_str(RSA_JWK_FIXTURE).unwrap();
"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();
assert_eq!( assert_eq!(
jwk, jwk,
JsonWebKey { JsonWebKey {
@ -262,24 +264,71 @@ fn mismatched_algorithm() {
); );
} }
#[cfg(feature = "conversion")] #[cfg(feature = "convert")]
#[test] #[test]
fn es256_to_pem() { fn p256_private_to_pem() {
let jwk_str = r#"{ // generated using mkjwk, converted using node-jwk-to-pem, verified using openssl
"kty": "EC", let jwk = JsonWebKey::from_str(P256_JWK_FIXTURE).unwrap();
"d": "ZoKQ9j4dhIBlMRVrv-QG8P_T9sutv3_95eio9MtpgKg",
"crv": "P-256",
"x": "QOMHmv96tVlJv-uNqprnDSKIj5AiLTXKRomXYnav0N0",
"y": "TjYZoHnctatEE6NCrKmXQdJJPnNzZEX8nBmZde3AY4k"
}"#;
let jwk = JsonWebKey::from_str(jwk_str).unwrap();
#[rustfmt::skip] #[rustfmt::skip]
assert_eq!( assert_eq!(
base64::encode(jwk.key.to_pem().unwrap()), jwk.key.to_pem().unwrap(),
"-----BEGIN PRIVATE KEY----- "-----BEGIN PRIVATE KEY-----
MHcCAQEEIGaCkPY+HYSAZTEVa7/kBvD/0/bLrb9//eXoqPTLaYCooAoGCCqGSM49 MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgZoKQ9j4dhIBlMRVr
AwEHoUQDQgAEQOMHmv96tVlJv+uNqprnDSKIj5AiLTXKRomXYnav0N1ONhmgedy1 v+QG8P/T9sutv3/95eio9MtpgKigCgYIKoZIzj0DAQehRANCAARA4wea/3q1WUm/
q0QTo0KsqZdB0kk+c3NkRfycGZl17cBjiQ== 642qmucNIoiPkCItNcpGiZdidq/Q3U42GaB53LWrRBOjQqypl0HSST5zc2RF/JwZ
-----END PRIVATE KEY-----" 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-----
"
); );
} }

View File

@ -30,3 +30,53 @@ pub fn deserialize_base64<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D:
de::Error::custom(err_msg.strip_suffix(".").unwrap_or(&err_msg)) 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<u8> {
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<u8> {
yasna::construct_der(|writer| {
writer.write_sequence(|writer| {
writer
.next()
.write_sequence(|writer| write_oids(writer, oids));
body_writer(writer.next());
})
})
}
}