From 4496aec1ba9506a3f4909cb42d62cfec8cadacde Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Sat, 25 Nov 2023 04:29:47 +0000 Subject: [PATCH 1/2] First pass at adopting core RustCrypto traits Traits work for everything except HMAC, might have to switch to not assume that everything is a digest and instead rely on Update and Finalize traits from the digest crate --- Cargo.toml | 4 +- README.md | 10 +- examples/acme-new-account.rs | 3 +- examples/rfc7515a2.rs | 10 +- src/algorithms/ecdsa.rs | 202 ++++++------------------------ src/algorithms/hmac.rs | 157 ++++++++++-------------- src/algorithms/mod.rs | 136 ++++++++++++++------- src/algorithms/rsa.rs | 229 +++++++++++++++-------------------- src/base64data.rs | 38 +++--- src/jose/mod.rs | 9 +- src/token/formats.rs | 42 ++++--- src/token/mod.rs | 75 ++++++------ src/token/state.rs | 34 +++--- 13 files changed, 410 insertions(+), 539 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8b1388a..9212a97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" sha1 = "0.10.5" sha2 = "0.10.6" -signature = "2.1.0" +signature = { version = "2.1.0", features = ["std", "digest"] } thiserror = "1.0.40" url = { version = "2.3.1", features = ["serde"] } zeroize = { version = "1.6.0", features = ["std", "serde", "derive"] } @@ -53,7 +53,7 @@ p521 = { version = "0.13.0", optional = true } hmac = { version = "0.12.1", features = ["reset"], optional = true } [features] -default = ["fmt", "rsa", "ecdsa", "p256", "p384", "p521", "hmac"] +default = ["fmt", "rsa", "ecdsa", "p256", "p384", "p521"] fmt = [] rsa = ["dep:rsa"] hmac = ["dep:hmac"] diff --git a/README.md b/README.md index 70012a4..fbbd57e 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ use jaws::JWTFormat; // signing and verification status. use jaws::Token; -use jaws::algorithms::rsa::RsaPkcs1v15Verify; // The unverified token state, used like `Token<.., Unverified<..>, ..>`. // It is generic over the type of the custom header parameters. use jaws::token::Unverified; @@ -104,10 +103,6 @@ use rsa::pkcs8::DecodePrivateKey; // function, so we get it here from the `sha2` crate in the RustCrypto suite. use sha2::Sha256; -// This is an alias for the RSA PKCS#1 v1.5 signing algorithm, which is -// implemented in the rsa crate as `rsa::pkcs1v15::SigningKey`. -use jaws::algorithms::rsa::RsaPkcs1v15; - // Using serde_json allows us to quickly construct a serializable payload, // but applications may want to instead define a struct and use serde to // derive serialize and deserialize for added type safety. @@ -127,7 +122,7 @@ fn main() -> Result<(), Box> { // RsaPkcs1v15 is really an alias to the digital signature algorithm // implementation in the `rsa` crate, but provided in JAWS to make // it clear which types are compatible with JWTs. - let alg = RsaPkcs1v15::::new_with_prefix(key); + let alg = rsa::pkcs1v15::SigningKey::::new_with_prefix(key); // Claims can combine registered and custom fields. The claims object // can be any type which implements [serde::Serialize]. @@ -196,7 +191,8 @@ fn main() -> Result<(), Box> { assert_eq!(&key, alg.as_ref().deref()); - let alg: RsaPkcs1v15Verify = RsaPkcs1v15Verify::new_with_prefix(key); + let alg: rsa::pkcs1v15::VerifyingKey = + rsa::pkcs1v15::VerifyingKey::new_with_prefix(key); // We can't access the claims until we verify the token. let verified = token.verify(&alg).unwrap(); diff --git a/examples/acme-new-account.rs b/examples/acme-new-account.rs index 2d8908c..ad72325 100644 --- a/examples/acme-new-account.rs +++ b/examples/acme-new-account.rs @@ -1,4 +1,3 @@ -use jaws::algorithms::rsa::RsaPkcs1v15; use jaws::Compact; use jaws::JWTFormat; use jaws::Token; @@ -48,7 +47,7 @@ fn main() { // RsaPkcs1v15 is really an alias to the digital signature algorithm // implementation in the `rsa` crate, but provided in JAWS to make // it clear which types are compatible with JWTs. - let alg = RsaPkcs1v15::::new_with_prefix(key); + let alg = rsa::pkcs1v15::SigningKey::::new_with_prefix(key); let payload = json!({ "termsOfServiceAgreed": true, diff --git a/examples/rfc7515a2.rs b/examples/rfc7515a2.rs index 716e28d..f2cc00b 100644 --- a/examples/rfc7515a2.rs +++ b/examples/rfc7515a2.rs @@ -10,7 +10,6 @@ use jaws::JWTFormat; // signing and verification status. use jaws::Token; -use jaws::algorithms::rsa::RsaPkcs1v15Verify; // The unverified token state, used like `Token<.., Unverified<..>, ..>`. // It is generic over the type of the custom header parameters. use jaws::token::Unverified; @@ -26,10 +25,6 @@ use rsa::pkcs8::DecodePrivateKey; // function, so we get it here from the `sha2` crate in the RustCrypto suite. use sha2::Sha256; -// This is an alias for the RSA PKCS#1 v1.5 signing algorithm, which is -// implemented in the rsa crate as `rsa::pkcs1v15::SigningKey`. -use jaws::algorithms::rsa::RsaPkcs1v15; - // Using serde_json allows us to quickly construct a serializable payload, // but applications may want to instead define a struct and use serde to // derive serialize and deserialize for added type safety. @@ -49,7 +44,7 @@ fn main() -> Result<(), Box> { // RsaPkcs1v15 is really an alias to the digital signature algorithm // implementation in the `rsa` crate, but provided in JAWS to make // it clear which types are compatible with JWTs. - let alg = RsaPkcs1v15::::new_with_prefix(key); + let alg = rsa::pkcs1v15::SigningKey::::new_with_prefix(key); // Claims can combine registered and custom fields. The claims object // can be any type which implements [serde::Serialize]. @@ -118,7 +113,8 @@ fn main() -> Result<(), Box> { assert_eq!(&key, alg.as_ref().deref()); - let alg: RsaPkcs1v15Verify = RsaPkcs1v15Verify::new_with_prefix(key); + let alg: rsa::pkcs1v15::VerifyingKey = + rsa::pkcs1v15::VerifyingKey::new_with_prefix(key); // We can't access the claims until we verify the token. let verified = token.verify(&alg).unwrap(); diff --git a/src/algorithms/ecdsa.rs b/src/algorithms/ecdsa.rs index 3328553..e7aa0e8 100644 --- a/src/algorithms/ecdsa.rs +++ b/src/algorithms/ecdsa.rs @@ -21,7 +21,7 @@ Signing with an ECDSA key: ```rust use serde_json::json; -use elliptic_curve::SecretKey; +use ecdsa::SigningKey; use elliptic_curve::FieldBytes; use base64ct::{Encoding, Base64UrlUnpadded}; @@ -33,7 +33,7 @@ use jaws::JWTFormat; let mut key_bytes = FieldBytes::::default(); base64ct::Base64UrlUnpadded::decode("jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI", &mut key_bytes).unwrap(); -let key: SecretKey = SecretKey::from_slice(&key_bytes).unwrap(); +let key: SigningKey = SigningKey::from_slice(&key_bytes).unwrap(); // Claims can combine registered and custom fields. The claims object // can be any type which implements [serde::Serialize] @@ -66,26 +66,16 @@ println!("{}", signed.formatted()); "# )] -use std::ops::Add; - -use ::ecdsa::{ - der::{MaxOverhead, MaxSize}, - hazmat::SignPrimitive, - PrimeCurve, SignatureSize, -}; +use ::ecdsa::{hazmat::SignPrimitive, PrimeCurve, SignatureSize, SigningKey, VerifyingKey}; use base64ct::Encoding; -use bytes::BytesMut; use digest::generic_array::ArrayLength; -use ecdsa::{hazmat::VerifyPrimitive, Signature, VerifyingKey}; use elliptic_curve::{ ops::Invert, sec1::{Coordinates, FromEncodedPoint, ModulusSize, ToEncodedPoint}, subtle::CtOption, - AffinePoint, CurveArithmetic, FieldBytesSize, JwkParameters, PublicKey, Scalar, + AffinePoint, CurveArithmetic, FieldBytesSize, JwkParameters, Scalar, }; -pub use elliptic_curve::SecretKey; - #[cfg(feature = "p256")] pub use p256::NistP256; @@ -94,16 +84,19 @@ pub use p384::NistP384; #[cfg(feature = "p521")] pub use p521::NistP521; -use signature::Verifier; -impl crate::key::JWKeyType for PublicKey +impl crate::key::JWKeyType for VerifyingKey where - C: elliptic_curve::Curve + elliptic_curve::CurveArithmetic, + C: PrimeCurve + CurveArithmetic + JwkParameters, + Scalar: Invert>> + SignPrimitive, + SignatureSize: ArrayLength, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, { const KEY_TYPE: &'static str = "EC"; } -impl crate::key::SerializeJWK for PublicKey +impl crate::key::SerializeJWK for VerifyingKey where C: PrimeCurve + CurveArithmetic + JwkParameters, Scalar: Invert>> + SignPrimitive, @@ -135,44 +128,7 @@ where } } -impl crate::key::SerializeJWK for SecretKey -where - C: PrimeCurve + CurveArithmetic + JwkParameters, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, -{ - fn parameters(&self) -> Vec<(String, serde_json::Value)> { - self.public_key().parameters() - } -} - -impl crate::key::SerializeJWK for ecdsa::VerifyingKey -where - C: PrimeCurve + CurveArithmetic + JwkParameters, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, -{ - fn parameters(&self) -> Vec<(String, serde_json::Value)> { - PublicKey::::from(self).parameters() - } -} - -impl crate::key::JWKeyType for ecdsa::VerifyingKey -where - C: PrimeCurve + CurveArithmetic + JwkParameters, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, -{ - const KEY_TYPE: &'static str = "EC"; -} - -impl crate::key::SerializeJWK for ecdsa::SigningKey +impl crate::key::SerializeJWK for SigningKey where C: PrimeCurve + CurveArithmetic + JwkParameters, Scalar: Invert>> + SignPrimitive, @@ -196,119 +152,41 @@ where const KEY_TYPE: &'static str = "EC"; } -impl crate::key::JWKeyType for SecretKey -where - C: elliptic_curve::Curve, -{ - const KEY_TYPE: &'static str = "EC"; -} - -#[cfg(feature = "p256")] -impl super::Algorithm for SecretKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::ES256; - type Signature = ecdsa::SignatureBytes; -} - -#[cfg(feature = "p384")] -impl super::Algorithm for SecretKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::ES384; - type Signature = ecdsa::SignatureBytes; -} - -#[cfg(feature = "p521")] -impl super::Algorithm for SecretKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::ES512; - type Signature = ecdsa::SignatureBytes; -} - -impl super::SigningAlgorithm for SecretKey -where - C: PrimeCurve + CurveArithmetic + JwkParameters + ecdsa::hazmat::DigestPrimitive, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, - MaxSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, - SecretKey: super::Algorithm>, - as Add>::Output: Add + ArrayLength, -{ - type Error = ecdsa::Error; - type Key = SecretKey; - - fn sign(&self, header: &str, payload: &str) -> Result { - let message = format!("{}.{}", header, payload); - let signature = - <::ecdsa::SigningKey as signature::Signer<::ecdsa::Signature>>::try_sign( - &self.into(), - message.as_bytes(), - ) - .map(|sig| sig.to_bytes())?; - Ok(signature) - } +macro_rules! jose_ecdsa_algorithm { + ($alg:ident, $curve:ty) => { + impl crate::algorithms::JoseAlgorithm for ecdsa::SigningKey<$curve> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = ecdsa::Signature<$curve>; + } - fn key(&self) -> &Self::Key { - self - } -} + impl crate::algorithms::JoseDigestAlgorithm for ecdsa::SigningKey<$curve> { + type Digest = <$curve as ::ecdsa::hazmat::DigestPrimitive>::Digest; + } -impl super::VerifyAlgorithm for VerifyingKey -where - C: PrimeCurve + CurveArithmetic + JwkParameters + ecdsa::hazmat::DigestPrimitive, - ::AffinePoint: VerifyPrimitive, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, - MaxSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, - VerifyingKey: super::Algorithm>, - as Add>::Output: Add + ArrayLength, -{ - type Error = ecdsa::Error; - type Key = VerifyingKey; - - fn verify( - &self, - header: &[u8], - payload: &[u8], - signature: &[u8], - ) -> Result { - let mut message = BytesMut::with_capacity(header.len() + payload.len() + 1); - message.extend_from_slice(header); - message.extend_from_slice(b"."); - message.extend_from_slice(payload); - - let signature = ecdsa::Signature::try_from(signature)?; - >>::verify(self, message.as_ref(), &signature)?; - Ok(signature.into()) - } + impl crate::algorithms::JoseAlgorithm for ecdsa::VerifyingKey<$curve> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = ecdsa::Signature<$curve>; + } - fn key(&self) -> &Self::Key { - self - } + impl crate::algorithms::JoseDigestAlgorithm for ecdsa::VerifyingKey<$curve> { + type Digest = <$curve as ::ecdsa::hazmat::DigestPrimitive>::Digest; + } + }; } #[cfg(feature = "p256")] -impl super::Algorithm for VerifyingKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::ES256; - type Signature = ecdsa::SignatureBytes; -} +jose_ecdsa_algorithm!(ES256, NistP256); #[cfg(feature = "p384")] -impl super::Algorithm for VerifyingKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::ES384; - type Signature = ecdsa::SignatureBytes; -} - -#[cfg(feature = "p521")] -impl super::Algorithm for VerifyingKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::ES512; - type Signature = ecdsa::SignatureBytes; -} +jose_ecdsa_algorithm!(ES384, NistP384); #[cfg(all(test, feature = "p256"))] mod test { use super::*; + use crate::algorithms::TokenSigner; use base64ct::Encoding; use elliptic_curve::FieldBytes; @@ -319,12 +197,12 @@ mod test { s.chars().filter(|c| !c.is_whitespace()).collect() } - fn ecdsa(jwk: &serde_json::Value) -> SecretKey { + fn ecdsa(jwk: &serde_json::Value) -> SigningKey { let d_b64 = strip_whitespace(jwk["d"].as_str().unwrap()); let mut d_bytes = FieldBytes::::default(); base64ct::Base64UrlUnpadded::decode(&d_b64, &mut d_bytes).unwrap(); - let key = SecretKey::from_slice(&d_bytes).unwrap(); + let key = SigningKey::from_slice(&d_bytes).unwrap(); d_bytes.zeroize(); key } @@ -348,12 +226,8 @@ mod test { let header = strip_whitespace("eyJhbGciOiJFUzI1NiJ9"); - let signature = as super::super::SigningAlgorithm>::sign( - &key, &header, &payload, - ) - .unwrap(); - - let _sig = base64ct::Base64UrlUnpadded::encode_string(signature.as_ref()); + let signature = key.sign_token(&header, &payload); + let _sig = base64ct::Base64UrlUnpadded::encode_string(signature.to_bytes().as_ref()); // This won't work because the signature is non-deterministic // assert_eq!( diff --git a/src/algorithms/hmac.rs b/src/algorithms/hmac.rs index 51259d6..506002a 100644 --- a/src/algorithms/hmac.rs +++ b/src/algorithms/hmac.rs @@ -2,12 +2,12 @@ //! //! Based on the [hmac](https://crates.io/crates/hmac) crate. -use std::{convert::Infallible, marker::PhantomData}; +use std::{marker::PhantomData, ops::Deref}; use base64ct::Encoding; -use bytes::BytesMut; -use digest::Mac; +use digest::Digest; use hmac::SimpleHmac; +use signature::SignatureEncoding; use crate::key::{JWKeyType, SerializeJWK}; @@ -58,38 +58,18 @@ impl AsRef<[u8]> for HmacKey { } } -impl JWKeyType for HmacKey { - const KEY_TYPE: &'static str = "oct"; -} - -impl SerializeJWK for HmacKey { - fn parameters(&self) -> Vec<(String, serde_json::Value)> { - vec![( - "k".to_string(), - serde_json::Value::String(base64ct::Base64UrlUnpadded::encode_string(&self.key)), - )] - } -} - /// The HMAC algorithm as used when signing a JWS. /// /// This type exists to associate the original key value with the digest. /// This is required for verification, and for the `jwk` field in the JWS header, /// if that field is enabled. #[derive(Debug, Clone)] -pub struct Hmac -where - D: digest::Digest + digest::core_api::BlockSizeUser, -{ +pub struct Hmac { key: HmacKey, _digest: PhantomData, } -impl Hmac -where - D: digest::Digest + digest::core_api::BlockSizeUser, - hmac::SimpleHmac: Mac, -{ +impl Hmac { /// Create a new HMAC digest wrapper with a given signing key. /// /// Signing keys are arbitrary bytes. @@ -106,90 +86,79 @@ where } } -impl super::Algorithm for Hmac { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::HS256; - type Signature = digest::Output>; +impl JWKeyType for Hmac { + const KEY_TYPE: &'static str = "oct"; } -impl super::Algorithm for Hmac { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::HS384; - type Signature = digest::Output>; +impl SerializeJWK for Hmac { + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + vec![( + "k".to_string(), + serde_json::Value::String(base64ct::Base64UrlUnpadded::encode_string(&self.key.key)), + )] + } } -impl super::Algorithm for Hmac { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::HS512; - type Signature = digest::Output>; -} +/// A signature produced by an HMAC algorithm. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DigestSignature(digest::Output>) +where + D: Digest + digest::core_api::BlockSizeUser; -impl super::SigningAlgorithm for Hmac +impl TryFrom<&[u8]> for DigestSignature where - D: digest::Digest - + digest::Reset - + digest::core_api::BlockSizeUser - + digest::FixedOutput - + digest::core_api::CoreProxy - + Clone, - Hmac: super::Algorithm>>, + D: Digest + digest::core_api::BlockSizeUser, { - type Error = Infallible; - type Key = HmacKey; - - fn sign(&self, header: &str, payload: &str) -> Result { - // Create a new, one-shot digest for this signature. - let mut digest: SimpleHmac = - SimpleHmac::new_from_slice(self.key.as_ref()).expect("Valid key"); - let message = format!("{}.{}", header, payload); - digest.update(message.as_bytes()); - Ok(digest.finalize().into_bytes()) - } + type Error = signature::Error; - fn key(&self) -> &Self::Key { - &self.key + fn try_from(value: &[u8]) -> Result { + Ok(Self( + digest::Output::>::from_slice(value).clone(), + )) } } -impl super::VerifyAlgorithm for Hmac +impl TryFrom> for Box<[u8]> where - D: digest::Digest - + digest::Reset - + digest::core_api::BlockSizeUser - + digest::FixedOutput - + digest::core_api::CoreProxy - + Clone, - Hmac: super::Algorithm>>, + D: Digest + digest::core_api::BlockSizeUser, { - type Error = digest::MacError; - type Key = HmacKey; - - fn verify( - &self, - header: &[u8], - payload: &[u8], - signature: &[u8], - ) -> Result { - // Create a new, one-shot digest for this signature. - let mut digest: SimpleHmac = - SimpleHmac::new_from_slice(self.key.as_ref()).expect("Valid key"); - let mut message = BytesMut::with_capacity(header.len() + payload.len() + 1); - message.extend_from_slice(header); - message.extend_from_slice(b"."); - message.extend_from_slice(payload); - - digest.update(message.as_ref()); - digest.clone().verify(signature.into())?; - Ok(digest.finalize().into_bytes()) - } + type Error = signature::Error; - fn key(&self) -> &Self::Key { - &self.key + fn try_from(value: DigestSignature) -> Result { + Ok(value.0.deref().into()) } } +impl SignatureEncoding for DigestSignature +where + D: Digest + digest::core_api::BlockSizeUser + Clone, +{ + type Repr = Box<[u8]>; +} + +macro_rules! hmac_algorithm { + ($alg:ident, $digest:ty) => { + impl crate::algorithms::JoseAlgorithm for Hmac<$digest> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = DigestSignature<$digest>; + } + + impl crate::algorithms::JoseDigestAlgorithm for Hmac<$digest> { + type Digest = $digest; + } + }; +} + +hmac_algorithm!(HS256, sha2::Sha256); +hmac_algorithm!(HS384, sha2::Sha384); +hmac_algorithm!(HS512, sha2::Sha512); + #[cfg(test)] mod test { - use crate::algorithms::SigningAlgorithm; use super::*; + use crate::algorithms::TokenSigner; use base64ct::Encoding; use serde_json::json; @@ -226,13 +195,13 @@ mod test { let algorithm: Hmac = Hmac::new(key); - let signature = algorithm.sign(&header, &payload).unwrap(); + // let signature = algorithm.sign_token(&header, &payload); - let sig = base64ct::Base64UrlUnpadded::encode_string(signature.as_ref()); + // let sig = base64ct::Base64UrlUnpadded::encode_string(signature.to_bytes().as_ref()); - assert_eq!( - sig, - strip_whitespace("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") - ); + // assert_eq!( + // sig, + // strip_whitespace("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") + // ); } } diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index a533c51..af3e9db 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -6,9 +6,10 @@ //! //! [RFC7518]: https://tools.ietf.org/html/rfc7518 +use bytes::Bytes; +use digest::Digest; use serde::{Deserialize, Serialize}; - -use crate::key; +use signature::SignatureEncoding; #[cfg(feature = "ecdsa")] pub mod ecdsa; @@ -88,59 +89,104 @@ impl AlgorithmIdentifier { /// /// Algorithm identifiers are used in JWS and JWE to indicate how a token is signed or encrypted. /// They are set in the [crate::jose::Header] automatically when signing the JWT. -pub trait Algorithm { +pub trait JoseAlgorithm { /// The identifier for this algorithm when used in a JWT registered header. /// /// This is the `alg` field in the JOSE header. const IDENTIFIER: AlgorithmIdentifier; - /// The type of the signature, which should be representable as bytes. - type Signature: AsRef<[u8]>; + /// The type of the signature, which must support encoding. + type Signature: SignatureEncoding; +} + +/// A trait to associate an algorithm with a digest for signing. +pub trait JoseDigestAlgorithm: JoseAlgorithm { + /// The digest algorithm used by this signature. + type Digest: Digest; } /// A trait to represent an algorithm which can sign a JWT. /// /// This trait should apply to signing keys. -pub trait SigningAlgorithm: Algorithm { - /// Error type returned when signing fails. - type Error; - - /// The inner key type (e.g. [::rsa::RsaPrivateKey]) used to complete the registered - /// header values. - type Key: key::SerializeJWK; +pub trait TokenSigner: JoseAlgorithm { + /// Sign the contents of the JWT, when provided with the base64url-encoded header + /// and payload. This is the JWS Signature value, and will be base64url-encoded + /// and appended to the compact representation of the JWT. + fn try_sign_token( + &self, + header: &str, + payload: &str, + ) -> Result; /// Sign the contents of the JWT, when provided with the base64url-encoded header /// and payload. This is the JWS Signature value, and will be base64url-encoded /// and appended to the compact representation of the JWT. - fn sign(&self, header: &str, payload: &str) -> Result; + /// + /// # Panics + /// + /// This function will panic if the signature cannot be computed. + fn sign_token(&self, header: &str, payload: &str) -> Self::Signature { + self.try_sign_token(header, payload).unwrap() + } +} + +impl TokenSigner for K +where + K: JoseDigestAlgorithm, + K: signature::DigestSigner, +{ + fn try_sign_token( + &self, + header: &str, + payload: &str, + ) -> Result { + let message = format!("{}.{}", header, payload); + + let mut digest = ::Digest::new(); + digest.update(message.as_bytes()); - /// Return a reference to the key used to sign the JWT. - fn key(&self) -> &Self::Key; + self.try_sign_digest(digest) + } } /// A trait to represent an algorithm which can verify a JWT. /// /// 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 VerifyAlgorithm: Algorithm { - /// Error type returned when verification fails. - type Error; - - /// The inner key type (e.g. [::rsa::RsaPublicKey]) used to complete the registered - /// header values. - type Key: key::SerializeJWK; - +pub trait TokenVerifier: JoseAlgorithm { /// Verify the signature of the JWT, when provided with the base64url-encoded header /// and payload. - fn verify( + fn verify_token( &self, header: &[u8], payload: &[u8], signature: &[u8], - ) -> Result; + ) -> Result; +} - /// Return a reference to the key used to verify the JWT. - fn key(&self) -> &Self::Key; +impl TokenVerifier for K +where + K: JoseDigestAlgorithm, + K: signature::DigestVerifier, +{ + fn verify_token( + &self, + header: &[u8], + payload: &[u8], + signature: &[u8], + ) -> Result { + let mut digest = ::Digest::new(); + digest.update(header); + digest.update(b"."); + digest.update(payload); + + let signature = signature + .try_into() + .map_err(|_| signature::Error::default())?; + + self.verify_digest(digest, &signature)?; + Ok(signature) + } } /// A signature which has not been matched to an algorithm or key. @@ -148,20 +194,8 @@ pub trait VerifyAlgorithm: Algorithm { /// This is a basic signature `struct` which can be used to store any signature /// on the heap. It is used to store the signature of a JWT before it is verified, /// or if a signature has a variable length. -#[derive(Debug, Clone, PartialEq, Eq, Hash, zeroize::Zeroize, zeroize::ZeroizeOnDrop)] -pub struct SignatureBytes(Vec); - -impl SignatureBytes { - /// Add to this signature from a byte slice. - pub fn extend_from_slice(&mut self, other: &[u8]) { - self.0.extend_from_slice(other); - } - - /// Create a new signature with the given capacity. - pub fn with_capacity(capacity: usize) -> Self { - SignatureBytes(Vec::with_capacity(capacity)) - } -} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SignatureBytes(Bytes); impl AsRef<[u8]> for SignatureBytes { fn as_ref(&self) -> &[u8] { @@ -171,12 +205,28 @@ impl AsRef<[u8]> for SignatureBytes { impl From<&[u8]> for SignatureBytes { fn from(bytes: &[u8]) -> Self { - SignatureBytes(bytes.to_vec()) + SignatureBytes(bytes.to_owned().into()) + } +} + +impl From for SignatureBytes { + fn from(bytes: Bytes) -> Self { + SignatureBytes(bytes) + } +} + +impl From for Bytes { + fn from(bytes: SignatureBytes) -> Self { + bytes.0.clone() } } impl From> for SignatureBytes { fn from(bytes: Vec) -> Self { - SignatureBytes(bytes) + SignatureBytes(bytes.into()) } } + +impl signature::SignatureEncoding for SignatureBytes { + type Repr = Bytes; +} diff --git a/src/algorithms/rsa.rs b/src/algorithms/rsa.rs index d5f35d1..f8d05e1 100644 --- a/src/algorithms/rsa.rs +++ b/src/algorithms/rsa.rs @@ -16,9 +16,6 @@ //! This algorithm is used to sign and verify JSON Web Tokens using the RSASSA-PSS. use base64ct::{Base64UrlUnpadded, Encoding}; -use bytes::BytesMut; -use rsa::rand_core::OsRng; -use rsa::signature::RandomizedSigner; use rsa::PublicKeyParts; impl crate::key::JWKeyType for rsa::RsaPublicKey { @@ -48,134 +45,107 @@ impl crate::key::SerializeJWK for rsa::RsaPrivateKey { } } -/// Alogrithm wrapper for the Digital Signature with RSASSA-PKCS1-v1_5 algorithm. -pub type RsaPkcs1v15 = rsa::pkcs1v15::SigningKey; - -/// Alogrithm wrapper for the Digital Signature with RSASSA-PKCS1-v1_5 algorithm. -pub type RsaPkcs1v15Verify = rsa::pkcs1v15::VerifyingKey; - -impl super::SigningAlgorithm for RsaPkcs1v15 -where - D: digest::Digest, - RsaPkcs1v15: super::Algorithm, -{ - type Error = signature::Error; - type Key = rsa::RsaPrivateKey; - - fn sign(&self, header: &str, payload: &str) -> Result { - let message = format!("{}.{}", header, payload); - self.try_sign_with_rng(&mut OsRng, message.as_bytes()) - } - - fn key(&self) -> &Self::Key { - self.as_ref() - } -} - -impl super::VerifyAlgorithm for RsaPkcs1v15Verify -where - D: digest::Digest, - RsaPkcs1v15Verify: super::Algorithm + Clone, -{ - type Error = signature::Error; - - type Key = rsa::RsaPublicKey; - - fn verify( - &self, - header: &[u8], - payload: &[u8], - signature: &[u8], - ) -> Result { - use rsa::signature::Verifier; - let signature = rsa::pkcs1v15::Signature::try_from(signature).unwrap(); - - let mut message = BytesMut::with_capacity(header.len() + payload.len() + 1); - message.extend_from_slice(header); - message.extend_from_slice(b"."); - message.extend_from_slice(payload); - - >::verify(self, message.as_ref(), &signature)?; - Ok(signature) - } - - fn key(&self) -> &Self::Key { - self.as_ref() - } -} - -impl super::Algorithm for RsaPkcs1v15 { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::RS256; - type Signature = rsa::pkcs1v15::Signature; -} - -impl super::Algorithm for RsaPkcs1v15 { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::RS384; - type Signature = rsa::pkcs1v15::Signature; -} - -impl super::Algorithm for RsaPkcs1v15 { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::RS512; - type Signature = rsa::pkcs1v15::Signature; -} - -impl super::Algorithm for RsaPkcs1v15Verify { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::RS256; - type Signature = rsa::pkcs1v15::Signature; -} - -impl super::Algorithm for RsaPkcs1v15Verify { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::RS384; - type Signature = rsa::pkcs1v15::Signature; -} - -impl super::Algorithm for RsaPkcs1v15Verify { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::RS512; - type Signature = rsa::pkcs1v15::Signature; -} - -/// Algorithm wrapper for RSA-PSS signatures, using [rsa::pss::BlindedSigningKey]. -pub type RsaPSSKey = rsa::pss::BlindedSigningKey; - -impl super::SigningAlgorithm for RsaPSSKey -where - D: digest::Digest + digest::FixedOutputReset, - RsaPSSKey: super::Algorithm, -{ - type Error = signature::Error; - type Key = rsa::RsaPrivateKey; - - fn sign(&self, header: &str, payload: &str) -> Result { - let message = format!("{}.{}", header, payload); - self.try_sign_with_rng(&mut OsRng, message.as_bytes()) - } - - fn key(&self) -> &Self::Key { - self.as_ref() - } -} - -impl super::Algorithm for RsaPSSKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::PS256; - type Signature = rsa::pss::Signature; -} - -impl super::Algorithm for RsaPSSKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::PS384; - type Signature = rsa::pss::Signature; -} - -impl super::Algorithm for RsaPSSKey { - const IDENTIFIER: super::AlgorithmIdentifier = super::AlgorithmIdentifier::PS512; - type Signature = rsa::pss::Signature; -} +macro_rules! jose_rsa_pkcs1v15_algorithm { + ($alg:ident, $digest:ty) => { + impl crate::algorithms::JoseAlgorithm for rsa::pkcs1v15::SigningKey<$digest> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = rsa::pkcs1v15::Signature; + } + + impl crate::algorithms::JoseDigestAlgorithm for rsa::pkcs1v15::SigningKey<$digest> { + type Digest = $digest; + } + + impl crate::key::JWKeyType for rsa::pkcs1v15::SigningKey<$digest> { + const KEY_TYPE: &'static str = "RSA"; + } + + impl crate::key::SerializeJWK for rsa::pkcs1v15::SigningKey<$digest> { + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().to_public_key().parameters() + } + } + + impl crate::algorithms::JoseAlgorithm for rsa::pkcs1v15::VerifyingKey<$digest> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = rsa::pkcs1v15::Signature; + } + + impl crate::algorithms::JoseDigestAlgorithm for rsa::pkcs1v15::VerifyingKey<$digest> { + type Digest = $digest; + } + + impl crate::key::JWKeyType for rsa::pkcs1v15::VerifyingKey<$digest> { + const KEY_TYPE: &'static str = "RSA"; + } + + impl crate::key::SerializeJWK for rsa::pkcs1v15::VerifyingKey<$digest> { + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().parameters() + } + } + }; +} + +jose_rsa_pkcs1v15_algorithm!(RS256, sha2::Sha256); +jose_rsa_pkcs1v15_algorithm!(RS384, sha2::Sha384); +jose_rsa_pkcs1v15_algorithm!(RS512, sha2::Sha512); + +macro_rules! jose_rsa_pss_algorithm { + ($alg:ident, $digest:ty) => { + impl crate::algorithms::JoseAlgorithm for rsa::pss::BlindedSigningKey<$digest> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = rsa::pss::Signature; + } + + impl crate::algorithms::JoseDigestAlgorithm for rsa::pss::BlindedSigningKey<$digest> { + type Digest = $digest; + } + + impl crate::key::JWKeyType for rsa::pss::BlindedSigningKey<$digest> { + const KEY_TYPE: &'static str = "RSA"; + } + + impl crate::key::SerializeJWK for rsa::pss::BlindedSigningKey<$digest> { + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().to_public_key().parameters() + } + } + + impl crate::algorithms::JoseAlgorithm for rsa::pss::VerifyingKey<$digest> { + const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = + crate::algorithms::AlgorithmIdentifier::$alg; + type Signature = rsa::pss::Signature; + } + + impl crate::algorithms::JoseDigestAlgorithm for rsa::pss::VerifyingKey<$digest> { + type Digest = $digest; + } + + impl crate::key::JWKeyType for rsa::pss::VerifyingKey<$digest> { + const KEY_TYPE: &'static str = "RSA"; + } + + impl crate::key::SerializeJWK for rsa::pss::VerifyingKey<$digest> { + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().parameters() + } + } + }; +} + +jose_rsa_pss_algorithm!(PS256, sha2::Sha256); +jose_rsa_pss_algorithm!(PS384, sha2::Sha384); +jose_rsa_pss_algorithm!(PS512, sha2::Sha512); #[cfg(test)] mod test { - use crate::algorithms::SigningAlgorithm; - use crate::key::jwk_reader::rsa; - use super::*; + use crate::algorithms::TokenSigner; + use crate::key::jwk_reader::rsa; use base64ct::Encoding; use serde_json::json; @@ -226,9 +196,10 @@ mod test { let header = strip_whitespace("eyJhbGciOiJSUzI1NiJ9"); - let algorithm: RsaPkcs1v15 = RsaPkcs1v15::new_with_prefix(pkey); + let algorithm: rsa::pkcs1v15::SigningKey = + rsa::pkcs1v15::SigningKey::new_with_prefix(pkey); - let signature = algorithm.sign(&header, &payload).unwrap(); + let signature = algorithm.sign_token(&header, &payload); let sig = base64ct::Base64UrlUnpadded::encode_string(signature.as_ref()); diff --git a/src/base64data.rs b/src/base64data.rs index e4354e7..750940c 100644 --- a/src/base64data.rs +++ b/src/base64data.rs @@ -35,18 +35,20 @@ pub enum DecodeError { /// Wrapper type to indicate that the inner type should be serialized /// as bytes with a Base64 URL-safe encoding. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Base64Data(pub T); +pub struct Base64Signature(pub T); -impl Base64Data +impl Base64Signature where - T: AsRef<[u8]>, + T: signature::SignatureEncoding, { pub(crate) fn serialized_value(&self) -> Result { - Ok(base64ct::Base64UrlUnpadded::encode_string(self.0.as_ref())) + Ok(base64ct::Base64UrlUnpadded::encode_string( + self.0.to_bytes().as_ref(), + )) } } -impl Base64Data +impl Base64Signature where T: TryFrom>, T::Error: std::error::Error + Send + Sync + 'static, @@ -54,19 +56,19 @@ where pub(crate) fn parse(value: &str) -> Result { let data = base64ct::Base64UrlUnpadded::decode_vec(value)?; let data = T::try_from(data).map_err(|err| DecodeError::InvalidData(err.into()))?; - Ok(Base64Data(data)) + Ok(Base64Signature(data)) } } -impl From for Base64Data { +impl From for Base64Signature { fn from(value: T) -> Self { - Base64Data(value) + Base64Signature(value) } } -impl ser::Serialize for Base64Data +impl ser::Serialize for Base64Signature where - T: AsRef<[u8]>, + T: signature::SignatureEncoding, { fn serialize(&self, serializer: S) -> Result where @@ -79,7 +81,7 @@ where } } -impl AsRef<[u8]> for Base64Data +impl AsRef<[u8]> for Base64Signature where T: AsRef<[u8]>, { @@ -94,7 +96,7 @@ impl<'de, T> de::Visitor<'de> for Base64Visitor where T: for<'a> TryFrom<&'a [u8]>, { - type Value = Base64Data; + type Value = Base64Signature; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("base64url encoded data") @@ -109,11 +111,11 @@ where 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)) + Ok(Base64Signature(realized)) } } -impl<'de, T> de::Deserialize<'de> for Base64Data +impl<'de, T> de::Deserialize<'de> for Base64Signature where T: for<'a> TryFrom<&'a [u8]>, { @@ -126,7 +128,7 @@ where } #[cfg(feature = "fmt")] -impl fmt::JWTFormat for Base64Data +impl fmt::JWTFormat for Base64Signature where T: AsRef<[u8]>, { @@ -274,13 +276,15 @@ mod test { use serde_json::{json, Value}; use super::*; + use crate::algorithms::SignatureBytes; #[test] fn test_base64_data() { - let data = Base64Data::from(vec![1, 2, 3, 4]); + let data = Base64Signature::from(SignatureBytes::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(); + let deserialized: Base64Signature = + serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, data); } diff --git a/src/jose/mod.rs b/src/jose/mod.rs index b2abc99..59a4d57 100644 --- a/src/jose/mod.rs +++ b/src/jose/mod.rs @@ -170,10 +170,9 @@ impl Header { } /// Construct the JOSE header from the builder and signing key. - pub(crate) fn into_signed_header(self, key: &A::Key) -> Header> + pub(crate) fn into_signed_header(self, key: &A) -> Header> where - A: crate::algorithms::SigningAlgorithm, - A::Key: Clone, + A: crate::algorithms::TokenSigner + crate::key::SerializeJWK + Clone, { let state = SignedHeader { algorithm: A::IDENTIFIER, @@ -245,9 +244,9 @@ impl Header { /// /// If the key algorithm does not match the header's algorithm. #[allow(unused_variables)] - pub(crate) fn into_signed_header(self, key: &A::Key) -> Header> + pub(crate) fn into_signed_header(self, key: &A) -> Header> where - A: crate::algorithms::VerifyAlgorithm, + A: crate::algorithms::TokenVerifier + crate::key::SerializeJWK, { if *self.algorithm() != A::IDENTIFIER { panic!( diff --git a/src/token/formats.rs b/src/token/formats.rs index 7d3f141..ae02216 100644 --- a/src/token/formats.rs +++ b/src/token/formats.rs @@ -3,11 +3,12 @@ use std::fmt::Write; use bytes::Bytes; use serde::de::DeserializeOwned; use serde::{ser, Deserialize, Serialize}; +use signature::SignatureEncoding; use super::{HasSignature, MaybeSigned, Unverified}; use super::{Payload, Token}; use crate::algorithms::SignatureBytes; -use crate::base64data::{Base64Data, Base64JSON, DecodeError}; +use crate::base64data::{Base64JSON, Base64Signature, DecodeError}; use crate::jose::{HeaderState, RenderedHeader}; use crate::Header; @@ -136,7 +137,10 @@ impl TokenFormat for Compact { { let header = Base64JSON(&token.state.header()).serialized_value()?; let payload = token.payload.serialized_value()?; - let signature = Base64Data(token.state.signature()).serialized_value()?; + let signature = Base64Signature(SignatureBytes::from( + token.state.signature().to_bytes().as_ref(), + )) + .serialized_value()?; write!(writer, "{}.{}.{}", header, payload, signature)?; Ok(()) } @@ -168,8 +172,8 @@ impl TokenFormat for Compact { let signature = { let signature = parts.next().ok_or(TokenParseError::MissingSignature)?; - let signature: Base64Data = - Base64Data::parse(std::str::from_utf8(signature)?)?; + let signature: Base64Signature = + Base64Signature::parse(std::str::from_utf8(signature)?)?; signature }; @@ -209,7 +213,10 @@ where ::HeaderState: HeaderState, { let header = Base64JSON(token.state.header()).serialized_value()?; - let signature = Base64Data(token.state.signature()).serialized_value()?; + let signature = Base64Signature(SignatureBytes::from( + token.state.signature().to_bytes().as_ref(), + )) + .serialized_value()?; let flat = FlatToken { payload: &token.payload, @@ -268,9 +275,11 @@ where let header = Base64JSON(self.state.header()) .serialized_value() .map_err(ser::Error::custom)?; - let signature = Base64Data(self.state.signature()) - .serialized_value() - .map_err(ser::Error::custom)?; + let signature = Base64Signature(SignatureBytes::from( + self.state.signature().to_bytes().as_ref(), + )) + .serialized_value() + .map_err(ser::Error::custom)?; let flat = FlatToken { payload: &self.payload, @@ -303,7 +312,10 @@ impl TokenFormat for Flat { ::HeaderState: HeaderState, { let header = Base64JSON(token.state.header()).serialized_value()?; - let signature = Base64Data(token.state.signature()).serialized_value()?; + let signature = Base64Signature(SignatureBytes::from( + token.state.signature().to_bytes().as_ref(), + )) + .serialized_value()?; let flat = FlatSimpleToken { payload: &token.payload, @@ -368,8 +380,8 @@ where let signature = object .remove("signature") .ok_or(TokenParseError::MissingSignature)?; - let signature: Base64Data = - Base64Data::parse(signature.as_str().ok_or_else(|| { + let signature: Base64Signature = + Base64Signature::parse(signature.as_str().ok_or_else(|| { TokenParseError::UnexpectedJSONValue("signature", signature.clone()) })?)?; signature @@ -400,9 +412,11 @@ where let header = Base64JSON(self.state.header()) .serialized_value() .map_err(ser::Error::custom)?; - let signature = Base64Data(self.state.signature()) - .serialized_value() - .map_err(ser::Error::custom)?; + let signature = Base64Signature(SignatureBytes::from( + self.state.signature().to_bytes().as_ref(), + )) + .serialized_value() + .map_err(ser::Error::custom)?; let flat = FlatSimpleToken { payload: &self.payload, diff --git a/src/token/mod.rs b/src/token/mod.rs index 70c263e..46e0eeb 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -17,13 +17,15 @@ use serde::{ de::{self, DeserializeOwned}, ser, Deserialize, Serialize, }; +use signature::SignatureEncoding; -use crate::algorithms::VerifyAlgorithm; +use crate::algorithms::TokenVerifier; #[cfg(feature = "fmt")] use crate::fmt; +use crate::key::SerializeJWK; use crate::{ - algorithms::{AlgorithmIdentifier, SigningAlgorithm}, - base64data::{Base64Data, Base64JSON, DecodeError}, + algorithms::{AlgorithmIdentifier, TokenSigner}, + base64data::{Base64JSON, Base64Signature, DecodeError}, jose::{HeaderAccess, HeaderAccessMut, HeaderState}, Header, }; @@ -352,20 +354,16 @@ where /// Once the signature is attached, the internal fields are no longer mutable (as that /// would invalidate the signature), but they are still recoverable. #[allow(clippy::type_complexity)] - pub fn sign( - self, - algorithm: &A, - ) -> Result, Fmt>, TokenSigningError> + pub fn sign(self, algorithm: &A) -> Result, Fmt>, TokenSigningError> where - A: crate::algorithms::SigningAlgorithm, - A::Key: Clone, + A: crate::algorithms::TokenSigner + SerializeJWK + Clone, // A::Signature: Serialize, { - let header = self.state.header.into_signed_header::(algorithm.key()); + let header = self.state.header.into_signed_header(algorithm); let headers = Base64JSON(&header).serialized_value()?; let payload = self.payload.serialized_value()?; let signature = algorithm - .sign(&headers, &payload) + .try_sign_token(&headers, &payload) .map_err(TokenSigningError::Signing)?; Ok(Token { payload: self.payload, @@ -391,10 +389,9 @@ where pub fn verify( self, algorithm: &A, - ) -> Result, Fmt>, TokenVerifyingError> + ) -> Result, Fmt>, TokenVerifyingError> where - A: crate::algorithms::VerifyAlgorithm, - A::Key: Clone, + A: crate::algorithms::TokenVerifier + SerializeJWK, P: Serialize, H: Serialize, { @@ -407,14 +404,14 @@ where let signature = &self.state.signature; let signature = algorithm - .verify( + .verify_token( &self.state.header.state.raw, &self.state.payload, signature.as_ref(), ) .map_err(TokenVerifyingError::Verify)?; - let header = self.state.header.into_signed_header::(algorithm.key()); + let header = self.state.header.into_signed_header::(algorithm); Ok(Token { payload: self.payload, @@ -440,8 +437,7 @@ where impl Token, Fmt> where Fmt: TokenFormat, - Alg: SigningAlgorithm, - Alg::Key: Clone, + Alg: TokenSigner + SerializeJWK + Clone, H: Serialize, P: Serialize, { @@ -459,7 +455,7 @@ where state: Unverified { payload, header: self.state.header.into_rendered_header(), - signature: Base64Data(self.state.signature.as_ref().to_owned().into()), + signature: Base64Signature(self.state.signature.to_bytes().as_ref().into()), }, fmt: self.fmt, } @@ -469,7 +465,7 @@ where impl Token, Fmt> where Fmt: TokenFormat, - Alg: SigningAlgorithm, + Alg: TokenSigner + SerializeJWK, { /// Get the payload of the token. pub fn payload(&self) -> Option<&P> { @@ -483,8 +479,7 @@ where impl Token, Fmt> where Fmt: TokenFormat, - Alg: VerifyAlgorithm, - Alg::Key: Clone, + Alg: TokenVerifier + SerializeJWK + Clone, H: Serialize, P: Serialize, { @@ -502,7 +497,7 @@ where state: Unverified { payload, header: self.state.header.into_rendered_header(), - signature: Base64Data(self.state.signature.as_ref().to_owned().into()), + signature: Base64Signature(self.state.signature.to_bytes().as_ref().into()), }, fmt: self.fmt, } @@ -512,7 +507,7 @@ where impl Token, Fmt> where Fmt: TokenFormat, - Alg: VerifyAlgorithm, + Alg: TokenVerifier + SerializeJWK, { /// Get the payload of the token. pub fn payload(&self) -> Option<&P> { @@ -535,7 +530,8 @@ where fn fmt(&self, f: &mut fmt::IndentWriter<'_, W>) -> std::fmt::Result { let header = serde_json::to_value(self.state.header()).unwrap(); let payload = serde_json::to_value(&self.payload).unwrap(); - let signature = serde_json::to_value(Base64Data(self.state.signature())).unwrap(); + let signature = + serde_json::to_value(Base64Signature(self.state.signature().clone())).unwrap(); let token = serde_json::json!({ "header": header, @@ -579,11 +575,11 @@ where /// An error which occured while verifying a token. #[derive(Debug, thiserror::Error)] -pub enum TokenVerifyingError { +pub enum TokenVerifyingError { /// The verification failed during the cryptographic process, meaning /// that the signature was invalid, or the algorithm was invalid. #[error("verifying: {0}")] - Verify(E), + Verify(signature::Error), /// An error occured while re-serailizing the header or payload for /// signature verification. This indicates that something is probably @@ -599,11 +595,11 @@ pub enum TokenVerifyingError { /// An error which occured while verifying a token. #[derive(Debug, thiserror::Error)] -pub enum TokenSigningError { +pub enum TokenSigningError { /// The verification failed during the cryptographic process, meaning /// that the signature was invalid, or the algorithm was invalid. #[error("signing: {0}")] - Signing(E), + Signing(signature::Error), /// An error occured while serailizing the header or payload for /// signature computation. This indicates that something is probably @@ -687,8 +683,8 @@ mod test_rsa { claims.registered.issuer = Some("joe".into()); let token = Token::new((), claims, Compact::new()); - let algorithm: crate::algorithms::rsa::RsaPkcs1v15 = - crate::algorithms::rsa::RsaPkcs1v15::new_with_prefix(pkey); + let algorithm: rsa::pkcs1v15::SigningKey = + rsa::pkcs1v15::SigningKey::new_with_prefix(pkey); let signed = token.sign(&algorithm).unwrap(); { let hdr = base64ct::Base64UrlUnpadded::encode_string( @@ -741,7 +737,8 @@ mod test_ecdsa { use super::*; use base64ct::Encoding; - use elliptic_curve::{FieldBytes, SecretKey}; + use ecdsa::SigningKey; + use elliptic_curve::FieldBytes; use serde_json::json; use zeroize::Zeroize; @@ -749,12 +746,12 @@ mod test_ecdsa { s.chars().filter(|c| !c.is_whitespace()).collect() } - fn ecdsa(jwk: &serde_json::Value) -> SecretKey { + fn ecdsa(jwk: &serde_json::Value) -> SigningKey { let d_b64 = strip_whitespace(jwk["d"].as_str().unwrap()); let mut d_bytes = FieldBytes::::default(); base64ct::Base64UrlUnpadded::decode(&d_b64, &mut d_bytes).unwrap(); - let key = SecretKey::from_slice(&d_bytes).unwrap(); + let key = SigningKey::from_slice(&d_bytes).unwrap(); d_bytes.zeroize(); key } @@ -775,9 +772,9 @@ mod test_ecdsa { let signed = token.sign(&key).unwrap(); - let verifying_key: ecdsa::VerifyingKey<_> = key.public_key().into(); + let verifying_key = key.verifying_key(); - let verified = signed.unverify().verify(&verifying_key).unwrap(); + let verified = signed.unverify().verify(verifying_key).unwrap(); assert_eq!(verified.payload(), Some(&"This is a signed message")); } @@ -819,10 +816,10 @@ mod test_hmac { let token = Token::compact((), "This is an HMAC'd message"); - let signed = token.sign(&algorithm).unwrap(); + // let signed = token.sign(&algorithm).unwrap(); - let verified = signed.unverify().verify(&algorithm).unwrap(); + // let verified = signed.unverify().verify(&algorithm).unwrap(); - assert_eq!(verified.payload(), Some(&"This is an HMAC'd message")); + // assert_eq!(verified.payload(), Some(&"This is an HMAC'd message")); } } diff --git a/src/token/state.rs b/src/token/state.rs index 3112ac6..b917ea4 100644 --- a/src/token/state.rs +++ b/src/token/state.rs @@ -1,10 +1,12 @@ use bytes::Bytes; use serde::Serialize; +use signature::SignatureEncoding; use crate::{ - algorithms::{SignatureBytes, SigningAlgorithm, VerifyAlgorithm}, - base64data::Base64Data, + algorithms::{JoseAlgorithm, SignatureBytes, TokenSigner}, + base64data::Base64Signature, jose, + key::SerializeJWK, }; /// A trait used to represent the state of a token with respect to @@ -44,7 +46,7 @@ pub trait MaybeSigned { pub trait HasSignature: MaybeSigned { /// The type of the signature, which should be representable /// as a byte slice. - type Signature: AsRef<[u8]>; + type Signature: SignatureEncoding; /// Get a reference to the signature. fn signature(&self) -> &Self::Signature; @@ -85,18 +87,18 @@ impl MaybeSigned for Unsigned { /// This state is used when this program applied the signature, so we know that the /// signature is both consistent and valid. #[derive(Debug, Clone, Serialize)] -#[serde(bound(serialize = "H: Serialize, Alg::Signature: Serialize, Alg::Key: Clone",))] +#[serde(bound(serialize = "H: Serialize, Alg: Clone, Alg::Signature: Serialize",))] pub struct Signed where - Alg: SigningAlgorithm, + Alg: JoseAlgorithm + SerializeJWK, { - pub(super) header: jose::Header>, + pub(super) header: jose::Header>, pub(super) signature: Alg::Signature, } impl HasSignature for Signed where - Alg: SigningAlgorithm, + Alg: TokenSigner + SerializeJWK, { type Signature = Alg::Signature; @@ -107,9 +109,9 @@ where impl MaybeSigned for Signed where - Alg: SigningAlgorithm, + Alg: TokenSigner + SerializeJWK, { - type HeaderState = jose::SignedHeader; + type HeaderState = jose::SignedHeader; type Header = H; fn header(&self) -> &jose::Header { @@ -136,20 +138,20 @@ where /// we did not create the token, and modifying it may result in headers which are not /// consistent with the signature. #[derive(Debug, Clone, Serialize)] -#[serde(bound(serialize = "H: Serialize, Alg::Signature: Serialize, Alg::Key: Clone",))] +#[serde(bound(serialize = "H: Serialize, Alg: Clone, Alg::Signature: Serialize",))] pub struct Verified where - Alg: VerifyAlgorithm, + Alg: JoseAlgorithm + SerializeJWK, { - pub(super) header: jose::Header>, + pub(super) header: jose::Header>, pub(super) signature: Alg::Signature, } impl MaybeSigned for Verified where - Alg: VerifyAlgorithm, + Alg: JoseAlgorithm + SerializeJWK, { - type HeaderState = jose::SignedHeader; + type HeaderState = jose::SignedHeader; type Header = H; fn header_mut(&mut self) -> &mut jose::Header { @@ -171,7 +173,7 @@ where impl HasSignature for Verified where - Alg: VerifyAlgorithm, + Alg: JoseAlgorithm + SerializeJWK, { type Signature = Alg::Signature; @@ -188,7 +190,7 @@ where pub struct Unverified { pub(super) payload: Bytes, pub(super) header: jose::Header, - pub(super) signature: Base64Data, + pub(super) signature: Base64Signature, } impl MaybeSigned for Unverified { From 1317f9a5a3b09e95b78ead63ca603526c05d2727 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Sat, 25 Nov 2023 17:41:54 +0000 Subject: [PATCH 2/2] Support algorithm traits with HMAC --- Cargo.toml | 2 +- justfile | 4 +++ src/algorithms/ecdsa.rs | 13 ++----- src/algorithms/hmac.rs | 80 +++++++++++++++++++++++++++++++++++------ src/algorithms/mod.rs | 54 ++++++++++++++++++++++++++++ src/algorithms/rsa.rs | 8 +++-- src/token/mod.rs | 6 ++-- 7 files changed, 140 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9212a97..1a45cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ p521 = { version = "0.13.0", optional = true } hmac = { version = "0.12.1", features = ["reset"], optional = true } [features] -default = ["fmt", "rsa", "ecdsa", "p256", "p384", "p521"] +default = ["fmt", "rsa", "ecdsa", "p256", "p384", "p521", "hmac"] fmt = [] rsa = ["dep:rsa"] hmac = ["dep:hmac"] diff --git a/justfile b/justfile index efb9b0a..345d6a6 100644 --- a/justfile +++ b/justfile @@ -4,6 +4,10 @@ test: @echo "Running tests..." cargo hack test --feature-powerset --group-features ecdsa,p256,p384,p521 +check: + @echo "Checking..." + cargo hack check --feature-powerset --group-features ecdsa,p256,p384,p521 + doc: @echo "Building docs..." cargo doc --all-features diff --git a/src/algorithms/ecdsa.rs b/src/algorithms/ecdsa.rs index e7aa0e8..aaea18c 100644 --- a/src/algorithms/ecdsa.rs +++ b/src/algorithms/ecdsa.rs @@ -2,7 +2,7 @@ //! //! This module provides implementations of the ECDSA signing algorithms for use with JSON Web Tokens. //! -//! It uses [elliptic_curve::SecretKey] as the base type, and provides implementations of the ECDSA +//! It uses [ecdsa::SigningKey] and [ecdsa::VerifyingKey] as the key types, and provides implementations of the ECDSA //! signing algorithms for the following curves: //! - P-256 (ES256) //! - P-384 (ES384) @@ -66,7 +66,8 @@ println!("{}", signed.formatted()); "# )] -use ::ecdsa::{hazmat::SignPrimitive, PrimeCurve, SignatureSize, SigningKey, VerifyingKey}; +use ::ecdsa::{hazmat::SignPrimitive, PrimeCurve, SignatureSize}; +pub use ::ecdsa::{SigningKey, VerifyingKey}; use base64ct::Encoding; use digest::generic_array::ArrayLength; use elliptic_curve::{ @@ -88,10 +89,6 @@ pub use p521::NistP521; impl crate::key::JWKeyType for VerifyingKey where C: PrimeCurve + CurveArithmetic + JwkParameters, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, { const KEY_TYPE: &'static str = "EC"; } @@ -99,8 +96,6 @@ where impl crate::key::SerializeJWK for VerifyingKey where C: PrimeCurve + CurveArithmetic + JwkParameters, - Scalar: Invert>> + SignPrimitive, - SignatureSize: ArrayLength, AffinePoint: FromEncodedPoint + ToEncodedPoint, FieldBytesSize: ModulusSize, { @@ -146,8 +141,6 @@ where C: PrimeCurve + CurveArithmetic + JwkParameters, Scalar: Invert>> + SignPrimitive, SignatureSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, { const KEY_TYPE: &'static str = "EC"; } diff --git a/src/algorithms/hmac.rs b/src/algorithms/hmac.rs index 506002a..6154e51 100644 --- a/src/algorithms/hmac.rs +++ b/src/algorithms/hmac.rs @@ -1,16 +1,35 @@ //! HMAC Signing algorithms for use with JWS //! //! Based on the [hmac](https://crates.io/crates/hmac) crate. +//! +//! ## Usage +//! +//! Unlike the other algorithms, HMAC algorithms use special types provided +//! by the JOSE crate. HMAC is not really a "signature" algorithm in a strict sense, +//! as it does not use a private key. Instead, it uses a symmetric key, which is +//! shared between the signer and verifier. +//! +//! The [`HmacKey`] type is used to represent the key. This type is a wrapper around +//! a byte vector, and can be created from any type which can be converted into a +//! byte vector. The [`Hmac`] type is used to represent the algorithm, and is a wrapper +//! which combines the key with the digest algorithm. +//! +//! The [`Hmac`] type implements [`TokenSigner`][crate::algorithms::TokenSigner] and [`TokenVerifier`][crate::algorithms::TokenVerifier], and can be used +//! to sign and verify tokens. Signatures are represented by the [`DigestSignature`] type, +//! which is a wrapper around the [`digest::Output`] type from the [`digest`](https://crates.io/crates/digest) +//! crate, but which provides the appropriate signature encoding behavior. use std::{marker::PhantomData, ops::Deref}; use base64ct::Encoding; -use digest::Digest; +use digest::{Digest, Mac}; use hmac::SimpleHmac; use signature::SignatureEncoding; use crate::key::{JWKeyType, SerializeJWK}; +use super::JoseAlgorithm; + /// A key used to seed an HMAC signature. #[derive(Debug, Clone, PartialEq, Eq, Hash, zeroize::Zeroize, zeroize::ZeroizeOnDrop, Default)] pub struct HmacKey { @@ -143,10 +162,6 @@ macro_rules! hmac_algorithm { crate::algorithms::AlgorithmIdentifier::$alg; type Signature = DigestSignature<$digest>; } - - impl crate::algorithms::JoseDigestAlgorithm for Hmac<$digest> { - type Digest = $digest; - } }; } @@ -154,6 +169,49 @@ hmac_algorithm!(HS256, sha2::Sha256); hmac_algorithm!(HS384, sha2::Sha384); hmac_algorithm!(HS512, sha2::Sha512); +impl super::TokenSigner for Hmac +where + Hmac: JoseAlgorithm>, + D: Digest + digest::core_api::BlockSizeUser, +{ + fn try_sign_token( + &self, + header: &str, + payload: &str, + ) -> Result { + let mut mac: SimpleHmac = + SimpleHmac::new_from_slice(self.key.as_ref()).expect("Valid key"); + mac.update(header.as_bytes()); + mac.update(b"."); + mac.update(payload.as_bytes()); + Ok(DigestSignature(mac.finalize().into_bytes())) + } +} + +impl super::TokenVerifier for Hmac +where + Hmac: JoseAlgorithm>, + D: Digest + digest::core_api::BlockSizeUser + Clone, +{ + fn verify_token( + &self, + header: &[u8], + payload: &[u8], + signature: &[u8], + ) -> Result { + let mut mac: SimpleHmac = + SimpleHmac::new_from_slice(self.key.as_ref()).expect("Valid key"); + mac.update(header); + mac.update(b"."); + mac.update(payload); + mac.clone() + .verify_slice(signature) + .map_err(signature::Error::from_source)?; + + Ok(DigestSignature(mac.finalize().into_bytes())) + } +} + #[cfg(test)] mod test { @@ -195,13 +253,13 @@ mod test { let algorithm: Hmac = Hmac::new(key); - // let signature = algorithm.sign_token(&header, &payload); + let signature = algorithm.sign_token(&header, &payload); - // let sig = base64ct::Base64UrlUnpadded::encode_string(signature.to_bytes().as_ref()); + let sig = base64ct::Base64UrlUnpadded::encode_string(signature.to_bytes().as_ref()); - // assert_eq!( - // sig, - // strip_whitespace("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") - // ); + assert_eq!( + sig, + strip_whitespace("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") + ); } } diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index af3e9db..9ff5368 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -4,6 +4,51 @@ //! //! See the submodules for specific algorithm implementations for signing. //! +//! # Algorithm Traits +//! +//! JOSE uses a few traits to define appropriate signing algorithms. [`JoseAlgorithm`] is the main trait, +//! which defines the algorithm identifier and the type of the signature. [`JoseDigestAlgorithm`] is a +//! subtrait which defines the digest algorithm used by the signature, for algorithms which use digest +//! signing (e.g. RSA-PSS, RSA-PKCS1-v1_5, ECDSA), and where a specific digest is specified by the algorithm +//! identifier. +//! +//! [`TokenSigner`] and [`TokenVerifier`] are traits which define the ability to sign and verify a JWT. +//! They are implemented for any [`JoseDigestAlgorithm`] which is also a [`DigestSigner`][signature::DigestSigner] or [`DigestVerifier`][signature::DigestVerifier]. +//! +//! # Supported Algorithms +//! +//! ## HMAC +//! +//! - HS256: HMAC using SHA-256 +//! - HS384: HMAC using SHA-384 +//! - HS512: HMAC using SHA-512 +//! +//! ## RSA +//! +//! - RS256: RSASSA-PKCS1-v1_5 using SHA-256 +//! - RS384: RSASSA-PKCS1-v1_5 using SHA-384 +//! - RS512: RSASSA-PKCS1-v1_5 using SHA-512 +//! - PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 +//! - PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 +//! +//! ## ECDSA +//! +//! - ES256: ECDSA using P-256 and SHA-256 +//! - ES384: ECDSA using P-384 and SHA-384 +//! +//! # Unsupported algorithms +//! +//! This crate does not support signing or verification with the `none` algorithm, +//! as it is generally a security vulnerability. +//! +//! Currently, there is no support for the following algorithms: +//! +//! - EdDSA: EdDSA using Ed25519 is not yet supported. +//! - ES512: ECDSA using P-521 and SHA-512 is not yet supported. +//! +//! All of these algorithms could be supported by providing suitable implementations +//! of the [`JoseAlgorithm`] trait and the [`TokenSigner`] and [`TokenVerifier`] traits. +//! //! [RFC7518]: https://tools.ietf.org/html/rfc7518 use bytes::Bytes; @@ -11,6 +56,15 @@ use digest::Digest; use serde::{Deserialize, Serialize}; use signature::SignatureEncoding; +#[cfg(any(feature = "p256", feature = "hmac", feature = "rsa"))] +pub use sha2::Sha256; + +#[cfg(any(feature = "p348", feature = "hmac", feature = "rsa"))] +pub use sha2::Sha384; + +#[cfg(any(feature = "hmac", feature = "rsa"))] +pub use sha2::Sha512; + #[cfg(feature = "ecdsa")] pub mod ecdsa; diff --git a/src/algorithms/rsa.rs b/src/algorithms/rsa.rs index f8d05e1..85a1bac 100644 --- a/src/algorithms/rsa.rs +++ b/src/algorithms/rsa.rs @@ -2,8 +2,7 @@ //! //! # PKCS#1 v1.5 (RS256, RS384, RS512) //! This algorithm is used to sign and verify JSON Web Tokens using the RSASSA-PKCS1-v1_5. -//! A [rsa::pkcs1v15::SigningKey] signing key is used to provide the underlying signature -//! algorithm. +//! Use [rsa::pkcs1v15::SigningKey] to sign tokens, and [rsa::pkcs1v15::VerifyingKey] to verify tokens. //! //! A key of size 2048 bits or larger MUST be used with these algorithms. //! @@ -14,10 +13,15 @@ //! //! # PSS (PS256, PS384, PS512) //! This algorithm is used to sign and verify JSON Web Tokens using the RSASSA-PSS. +//! +//! Use [rsa::pss::BlindedSigningKey] to sign tokens, and [rsa::pss::VerifyingKey] to verify tokens. use base64ct::{Base64UrlUnpadded, Encoding}; use rsa::PublicKeyParts; +pub use rsa::pkcs1v15; +pub use rsa::pss; + impl crate::key::JWKeyType for rsa::RsaPublicKey { const KEY_TYPE: &'static str = "RSA"; } diff --git a/src/token/mod.rs b/src/token/mod.rs index 46e0eeb..12983e2 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -816,10 +816,10 @@ mod test_hmac { let token = Token::compact((), "This is an HMAC'd message"); - // let signed = token.sign(&algorithm).unwrap(); + let signed = token.sign(&algorithm).unwrap(); - // let verified = signed.unverify().verify(&algorithm).unwrap(); + let verified = signed.unverify().verify(&algorithm).unwrap(); - // assert_eq!(verified.payload(), Some(&"This is an HMAC'd message")); + assert_eq!(verified.payload(), Some(&"This is an HMAC'd message")); } }