diff --git a/Cargo.toml b/Cargo.toml index 3ed0158..aa66e7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,6 @@ simple_asn1 = "0.4" [dev-dependencies] # For the custom chrono example chrono = "0.4" + +[badges] +maintenance = { status = "passively-developed" } diff --git a/src/crypto/ecdsa.rs b/src/crypto/ecdsa.rs index 28974e2..eb7aa05 100644 --- a/src/crypto/ecdsa.rs +++ b/src/crypto/ecdsa.rs @@ -2,17 +2,28 @@ use ring::{rand, signature}; use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::encode; +use crate::serialization::b64_encode; +use crate::algorithms::Algorithm; + + +/// Only used internally when validating EC, to map from our enum to the Ring EcdsaVerificationAlgorithm structs. +pub(crate) fn alg_to_ec_verification(alg: Algorithm) -> &'static signature::EcdsaVerificationAlgorithm { + match alg { + Algorithm::ES256 => &signature::ECDSA_P256_SHA256_FIXED, + Algorithm::ES384 => &signature::ECDSA_P384_SHA384_FIXED, + _ => unreachable!("Tried to get EC signature for a non-EC algorithm"), + } +} /// The actual ECDSA signing + encoding pub fn sign( alg: &'static signature::EcdsaSigningAlgorithm, key: &[u8], - signing_input: &str, + message: &str, ) -> Result { let pem_key = PemEncodedKey::new(key)?; let signing_key = signature::EcdsaKeyPair::from_pkcs8(alg, pem_key.as_ec_private_key()?)?; let rng = rand::SystemRandom::new(); - let out = signing_key.sign(&rng, signing_input.as_bytes())?; - Ok(encode(out.as_ref())) + let out = signing_key.sign(&rng, message.as_bytes())?; + Ok(b64_encode(out.as_ref())) } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 8f624aa..4c1132d 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,19 +1,19 @@ use ring::constant_time::verify_slices_are_equal; use ring::{hmac, signature}; -use simple_asn1::BigUint; use crate::algorithms::Algorithm; use crate::errors::Result; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::{decode, encode}; +use crate::serialization::{b64_decode, b64_encode}; pub(crate) mod ecdsa; pub(crate) mod rsa; /// The actual HS signing + encoding +/// Could be in its own file to match RSA/EC but it's 2 lines... pub(crate) fn sign_hmac(alg: hmac::Algorithm, key: &[u8], message: &str) -> Result { let digest = hmac::sign(&hmac::Key::new(alg, key), message.as_bytes()); - Ok(encode(digest.as_ref())) + Ok(b64_encode(digest.as_ref())) } /// Take the payload of a JWT, sign it using the algorithm given and return @@ -40,27 +40,16 @@ pub fn sign(message: &str, key: &[u8], algorithm: Algorithm) -> Result { } } -fn rsa_alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters { - match alg { - Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, - Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, - Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, - Algorithm::PS256 => &signature::RSA_PSS_2048_8192_SHA256, - Algorithm::PS384 => &signature::RSA_PSS_2048_8192_SHA384, - Algorithm::PS512 => &signature::RSA_PSS_2048_8192_SHA512, - _ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"), - } -} - /// Compares the signature given with a re-computed signature for HMAC or using the public key -/// for RSA. +/// for RSA/EC. /// /// Only use this function if you want to do something other than JWT. /// /// `signature` is the signature part of a jwt (text after the second '.') /// -/// `signing_input` is base64(header) + "." + base64(claims) -/// For ECDSA/RSS, the `key` is the pem public key +/// `message` is base64(header) + "." + base64(claims) +/// For ECDSA/RSA, the `key` is the pem public key. If you want to verify using the public key +/// components (modulus/exponent), use `verify_rsa_components` instead. pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) -> Result { match algorithm { Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => { @@ -68,17 +57,18 @@ pub fn verify(signature: &str, message: &str, key: &[u8], algorithm: Algorithm) let signed = sign(message, key, algorithm)?; Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok()) } - Algorithm::ES256 => { - verify_ring_es(&signature::ECDSA_P256_SHA256_FIXED, signature, message, key) + Algorithm::ES256 | Algorithm::ES384 => { + let pem_key = PemEncodedKey::new(key)?; + verify_ring(ecdsa::alg_to_ec_verification(algorithm), signature, message, pem_key.as_ec_public_key()?) } - Algorithm::ES384 => { - verify_ring_es(&signature::ECDSA_P384_SHA384_FIXED, signature, message, key) - } - _ => verify_ring_rsa(rsa_alg_to_rsa_parameters(algorithm), signature, message, key), + // only RSAs left + _ => { + let pem_key = PemEncodedKey::new(key)?; + verify_ring(rsa::alg_to_rsa_parameters(algorithm), signature, message, pem_key.as_rsa_key()?) + }, } } -// TODO: see if we can remove stuff? /// See Ring docs for more details fn verify_ring( @@ -87,43 +77,24 @@ fn verify_ring( message: &str, key: &[u8], ) -> Result { - let signature_bytes = decode(signature)?; + let signature_bytes = b64_decode(signature)?; let public_key = signature::UnparsedPublicKey::new(alg, key); let res = public_key.verify(message.as_bytes(), &signature_bytes); Ok(res.is_ok()) } -fn verify_ring_es( - alg: &'static dyn signature::VerificationAlgorithm, - signature: &str, - message: &str, - key: &[u8], -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, message, pem_key.as_ec_public_key()?) -} - -fn verify_ring_rsa( - alg: &'static signature::RsaParameters, - signature: &str, - message: &str, - key: &[u8], -) -> Result { - let pem_key = PemEncodedKey::new(key)?; - verify_ring(alg, signature, message, pem_key.as_rsa_key()?) -} - -pub fn verify_rsa_modulus_exponent( - alg: Algorithm, +/// Verify the signature given using the (n, e) components of a RSA public key. +/// +/// `signature` is the signature part of a jwt (text after the second '.') +/// +/// `message` is base64(header) + "." + base64(claims) +pub fn verify_rsa_components( signature: &str, message: &str, components: (&str, &str), + alg: Algorithm, ) -> Result { - let signature_bytes = decode(signature)?; - let n = BigUint::from_bytes_be(&decode(components.0)?).to_bytes_be(); - let e = BigUint::from_bytes_be(&decode(components.1)?).to_bytes_be(); - let pubkey = signature::RsaPublicKeyComponents { n, e }; - let res = pubkey.verify(rsa_alg_to_rsa_parameters(alg), message.as_ref(), &signature_bytes); - Ok(res.is_ok()) + let signature_bytes = b64_decode(signature)?; + rsa::verify_from_components(&signature_bytes, message, components, alg) } diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs index bc4a78c..a7241a2 100644 --- a/src/crypto/rsa.rs +++ b/src/crypto/rsa.rs @@ -1,15 +1,32 @@ use ring::{rand, signature}; +use simple_asn1::BigUint; use crate::errors::{ErrorKind, Result}; use crate::pem_decoder::PemEncodedKey; -use crate::serialization::encode; +use crate::serialization::{b64_encode, b64_decode}; +use crate::algorithms::Algorithm; + + +/// Only used internally when validating RSA, to map from our enum to the Ring param structs. +pub(crate) fn alg_to_rsa_parameters(alg: Algorithm) -> &'static signature::RsaParameters { + match alg { + Algorithm::RS256 => &signature::RSA_PKCS1_2048_8192_SHA256, + Algorithm::RS384 => &signature::RSA_PKCS1_2048_8192_SHA384, + Algorithm::RS512 => &signature::RSA_PKCS1_2048_8192_SHA512, + Algorithm::PS256 => &signature::RSA_PSS_2048_8192_SHA256, + Algorithm::PS384 => &signature::RSA_PSS_2048_8192_SHA384, + Algorithm::PS512 => &signature::RSA_PSS_2048_8192_SHA512, + _ => unreachable!("Tried to get RSA signature for a non-rsa algorithm"), + } +} + /// The actual RSA signing + encoding /// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html -pub fn sign( +pub(crate) fn sign( alg: &'static dyn signature::RsaEncoding, key: &[u8], - signing_input: &str, + message: &str, ) -> Result { let pem_key = PemEncodedKey::new(key)?; let key_pair = signature::RsaKeyPair::from_der(pem_key.as_rsa_key()?) @@ -18,8 +35,20 @@ pub fn sign( let mut signature = vec![0; key_pair.public_modulus_len()]; let rng = rand::SystemRandom::new(); key_pair - .sign(alg, &rng, signing_input.as_bytes(), &mut signature) + .sign(alg, &rng, message.as_bytes(), &mut signature) .map_err(|_| ErrorKind::InvalidRsaKey)?; - Ok(encode(&signature)) + Ok(b64_encode(&signature)) +} + +pub(crate) fn verify_from_components( + signature_bytes: &[u8], + message: &str, + components: (&str, &str), + alg: Algorithm) -> Result { + let n = BigUint::from_bytes_be(&b64_decode(components.0)?).to_bytes_be(); + let e = BigUint::from_bytes_be(&b64_decode(components.1)?).to_bytes_be(); + let pubkey = signature::RsaPublicKeyComponents { n, e }; + let res = pubkey.verify(alg_to_rsa_parameters(alg), message.as_ref(), &signature_bytes); + Ok(res.is_ok()) } diff --git a/src/decoding.rs b/src/decoding.rs index 0be1c4c..c2934cc 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -1,6 +1,6 @@ use serde::de::DeserializeOwned; -use crate::crypto::{verify, verify_rsa_modulus_exponent}; +use crate::crypto::{verify, verify_rsa_components}; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; use crate::serialization::{from_jwt_part_claims, TokenData}; @@ -40,7 +40,7 @@ fn _decode( let is_valid = match key { DecodingKey::SecretOrPem(k) => verify(signature, message, k, header.alg), DecodingKey::RsaModulusExponent { n, e } => { - verify_rsa_modulus_exponent(header.alg, signature, message, (n, e)) + verify_rsa_components(signature, message, (n, e), header.alg) } }?; diff --git a/src/header.rs b/src/header.rs index 0ced490..c6ac473 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::algorithms::Algorithm; use crate::errors::Result; -use crate::serialization::decode; +use crate::serialization::b64_decode; /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. @@ -60,7 +60,7 @@ impl Header { /// Converts an encoded part into the Header struct if possible pub(crate) fn from_encoded(encoded_part: &str) -> Result { - let decoded = decode(encoded_part)?; + let decoded = b64_decode(encoded_part)?; let s = String::from_utf8(decoded)?; Ok(serde_json::from_str(&s)?) diff --git a/src/lib.rs b/src/lib.rs index fa963b5..4e067f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub use validation::Validation; use serde::ser::Serialize; use crate::errors::Result; -use crate::serialization::encode_part; +use crate::serialization::b64_encode_part; /// Encode the header and claims given and sign the payload using the algorithm from the header and the key /// @@ -48,8 +48,8 @@ use crate::serialization::encode_part; /// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap(); /// ``` pub fn encode(header: &Header, claims: &T, key: &[u8]) -> Result { - let encoded_header = encode_part(&header)?; - let encoded_claims = encode_part(&claims)?; + let encoded_header = b64_encode_part(&header)?; + let encoded_claims = b64_encode_part(&claims)?; let message = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); let signature = sign(&*message, key, header.alg)?; diff --git a/src/serialization.rs b/src/serialization.rs index a3a7d4d..22f83f4 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -15,18 +15,18 @@ pub struct TokenData { pub claims: T, } -pub(crate) fn encode(input: &[u8]) -> String { +pub(crate) fn b64_encode(input: &[u8]) -> String { base64::encode_config(input, base64::URL_SAFE_NO_PAD) } -pub(crate) fn decode(input: &str) -> Result> { +pub(crate) fn b64_decode(input: &str) -> Result> { base64::decode_config(input, base64::URL_SAFE_NO_PAD).map_err(|e| e.into()) } /// Serializes a struct to JSON and encodes it in base64 -pub(crate) fn encode_part(input: &T) -> Result { +pub(crate) fn b64_encode_part(input: &T) -> Result { let json = to_string(input)?; - Ok(encode(json.as_bytes())) + Ok(b64_encode(json.as_bytes())) } /// Decodes from base64 and deserializes from JSON to a struct AND a hashmap of Value so we can @@ -34,7 +34,7 @@ pub(crate) fn encode_part(input: &T) -> Result { pub(crate) fn from_jwt_part_claims, T: DeserializeOwned>( encoded: B, ) -> Result<(T, Map)> { - let s = String::from_utf8(decode(encoded.as_ref())?)?; + let s = String::from_utf8(b64_decode(encoded.as_ref())?)?; let claims: T = from_str(&s)?; let validation_map: Map<_, _> = from_str(&s)?;