1use std::sync::Arc;
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15use tracing::{debug, info, warn};
16
17#[derive(Debug, Error)]
19pub enum IssuanceError {
20 #[error("invalid CSR: {0}")]
22 InvalidCsr(String),
23
24 #[error("key too small: {algorithm} {bits}-bit (minimum {min_bits}-bit required)")]
26 KeyTooSmall {
27 algorithm: String,
28 bits: u32,
29 min_bits: u32,
30 },
31
32 #[error("validity period {requested_days} days exceeds maximum {max_days} days")]
34 ValidityTooLong { requested_days: u32, max_days: u32 },
35
36 #[error("missing required extension: {0}")]
38 MissingExtension(String),
39
40 #[error("unknown enrollment profile: {0}")]
42 UnknownProfile(String),
43
44 #[error("signing failed: {0}")]
46 SigningError(String),
47
48 #[error("storage error: {0}")]
50 StorageError(String),
51}
52
53#[derive(Debug, Clone)]
55pub struct IssuanceResult {
56 pub certificate_der: Vec<u8>,
58 pub serial_number: String,
60 pub subject_dn: String,
62 pub not_before: DateTime<Utc>,
64 pub not_after: DateTime<Utc>,
66}
67
68pub enum CaSigningKey<'a> {
73 Pem(&'a [u8]),
75 Hsm {
77 context: &'a Arc<kipuka_hsm::HsmContext>,
79 key_label: &'a str,
81 },
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct EnrollmentProfile {
90 pub name: String,
92 pub max_validity_days: u32,
94 pub key_usage: Vec<String>,
96 pub extended_key_usage: Vec<String>,
98 pub include_ski: bool,
100 pub include_aki: bool,
102 pub min_rsa_bits: u32,
104 pub min_ecdsa_curve: String,
106 pub ct_enabled: bool,
108
109 #[serde(default)]
114 pub allowed_ml_dsa_levels: Vec<String>,
115
116 #[serde(default)]
120 pub allowed_ml_kem_levels: Vec<String>,
121
122 #[serde(default)]
125 pub allow_composite_ml_dsa: bool,
126
127 #[serde(default)]
132 pub require_dual_cert: bool,
133
134 #[serde(default)]
155 pub must_staple: bool,
156}
157
158impl Default for EnrollmentProfile {
159 fn default() -> Self {
160 Self {
161 name: "default".into(),
162 max_validity_days: 398, key_usage: vec!["digitalSignature".into(), "keyEncipherment".into()],
164 extended_key_usage: vec!["serverAuth".into(), "clientAuth".into()],
165 include_ski: true,
166 include_aki: true,
167 min_rsa_bits: 2048,
168 min_ecdsa_curve: "P-256".into(),
169 ct_enabled: false,
170 allowed_ml_dsa_levels: vec!["ml-dsa-44".into(), "ml-dsa-65".into(), "ml-dsa-87".into()],
172 allowed_ml_kem_levels: vec![
173 "ml-kem-512".into(),
174 "ml-kem-768".into(),
175 "ml-kem-1024".into(),
176 ],
177 allow_composite_ml_dsa: true,
178 require_dual_cert: false,
179 must_staple: false,
180 }
181 }
182}
183
184pub const TLS_FEATURE_MUST_STAPLE_DER: &[u8] = &[0x30, 0x03, 0x02, 0x01, 0x05];
196
197pub const OID_TLS_FEATURE: &str = "1.3.6.1.5.5.7.1.24";
201
202const OID_TLS_FEATURE_COMPONENTS: &[u32] = &[1, 3, 6, 1, 5, 5, 7, 1, 24];
204
205pub fn issue_certificate(
225 csr_der: &[u8],
226 profile: &EnrollmentProfile,
227 ca_cert_der: &[u8],
228 signing_key: CaSigningKey<'_>,
229 hash_algorithm: &str,
230) -> Result<IssuanceResult, IssuanceError> {
231 validate_csr(csr_der)?;
233
234 check_key_size(csr_der, profile)?;
236
237 check_validity_period(profile)?;
239
240 check_required_extensions(profile)?;
242
243 let csr = synta_certificate::csr::CertificationRequest::from_der(csr_der)
245 .map_err(|e| IssuanceError::InvalidCsr(format!("CSR parse failed: {e}")))?;
246
247 let csr_subject_der = csr
248 .certification_request_info
249 .subject
250 .to_der()
251 .map_err(|e| IssuanceError::InvalidCsr(format!("CSR subject encode failed: {e}")))?;
252
253 let csr_spki_der = csr
254 .certification_request_info
255 .subject_pkinfo
256 .to_der()
257 .map_err(|e| IssuanceError::InvalidCsr(format!("CSR SPKI encode failed: {e}")))?;
258
259 let ca_cert = synta_certificate::Certificate::from_der(ca_cert_der)
261 .map_err(|e| IssuanceError::SigningError(format!("CA cert parse failed: {e}")))?;
262
263 let ca_subject_der = ca_cert.tbs_certificate.subject.0;
264 let ca_spki_der = ca_cert
265 .tbs_certificate
266 .subject_public_key_info
267 .to_der()
268 .map_err(|e| IssuanceError::SigningError(format!("CA SPKI encode failed: {e}")))?;
269
270 let pem_key: Option<synta_certificate::BackendPrivateKey>;
272 match &signing_key {
273 CaSigningKey::Pem(pem) => {
274 pem_key = Some(
275 synta_certificate::BackendPrivateKey::from_pem(pem, None).map_err(|e| {
276 IssuanceError::SigningError(format!("CA key parse failed: {e}"))
277 })?,
278 );
279 debug!("loaded CA private key from PEM");
280 }
281 CaSigningKey::Hsm { key_label, .. } => {
282 pem_key = None;
283 debug!(key_label = %key_label, "using HSM-backed CA signing key");
284 }
285 }
286
287 let serial_bytes = generate_serial_bytes();
290 let serial = synta::Integer::from_unsigned_bytes(&serial_bytes);
291 let serial_hex = hex::encode(&serial_bytes);
292
293 let now = Utc::now();
295 let not_after_chrono = now + chrono::Duration::days(profile.max_validity_days as i64);
296
297 let not_before_time = chrono_to_synta_time(now)
298 .map_err(|e| IssuanceError::SigningError(format!("not_before time conversion: {e}")))?;
299 let not_after_time = chrono_to_synta_time(not_after_chrono)
300 .map_err(|e| IssuanceError::SigningError(format!("not_after time conversion: {e}")))?;
301
302 debug!(
304 profile = %profile.name,
305 max_days = profile.max_validity_days,
306 ct = profile.ct_enabled,
307 must_staple = profile.must_staple,
308 "building certificate from CSR"
309 );
310
311 let mut builder = synta_certificate::CertificateBuilder::new()
312 .issuer_name(ca_subject_der)
313 .subject_name(&csr_subject_der)
314 .public_key_der(&csr_spki_der)
315 .serial_number(serial)
316 .not_valid_before(not_before_time)
317 .not_valid_after(not_after_time);
318
319 if let Some(bc_der) = synta_certificate::encode_basic_constraints(false, None) {
321 builder =
322 builder.add_extension_oid(synta_certificate::oids::BASIC_CONSTRAINTS, true, &bc_der);
323 }
324
325 let ku_bits = profile_key_usage_bits(profile);
327 if let Some(ku_der) = synta_certificate::encode_key_usage(ku_bits) {
328 builder = builder.add_extension_oid(synta_certificate::oids::KEY_USAGE, true, &ku_der);
329 }
330
331 let eku_der = profile_extended_key_usage(profile);
333 if let Some(eku) = eku_der {
334 builder =
335 builder.add_extension_oid(synta_certificate::oids::EXTENDED_KEY_USAGE, false, &eku);
336 }
337
338 if profile.include_ski {
340 let hasher = synta_certificate::OpensslKeyIdHasher;
341 if let Some(ski_der) = synta_certificate::encode_subject_key_identifier(
342 &csr_spki_der,
343 synta_certificate::KeyIdMethod::Rfc5280Sha1,
344 &hasher,
345 ) {
346 builder = builder.add_extension_oid(
347 synta_certificate::oids::SUBJECT_KEY_IDENTIFIER,
348 false,
349 &ski_der,
350 );
351 }
352 }
353
354 if profile.include_aki {
356 let hasher = synta_certificate::OpensslKeyIdHasher;
357 if let Some(aki_der) = synta_certificate::encode_authority_key_identifier(
358 &ca_spki_der,
359 synta_certificate::KeyIdMethod::Rfc5280Sha1,
360 &hasher,
361 ) {
362 builder = builder.add_extension_oid(
363 synta_certificate::oids::AUTHORITY_KEY_IDENTIFIER,
364 false,
365 &aki_der,
366 );
367 }
368 }
369
370 if profile.must_staple {
372 debug!(
373 oid = OID_TLS_FEATURE,
374 "including TLS Feature Extension (must-staple) per RFC 7633 §4"
375 );
376 builder = builder.add_extension_oid(
377 OID_TLS_FEATURE_COMPONENTS,
378 false,
379 TLS_FEATURE_MUST_STAPLE_DER,
380 );
381 }
382
383 let cert_der = match &signing_key {
385 CaSigningKey::Pem(_) => {
386 use synta_certificate::PrivateKey as _;
388 let ca_pkey = pem_key.as_ref().expect("PEM key loaded in step 7");
389 let signer = ca_pkey.as_signer(hash_algorithm);
390 builder.sign(&signer).map_err(|e| {
391 IssuanceError::SigningError(format!("certificate signing failed: {e}"))
392 })?
393 }
394 CaSigningKey::Hsm { context, key_label } => {
395 let hsm_signer = HsmCertificateSigner {
397 context,
398 key_label,
399 hash_algorithm,
400 };
401 builder.sign(&hsm_signer).map_err(|e| {
402 IssuanceError::SigningError(format!("HSM certificate signing failed: {e}"))
403 })?
404 }
405 };
406
407 let subject_dn = synta_certificate::format_dn(&csr_subject_der);
409
410 info!(
411 serial = %serial_hex,
412 profile = %profile.name,
413 subject = %subject_dn,
414 not_after = %not_after_chrono,
415 cert_len = cert_der.len(),
416 "certificate issued"
417 );
418
419 Ok(IssuanceResult {
420 certificate_der: cert_der,
421 serial_number: serial_hex,
422 subject_dn,
423 not_before: now,
424 not_after: not_after_chrono,
425 })
426}
427
428fn generate_serial_bytes() -> Vec<u8> {
434 use rand::Rng;
435 let mut rng = rand::thread_rng();
436 let mut bytes = vec![0u8; 20];
437 rng.fill(&mut bytes[..]);
438 bytes[0] &= 0x7F;
440 if bytes[0] == 0 {
442 bytes[0] = 1;
443 }
444 bytes
445}
446
447fn chrono_to_synta_time(dt: DateTime<Utc>) -> Result<synta_certificate::Time, String> {
453 let year = dt.format("%Y").to_string().parse::<u16>().unwrap_or(2024);
454 let month = dt.format("%m").to_string().parse::<u8>().unwrap_or(1);
455 let day = dt.format("%d").to_string().parse::<u8>().unwrap_or(1);
456 let hour = dt.format("%H").to_string().parse::<u8>().unwrap_or(0);
457 let minute = dt.format("%M").to_string().parse::<u8>().unwrap_or(0);
458 let second = dt.format("%S").to_string().parse::<u8>().unwrap_or(0);
459
460 if year < 2050 {
461 let utc_time = synta::UtcTime::new(year, month, day, hour, minute, second)
462 .map_err(|e| format!("UtcTime creation failed: {e}"))?;
463 Ok(synta_certificate::Time::UtcTime(utc_time))
464 } else {
465 let gen_time = synta::GeneralizedTime::new(year, month, day, hour, minute, second, None)
466 .map_err(|e| format!("GeneralizedTime creation failed: {e}"))?;
467 Ok(synta_certificate::Time::GeneralTime(gen_time))
468 }
469}
470
471fn profile_key_usage_bits(profile: &EnrollmentProfile) -> u16 {
473 use synta_certificate::{
474 KEY_USAGE_DATA_ENCIPHERMENT, KEY_USAGE_DIGITAL_SIGNATURE, KEY_USAGE_KEY_AGREEMENT,
475 KEY_USAGE_KEY_ENCIPHERMENT, KEY_USAGE_NON_REPUDIATION,
476 };
477
478 let mut bits: u16 = 0;
479 for ku in &profile.key_usage {
480 match ku.as_str() {
481 "digitalSignature" => bits |= 1 << KEY_USAGE_DIGITAL_SIGNATURE,
482 "nonRepudiation" | "contentCommitment" => bits |= 1 << KEY_USAGE_NON_REPUDIATION,
483 "keyEncipherment" => bits |= 1 << KEY_USAGE_KEY_ENCIPHERMENT,
484 "dataEncipherment" => bits |= 1 << KEY_USAGE_DATA_ENCIPHERMENT,
485 "keyAgreement" => bits |= 1 << KEY_USAGE_KEY_AGREEMENT,
486 other => {
487 warn!(key_usage = %other, "unknown key usage flag in profile; skipping");
488 }
489 }
490 }
491 bits
492}
493
494fn profile_extended_key_usage(profile: &EnrollmentProfile) -> Option<Vec<u8>> {
496 if profile.extended_key_usage.is_empty() {
497 return None;
498 }
499
500 let mut builder = synta_certificate::ExtendedKeyUsageBuilder::new();
501 for eku in &profile.extended_key_usage {
502 builder = match eku.as_str() {
503 "serverAuth" => builder.server_auth(),
504 "clientAuth" => builder.client_auth(),
505 "codeSigning" => builder.code_signing(),
506 "emailProtection" => builder.email_protection(),
507 "timeStamping" => builder.time_stamping(),
508 "OCSPSigning" | "ocspSigning" => builder.ocsp_signing(),
509 other => {
510 warn!(eku = %other, "unknown extended key usage in profile; skipping");
511 builder
512 }
513 };
514 }
515 builder.build().ok()
516}
517
518fn validate_csr(csr_der: &[u8]) -> Result<(), IssuanceError> {
520 if csr_der.is_empty() {
521 return Err(IssuanceError::InvalidCsr("empty CSR".into()));
522 }
523
524 if csr_der[0] != 0x30 {
526 return Err(IssuanceError::InvalidCsr(
527 "does not start with ASN.1 SEQUENCE".into(),
528 ));
529 }
530
531 synta_certificate::csr::CertificationRequest::from_der(csr_der)
533 .map_err(|e| IssuanceError::InvalidCsr(format!("PKCS#10 parse failed: {e}")))?;
534
535 debug!(len = csr_der.len(), "CSR structure validated");
536 Ok(())
537}
538
539fn check_key_size(csr_der: &[u8], profile: &EnrollmentProfile) -> Result<(), IssuanceError> {
541 let csr = synta_certificate::csr::CertificationRequest::from_der(csr_der).map_err(|e| {
543 IssuanceError::InvalidCsr(format!("CSR parse failed in key size check: {e}"))
544 })?;
545
546 let spki = &csr.certification_request_info.subject_pkinfo;
547 let alg_oid = spki.algorithm.algorithm.components();
548 let key_bits = spki.subject_public_key.bit_len();
549
550 let pk_info = synta_certificate::decode_public_key_info(
551 &spki.algorithm.algorithm,
552 spki.algorithm.parameters.as_ref(),
553 spki.subject_public_key.as_bytes(),
554 key_bits,
555 );
556
557 match &pk_info {
558 synta_certificate::PublicKeyInfo::Rsa { bit_count, .. } => {
559 debug!(
560 algorithm = "RSA",
561 key_bits = bit_count,
562 "CSR public key info"
563 );
564 if (*bit_count as u32) < profile.min_rsa_bits {
565 return Err(IssuanceError::KeyTooSmall {
566 algorithm: "RSA".into(),
567 bits: *bit_count as u32,
568 min_bits: profile.min_rsa_bits,
569 });
570 }
571 }
572 synta_certificate::PublicKeyInfo::Ec {
573 bit_count,
574 curve_nist_name,
575 ..
576 } => {
577 let curve_name = curve_nist_name.unwrap_or("unknown");
578 debug!(
579 algorithm = "EC",
580 curve = curve_name,
581 key_bits = bit_count,
582 "CSR public key info"
583 );
584 let min_bits: usize = match profile.min_ecdsa_curve.as_str() {
585 "P-256" => 256,
586 "P-384" => 384,
587 "P-521" => 521,
588 _ => 256,
589 };
590 if *bit_count < min_bits {
591 return Err(IssuanceError::KeyTooSmall {
592 algorithm: format!("EC {curve_name}"),
593 bits: *bit_count as u32,
594 min_bits: min_bits as u32,
595 });
596 }
597 }
598 synta_certificate::PublicKeyInfo::Unknown {
599 alg_name,
600 bit_count,
601 ..
602 } => {
603 debug!(
604 algorithm = %alg_name,
605 key_bits = bit_count,
606 alg_oid = ?alg_oid,
607 "CSR public key: unknown algorithm (skipping size check)"
608 );
609 }
610 }
611 Ok(())
612}
613
614fn check_validity_period(profile: &EnrollmentProfile) -> Result<(), IssuanceError> {
616 const CAB_CURRENT_MAX_DAYS: u32 = 398;
618
619 if profile.max_validity_days > CAB_CURRENT_MAX_DAYS {
620 warn!(
621 requested = profile.max_validity_days,
622 max = CAB_CURRENT_MAX_DAYS,
623 "validity period exceeds CA/B Forum maximum"
624 );
625 return Err(IssuanceError::ValidityTooLong {
626 requested_days: profile.max_validity_days,
627 max_days: CAB_CURRENT_MAX_DAYS,
628 });
629 }
630
631 Ok(())
632}
633
634struct HsmCertificateSigner<'a> {
641 context: &'a Arc<kipuka_hsm::HsmContext>,
642 key_label: &'a str,
643 hash_algorithm: &'a str,
644}
645
646#[derive(Debug)]
648struct HsmSignerError(String);
649
650impl std::fmt::Display for HsmSignerError {
651 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
652 write!(f, "{}", self.0)
653 }
654}
655
656impl std::error::Error for HsmSignerError {}
657
658impl<'a> synta_certificate::CertificateSigner for HsmCertificateSigner<'a> {
659 type Error = HsmSignerError;
660
661 fn signature_algorithm_der(&self) -> Result<Vec<u8>, Self::Error> {
662 match self.hash_algorithm {
672 "sha256" => {
673 Ok(vec![
675 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
678 0x0b, 0x05, 0x00, ])
681 }
682 "sha384" => {
683 Ok(vec![
685 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0c,
686 0x05, 0x00,
687 ])
688 }
689 "sha512" => {
690 Ok(vec![
692 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0d,
693 0x05, 0x00,
694 ])
695 }
696 other => Err(HsmSignerError(format!(
697 "unsupported hash algorithm for HSM RSA signing: {other}"
698 ))),
699 }
700 }
701
702 fn sign_tbs(&self, tbs_der: &[u8]) -> Result<Vec<u8>, Self::Error> {
703 self.context
706 .sign_data(self.key_label, tbs_der, self.hash_algorithm)
707 .map_err(|e| HsmSignerError(format!("PKCS#11 sign failed: {e}")))
708 }
709}
710
711fn check_required_extensions(profile: &EnrollmentProfile) -> Result<(), IssuanceError> {
713 if !profile.include_aki {
714 return Err(IssuanceError::MissingExtension(
715 "Authority Key Identifier (required by CA/B Forum)".into(),
716 ));
717 }
718 if !profile.include_ski {
719 return Err(IssuanceError::MissingExtension(
720 "Subject Key Identifier (required by CA/B Forum)".into(),
721 ));
722 }
723 if profile.key_usage.is_empty() {
724 return Err(IssuanceError::MissingExtension(
725 "Key Usage (required by CA/B Forum)".into(),
726 ));
727 }
728
729 Ok(())
730}