diff --git a/Cargo.toml b/Cargo.toml index ba32b11..19fa154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,9 @@ version = "0.13" features = ["pkcs8", "jwk", "arithmetic", "sec1", "ecdh"] optional = true +[dev-dependencies] +static_assertions = "1.1.0" + [features] default = ["fmt", "rsa", "ecdsa", "p256", "p384", "p521", "hmac"] fmt = [] @@ -60,7 +63,18 @@ name = "rfc7515-a2" path = "examples/rfc7515a2.rs" required-features = ["fmt", "rsa"] +[[example]] +name = "dyn-key" +path = "examples/dyn-key.rs" +required-features = ["fmt", "rsa"] + + [[example]] name = "save-key" path = "examples/save-key.rs" required-features = ["fmt", "rsa"] + +[[test]] +name = "dyn-rsa-key" +path = "tests/dyn-rsa-key.rs" +required-features = ["fmt", "rsa"] diff --git a/README.md b/README.md index d078223..3f59ecc 100644 --- a/README.md +++ b/README.md @@ -157,16 +157,15 @@ fn main() -> Result<(), Box> { // this will derive the JWK field in the header from the signing key. token.header_mut().key().derived(); - println!("Initial JWT"); + println!("=== {} ===", "Initial JWT"); // Initially the JWT has no defined signature: - println!("JWT:"); println!("{}", token.formatted()); // Sign the token with the algorithm, and print the result. - let signed = token.sign(&alg).unwrap(); + let signed = token.sign::<_, rsa::pkcs1v15::Signature>(&alg).unwrap(); - println!("Signed JWT"); + println!("=== {} ===", "Signed JWT"); println!("JWT:"); println!("{}", signed.formatted()); @@ -184,7 +183,7 @@ fn main() -> Result<(), Box> { let token: Token, Unverified<()>, Compact> = signed.rendered().unwrap().parse().unwrap(); - println!("Parsed JWT"); + println!("=== {} ===", "Parsed JWT"); // Unverified tokens can be printed for debugging, but there is deliberately // no access to the payload, only to the header fields. @@ -197,13 +196,18 @@ fn main() -> Result<(), Box> { let key = rsa_jwk_reader::rsa_pub(&serde_json::to_value(jwk).unwrap()); assert_eq!(&key, alg.verifying_key().as_ref()); + println!("=== {} === ", "Verification"); - let alg: rsa::pkcs1v15::VerifyingKey = rsa::pkcs1v15::VerifyingKey::new(key); + // let alg: rsa::pkcs1v15::VerifyingKey = rsa::pkcs1v15::VerifyingKey::new(key); + let alg: rsa::pkcs1v15::VerifyingKey = alg.verifying_key(); // We can't access the claims until we verify the token. - let verified = token.verify(&alg).unwrap(); + // let verified = token.verify::<_, rsa::pkcs1v15::Signature>(&alg).unwrap(); + let verified = token + .verify::<_, jaws::algorithms::SignatureBytes>(&alg) + .unwrap(); - println!("Verified JWT"); + println!("=== {} ===", "Verified JWT"); println!("JWT:"); println!("{}", verified.formatted()); println!( diff --git a/examples/acme-new-account.rs b/examples/acme-new-account.rs index 239ab58..d3c7457 100644 --- a/examples/acme-new-account.rs +++ b/examples/acme-new-account.rs @@ -68,7 +68,7 @@ fn main() { token.header_mut().key().derived(); // Sign the token with the algorithm and key we specified above. - let signed = token.sign(&alg).unwrap(); + let signed = token.sign::<_, rsa::pkcs1v15::Signature>(&alg).unwrap(); // Print the token in the ACME example format. println!("{}", signed.formatted()); diff --git a/examples/dyn-key.rs b/examples/dyn-key.rs new file mode 100644 index 0000000..5828aa9 --- /dev/null +++ b/examples/dyn-key.rs @@ -0,0 +1,148 @@ +use jaws::algorithms::SignatureBytes; +use jaws::algorithms::TokenSigner; +use jaws::algorithms::TokenVerifier; +use jaws::key::SerializeJWK; +use jaws::token::Unverified; +use jaws::Compact; +use jaws::JWTFormat; +use jaws::Token; +use jaws::{Claims, RegisteredClaims}; +use rsa::pkcs8::DecodePrivateKey; +use serde_json::json; +use sha2::Sha256; + +trait TokenSigningKey: TokenSigner + SerializeJWK {} + +impl TokenSigningKey for T where T: TokenSigner + SerializeJWK {} + +fn main() -> Result<(), Box> { + // This key is from RFC 7515, Appendix A.2. Provide your own key instead! + // The key here is stored as a PKCS#8 PEM file, but you can leverage + // RustCrypto to load a variety of other formats. + let key = rsa::RsaPrivateKey::from_pkcs8_pem(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/examples/rfc7515a2.pem" + ))) + .unwrap(); + let verify_key: rsa::pkcs1v15::VerifyingKey = + rsa::pkcs1v15::VerifyingKey::new(key.to_public_key()); + let verify_alg: Box> = Box::new(verify_key.clone()); + let alg: Box = + Box::new(rsa::pkcs1v15::SigningKey::::new(key.clone())); + + // Claims can combine registered and custom fields. The claims object + // can be any type which implements [serde::Serialize]. + let claims: Claims = Claims { + registered: RegisteredClaims { + subject: "1234567890".to_string().into(), + ..Default::default() + }, + claims: json!({ + "name": "John Doe", + "admin": true, + }), + }; + + // Create a token with the default headers, and no custom headers. + // The unit type can be used here because it implements [serde::Serialize], + // but a custom type could be passed if we wanted to have custom header + // fields. + let mut token = Token::compact((), claims); + // We can modify the headers freely before signing the JWT. In this case, + // we provide the `typ` header, which is optional in the JWT spec. + *token.header_mut().r#type() = Some("JWT".to_string()); + + // We can also ask that some fields be derived from the signing key, for example, + // this will derive the JWK field in the header from the signing key. + token.header_mut().key().derived(); + + println!("=== Initial JWT ==="); + + // Initially the JWT has no defined signature: + println!("{}", token.formatted()); + + // Sign the token with the algorithm, and print the result. + let signed = token.sign::<_, SignatureBytes>(alg.as_ref()).unwrap(); + + let rendered = signed.rendered().unwrap(); + + // We can also verify tokens. + let token: Token, Unverified<()>, Compact> = + rendered.parse().unwrap(); + + println!("=== Parsed JWT ==="); + + // Unverified tokens can be printed for debugging, but there is deliberately + // no access to the payload, only to the header fields. + println!("JWT:"); + println!("{}", token.formatted()); + + // We can use the JWK to verify that the token is signed with the correct key. + let hdr = token.header(); + let jwk = hdr.key().unwrap(); + let key: rsa::pkcs1v15::VerifyingKey = rsa::pkcs1v15::VerifyingKey::new( + rsa_jwk_reader::rsa_pub(&serde_json::to_value(jwk).unwrap()), + ); + + println!("=== Verification === "); + // Check it against the verified key + token + .clone() + .verify::<_, rsa::pkcs1v15::Signature>(&verify_key) + .unwrap(); + println!("Verified with dyn verify key (typed)"); + + // Check it against the verified key + token + .clone() + .verify::<_, SignatureBytes>(verify_alg.as_ref()) + .unwrap(); + println!("Verified with dyn verify key"); + + // Check it against its own JWT + token + .clone() + .verify::<_, rsa::pkcs1v15::Signature>(&key) + .unwrap(); + println!("Verified with JWT"); + + // We can't access the claims until we verify the token. + let verified = token + .verify::<_, SignatureBytes>(verify_alg.as_ref()) + .unwrap(); + println!("Verified with original key"); + + println!("=== Verified JWT ==="); + println!("JWT:"); + println!("{}", verified.formatted()); + println!( + "Payload: \n{}", + serde_json::to_string_pretty(&verified.payload()).unwrap() + ); + + Ok(()) +} + +mod rsa_jwk_reader { + use base64ct::Encoding; + + fn strip_whitespace(s: &str) -> String { + s.chars().filter(|c| !c.is_whitespace()).collect() + } + + fn to_biguint(v: &serde_json::Value) -> Option { + let val = strip_whitespace(v.as_str()?); + Some(rsa::BigUint::from_bytes_be( + base64ct::Base64UrlUnpadded::decode_vec(&val) + .ok()? + .as_slice(), + )) + } + + pub(crate) fn rsa_pub(key: &serde_json::Value) -> rsa::RsaPublicKey { + let n = to_biguint(&key["n"]).expect("decode n"); + let e = to_biguint(&key["e"]).expect("decode e"); + + rsa::RsaPublicKey::new(n, e).expect("valid key parameters") + } +} diff --git a/examples/rfc7515a2.rs b/examples/rfc7515a2.rs index 08173a9..8a5291b 100644 --- a/examples/rfc7515a2.rs +++ b/examples/rfc7515a2.rs @@ -74,16 +74,15 @@ fn main() -> Result<(), Box> { // this will derive the JWK field in the header from the signing key. token.header_mut().key().derived(); - println!("Initial JWT"); + println!("=== Initial JWT ==="); // Initially the JWT has no defined signature: - println!("JWT:"); println!("{}", token.formatted()); // Sign the token with the algorithm, and print the result. - let signed = token.sign(&alg).unwrap(); + let signed = token.sign::<_, rsa::pkcs1v15::Signature>(&alg).unwrap(); - println!("Signed JWT"); + println!("=== Signed JWT ==="); println!("JWT:"); println!("{}", signed.formatted()); @@ -101,7 +100,7 @@ fn main() -> Result<(), Box> { let token: Token, Unverified<()>, Compact> = signed.rendered().unwrap().parse().unwrap(); - println!("Parsed JWT"); + println!("=== Parsed JWT ==="); // Unverified tokens can be printed for debugging, but there is deliberately // no access to the payload, only to the header fields. @@ -114,13 +113,18 @@ fn main() -> Result<(), Box> { let key = rsa_jwk_reader::rsa_pub(&serde_json::to_value(jwk).unwrap()); assert_eq!(&key, alg.verifying_key().as_ref()); + println!("=== Verification === "); - let alg: rsa::pkcs1v15::VerifyingKey = rsa::pkcs1v15::VerifyingKey::new(key); + // let alg: rsa::pkcs1v15::VerifyingKey = rsa::pkcs1v15::VerifyingKey::new(key); + let alg: rsa::pkcs1v15::VerifyingKey = alg.verifying_key(); // We can't access the claims until we verify the token. - let verified = token.verify(&alg).unwrap(); + // let verified = token.verify::<_, rsa::pkcs1v15::Signature>(&alg).unwrap(); + let verified = token + .verify::<_, jaws::algorithms::SignatureBytes>(&alg) + .unwrap(); - println!("Verified JWT"); + println!("=== Verified JWT ==="); println!("JWT:"); println!("{}", verified.formatted()); println!( diff --git a/src/algorithms/ecdsa.rs b/src/algorithms/ecdsa.rs index aaea18c..f240c5b 100644 --- a/src/algorithms/ecdsa.rs +++ b/src/algorithms/ecdsa.rs @@ -22,6 +22,7 @@ Signing with an ECDSA key: use serde_json::json; use ecdsa::SigningKey; +use ecdsa::Signature; use elliptic_curve::FieldBytes; use base64ct::{Encoding, Base64UrlUnpadded}; @@ -56,7 +57,7 @@ let mut token = Token::compact((), claims); *token.header_mut().r#type() = Some("JWT".to_string()); // Sign the token with the ECDSA key, and print the result. -let signed = token.sign(&key).unwrap(); +let signed = token.sign::<_, Signature<_>>(&key).unwrap(); // Print out the compact form you would use as a token println!("{}", signed.rendered().unwrap()); @@ -69,6 +70,7 @@ println!("{}", signed.formatted()); use ::ecdsa::{hazmat::SignPrimitive, PrimeCurve, SignatureSize}; pub use ::ecdsa::{SigningKey, VerifyingKey}; use base64ct::Encoding; +use bytes::Bytes; use digest::generic_array::ArrayLength; use elliptic_curve::{ ops::Invert, @@ -85,6 +87,17 @@ pub use p384::NistP384; #[cfg(feature = "p521")] pub use p521::NistP521; +use signature::SignatureEncoding; + +impl From<::ecdsa::Signature> for super::SignatureBytes +where + C: PrimeCurve, + ::ecdsa::Signature: SignatureEncoding, +{ + fn from(sig: ::ecdsa::Signature) -> Self { + Self(Bytes::copy_from_slice(sig.to_bytes().as_ref())) + } +} impl crate::key::JWKeyType for VerifyingKey where @@ -147,25 +160,13 @@ where 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>; - } - - impl crate::algorithms::JoseDigestAlgorithm for ecdsa::SigningKey<$curve> { - type Digest = <$curve as ::ecdsa::hazmat::DigestPrimitive>::Digest; - } - - impl crate::algorithms::JoseAlgorithm for ecdsa::VerifyingKey<$curve> { - const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = - crate::algorithms::AlgorithmIdentifier::$alg; - type Signature = ecdsa::Signature<$curve>; - } - - impl crate::algorithms::JoseDigestAlgorithm for ecdsa::VerifyingKey<$curve> { - type Digest = <$curve as ::ecdsa::hazmat::DigestPrimitive>::Digest; - } + $crate::jose_algorithm!( + $alg, + ecdsa::SigningKey<$curve>, + ecdsa::VerifyingKey<$curve>, + <$curve as ::ecdsa::hazmat::DigestPrimitive>::Digest, + ::ecdsa::Signature<$curve> + ); }; } @@ -179,13 +180,20 @@ jose_ecdsa_algorithm!(ES384, NistP384); mod test { use super::*; - use crate::algorithms::TokenSigner; + use crate::{ + algorithms::{TokenSigner, TokenVerifier}, + key::SerializeJWK, + }; use base64ct::Encoding; use elliptic_curve::FieldBytes; use serde_json::json; + use static_assertions as sa; use zeroize::Zeroize; + sa::assert_impl_all!(SigningKey: TokenSigner>, SerializeJWK); + sa::assert_impl_all!(VerifyingKey: TokenVerifier>, SerializeJWK); + fn strip_whitespace(s: &str) -> String { s.chars().filter(|c| !c.is_whitespace()).collect() } @@ -219,7 +227,7 @@ mod test { let header = strip_whitespace("eyJhbGciOiJFUzI1NiJ9"); - let signature = key.sign_token(&header, &payload); + let signature: ::ecdsa::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 diff --git a/src/algorithms/hmac.rs b/src/algorithms/hmac.rs index 6154e51..411fcf9 100644 --- a/src/algorithms/hmac.rs +++ b/src/algorithms/hmac.rs @@ -160,7 +160,6 @@ macro_rules! hmac_algorithm { impl crate::algorithms::JoseAlgorithm for Hmac<$digest> { const IDENTIFIER: crate::algorithms::AlgorithmIdentifier = crate::algorithms::AlgorithmIdentifier::$alg; - type Signature = DigestSignature<$digest>; } }; } @@ -169,16 +168,16 @@ hmac_algorithm!(HS256, sha2::Sha256); hmac_algorithm!(HS384, sha2::Sha384); hmac_algorithm!(HS512, sha2::Sha512); -impl super::TokenSigner for Hmac +impl super::TokenSigner> for Hmac where - Hmac: JoseAlgorithm>, - D: Digest + digest::core_api::BlockSizeUser, + Hmac: JoseAlgorithm, + D: Digest + digest::core_api::BlockSizeUser + Clone, { fn try_sign_token( &self, header: &str, payload: &str, - ) -> Result { + ) -> Result, signature::Error> { let mut mac: SimpleHmac = SimpleHmac::new_from_slice(self.key.as_ref()).expect("Valid key"); mac.update(header.as_bytes()); @@ -188,9 +187,9 @@ where } } -impl super::TokenVerifier for Hmac +impl super::TokenVerifier> for Hmac where - Hmac: JoseAlgorithm>, + Hmac: JoseAlgorithm, D: Digest + digest::core_api::BlockSizeUser + Clone, { fn verify_token( @@ -198,7 +197,7 @@ where header: &[u8], payload: &[u8], signature: &[u8], - ) -> Result { + ) -> Result, signature::Error> { let mut mac: SimpleHmac = SimpleHmac::new_from_slice(self.key.as_ref()).expect("Valid key"); mac.update(header); diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index b5a5a47..cb7672a 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -51,6 +51,9 @@ //! //! [RFC7518]: https://tools.ietf.org/html/rfc7518 +use std::fmt; + +use base64ct::Encoding as _; use bytes::Bytes; use digest::Digest; use serde::{Deserialize, Serialize}; @@ -148,9 +151,6 @@ pub trait JoseAlgorithm { /// /// This is the `alg` field in the JOSE header. const IDENTIFIER: AlgorithmIdentifier; - - /// The type of the signature, which must support encoding. - type Signature: SignatureEncoding; } /// A trait to associate an alogritm identifier with an algorithm. @@ -161,9 +161,6 @@ pub trait JoseAlgorithm { /// This trait does not need to be implemented manually, as it is implemented /// for any type which implements [`JoseAlgorithm`]. pub trait DynJoseAlgorithm { - /// The type of the signature, which must support encoding. - type Signature: SignatureEncoding; - /// The identifier for this algorithm when used in a JWT registered header. fn identifier(&self) -> AlgorithmIdentifier; } @@ -172,8 +169,6 @@ impl DynJoseAlgorithm for T where T: JoseAlgorithm, { - type Signature = T::Signature; - fn identifier(&self) -> AlgorithmIdentifier { T::IDENTIFIER } @@ -188,15 +183,14 @@ pub trait JoseDigestAlgorithm: JoseAlgorithm { /// A trait to represent an algorithm which can sign a JWT. /// /// This trait should apply to signing keys. -pub trait TokenSigner: DynJoseAlgorithm { +pub trait TokenSigner: DynJoseAlgorithm +where + S: SignatureEncoding, +{ /// 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; + 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 @@ -205,21 +199,18 @@ pub trait TokenSigner: DynJoseAlgorithm { /// # Panics /// /// This function will panic if the signature cannot be computed. - fn sign_token(&self, header: &str, payload: &str) -> Self::Signature { + fn sign_token(&self, header: &str, payload: &str) -> S { self.try_sign_token(header, payload).unwrap() } } -impl TokenSigner for K +impl TokenSigner for K where K: JoseDigestAlgorithm, - K: signature::DigestSigner, + K: signature::DigestSigner, + S: SignatureEncoding, { - fn try_sign_token( - &self, - header: &str, - payload: &str, - ) -> Result { + fn try_sign_token(&self, header: &str, payload: &str) -> Result { let message = format!("{}.{}", header, payload); let mut digest = ::Digest::new(); @@ -233,7 +224,10 @@ 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: DynJoseAlgorithm { +pub trait TokenVerifier: DynJoseAlgorithm +where + S: SignatureEncoding, +{ /// Verify the signature of the JWT, when provided with the base64url-encoded header /// and payload. fn verify_token( @@ -241,20 +235,23 @@ pub trait TokenVerifier: DynJoseAlgorithm { header: &[u8], payload: &[u8], signature: &[u8], - ) -> Result; + ) -> Result; } -impl TokenVerifier for K +impl TokenVerifier for K where - K: JoseDigestAlgorithm, - K: signature::DigestVerifier, + K: JoseDigestAlgorithm + std::fmt::Debug, + K: signature::DigestVerifier, + K::Digest: Clone + std::fmt::Debug, + S: SignatureEncoding + std::fmt::Debug, + for<'a> >::Error: std::error::Error + Send + Sync + 'static, { fn verify_token( &self, header: &[u8], payload: &[u8], signature: &[u8], - ) -> Result { + ) -> Result { let mut digest = ::Digest::new(); digest.update(header); digest.update(b"."); @@ -262,7 +259,7 @@ where let signature = signature .try_into() - .map_err(|_| signature::Error::default())?; + .map_err(signature::Error::from_source)?; self.verify_digest(digest, &signature)?; Ok(signature) @@ -274,9 +271,17 @@ where /// 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)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct SignatureBytes(Bytes); +impl fmt::Debug for SignatureBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("SignatureBytes") + .field(&base64ct::Base64UrlUnpadded::encode_string(self.0.as_ref())) + .finish() + } +} + impl AsRef<[u8]> for SignatureBytes { fn as_ref(&self) -> &[u8] { &self.0 @@ -310,3 +315,79 @@ impl From> for SignatureBytes { impl signature::SignatureEncoding for SignatureBytes { type Repr = Bytes; } + +/// A macro to implement the required traits for common JWS alogorithms. +#[macro_export] +macro_rules! jose_algorithm { + ($alg:ident, $signer:ty, $verifier:ty, $digest:ty, $signature:ty) => { + impl $crate::algorithms::JoseAlgorithm for $signer { + const IDENTIFIER: $crate::algorithms::AlgorithmIdentifier = + $crate::algorithms::AlgorithmIdentifier::$alg; + } + + impl $crate::algorithms::JoseDigestAlgorithm for $signer { + type Digest = $digest; + } + + impl signature::DigestSigner<$digest, $crate::algorithms::SignatureBytes> for $signer { + fn try_sign_digest( + &self, + digest: $digest, + ) -> Result<$crate::algorithms::SignatureBytes, signature::Error> { + #[allow(unused_imports)] + use signature::SignatureEncoding as _; + + let sig = >::sign_digest( + self, digest, + ); + Ok($crate::algorithms::SignatureBytes::from( + sig.to_bytes().as_ref(), + )) + } + } + + impl $crate::algorithms::JoseAlgorithm for $verifier { + const IDENTIFIER: $crate::algorithms::AlgorithmIdentifier = + $crate::algorithms::AlgorithmIdentifier::$alg; + } + + impl $crate::algorithms::JoseDigestAlgorithm for $verifier { + type Digest = $digest; + } + + impl signature::DigestVerifier<$digest, $crate::algorithms::SignatureBytes> for $verifier { + fn verify_digest( + &self, + digest: $digest, + signature: &$crate::algorithms::SignatureBytes, + ) -> Result<(), signature::Error> { + #[allow(unused_imports)] + use signature::SignatureEncoding as _; + + let sig: $signature = signature + .to_bytes() + .as_ref() + .try_into() + .map_err(|error| signature::Error::from_source(error))?; + + >::verify_digest( + self, digest, &sig, + ) + } + } + }; +} + +#[cfg(test)] +mod test { + use super::*; + + use static_assertions as sa; + + // NOTE: The test requires an explicit value for the signature + // associated type, and we use `SignatureBytes` for this. + // it is assumed that external dependencies will provide either + // a concrete `Signature` type, or an object-safe trait. + sa::assert_obj_safe!(TokenSigner); + sa::assert_obj_safe!(TokenVerifier); +} diff --git a/src/algorithms/rsa.rs b/src/algorithms/rsa.rs index e8cd4f9..2571efe 100644 --- a/src/algorithms/rsa.rs +++ b/src/algorithms/rsa.rs @@ -49,47 +49,47 @@ impl crate::key::SerializeJWK for rsa::RsaPrivateKey { } } +impl crate::key::JWKeyType for rsa::pkcs1v15::SigningKey +where + D: signature::digest::Digest, +{ + const KEY_TYPE: &'static str = "RSA"; +} + +impl crate::key::SerializeJWK for rsa::pkcs1v15::SigningKey +where + D: signature::digest::Digest, +{ + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().to_public_key().parameters() + } +} + +impl crate::key::JWKeyType for rsa::pkcs1v15::VerifyingKey +where + D: signature::digest::Digest, +{ + const KEY_TYPE: &'static str = "RSA"; +} + +impl crate::key::SerializeJWK for rsa::pkcs1v15::VerifyingKey +where + D: signature::digest::Digest, +{ + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().parameters() + } +} + 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() - } - } + $crate::jose_algorithm!( + $alg, + rsa::pkcs1v15::SigningKey<$digest>, + rsa::pkcs1v15::VerifyingKey<$digest>, + $digest, + rsa::pkcs1v15::Signature + ); }; } @@ -97,53 +97,53 @@ 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() - } - } - }; +impl crate::key::JWKeyType for rsa::pss::BlindedSigningKey +where + D: signature::digest::Digest, +{ + const KEY_TYPE: &'static str = "RSA"; +} + +impl crate::key::SerializeJWK for rsa::pss::BlindedSigningKey +where + D: signature::digest::Digest, +{ + fn parameters(&self) -> Vec<(String, serde_json::Value)> { + self.as_ref().to_public_key().parameters() + } +} + +impl crate::key::JWKeyType for rsa::pss::VerifyingKey +where + D: signature::digest::Digest, +{ + const KEY_TYPE: &'static str = "RSA"; +} + +impl crate::key::SerializeJWK for rsa::pss::VerifyingKey +where + D: signature::digest::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); +// 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(test)] mod test { @@ -154,7 +154,7 @@ mod test { use base64ct::Encoding as _; use serde_json::json; use sha2::Sha256; - use signature::SignatureEncoding as _; + use signature::SignatureEncoding; fn strip_whitespace(s: &str) -> String { s.chars().filter(|c| !c.is_whitespace()).collect() @@ -203,7 +203,7 @@ mod test { let algorithm: rsa::pkcs1v15::SigningKey = rsa::pkcs1v15::SigningKey::new(pkey); - let signature = algorithm.sign_token(&header, &payload); + let signature: rsa::pkcs1v15::Signature = algorithm.sign_token(&header, &payload); let sig = base64ct::Base64UrlUnpadded::encode_string(signature.to_bytes().as_ref()); diff --git a/src/base64data.rs b/src/base64data.rs index 750940c..189ac32 100644 --- a/src/base64data.rs +++ b/src/base64data.rs @@ -34,7 +34,7 @@ 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)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Base64Signature(pub T); impl Base64Signature @@ -48,6 +48,17 @@ where } } +impl std::fmt::Debug for Base64Signature +where + T: signature::SignatureEncoding, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Base64Signature") + .field(&self.serialized_value().unwrap()) + .finish() + } +} + impl Base64Signature where T: TryFrom>, diff --git a/src/jose/derive.rs b/src/jose/derive.rs index fa1c7bc..fdfbf94 100644 --- a/src/jose/derive.rs +++ b/src/jose/derive.rs @@ -1,12 +1,13 @@ -use serde::{ser, Serialize}; +use serde::Serialize; use serde_json::json; +use signature::Error as SignatureError; -use crate::key::KeyDerivedBuilder; +use crate::key::BuildFromKey; /// A builder for the registered JOSE header fields for using JWTs, /// when those fields are derived from the signing key. -#[derive(Debug, Clone, Default)] -pub enum KeyDerivation { +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum DeriveFromKey { /// Omit this value from the rendered JOSE header. #[default] Omit, @@ -18,7 +19,7 @@ pub enum KeyDerivation { Explicit(Value), } -impl KeyDerivation { +impl DeriveFromKey { /// Set this field to be omitteded from the rendered JOSE header. pub fn omit(&mut self) { *self = Self::Omit; @@ -35,126 +36,32 @@ impl KeyDerivation { } } -impl KeyDerivation +impl DeriveFromKey where Value: Serialize, { - pub(super) fn parameter(&self, key: &str) -> Option { + pub(super) fn parameter( + &self, + key: &str, + ) -> Result, serde_json::Error> { match self { - KeyDerivation::Omit => None, - KeyDerivation::Derived => Some(json!(format!("<{key}>"))), - KeyDerivation::Explicit(value) => serde_json::to_value(value).ok(), + DeriveFromKey::Omit => Ok(None), + DeriveFromKey::Derived => Ok(Some(json!(format!("<{key}>")))), + DeriveFromKey::Explicit(value) => Ok(Some(serde_json::to_value(value)?)), } } } -/// A builder for the registered JOSE header fields for using JWTs. -/// -/// Some header values must be set explicitly, while others can -/// be derived from the signing key. This type helps to keep track -/// of that distinction, allowing a field to be marked as derived -/// from the signing key. -#[derive(Debug, Default)] -pub(super) enum DerivedKeyValue -where - Builder: KeyDerivedBuilder, -{ - #[default] - Omit, - Derived(Key), - Explicit(Builder::Value), -} - -impl Clone for DerivedKeyValue -where - Builder: KeyDerivedBuilder, - >::Value: Clone, - Key: Clone, -{ - fn clone(&self) -> Self { - match self { - Self::Omit => Self::Omit, - Self::Derived(key) => Self::Derived(key.clone()), - Self::Explicit(value) => Self::Explicit(value.clone()), - } - } -} - -impl DerivedKeyValue -where - Builder: KeyDerivedBuilder, -{ - pub(super) fn is_none(&self) -> bool { - matches!(self, DerivedKeyValue::Omit) - } - - pub(super) fn build(self) -> Option { - match self { - DerivedKeyValue::Omit => None, - DerivedKeyValue::Derived(key) => Some(Builder::from(key).build()), - DerivedKeyValue::Explicit(value) => Some(value), - } - } - - pub(super) fn derive(derivation: KeyDerivation, key: &Key) -> Self +impl DeriveFromKey { + pub(super) fn render(self, key: &K) -> Result, SignatureError> where - Key: Clone, + Value: BuildFromKey, + K: ?Sized, { - match derivation { - KeyDerivation::Omit => DerivedKeyValue::Omit, - KeyDerivation::Derived => DerivedKeyValue::Derived(key.clone()), - KeyDerivation::Explicit(value) => DerivedKeyValue::Explicit(value), - } - } - - pub(super) fn explicit(value: Option) -> Self { - match value { - Some(value) => DerivedKeyValue::Explicit(value), - None => DerivedKeyValue::Omit, - } - } -} - -impl ser::Serialize for DerivedKeyValue -where - Builder: KeyDerivedBuilder, - >::Value: Serialize + Clone, - Key: Clone, -{ - fn serialize(&self, serializer: S) -> Result { - self.clone().build().serialize(serializer) - } -} - -impl DerivedKeyValue -where - Builder: KeyDerivedBuilder, - >::Value: Serialize, - Key: Clone, -{ - pub(super) fn parameter(&self) -> Option { - match self { - DerivedKeyValue::Omit => None, - DerivedKeyValue::Derived(key) => Some( - serde_json::to_value(Builder::from(key.clone()).build()) - .expect("failed to serialize derived key"), - ), - DerivedKeyValue::Explicit(value) => serde_json::to_value(value).ok(), - } - } -} - -impl DerivedKeyValue -where - Builder: KeyDerivedBuilder, - >::Value: Serialize + Clone, - Key: Clone, -{ - pub(super) fn value(&self) -> Option { match self { - DerivedKeyValue::Omit => None, - DerivedKeyValue::Derived(key) => Some(Builder::from(key.clone()).build()), - DerivedKeyValue::Explicit(value) => Some(value.clone()), + DeriveFromKey::Omit => Ok(None), + DeriveFromKey::Derived => Ok(Some(Value::build(key)?)), + DeriveFromKey::Explicit(value) => Ok(Some(value)), } } } diff --git a/src/jose/mod.rs b/src/jose/mod.rs index 8a2a9be..5b370d8 100644 --- a/src/jose/mod.rs +++ b/src/jose/mod.rs @@ -14,10 +14,11 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use sha1::Sha1; use sha2::Sha256; +use signature::SignatureEncoding; use url::Url; +use crate::algorithms::AlgorithmIdentifier; use crate::base64data::Base64JSON; -use crate::{algorithms::AlgorithmIdentifier, key::SerializeJWK}; #[cfg(feature = "fmt")] use crate::fmt; @@ -28,8 +29,7 @@ mod rendered; mod signed; mod unsigned; -use self::derive::DerivedKeyValue; -pub use self::derive::KeyDerivation; +pub use self::derive::DeriveFromKey; pub use self::rendered::RenderedHeader; pub use self::signed::SignedHeader; pub use self::unsigned::UnsignedHeader; @@ -44,7 +44,7 @@ pub struct Certificate; /// The state of a JOSE header with respect to its signature. pub trait HeaderState { /// The state-dependent parameters of the header. - fn parameters(&self) -> BTreeMap; + fn parameters(&self) -> Result, serde_json::Error>; } /// Error when constructing a JOSE header. @@ -182,29 +182,30 @@ impl Header { } /// Construct the JOSE header from the builder and signing key. - pub(crate) fn into_signed_header(self, key: &A) -> Header> + pub(crate) fn into_signed_header( + self, + key: &A, + ) -> Result, signature::Error> where - A: crate::algorithms::TokenSigner + crate::key::SerializeJWK + Clone, + A: crate::algorithms::TokenSigner + crate::key::SerializeJWK + ?Sized, + S: SignatureEncoding, { let state = SignedHeader { algorithm: key.identifier(), - key: DerivedKeyValue::derive(self.state.key, key), - thumbprint: DerivedKeyValue::derive(self.state.thumbprint, key), - thumbprint_sha256: DerivedKeyValue::derive(self.state.thumbprint_sha256, key), + json_web_key: self.state.key.render(key)?, + thumbprint: self.state.thumbprint.render(key)?, + thumbprint_sha256: self.state.thumbprint_sha256.render(key)?, }; - Header { + Ok(Header { state, registered: self.registered, custom: self.custom, - } + }) } } -impl Header> -where - Key: SerializeJWK, -{ +impl Header { /// JWK signing algorithm in use. pub(crate) fn algorithm(&self) -> &AlgorithmIdentifier { &self.state.algorithm @@ -220,7 +221,6 @@ where pub(crate) fn into_rendered_header(self) -> Header where H: Serialize, - SignedHeader: HeaderState, { let headers = Base64JSON(&self) .serialized_bytes() @@ -229,9 +229,9 @@ where let state = RenderedHeader { raw: headers, algorithm: *self.algorithm(), - key: self.state.key.build(), - thumbprint: self.state.thumbprint.build(), - thumbprint_sha256: self.state.thumbprint_sha256.build(), + key: self.state.json_web_key, + thumbprint: self.state.thumbprint, + thumbprint_sha256: self.state.thumbprint_sha256, }; Header { @@ -256,9 +256,10 @@ 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) -> Header> + pub(crate) fn into_signed_header(self, key: &A) -> Header where - A: crate::algorithms::TokenVerifier + crate::key::SerializeJWK, + A: crate::algorithms::TokenVerifier + ?Sized, + S: SignatureEncoding, { if *self.algorithm() != key.identifier() { panic!( @@ -271,9 +272,9 @@ impl Header { Header { state: SignedHeader { algorithm: *self.algorithm(), - key: DerivedKeyValue::explicit(self.state.key), - thumbprint: DerivedKeyValue::explicit(self.state.thumbprint), - thumbprint_sha256: DerivedKeyValue::explicit(self.state.thumbprint_sha256), + json_web_key: self.state.key, + thumbprint: self.state.thumbprint, + thumbprint_sha256: self.state.thumbprint_sha256, }, registered: self.registered, custom: self.custom, @@ -301,7 +302,7 @@ where // Re-using the parameters map here is important, because it will // alphabetize our keys, resulting in a consistent key order in rendered // tokens. - let mut parameters = self.state.parameters(); + let mut parameters = self.state.parameters()?; let header = serde_json::to_value(&self.registered)?; let custom = serde_json::to_value(&self.custom)?; @@ -410,43 +411,40 @@ impl<'h, H, State> HeaderAccess<'h, H, State> { impl<'h, H> HeaderAccess<'h, H, UnsignedHeader> { #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/json_web_key.md"))] - pub fn key(&self) -> &KeyDerivation { + pub fn key(&self) -> &DeriveFromKey { &self.header.state.key } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint.md"))] - pub fn thumbprint(&self) -> &KeyDerivation> { + pub fn thumbprint(&self) -> &DeriveFromKey> { &self.header.state.thumbprint } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint_sha256.md"))] - pub fn thumbprint_sha256(&self) -> &KeyDerivation> { + pub fn thumbprint_sha256(&self) -> &DeriveFromKey> { &self.header.state.thumbprint_sha256 } } -impl<'h, H, K> HeaderAccess<'h, H, SignedHeader> -where - K: SerializeJWK + Clone, -{ +impl<'h, H> HeaderAccess<'h, H, SignedHeader> { #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/algorithm.md"))] pub fn algorithm(&self) -> &AlgorithmIdentifier { self.header.algorithm() } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/json_web_key.md"))] - pub fn key(&self) -> Option { - self.header.state.key.value() + pub fn key(&self) -> Option<&JsonWebKey> { + self.header.state.json_web_key.as_ref() } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint.md"))] - pub fn thumbprint(&self) -> Option> { - self.header.state.thumbprint.value() + pub fn thumbprint(&self) -> Option<&Thumbprint> { + self.header.state.thumbprint.as_ref() } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint_sha256.md"))] - pub fn thumbprint_sha256(&self) -> Option> { - self.header.state.thumbprint_sha256.value() + pub fn thumbprint_sha256(&self) -> Option<&Thumbprint> { + self.header.state.thumbprint_sha256.as_ref() } } @@ -541,43 +539,40 @@ impl<'h, H, State> HeaderAccessMut<'h, H, State> { impl<'h, H> HeaderAccessMut<'h, H, UnsignedHeader> { #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/json_web_key.md"))] - pub fn key(&mut self) -> &mut KeyDerivation { + pub fn key(&mut self) -> &mut DeriveFromKey { &mut self.header.state.key } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint.md"))] - pub fn thumbprint(&mut self) -> &mut KeyDerivation> { + pub fn thumbprint(&mut self) -> &mut DeriveFromKey> { &mut self.header.state.thumbprint } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint_sha256.md"))] - pub fn thumbprint_sha256(&mut self) -> &mut KeyDerivation> { + pub fn thumbprint_sha256(&mut self) -> &mut DeriveFromKey> { &mut self.header.state.thumbprint_sha256 } } -impl<'h, H, K> HeaderAccessMut<'h, H, SignedHeader> -where - K: SerializeJWK + Clone, -{ +impl<'h, H> HeaderAccessMut<'h, H, SignedHeader> { #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/algorithm.md"))] pub fn algorithm(&self) -> &AlgorithmIdentifier { self.header.algorithm() } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/json_web_key.md"))] - pub fn key(&self) -> Option { - self.header.state.key.value() + pub fn key(&self) -> Option<&JsonWebKey> { + self.header.state.json_web_key.as_ref() } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint.md"))] - pub fn thumbprint(&self) -> Option> { - self.header.state.thumbprint.value() + pub fn thumbprint(&self) -> Option<&Thumbprint> { + self.header.state.thumbprint.as_ref() } #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint_sha256.md"))] - pub fn thumbprint_sha256(&self) -> Option> { - self.header.state.thumbprint_sha256.value() + pub fn thumbprint_sha256(&self) -> Option<&Thumbprint> { + self.header.state.thumbprint_sha256.as_ref() } } diff --git a/src/jose/rendered.rs b/src/jose/rendered.rs index 63573dc..85c9116 100644 --- a/src/jose/rendered.rs +++ b/src/jose/rendered.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use bytes::Bytes; use serde::{Deserialize, Serialize}; use sha1::Sha1; @@ -14,7 +16,7 @@ use super::HeaderState; /// /// This is different from [super::SignedHeader] in that it contains the actual data, /// and not thd derivation, so the fields may be in inconsistent states. -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct RenderedHeader { /// The raw bytes of the header, as it was signed. #[serde(skip)] @@ -38,26 +40,25 @@ pub struct RenderedHeader { } impl HeaderState for RenderedHeader { - fn parameters(&self) -> std::collections::BTreeMap { + fn parameters( + &self, + ) -> Result, serde_json::Error> { let mut data = std::collections::BTreeMap::new(); - data.insert( - "alg".to_owned(), - serde_json::to_value(self.algorithm).unwrap(), - ); + data.insert("alg".to_owned(), serde_json::to_value(self.algorithm)?); if let Some(value) = self.key.as_ref() { - data.insert("jwk".to_owned(), serde_json::to_value(value).unwrap()); + data.insert("jwk".to_owned(), serde_json::to_value(value)?); } if let Some(value) = self.thumbprint.as_ref() { - data.insert("x5t".to_owned(), serde_json::to_value(value).unwrap()); + data.insert("x5t".to_owned(), serde_json::to_value(value)?); } if let Some(value) = self.thumbprint_sha256.as_ref() { - data.insert("x5t#S256".to_owned(), serde_json::to_value(value).unwrap()); + data.insert("x5t#S256".to_owned(), serde_json::to_value(value)?); } - data + Ok(data) } } diff --git a/src/jose/signed.rs b/src/jose/signed.rs index 4d35603..493be2e 100644 --- a/src/jose/signed.rs +++ b/src/jose/signed.rs @@ -2,62 +2,53 @@ use serde::Serialize; use sha1::Sha1; use sha2::Sha256; -use crate::{algorithms::AlgorithmIdentifier, key::SerializeJWK}; +use crate::algorithms::AlgorithmIdentifier; -use crate::key::{JsonWebKeyBuilder, Thumbprinter}; +use crate::key::{JsonWebKey, Thumbprint}; -use super::derive::DerivedKeyValue; use super::HeaderState; /// The registered fields of a JOSE header, which are interdependent /// with the signing key. -#[derive(Debug, Clone, Serialize)] -#[serde(bound(serialize = "Key: SerializeJWK + Clone"))] -pub struct SignedHeader -where - Key: SerializeJWK, -{ +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SignedHeader { #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/algorithm.md"))] #[serde(rename = "alg")] pub(super) algorithm: AlgorithmIdentifier, #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/json_web_key.md"))] - #[serde(rename = "jwk", skip_serializing_if = "DerivedKeyValue::is_none")] - pub(super) key: DerivedKeyValue, Key>, + #[serde(rename = "jwk", skip_serializing_if = "Option::is_none")] + pub(super) json_web_key: Option, #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint.md"))] - #[serde(rename = "x5t", skip_serializing_if = "DerivedKeyValue::is_none")] - pub(super) thumbprint: DerivedKeyValue, Key>, + #[serde(rename = "x5t", skip_serializing_if = "Option::is_none")] + pub(super) thumbprint: Option>, #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/jose/thumbprint_sha256.md"))] - #[serde(rename = "x5t#S256", skip_serializing_if = "DerivedKeyValue::is_none")] - pub(super) thumbprint_sha256: DerivedKeyValue, Key>, + #[serde(rename = "x5t#S256", skip_serializing_if = "Option::is_none")] + pub(super) thumbprint_sha256: Option>, } -impl HeaderState for SignedHeader -where - Key: SerializeJWK + Clone, -{ - fn parameters(&self) -> std::collections::BTreeMap { +impl HeaderState for SignedHeader { + fn parameters( + &self, + ) -> Result, serde_json::Error> { let mut data = std::collections::BTreeMap::new(); - data.insert( - "alg".to_owned(), - serde_json::to_value(self.algorithm).unwrap(), - ); + data.insert("alg".to_owned(), serde_json::to_value(self.algorithm)?); - if let Some(value) = self.key.parameter() { - data.insert("jwk".to_owned(), value); + if let Some(value) = self.json_web_key.as_ref() { + data.insert("jwk".to_owned(), serde_json::to_value(value)?); } - if let Some(value) = self.thumbprint.parameter() { - data.insert("x5t".to_owned(), value); + if let Some(value) = self.thumbprint.as_ref() { + data.insert("x5t".to_owned(), serde_json::to_value(value)?); } - if let Some(value) = self.thumbprint_sha256.parameter() { - data.insert("x5t#S256".to_owned(), value); + if let Some(value) = self.thumbprint_sha256.as_ref() { + data.insert("x5t#S256".to_owned(), serde_json::to_value(value)?); } - data + Ok(data) } } diff --git a/src/jose/unsigned.rs b/src/jose/unsigned.rs index db686e8..52c5059 100644 --- a/src/jose/unsigned.rs +++ b/src/jose/unsigned.rs @@ -1,49 +1,53 @@ +use std::collections::BTreeMap; + use sha1::Sha1; use sha2::Sha256; use crate::key::{JsonWebKey, Thumbprint}; -use super::{HeaderState, KeyDerivation}; +use super::{DeriveFromKey, HeaderState}; /// A builder for the registered JOSE header fields for using JWTs. /// /// Some fields are set indirectly by the builder, e.g. the `key` field is set /// to `true` when you'd like to serialize the signing key in the JOSE header /// as a JSON Web Key (JWK). -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct UnsignedHeader { /// Whether to include the signing key in the JOSE header as a JWK. /// /// See [RenderedHeader::key] for field details. - pub(super) key: KeyDerivation, + pub(super) key: DeriveFromKey, /// Whether to include the X.509 certificate thumbprint in the JOSE header with the SHA1 digest. /// /// See [RenderedHeader::thumbprint] for field details. - pub(super) thumbprint: KeyDerivation>, + pub(super) thumbprint: DeriveFromKey>, /// Whether to include the X.509 certificate thumbprint in the JOSE header with the SHA256 digest. /// /// See [RenderedHeader::thumbprint_sha256] for field details. - pub(super) thumbprint_sha256: KeyDerivation>, + pub(super) thumbprint_sha256: DeriveFromKey>, } impl HeaderState for UnsignedHeader { - fn parameters(&self) -> std::collections::BTreeMap { + fn parameters( + &self, + ) -> Result, serde_json::Error> { let mut data = std::collections::BTreeMap::new(); - if let Some(value) = self.key.parameter("jwk") { + if let Some(value) = self.key.parameter("jwk")? { data.insert("jwk".to_owned(), value); } - if let Some(value) = self.thumbprint.parameter("x5t") { + if let Some(value) = self.thumbprint.parameter("x5t")? { data.insert("x5t".to_owned(), value); } - if let Some(value) = self.thumbprint_sha256.parameter("x5t#S256") { + if let Some(value) = self.thumbprint_sha256.parameter("x5t#S256")? { data.insert("x5t#S256".to_owned(), value); } - data + Ok(data) } } diff --git a/src/key.rs b/src/key.rs index 5b15cd9..08113cf 100644 --- a/src/key.rs +++ b/src/key.rs @@ -8,12 +8,12 @@ use std::{collections::BTreeMap, hash::Hash, marker::PhantomData}; use base64ct::Encoding; -use digest::Digest; use serde::{ de, ser::{self, SerializeMap}, Deserialize, Serialize, }; +use signature::Error as SignatureError; /// Trait for keys which can be used as a JWK. pub trait JWKeyType { @@ -28,21 +28,28 @@ where const KEY_TYPE: &'static str = T::KEY_TYPE; } -/// Trait for keys which can be serialized as a JWK. -pub trait SerializeJWK: JWKeyType { - /// Return a list of parameters to be serialized in the JWK. - fn parameters(&self) -> Vec<(String, serde_json::Value)>; +/// Trait for keys which can be used as a JWK, automatically implemented for +/// types which implement `JWKeyType`, to make `SerializeJWK` object-safe. +pub trait DynJwkKeyType { + /// The string used to identify the JWK type in the `kty` field. + fn key_type(&self) -> &'static str; } -impl SerializeJWK for &T +impl DynJwkKeyType for T where - T: SerializeJWK, + T: JWKeyType, { - fn parameters(&self) -> Vec<(String, serde_json::Value)> { - (*self).parameters() + fn key_type(&self) -> &'static str { + T::KEY_TYPE } } +/// Trait for keys which can be serialized as a JWK. +pub trait SerializeJWK: DynJwkKeyType { + /// Return a list of parameters to be serialized in the JWK. + fn parameters(&self) -> Vec<(String, serde_json::Value)>; +} + /// Trait for keys which can be deserialized from a JWK. pub trait DeserializeJWK: JWKeyType { /// From a set of parameters, build a key. @@ -52,61 +59,22 @@ pub trait DeserializeJWK: JWKeyType { } /// Trait for building values derived from a key. -pub trait KeyDerivedBuilder: From { - /// Output value type. - type Value; - +pub trait BuildFromKey { /// Build a value from a key. - fn build(self) -> Self::Value; -} -/// A JSON Web Key with the original key contained inside. -/// -/// The actual key isn't produced until this is serialized. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct JsonWebKeyBuilder(K); - -impl From for JsonWebKeyBuilder { - fn from(key: K) -> Self { - Self(key) - } -} - -impl Serialize for JsonWebKeyBuilder -where - Key: SerializeJWK, -{ - fn serialize(&self, serializer: S) -> Result + fn build(key: &Key) -> Result where - S: serde::Serializer, - { - // Asseble keys first so that we can order them. - let mut keys = BTreeMap::new(); - keys.insert( - "kty".to_owned(), - serde_json::Value::String(Key::KEY_TYPE.to_owned()), - ); - for (key, value) in self.0.parameters() { - keys.insert(key, value); - } - - // Put them back so we can serialize them in lexical order. - let mut map = serializer.serialize_map(Some(keys.len()))?; - for (key, value) in keys { - map.serialize_entry(&key, &value)?; - } - - map.end() - } + Self: Sized; } -impl KeyDerivedBuilder for JsonWebKeyBuilder +impl BuildFromKey for JsonWebKey where - Key: SerializeJWK, + Key: SerializeJWK + ?Sized, { - type Value = JsonWebKey; - - fn build(self) -> Self::Value { - JsonWebKey::from(self) + fn build(key: &Key) -> Result { + Ok(JsonWebKey { + key_type: key.key_type().into(), + parameters: key.parameters().into_iter().collect(), + }) } } @@ -123,22 +91,11 @@ pub struct JsonWebKey { } impl JsonWebKey { - /// Create a builder for a new JWK. - /// - /// The builder ensures that the JWK fields are set consistently. - pub fn builder(key: Key) -> JsonWebKeyBuilder { - JsonWebKeyBuilder::from(key) - } -} - -impl From> for JsonWebKey -where - K: SerializeJWK, -{ - fn from(key: JsonWebKeyBuilder) -> Self { + /// Build a JWK from a key. + pub fn build(key: &K) -> Self { JsonWebKey { - key_type: K::KEY_TYPE.into(), - parameters: key.0.parameters().into_iter().collect(), + key_type: key.key_type().into(), + parameters: key.parameters().into_iter().collect(), } } } @@ -166,78 +123,6 @@ impl Serialize for JsonWebKey { } } -/// A JSON Web Key Thumbprint (RFC 7638) calculator. -/// -/// This type contains the raw parts to build a JWK and then digest -/// them to form a thumbprint. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Thumbprinter { - digest: PhantomData, - key: JsonWebKeyBuilder, -} - -impl Thumbprinter { - /// Create a new JWK Thumbprinter from a key. - pub fn new(key: K) -> Self { - Self { - digest: PhantomData, - key: JsonWebKeyBuilder::from(key), - } - } -} - -impl Thumbprinter -where - D: Digest, - K: SerializeJWK, -{ - /// Compute the raw digest of the JWK. - pub fn digest(&self) -> Vec { - let thumb = serde_json::to_vec(&self.key).expect("Valid JSON format"); - - let mut hasher = D::new(); - hasher.update(&thumb); - let digest = hasher.finalize(); - digest.to_vec() - } - - /// Compute the base64url-encoded digest of the JWK. - pub fn thumbprint(&self) -> Thumbprint { - Thumbprint::new(base64ct::Base64UrlUnpadded::encode_string(&self.digest())) - } -} - -impl Serialize for Thumbprinter -where - K: SerializeJWK, - D: Digest, -{ - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.thumbprint()) - } -} - -impl KeyDerivedBuilder for Thumbprinter -where - K: SerializeJWK, - D: Digest, -{ - type Value = Thumbprint; - - fn build(self) -> Self::Value { - self.thumbprint() - } -} - -impl From for Thumbprinter { - fn from(key: K) -> Self { - Self::new(key) - } -} - /// A computed thumbprint. #[derive(Debug, zeroize::Zeroize, zeroize::ZeroizeOnDrop)] pub struct Thumbprint { @@ -268,7 +153,10 @@ impl PartialEq for Thumbprint { impl Eq for Thumbprint {} -impl Thumbprint { +impl Thumbprint +where + Digest: digest::Digest, +{ /// Create a new thumbprint from a base64url-encoded digest. pub fn new(thumbprint: String) -> Self { Self { @@ -276,6 +164,31 @@ impl Thumbprint { digest: PhantomData, } } + + fn build(key: &K) -> Result + where + K: SerializeJWK + ?Sized, + { + let jwk = JsonWebKey::build(key); + let thumb = serde_json::to_vec(&jwk).map_err(SignatureError::from_source)?; + + let mut hasher = Digest::new(); + hasher.update(&thumb); + let digest = hasher.finalize(); + Ok(Self::new(base64ct::Base64UrlUnpadded::encode_string( + &digest, + ))) + } +} + +impl BuildFromKey for Thumbprint +where + Key: SerializeJWK + ?Sized, + Digest: digest::Digest, +{ + fn build(key: &Key) -> Result, signature::Error> { + Thumbprint::build(key).map_err(signature::Error::from_source) + } } impl ser::Serialize for Thumbprint { @@ -289,7 +202,10 @@ impl ser::Serialize for Thumbprint { struct ThumbprintVisitor(PhantomData); -impl<'de, D> de::Visitor<'de> for ThumbprintVisitor { +impl<'de, D> de::Visitor<'de> for ThumbprintVisitor +where + D: digest::Digest, +{ type Value = Thumbprint; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -304,7 +220,10 @@ impl<'de, D> de::Visitor<'de> for ThumbprintVisitor { } } -impl<'de, Digest> de::Deserialize<'de> for Thumbprint { +impl<'de, Digest> de::Deserialize<'de> for Thumbprint +where + Digest: digest::Digest, +{ fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -376,10 +295,15 @@ pub(crate) mod jwk_reader { #[cfg(test)] mod test { + use super::*; + + use static_assertions as sa; + + sa::assert_obj_safe!(SerializeJWK); + #[cfg(all(test, feature = "rsa"))] mod rsa { use super::super::*; - use std::ops::Deref; use serde_json::json; @@ -400,12 +324,9 @@ mod test { } )); - let thumb = Thumbprinter::::new(key); + let thumb: Thumbprint = Thumbprint::build(&key).unwrap(); - assert_eq!( - thumb.thumbprint().deref(), - "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" - ); + assert_eq!(&*thumb, "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"); } } } diff --git a/src/token/formats.rs b/src/token/formats.rs index afa8fd2..c9823ea 100644 --- a/src/token/formats.rs +++ b/src/token/formats.rs @@ -16,7 +16,7 @@ use crate::base64data::{Base64JSON, Base64Signature, DecodeError}; use crate::jose::{Header, HeaderState, RenderedHeader}; /// A token format that serializes the token as a compact string. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Compact; impl Compact { @@ -31,7 +31,7 @@ impl Compact { /// A token format that serializes the token as a single JSON object, /// with the unprotected header as a top-level field. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FlatUnprotected { unprotected: U, } @@ -54,7 +54,7 @@ impl FlatUnprotected { } /// A token format that serializes the token as a single JSON object. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Flat; /// Error returned when a token cannot be formatted as a string. diff --git a/src/token/mod.rs b/src/token/mod.rs index 51ed1df..9e799b1 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -19,12 +19,11 @@ use serde::{ }; use signature::SignatureEncoding; -use crate::algorithms::TokenVerifier; #[cfg(feature = "fmt")] use crate::fmt; use crate::key::SerializeJWK; use crate::{ - algorithms::{AlgorithmIdentifier, TokenSigner}, + algorithms::{AlgorithmIdentifier, DynJoseAlgorithm}, base64data::{Base64JSON, Base64Signature, DecodeError}, jose::{Header, HeaderAccess, HeaderAccessMut, HeaderState}, }; @@ -42,7 +41,7 @@ pub use self::state::{HasSignature, MaybeSigned, Signed, Unsigned, Unverified, V /// /// It is hard to express this empty type naturally in the Rust type system in a way that interacts /// well with [serde_json]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] enum Payload

{ /// A payload which will be serialized as JSON and then base64url encoded. Json(Base64JSON

), @@ -216,7 +215,7 @@ let key = rsa::pkcs1v15::SigningKey::random(&mut rand::OsRng, 2048).unwrap(); let token = Token::compact((), ()); // The only way to get a signed token is to sign an Unsigned token! -let signed = token.sign::>(&key).unwrap(); +let signed = token.sign::, rsa::pkcs1v15::Signature>(&key).unwrap(); println!("Token: {}", signed.rendered().unwrap()); ``` Signing often requires specifying the algorithm to use. In the example above, we use @@ -231,7 +230,7 @@ them. This is done with the [`Token::unverify`] method: # use signature::rand_core as rand; # let key: rsa::pkcs1v15::SigningKey = rsa::pkcs1v15::SigningKey::random(&mut rand::OsRng, 2048).unwrap(); # let token = Token::compact((), ()); -# let signed = token.sign(&key).unwrap(); +# let signed = token.sign::<_, rsa::pkcs1v15::Signature>(&key).unwrap(); // We can unverify the token, which discard the memory of the key used to sign it. let unverified = signed.unverify(); @@ -249,9 +248,9 @@ by checking the signature. This is done with the [`Token::verify`] method: # let key: rsa::pkcs1v15::SigningKey = rsa::pkcs1v15::SigningKey::random(&mut rand::OsRng, 2048).unwrap(); # let verifying_key = key.verifying_key(); # let token = Token::compact((), ()); -# let signed = token.sign(&key).unwrap(); +# let signed = token.sign::<_, rsa::pkcs1v15::Signature>(&key).unwrap(); # let unverified = signed.unverify(); -let verified = unverified.verify(&verifying_key).unwrap(); +let verified = unverified.verify::<_, rsa::pkcs1v15::Signature>(&verifying_key).unwrap(); println!("Token: {}", verified.rendered().unwrap()); ``` @@ -260,7 +259,7 @@ one specified in the header. Since keys are strongly typed, it is not possible t signature substitution attack using a different key type. "# )] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Token, Fmt: TokenFormat = Compact> { payload: Payload

, state: State, @@ -426,12 +425,15 @@ 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::TokenSigner + SerializeJWK + Clone, - // A::Signature: Serialize, + A: crate::algorithms::TokenSigner + SerializeJWK + ?Sized, + S: SignatureEncoding, { - let header = self.state.header.into_signed_header(algorithm); + let header = self.state.header.into_signed_header(algorithm)?; let headers = Base64JSON(&header).serialized_value()?; let payload = self.payload.serialized_value()?; let signature = algorithm @@ -439,7 +441,11 @@ where .map_err(TokenSigningError::Signing)?; Ok(Token { payload: self.payload, - state: Signed { header, signature }, + state: Signed { + header, + signature, + _phantom_key: PhantomData, + }, fmt: self.fmt, }) } @@ -458,14 +464,15 @@ where /// The algorithm must be uniquely specified for verification, otherwise the token /// could perform a signature downgrade attack. #[allow(clippy::type_complexity)] - pub fn verify( + pub fn verify( self, algorithm: &A, - ) -> Result, Fmt>, TokenVerifyingError> + ) -> Result, Fmt>, TokenVerifyingError> where - A: crate::algorithms::TokenVerifier + SerializeJWK, + A: crate::algorithms::TokenVerifier + ?Sized, + S: SignatureEncoding, P: Serialize, - H: Serialize, + H: Serialize + std::fmt::Debug, { if algorithm.identifier() != *self.state.header.algorithm() { return Err(TokenVerifyingError::Algorithm( @@ -483,11 +490,15 @@ where ) .map_err(TokenVerifyingError::Verify)?; - let header = self.state.header.into_signed_header::(algorithm); + let header = self.state.header.into_signed_header::(algorithm); Ok(Token { payload: self.payload, - state: Verified { header, signature }, + state: Verified { + header, + signature, + _phantom_key: PhantomData, + }, fmt: self.fmt, }) } @@ -506,10 +517,11 @@ where } } -impl Token, Fmt> +impl Token, Fmt> where Fmt: TokenFormat, - Alg: TokenSigner + SerializeJWK + Clone, + Alg: DynJoseAlgorithm + ?Sized, + Sig: SignatureEncoding, H: Serialize, P: Serialize, { @@ -534,10 +546,10 @@ where } } -impl Token, Fmt> +impl Token, Fmt> where Fmt: TokenFormat, - Alg: TokenSigner + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, { /// Get the payload of the token. pub fn payload(&self) -> Option<&P> { @@ -548,10 +560,11 @@ where } } -impl Token, Fmt> +impl Token, Fmt> where Fmt: TokenFormat, - Alg: TokenVerifier + SerializeJWK + Clone, + Alg: DynJoseAlgorithm + ?Sized, + Sig: SignatureEncoding, H: Serialize, P: Serialize, { @@ -576,10 +589,10 @@ where } } -impl Token, Fmt> +impl Token, Fmt> where Fmt: TokenFormat, - Alg: TokenVerifier + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, { /// Get the payload of the token. pub fn payload(&self) -> Option<&P> { @@ -671,7 +684,7 @@ 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(signature::Error), + Signing(#[from] signature::Error), /// An error occured while serailizing the header or payload for /// signature computation. This indicates that something is probably @@ -756,7 +769,9 @@ mod test_rsa { let token = Token::new((), claims, Compact::new()); let algorithm: rsa::pkcs1v15::SigningKey = rsa::pkcs1v15::SigningKey::new(pkey); - let signed = token.sign(&algorithm).unwrap(); + let signed = token + .sign::<_, rsa::pkcs1v15::Signature>(&algorithm) + .unwrap(); { let hdr = base64ct::Base64UrlUnpadded::encode_string( &serde_json::to_vec(&signed.state.header()).unwrap(), @@ -799,7 +814,10 @@ mod test_rsa { let algorithm = algorithm.verifying_key(); - signed.unverify().verify(&algorithm).unwrap(); + signed + .unverify() + .verify::<_, rsa::pkcs1v15::Signature>(&algorithm) + .unwrap(); } } @@ -841,11 +859,14 @@ mod test_ecdsa { let token = Token::compact((), "This is a signed message"); - let signed = token.sign(&key).unwrap(); + let signed = token.sign::<_, ::ecdsa::Signature<_>>(&key).unwrap(); let verifying_key = key.verifying_key(); - let verified = signed.unverify().verify(verifying_key).unwrap(); + let verified = signed + .unverify() + .verify::<_, ::ecdsa::Signature<_>>(verifying_key) + .unwrap(); assert_eq!(verified.payload(), Some(&"This is a signed message")); } diff --git a/src/token/state.rs b/src/token/state.rs index 2045145..03e5264 100644 --- a/src/token/state.rs +++ b/src/token/state.rs @@ -1,12 +1,13 @@ +use std::marker::PhantomData; + use bytes::Bytes; use serde::Serialize; use signature::SignatureEncoding; use crate::{ - algorithms::{DynJoseAlgorithm, SignatureBytes, TokenSigner}, + algorithms::{DynJoseAlgorithm, SignatureBytes}, base64data::Base64Signature, jose, - key::SerializeJWK, }; /// A trait used to represent the state of a token with respect to @@ -58,7 +59,7 @@ pub trait HasSignature: MaybeSigned { /// /// This token contains just the unsigned parts which are used as the /// input to the cryptographic signature. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Unsigned { pub(super) header: jose::Header, } @@ -88,32 +89,36 @@ 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: Clone, Alg::Signature: Serialize",))] -pub struct Signed +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(bound(serialize = "H: Serialize, Sig: Serialize",))] +pub struct Signed where - Alg: DynJoseAlgorithm + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, { - pub(super) header: jose::Header>, - pub(super) signature: Alg::Signature, + pub(super) header: jose::Header, + pub(super) signature: Sig, + + #[serde(skip)] + pub(super) _phantom_key: PhantomData, } -impl HasSignature for Signed +impl HasSignature for Signed where - Alg: TokenSigner + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, + Sig: SignatureEncoding, { - type Signature = Alg::Signature; + type Signature = Sig; fn signature(&self) -> &Self::Signature { &self.signature } } -impl MaybeSigned for Signed +impl MaybeSigned for Signed where - Alg: TokenSigner + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, { - type HeaderState = jose::SignedHeader; + type HeaderState = jose::SignedHeader; type Header = H; fn header(&self) -> &jose::Header { @@ -139,21 +144,24 @@ where /// signature is valid and consistent with the header values. However, we also know that /// 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: Clone, Alg::Signature: Serialize",))] -pub struct Verified +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(bound(serialize = "H: Serialize, Sig: Serialize",))] +pub struct Verified where - Alg: DynJoseAlgorithm + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, { - pub(super) header: jose::Header>, - pub(super) signature: Alg::Signature, + pub(super) header: jose::Header, + pub(super) signature: Sig, + + #[serde(skip)] + pub(super) _phantom_key: PhantomData, } -impl MaybeSigned for Verified +impl MaybeSigned for Verified where - Alg: DynJoseAlgorithm + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, { - type HeaderState = jose::SignedHeader; + type HeaderState = jose::SignedHeader; type Header = H; fn header_mut(&mut self) -> &mut jose::Header { @@ -173,11 +181,12 @@ where } } -impl HasSignature for Verified +impl HasSignature for Verified where - Alg: DynJoseAlgorithm + SerializeJWK, + Alg: DynJoseAlgorithm + ?Sized, + Sig: SignatureEncoding, { - type Signature = Alg::Signature; + type Signature = Sig; fn signature(&self) -> &Self::Signature { &self.signature @@ -188,7 +197,7 @@ where /// /// This state indicates that we have recieved the token from elsewhere, and /// many fields could be in inconsistnet states. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Unverified { pub(super) payload: Bytes, pub(super) header: jose::Header, diff --git a/tests/dyn-rsa-key.rs b/tests/dyn-rsa-key.rs new file mode 100644 index 0000000..cf17179 --- /dev/null +++ b/tests/dyn-rsa-key.rs @@ -0,0 +1,143 @@ +#![allow(dead_code)] + +use jaws::algorithms::SignatureBytes; +use jaws::algorithms::TokenSigner; +use jaws::algorithms::TokenVerifier; +use jaws::key::SerializeJWK; +use jaws::token::{Unsigned, Unverified}; +use jaws::Compact; +use jaws::JWTFormat; +use jaws::RegisteredClaims; +use rsa::pkcs8::DecodePrivateKey; +use serde_json::json; +use sha2::Sha256; + +fn rsa_private() -> rsa::RsaPrivateKey { + // This key is from RFC 7515, Appendix A.2. Provide your own key instead! + // The key here is stored as a PKCS#8 PEM file, but you can leverage + // RustCrypto to load a variety of other formats. + rsa::RsaPrivateKey::from_pkcs8_pem(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/examples/rfc7515a2.pem" + ))) + .unwrap() +} + +fn rsa_public() -> rsa::RsaPublicKey { + let key = rsa_private(); + key.to_public_key() +} + +trait TokenSigningKey: TokenSigner + SerializeJWK {} + +impl TokenSigningKey for T where T: TokenSigner + SerializeJWK {} + +fn rsa_signer() -> rsa::pkcs1v15::SigningKey { + rsa::pkcs1v15::SigningKey::::new(rsa_private()) +} + +fn rsa_verifier() -> rsa::pkcs1v15::VerifyingKey { + rsa::pkcs1v15::VerifyingKey::new(rsa_public()) +} + +fn dyn_signer() -> Box { + Box::new(rsa_signer()) +} + +fn dyn_verifier() -> Box> { + Box::new(rsa_verifier()) +} + +type Claims = jaws::Claims; +type Token = jaws::Token; + +fn unsigned_token() -> Token> { + let claims = Claims { + registered: RegisteredClaims { + subject: "1234567890".to_string().into(), + ..Default::default() + }, + claims: json!({ + "name": "John Doe", + "admin": true, + }), + }; + + let mut token = Token::compact((), claims); + *token.header_mut().r#type() = Some("JWT".to_string()); + token.header_mut().key().derived(); + token +} + +fn roundtrip(token: Token>) -> Token> { + let rendered = token.rendered().unwrap(); + let parsed: Token> = rendered.parse().unwrap(); + assert_eq!(token, parsed); + parsed +} + +#[test] +fn dyn_rsa_verify() { + let token = unsigned_token(); + println!("=== Unsigned Token ==="); + println!("{}", token.formatted()); + println!( + "Payload: {}", + serde_json::to_string_pretty(&token.payload().unwrap()).unwrap() + ); + + let signed = token + .sign::<_, rsa::pkcs1v15::Signature>(&rsa_signer()) + .unwrap(); + + let unverified = roundtrip(signed.unverify()); + + println!("=== Unverified Token ==="); + println!("{}", unverified.formatted()); + + let verified = unverified + .verify::<_, rsa::pkcs1v15::Signature>(&rsa_verifier()) + .unwrap(); + + let unverified = roundtrip(verified.unverify()); + + let verified = unverified + .verify::<_, SignatureBytes>(dyn_verifier().as_ref()) + .unwrap(); + + println!("=== Verified Token ==="); + println!("{}", verified.formatted()); +} + +#[test] +fn dyn_rsa_sign() { + let token = unsigned_token(); + println!("=== Unsigned Token ==="); + println!("{}", token.formatted()); + println!( + "Payload: {}", + serde_json::to_string_pretty(&token.payload().unwrap()).unwrap() + ); + + let signed = token + .sign::<_, SignatureBytes>(dyn_signer().as_ref()) + .unwrap(); + + let unverified = roundtrip(signed.unverify()); + + println!("=== Unverified Token ==="); + println!("{}", unverified.formatted()); + + let verified = unverified + .verify::<_, rsa::pkcs1v15::Signature>(&rsa_verifier()) + .unwrap(); + + let unverified = roundtrip(verified.unverify()); + + let verified = unverified + .verify::<_, SignatureBytes>(dyn_verifier().as_ref()) + .unwrap(); + + println!("=== Verified Token ==="); + println!("{}", verified.formatted()); +}