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;
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
}
}
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
}
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),
#[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> {
#[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();
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 {
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());
}
}