1use std::sync::Arc;
16
17use axum::body::Bytes;
18use axum::extract::State;
19use axum::http::{HeaderValue, StatusCode, header};
20use axum::response::{IntoResponse, Response};
21
22use crate::auth::EstAuth;
23use crate::error::KipukaError;
24use crate::routes::LabelExtractor;
25use crate::routes::est::{content_types, decode_est_base64, encode_est_base64};
26use crate::state::AppState;
27
28const MULTIPART_BOUNDARY: &str = "estServerKeyGenBoundary";
33
34pub async fn post_serverkeygen(
86 auth: EstAuth,
87 label: LabelExtractor,
88 State(state): State<Arc<AppState>>,
89 body: Bytes,
90) -> Result<Response, KipukaError> {
91 let ca_id = label.ca_id();
92 let identity = &auth.0.identity;
93 state.admit_enrollment(identity, "serverkeygen").await?;
94
95 if !state.config.est.serverkeygen {
97 return Err(KipukaError::Est(
98 "server-side key generation is not enabled".into(),
99 ));
100 }
101
102 tracing::info!(
103 ca_id = %ca_id,
104 label = %label.label,
105 identity = %identity,
106 method = ?auth.0.method,
107 "serverkeygen request"
108 );
109
110 let csr_der = decode_est_base64(&body)
112 .map_err(|e| KipukaError::BadRequest(format!("CSR template decoding failed: {e}")))?;
113
114 if csr_der.is_empty() {
115 return Err(KipukaError::BadRequest("empty CSR template".into()));
116 }
117
118 if let Err(reason) =
127 crate::auth::enroll_authz::authorize_csr_der(&csr_der, identity, &label.enroll_policy())
128 {
129 tracing::warn!(
130 ca_id = %ca_id,
131 identity = %identity,
132 %reason,
133 "serverkeygen rejected: CSR template not authorized for requester"
134 );
135 state
136 .record_audit_event_with_actor(
137 "serverkeygen_denied",
138 identity,
139 &format!("ca_id={ca_id}, identity={identity}, reason={reason}"),
140 )
141 .await;
142 return Err(KipukaError::Forbidden(format!(
143 "enrollment not authorized: {reason}"
144 )));
145 }
146
147 let _ca = state.get_ca(ca_id).ok_or(KipukaError::NotFound)?;
149
150 if let Some(ref dogtag_pool) = state.dogtag {
156 let dogtag_cfg = state
157 .config
158 .dogtag
159 .as_ref()
160 .expect("dogtag config present when pool is set");
161
162 let sskg_profile = dogtag_cfg
163 .sskg_profile_id
164 .as_deref()
165 .unwrap_or("caServerKeygenEST");
166
167 let template = openssl::x509::X509Req::from_der(&csr_der)
169 .map_err(|e| KipukaError::BadRequest(format!("CSR template parse: {e}")))?;
170 let subject_cn = {
171 let sn = template.subject_name();
172 sn.entries_by_nid(openssl::nid::Nid::COMMONNAME)
173 .next()
174 .and_then(|e| String::from_utf8(e.data().as_slice().to_vec()).ok())
175 .unwrap_or_else(|| identity.to_string())
176 };
177
178 let key_type = detect_key_type_from_csr(&csr_der);
180 let (kt_str, ks_str) = match &key_type {
181 crate::ca::keygen::KeyType::Rsa(bits) => ("RSA".to_string(), bits.to_string()),
182 crate::ca::keygen::KeyType::Ecdsa(curve) => {
183 let bits = match curve {
184 crate::ca::keygen::EcCurve::P256 => 256,
185 crate::ca::keygen::EcCurve::P384 => 384,
186 };
187 ("EC".to_string(), bits.to_string())
188 }
189 other => {
192 return Err(KipukaError::BadRequest(format!(
193 "Dogtag server-side keygen supports RSA/EC only; \
194 requested key type {other:?} requires a local CA label"
195 )));
196 }
197 };
198
199 let mut rand = [0u8; 24];
201 openssl::rand::rand_bytes(&mut rand).map_err(|e| KipukaError::Ca(format!("rand: {e}")))?;
202 let p12_password = hex::encode(rand);
203
204 tracing::info!(
205 ca_id = %ca_id,
206 identity = %identity,
207 sskg_profile = %sskg_profile,
208 subject_cn = %subject_cn,
209 key_type = %kt_str,
210 key_size = %ks_str,
211 "SSKG: submitting to CA SSKG profile"
212 );
213
214 let ca_client = dogtag_pool
215 .get_client()
216 .map_err(|e| KipukaError::ServiceUnavailable(format!("Dogtag CA unavailable: {e}")))?;
217
218 let sskg_result = ca_client
219 .server_keygen(
220 sskg_profile,
221 &subject_cn,
222 &subject_cn,
223 &p12_password,
224 &kt_str,
225 ks_str.parse().unwrap_or(2048),
226 )
227 .await
228 .map_err(|e| KipukaError::Ca(format!("SSKG enrollment failed: {e}")))?;
229
230 let (cert_der, private_key_der) = if let Some(ref p12_b64) = sskg_result.pkcs12_b64 {
235 use base64::Engine;
237 let p12_der = base64::engine::general_purpose::STANDARD
238 .decode(p12_b64)
239 .map_err(|e| KipukaError::Ca(format!("PKCS#12 base64 decode: {e}")))?;
240
241 let parsed = openssl::pkcs12::Pkcs12::from_der(&p12_der)
242 .map_err(|e| KipukaError::Ca(format!("PKCS#12 parse: {e}")))?
243 .parse2(&p12_password)
244 .map_err(|e| KipukaError::Ca(format!("PKCS#12 decrypt: {e}")))?;
245
246 let pkey = parsed
247 .pkey
248 .ok_or_else(|| KipukaError::Ca("No private key in PKCS#12".into()))?;
249 let key_der = encode_pkcs8_private_key(&pkey)?;
250
251 let cert = if let Some(cert) = &parsed.cert {
252 cert.to_der()
253 .map_err(|e| KipukaError::Ca(format!("cert DER: {e}")))?
254 } else {
255 sskg_result
256 .certificate_der
257 .clone()
258 .ok_or_else(|| KipukaError::Ca("No certificate in SSKG response".into()))?
259 };
260 (cert, key_der)
261 } else {
262 tracing::info!("SSKG: no P12 in enrollment output — recovering key from KRA");
264
265 let cert = sskg_result.certificate_der.clone().ok_or_else(|| {
266 KipukaError::Ca("No certificate in SSKG response (needed for KRA recovery)".into())
267 })?;
268
269 let kra_client = kipuka_dogtag::KraClient::new(dogtag_cfg)
271 .map_err(|e| KipukaError::Ca(format!("KRA client init: {e}")))?;
272
273 let keys = kra_client
277 .search_keys(Some(&subject_cn), 3)
278 .await
279 .map_err(|e| KipukaError::Ca(format!("KRA key search: {e}")))?;
280
281 let keys = if keys.is_empty() {
283 tracing::warn!("KRA key search by CN returned nothing, trying unfiltered");
284 kra_client
285 .search_keys(None, 1)
286 .await
287 .map_err(|e| KipukaError::Ca(format!("KRA key search (unfiltered): {e}")))?
288 } else {
289 keys
290 };
291
292 let key_entry = keys.first().ok_or_else(|| {
293 KipukaError::Ca("No keys found on KRA after SSKG enrollment".into())
294 })?;
295
296 tracing::info!(
297 key_id = %key_entry.key_id,
298 client_key_id = %key_entry.client_key_id,
299 algorithm = %key_entry.algorithm,
300 size = key_entry.size,
301 "SSKG: found archived key on KRA, recovering"
302 );
303
304 state
305 .record_audit_event(
306 "sskg_kra_recovery",
307 &format!(
308 "ca_id={ca_id}, identity={identity}, key_id={}, algorithm={}",
309 key_entry.key_id, key_entry.algorithm
310 ),
311 )
312 .await;
313
314 let key_der = kra_client
317 .recover_key_no_cert(&key_entry.key_id)
318 .await
319 .map_err(|e| KipukaError::Ca(format!("KRA key recovery: {e}")))?;
320
321 (cert, key_der)
322 };
323
324 tracing::info!(
325 cert_len = cert_der.len(),
326 key_len = private_key_der.len(),
327 request_id = %sskg_result.request_id,
328 "SSKG complete via CA profile — cert + key ready"
329 );
330
331 let cert_pkcs7_der =
333 crate::routes::cacerts::build_certs_only_pkcs7(std::slice::from_ref(&cert_der))?;
334 let response_body = build_multipart_response(&cert_pkcs7_der, &private_key_der);
335
336 let content_type = format!(
337 "{}; boundary={}",
338 content_types::MULTIPART_MIXED,
339 MULTIPART_BOUNDARY
340 );
341
342 let mut resp = (StatusCode::OK, response_body).into_response();
343 if let Ok(hv) = HeaderValue::from_str(&content_type) {
344 resp.headers_mut().insert(header::CONTENT_TYPE, hv);
345 }
346
347 state
348 .record_audit_event(
349 "serverkeygen_success",
350 &format!(
351 "ca_id={ca_id}, identity={identity}, backend=sskg-profile, profile={sskg_profile}, request_id={}",
352 sskg_result.request_id
353 ),
354 )
355 .await;
356
357 return Ok(resp);
358 }
359
360 let ca = state.get_ca(ca_id).ok_or(KipukaError::NotFound)?;
364
365 let key_type = detect_key_type_from_csr(&csr_der);
370
371 tracing::info!(
372 ca_id = %ca_id,
373 identity = %identity,
374 key_type = ?key_type,
375 "generating software key pair for serverkeygen"
376 );
377
378 let keygen_config = crate::ca::keygen::KeyGenConfig::default();
380 let keygen_result = crate::ca::keygen::generate_key_pair(&key_type, &keygen_config)
381 .map_err(|e| KipukaError::Ca(format!("software key generation failed: {e}")))?;
382
383 let new_csr_der = build_keygen_csr(
390 &csr_der,
391 &keygen_result.public_key_der,
392 &keygen_result.private_key_der,
393 )?;
394
395 let ca_cfg = state
397 .config
398 .cas
399 .iter()
400 .find(|c| c.id == ca_id)
401 .ok_or_else(|| KipukaError::Ca(format!("CA config not found for id={ca_id}")))?;
402
403 let resolved_key = crate::ca::issue::resolve_signing_key(ca_cfg, state.hsm.as_ref()).await?;
404
405 let profile = crate::ca::issue::EnrollmentProfile {
407 max_validity_days: ca
408 .validity_days
409 .min(crate::ca::issue::cab_forum_max_validity_days()),
410 ..crate::ca::issue::EnrollmentProfile::default()
411 };
412
413 let issuance_result = crate::ca::issue::issue_certificate(
414 &new_csr_der,
415 &profile,
416 &ca.cert_der,
417 resolved_key.as_signing_key(),
418 &ca.hash_algorithm,
419 ca.ocsp_url.as_deref(),
420 ca.crl_url.as_deref(),
421 )
422 .map_err(|e| KipukaError::Ca(format!("certificate issuance for keygen failed: {e}")))?;
423
424 crate::ca::issue::persist_certificate(&state, ca_id, &profile.name, &issuance_result).await?;
425
426 let cert_pkcs7_der = crate::routes::cacerts::build_certs_only_pkcs7(std::slice::from_ref(
428 &issuance_result.certificate_der,
429 ))?;
430
431 let response_body = build_multipart_response(&cert_pkcs7_der, &keygen_result.private_key_der);
433
434 let content_type = format!(
435 "{}; boundary={}",
436 content_types::MULTIPART_MIXED,
437 MULTIPART_BOUNDARY
438 );
439
440 let mut resp = (StatusCode::OK, response_body).into_response();
441 if let Ok(hv) = HeaderValue::from_str(&content_type) {
442 resp.headers_mut().insert(header::CONTENT_TYPE, hv);
443 }
444
445 state
446 .record_audit_event(
447 "serverkeygen_success",
448 &format!(
449 "ca_id={ca_id}, identity={identity}, backend=software, key_type={key_type:?}, serial={}",
450 issuance_result.serial_number
451 ),
452 )
453 .await;
454
455 Ok(resp)
456}
457
458pub(crate) fn build_keygen_csr(
467 template_csr_der: &[u8],
468 public_key_der: &[u8],
469 private_key_pkcs8_der: &[u8],
470) -> Result<Vec<u8>, KipukaError> {
471 let template_csr = synta_certificate::csr::CertificationRequest::from_der(template_csr_der)
472 .map_err(|e| KipukaError::BadRequest(format!("CSR template parse failed: {e}")))?;
473
474 let subject_der = template_csr
475 .certification_request_info
476 .subject
477 .to_der()
478 .map_err(|e| KipukaError::Ca(format!("CSR subject encode failed: {e}")))?;
479
480 let generated_key = synta_certificate::BackendPrivateKey::from_der(private_key_pkcs8_der)
482 .map_err(|e| KipukaError::Ca(format!("failed to load generated key: {e}")))?;
483
484 let signer = {
485 use synta_certificate::PrivateKey as _;
486 generated_key.as_signer("sha256")
487 };
488
489 let mut builder = synta_certificate::CsrBuilder::new()
490 .subject_name(&subject_der)
491 .public_key_der(public_key_der);
492 for (oid, critical, value) in crate::ca::issue::requested_extensions(template_csr_der)
493 .map_err(|e| KipukaError::BadRequest(e.to_string()))?
494 {
495 builder = builder.add_extension(oid, critical, &value);
496 }
497 builder
498 .sign(&signer)
499 .map_err(|e| KipukaError::Ca(format!("CSR construction failed: {e}")))
500}
501
502pub(crate) fn detect_key_type_from_csr(csr_der: &[u8]) -> crate::ca::keygen::KeyType {
508 use crate::ca::keygen::{EcCurve, KeyType};
509
510 let Ok(csr) = synta_certificate::csr::CertificationRequest::from_der(csr_der) else {
511 tracing::debug!("CSR template unparseable; defaulting to RSA-2048");
512 return KeyType::Rsa(2048);
513 };
514
515 let spki = &csr.certification_request_info.subject_pkinfo;
516 let key_bits = spki.subject_public_key.bit_len();
517
518 let pk_info = synta_certificate::decode_public_key_info(
519 &spki.algorithm.algorithm,
520 spki.algorithm.parameters.as_ref(),
521 spki.subject_public_key.as_bytes(),
522 key_bits,
523 );
524
525 match &pk_info {
526 synta_certificate::PublicKeyInfo::Rsa { bit_count, .. } => {
527 let bits = match *bit_count {
529 0..=2048 => 2048u32,
530 2049..=3072 => 3072,
531 _ => 4096,
532 };
533 tracing::debug!(bits, "detected RSA key type from CSR template");
534 KeyType::Rsa(bits)
535 }
536 synta_certificate::PublicKeyInfo::Ec {
537 curve_nist_name, ..
538 } => {
539 let curve = match curve_nist_name {
540 Some("P-384") => EcCurve::P384,
541 _ => EcCurve::P256, };
543 tracing::debug!(curve = %curve, "detected EC key type from CSR template");
544 KeyType::Ecdsa(curve)
545 }
546 synta_certificate::PublicKeyInfo::Unknown { alg_name, .. } => {
547 tracing::debug!(
548 algorithm = %alg_name,
549 "unrecognised CSR template algorithm; defaulting to RSA-2048"
550 );
551 KeyType::Rsa(2048)
552 }
553 }
554}
555
556pub(crate) fn build_multipart_response(cert_pkcs7_der: &[u8], private_key_pkcs8: &[u8]) -> String {
562 let cert_b64 = encode_est_base64(cert_pkcs7_der);
563 let key_b64 = encode_est_base64(private_key_pkcs8);
564
565 format!(
566 "\r\n--{boundary}\r\n\
567 Content-Type: {cert_type}\r\n\
568 Content-Transfer-Encoding: base64\r\n\
569 \r\n\
570 {cert_b64}\r\n\
571 --{boundary}\r\n\
572 Content-Type: {key_type}\r\n\
573 Content-Transfer-Encoding: base64\r\n\
574 \r\n\
575 {key_b64}\r\n\
576 --{boundary}--\r\n",
577 boundary = MULTIPART_BOUNDARY,
578 cert_type = content_types::PKCS7_CERTS,
579 key_type = content_types::PKCS8,
580 )
581}
582
583fn encode_pkcs8_private_key(
585 key: &openssl::pkey::PKeyRef<openssl::pkey::Private>,
586) -> Result<Vec<u8>, KipukaError> {
587 key.private_key_to_pkcs8()
588 .map_err(|e| KipukaError::Ca(format!("PKCS#8 DER: {e}")))
589}
590
591#[cfg(test)]
592mod pkcs8_response_regression {
593 #[test]
594 fn recovered_rsa_and_ec_keys_use_strict_pkcs8_encoding() {
595 use openssl::{
596 ec::{EcGroup, EcKey},
597 nid::Nid,
598 pkey::PKey,
599 rsa::Rsa,
600 };
601 let rsa = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
602 let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
603 let ec = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
604 for key in [rsa, ec] {
605 let der = super::encode_pkcs8_private_key(&key).unwrap();
606 let parsed = PKey::private_key_from_pkcs8(&der)
607 .expect("application/pkcs8 must contain PrivateKeyInfo");
608 assert!(parsed.public_eq(&key));
609 assert!(PKey::private_key_from_pkcs8(&key.private_key_to_der().unwrap()).is_err());
610 }
611 }
612}