use std::time::{SystemTime, UNIX_EPOCH};
use elliptic_curve::pkcs8::LineEnding::CR;
use elliptic_curve::pkcs8::{AssociatedOid, EncodePublicKey};
use elliptic_curve::sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint};
use elliptic_curve::{CurveArithmetic, JwkParameters, PublicKey};
use jwt_simple::prelude::{
Duration, ECDSAP256PublicKeyLike, ECDSAP384PublicKeyLike, ES256PublicKey, ES384PublicKey,
VerificationOptions,
};
use super::signature_validator::SignatureValidation;
use crate::error::jwt_error::JWTError;
use crate::model::claims::{CustomClaims, JWTClaims, TryFromTrustedJWTSimpleClaims};
use crate::model::signing_algorithm::SigningKeyLength;
use crate::model::untrusted_token::untrusted_jwt::UntrustedJWT;
use crate::parser::es_pk_parser::parse_es_pk;
pub struct EsSignatureValidator<C: CurveArithmetic> {
key_length: SigningKeyLength,
key: PublicKey<C>,
}
impl<C> EsSignatureValidator<C>
where
C: CurveArithmetic + JwkParameters + AssociatedOid,
<C as CurveArithmetic>::AffinePoint: FromEncodedPoint<C>,
<C as elliptic_curve::Curve>::FieldBytesSize: ModulusSize,
<C as CurveArithmetic>::AffinePoint: ToEncodedPoint<C>,
{
pub fn new(key_length: SigningKeyLength, key: String) -> Result<Self, JWTError> {
let key = parse_es_pk(key)?;
Ok(Self { key_length, key })
}
}
impl<C> SignatureValidation for EsSignatureValidator<C>
where
C: CurveArithmetic + JwkParameters + AssociatedOid,
<C as CurveArithmetic>::AffinePoint: FromEncodedPoint<C>,
<C as elliptic_curve::Curve>::FieldBytesSize: ModulusSize,
<C as CurveArithmetic>::AffinePoint: ToEncodedPoint<C>,
{
fn validate(&self, token: String) -> Result<JWTClaims, JWTError> {
UntrustedJWT::new(token.as_str())
.and_then(|token| {
let str_key = self.key.to_public_key_pem(CR)?;
as_es_algorithm(&self.key_length, str_key.as_str())?
.verify_token(token.value().as_str())
})
.and_then(|claims| JWTClaims::try_from_trusted_jwt_simple_claims(claims, token))
}
}
fn full_tolerance() -> core::time::Duration {
core::time::Duration::from_secs(Duration::from_days(365 * 50).as_secs())
}
enum EsPublicKey {
ES256 { pk: ES256PublicKey },
ES384 { pk: ES384PublicKey },
}
impl EsPublicKey {
fn verify_token(
&self,
token: &str,
) -> Result<jwt_simple::claims::JWTClaims<CustomClaims>, JWTError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| full_tolerance());
let options = VerificationOptions {
accept_future: true,
time_tolerance: Some(Duration::from(now)),
..Default::default()
};
match self {
EsPublicKey::ES256 { pk } => pk
.verify_token::<CustomClaims>(token, Some(options))
.map_err(JWTError::from),
EsPublicKey::ES384 { pk } => pk
.verify_token::<CustomClaims>(token, Some(options))
.map_err(JWTError::from),
}
}
}
fn as_es_algorithm(key_length: &SigningKeyLength, key: &str) -> Result<EsPublicKey, JWTError> {
match (key_length, key) {
(SigningKeyLength::Len256, key) => Ok(EsPublicKey::ES256 {
pk: ES256PublicKey::from_pem(key)?,
}),
(SigningKeyLength::Len384, key) => Ok(EsPublicKey::ES384 {
pk: ES384PublicKey::from_pem(key)?,
}),
_ => Err(JWTError::InvalidKeyLength(format!(
"Invalid configuration: Key length {key_length:?} is not valid for algorithm ES"
))),
}
}
impl From<elliptic_curve::pkcs8::spki::Error> for crate::error::jwt_error::JWTError {
fn from(e: elliptic_curve::pkcs8::spki::Error) -> Self {
JWTError::EsPublicKeyParseFailed(format!("ES Public key parsing error: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::EsSignatureValidator;
use crate::model::signing_algorithm::SigningKeyLength;
use p256::NistP256;
use p384::NistP384;
const ES_PUBLIC_KEY_PEM: &str = "-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERqVXn+o+6zEOpWEsGw5CsB+wd8zO
jxu0uASGpiGP+wYfcc1unyMxcStbDzUjRuObY8DalaCJ9/J6UrkQkZBtZw==
-----END PUBLIC KEY-----";
const ES_384_PUBLIC_KEY_PEM: &str = "-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEjjUFluy6cFrvj/UQwTJoEhPLEJSrKkx0
Rqxc0HEHdFuj0dpj977K9LoYRvZopFk8roS1DW3wghytCEfnW6AWNiesqa4S4iux
oomerb1S3+mXzX4p5459w9axcam9qgjo
-----END PUBLIC KEY-----";
fn es256_validator() -> EsSignatureValidator<NistP256> {
EsSignatureValidator::new(SigningKeyLength::Len256, String::from(ES_PUBLIC_KEY_PEM))
.unwrap()
}
fn es384_validator() -> EsSignatureValidator<NistP384> {
EsSignatureValidator::new(
SigningKeyLength::Len384,
String::from(ES_384_PUBLIC_KEY_PEM),
)
.unwrap()
}
mod es_validator_256 {
use crate::error::jwt_error::JWTErrorEnumKind;
use crate::validator::es_validator::tests::es256_validator;
use crate::validator::es_validator::SignatureValidation;
use crate::validator::signature_validator::tests::assert_error_kind;
use test_case::test_case;
extern crate test_case;
#[test]
fn validation_ok_and_jwt_parsed() {
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJleHAiOjkyMDA5ODUyNH0.ba5H-gFMcWnu-02y_5ITZVX4FSw2d9zE6BJJCWulWOMMuXaSX4-lBVPCjMpSPBE6fNlnWg4Po6bUf0gRdZCAlg";
let validation = es256_validator().validate(token.to_string());
assert!(validation.is_ok());
let jwt_claims = validation.unwrap();
assert_eq!(jwt_claims.get_header("alg").unwrap(), "ES256");
assert_eq!(
jwt_claims.expiration().unwrap().to_string(),
"1999-02-27 06:55:24 UTC"
);
}
#[test]
fn validation_fails_when_token_not_sent() {
let token = "";
let validation = es256_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test]
fn unparseable_token_is_rejected() {
let token = "WhatAmI?AnInvalidToken!";
let validation = es256_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test]
fn validation_fails_on_invalid_signature() {
let token = "eyJraWQiOiI5OWNiY2EzYS1kZmM4LTQ3NjgtODQ4MC0yZmIwZWVjZTkyZjEiLCJhbGciOiJFUzI1NiJ9.ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJBbmlzaCBOYXRoIiwNCiAgImlhdCI6IDE1MTYyMzkwMjINCn0.OS4wEFUvkfHG_wOPFBDVq04F7NDUvvwFmTR147hoP6IjqmwkDBeslsN_5YMcqGR8wztvQYmPi785xJshja9uVA";
let validation = es256_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test]
fn validation_fails_on_missing_signature() {
let token = "eyJraWQiOiI5NDE2YWY5ZS1jMTZlLTRjM2UtYmNkNy1kODFmMTdkZjBhN2YiLCJhbGciOiJFUzI1NiJ9.ew0KfQ.X_HB6PXUBcMlFjPAZYFRpgJLIzZO6ADcrqnSS4Sq2nVdcQtjkknuwxptjyFpNU5iP7PAFjYwRSMubgh4cQHNqA";
let validation = es256_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.e30.zWty50iXfn_2RtgdvqJ-A7xhkqSeO2UtAa_W9-RASE7ofEkGArOffQUOtQv76v1tQXDUqlQFf9FOms4FKPaQKA"; "verifyValidToken 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJleHAiOjkyMDA5ODUyNH0.ba5H-gFMcWnu-02y_5ITZVX4FSw2d9zE6BJJCWulWOMMuXaSX4-lBVPCjMpSPBE6fNlnWg4Po6bUf0gRdZCAlg"; "exceptionNotThrownOnExpiredTokenWithNoClaimValidation 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.e30.zWty50iXfn_2RtgdvqJ-A7xhkqSeO2UtAa_W9-RASE7ofEkGArOffQUOtQv76v1tQXDUqlQFf9FOms4FKPaQKA"; "exceptionNotThrownOnMissingClaim 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJleHAiOjMyODAzOTQwOTh9.0rFDoK4mv20SBw2rhNIXcNb4gCIElIzXm9pZYyi0XJ3wvFP9-NmRtOBhBOhEiF4S6tpw2u1Mjvv05yQbqsWfRA"; "verifyNotExpiredToken 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJleHAiOjkyMDA5ODUyNH0.ba5H-gFMcWnu-02y_5ITZVX4FSw2d9zE6BJJCWulWOMMuXaSX4-lBVPCjMpSPBE6fNlnWg4Po6bUf0gRdZCAlg"; "exceptionNotThrownOnExpiredToken 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJuYmYiOjgyMDA5ODUyNH0.jpshS_OqCYjhHlji9vLFlQ4gRFeHXOwx4TzDGpz3lKzgIqGpYgXvuspVrDAZNsbt_WUq4XkynJj7I1SKXdljUg"; "verifyTokenWithNbf 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJuYmYiOjMyODAzOTQwOTh9.BFmaYwAqljy5T7CWwndf_5hwJljasmCNwyM8paOqtBULbyZBrdpT9ZK1co2lD60IdFvAivmEuWEZqpldK3CuJg"; "exceptionNotThrownNotYetActiveToken 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJmb28ifQ.1urtAG-oabmrWjH3OVxu--e0UPa5dpxXtZ7CFJGQxFG0IGzPM4LLyxBenWs-9Ue07dPXISuBa77yMTzmuqcLBQ"; "verifyTokenWithAud 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiZm9vIiwiYmxhaCJdfQ.wEQIdgpzCMK4O3f9obSVit7-IJTOvJm8fWY9j0w-9vJxyxU2zCto7oCx5B2Z8OmWS9oD4bvc1KlEpd_-i7Vi2A"; "verifyTokenWithAudArray 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiZm9vIGJhciIsImJsYWgiXX0.rtdfw_0B0OVyzRg86IfvXpymK4ukg5vVLxsCKtRj9soxk_4X-xSyDgv9sN_cTfTPOq1upsotUFBoWMhIEUve2Q"; "verifyTokenWithAudiencesWithSpaces 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiRm9vIiwiYmxhaCJdfQ.Wk6KIMh858nuaa2caoRNiUahZl9KlJbAZ5AgtSUMH-96hJs7naNfzeux-cZ-tuil6ePTLvjwsranIAvDWznamw"; "verifyTokenWithAudiencesCaseSensitive 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiYmxhaCJdfQ.SFdcVoIGlJTDAkao6QfjOQEDPq4lAzVjcWQOBXUZ3_EqIWUCJ6XEohhghuux8QbSDmhj36mVimrBbQ4IcyvYJw"; "exceptionNotThrownWhenInvalidAudToken 256")]
#[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJmb28iLCJuYmYiOjgyMDA5ODUyNCwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGVzIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjMyODAzOTQwOTh9.xfaGYUA1xGgYQjdiWFT8JT8AHdZochXx8VkvP1dnUgV-SmdefElAy3JaXKfpnVFM-7DvcnEeUYycQCNL3qS9lA"; "checkAllValidations 256")]
fn verify_valid_token_256(token: &str) {
let validation = es256_validator().validate(token.to_string());
assert!(validation.is_ok());
}
}
mod es_validator_384 {
use crate::error::jwt_error::JWTErrorEnumKind;
use crate::validator::es_validator::tests::es384_validator;
use crate::validator::es_validator::SignatureValidation;
use crate::validator::signature_validator::tests::assert_error_kind;
use test_case::test_case;
extern crate test_case;
#[test]
fn validation_ok() {
let token = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjU4NDMwNjg2LCJleHAiOjE2NTg5NDg2MzF9.Bm6E2fuWizA8WObF6tWl80Gyak79H6FaD4Fxn9d0Gr-IGih5PvntcXHHl18p1DYFLsrt9xPfIAAaWsIk6VfrtCNb65Y2nzqDJUCNcYtFBvxY8NPY2dRLC3Gl5HAqVvex";
let validation = es384_validator().validate(token.to_string());
assert!(validation.is_ok());
}
#[test]
fn validation_fails_when_token_not_sent() {
let token = "eyJraWQiOiI5NDE2YWY5ZS1jMTZlLTRjM2UtYmNkNy1kODFmMTdkZjBhN2YiLCJhbGciOiJFUzI1NiJ9.ew0KfQ.X_HB6PXUBcMlFjPAZYFRpgJLIzZO6ADcrqnSS4Sq2nVdcQtjkknuwxptjyFpNU5iP7PAFjYwRSMubgh4cQHNqA";
let validation = es384_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test]
fn unparseable_token_is_rejected() {
let token = "WhatAmI?AnInvalidToken!";
let validation = es384_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test]
fn validation_fails_on_invalid_signature() {
let token = "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFuaXNoIE5hdGgiLCJpYXQiOjE1MTYyMzkwMjJ9.6adcW-2ww22MC3n41o5b6wULVolXno4md0glQR1U9hfIpbyyWQS2U-947x4mnc6bx-pxKO9SjnLtv7rsvOjvP300K5WayPIISQ0lc7CDkKH2s3oT1Bw5gHVtKfYT5Gjv";
let validation = es384_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test]
fn validation_fails_on_missing_signature() {
let token = "";
let validation = es384_validator().validate(token.to_string());
assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
}
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.e30.LLv51B6Y7ksfwFNLeMuF_nWNVHjvZwU75MC6uVJCFqYag7RogTHuEMIc5PsE42fwAlsKaY8pWQbzLeMkl7-vC0fkMvj7YDIowWYE15gfHigzbrelwpgFZ8k6SzdIKZJ2"; "verifyValidToken 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJleHAiOjkyMDA5ODUyNH0._IIq-sOy4JExOTJphYrxcogGc7dEc80xNLqz0FmY_6FAgH_W1E6ihitVnMwAg1WUKJZAPIuLCZtdCiWi69aH5Uz_YGKPZjppOqETgHuoQ8C89z2gQ313LgoyBv-ebvpB"; "exceptionNotThrownOnExpiredTokenWithNoClaimValidation 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.e30.6BJi3OGsiXOyRwUfu663r9SL0NzuYkdOQ_MS46WfGSWBXHc8WS_VpKlt4wZXaZMOQKndDCPdKbvXSLnrJ0pNWGiwQ7IufviWxhXkMJL6jll9Jwf1-y8EXQt1NiY8aYvR"; "exceptionNotThrownOnMissingClaim 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJleHAiOjMyODAzOTQwOTh9.LxipZW5nkk-T-2mRxLzmEE44lKy4cc3eHLWI3LfDPHFO_qhZIqaGxhS7SwBk6HNCiJ61EOHq-YjehMa1xxaKupfa6n7MlVm9EqHhPXyONqfiH3BchxhqC1KbDv9TrKyz"; "verifyNotExpiredToken 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJleHAiOjkyMDA5ODUyNC4zMjF9.Xyn-wcsh_SCoBFJZkQUC5jeJYSuXpF1k7UbgQN0jwZJSBCCkxOYjzevuCXtZJsnySmYkyuP-7K6YPtLpiS7Nmqz--1AN6S6u4l3TO-20PjybFhEckZEGVYf98esmcwHm"; "exceptionNotThrownOnExpiredToken 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYmYiOjgyMDA5ODUyNC40MzN9.Db2CqHjYHIvB_pb2_p3GfI57KRHGa_MYJ3dF5Or4vM4JQG5E756Lm21_dSqYDJnA6roan7z92uuoqpFRRNmdHjTz8zgP_QBcdSQhUsogA3YlE27o-oveGsjDOWQG_88l"; "verifyTokenWithNbf 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYmYiOjMyODAzOTQwOTguMzQyfQ.H3DWycc7O1QY4_V8alWXQf9cuxYmRPkKdmaiQ8yTVj5LaOTDxoVeFYusQWC8NQ0ZbYrEqZNHLcZlTQZ55fsaYPXrRgLaeqCQ_BGeMJZqj8btzoZ8AKGUtWYbUhCD6UfL"; "exceptionNotThrownNotYetActiveToken 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJmb28ifQ.pdejdj6fkVgxQLzW_hj40tbk2Nc2WFcfrzdtIT2ZKbd9UA24nO_eYjz411eR8wN0uYMPlgkGXeMFRCw-TMmmTvhhqLy-3tgFfdMtfD6HkxVhFRSK1q7ZUWOfumNmspgR"; "verifyTokenWithAud 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZXRjIiwiZm9vIiwiYmxhaCJdfQ.KhcqpcQdiOPy_fiVjDqmETW1uxgKoVlQqF4aMvterke0i6GdD0d5Bst5_wxz4l4Q54Ybw6uLHRwXT7Afc9RTDPBQqRLf2CgidRu72NukcXufYpJyPsyelneETSAC855N"; "verifyTokenWithAudArray 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZXRjIiwiZm9vIGJhciIsImJsYWgiXX0.v-yysmA64_SJ0w9eZPL3NNQFCpJYweFvy7LJP84N_6NDA1RjAicAi7Gt6DIAdG-cFG0Jn8NJM4DdwRBJUycGrOILCOgmio-bh63S-FwBLk3ExAJzrAr_yiMWLEvekgR6"; "verifyTokenWithAudiencesWithSpaces 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZXRjIiwiRm9vIiwiYmxhaCJdfQ.irwNz94eGFFXblZSxC4OGAs3pzR07b1VAaEGfzu_2QcTAHqMSLw1c7HmeBDczwNY89X1smiVhFx6R1COJ8CA9c1h9vrYe1ED_LfElVfvTSpYHNcPWgBLFsvQI7k98xL4"; "verifyTokenWithAudiencesCaseSensitive 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiZXRjIiwiYmxhaCJdfQ.9C3CuglaBOiKoJQycEjVg2NOap2RbQVhSdqJk6V3tC3mK3OzxFouPuzb7ZLNlJh73UbD-gHXZxieFTNAc60dcYVt-tuUMYA9Atp-FZTeoZ973a-KI_DHb9BLiLo0hgYi"; "exceptionNotThrownWhenInvalidAudToken 384")]
#[test_case("eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJmb28iLCJuYmYiOjgyMDA5ODUyNCwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGVzIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjMyODAzOTQwOTh9.4_AS9rscqomVqrAuF3JuV9WT-dahxNik3U5ou-nLmYK8Is-7ddkP1tLfQm5ufcRUXrN9HbyXdNy9Afr8TP0Vd72v45HKEZancXaghzlhmGGROesshVwBRltC1XLYEsIE"; "checkAllValidations 384")]
fn verify_valid_token_384(token: &str) {
let validation = es384_validator().validate(token.to_string());
assert!(validation.is_ok());
}
}
}