pdk-jwt-lib 1.10.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 base64::{decode_config, URL_SAFE_NO_PAD};

use crate::{
    api::model::{JWTClaims, SigningAlgorithm, SigningKeyLength},
    error::jwt_error::JWTError,
    model::{
        claims::{RawClaims, TryFromRawClaims},
        untrusted_token::untrusted_jwt_raw::UntrustedJWT,
    },
};
use pdk_core::ffi::{self, Message, ProtobufError};

use super::signature_validator::SignatureValidation;

/// Encode a DER length header (short form < 128, else long form).
fn der_encode_length(len: usize) -> Vec<u8> {
    if len < 0x80 {
        vec![len as u8]
    } else {
        let mut len_bytes = Vec::new();
        let mut n = len;
        while n > 0 {
            len_bytes.insert(0, (n & 0xff) as u8);
            n >>= 8;
        }
        let mut out = vec![0x80 | len_bytes.len() as u8];
        out.extend(len_bytes);
        out
    }
}

/// DER-encode a single unsigned big-endian integer (minimal length: leading
/// zeros trimmed, a 0x00 prefix added when the high bit is set).
fn der_encode_integer(bytes: &[u8]) -> Vec<u8> {
    let mut start = 0;
    while start + 1 < bytes.len() && bytes[start] == 0 {
        start += 1;
    }
    let trimmed = &bytes[start..];

    let mut content = Vec::with_capacity(trimmed.len() + 1);
    if trimmed[0] & 0x80 != 0 {
        content.push(0x00);
    }
    content.extend_from_slice(trimmed);

    let mut out = vec![0x02];
    out.extend(der_encode_length(content.len()));
    out.extend(content);
    out
}

/// Convert a raw JWS ECDSA signature (`r || s`, each `coordinate_size` bytes)
/// into an ASN.1 DER `SEQUENCE { INTEGER r, INTEGER s }`, the encoding expected
/// by the host verify_signature ABI. Replaces the p256/p384/p521 `to_der` path.
fn raw_ecdsa_to_der(signature: &[u8], coordinate_size: usize) -> Result<Vec<u8>, JWTError> {
    if signature.len() != coordinate_size * 2 {
        return Err(JWTError::ValidationFailed(
            "Invalid ECDSA signature length".to_string(),
        ));
    }

    let (r, s) = signature.split_at(coordinate_size);
    let mut content = der_encode_integer(r);
    content.extend(der_encode_integer(s));

    let mut out = vec![0x30];
    out.extend(der_encode_length(content.len()));
    out.extend(content);
    Ok(out)
}

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/HexEncodedDer are exercised only by this module's tests; production
    // callers always pass a PEM (see SignatureValidator::new).
    #[cfg_attr(not(test), allow(dead_code))]
    Der(Vec<u8>),
    #[cfg_attr(not(test), allow(dead_code))]
    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> {
        // `default()`, not the unit literal: under `#[cfg(test)]` faux replaces
        // ProxyWasmCall with a generated (non-unit) type whose Default is a real
        // constructor. clippy's unit-struct lint is a false positive here.
        #[allow(clippy::default_constructed_unit_structs)]
        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();

        // Structural validation (three segments, decodable header). Also guards
        // the signature/claims splits below.
        UntrustedJWT::new(&token)?;

        let raw_signature = {
            let signature_b64 = token.rsplit_once('.').ok_or(JWTError::BearerMalformed)?.1;
            decode_config(signature_b64, URL_SAFE_NO_PAD)
                .map_err(|e| JWTError::ValidationFailed(e.to_string()))?
        };

        let signature = if let SigningAlgorithm::Es = self.algorithm {
            // JWS packs ECDSA signatures as raw r||s; the host expects DER.
            match self.key_length {
                SigningKeyLength::Len256 => raw_ecdsa_to_der(&raw_signature, 32)?,
                SigningKeyLength::Len384 => raw_ecdsa_to_der(&raw_signature, 48)?,
                SigningKeyLength::Len512 => raw_ecdsa_to_der(&raw_signature, 66)?,
            }
        } else {
            raw_signature
        };

        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_b64 = token
                        .split('.')
                        .nth(1)
                        .ok_or_else(|| JWTError::ValidationFailed("Invalid token".to_string()))?;
                    let claims_bytes = decode_config(claims_b64, URL_SAFE_NO_PAD)
                        .map_err(|_| JWTError::ValidationFailed("Invalid token".to_string()))?;
                    let raw = RawClaims::from_json_slice(&claims_bytes)
                        .map_err(|_| JWTError::ValidationFailed("Invalid token".to_string()))?;
                    let claims = JWTClaims::try_from_raw_claims(raw, 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());
    }
}