use std::fs;
use std::sync::OnceLock;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TokenVerifierError {
#[error("RA public key not initialized")]
NotInitialized,
#[error("failed to read RA public key from {path}: {source}")]
KeyReadFailed {
path: String,
source: std::io::Error,
},
#[error("token verification failed: {0}")]
VerificationFailed(String),
#[error("token signature verification failed")]
SignatureInvalid,
#[error("token body is empty")]
EmptyBody,
#[error("token verification status is not pass")]
StatusNotPass,
#[error("base64 decode failed: {0}")]
Base64Error(String),
#[error("utf8 decode failed: {0}")]
Utf8Error(String),
#[error("json parse failed: {0}")]
JsonError(String),
}
static RA_PUBLIC_KEY_PEM: OnceLock<String> = OnceLock::new();
pub fn init_token_verifier() -> Result<(), TokenVerifierError> {
let path = crate::config_manager::get_agent_config()
.map(|c| c.ra_public_key_path.clone())
.unwrap_or_default();
if path.is_empty() {
return Err(TokenVerifierError::NotInitialized);
}
let pem = fs::read_to_string(&path).map_err(|e| TokenVerifierError::KeyReadFailed {
path: path.clone(),
source: e,
})?;
let _ = RA_PUBLIC_KEY_PEM.set(pem);
Ok(())
}
pub fn verify_token(token: &str) -> Result<serde_json::Value, TokenVerifierError> {
let ra_pub_key = RA_PUBLIC_KEY_PEM
.get()
.ok_or_else(|| {
log::error!("token verification failed: RA public key not initialized");
TokenVerifierError::NotInitialized
})?;
verify_token_with_key(token, ra_pub_key)
}
fn verify_token_with_key(
token: &str,
ra_pub_key: &str,
) -> Result<serde_json::Value, TokenVerifierError> {
let result =
token_verifier::manager::TokenManager::verify_token_with_pub_key(token, ra_pub_key)
.map_err(|e| {
log::error!("token verification failed: {}", e);
TokenVerifierError::VerificationFailed(e.to_string())
})?;
if !result.verify_result {
log::error!("token signature verification failed");
return Err(TokenVerifierError::SignatureInvalid);
}
let token_body = result.token_body.ok_or_else(|| {
log::error!("token verification failed: token body is empty");
TokenVerifierError::EmptyBody
})?;
match token_body.get("status").and_then(|v| v.as_str()) {
Some(s) if s == "pass" => Ok(token_body),
_ => {
log::error!("token verification failed: status is not 'pass'");
Err(TokenVerifierError::StatusNotPass)
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_token_not_initialized() {
let result = verify_token("any_token");
assert!(result.is_err());
match result.unwrap_err() {
TokenVerifierError::NotInitialized | TokenVerifierError::VerificationFailed(_) => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn test_init_token_verifier_no_config() {
let result = init_token_verifier();
assert!(result.is_err());
match result.unwrap_err() {
TokenVerifierError::NotInitialized | TokenVerifierError::KeyReadFailed { .. } => {}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn test_error_display_messages() {
assert!(format!("{}", TokenVerifierError::NotInitialized).contains("not initialized"));
assert!(format!("{}", TokenVerifierError::SignatureInvalid).contains("signature"));
assert!(format!("{}", TokenVerifierError::EmptyBody).contains("empty"));
assert!(format!("{}", TokenVerifierError::StatusNotPass).contains("not pass"));
}
#[test]
fn test_wrapped_error_messages() {
let err = TokenVerifierError::Base64Error("bad b64".to_string());
assert!(format!("{}", err).contains("bad b64"));
let err = TokenVerifierError::Utf8Error("bad utf8".to_string());
assert!(format!("{}", err).contains("bad utf8"));
let err = TokenVerifierError::JsonError("bad json".to_string());
assert!(format!("{}", err).contains("bad json"));
}
#[test]
fn test_error_debug_format() {
let err = TokenVerifierError::NotInitialized;
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("NotInitialized"));
}
#[test]
fn test_key_read_failed_display() {
let err = TokenVerifierError::KeyReadFailed {
path: "/etc/kdc/ra_key.pem".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "No such file"),
};
let msg = format!("{}", err);
assert!(msg.contains("/etc/kdc/ra_key.pem"));
assert!(msg.contains("No such file"));
}
#[test]
fn test_verification_failed_display() {
let err = TokenVerifierError::VerificationFailed("bad signature".to_string());
let msg = format!("{}", err);
assert!(msg.contains("bad signature"));
}
#[test]
fn test_verify_token_with_invalid_token() {
let result = verify_token("eyJhbGciOiJFUzI1NiJ9.invalid.signature");
assert!(result.is_err());
}
#[test]
fn test_all_error_variants_debug() {
assert!(format!("{:?}", TokenVerifierError::NotInitialized).contains("NotInitialized"));
assert!(format!("{:?}", TokenVerifierError::SignatureInvalid).contains("SignatureInvalid"));
assert!(format!("{:?}", TokenVerifierError::EmptyBody).contains("EmptyBody"));
assert!(format!("{:?}", TokenVerifierError::StatusNotPass).contains("StatusNotPass"));
assert!(format!("{:?}", TokenVerifierError::Base64Error("b64".to_string())).contains("Base64Error"));
assert!(format!("{:?}", TokenVerifierError::Utf8Error("utf8".to_string())).contains("Utf8Error"));
assert!(format!("{:?}", TokenVerifierError::JsonError("json".to_string())).contains("JsonError"));
assert!(format!("{:?}", TokenVerifierError::VerificationFailed("vf".to_string())).contains("VerificationFailed"));
}
#[test]
fn test_key_read_failed_preserves_context() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
let err = TokenVerifierError::KeyReadFailed {
path: "/protected/key.pem".to_string(),
source: io_err,
};
let debug = format!("{:?}", err);
assert!(debug.contains("KeyReadFailed"));
}
#[test]
fn test_verify_token_with_key_malformed() {
let ra_pub_key = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinvalid\n-----END PUBLIC KEY-----";
let result = verify_token_with_key("not-a-valid-token", ra_pub_key);
assert!(result.is_err());
}
#[test]
fn test_verify_token_with_key_empty() {
let ra_pub_key = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinvalid\n-----END PUBLIC KEY-----";
let result = verify_token_with_key("", ra_pub_key);
assert!(result.is_err());
}
#[test]
fn test_init_token_verifier_empty_ra_key_path() {
fn mock_get_agent_config() -> Option<&'static crate::config_manager::ConfigCenter> {
None
}
let _mock = mockrs::mock!(
crate::config_manager::get_agent_config,
mock_get_agent_config
);
let result = init_token_verifier();
assert!(result.is_err());
match result.unwrap_err() {
TokenVerifierError::NotInitialized => {}
other => panic!("expected NotInitialized, got: {:?}", other),
}
}
#[test]
fn test_init_token_verifier_invalid_key_path() {
fn mock_get_agent_config() -> Option<&'static crate::config_manager::ConfigCenter> {
None
}
let _mock = mockrs::mock!(
crate::config_manager::get_agent_config,
mock_get_agent_config
);
let result = init_token_verifier();
assert!(result.is_err());
}
#[test]
fn test_verify_token_with_key_valid_key_invalid_jwt() {
let group =
openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1).unwrap();
let ec_key = openssl::ec::EcKey::generate(&group).unwrap();
let pkey = openssl::pkey::PKey::from_ec_key(ec_key).unwrap();
let pub_pem = String::from_utf8(pkey.public_key_to_pem().unwrap()).unwrap();
let fake_jwt = "eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.fake_signature_here";
let result = verify_token_with_key(fake_jwt, &pub_pem);
assert!(result.is_err());
}
#[test]
fn test_verify_token_with_key_garbage_key() {
let result = verify_token_with_key("some.token.here", "not-a-valid-key");
assert!(result.is_err());
}
#[test]
fn test_init_token_verifier_key_read_failed() {
fn mock_get_agent_config() -> Option<&'static crate::config_manager::ConfigCenter> {
use crate::types::KdcLogLevel;
let config = crate::config_manager::ConfigCenter {
ip: "127.0.0.1".to_string(),
port: 8080,
ca_path: "/nonexistent".to_string(),
cert_path: "/nonexistent".to_string(),
private_path: "/nonexistent".to_string(),
crl_path: String::new(),
ra_public_key_path: "/nonexistent/ra_key.pem".to_string(),
log_path: "/tmp/kdc_test.log".to_string(),
data_path: "/tmp".to_string(),
plugins: vec![],
log_max_size: 10,
log_backup_count: 5,
min_log_level: KdcLogLevel::Info,
};
Some(Box::leak(Box::new(config)))
}
let _mock = mockrs::mock!(
crate::config_manager::get_agent_config,
mock_get_agent_config
);
let result = init_token_verifier();
assert!(result.is_err());
match result.unwrap_err() {
TokenVerifierError::KeyReadFailed { path, .. } => {
assert!(path.contains("/nonexistent/ra_key.pem"));
}
other => panic!("expected KeyReadFailed, got: {:?}", other),
}
}
#[test]
fn test_verify_token_with_key_empty_body_path() {
fn mock_verify(
_token: &str,
_pub_key: &str,
) -> Result<
token_verifier::entity::SimpleVerifyResult,
token_verifier::error::TokenVerificationError,
> {
Ok(token_verifier::entity::SimpleVerifyResult {
verify_result: true,
token_body: None,
token_header: None,
})
}
let _mock = mockrs::mock!(
token_verifier::manager::TokenManager::verify_token_with_pub_key,
mock_verify
);
let result = verify_token_with_key("some.token", "any_key");
assert!(result.is_err());
match result.unwrap_err() {
TokenVerifierError::EmptyBody => {}
other => panic!("expected EmptyBody, got: {:?}", other),
}
}
#[test]
fn test_verify_token_with_key_status_not_pass() {
fn mock_verify(
_token: &str,
_pub_key: &str,
) -> Result<
token_verifier::entity::SimpleVerifyResult,
token_verifier::error::TokenVerificationError,
> {
Ok(token_verifier::entity::SimpleVerifyResult {
verify_result: true,
token_body: Some(serde_json::json!({"status": "fail"})),
token_header: None,
})
}
let _mock = mockrs::mock!(
token_verifier::manager::TokenManager::verify_token_with_pub_key,
mock_verify
);
let result = verify_token_with_key("some.token", "any_key");
assert!(result.is_err());
match result.unwrap_err() {
TokenVerifierError::StatusNotPass => {}
other => panic!("expected StatusNotPass, got: {:?}", other),
}
}
#[test]
fn test_verify_token_with_key_success_path() {
fn mock_verify(
_token: &str,
_pub_key: &str,
) -> Result<
token_verifier::entity::SimpleVerifyResult,
token_verifier::error::TokenVerificationError,
> {
Ok(token_verifier::entity::SimpleVerifyResult {
verify_result: true,
token_body: Some(serde_json::json!({"status": "pass", "data": "test"})),
token_header: None,
})
}
let _mock = mockrs::mock!(
token_verifier::manager::TokenManager::verify_token_with_pub_key,
mock_verify
);
let result = verify_token_with_key("some.token", "any_key");
assert!(result.is_ok());
let body = result.unwrap();
assert_eq!(body["status"], "pass");
}
}