use std::collections::HashMap;
use a3s_box_core::error::{BoxError, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KbsConfig {
pub url: String,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default)]
pub insecure_tls: bool,
}
fn default_timeout() -> u64 {
30
}
impl Default for KbsConfig {
fn default() -> Self {
Self {
url: String::new(),
api_key: None,
timeout_secs: 30,
insecure_tls: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KbsRequest {
pub resource_path: String,
pub evidence: String,
#[serde(default = "default_tee_type")]
pub tee_type: String,
#[serde(default)]
pub extra_claims: HashMap<String, String>,
}
fn default_tee_type() -> String {
"snp".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KbsResponse {
pub verified: bool,
pub payload: Option<String>,
pub error: Option<String>,
#[serde(default)]
pub token: Option<String>,
}
#[derive(Debug, Clone)]
pub struct KbsSecret {
pub resource_path: String,
pub secret: Vec<u8>,
pub token: Option<String>,
}
pub struct KbsClient {
config: KbsConfig,
}
impl KbsClient {
pub fn new(config: KbsConfig) -> Self {
Self { config }
}
pub fn build_request(&self, resource_path: &str, evidence: &[u8]) -> KbsRequest {
use base64::Engine;
KbsRequest {
resource_path: resource_path.to_string(),
evidence: base64::engine::general_purpose::STANDARD.encode(evidence),
tee_type: "snp".to_string(),
extra_claims: HashMap::new(),
}
}
pub fn parse_response(&self, resource_path: &str, response: &KbsResponse) -> Result<KbsSecret> {
if !response.verified {
return Err(BoxError::AttestationError(format!(
"KBS attestation verification failed: {}",
response.error.as_deref().unwrap_or("unknown error")
)));
}
let payload = response.payload.as_ref().ok_or_else(|| {
BoxError::AttestationError("KBS response verified but no payload returned".to_string())
})?;
use base64::Engine;
let secret = base64::engine::general_purpose::STANDARD
.decode(payload)
.map_err(|e| {
BoxError::AttestationError(format!("Failed to decode KBS payload: {}", e))
})?;
Ok(KbsSecret {
resource_path: resource_path.to_string(),
secret,
token: response.token.clone(),
})
}
pub fn resource_url(&self, resource_path: &str) -> String {
let base = self.config.url.trim_end_matches('/');
format!("{}/kbs/v0/resource/{}", base, resource_path)
}
pub fn attest_url(&self) -> String {
let base = self.config.url.trim_end_matches('/');
format!("{}/kbs/v0/attest", base)
}
pub fn config(&self) -> &KbsConfig {
&self.config
}
}
pub fn parse_resource_path(path: &str) -> Result<(&str, &str, &str)> {
let parts: Vec<&str> = path.splitn(3, '/').collect();
if parts.len() != 3 {
return Err(BoxError::AttestationError(format!(
"Invalid KBS resource path '{}': expected 'repository/type/tag'",
path
)));
}
Ok((parts[0], parts[1], parts[2]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kbs_config_default() {
let config = KbsConfig::default();
assert!(config.url.is_empty());
assert!(config.api_key.is_none());
assert_eq!(config.timeout_secs, 30);
assert!(!config.insecure_tls);
}
#[test]
fn test_kbs_config_serde_roundtrip() {
let config = KbsConfig {
url: "https://kbs.example.com".to_string(),
api_key: Some("secret-key".to_string()),
timeout_secs: 60,
insecure_tls: true,
};
let json = serde_json::to_string(&config).unwrap();
let parsed: KbsConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.url, "https://kbs.example.com");
assert_eq!(parsed.api_key, Some("secret-key".to_string()));
assert_eq!(parsed.timeout_secs, 60);
assert!(parsed.insecure_tls);
}
#[test]
fn test_kbs_client_build_request() {
let config = KbsConfig {
url: "https://kbs.example.com".to_string(),
..Default::default()
};
let client = KbsClient::new(config);
let evidence = b"fake-snp-report";
let request = client.build_request("default/keys/my-key", evidence);
assert_eq!(request.resource_path, "default/keys/my-key");
assert_eq!(request.tee_type, "snp");
assert!(!request.evidence.is_empty());
use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD
.decode(&request.evidence)
.unwrap();
assert_eq!(decoded, evidence);
}
#[test]
fn test_kbs_client_parse_response_success() {
let config = KbsConfig::default();
let client = KbsClient::new(config);
use base64::Engine;
let secret_data = b"my-secret-value";
let payload = base64::engine::general_purpose::STANDARD.encode(secret_data);
let response = KbsResponse {
verified: true,
payload: Some(payload),
error: None,
token: Some("session-token".to_string()),
};
let secret = client
.parse_response("default/keys/test", &response)
.unwrap();
assert_eq!(secret.resource_path, "default/keys/test");
assert_eq!(secret.secret, secret_data);
assert_eq!(secret.token, Some("session-token".to_string()));
}
#[test]
fn test_kbs_client_parse_response_verification_failed() {
let config = KbsConfig::default();
let client = KbsClient::new(config);
let response = KbsResponse {
verified: false,
payload: None,
error: Some("measurement mismatch".to_string()),
token: None,
};
let result = client.parse_response("default/keys/test", &response);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("measurement mismatch"));
}
#[test]
fn test_kbs_client_parse_response_no_payload() {
let config = KbsConfig::default();
let client = KbsClient::new(config);
let response = KbsResponse {
verified: true,
payload: None,
error: None,
token: None,
};
let result = client.parse_response("default/keys/test", &response);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no payload"));
}
#[test]
fn test_kbs_client_parse_response_invalid_base64() {
let config = KbsConfig::default();
let client = KbsClient::new(config);
let response = KbsResponse {
verified: true,
payload: Some("not-valid-base64!!!".to_string()),
error: None,
token: None,
};
let result = client.parse_response("default/keys/test", &response);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("decode"));
}
#[test]
fn test_kbs_client_resource_url() {
let config = KbsConfig {
url: "https://kbs.example.com".to_string(),
..Default::default()
};
let client = KbsClient::new(config);
assert_eq!(
client.resource_url("default/keys/my-key"),
"https://kbs.example.com/kbs/v0/resource/default/keys/my-key"
);
}
#[test]
fn test_kbs_client_resource_url_trailing_slash() {
let config = KbsConfig {
url: "https://kbs.example.com/".to_string(),
..Default::default()
};
let client = KbsClient::new(config);
assert_eq!(
client.resource_url("default/keys/my-key"),
"https://kbs.example.com/kbs/v0/resource/default/keys/my-key"
);
}
#[test]
fn test_kbs_client_attest_url() {
let config = KbsConfig {
url: "https://kbs.example.com".to_string(),
..Default::default()
};
let client = KbsClient::new(config);
assert_eq!(client.attest_url(), "https://kbs.example.com/kbs/v0/attest");
}
#[test]
fn test_parse_resource_path_valid() {
let (repo, rtype, tag) = parse_resource_path("default/keys/my-secret").unwrap();
assert_eq!(repo, "default");
assert_eq!(rtype, "keys");
assert_eq!(tag, "my-secret");
}
#[test]
fn test_parse_resource_path_with_nested_tag() {
let (repo, rtype, tag) = parse_resource_path("myrepo/certs/tls/server.pem").unwrap();
assert_eq!(repo, "myrepo");
assert_eq!(rtype, "certs");
assert_eq!(tag, "tls/server.pem");
}
#[test]
fn test_parse_resource_path_invalid_too_few() {
assert!(parse_resource_path("default/keys").is_err());
assert!(parse_resource_path("just-one").is_err());
assert!(parse_resource_path("").is_err());
}
#[test]
fn test_kbs_request_serde_roundtrip() {
let request = KbsRequest {
resource_path: "default/keys/test".to_string(),
evidence: "base64evidence".to_string(),
tee_type: "snp".to_string(),
extra_claims: HashMap::from([("nonce".to_string(), "abc123".to_string())]),
};
let json = serde_json::to_string(&request).unwrap();
let parsed: KbsRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.resource_path, "default/keys/test");
assert_eq!(parsed.evidence, "base64evidence");
assert_eq!(parsed.tee_type, "snp");
assert_eq!(parsed.extra_claims.get("nonce").unwrap(), "abc123");
}
#[test]
fn test_kbs_response_serde_roundtrip() {
let response = KbsResponse {
verified: true,
payload: Some("c2VjcmV0".to_string()),
error: None,
token: Some("tok-123".to_string()),
};
let json = serde_json::to_string(&response).unwrap();
let parsed: KbsResponse = serde_json::from_str(&json).unwrap();
assert!(parsed.verified);
assert_eq!(parsed.payload, Some("c2VjcmV0".to_string()));
assert!(parsed.error.is_none());
assert_eq!(parsed.token, Some("tok-123".to_string()));
}
}