1use serde::{Deserialize, Serialize};
11use std::time::Duration;
12use tracing::debug;
13
14use reqwest::{Certificate, Client, Identity};
15
16use crate::config::DogtagConfig;
17use crate::{DogtagError, DogtagResult};
18
19pub struct KraClient {
25 http: Client,
26 base_url: String,
27 retry_max: u32,
28 retry_delay: Duration,
29}
30
31#[derive(Debug, Clone)]
33pub struct KeyGenResult {
34 pub key_id: String,
36 pub public_key_der: Vec<u8>,
38 pub wrapped_private_key: Option<Vec<u8>>,
44}
45
46#[derive(Serialize)]
48#[serde(rename_all = "PascalCase")]
49struct KeyGenRequest {
50 key_algorithm: String,
52 key_size: u32,
54 client_key_id: String,
56}
57
58#[derive(Serialize)]
60#[serde(rename_all = "PascalCase")]
61struct ArchiveRequest {
62 client_key_id: String,
64 wrapped_private_data: String,
66 algorithm_oid: Option<String>,
68}
69
70#[derive(Serialize)]
72#[serde(rename_all = "PascalCase")]
73struct RecoverRequest {
74 key_id: String,
76}
77
78#[derive(Deserialize)]
80#[serde(rename_all = "PascalCase")]
81struct KeyGenResponse {
82 #[serde(default)]
84 request_info: Option<KeyRequestInfo>,
85}
86
87#[derive(Deserialize)]
88#[serde(rename_all = "PascalCase")]
89struct KeyRequestInfo {
90 #[serde(default)]
91 key_id: Option<String>,
92 #[serde(default)]
93 public_key: Option<String>,
94 #[serde(default)]
95 wrapped_private_data: Option<String>,
96}
97
98#[derive(Deserialize)]
100#[serde(rename_all = "PascalCase")]
101struct RecoverResponse {
102 #[serde(default)]
104 data: Option<String>,
105}
106
107#[derive(Deserialize)]
109#[serde(rename_all = "PascalCase")]
110struct ArchiveResponse {
111 #[serde(default)]
113 key_id: Option<String>,
114}
115
116impl KraClient {
117 pub fn new(config: &DogtagConfig) -> DogtagResult<Self> {
123 let kra_url = config.kra_url.as_ref().ok_or_else(|| {
124 DogtagError::ConfigError("kra_url is required for KRA operations".into())
125 })?;
126
127 let cert_pem = std::fs::read(&config.agent_cert_file)?;
128 let key_pem = std::fs::read(&config.agent_key_file)?;
129 let ca_pem = std::fs::read(&config.ca_cert_file)?;
130
131 let mut identity_pem = cert_pem;
132 identity_pem.extend_from_slice(b"\n");
133 identity_pem.extend_from_slice(&key_pem);
134
135 let identity = Identity::from_pem(&identity_pem)
136 .map_err(|e| DogtagError::TlsError(format!("Failed to load agent identity: {e}")))?;
137
138 let ca_cert = Certificate::from_pem(&ca_pem)
139 .map_err(|e| DogtagError::TlsError(format!("Failed to load CA certificate: {e}")))?;
140
141 let http = Client::builder()
142 .identity(identity)
143 .add_root_certificate(ca_cert)
144 .timeout(Duration::from_secs(config.timeout_secs))
145 .build()?;
146
147 let base_url = kra_url.as_str().trim_end_matches('/').to_owned();
148
149 Ok(Self {
150 http,
151 base_url,
152 retry_max: config.retry_max,
153 retry_delay: Duration::from_millis(config.retry_delay_ms),
154 })
155 }
156
157 pub async fn generate_key(&self, key_type: &str, key_size: u32) -> DogtagResult<KeyGenResult> {
169 debug!(key_type, key_size, "Generating key on KRA");
170
171 let request = KeyGenRequest {
172 key_algorithm: key_type.to_owned(),
173 key_size,
174 client_key_id: uuid_v4(),
175 };
176
177 let resp = self
178 .post_json("/kra/rest/agent/keys/generate", &request)
179 .await?;
180
181 let keygen_resp: KeyGenResponse = json_response(resp).await?;
182
183 let info = keygen_resp
184 .request_info
185 .ok_or_else(|| DogtagError::KraError("Missing request_info in response".into()))?;
186
187 let key_id = info
188 .key_id
189 .ok_or_else(|| DogtagError::KraError("Missing key_id in response".into()))?;
190
191 let public_key_b64 = info
192 .public_key
193 .ok_or_else(|| DogtagError::KraError("Missing public_key in response".into()))?;
194
195 use base64::Engine;
196 let public_key_der = base64::engine::general_purpose::STANDARD
197 .decode(&public_key_b64)
198 .map_err(|e| DogtagError::KraError(format!("Invalid base64 in public key: {e}")))?;
199
200 let wrapped_private_key = if let Some(ref wrapped) = info.wrapped_private_data {
201 Some(
202 base64::engine::general_purpose::STANDARD
203 .decode(wrapped)
204 .map_err(|e| {
205 DogtagError::KraError(format!("Invalid base64 in wrapped key: {e}"))
206 })?,
207 )
208 } else {
209 None
210 };
211
212 Ok(KeyGenResult {
213 key_id,
214 public_key_der,
215 wrapped_private_key,
216 })
217 }
218
219 pub async fn archive_key(&self, key_id: &str, wrapped_key: &[u8]) -> DogtagResult<String> {
224 debug!(key_id, size = wrapped_key.len(), "Archiving key in KRA");
225
226 use base64::Engine;
227 let wrapped_b64 = base64::engine::general_purpose::STANDARD.encode(wrapped_key);
228
229 let request = ArchiveRequest {
230 client_key_id: key_id.to_owned(),
231 wrapped_private_data: wrapped_b64,
232 algorithm_oid: None,
233 };
234
235 let resp = self
236 .post_json("/kra/rest/agent/keys/archive", &request)
237 .await?;
238
239 let archive: ArchiveResponse = json_response(resp).await?;
240
241 archive
242 .key_id
243 .ok_or_else(|| DogtagError::KraError("Missing key_id in archive response".into()))
244 }
245
246 pub async fn recover_key(&self, key_id: &str) -> DogtagResult<Vec<u8>> {
251 debug!(key_id, "Recovering key from KRA");
252
253 let request = RecoverRequest {
254 key_id: key_id.to_owned(),
255 };
256
257 let resp = self
258 .post_json(&format!("/kra/rest/agent/keys/{key_id}/recover"), &request)
259 .await?;
260
261 let recover: RecoverResponse = json_response(resp).await?;
262
263 let data_b64 = recover
264 .data
265 .ok_or_else(|| DogtagError::KraError("Missing data in recovery response".into()))?;
266
267 use base64::Engine;
268 base64::engine::general_purpose::STANDARD
269 .decode(&data_b64)
270 .map_err(|e| DogtagError::KraError(format!("Invalid base64 in recovered key: {e}")))
271 }
272
273 async fn post_json<T: serde::Serialize + ?Sized>(
275 &self,
276 path: &str,
277 body: &T,
278 ) -> DogtagResult<reqwest::Response> {
279 let url = format!("{}{}", self.base_url, path);
280 let mut last_error = None;
281
282 for attempt in 0..=self.retry_max {
283 if attempt > 0 {
284 debug!(attempt, max = self.retry_max, "Retrying KRA request");
285 tokio::time::sleep(self.retry_delay).await;
286 }
287
288 match self.http.post(&url).json(body).send().await {
289 Ok(resp) if resp.status().is_server_error() => {
290 let status = resp.status();
291 let body = resp.text().await.unwrap_or_default();
292 tracing::warn!(attempt, status = status.as_u16(), "KRA server error");
293 last_error = Some(DogtagError::ApiError {
294 status: status.as_u16(),
295 body,
296 });
297 }
298 Ok(resp) => return Ok(resp),
299 Err(e) => {
300 tracing::warn!(attempt, error = %e, "KRA request failed");
301 last_error = Some(DogtagError::Http(e));
302 }
303 }
304 }
305
306 Err(last_error.unwrap_or(DogtagError::KraError("All retry attempts exhausted".into())))
307 }
308}
309
310async fn json_response<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> DogtagResult<T> {
312 let status = resp.status();
313 if !status.is_success() {
314 let body = resp.text().await.unwrap_or_default();
315 return Err(DogtagError::ApiError {
316 status: status.as_u16(),
317 body,
318 });
319 }
320 resp.json::<T>()
321 .await
322 .map_err(|e| DogtagError::ParseError(e.to_string()))
323}
324
325fn uuid_v4() -> String {
327 use std::time::{SystemTime, UNIX_EPOCH};
328 let nanos = SystemTime::now()
329 .duration_since(UNIX_EPOCH)
330 .unwrap_or_default()
331 .as_nanos();
332 format!("kipuka-{nanos:x}")
333}