use std::fs;
use std::sync::OnceLock;
use std::time::Duration;
use openssl::ec::EcKey;
use openssl::pkey::PKey;
use parking_lot::Mutex;
use reqwest::blocking::Client;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use common::constants::{CommonError, MAX_INNER_REQUEST_SIZE, PSK_LEN, TRUSTED_REGISTER_PSK_REQ};
use common::http_types::{KdcHttpRequest, KdcHttpResponse};
use crate::config_manager::get_config_center;
use crate::proxy_log;
use crate::types::{
KDC_LOG_DEBUG, KDC_LOG_ERROR, KDC_LOG_INFO, KDC_LOG_WARN, KDC_PROTOCOL_VERSION,
};
use common::psk::{psk_decrypt_with_key, psk_encrypt_with_key};
static KDC_AGENT_CLIENT: OnceLock<Mutex<Option<Client>>> = OnceLock::new();
fn get_client_holder() -> &'static Mutex<Option<Client>> {
KDC_AGENT_CLIENT.get_or_init(|| Mutex::new(None))
}
fn build_kdc_agent_client() -> Result<Client, CommonError> {
let config = get_config_center().lock();
if config.capath.is_empty() {
proxy_log!(KDC_LOG_ERROR, "CA certificate path is empty");
return Err(CommonError::ConfigError);
}
let ca_pem = fs::read(&config.capath).map_err(|e| {
proxy_log!(
KDC_LOG_ERROR,
&format!("failed to read CA certificate file '{}': {}", config.capath, e)
);
CommonError::ConfigError
})?;
let has_client_cert = !config.certpath.is_empty() && !config.privatepath.is_empty();
let client_cert = if has_client_cert {
let cert_pem = fs::read(&config.certpath).map_err(|e| {
proxy_log!(
KDC_LOG_ERROR,
&format!("failed to read client certificate file '{}': {}", config.certpath, e)
);
CommonError::ConfigError
})?;
let key_pem = fs::read(&config.privatepath).map_err(|e| {
proxy_log!(
KDC_LOG_ERROR,
&format!("failed to read private key file '{}': {}", config.privatepath, e)
);
CommonError::ConfigError
})?;
let key_pwd = config.key_pwd.clone();
Some((cert_pem, key_pem, key_pwd))
} else {
None
};
let crlpath = config.crlpath.clone();
drop(config);
let ca_cert = reqwest::Certificate::from_pem(&ca_pem).map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to parse CA certificate PEM: {:?}", e));
CommonError::ConfigError
})?;
let mut builder = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(30))
.pool_idle_timeout(Duration::from_secs(7200))
.add_root_certificate(ca_cert);
if let Ok(crl_bytes) = fs::read(&crlpath) {
if let Ok(crls) = reqwest::tls::CertificateRevocationList::from_pem_bundle(&crl_bytes) {
if !crls.is_empty() {
for crl in crls { builder = builder.add_crl(crl); }
proxy_log!(KDC_LOG_DEBUG, "CRL loaded for client verification");
} else {
proxy_log!(
KDC_LOG_WARN,
&format!(
"CRL file '{}' is empty or contains no valid CRL entries, treating as no CRL",
crlpath
)
);
}
}
}
if let Some((cert_pem, key_pem, key_pwd)) = client_cert {
let decrypted_key_pem = if !key_pwd.is_empty() {
proxy_log!(KDC_LOG_DEBUG, "decrypting private key with password");
let rsa = PKey::private_key_from_pem_passphrase(&key_pem, key_pwd.as_bytes())
.map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to decrypt private key: {}", e));
CommonError::ConfigError
})?;
rsa.private_key_to_pem_pkcs8()
.map_err(|e| {
proxy_log!(
KDC_LOG_ERROR,
&format!("failed to convert private key to PKCS8 PEM: {}", e)
);
CommonError::ConfigError
})?
} else {
key_pem
};
let mut pem_buf = cert_pem;
pem_buf.extend_from_slice(&decrypted_key_pem);
let identity =
reqwest::Identity::from_pem(&pem_buf).map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to create client identity: {:?}", e));
CommonError::ConfigError
})?;
builder = builder.identity(identity);
}
let client = builder.build().map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to build HTTPS client: {:?}", e));
CommonError::ConfigError
})?;
Ok(client)
}
fn ensure_client() -> Result<Client, CommonError> {
let mut guard = get_client_holder().lock();
if let Some(ref client) = *guard {
proxy_log!(KDC_LOG_DEBUG, "reusing cached HTTPS client");
return Ok(client.clone());
}
let client = build_kdc_agent_client()?;
*guard = Some(client.clone());
proxy_log!(KDC_LOG_DEBUG, "HTTPS client created successfully");
Ok(client)
}
fn prepare_request_data(
psk: Option<[u8; PSK_LEN]>,
data: Option<&str>,
) -> Result<Option<String>, CommonError> {
match (psk, data) {
(Some(key), Some(payload)) => {
let encrypted = psk_encrypt_with_key(payload.as_bytes(), &key)
.map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to encrypt request data: {}", e));
CommonError::InternalError
})?;
Ok(Some(BASE64_STANDARD.encode(&encrypted)))
}
_ => Ok(data.map(String::from)),
}
}
fn decrypt_response_data(
psk: Option<[u8; PSK_LEN]>,
data: Option<String>,
) -> Result<Option<String>, CommonError> {
match (psk, &data) {
(Some(key), Some(enc_data)) => {
let decoded = BASE64_STANDARD.decode(enc_data).map_err(|e| {
proxy_log!(
KDC_LOG_ERROR,
&format!("failed to decode base64 response data: {}", e)
);
CommonError::InternalError
})?;
let decrypted =
psk_decrypt_with_key(&decoded, &key).map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to decrypt response data: {}", e));
CommonError::InternalError
})?;
Ok(Some(
String::from_utf8(decrypted).map_err(|e| {
proxy_log!(
KDC_LOG_ERROR,
&format!("failed to convert decrypted data to UTF-8: {}", e)
);
CommonError::InternalError
})?,
))
}
_ => Ok(data),
}
}
pub fn send_to_kdc_agent(
msg_type: u32,
data: Option<&str>,
) -> Result<KdcHttpResponse, CommonError> {
let config = get_config_center().lock();
let url = config.kdc_agent_url.clone();
let psk = config.psk.as_ref().map(|s| s.0);
drop(config);
if url.is_empty() {
proxy_log!(KDC_LOG_ERROR, "kdc_agent URL not configured");
return Err(CommonError::ConfigError);
}
let effective_psk = if msg_type == TRUSTED_REGISTER_PSK_REQ {
None
} else {
psk
};
let task_id = uuid::Uuid::new_v4().to_string();
proxy_log!(KDC_LOG_INFO, &format!("sending request type=0x{:04X} to {}", msg_type, url));
let encrypted_data = prepare_request_data(effective_psk, data)?;
let request = KdcHttpRequest {
task_id,
version: KDC_PROTOCOL_VERSION.to_string(),
msg_type,
data: encrypted_data,
};
let body_bytes = serde_json::to_vec(&request).map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to serialize request: {}", e));
CommonError::InternalError
})?;
if body_bytes.len() > MAX_INNER_REQUEST_SIZE {
proxy_log!(
KDC_LOG_ERROR,
&format!(
"request body too large: {} bytes (max {})",
body_bytes.len(),
MAX_INNER_REQUEST_SIZE
)
);
return Err(CommonError::RequestError);
}
let client = ensure_client()?;
let response = client
.post(&url)
.body(body_bytes)
.header("Content-Type", "application/json")
.send()
.map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("HTTPS request failed: {:?}", e));
CommonError::NetworkError
})?;
if !response.status().is_success() {
let status = response.status();
proxy_log!(KDC_LOG_ERROR, &format!("HTTPS request returned non-success status: {}", status));
return Err(CommonError::NetworkError);
}
let mut kdc_response: KdcHttpResponse = response.json().map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to parse response: {:?}", e));
CommonError::NetworkError
})?;
if kdc_response.ret_code == CommonError::PskMismatchError as u32 {
proxy_log!(KDC_LOG_WARN, "PSK mismatch detected, re-registration needed");
return Err(CommonError::PskMismatchError);
}
kdc_response.data = decrypt_response_data(effective_psk, kdc_response.data)?;
proxy_log!(
KDC_LOG_INFO, &format!(
"request type=0x{:04X} completed, ret_code={}, ret_msg={}",
msg_type, kdc_response.ret_code, kdc_response.ret_msg)
);
Ok(kdc_response)
}
pub fn request_token_from_ra_agent(
ec_key: Option<&EcKey<openssl::pkey::Private>>,
) -> Result<String, CommonError> {
let config = get_config_center().lock();
let url = config.ra_agent_url.clone();
drop(config);
if url.is_empty() {
proxy_log!(KDC_LOG_ERROR, "RA agent URL not configured");
return Err(CommonError::ConfigError);
}
let ra_body = match ec_key {
Some(key) => {
let pub_pkey = PKey::from_ec_key(key.clone()).map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to get PKey: {}", e));
CommonError::InternalError
})?;
let pub_pem = pub_pkey.public_key_to_pem().map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to get PEM: {}", e));
CommonError::InternalError
})?;
let pub_pem_str = String::from_utf8(pub_pem).map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("invalid PEM UTF-8: {}", e));
CommonError::InternalError
})?;
proxy_log!(KDC_LOG_DEBUG, "ra_body: [REDACTED - contains public key]");
build_ra_request_body(&pub_pem_str)
}
None => {
serde_json::json!({
"attester_info": [{"attester_type": "virt_cca"}],
"challenge": false,
"token_fmt": "eat"
})
}
};
proxy_log!(KDC_LOG_INFO, &format!("sending attestation request to RA agent: {}", url));
let client = ensure_client()?;
let response = client.post(&url).json(&ra_body).send().map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("RA agent request failed: {:?}", e));
CommonError::NetworkError
})?;
let ra_response: serde_json::Value = response.json().map_err(|e| {
proxy_log!(KDC_LOG_ERROR, &format!("failed to parse RA agent response: {:?}", e));
CommonError::NetworkError
})?;
let token = ra_response
.get("token")
.and_then(|v| v.as_str())
.ok_or_else(|| {
let msg = ra_response
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("unknown error");
proxy_log!(KDC_LOG_ERROR, &format!("RA agent returned error message: {}", msg));
CommonError::InternalError
})?
.to_string();
proxy_log!(KDC_LOG_INFO, "received attestation token from RA agent");
Ok(token)
}
pub fn build_ra_request_body(pub_pem_str: &str) -> serde_json::Value {
serde_json::json!({
"attester_info": [{"attester_type": "virt_cca"}],
"challenge": true,
"token_fmt": "eat",
"attester_data": {"public_key": pub_pem_str}
})
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use common::psk::generate_random_psk;
use zeroize::Zeroizing;
use crate::config_manager::SecurePsk;
fn reset_config() {
let config = get_config_center();
let mut guard = config.lock();
guard.capath = String::new();
guard.certpath = String::new();
guard.privatepath = String::new();
guard.key_pwd = Zeroizing::new(String::new());
guard.psk = None;
guard.kdc_agent_url = String::new();
guard.ra_agent_url = String::new();
}
fn reset_client_holder() {
let holder = get_client_holder();
let mut guard = holder.lock();
*guard = None;
}
fn create_self_signed_ca(
temp_name: &str,
) -> (
std::path::PathBuf,
openssl::pkey::PKey<openssl::pkey::Private>,
) {
use openssl::asn1::Asn1Time;
use openssl::bn::BigNum;
use openssl::x509::X509Builder;
let rsa = openssl::rsa::Rsa::generate(2048).unwrap();
let pkey = openssl::pkey::PKey::from_rsa(rsa).unwrap();
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
let serial = BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap();
builder.set_serial_number(&serial).unwrap();
builder.set_pubkey(&pkey).unwrap();
builder
.set_not_before(Asn1Time::days_from_now(0).unwrap().as_ref())
.unwrap();
builder
.set_not_after(Asn1Time::days_from_now(365).unwrap().as_ref())
.unwrap();
builder
.sign(&pkey, openssl::hash::MessageDigest::sha256())
.unwrap();
let cert = builder.build();
let pem = cert.to_pem().unwrap();
let path = std::env::temp_dir().join(temp_name);
std::fs::write(&path, &pem).unwrap();
(path, pkey)
}
#[test]
fn test_build_ra_request_body() {
let pub_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n-----END PUBLIC KEY-----";
let body = build_ra_request_body(pub_key);
assert!(body.get("attester_info").is_some());
let info = body.get("attester_info").unwrap().as_array().unwrap();
assert_eq!(info[0]["attester_type"], "virt_cca");
assert_eq!(body["challenge"], true);
assert_eq!(body["token_fmt"], "eat");
assert_eq!(body["attester_data"]["public_key"], pub_key);
}
#[test]
fn test_psk_mismatch_detection() {
let json = r#"{"retCode": 2, "retMsg": "PSK mismatch"}"#;
let resp: KdcHttpResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.ret_code, CommonError::PskMismatchError as u32);
}
#[test]
fn test_prepare_request_data_without_psk() {
let result = prepare_request_data(None, Some("plaintext")).unwrap();
assert_eq!(result, Some("plaintext".to_string()));
}
#[test]
fn test_prepare_request_data_with_psk() {
let key = generate_random_psk().unwrap();
let result = prepare_request_data(Some(key), Some("secret message")).unwrap();
let encrypted_hex = result.unwrap();
assert_ne!(encrypted_hex, "secret message");
assert!(BASE64_STANDARD.decode(&encrypted_hex).is_ok());
}
#[test]
fn test_prepare_request_data_none_none() {
let result = prepare_request_data(None, None).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_decrypt_response_data_roundtrip() {
let key = generate_random_psk().unwrap();
let plaintext = "response payload";
let encrypted = psk_encrypt_with_key(plaintext.as_bytes(), &key).unwrap();
let encrypted_hex = BASE64_STANDARD.encode(&encrypted);
let decrypted = decrypt_response_data(Some(key), Some(encrypted_hex)).unwrap();
assert_eq!(decrypted, Some(plaintext.to_string()));
}
#[test]
fn test_decrypt_response_data_invalid_hex() {
let key = generate_random_psk().unwrap();
let result = decrypt_response_data(Some(key), Some("zz".to_string()));
assert!(result.is_err());
}
#[test]
fn test_decrypt_response_data_none_none() {
let result = decrypt_response_data(None, None).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_decrypt_response_data_none_some() {
let result = decrypt_response_data(None, Some("hello".to_string())).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_send_to_kdc_agent_no_config() {
reset_config();
let result = send_to_kdc_agent(0x0010, Some("test"));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
}
#[test]
fn test_a_send_to_kdc_agent_empty_url() {
reset_config();
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = String::new();
}
let result = send_to_kdc_agent(0x0010, Some("test"));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
reset_config();
}
#[test]
fn test_request_token_from_ra_agent_no_config() {
reset_config();
let (ec_key, _pub_pem) = crate::crypto::generate_ec_keypair().unwrap();
let result = request_token_from_ra_agent(Some(&ec_key));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
}
#[test]
fn test_build_client_needs_config() {
let result = build_kdc_agent_client();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
}
#[test]
fn test_ensure_client_no_config() {
reset_config();
let result = ensure_client();
assert!(result.is_err());
reset_config();
}
#[test]
fn test_a_ensure_client_creates_and_caches() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_ensure_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let client1 = ensure_client();
assert!(client1.is_ok(), "first ensure_client should succeed");
let client2 = ensure_client();
assert!(
client2.is_ok(),
"second ensure_client should return cached client"
);
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_build_kdc_agent_client_nonexistent_ca() {
reset_config();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = "/nonexistent/path/ca.pem".to_string();
}
let result = build_kdc_agent_client();
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_with_valid_ca() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_valid_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_ok());
let _ = std::fs::remove_file(&ca_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_cert_not_found() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_ca_cnf.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = "/nonexistent/cert.pem".to_string();
guard.privatepath = "/nonexistent/key.pem".to_string();
}
let result = build_kdc_agent_client();
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
let _ = std::fs::remove_file(&ca_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_key_not_found() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_ca_knf.pem");
let cert_path = std::env::temp_dir().join("kdc_test_cert_knf.pem");
let cert_pem = std::fs::read(&ca_path).unwrap();
std::fs::write(&cert_path, &cert_pem).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_path.to_str().unwrap().to_string();
guard.privatepath = "/nonexistent/key.pem".to_string();
}
let result = build_kdc_agent_client();
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&cert_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_with_cert_and_key() {
reset_config();
let (ca_path, pkey) = create_self_signed_ca("kdc_test_ca_ck.pem");
let key_pem = pkey.private_key_to_pem_pkcs8().unwrap();
let cert_pem = std::fs::read(&ca_path).unwrap();
let cert_path = std::env::temp_dir().join("kdc_test_cert_ck.pem");
let key_path = std::env::temp_dir().join("kdc_test_key_ck.pem");
std::fs::write(&cert_path, &cert_pem).unwrap();
std::fs::write(&key_path, &key_pem).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_path.to_str().unwrap().to_string();
guard.privatepath = key_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_ok());
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&cert_path);
let _ = std::fs::remove_file(&key_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_with_key_pwd() {
reset_config();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = "/nonexistent/ca.pem".to_string();
guard.certpath = "/nonexistent/cert.pem".to_string();
guard.privatepath = "/nonexistent/key.pem".to_string();
guard.key_pwd = Zeroizing::new("secret".to_string());
}
let result = build_kdc_agent_client();
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_with_encrypted_key() {
use openssl::asn1::Asn1Time;
use openssl::bn::BigNum;
use openssl::x509::X509Builder;
reset_config();
let password = "test_password";
let ca_path = std::env::temp_dir().join("kdc_test_ca_enc.pem");
let rsa = openssl::rsa::Rsa::generate(2048).unwrap();
let pkey = openssl::pkey::PKey::from_rsa(rsa).unwrap();
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
let serial = BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap();
builder.set_serial_number(&serial).unwrap();
builder.set_pubkey(&pkey).unwrap();
builder
.set_not_before(Asn1Time::days_from_now(0).unwrap().as_ref())
.unwrap();
builder
.set_not_after(Asn1Time::days_from_now(365).unwrap().as_ref())
.unwrap();
builder
.sign(&pkey, openssl::hash::MessageDigest::sha256())
.unwrap();
let cert = builder.build();
let cert_pem = cert.to_pem().unwrap();
std::fs::write(&ca_path, &cert_pem).unwrap();
let unencrypted_key_pem = pkey.private_key_to_pem_pkcs8().unwrap();
let plain_key_path = std::env::temp_dir().join("kdc_test_plain_key_enc.pem");
std::fs::write(&plain_key_path, &unencrypted_key_pem).unwrap();
let enc_result = std::process::Command::new("openssl")
.args([
"rsa",
"-aes128",
"-in",
plain_key_path.to_str().unwrap(),
"-passout",
&format!("pass:{}", password),
])
.output();
let encrypted_key_pem = match enc_result {
Ok(out) if out.status.success() => out.stdout,
Ok(out) => {
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&plain_key_path);
reset_config();
panic!(
"openssl rsa encryption failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
Err(e) => {
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&plain_key_path);
reset_config();
panic!("failed to spawn openssl command: {}", e);
}
};
let _ = std::fs::remove_file(&plain_key_path);
let cert_path = std::env::temp_dir().join("kdc_test_cert_enc.pem");
let key_path = std::env::temp_dir().join("kdc_test_key_enc.pem");
std::fs::write(&cert_path, &cert_pem).unwrap();
std::fs::write(&key_path, &encrypted_key_pem).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_path.to_str().unwrap().to_string();
guard.privatepath = key_path.to_str().unwrap().to_string();
guard.key_pwd = Zeroizing::new(password.to_string());
}
let result = build_kdc_agent_client();
assert!(
result.is_ok(),
"should succeed with encrypted key and correct password"
);
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&cert_path);
let _ = std::fs::remove_file(&key_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_only_certpath_no_key() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_ca_certonly.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = ca_path.to_str().unwrap().to_string();
guard.privatepath = String::new();
}
let result = build_kdc_agent_client();
assert!(result.is_ok());
let _ = std::fs::remove_file(&ca_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_only_keypath_no_cert() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_ca_keyonly.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = String::new();
guard.privatepath = ca_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_ok());
let _ = std::fs::remove_file(&ca_path);
reset_config();
}
#[test]
fn test_request_body_size_limit() {
let large_data = "x".repeat(110_000);
let req = KdcHttpRequest {
task_id: "test".to_string(),
version: "1.0".to_string(),
msg_type: 0x0011,
data: Some(large_data),
};
let body = serde_json::to_vec(&req).unwrap();
assert!(
body.len() > MAX_INNER_REQUEST_SIZE,
"body should exceed 100KB limit"
);
}
#[test]
fn test_request_body_within_limit() {
let req = KdcHttpRequest {
task_id: "test".to_string(),
version: "1.0".to_string(),
msg_type: 0x0011,
data: Some("small payload".to_string()),
};
let body = serde_json::to_vec(&req).unwrap();
assert!(
body.len() <= MAX_INNER_REQUEST_SIZE,
"body should be within 100KB limit"
);
}
#[test]
fn test_prepare_request_data_psk_none_data() {
let key = generate_random_psk().unwrap();
let result = prepare_request_data(Some(key), None).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_decrypt_response_data_psk_none_data() {
let key = generate_random_psk().unwrap();
let result = decrypt_response_data(Some(key), None).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_decrypt_response_data_wrong_key() {
let key_a = generate_random_psk().unwrap();
let key_b = generate_random_psk().unwrap();
let encrypted = psk_encrypt_with_key(b"secret", &key_a).unwrap();
let encrypted_hex = BASE64_STANDARD.encode(&encrypted);
let result = decrypt_response_data(Some(key_b), Some(encrypted_hex));
assert!(result.is_err());
}
#[test]
fn test_decrypt_response_data_invalid_utf8() {
let key = generate_random_psk().unwrap();
let invalid_utf8: Vec<u8> = vec![0xFF, 0xFE, 0xFD, 0xFC];
let encrypted = psk_encrypt_with_key(&invalid_utf8, &key).unwrap();
let encrypted_hex = BASE64_STANDARD.encode(&encrypted);
let result = decrypt_response_data(Some(key), Some(encrypted_hex));
assert!(result.is_err());
}
#[test]
fn test_send_to_kdc_agent_register_skips_psk() {
reset_config();
reset_client_holder();
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = "https://127.0.0.1:1".to_string();
let (ca_path, _) = create_self_signed_ca("kdc_test_register_ca.pem");
guard.capath = ca_path.to_str().unwrap().to_string();
guard.psk = Some(SecurePsk([1u8; PSK_LEN]));
}
let result = send_to_kdc_agent(TRUSTED_REGISTER_PSK_REQ, Some("test_data"));
assert!(result.is_err());
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_with_psk_encrypts() {
reset_config();
reset_client_holder();
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = "https://127.0.0.1:1".to_string();
let (ca_path, _) = create_self_signed_ca("kdc_test_psk_ca.pem");
guard.capath = ca_path.to_str().unwrap().to_string();
guard.psk = Some(SecurePsk([2u8; PSK_LEN]));
}
let result = send_to_kdc_agent(0x0020, Some("test_data"));
assert!(result.is_err());
reset_config();
reset_client_holder();
}
#[test]
fn test_request_token_no_ec_key() {
reset_config();
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = String::new();
}
let result = request_token_from_ra_agent(None);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::ConfigError);
}
#[test]
fn test_build_kdc_agent_client_with_invalid_crl() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_bad_crl_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.crlpath = "/nonexistent/crl.pem".to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_ok(), "should succeed even with invalid CRL path");
let _ = std::fs::remove_file(&ca_path);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_with_crl() {
reset_config();
let (ca_path, pkey) = create_self_signed_ca("kdc_test_crl_ca.pem");
let crl_path = std::env::temp_dir().join("kdc_test_crl.pem");
let crl_result = generate_test_crl_pem(&pkey);
match crl_result {
Some(pem) => {
std::fs::write(&crl_path, &pem).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.crlpath = crl_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_ok(), "should succeed with valid CA and CRL");
let _ = std::fs::remove_file(&crl_path);
}
None => {
let (ca_path2, _) = create_self_signed_ca("kdc_test_crl_ca_fallback.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path2.to_str().unwrap().to_string();
guard.crlpath = "/nonexistent/crl.pem".to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_ok());
let _ = std::fs::remove_file(&ca_path2);
}
}
let _ = std::fs::remove_file(&ca_path);
reset_config();
}
#[test]
fn test_ensure_client_caching() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_cache_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let client1 = ensure_client().unwrap();
let client2 = ensure_client().unwrap();
let _ = client1;
let _ = client2;
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_get_client_holder_singleton() {
let holder1 = get_client_holder();
let holder2 = get_client_holder();
assert!(std::ptr::eq(holder1, holder2));
}
fn generate_test_crl_pem(_pkey: &openssl::pkey::PKey<openssl::pkey::Private>) -> Option<Vec<u8>> {
let crl_result = std::process::Command::new("openssl")
.args(["ca", "-gencrl"])
.output()
.ok()?;
if crl_result.status.success() {
Some(crl_result.stdout)
} else {
None
}
}
#[test]
fn test_prepare_request_data_psk_empty_data() {
let key = generate_random_psk().unwrap();
let result = prepare_request_data(Some(key), Some("")).unwrap();
let encrypted_hex = result.unwrap();
assert!(!encrypted_hex.is_empty());
assert_ne!(encrypted_hex, "");
}
#[test]
fn test_decrypt_response_data_valid_roundtrip_long() {
let key = generate_random_psk().unwrap();
let plaintext = "a longer response payload with special chars: \u{4e2d}\u{6587}";
let encrypted = psk_encrypt_with_key(plaintext.as_bytes(), &key).unwrap();
let encrypted_hex = BASE64_STANDARD.encode(&encrypted);
let decrypted = decrypt_response_data(Some(key), Some(encrypted_hex)).unwrap();
assert_eq!(decrypted, Some(plaintext.to_string()));
}
#[test]
fn test_get_client_holder_consistent() {
let h1 = get_client_holder();
let h2 = get_client_holder();
assert!(std::ptr::eq(h1, h2));
let guard = h1.lock();
let _ = guard;
}
#[test]
fn test_build_kdc_agent_client_with_password_valid() {
use openssl::asn1::Asn1Time;
use openssl::bn::BigNum;
use openssl::x509::X509Builder;
reset_config();
reset_client_holder();
let password = "test_pwd_123";
let rsa = openssl::rsa::Rsa::generate(2048).unwrap();
let pkey = openssl::pkey::PKey::from_rsa(rsa).unwrap();
let ca_path = std::env::temp_dir().join("kdc_test_pwd_ca.pem");
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
let serial = BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap();
builder.set_serial_number(&serial).unwrap();
builder.set_pubkey(&pkey).unwrap();
builder
.set_not_before(Asn1Time::days_from_now(0).unwrap().as_ref())
.unwrap();
builder
.set_not_after(Asn1Time::days_from_now(365).unwrap().as_ref())
.unwrap();
builder
.sign(&pkey, openssl::hash::MessageDigest::sha256())
.unwrap();
let cert = builder.build();
let cert_pem = cert.to_pem().unwrap();
std::fs::write(&ca_path, &cert_pem).unwrap();
let plain_key = pkey.private_key_to_pem_pkcs8().unwrap();
let plain_key_path = std::env::temp_dir().join("kdc_test_pwd_plain.pem");
std::fs::write(&plain_key_path, &plain_key).unwrap();
let enc_output = std::process::Command::new("openssl")
.args([
"rsa", "-aes128", "-in", plain_key_path.to_str().unwrap(),
"-passout", &format!("pass:{}", password),
])
.output()
.unwrap();
if !enc_output.status.success() {
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&plain_key_path);
reset_config();
reset_client_holder();
return;
}
let key_path = std::env::temp_dir().join("kdc_test_pwd_enc_key.pem");
std::fs::write(&key_path, &enc_output.stdout).unwrap();
let cert_only_path = std::env::temp_dir().join("kdc_test_pwd_cert.pem");
std::fs::write(&cert_only_path, &cert_pem).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_only_path.to_str().unwrap().to_string();
guard.privatepath = key_path.to_str().unwrap().to_string();
guard.key_pwd = Zeroizing::new(password.to_string());
}
let result = build_kdc_agent_client();
assert!(result.is_ok(), "should succeed with encrypted key and password");
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&plain_key_path);
let _ = std::fs::remove_file(&key_path);
let _ = std::fs::remove_file(&cert_only_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_build_client_wrong_password() {
use openssl::asn1::Asn1Time;
use openssl::bn::BigNum;
use openssl::x509::X509Builder;
reset_config();
let rsa = openssl::rsa::Rsa::generate(2048).unwrap();
let pkey = openssl::pkey::PKey::from_rsa(rsa).unwrap();
let ca_path = std::env::temp_dir().join("kdc_test_wrong_pwd_ca.pem");
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
let serial = BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap();
builder.set_serial_number(&serial).unwrap();
builder.set_pubkey(&pkey).unwrap();
builder
.set_not_before(Asn1Time::days_from_now(0).unwrap().as_ref())
.unwrap();
builder
.set_not_after(Asn1Time::days_from_now(365).unwrap().as_ref())
.unwrap();
builder
.sign(&pkey, openssl::hash::MessageDigest::sha256())
.unwrap();
let cert = builder.build();
std::fs::write(&ca_path, cert.to_pem().unwrap()).unwrap();
let plain_key = pkey.private_key_to_pem_pkcs8().unwrap();
let plain_key_path = std::env::temp_dir().join("kdc_test_wrong_pwd_plain.pem");
std::fs::write(&plain_key_path, &plain_key).unwrap();
let enc_output = std::process::Command::new("openssl")
.args([
"rsa", "-aes128", "-in", plain_key_path.to_str().unwrap(),
"-passout", "pass:correct_password",
])
.output()
.unwrap();
if !enc_output.status.success() {
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&plain_key_path);
reset_config();
return;
}
let key_path = std::env::temp_dir().join("kdc_test_wrong_pwd_key.pem");
std::fs::write(&key_path, &enc_output.stdout).unwrap();
let cert_path = std::env::temp_dir().join("kdc_test_wrong_pwd_cert.pem");
std::fs::write(&cert_path, cert.to_pem().unwrap()).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_path.to_str().unwrap().to_string();
guard.privatepath = key_path.to_str().unwrap().to_string();
guard.key_pwd = Zeroizing::new("wrong_password".to_string());
}
let result = build_kdc_agent_client();
assert!(result.is_err(), "should fail with wrong password");
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&plain_key_path);
let _ = std::fs::remove_file(&key_path);
let _ = std::fs::remove_file(&cert_path);
reset_config();
}
#[test]
fn test_request_token_with_url_set_attempts_connection() {
reset_config();
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = "https://127.0.0.1:1/nonexistent".to_string();
}
let result = request_token_from_ra_agent(None);
assert!(result.is_err(), "should fail connecting to non-existent server");
reset_config();
}
#[test]
fn test_request_token_with_ec_key_unreachable_url() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_ra_ec_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = "https://127.0.0.1:1/ra".to_string();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let (ec_key, _pub_pem) = crate::crypto::generate_ec_keypair().unwrap();
let result = request_token_from_ra_agent(Some(&ec_key));
assert!(result.is_err(), "should fail with unreachable URL");
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_network_unreachable() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_net_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = "https://127.0.0.1:1/kdc".to_string();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = send_to_kdc_agent(0x0020, Some("test"));
assert!(result.is_err());
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_no_psk_no_encrypt() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_nopsk_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = "https://127.0.0.1:1/kdc".to_string();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = send_to_kdc_agent(0x0020, Some("plaintext"));
assert!(result.is_err());
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_decrypt_response_data_psk_empty_data() {
let key = generate_random_psk().unwrap();
let result = decrypt_response_data(Some(key), Some(String::new()));
assert!(result.is_err(), "empty base64 should fail decode");
}
#[test]
fn test_build_kdc_agent_client_mismatched_cert_key() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_mismatch_ca.pem");
let rsa = openssl::rsa::Rsa::generate(2048).unwrap();
let other_pkey = openssl::pkey::PKey::from_rsa(rsa).unwrap();
let other_key_pem = other_pkey.private_key_to_pem_pkcs8().unwrap();
let cert_pem = std::fs::read(&ca_path).unwrap();
let cert_path = std::env::temp_dir().join("kdc_test_mismatch_cert.pem");
let key_path = std::env::temp_dir().join("kdc_test_mismatch_key.pem");
std::fs::write(&cert_path, &cert_pem).unwrap();
std::fs::write(&key_path, &other_key_pem).unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_path.to_str().unwrap().to_string();
guard.privatepath = key_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_err(), "mismatched cert/key should fail");
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_file(&cert_path);
let _ = std::fs::remove_file(&key_path);
reset_config();
}
#[test]
fn test_request_token_ec_key_unreachable_network_error() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_ra_net_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = "https://127.0.0.1:1/ra".to_string();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let (ec_key, _pub_pem) = crate::crypto::generate_ec_keypair().unwrap();
let result = request_token_from_ra_agent(Some(&ec_key));
assert!(result.is_err());
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_ensure_client_recreates_after_reset() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_recreate_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let client1 = ensure_client();
assert!(client1.is_ok());
reset_client_holder();
let client2 = ensure_client();
assert!(client2.is_ok());
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_psk_network_error() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_psk_net_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = "https://127.0.0.1:1/kdc".to_string();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.psk = Some(SecurePsk([3u8; PSK_LEN]));
}
let result = send_to_kdc_agent(0x0020, Some("test"));
assert!(result.is_err());
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_build_kdc_agent_client_invalid_ca_cert_content() {
reset_config();
let dir = std::env::temp_dir().join("kdc_test_inv_ca");
let _ = std::fs::create_dir_all(&dir);
let ca_path = dir.join("invalid_ca.pem");
std::fs::write(
&ca_path,
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
)
.unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_err(), "invalid CA cert content should fail");
let _ = std::fs::remove_dir_all(&dir);
reset_config();
}
#[test]
fn test_build_kdc_agent_client_invalid_identity_key() {
reset_config();
let (ca_path, _) = create_self_signed_ca("kdc_test_inv_id_ca.pem");
let cert_pem = std::fs::read(&ca_path).unwrap();
let dir = std::env::temp_dir().join("kdc_test_inv_id");
let _ = std::fs::create_dir_all(&dir);
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
std::fs::write(&cert_path, &cert_pem).unwrap();
std::fs::write(
&key_path,
"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
)
.unwrap();
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = ca_path.to_str().unwrap().to_string();
guard.certpath = cert_path.to_str().unwrap().to_string();
guard.privatepath = key_path.to_str().unwrap().to_string();
}
let result = build_kdc_agent_client();
assert!(result.is_err(), "invalid identity key should fail");
let _ = std::fs::remove_file(&ca_path);
let _ = std::fs::remove_dir_all(&dir);
reset_config();
}
#[test]
fn test_send_to_kdc_agent_non_json_response() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let resp = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_mock_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = format!("http://127.0.0.1:{}/kdc", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = send_to_kdc_agent(0x0020, Some("test"));
assert!(result.is_err());
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_request_token_ra_no_token_in_response() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let body = r#"{"status":"error","message":"denied"}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_ra_mock_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = format!("http://127.0.0.1:{}/ra", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = request_token_from_ra_agent(None);
assert!(result.is_err());
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_request_token_ra_non_json_response() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let resp = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_ra_nj_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = format!("http://127.0.0.1:{}/ra", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = request_token_from_ra_agent(None);
assert!(result.is_err());
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_psk_mismatch_response() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let body = r#"{"retCode":2,"retMsg":"PSK mismatch","data":null}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_psk_mismatch_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = format!("http://127.0.0.1:{}/kdc", port);
guard.capath = ca_path.to_str().unwrap().to_string();
guard.psk = Some(SecurePsk([4u8; PSK_LEN]));
}
let result = send_to_kdc_agent(0x0020, Some("test"));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::PskMismatchError);
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_request_token_ra_with_ec_key_success() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let body = r#"{"token":"test_attestation_token_123"}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_ra_ec_ok_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = format!("http://127.0.0.1:{}/ra", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let (ec_key, _pub_pem) = crate::crypto::generate_ec_keypair().unwrap();
let result = request_token_from_ra_agent(Some(&ec_key));
assert!(result.is_ok(), "should succeed with mock server");
let token = result.unwrap();
assert_eq!(token, "test_attestation_token_123");
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_request_too_large() {
reset_config();
reset_client_holder();
let (ca_path, _) = create_self_signed_ca("kdc_test_large_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = "http://127.0.0.1:1/kdc".to_string();
guard.capath = ca_path.to_str().unwrap().to_string();
}
let large_data = "x".repeat(110_000);
let result = send_to_kdc_agent(0x0020, Some(&large_data));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CommonError::RequestError);
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_success_response() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let body = r#"{"retCode":0,"retMsg":"ok","data":"hello"}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_success_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = format!("http://127.0.0.1:{}/kdc", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = send_to_kdc_agent(TRUSTED_REGISTER_PSK_REQ, Some("test"));
assert!(result.is_ok(), "should succeed with 200 OK response");
let resp = result.unwrap();
assert_eq!(resp.ret_code, 0);
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_request_token_ra_without_ec_key_success() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let body = r#"{"token":"ra_token_abc"}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
);
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_ra_no_ec_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.ra_agent_url = format!("http://127.0.0.1:{}/ra", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = request_token_from_ra_agent(None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "ra_token_abc");
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_send_to_kdc_agent_http_error_status() {
reset_config();
reset_client_holder();
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 4096];
let _ = std::io::Read::read(&mut stream, &mut buf);
let resp = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n";
let _ = std::io::Write::write_all(&mut stream, resp.as_bytes());
});
let (ca_path, _) = create_self_signed_ca("kdc_test_500_ca.pem");
{
let config = get_config_center();
let mut guard = config.lock();
guard.kdc_agent_url = format!("http://127.0.0.1:{}/kdc", port);
guard.capath = ca_path.to_str().unwrap().to_string();
}
let result = send_to_kdc_agent(0x0020, Some("test"));
assert!(result.is_err());
let _ = handle.join();
let _ = std::fs::remove_file(&ca_path);
reset_config();
reset_client_holder();
}
#[test]
fn test_build_kdc_agent_client_with_valid_crl_pem() {
reset_config();
let tmp_dir = std::env::temp_dir().join("kdc_test_crl_gen");
let _ = std::fs::create_dir_all(&tmp_dir);
let key_path = tmp_dir.join("ca_key.pem");
let cert_path = tmp_dir.join("ca_cert.pem");
let crl_path_file = tmp_dir.join("ca.crl.pem");
let db_path = tmp_dir.join("index.txt");
let serial_path = tmp_dir.join("serial");
let crlnum_path = tmp_dir.join("crlnumber");
let cnf_path = tmp_dir.join("openssl.cnf");
let _ = std::fs::write(&db_path, "");
let _ = std::fs::write(&serial_path, "01\n");
let _ = std::fs::write(&crlnum_path, "01\n");
let cnf_content = format!(
"[ca]\ndefault_ca = CA_default\n\n[CA_default]\ndatabase = {}\ncrlnumber = {}\ndefault_md = sha256\ndefault_crl_days = 30\n",
db_path.display(),
crlnum_path.display()
);
let _ = std::fs::write(&cnf_path, &cnf_content);
let gen = std::process::Command::new("openssl")
.args([
"req",
"-x509",
"-newkey",
"rsa:2048",
"-keyout",
key_path.to_str().unwrap(),
"-out",
cert_path.to_str().unwrap(),
"-days",
"365",
"-nodes",
"-subj",
"/CN=kdc-test-ca",
])
.output();
let gen = match gen {
Ok(o) if o.status.success() => o,
_ => {
let _ = std::fs::remove_dir_all(&tmp_dir);
reset_config();
return;
}
};
let _ = gen;
let crl_gen = std::process::Command::new("openssl")
.args([
"ca",
"-gencrl",
"-keyfile",
key_path.to_str().unwrap(),
"-cert",
cert_path.to_str().unwrap(),
"-out",
crl_path_file.to_str().unwrap(),
"-config",
cnf_path.to_str().unwrap(),
])
.output();
let crl_valid = match crl_gen {
Ok(o) if o.status.success() && crl_path_file.exists() => true,
_ => false,
};
if crl_valid {
{
let config = get_config_center();
let mut guard = config.lock();
guard.capath = cert_path.to_str().unwrap().to_string();
guard.crlpath = crl_path_file.to_str().unwrap().to_string();
}
let _ = build_kdc_agent_client();
}
let _ = std::fs::remove_dir_all(&tmp_dir);
reset_config();
}
}