From 361959cce4c0ce52741fa7af54c3f80593f10e4d Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 30 Nov 2023 18:36:00 +0000 Subject: [PATCH 01/10] Tweak syntax highlighting in Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b42f0e4..066b4b6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ecosystem. This is an example [JWT][], taken from the ACME standard ([RFC 8555][RFC8555]): -```json +```javascript { "protected": base64url({ "alg": "ES256", From 6748486239c775ca03263d50c40d53b57eaf9fe2 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 30 Nov 2023 18:46:57 +0000 Subject: [PATCH 02/10] Add some more RSA tests --- src/algorithms/rsa.rs | 79 ++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/src/algorithms/rsa.rs b/src/algorithms/rsa.rs index 582afa5..73d5bf5 100644 --- a/src/algorithms/rsa.rs +++ b/src/algorithms/rsa.rs @@ -301,6 +301,7 @@ mod test { use base64ct::Encoding as _; use serde_json::json; use sha2::Sha256; + use signature::Keypair; use signature::SignatureEncoding; fn strip_whitespace(s: &str) -> String { @@ -311,39 +312,43 @@ mod test { rsa::RsaPrivateKey::from_value(jwk).unwrap() } - #[test] - fn rfc7515_example_a2_signature() { - let pkey = rsa(json!( {"kty":"RSA", + fn jwk() -> serde_json::Value { + json!( {"kty":"RSA", "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx - HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs - D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH - SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV - MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8 - NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", + HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs + D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH + SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV + MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8 + NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", "e":"AQAB", "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I - jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0 - BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn - 439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT - CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh - BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", + jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0 + BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn + 439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT + CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh + BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", "p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi - YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG - BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", + YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG + BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", "q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa - ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA - -njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", + ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA + -njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", "dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q - CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb - 34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", + CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb + 34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", "dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa - 7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky - NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", + 7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky + NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", "qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o - y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU - W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U" + y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU + W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U" } - )); + ) + } + + #[test] + fn rfc7515_example_a2_signature() { + let pkey = rsa(jwk()); let payload = strip_whitespace( "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt @@ -371,4 +376,30 @@ mod test { ) ); } + + #[test] + fn rsa_pkcs1v15_algorithm() { + let pkey = rsa(jwk()); + + let payload = json! { + { + "iss": "joe", + "exp": 1300819380, + "http://example.com/is_root": true + } + }; + + let token = crate::Token::compact((), payload); + let algorithm: rsa::pkcs1v15::SigningKey = rsa::pkcs1v15::SigningKey::new(pkey); + + let signed = token + .sign::<_, rsa::pkcs1v15::Signature>(&algorithm) + .unwrap(); + + let unverified = signed.unverify(); + let verifying_key = algorithm.verifying_key(); + unverified + .verify::<_, rsa::pkcs1v15::Signature>(&verifying_key) + .unwrap(); + } } From 366143fb4a507af047b4f7ab8e6b12119da9d2bf Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 30 Nov 2023 19:00:41 +0000 Subject: [PATCH 03/10] Add a default generic parameter for the signature type --- src/algorithms/mod.rs | 3 ++- src/token/state.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index 9a76f53..c099644 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -159,7 +159,7 @@ pub trait JsonWebAlgorithm { const IDENTIFIER: AlgorithmIdentifier; } -/// A trait to associate an alogritm identifier with an algorithm. +/// An object-safe trait to associate an alogritm identifier with an algorithm. /// /// This is a dynamic version of [`JsonWebAlgorithm`], which allows for /// dynamic dispatch of the algorithm, and object-safety for the trait. @@ -434,4 +434,5 @@ mod test { // a concrete `Signature` type, or an object-safe trait. sa::assert_obj_safe!(TokenSigner); sa::assert_obj_safe!(TokenVerifier); + sa::assert_obj_safe!(DynJsonWebAlgorithm); } diff --git a/src/token/state.rs b/src/token/state.rs index 18dfccb..1463caa 100644 --- a/src/token/state.rs +++ b/src/token/state.rs @@ -91,7 +91,7 @@ impl MaybeSigned for Unsigned { /// signature is both consistent and valid. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(bound(serialize = "H: Serialize, Sig: Serialize",))] -pub struct Signed +pub struct Signed where Alg: DynJsonWebAlgorithm + ?Sized, { @@ -146,7 +146,7 @@ where /// consistent with the signature. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(bound(serialize = "H: Serialize, Sig: Serialize",))] -pub struct Verified +pub struct Verified where Alg: DynJsonWebAlgorithm + ?Sized, { From c37f54940ac7ca3edf7a49cd3e881f74cad6fb67 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 30 Nov 2023 19:08:57 +0000 Subject: [PATCH 04/10] Add default to SignatureBytes for signature types --- src/algorithms/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index c099644..a3bfaac 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -189,7 +189,7 @@ pub trait JsonWebAlgorithmDigest: JsonWebAlgorithm { /// A trait to represent an algorithm which can sign a JWT. /// /// This trait should apply to signing keys. -pub trait TokenSigner: DynJsonWebAlgorithm + SerializePublicJWK +pub trait TokenSigner: DynJsonWebAlgorithm + SerializePublicJWK where S: SignatureEncoding, { @@ -229,7 +229,8 @@ where #[cfg(feature = "rand")] /// A trait to represent an algorithm which can sign a JWT, with a source of /// randomness. -pub trait RandomizedTokenSigner: DynJsonWebAlgorithm + SerializePublicJWK +pub trait RandomizedTokenSigner: + DynJsonWebAlgorithm + SerializePublicJWK where S: SignatureEncoding, { @@ -259,7 +260,7 @@ where /// /// This trait should apply to the equivalent of public keys, which have enough information /// to verify a JWT signature, but not necessarily to sing it. -pub trait TokenVerifier: DynJsonWebAlgorithm +pub trait TokenVerifier: DynJsonWebAlgorithm where S: SignatureEncoding, { From 71e382fd1725fc1b8442dab5af567631e84ca275 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 30 Nov 2023 19:28:52 +0000 Subject: [PATCH 05/10] Add Base64Data type for generically handling base64 data --- src/base64data.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/src/base64data.rs b/src/base64data.rs index 189ac32..13ec936 100644 --- a/src/base64data.rs +++ b/src/base64data.rs @@ -32,6 +32,112 @@ pub enum DecodeError { InvalidData(#[source] Box), } +/// Wrapper type for types which implement AsRef<[u8]> to indicate that +/// they should serialize as bytes with a Base64 URL-safe encoding. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct Base64Data(pub T); + +impl Base64Data +where + T: AsRef<[u8]>, +{ + pub(crate) fn serialized_value(&self) -> Result { + Ok(base64ct::Base64UrlUnpadded::encode_string(self.0.as_ref())) + } +} + +impl std::fmt::Debug for Base64Data +where + T: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Base64Data") + .field(&self.serialized_value().unwrap()) + .finish() + } +} + +impl From for Base64Data { + fn from(value: T) -> Self { + Base64Data(value) + } +} + +impl ser::Serialize for Base64Data +where + T: AsRef<[u8]>, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let target = self + .serialized_value() + .map_err(|err| unreachable!("serialization error: {}", err))?; + serializer.serialize_str(&target) + } +} + +impl AsRef<[u8]> for Base64Data +where + T: AsRef<[u8]>, +{ + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +struct Base64DataVisitor(PhantomData); + +impl<'de, T> de::Visitor<'de> for Base64DataVisitor +where + T: for<'a> TryFrom<&'a [u8]>, +{ + type Value = Base64Data; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("base64url encoded data") + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: de::Error, + { + let data = base64ct::Base64UrlUnpadded::decode_vec(v) + .map_err(|_| E::invalid_value(de::Unexpected::Str(v), &"invalid base64url encoding"))?; + + let realized = T::try_from(data.as_ref()) + .map_err(|_| E::invalid_value(de::Unexpected::Str(v), &"can't parse internal data"))?; + Ok(Base64Data(realized)) + } +} + +impl<'de, T> de::Deserialize<'de> for Base64Data +where + T: for<'a> TryFrom<&'a [u8]>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(Base64DataVisitor(PhantomData)) + } +} + +#[cfg(feature = "fmt")] +impl fmt::JWTFormat for Base64Data +where + T: AsRef<[u8]>, +{ + fn fmt(&self, f: &mut IndentWriter<'_, W>) -> fmt::Result { + write!( + f, + "b64\"{}\"", + base64ct::Base64UrlUnpadded::encode_string(self.0.as_ref()) + ) + } +} + /// Wrapper type to indicate that the inner type should be serialized /// as bytes with a Base64 URL-safe encoding. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -101,9 +207,9 @@ where } } -struct Base64Visitor(PhantomData); +struct Base64SignatureVisitor(PhantomData); -impl<'de, T> de::Visitor<'de> for Base64Visitor +impl<'de, T> de::Visitor<'de> for Base64SignatureVisitor where T: for<'a> TryFrom<&'a [u8]>, { @@ -134,7 +240,7 @@ where where D: serde::Deserializer<'de>, { - deserializer.deserialize_str(Base64Visitor(PhantomData)) + deserializer.deserialize_str(Base64SignatureVisitor(PhantomData)) } } @@ -291,6 +397,15 @@ mod test { #[test] fn test_base64_data() { + let data = Base64Data::from(vec![1, 2, 3, 4]); + let serialized = serde_json::to_string(&data).unwrap(); + assert_eq!(serialized, r#""AQIDBA""#); + let deserialized: Base64Data> = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, data); + } + + #[test] + fn test_base64_signature() { let data = Base64Signature::from(SignatureBytes::from(vec![1, 2, 3, 4])); let serialized = serde_json::to_string(&data).unwrap(); assert_eq!(serialized, r#""AQIDBA""#); From a49b995bea64809390c89bd169280494bd427e64 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Thu, 30 Nov 2023 19:35:21 +0000 Subject: [PATCH 06/10] Support signature bytes for HMAC --- src/algorithms/hmac.rs | 24 ++++++++++++++++++++++-- src/token/mod.rs | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/algorithms/hmac.rs b/src/algorithms/hmac.rs index 9c109c4..2575c99 100644 --- a/src/algorithms/hmac.rs +++ b/src/algorithms/hmac.rs @@ -26,7 +26,10 @@ use digest::{Digest, Mac}; use hmac::SimpleHmac; use signature::{Keypair, SignatureEncoding}; -use crate::key::{JWKeyType, SerializeJWK}; +use crate::{ + key::{JWKeyType, SerializeJWK}, + SignatureBytes, +}; use super::JsonWebAlgorithm; @@ -204,6 +207,23 @@ where } } +impl super::TokenSigner for Hmac +where + Hmac: JsonWebAlgorithm, + D: Digest + digest::core_api::BlockSizeUser + Clone, +{ + fn try_sign_token( + &self, + header: &str, + payload: &str, + ) -> Result { + let signature = >>::try_sign_token( + self, header, payload, + )?; + Ok(signature.to_bytes().as_ref().into()) + } +} + impl super::TokenVerifier> for Hmac where Hmac: JsonWebAlgorithm, @@ -269,7 +289,7 @@ mod test { let algorithm: Hmac = Hmac::new(key); - let signature = algorithm.sign_token(&header, &payload); + let signature: DigestSignature<_> = algorithm.sign_token(&header, &payload); let sig = base64ct::Base64UrlUnpadded::encode_string(signature.to_bytes().as_ref()); diff --git a/src/token/mod.rs b/src/token/mod.rs index d0981d4..48536ed 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -933,7 +933,7 @@ mod test_ecdsa { #[cfg(all(test, feature = "hmac"))] mod test_hmac { - use crate::algorithms::hmac::{Hmac, HmacKey}; + use crate::algorithms::hmac::{DigestSignature, Hmac, HmacKey}; use super::*; @@ -967,7 +967,7 @@ mod test_hmac { let token = Token::compact((), "This is an HMAC'd message"); - let signed = token.sign(&algorithm).unwrap(); + let signed = token.sign::<_, DigestSignature<_>>(&algorithm).unwrap(); let verified = signed.unverify().verify(&algorithm).unwrap(); From 1bbb780799cb8089d765c189ad01c61846c9d3fc Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Fri, 1 Dec 2023 17:17:46 +0000 Subject: [PATCH 07/10] Suport for RSA-PSS signatures --- src/algorithms/rsa.rs | 152 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 137 insertions(+), 15 deletions(-) diff --git a/src/algorithms/rsa.rs b/src/algorithms/rsa.rs index 73d5bf5..03f6897 100644 --- a/src/algorithms/rsa.rs +++ b/src/algorithms/rsa.rs @@ -20,6 +20,8 @@ use base64ct::{Base64UrlUnpadded, Encoding}; use rsa::traits::PrivateKeyParts; use rsa::traits::PublicKeyParts; use rsa::RsaPrivateKey; +#[cfg(feature = "rand")] +use signature::RandomizedDigestSigner; pub use rsa::pkcs1v15; pub use rsa::pss; @@ -276,21 +278,112 @@ where } } -// macro_rules! jose_rsa_pss_algorithm { -// ($alg:ident, $digest:ty) => { -// $crate::jose_algorithm!( -// $alg, -// rsa::pss::SigningKey<$digest>, -// rsa::pss::VerifyingKey<$digest>, -// $digest, -// rsa::pss::Signature -// ); -// }; -// } - -// jose_rsa_pss_algorithm!(PS256, sha2::Sha256); -// jose_rsa_pss_algorithm!(PS384, sha2::Sha384); -// jose_rsa_pss_algorithm!(PS512, sha2::Sha512); +#[cfg(feature = "rand")] +impl crate::key::JWKeyType for rsa::pss::SigningKey +where + D: signature::digest::Digest, +{ + const KEY_TYPE: &'static str = "RSA"; +} + +#[cfg(feature = "rand")] +macro_rules! rsa_pss_algorithm { + ($alg:ident, $digest:ty) => { + impl $crate::algorithms::JsonWebAlgorithm for rsa::pss::SigningKey<$digest> { + const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::$alg; + } + + impl $crate::algorithms::JsonWebAlgorithm for rsa::pss::VerifyingKey<$digest> { + const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::$alg; + } + }; +} + +#[cfg(feature = "rand")] +rsa_pss_algorithm!(PS256, sha2::Sha256); +#[cfg(feature = "rand")] +rsa_pss_algorithm!(PS384, sha2::Sha384); +#[cfg(feature = "rand")] +rsa_pss_algorithm!(PS512, sha2::Sha512); + +#[cfg(feature = "rand")] +impl crate::key::SerializeJWK for rsa::pss::SigningKey +where + D: signature::digest::Digest, +{ + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().to_public_key().parameters() + } +} + +#[cfg(feature = "rand")] +impl crate::key::DeserializeJWK for rsa::pss::SigningKey +where + D: signature::digest::Digest, +{ + fn build( + parameters: std::collections::BTreeMap, + ) -> Result + where + Self: Sized, + { + let key = rsa::RsaPrivateKey::build(parameters)?; + + Ok(rsa::pss::SigningKey::new(key)) + } +} + +#[cfg(feature = "rand")] +impl crate::algorithms::RandomizedTokenSigner for rsa::pss::SigningKey +where + D: signature::digest::Digest, + S: signature::SignatureEncoding, + Self: RandomizedDigestSigner + crate::algorithms::DynJsonWebAlgorithm, +{ + fn try_sign_token( + &self, + header: &str, + payload: &str, + rng: &mut impl rand_core::CryptoRngCore, + ) -> Result { + let mut digest = D::new(); + digest.update(header.as_bytes()); + digest.update(b"."); + digest.update(payload.as_bytes()); + + self.try_sign_digest_with_rng(rng, digest) + } +} + +#[cfg(feature = "rand")] +impl crate::algorithms::TokenVerifier for rsa::pss::VerifyingKey +where + D: signature::digest::Digest, + S: signature::SignatureEncoding, + for<'a> >::Error: std::error::Error + Send + Sync + 'static, + Self: signature::DigestVerifier + crate::algorithms::DynJsonWebAlgorithm, +{ + fn verify_token( + &self, + header: &[u8], + payload: &[u8], + signature: &[u8], + ) -> Result { + use signature::DigestVerifier; + + let mut digest = D::new(); + digest.update(header); + digest.update(b"."); + digest.update(payload); + + let signature = signature + .try_into() + .map_err(signature::Error::from_source)?; + + self.verify_digest(digest, &signature)?; + Ok(signature) + } +} #[cfg(test)] mod test { @@ -402,4 +495,33 @@ mod test { .verify::<_, rsa::pkcs1v15::Signature>(&verifying_key) .unwrap(); } + + #[cfg(feature = "rand")] + #[test] + fn rsa_pss_algorithm() { + use rand_core::OsRng; + + let pkey = rsa(jwk()); + + let payload = json! { + { + "iss": "joe", + "exp": 1300819380, + "http://example.com/is_root": true + } + }; + + let token = crate::Token::compact((), payload); + let algorithm: rsa::pss::SigningKey = rsa::pss::SigningKey::new(pkey); + + let signed = token + .sign_randomized::<_, rsa::pss::Signature>(&algorithm, &mut OsRng) + .unwrap(); + + let unverified = signed.unverify(); + let verifying_key = algorithm.verifying_key(); + unverified + .verify::<_, rsa::pss::Signature>(&verifying_key) + .unwrap(); + } } From 296e5a542c92fdcfce35dc69e15a05e0eadfd573 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Fri, 1 Dec 2023 18:41:00 +0000 Subject: [PATCH 08/10] Test support for RSA signatures --- src/algorithms/mod.rs | 1 + src/algorithms/rsa.rs | 152 +++++++++++++++++++++++++++++++++--------- 2 files changed, 120 insertions(+), 33 deletions(-) diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index a3bfaac..9f3d6c2 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -30,6 +30,7 @@ //! - RS512: RSASSA-PKCS1-v1_5 using SHA-512 via [`rsa::pkcs1v15::SigningKey`][rsa::pkcs1v15::SigningKey] / [`rsa::pkcs1v15::VerifyingKey`][rsa::pkcs1v15::VerifyingKey] //! - PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 via [`rsa::pss::SigningKey`][rsa::pss::SigningKey] / [`rsa::pss::VerifyingKey`][rsa::pss::VerifyingKey] //! - PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 via [`rsa::pss::SigningKey`][rsa::pss::SigningKey] / [`rsa::pss::VerifyingKey`][rsa::pss::VerifyingKey] +//! - PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-384 via [`rsa::pss::SigningKey`][rsa::pss::SigningKey] / [`rsa::pss::VerifyingKey`][rsa::pss::VerifyingKey] //! //! ## ECDSA //! diff --git a/src/algorithms/rsa.rs b/src/algorithms/rsa.rs index 03f6897..7b97a47 100644 --- a/src/algorithms/rsa.rs +++ b/src/algorithms/rsa.rs @@ -27,6 +27,7 @@ pub use rsa::pkcs1v15; pub use rsa::pss; use crate::key::DeserializeJWK; +use crate::SignatureBytes; impl crate::key::JWKeyType for rsa::RsaPublicKey { const KEY_TYPE: &'static str = "RSA"; @@ -296,6 +297,46 @@ macro_rules! rsa_pss_algorithm { impl $crate::algorithms::JsonWebAlgorithm for rsa::pss::VerifyingKey<$digest> { const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::$alg; } + + impl signature::RandomizedDigestSigner<$digest, SignatureBytes> + for rsa::pss::SigningKey<$digest> + where + Self: RandomizedDigestSigner<$digest, rsa::pss::Signature> + + crate::algorithms::DynJsonWebAlgorithm, + { + fn try_sign_digest_with_rng( + &self, + rng: &mut impl rand_core::CryptoRngCore, + digest: $digest, + ) -> Result { + use signature::SignatureEncoding; + + let signature: rsa::pss::Signature = self.try_sign_digest_with_rng(rng, digest)?; + Ok(signature.to_bytes().as_ref().into()) + } + } + + impl signature::DigestVerifier<$digest, SignatureBytes> for rsa::pss::VerifyingKey<$digest> + where + Self: signature::DigestVerifier<$digest, rsa::pss::Signature> + + crate::algorithms::DynJsonWebAlgorithm, + { + fn verify_digest( + &self, + digest: $digest, + signature: &SignatureBytes, + ) -> Result<(), signature::Error> { + use signature::SignatureEncoding; + + let signature: rsa::pss::Signature = signature + .to_bytes() + .as_ref() + .try_into() + .map_err(signature::Error::from_source)?; + + self.verify_digest(digest, &signature) + } + } }; } @@ -388,8 +429,9 @@ where #[cfg(test)] mod test { - use crate::algorithms::TokenSigner as _; use crate::key::DeserializeJWK; + use crate::SignatureBytes; + use crate::TokenSigner; use base64ct::Encoding as _; use serde_json::json; @@ -470,10 +512,12 @@ mod test { ); } - #[test] - fn rsa_pkcs1v15_algorithm() { - let pkey = rsa(jwk()); - + fn algorithm_roundtrip( + sign: &impl crate::algorithms::TokenSigner, + verify: &impl crate::algorithms::TokenVerifier, + ) where + S: SignatureEncoding, + { let payload = json! { { "iss": "joe", @@ -483,45 +527,87 @@ mod test { }; let token = crate::Token::compact((), payload); - let algorithm: rsa::pkcs1v15::SigningKey = rsa::pkcs1v15::SigningKey::new(pkey); - let signed = token - .sign::<_, rsa::pkcs1v15::Signature>(&algorithm) - .unwrap(); + let signed = token.sign::<_, S>(sign).expect("signing"); let unverified = signed.unverify(); - let verifying_key = algorithm.verifying_key(); - unverified - .verify::<_, rsa::pkcs1v15::Signature>(&verifying_key) - .unwrap(); + unverified.verify::<_, S>(verify).expect("verifying"); } - #[cfg(feature = "rand")] - #[test] - fn rsa_pss_algorithm() { - use rand_core::OsRng; + macro_rules! rsa_pkcs1v15_algorithm_test { + ($name:ident, $digest:ty) => { + #[test] + fn $name() { + let pkey = rsa(jwk()); - let pkey = rsa(jwk()); + let algorithm: rsa::pkcs1v15::SigningKey<$digest> = + rsa::pkcs1v15::SigningKey::new(pkey); - let payload = json! { - { - "iss": "joe", - "exp": 1300819380, - "http://example.com/is_root": true + algorithm_roundtrip::( + &algorithm, + &algorithm.verifying_key(), + ); + algorithm_roundtrip::(&algorithm, &algorithm.verifying_key()); } }; + } - let token = crate::Token::compact((), payload); - let algorithm: rsa::pss::SigningKey = rsa::pss::SigningKey::new(pkey); + rsa_pkcs1v15_algorithm_test!(rs256_algorithm, sha2::Sha256); + rsa_pkcs1v15_algorithm_test!(rs384_algorithm, sha2::Sha384); + rsa_pkcs1v15_algorithm_test!(rs512_algorithm, sha2::Sha512); - let signed = token - .sign_randomized::<_, rsa::pss::Signature>(&algorithm, &mut OsRng) - .unwrap(); + #[cfg(feature = "rand")] + mod pss { + use rand_core::OsRng; + use serde_json::json; + use signature::Keypair; + use signature::SignatureEncoding; + + use crate::SignatureBytes; + + fn algorithm_roundtrip( + sign: &impl crate::algorithms::RandomizedTokenSigner, + verify: &impl crate::algorithms::TokenVerifier, + ) where + S: SignatureEncoding, + { + let payload = json! { + { + "iss": "joe", + "exp": 1300819380, + "http://example.com/is_root": true + } + }; - let unverified = signed.unverify(); - let verifying_key = algorithm.verifying_key(); - unverified - .verify::<_, rsa::pss::Signature>(&verifying_key) - .unwrap(); + let token = crate::Token::compact((), payload); + + let signed = token + .sign_randomized::<_, S>(sign, &mut OsRng) + .expect("signing"); + + let unverified = signed.unverify(); + unverified.verify::<_, S>(verify).expect("verifying"); + } + + macro_rules! rsa_pss_algorithm_test { + ($name:ident, $digest:ty) => { + #[test] + fn $name() { + let pkey = super::rsa(super::jwk()); + + let algorithm: rsa::pss::SigningKey<$digest> = rsa::pss::SigningKey::new(pkey); + + algorithm_roundtrip::( + &algorithm, + &algorithm.verifying_key(), + ); + algorithm_roundtrip::(&algorithm, &algorithm.verifying_key()); + } + }; + } + + rsa_pss_algorithm_test!(ps256_algorithm, sha2::Sha256); + rsa_pss_algorithm_test!(ps384_algorithm, sha2::Sha384); + rsa_pss_algorithm_test!(ps512_algorithm, sha2::Sha512); } } From 1a709e558200ec82acf091a0f8717be3292ae642 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Fri, 1 Dec 2023 21:23:44 +0000 Subject: [PATCH 09/10] Test support for ECDSA signatures --- Cargo.toml | 4 ++-- src/algorithms/ecdsa.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c86dac7..3007e70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ digest = { version = "0.10" } ecdsa = { version = "0.16", features = ["signing", "der"], optional = true } hmac = { version = "0.12", optional = true } p256 = { version = "0.13", features = ["ecdsa", "jwk"], optional = true } -p384 = { version = "0.13", optional = true } -p521 = { version = "0.13", optional = true } +p384 = { version = "0.13", features = ["ecdsa", "jwk"], optional = true } +p521 = { version = "0.13", features = ["ecdsa", "jwk"], optional = true } pkcs8 = "0.10" rand_core = { version = "0.6.4", optional = true, default-features = false } rsa = { version = "0.9", features = ["sha2"], optional = true } diff --git a/src/algorithms/ecdsa.rs b/src/algorithms/ecdsa.rs index 9d13aed..f4317e8 100644 --- a/src/algorithms/ecdsa.rs +++ b/src/algorithms/ecdsa.rs @@ -454,4 +454,44 @@ mod test { ) .expect("signature verification for RFC7515a3 example failed"); } + + macro_rules! ecdsa_algorithm_test { + ($name:ident, $curve:ty) => { + #[cfg(feature = "rand")] + #[test] + fn $name() { + let key = SigningKey::<$curve>::random(&mut rand_core::OsRng); + let verify = *key.verifying_key(); + + let payload = json! { + { + "iss": "joe", + "exp": 1300819380, + "http://example.com/is_root": true + } + }; + + let token = crate::Token::compact((), payload); + + let signed = token + .clone() + .sign::<_, ecdsa::Signature<$curve>>(&key) + .unwrap(); + let unverified = signed.unverify(); + unverified + .verify::<_, ecdsa::Signature<$curve>>(&verify) + .unwrap(); + + let signed = token.clone().sign::<_, SignatureBytes>(&key).unwrap(); + let unverified = signed.unverify(); + unverified.verify::<_, SignatureBytes>(&verify).unwrap(); + } + }; + } + + #[cfg(feature = "p256")] + ecdsa_algorithm_test!(p256_roundtrip, NistP256); + + #[cfg(feature = "p384")] + ecdsa_algorithm_test!(p384_roundtrip, NistP384); } From be0c9f0b5d578f9a809ff93047dda9faec60c117 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Sat, 2 Dec 2023 16:59:29 +0000 Subject: [PATCH 10/10] Adds a method to build a public JWK --- src/key.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/key.rs b/src/key.rs index e95b99f..db33091 100644 --- a/src/key.rs +++ b/src/key.rs @@ -174,6 +174,14 @@ impl JsonWebKey { } } + /// Build a JWK from a public key. + pub fn build_public(key: &K) -> Self { + JsonWebKey { + key_type: key.key_type().into(), + parameters: key.public_parameters().into_iter().collect(), + } + } + /// Build a JWK from a key. pub fn build(key: &K) -> Self { JsonWebKey { @@ -342,7 +350,7 @@ where impl std::fmt::Display for Thumbprint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.thumbprint) + f.write_str(&self.thumbprint) } } @@ -363,13 +371,12 @@ mod test { sa::assert_obj_safe!(SerializeJWK); - #[cfg(all(test, feature = "rsa"))] + #[cfg(feature = "rsa")] mod rsa { use super::super::*; use serde_json::json; - #[cfg(feature = "rsa")] #[test] fn rfc7639_example() { let key = rsa::RsaPublicKey::from_value(json!({