pdk-jwt-lib 1.9.0

PDK JWT Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

//! # Proxy WASM Signature Validator
//!
//! The following signature validation relies on a proxy_wasm FFI with the following signature:
//! verify_signature([`pdk_core::ffi::VerifySignatureArguments`]) -> [`pdk_core::ffi::VerifySignatureResult`]
//! FIPS compliance is not guaranteed by this validator. FIPS compliance needs to be supported by
//! the host implementing the verify_signature ABI. Envoy with FIPS enabled provides such features.

use std::convert::TryFrom;

use crate::{
    api::model::{JWTClaims, SigningAlgorithm, SigningKeyLength},
    error::jwt_error::JWTError,
    model::{
        claims::CustomClaims, claims::TryFromUntrustedJWTCompactClaims,
        untrusted_token::untrusted_jwt_compact::UntrustedJWT,
    },
};
use jwt_compact::{Claims, UntrustedToken};
use pdk_core::ffi::{self, Message, ProtobufError};

use super::signature_validator::SignatureValidation;

mod hostcall {

    #[cfg_attr(test, faux::create)]
    #[derive(Default)]
    pub struct ProxyWasmCall;

    #[cfg_attr(test, faux::methods)]
    impl ProxyWasmCall {
        pub fn call(
            &self,
            function_name: &str,
            arguments: Option<&[u8]>,
        ) -> Result<
            Option<pdk_core::classy::proxy_wasm::types::Bytes>,
            pdk_core::classy::proxy_wasm::types::Status,
        > {
            pdk_core::classy::proxy_wasm::hostcalls::call_foreign_function(function_name, arguments)
        }
    }
}

pub struct ProxyWasmSignatureValidator {
    algorithm: SigningAlgorithm,
    key_length: SigningKeyLength,
    pk_der: Vec<u8>,
    ffi: hostcall::ProxyWasmCall,
}

pub enum Key {
    Pem(String),
    Der(Vec<u8>),
    HexEncodedDer(String),
}

impl ProxyWasmSignatureValidator {
    fn new_with_ffi(
        algorithm: SigningAlgorithm,
        key_length: SigningKeyLength,
        key: Key,
        ffi: hostcall::ProxyWasmCall,
    ) -> Result<Self, JWTError> {
        let pk_der = match key {
            Key::Pem(pem) => match algorithm {
                SigningAlgorithm::Es => crate::parser::es_pk_parser::parse_pem(pem)?
                    .contents()
                    .to_vec(),
                SigningAlgorithm::Rsa => crate::parser::rsa_pk_parser::parse_pem(pem)
                    .ok_or(JWTError::SigningKeyLengthParseFailed)?
                    .contents()
                    .to_vec(),
            },
            Key::HexEncodedDer(encoded) => const_hex::decode(encoded)
                .map_err(|_| JWTError::SigningKeyLengthParseFailed)?
                .to_vec(),
            Key::Der(der) => der,
        };
        Ok(Self {
            algorithm,
            key_length,
            pk_der,
            ffi,
        })
    }

    pub fn new(
        algorithm: SigningAlgorithm,
        key_length: SigningKeyLength,
        key: Key,
    ) -> Result<Self, JWTError> {
        Self::new_with_ffi(
            algorithm,
            key_length,
            key,
            hostcall::ProxyWasmCall::default(),
        )
    }
}

impl SignatureValidation for ProxyWasmSignatureValidator {
    fn validate(&self, token: String) -> Result<JWTClaims, JWTError> {
        let data = token
            .rsplit_once('.')
            .ok_or(JWTError::BearerMalformed)?
            .0
            .as_bytes();

        let untrusted_token: UntrustedToken = UntrustedJWT::new(&token)?.into();

        let signature = if let SigningAlgorithm::Es = self.algorithm {
            let signature = untrusted_token.signature_bytes();
            match self.key_length {
                SigningKeyLength::Len256 => p256::ecdsa::Signature::try_from(signature)
                    .map_err(|e| JWTError::ValidationFailed(e.to_string()))?
                    .to_der()
                    .to_bytes()
                    .to_vec(),
                SigningKeyLength::Len384 => p384::ecdsa::Signature::try_from(signature)
                    .map_err(|e| JWTError::ValidationFailed(e.to_string()))?
                    .to_der()
                    .to_bytes()
                    .to_vec(),
                SigningKeyLength::Len512 => p521::ecdsa::Signature::try_from(signature)
                    .map_err(|e| JWTError::ValidationFailed(e.to_string()))?
                    .to_der()
                    .to_bytes()
                    .to_vec(),
            }
        } else {
            untrusted_token.signature_bytes().to_vec()
        };

        let args = ffi::VerifySignatureArguments {
            hash_function: match self.key_length {
                SigningKeyLength::Len256 => "sha256",
                SigningKeyLength::Len384 => "sha384",
                SigningKeyLength::Len512 => "sha512",
            }
            .to_string(),
            signature,
            public_key: self.pk_der.clone(),
            text: data.to_vec(),
            ..Default::default()
        };

        let args = args.write_to_bytes()?;

        match self.ffi.call("verify_signature", Some(&args)) {
            Ok(Some(bytes)) => match ffi::VerifySignatureResult::parse_from_bytes(&bytes) {
                Ok(result) if result.result => {
                    let claims: Claims<CustomClaims> =
                        untrusted_token.deserialize_claims_unchecked()?;
                    let claims = JWTClaims::try_from_untrusted_jwt_compact_claims(claims, token)?;
                    Ok(claims)
                }
                Err(_) => Err(JWTError::ValidationFailed(
                    "Invalid deserialization".to_string(),
                )),
                Ok(_) => Err(JWTError::ValidationFailed("Invalid token".to_string())),
            },
            Ok(None) => Err(JWTError::ValidationFailed("Invalid token".to_string())),
            Err(_) => Err(JWTError::ValidationFailed(
                "Invalid serialization".to_string(),
            )),
        }
    }
}

impl From<ProtobufError> for JWTError {
    fn from(value: ProtobufError) -> Self {
        Self::ValidationFailed(value.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pdk_core::ffi::{Message, VerifySignatureResult};

    #[test]
    fn hex_encoded() {
        let encoded_der = "30819b301006072a8648ce3d020106052b8104002303818600040181ce07673fbf7c16c2ee5984c30d003b734af705593c36745449ec69414b118a53cdbf749c84ed25470f51bbceb1d64867543c7e76eab80620c572f95201cca32c0089be3be8c932c9880f93405766ade6ab8cc899344db9a53db258fd4d527749c4f72f56d7c15b8f243ce9af918e1696a3f96c0c025f06ec2ab0a093ac5bb29d767b";
        let token = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTcxNjIzOTAyMiwicm9sZSI6Ik1lbWJlciJ9.ASFo8KygRitPeJw2HG_xJct9VXGupGriBzn9AV0W3jsApPhTVZFVFj5s-P-KaoJ2-39_CbMTas0mCF08jyy68J4VAB9h1fDWwIDqofWUWGnPr2NJABsK3ecUOE0j6g1AXjWZaEDHeDxoE4eMqnMn3H7xJGfITDWXHNR6fvbpCsX_aYsQ".to_string();

        let mut mock = hostcall::ProxyWasmCall::faux();

        faux::when!(mock.call("verify_signature", _)).then(|(_, _)| {
            let result = VerifySignatureResult {
                result: true,
                ..Default::default()
            };
            Ok(Some(result.write_to_bytes().unwrap()))
        });

        let validator = ProxyWasmSignatureValidator::new_with_ffi(
            SigningAlgorithm::Es,
            SigningKeyLength::Len512,
            Key::HexEncodedDer(encoded_der.to_string()),
            mock,
        )
        .unwrap();

        assert!(validator.validate(token).is_ok());
    }

    #[test]
    fn der() {
        let encoded_der = "30819b301006072a8648ce3d020106052b8104002303818600040181ce07673fbf7c16c2ee5984c30d003b734af705593c36745449ec69414b118a53cdbf749c84ed25470f51bbceb1d64867543c7e76eab80620c572f95201cca32c0089be3be8c932c9880f93405766ade6ab8cc899344db9a53db258fd4d527749c4f72f56d7c15b8f243ce9af918e1696a3f96c0c025f06ec2ab0a093ac5bb29d767b";
        let token = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTcxNjIzOTAyMiwicm9sZSI6Ik1lbWJlciJ9.ASFo8KygRitPeJw2HG_xJct9VXGupGriBzn9AV0W3jsApPhTVZFVFj5s-P-KaoJ2-39_CbMTas0mCF08jyy68J4VAB9h1fDWwIDqofWUWGnPr2NJABsK3ecUOE0j6g1AXjWZaEDHeDxoE4eMqnMn3H7xJGfITDWXHNR6fvbpCsX_aYsQ".to_string();

        let mut mock = hostcall::ProxyWasmCall::faux();

        faux::when!(mock.call("verify_signature", _)).then(|(_, _)| {
            let result = VerifySignatureResult {
                result: true,
                ..Default::default()
            };
            Ok(Some(result.write_to_bytes().unwrap()))
        });

        let validator = ProxyWasmSignatureValidator::new_with_ffi(
            SigningAlgorithm::Es,
            SigningKeyLength::Len512,
            Key::Der(const_hex::decode(encoded_der).unwrap()),
            mock,
        )
        .unwrap();

        assert!(validator.validate(token).is_ok());
    }

    #[test]
    fn pem() {
        let pem = r#"-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
Al8G7CqwoJOsW7Kddns=
-----END PUBLIC KEY-----"#;
        let token = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTcxNjIzOTAyMiwicm9sZSI6Ik1lbWJlciJ9.ASFo8KygRitPeJw2HG_xJct9VXGupGriBzn9AV0W3jsApPhTVZFVFj5s-P-KaoJ2-39_CbMTas0mCF08jyy68J4VAB9h1fDWwIDqofWUWGnPr2NJABsK3ecUOE0j6g1AXjWZaEDHeDxoE4eMqnMn3H7xJGfITDWXHNR6fvbpCsX_aYsQ".to_string();

        let mut mock = hostcall::ProxyWasmCall::faux();

        faux::when!(mock.call("verify_signature", _)).then(|(_, _)| {
            let result = VerifySignatureResult {
                result: true,
                ..Default::default()
            };
            Ok(Some(result.write_to_bytes().unwrap()))
        });

        let validator = ProxyWasmSignatureValidator::new_with_ffi(
            SigningAlgorithm::Es,
            SigningKeyLength::Len512,
            Key::Pem(pem.to_string()),
            mock,
        )
        .unwrap();

        assert!(validator.validate(token).is_ok());
    }

    #[test]
    fn invalid_token() {
        let pem = r#"-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
Al8G7CqwoJOsW7Kddns=
-----END PUBLIC KEY-----"#;
        let token = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTcxNjIzOTAyMiwicm9sZSI6Ik1lbWJlciJ9.ASFo8KygRitPeJw2HG_xJct9VXGupGriBzn9AV0W3jsApPhTVZFVFj5s-P-KaoJ2-39_CbMTas0mCF08jyy68J4VAB9h1fDWwIDqofWUWGnPr2NJABsK3ecUOE0j6g1AXjWZaEDHeDxoE4eMqnMn3H7xJGfITDWXHNR6fvbpCsX_aYsQ".to_string();

        let mut mock = hostcall::ProxyWasmCall::faux();

        faux::when!(mock.call("verify_signature", _)).then(|(_, _)| {
            let result = VerifySignatureResult {
                result: false,
                ..Default::default()
            };
            Ok(Some(result.write_to_bytes().unwrap()))
        });

        let validator = ProxyWasmSignatureValidator::new_with_ffi(
            SigningAlgorithm::Es,
            SigningKeyLength::Len512,
            Key::Pem(pem.to_string()),
            mock,
        )
        .unwrap();

        assert!(validator.validate(token).is_err());
    }

    #[test]
    fn malformed_token() {
        let pem = r#"-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
Al8G7CqwoJOsW7Kddns=
-----END PUBLIC KEY-----"#;
        let token = "malformed".to_string();

        let mut mock = hostcall::ProxyWasmCall::faux();

        faux::when!(mock.call("verify_signature", _)).then(|(_, _)| {
            let result = VerifySignatureResult {
                result: true,
                ..Default::default()
            };
            Ok(Some(result.write_to_bytes().unwrap()))
        });

        let validator = ProxyWasmSignatureValidator::new_with_ffi(
            SigningAlgorithm::Es,
            SigningKeyLength::Len512,
            Key::Pem(pem.to_string()),
            mock,
        )
        .unwrap();

        assert!(validator.validate(token).is_err());
    }
}