Skip to main content

kipuka_dogtag/
kra.rs

1//! KRA (Key Recovery Authority) operations for server-side key generation.
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5use tracing::debug;
6use zeroize::Zeroizing;
7
8use reqwest::{Certificate, Client, Identity};
9
10use crate::config::DogtagConfig;
11use crate::{DogtagError, DogtagResult};
12
13pub struct KraClient {
14    http: Client,
15    base_url: String,
16    basic_auth: Option<(String, String)>,
17}
18
19/// Result of a key generation operation.
20pub struct KeyGenResult {
21    pub key_id: String,
22    pub request_id: String,
23    pub public_key: Option<String>,
24    pub wrapped_private_key: Option<Vec<u8>>,
25}
26
27#[derive(Debug, Clone)]
28pub struct KeySearchEntry {
29    pub key_id: String,
30    pub client_key_id: String,
31    pub algorithm: String,
32    pub size: u32,
33    pub status: String,
34}
35
36/// Dogtag RESTMessage attribute entry.
37/// KRA REST API uses lowercase name/value (unlike CA ProfileAttribute).
38#[derive(Serialize)]
39struct RestAttribute {
40    name: String,
41    value: String,
42}
43
44/// Dogtag RESTMessage-format request for KRA key generation.
45///
46/// Dogtag's KRA REST API uses the RESTMessage wire format where parameters
47/// are carried in `Attributes.Attribute[]`, not as flat JSON fields.
48/// `ClassName` selects the Java handler class via reflection.
49#[derive(Serialize)]
50struct KeyGenRequest {
51    #[serde(rename = "ClassName")]
52    class_name: String,
53    #[serde(rename = "Attributes")]
54    attributes: RestAttributes,
55}
56
57#[derive(Serialize)]
58struct RestAttributes {
59    #[serde(rename = "Attribute")]
60    attribute: Vec<RestAttribute>,
61}
62
63#[derive(Deserialize)]
64struct KeyGenResponse {
65    #[serde(rename = "requestInfo")]
66    request_info: Option<RequestInfo>,
67}
68
69#[derive(Deserialize)]
70struct RequestInfo {
71    #[serde(rename = "requestID")]
72    request_id: Option<String>,
73    #[serde(rename = "keyURL")]
74    key_url: Option<String>,
75}
76
77#[derive(Deserialize)]
78struct RecoverResponse {
79    #[serde(rename = "wrappedPrivateData")]
80    data: Option<String>,
81}
82
83#[derive(Deserialize)]
84struct ArchiveResponse {
85    #[serde(rename = "requestInfo")]
86    request_info: Option<RequestInfo>,
87}
88
89impl KraClient {
90    pub fn new(config: &DogtagConfig) -> DogtagResult<Self> {
91        let kra_url = config.kra_url.as_ref().ok_or_else(|| {
92            DogtagError::ConfigError("kra_url is required for KRA operations".into())
93        })?;
94
95        let is_http = kra_url.scheme() == "http";
96
97        let mut builder = Client::builder()
98            .cookie_store(true)
99            .redirect(reqwest::redirect::Policy::none())
100            .danger_accept_invalid_certs(config.accept_invalid_certs)
101            .timeout(Duration::from_secs(config.timeout_secs));
102
103        if config.accept_invalid_certs {
104            tracing::warn!(
105                "KRA client: TLS certificate validation disabled (accept_invalid_certs=true)"
106            );
107        }
108
109        let use_mtls = !is_http && !config.skip_mtls;
110        if use_mtls {
111            let cert_pem = std::fs::read(&config.agent_cert_file)?;
112            let key_pem = std::fs::read(&config.agent_key_file)?;
113            let identity = Identity::from_pkcs8_pem(&cert_pem, &key_pem).map_err(|e| {
114                DogtagError::TlsError(format!("Failed to load agent identity: {e}"))
115            })?;
116            builder = builder.identity(identity);
117            tracing::info!(
118                "KRA client: HTTPS with mTLS agent cert (cert-only auth, no basic auth)"
119            );
120        }
121
122        let ca_pem = std::fs::read(&config.ca_cert_file)?;
123        let ca_cert = Certificate::from_pem(&ca_pem)
124            .map_err(|e| DogtagError::TlsError(format!("Failed to load CA certificate: {e}")))?;
125        builder = builder.add_root_certificate(ca_cert);
126
127        let http = builder
128            .build()
129            .map_err(|e| DogtagError::TlsError(format!("KRA client build failed: {e:?}")))?;
130
131        let base_url = kra_url.as_str().trim_end_matches('/').to_owned();
132
133        // When mTLS is active, skip basic auth — cert-based auth triggers
134        // the correct recovery code path in Dogtag's SecurityDataProcessor.
135        // Basic auth + mTLS causes Dogtag to authenticate via password
136        // (ignoring the TLS cert identity), which routes through a different
137        // unwrap path that fails with NSS error -8152.
138        let basic_auth = if use_mtls {
139            None
140        } else {
141            match (
142                config.kra_username.as_ref().or(config.username.as_ref()),
143                config.kra_password.as_ref().or(config.password.as_ref()),
144            ) {
145                (Some(user), Some(pass)) => Some((user.clone(), pass.clone())),
146                _ if is_http => {
147                    tracing::error!(
148                        "KRA HTTP mode requires explicit kra_username/kra_password in config"
149                    );
150                    return Err(DogtagError::ConfigError(
151                        "kra_username and kra_password are required for HTTP connections".into(),
152                    ));
153                }
154                _ => None,
155            }
156        };
157
158        Ok(Self {
159            http,
160            base_url,
161            basic_auth,
162        })
163    }
164
165    pub async fn login(&self) {
166        // Dogtag v2 uses GET for session login (v1 uses POST).
167        // Try GET first, fall back to POST for older KRAs.
168        let login_resp = self.get_json("/kra/v2/account/login").await;
169        match login_resp {
170            Ok(r) if r.status().is_success() => {
171                tracing::info!("KRA session login succeeded (v2 GET)");
172                return;
173            }
174            Ok(r) => {
175                tracing::debug!(
176                    status = r.status().as_u16(),
177                    "KRA v2 GET login returned non-200, trying v1 POST"
178                );
179            }
180            Err(e) => {
181                tracing::debug!(error = %e, "KRA v2 GET login failed, trying v1 POST");
182            }
183        }
184        let login_resp = self
185            .post_json("/kra/rest/account/login", &serde_json::json!({}))
186            .await;
187        match login_resp {
188            Ok(r) if r.status().is_success() => {
189                tracing::info!("KRA session login succeeded (v1 POST)");
190            }
191            Ok(r) => {
192                tracing::warn!(
193                    status = r.status().as_u16(),
194                    "KRA session login returned non-200 (continuing)"
195                );
196            }
197            Err(e) => {
198                tracing::warn!(error = %e, "KRA session login failed (continuing without session)");
199            }
200        }
201    }
202
203    pub async fn generate_key(&self, key_type: &str, key_size: u32) -> DogtagResult<KeyGenResult> {
204        self.login().await;
205        debug!(key_type, key_size, "Generating key on KRA");
206
207        let class_name =
208            if key_type.eq_ignore_ascii_case("AES") || key_type.eq_ignore_ascii_case("DESede") {
209                "com.netscape.certsrv.key.SymKeyGenerationRequest"
210            } else {
211                "com.netscape.certsrv.key.AsymKeyGenerationRequest"
212            };
213
214        let client_key_id = format!(
215            "kipuka-{:x}",
216            std::time::SystemTime::now()
217                .duration_since(std::time::UNIX_EPOCH)
218                .unwrap_or_default()
219                .as_nanos()
220        );
221
222        let request = KeyGenRequest {
223            class_name: class_name.to_owned(),
224            attributes: RestAttributes {
225                attribute: vec![
226                    RestAttribute {
227                        name: "clientKeyID".into(),
228                        value: client_key_id,
229                    },
230                    RestAttribute {
231                        name: "keyAlgorithm".into(),
232                        value: key_type.to_owned(),
233                    },
234                    RestAttribute {
235                        name: "keySize".into(),
236                        value: key_size.to_string(),
237                    },
238                    RestAttribute {
239                        name: "keyUsage".into(),
240                        value: "wrap,unwrap".into(),
241                    },
242                ],
243            },
244        };
245
246        let resp = self
247            .post_json("/kra/v2/agent/keyrequests", &request)
248            .await?;
249        let keygen_resp: KeyGenResponse = Self::json_response(resp).await?;
250
251        let info = keygen_resp
252            .request_info
253            .ok_or_else(|| DogtagError::KraError("Missing request_info in response".into()))?;
254
255        let key_id = info
256            .key_url
257            .as_ref()
258            .and_then(|url| url.rsplit('/').next())
259            .filter(|s| !s.is_empty())
260            .ok_or_else(|| DogtagError::KraError("KRA response missing keyURL or key ID".into()))?
261            .to_owned();
262
263        // Fetch the public key via GET /kra/v2/agent/keys/{key_id}.
264        // The keygen response doesn't include the public key directly,
265        // but the key info endpoint returns it as base64 SPKI DER.
266        let public_key = self.get_public_key(&key_id).await?;
267
268        Ok(KeyGenResult {
269            key_id,
270            request_id: info.request_id.unwrap_or_default(),
271            public_key: Some(public_key),
272            wrapped_private_key: None,
273        })
274    }
275
276    pub async fn archive_key(&self, key_data: &[u8], algorithm: &str) -> DogtagResult<String> {
277        use base64::Engine;
278        let wrapped = base64::engine::general_purpose::STANDARD.encode(key_data);
279
280        let body = serde_json::json!({
281            "clientKeyID": format!("kipuka-archive-{:x}", std::time::SystemTime::now()
282                .duration_since(std::time::UNIX_EPOCH)
283                .unwrap_or_default()
284                .as_nanos()),
285            "dataType": "symmetricKey",
286            "keyAlgorithm": algorithm,
287            "wrappedPrivateData": wrapped,
288        });
289
290        let resp = self.post_json("/kra/v2/agent/keys/archive", &body).await?;
291        let archive: ArchiveResponse = Self::json_response(resp).await?;
292
293        archive
294            .request_info
295            .and_then(|i| i.request_id)
296            .ok_or_else(|| DogtagError::KraError("No request_id in archive response".into()))
297    }
298
299    pub async fn recover_key(&self, key_id: &str) -> DogtagResult<Vec<u8>> {
300        let body = serde_json::json!({});
301        let resp = self
302            .post_json(&format!("/kra/v2/agent/keys/{key_id}/recover"), &body)
303            .await?;
304        let recover: RecoverResponse = Self::json_response(resp).await?;
305
306        let data_b64 = recover
307            .data
308            .ok_or_else(|| DogtagError::KraError("Missing data in recovery response".into()))?;
309
310        use base64::Engine;
311        base64::engine::general_purpose::STANDARD
312            .decode(&data_b64)
313            .map_err(|e| DogtagError::KraError(format!("Invalid base64 in recovered key: {e}")))
314    }
315
316    /// Recover a private key via PKCS#12 passphrase path.
317    ///
318    /// Three-call flow per Dogtag `KeyClient.retrieveKeyByPKCS12()`:
319    /// 1. Submit recovery request (keyId + certificate)
320    /// 2. Approve the recovery (mandatory, even with single agent)
321    /// 3. Retrieve PKCS#12 (keyId + requestId + passphrase)
322    ///
323    /// Returns the PKCS#8 DER-encoded private key.
324    pub async fn recover_key_p12(&self, key_id: &str, cert_der: &[u8]) -> DogtagResult<Vec<u8>> {
325        self.recover_key_inner(key_id, Some(cert_der)).await
326    }
327
328    /// Recover a private key via PKCS#12 without requiring a certificate.
329    ///
330    /// Variant for freshly KRA-generated keys (SSKG flow: generate →
331    /// recover → build CSR → enroll). Returns PKCS#8 DER.
332    pub async fn recover_key_no_cert(&self, key_id: &str) -> DogtagResult<Vec<u8>> {
333        self.recover_key_inner(key_id, None).await
334    }
335
336    /// Session-key-based key recovery.
337    ///
338    /// Uses a single POST to `/kra/v2/agent/keys/retrieve` with
339    /// `transWrappedSessionKey` — the same flow as Dogtag's `pki
340    /// kra-key-retrieve` CLI. `KeyProcessor.retrieveKey()` creates
341    /// the recovery request, auto-approves (when the caller is in
342    /// `Data Recovery Manager Agents` and `noOfRequiredRecoveryAgents=1`),
343    /// and returns the wrapped key in one round-trip.
344    async fn recover_key_inner(
345        &self,
346        key_id: &str,
347        _cert_der: Option<&[u8]>,
348    ) -> DogtagResult<Vec<u8>> {
349        use base64::Engine;
350
351        self.login().await;
352
353        let mut session_key = Zeroizing::new(vec![0u8; 16]);
354        openssl::rand::rand_bytes(&mut session_key)
355            .map_err(|e| DogtagError::KraError(format!("Failed to generate session key: {e}")))?;
356
357        let transport_der = self.get_transport_cert().await?;
358        let trans_wrapped_b64 = wrap_session_key(&transport_der, &session_key, false)?;
359
360        let retrieve_req = KeyGenRequest {
361            class_name: "com.netscape.certsrv.key.KeyRecoveryRequest".to_owned(),
362            attributes: RestAttributes {
363                attribute: vec![
364                    RestAttribute {
365                        name: "keyId".into(),
366                        value: key_id.to_owned(),
367                    },
368                    RestAttribute {
369                        name: "transWrappedSessionKey".into(),
370                        value: trans_wrapped_b64,
371                    },
372                ],
373            },
374        };
375        tracing::info!(key_id = %key_id, "retrieving key via single-call session-key wrapping");
376        let resp = self
377            .post_json("/kra/v2/agent/keys/retrieve", &retrieve_req)
378            .await?;
379        if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
380            return Err(DogtagError::KraError(
381                "Recovery retrieve returned 401 — session may have expired".into(),
382            ));
383        }
384
385        let body: serde_json::Value = Self::json_response(resp).await?;
386
387        let wrapped_b64 = body
388            .get("wrappedPrivateData")
389            .and_then(|v| v.as_str())
390            .ok_or_else(|| {
391                DogtagError::KraError(format!(
392                    "No wrappedPrivateData in retrieve response: {}",
393                    crate::truncate_str(&body.to_string(), 300)
394                ))
395            })?;
396        let wrapped = base64::engine::general_purpose::STANDARD
397            .decode(wrapped_b64)
398            .map_err(|e| {
399                DogtagError::KraError(format!("Invalid base64 in wrappedPrivateData: {e}"))
400            })?;
401
402        // Branch on wrap mode: nonceData present → AES-CBC, absent → AES-KWP
403        let der = match body.get("nonceData").and_then(|v| v.as_str()) {
404            Some(nonce_b64) => {
405                let iv = base64::engine::general_purpose::STANDARD
406                    .decode(nonce_b64)
407                    .map_err(|e| DogtagError::KraError(format!("Invalid nonceData: {e}")))?;
408                let cipher = match session_key.len() {
409                    16 => openssl::symm::Cipher::aes_128_cbc(),
410                    32 => openssl::symm::Cipher::aes_256_cbc(),
411                    n => {
412                        return Err(DogtagError::KraError(format!(
413                            "Unsupported session key length {n} for AES-CBC"
414                        )));
415                    }
416                };
417                Zeroizing::new(
418                    openssl::symm::decrypt(cipher, &session_key, Some(&iv), &wrapped).map_err(
419                        |e| DogtagError::KraError(format!("AES-CBC unwrap failed: {e}")),
420                    )?,
421                )
422            }
423            None => aes_kwp_unwrap(&session_key, &wrapped)?,
424        };
425
426        tracing::info!(key_len = der.len(), "private key recovered (PKCS#8 DER)");
427        let out = der.to_vec();
428        // der (Zeroizing) scrubs on drop; out is the caller's responsibility
429        Ok(out)
430    }
431
432    /// Fetch the KRA transport certificate as DER.
433    ///
434    /// The transport cert's ML-KEM-1024 public key is used by clients to
435    /// encapsulate the shared secret for key archival.
436    pub async fn get_transport_cert(&self) -> DogtagResult<Vec<u8>> {
437        let resp = self.get_json("/kra/v2/config/cert/transport").await?;
438        let body: serde_json::Value = Self::json_response(resp).await?;
439        let pem = body
440            .get("Encoded")
441            .and_then(|v| v.as_str())
442            .ok_or_else(|| {
443                DogtagError::KraError("No Encoded field in transport cert response".into())
444            })?;
445
446        // Strip PEM headers and decode
447        let b64: String = pem.lines().filter(|l| !l.starts_with("-----")).collect();
448        use base64::Engine;
449        base64::engine::general_purpose::STANDARD
450            .decode(&b64)
451            .map_err(|e| DogtagError::KraError(format!("Transport cert base64 decode failed: {e}")))
452    }
453
454    /// Search for archived keys on the KRA.
455    ///
456    /// Returns the most recent key matching the query. The KRA stores keys
457    /// with a `clientKeyID` that typically contains the cert subject DN.
458    /// If no query is given, returns the most recently archived key.
459    pub async fn search_keys(
460        &self,
461        client_key_id: Option<&str>,
462        max_results: u32,
463    ) -> DogtagResult<Vec<KeySearchEntry>> {
464        self.login().await;
465
466        let mut path = format!("/kra/rest/agent/keys?maxResults={max_results}&status=active");
467        if let Some(ckid) = client_key_id {
468            use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
469            let encoded = utf8_percent_encode(ckid, NON_ALPHANUMERIC);
470            path.push_str(&format!("&clientKeyID={encoded}"));
471        }
472
473        let resp = self.get_json(&path).await?;
474        let body: serde_json::Value = Self::json_response(resp).await?;
475
476        let entries = body
477            .get("entries")
478            .and_then(|v| v.as_array())
479            .map(|arr| {
480                arr.iter()
481                    .filter_map(|e| {
482                        let key_url = e.get("keyURL").and_then(|v| v.as_str()).unwrap_or("");
483                        let key_id = key_url.rsplit('/').next().unwrap_or("").to_owned();
484                        if key_id.is_empty() {
485                            return None;
486                        }
487                        Some(KeySearchEntry {
488                            key_id,
489                            client_key_id: e
490                                .get("clientKeyID")
491                                .and_then(|v| v.as_str())
492                                .unwrap_or("")
493                                .to_owned(),
494                            algorithm: e
495                                .get("algorithm")
496                                .and_then(|v| v.as_str())
497                                .unwrap_or("")
498                                .to_owned(),
499                            size: e.get("size").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
500                            status: e
501                                .get("status")
502                                .and_then(|v| v.as_str())
503                                .unwrap_or("")
504                                .to_owned(),
505                        })
506                    })
507                    .collect()
508            })
509            .unwrap_or_default();
510
511        Ok(entries)
512    }
513
514    /// Fetch the public key (base64 SPKI DER) for a KRA-generated key.
515    async fn get_public_key(&self, key_id: &str) -> DogtagResult<String> {
516        let path = format!("/kra/v2/agent/keys/{key_id}");
517        let resp = self.get_json(&path).await?;
518        let body: serde_json::Value = Self::json_response(resp).await?;
519        body.get("publicKey")
520            .and_then(|v| v.as_str())
521            .map(|s| s.to_owned())
522            .ok_or_else(|| DogtagError::KraError("No publicKey in key info response".into()))
523    }
524
525    async fn get_json(&self, path: &str) -> DogtagResult<reqwest::Response> {
526        let url = format!("{}{}", self.base_url, path);
527        let mut req = self.http.get(&url).header("Accept", "application/json");
528        if let Some((ref user, ref pass)) = self.basic_auth {
529            req = req.basic_auth(user, Some(pass));
530        }
531        req.send()
532            .await
533            .map_err(|e| DogtagError::HttpError(e.to_string()))
534    }
535
536    async fn post_json<T: serde::Serialize + ?Sized>(
537        &self,
538        path: &str,
539        body: &T,
540    ) -> DogtagResult<reqwest::Response> {
541        let url = format!("{}{}", self.base_url, path);
542        let mut req = self
543            .http
544            .post(&url)
545            .header("Accept", "application/json")
546            .json(body);
547        if let Some((ref user, ref pass)) = self.basic_auth {
548            req = req.basic_auth(user, Some(pass));
549        }
550        req.send()
551            .await
552            .map_err(|e| DogtagError::HttpError(e.to_string()))
553    }
554
555    async fn json_response<T: serde::de::DeserializeOwned>(
556        resp: reqwest::Response,
557    ) -> DogtagResult<T> {
558        let status = resp.status();
559        let content_type = resp
560            .headers()
561            .get(reqwest::header::CONTENT_TYPE)
562            .and_then(|v| v.to_str().ok())
563            .unwrap_or("unknown")
564            .to_owned();
565
566        let body = crate::bounded_text(resp).await?;
567
568        if !status.is_success() {
569            return Err(DogtagError::ApiError {
570                status: status.as_u16(),
571                body,
572            });
573        }
574
575        tracing::debug!(
576            body_len = body.len(),
577            content_type = %content_type,
578            "parsing KRA response"
579        );
580
581        serde_json::from_str(&body).map_err(|e| {
582            let preview = crate::truncate_str(&body, 500);
583            DogtagError::ParseError(format!(
584                "JSON parse failed (content-type: {content_type}): {e}; body: {preview}"
585            ))
586        })
587    }
588}
589
590/// RSA-wrap a session key with the KRA transport cert's public key.
591///
592/// `use_oaep`: false = PKCS#1 v1.5 (Dogtag default), true = RSA-OAEP
593/// (only when KRA has `keyWrap.useOAEP=true` in CS.cfg).
594fn wrap_session_key(
595    transport_cert_der: &[u8],
596    session_key: &[u8],
597    use_oaep: bool,
598) -> DogtagResult<String> {
599    use base64::Engine;
600
601    let transport_cert = openssl::x509::X509::from_der(transport_cert_der)
602        .map_err(|e| DogtagError::KraError(format!("Failed to parse transport cert: {e}")))?;
603    let rsa = transport_cert
604        .public_key()
605        .map_err(|e| DogtagError::KraError(format!("Failed to extract transport public key: {e}")))?
606        .rsa()
607        .map_err(|e| DogtagError::KraError(format!("Transport cert is not RSA: {e}")))?;
608
609    let padding = if use_oaep {
610        openssl::rsa::Padding::PKCS1_OAEP
611    } else {
612        openssl::rsa::Padding::PKCS1
613    };
614
615    let mut out = vec![0u8; rsa.size() as usize];
616    let n = rsa
617        .public_encrypt(session_key, &mut out, padding)
618        .map_err(|e| DogtagError::KraError(format!("RSA session key wrap failed: {e}")))?;
619    out.truncate(n);
620
621    Ok(base64::engine::general_purpose::STANDARD.encode(&out))
622}
623
624/// AES Key Wrap with Padding (RFC 5649) unwrap.
625///
626/// Used when the KRA returns `wrappedPrivateData` without `nonceData` —
627/// the default config (`kra.allowEncDecrypt.recovery=false`).
628fn aes_kwp_unwrap(key: &[u8], wrapped: &[u8]) -> DogtagResult<Zeroizing<Vec<u8>>> {
629    unsafe {
630        let cipher_name = match key.len() {
631            16 => c"AES-128-WRAP-PAD",
632            24 => c"AES-192-WRAP-PAD",
633            32 => c"AES-256-WRAP-PAD",
634            _ => {
635                return Err(DogtagError::KraError(format!(
636                    "Invalid KEK length {}",
637                    key.len()
638                )));
639            }
640        };
641
642        let cipher = openssl_sys::EVP_CIPHER_fetch(
643            std::ptr::null_mut(),
644            cipher_name.as_ptr(),
645            std::ptr::null(),
646        );
647        if cipher.is_null() {
648            return Err(DogtagError::KraError("AES-KWP cipher not available".into()));
649        }
650
651        let ctx = openssl_sys::EVP_CIPHER_CTX_new();
652        if ctx.is_null() {
653            openssl_sys::EVP_CIPHER_free(cipher as *mut _);
654            return Err(DogtagError::KraError("EVP_CIPHER_CTX_new failed".into()));
655        }
656
657        openssl_sys::EVP_CIPHER_CTX_set_flags(ctx, openssl_sys::EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);
658
659        let rc = openssl_sys::EVP_DecryptInit_ex(
660            ctx,
661            cipher,
662            std::ptr::null_mut(),
663            key.as_ptr(),
664            std::ptr::null(),
665        );
666        openssl_sys::EVP_CIPHER_free(cipher as *mut _);
667        if rc != 1 {
668            openssl_sys::EVP_CIPHER_CTX_free(ctx);
669            return Err(DogtagError::KraError(
670                "EVP_DecryptInit_ex failed for AES-KWP".into(),
671            ));
672        }
673
674        let max_out = wrapped.len() + 32;
675        let mut output = Zeroizing::new(vec![0u8; max_out]);
676        let mut out_len: i32 = 0;
677
678        let rc = openssl_sys::EVP_DecryptUpdate(
679            ctx,
680            output.as_mut_ptr(),
681            &mut out_len,
682            wrapped.as_ptr(),
683            wrapped.len() as i32,
684        );
685        if rc != 1 {
686            openssl_sys::EVP_CIPHER_CTX_free(ctx);
687            return Err(DogtagError::KraError(
688                "AES-KWP unwrap failed (DecryptUpdate)".into(),
689            ));
690        }
691
692        let mut final_len: i32 = 0;
693        let rc = openssl_sys::EVP_DecryptFinal_ex(
694            ctx,
695            output.as_mut_ptr().add(out_len as usize),
696            &mut final_len,
697        );
698        openssl_sys::EVP_CIPHER_CTX_free(ctx);
699
700        if rc != 1 {
701            return Err(DogtagError::KraError(
702                "AES-KWP unwrap failed (DecryptFinal)".into(),
703            ));
704        }
705
706        let total = (out_len + final_len) as usize;
707        output.truncate(total);
708        Ok(output)
709    }
710}