1use std::sync::Arc;
28
29use axum::body::Bytes;
30use axum::extract::State;
31use axum::http::{HeaderValue, StatusCode, header};
32use axum::response::{IntoResponse, Response};
33
34use synta::{Integer, Null, OctetStringRef, RawDer};
35use synta_certificate::GeneralNameSpec;
36use synta_certificate::cmp_types::{
37 CertOrEncCert, CertRepMessage, CertResponse, CertifiedKeyPair, PBMParameter, PKIBody,
38 PKIHeader, PKIMessage, PKIStatusInfo, RevRepContent,
39};
40use synta_certificate::crmf_types::CertReqMsg;
41
42use crate::error::KipukaError;
43use crate::state::AppState;
44
45const CONTENT_TYPE_CMP: &str = "application/pkixcmp";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum CmpMessageType {
55 Ir,
58 Ip,
60 Cr,
63 Cp,
65 Kur,
67 Kup,
69 Rr,
71 Rp,
73 GenM,
75 GenP,
77 Error,
79 CertConf,
81 PkiConf,
83}
84
85impl CmpMessageType {
86 pub fn from_tag(tag: u8) -> Option<Self> {
109 match tag {
110 0 => Some(Self::Ir),
111 1 => Some(Self::Ip),
112 2 => Some(Self::Cr),
113 3 => Some(Self::Cp),
114 7 => Some(Self::Kur),
115 8 => Some(Self::Kup),
116 11 => Some(Self::Rr),
117 12 => Some(Self::Rp),
118 21 => Some(Self::GenM),
119 22 => Some(Self::GenP),
120 23 => Some(Self::Error),
121 24 => Some(Self::CertConf),
122 25 => Some(Self::PkiConf),
123 _ => None,
124 }
125 }
126
127 pub fn is_request(&self) -> bool {
129 matches!(
130 self,
131 Self::Ir | Self::Cr | Self::Kur | Self::Rr | Self::GenM | Self::CertConf
132 )
133 }
134
135 pub fn expected_response(&self) -> Option<Self> {
141 match self {
142 Self::Ir => Some(Self::Ip),
143 Self::Cr => Some(Self::Cp),
144 Self::Kur => Some(Self::Kup),
145 Self::Rr => Some(Self::Rp),
146 Self::GenM => Some(Self::GenP),
147 Self::CertConf => Some(Self::PkiConf),
148 _ => None,
149 }
150 }
151}
152
153impl std::fmt::Display for CmpMessageType {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 let name = match self {
156 Self::Ir => "ir",
157 Self::Ip => "ip",
158 Self::Cr => "cr",
159 Self::Cp => "cp",
160 Self::Kur => "kur",
161 Self::Kup => "kup",
162 Self::Rr => "rr",
163 Self::Rp => "rp",
164 Self::GenM => "genm",
165 Self::GenP => "genp",
166 Self::Error => "error",
167 Self::CertConf => "certConf",
168 Self::PkiConf => "pkiConf",
169 };
170 f.write_str(name)
171 }
172}
173
174#[derive(Debug, Clone)]
183pub enum CmpProtectionType {
184 Signature {
189 algorithm: String,
191 cert_der: Vec<u8>,
193 },
194 Mac {
200 algorithm: String,
202 },
203}
204
205#[derive(Debug, Clone)]
210pub struct CmpRequest {
211 pub message_type: CmpMessageType,
213
214 pub transaction_id: Vec<u8>,
219
220 pub sender_nonce: Vec<u8>,
225
226 pub sender: String,
231
232 pub protection: CmpProtectionType,
234
235 pub body_der: Vec<u8>,
237}
238
239#[derive(Debug, Clone)]
244pub struct CmpResponse {
245 pub message_type: CmpMessageType,
247
248 pub transaction_id: Vec<u8>,
250
251 pub recip_nonce: Vec<u8>,
253
254 pub sender_nonce: Vec<u8>,
256
257 pub sender: String,
259
260 pub body_der: Vec<u8>,
262}
263
264pub fn parse_cmp_message(der: &[u8]) -> Result<CmpRequest, KipukaError> {
286 if der.is_empty() {
287 return Err(KipukaError::BadRequest("empty CMP message".into()));
288 }
289
290 if der.len() < 50 {
292 return Err(KipukaError::BadRequest(
293 "CMP message is too short to be a valid PKIMessage".into(),
294 ));
295 }
296
297 if der[0] != 0x30 {
299 return Err(KipukaError::BadRequest(
300 "CMP message does not start with a SEQUENCE tag".into(),
301 ));
302 }
303
304 let pki_msg = PKIMessage::from_der(der)
307 .map_err(|e| KipukaError::BadRequest(format!("failed to parse CMP PKIMessage: {e}")))?;
308
309 let transaction_id = pki_msg
311 .header
312 .transaction_id
313 .map(|o| o.as_bytes().to_vec())
314 .unwrap_or_default();
315
316 let sender_nonce = pki_msg
317 .header
318 .sender_nonce
319 .map(|o| o.as_bytes().to_vec())
320 .unwrap_or_default();
321
322 let sender = format_general_name(&pki_msg.header.sender);
324
325 let (message_type, body_der) = match &pki_msg.body {
327 PKIBody::Ir(raw) => (CmpMessageType::Ir, raw.0.to_vec()),
328 PKIBody::Ip(raw) => (CmpMessageType::Ip, raw.0.to_vec()),
329 PKIBody::Cr(raw) => (CmpMessageType::Cr, raw.0.to_vec()),
330 PKIBody::Cp(raw) => (CmpMessageType::Cp, raw.0.to_vec()),
331 PKIBody::P10cr(raw) => (CmpMessageType::Cr, raw.0.to_vec()),
332 PKIBody::Kur(raw) => (CmpMessageType::Kur, raw.0.to_vec()),
333 PKIBody::Kup(raw) => (CmpMessageType::Kup, raw.0.to_vec()),
334 PKIBody::Rr(raw) => (CmpMessageType::Rr, raw.0.to_vec()),
335 PKIBody::Rp(raw) => (CmpMessageType::Rp, raw.0.to_vec()),
336 PKIBody::Genm(raw) => (CmpMessageType::GenM, raw.0.to_vec()),
337 PKIBody::Genp(raw) => (CmpMessageType::GenP, raw.0.to_vec()),
338 PKIBody::Error(raw) => (CmpMessageType::Error, raw.0.to_vec()),
339 PKIBody::CertConf(raw) => (CmpMessageType::CertConf, raw.0.to_vec()),
340 PKIBody::Pkiconf(_) => (CmpMessageType::PkiConf, Vec::new()),
341 _ => {
342 return Err(KipukaError::BadRequest(
343 "unsupported CMP PKIBody variant".into(),
344 ));
345 }
346 };
347
348 let protection = if let Some(prot_bits) = &pki_msg.protection {
350 if let Some(ref alg_id) = pki_msg.header.protection_alg {
352 let alg_oid_str = alg_id.algorithm.to_string();
353 if alg_oid_str.contains("1.2.840.113533.7.66.13")
356 || alg_oid_str.contains("1.2.840.113533.7.66.30")
357 || alg_oid_str.contains("1.3.6.1.5.5.7.15.10")
358 {
359 CmpProtectionType::Mac {
360 algorithm: alg_oid_str,
361 }
362 } else {
363 let cert_der = pki_msg
366 .extra_certs
367 .as_ref()
368 .and_then(|certs| certs.first())
369 .map(|c| c.0.to_vec())
370 .unwrap_or_default();
371 CmpProtectionType::Signature {
372 algorithm: alg_oid_str,
373 cert_der,
374 }
375 }
376 } else {
377 let _ = prot_bits;
380 return Err(KipukaError::BadRequest(
381 "CMP message has protection bits but no protectionAlg in header".into(),
382 ));
383 }
384 } else {
385 return Err(KipukaError::Auth("CMP message has no protection".into()));
387 };
388
389 Ok(CmpRequest {
390 message_type,
391 transaction_id,
392 sender_nonce,
393 sender,
394 protection,
395 body_der,
396 })
397}
398
399pub fn build_cmp_response(
412 req: &CmpRequest,
413 response_type: CmpMessageType,
414 body: &[u8],
415 ca_subject_der: Option<&[u8]>,
416) -> Result<Vec<u8>, KipukaError> {
417 if req.transaction_id.is_empty() {
418 return Err(KipukaError::BadRequest(
419 "cannot build CMP response: empty transaction ID".into(),
420 ));
421 }
422
423 if body.is_empty() {
424 return Err(KipukaError::BadRequest(
425 "cannot build CMP response: empty body".into(),
426 ));
427 }
428
429 if let Some(expected) = req.message_type.expected_response()
431 && expected != response_type
432 {
433 tracing::warn!(
434 request_type = %req.message_type,
435 response_type = %response_type,
436 expected = %expected,
437 "CMP response type mismatch"
438 );
439 }
440
441 let server_nonce = generate_nonce();
443
444 let pki_body = match response_type {
446 CmpMessageType::Ip => PKIBody::Ip(RawDer(body)),
447 CmpMessageType::Cp => PKIBody::Cp(RawDer(body)),
448 CmpMessageType::Kup => PKIBody::Kup(RawDer(body)),
449 CmpMessageType::Rp => PKIBody::Rp(RawDer(body)),
450 CmpMessageType::GenP => PKIBody::Genp(RawDer(body)),
451 CmpMessageType::Error => PKIBody::Error(RawDer(body)),
452 CmpMessageType::PkiConf => PKIBody::Pkiconf(Null),
453 _ => {
454 return Err(KipukaError::Internal(format!(
455 "cannot build CMP response for body type: {response_type}"
456 )));
457 }
458 };
459
460 let sender_spec = if let Some(der) = ca_subject_der {
468 GeneralNameSpec::directory_name(der)
469 } else {
470 GeneralNameSpec::rfc822("ca@kipuka.dev")
471 };
472 let recipient_spec = if req.sender.is_empty() {
474 GeneralNameSpec::rfc822("unknown@kipuka.dev")
475 } else {
476 GeneralNameSpec::rfc822(&req.sender)
477 };
478
479 let sender_gn = sender_spec
480 .to_general_name()
481 .map_err(|e| KipukaError::Internal(format!("failed to encode CA sender name: {e}")))?;
482 let recipient_gn = recipient_spec
483 .to_general_name()
484 .map_err(|e| KipukaError::Internal(format!("failed to encode recipient name: {e}")))?;
485
486 let header = PKIHeader {
487 pvno: Integer::from_i64(2),
488 sender: sender_gn,
489 recipient: recipient_gn,
490 message_time: None,
491 protection_alg: None,
492 sender_kid: None,
493 recip_kid: None,
494 transaction_id: Some(OctetStringRef::new(&req.transaction_id)),
495 sender_nonce: Some(OctetStringRef::new(&server_nonce)),
496 recip_nonce: Some(OctetStringRef::new(&req.sender_nonce)),
497 free_text: None,
498 general_info: None,
499 };
500
501 let pki_msg = PKIMessage {
504 header,
505 body: pki_body,
506 protection: None,
507 extra_certs: None,
508 };
509
510 pki_msg
511 .to_der()
512 .map_err(|e| KipukaError::Internal(format!("failed to DER-encode CMP response: {e}")))
513}
514
515fn protected_part(message: &PKIMessage<'_>) -> Result<Vec<u8>, KipukaError> {
516 let encode = || -> Result<Vec<u8>, synta::Error> {
517 let mut encoder = synta::Encoder::new(synta::Encoding::Der);
518 encoder.start_constructed_no_guard(synta::Tag::universal_constructed(16))?;
519 encoder.encode(&message.header)?;
520 encoder.encode(&message.body)?;
521 encoder.end_constructed()?;
522 encoder.finish()
523 };
524 encode().map_err(|e| KipukaError::BadRequest(format!("CMP ProtectedPart: {e}")))
525}
526
527async fn validate_cmp_signer(cert_der: &[u8], state: &Arc<AppState>) -> Result<(), KipukaError> {
528 use crate::auth::certificate::{issued_by, valid_now};
529 valid_now(cert_der).map_err(KipukaError::Auth)?;
530 let cert = synta_certificate::Certificate::from_der(cert_der)
531 .map_err(|e| KipukaError::Auth(e.to_string()))?;
532 if let Some(extensions) = &cert.tbs_certificate.extensions
533 && let Some(usage) = synta_certificate::find_extension_value(
534 extensions.as_bytes(),
535 synta_certificate::oids::KEY_USAGE,
536 )
537 {
538 let bits: synta::BitStringRef<'_> = synta::Decoder::new(usage, synta::Encoding::Der)
539 .decode()
540 .map_err(|e| KipukaError::Auth(format!("invalid CMP signer key usage: {e}")))?;
541 if bits.as_bytes().first().is_none_or(|byte| byte & 0x80 == 0) {
542 return Err(KipukaError::Auth(
543 "CMP signer key usage forbids digital signatures".into(),
544 ));
545 }
546 }
547 let trusted = state.cas.values().any(|ca| {
548 valid_now(&ca.cert_der).is_ok()
549 && (ca.cert_der == cert_der || issued_by(cert_der, &ca.cert_der))
550 });
551 if !trusted {
552 return Err(KipukaError::Auth(
553 "CMP signer does not chain to a valid configured CA".into(),
554 ));
555 }
556 let revoked: Option<(String,)> = sqlx::query_as(crate::db::pg_sql(
557 "SELECT status FROM certificates WHERE der_encoded = ? AND status = 'revoked'",
558 ))
559 .bind(cert_der)
560 .fetch_optional(&state.db)
561 .await
562 .map_err(|e| KipukaError::Db(format!("CMP signer revocation lookup failed: {e}")))?;
563 if revoked.is_some() {
564 return Err(KipukaError::Auth(
565 "CMP signer certificate is revoked".into(),
566 ));
567 }
568 crate::auth::mtls::check_revocation(cert_der, state)
569 .await
570 .map_err(KipukaError::Auth)
571}
572
573pub async fn post_cmp(
599 State(state): State<Arc<AppState>>,
600 body: Bytes,
601) -> Result<Response, KipukaError> {
602 let cmp_config = match state.config.cmp {
604 Some(ref cfg) if cfg.enabled => cfg,
605 _ => return Err(KipukaError::Est("CMP is not enabled".into())),
606 };
607
608 tracing::info!("CMP request received ({} bytes)", body.len());
609
610 if body.is_empty() {
611 return Err(KipukaError::BadRequest("empty CMP message".into()));
612 }
613
614 let cmp_req = parse_cmp_message(&body)?;
616
617 tracing::info!(
618 message_type = %cmp_req.message_type,
619 sender = %cmp_req.sender,
620 transaction_id_len = cmp_req.transaction_id.len(),
621 "CMP message parsed"
622 );
623
624 if !cmp_req.message_type.is_request() {
626 return Err(KipukaError::BadRequest(format!(
627 "unexpected CMP message type '{}' — only request types are accepted",
628 cmp_req.message_type,
629 )));
630 }
631
632 let pki_msg_for_verify = PKIMessage::from_der(&body)
634 .map_err(|e| KipukaError::BadRequest(format!("invalid CMP message: {e}")))?;
635 let protected_bytes = protected_part(&pki_msg_for_verify)?;
636
637 let protection_bits = pki_msg_for_verify
639 .protection
640 .as_ref()
641 .map(|bits| bits.as_bytes().to_vec())
642 .unwrap_or_default();
643
644 let protection_alg_der = pki_msg_for_verify
646 .header
647 .protection_alg
648 .as_ref()
649 .and_then(|alg| synta::ToDer::to_der(alg).ok())
650 .unwrap_or_default();
651
652 match &cmp_req.protection {
653 CmpProtectionType::Signature {
654 algorithm,
655 cert_der,
656 } => {
657 tracing::debug!(
658 algorithm = %algorithm,
659 cert_len = cert_der.len(),
660 "verifying signature-based CMP protection"
661 );
662
663 if cert_der.is_empty() {
664 return Err(KipukaError::Auth(
665 "CMP signature protection: no signer certificate in extraCerts".into(),
666 ));
667 }
668
669 let signer_ranges = synta_certificate::cert_byte_ranges(cert_der).ok_or_else(|| {
671 KipukaError::Auth(
672 "failed to parse signer certificate structure from extraCerts".into(),
673 )
674 })?;
675 let signer_spki_der = &cert_der[signer_ranges.subject_public_key_info.clone()];
676
677 let pub_key =
681 synta_certificate::BackendPublicKey::from_spki_der(signer_spki_der.to_vec());
682 pub_key
683 .verify_signature(&protected_bytes, &protection_alg_der, &protection_bits)
684 .map_err(|e| {
685 KipukaError::Auth(format!("CMP signature verification failed: {e}"))
686 })?;
687
688 tracing::info!("CMP signature protection verified successfully");
689
690 validate_cmp_signer(cert_der, &state).await?;
691
692 tracing::info!("CMP signer certificate chain verified against CA truststore");
693 }
694 CmpProtectionType::Mac { algorithm } => {
695 if !cmp_config.allow_mac_protection {
696 return Err(KipukaError::Auth(
697 "MAC-based CMP protection is not allowed by policy".into(),
698 ));
699 }
700
701 tracing::debug!(
702 algorithm = %algorithm,
703 sender = %cmp_req.sender,
704 "verifying MAC-based CMP protection"
705 );
706
707 let pbm_param_element = pki_msg_for_verify
712 .header
713 .protection_alg
714 .as_ref()
715 .and_then(|alg| alg.parameters.as_ref())
716 .ok_or_else(|| {
717 KipukaError::Auth(
718 "CMP MAC protection: protectionAlg has no PBMParameter".into(),
719 )
720 })?;
721
722 let pbm_param_bytes = synta::ToDer::to_der(pbm_param_element).map_err(|e| {
725 KipukaError::Auth(format!(
726 "CMP MAC protection: failed to encode PBMParameter element: {e}"
727 ))
728 })?;
729
730 let pbm = PBMParameter::from_der(&pbm_param_bytes).map_err(|e| {
731 KipukaError::Auth(format!(
732 "CMP MAC protection: failed to parse PBMParameter: {e}"
733 ))
734 })?;
735
736 let secret_entry = cmp_config
738 .mac_secrets
739 .iter()
740 .find(|s| s.reference == cmp_req.sender)
741 .ok_or_else(|| {
742 tracing::warn!(
743 sender = %cmp_req.sender,
744 "CMP MAC protection: no shared secret configured for sender"
745 );
746 KipukaError::Auth(format!(
747 "CMP MAC protection: no shared secret found for reference '{}'",
748 cmp_req.sender,
749 ))
750 })?;
751
752 let resolved_hex = state
753 .secrets
754 .cmp_mac_secrets
755 .get(&secret_entry.reference)
756 .ok_or_else(|| {
757 KipukaError::Internal(format!(
758 "CMP MAC secret for '{}' was not resolved at startup",
759 secret_entry.reference,
760 ))
761 })?;
762 let shared_secret = hex::decode(resolved_hex).map_err(|e| {
763 KipukaError::Internal(format!(
764 "CMP MAC secret for '{}' has invalid hex encoding: {e}",
765 secret_entry.reference,
766 ))
767 })?;
768
769 let derived_key = derive_pbm_key(
775 &shared_secret,
776 pbm.salt.as_bytes(),
777 &pbm.owf,
778 pbm.iteration_count.as_i64().unwrap_or(1) as u32,
779 )?;
780
781 let computed_mac = compute_pbm_hmac(&derived_key, &protected_bytes, &pbm.mac)?;
784
785 use subtle::ConstantTimeEq;
787 if computed_mac.ct_eq(&protection_bits).into() {
788 tracing::info!("CMP MAC protection verified successfully");
789 } else {
790 return Err(KipukaError::Auth(
791 "CMP MAC protection verification failed: MAC mismatch".into(),
792 ));
793 }
794 }
795 }
796
797 let response_type = cmp_req.message_type.expected_response().ok_or_else(|| {
799 KipukaError::BadRequest(format!(
800 "CMP message type '{}' has no defined response",
801 cmp_req.message_type,
802 ))
803 })?;
804
805 state.admit_enrollment(&cmp_req.sender, "cmp").await?;
806
807 let response_body_der = match cmp_req.message_type {
809 CmpMessageType::Ir => {
810 if !cmp_config.allow_ir {
811 return Err(KipukaError::Est(
812 "CMP initialization requests (ir) are not allowed".into(),
813 ));
814 }
815 tracing::info!("CMP: processing initialization request (ir)");
816 process_enrollment_request(&state, &cmp_req, "ir").await?
817 }
818 CmpMessageType::Cr => {
819 if !cmp_config.allow_cr {
820 return Err(KipukaError::Est(
821 "CMP certification requests (cr) are not allowed".into(),
822 ));
823 }
824 tracing::info!("CMP: processing certification request (cr)");
825 process_enrollment_request(&state, &cmp_req, "cr").await?
826 }
827 CmpMessageType::Kur => {
828 if !cmp_config.allow_kur {
829 return Err(KipukaError::Est(
830 "CMP key update requests (kur) are not allowed".into(),
831 ));
832 }
833 tracing::info!("CMP: processing key update request (kur)");
834 process_enrollment_request(&state, &cmp_req, "kur").await?
838 }
839 CmpMessageType::Rr => {
840 if !cmp_config.allow_rr {
841 return Err(KipukaError::Est(
842 "CMP revocation requests (rr) are not allowed".into(),
843 ));
844 }
845 tracing::info!("CMP: processing revocation request (rr)");
846 process_revocation_request(&state, &cmp_req).await?
847 }
848 CmpMessageType::GenM => {
849 tracing::info!("CMP: processing general message (genm)");
850 process_general_message(&state, &cmp_req)?
851 }
852 CmpMessageType::CertConf => {
853 tracing::info!("CMP: processing certificate confirmation (certConf)");
854 synta::ToDer::to_der(&Null)
860 .map_err(|e| KipukaError::Internal(format!("failed to encode PKIConfirm: {e}")))?
861 }
862 _ => {
863 return Err(KipukaError::BadRequest(format!(
864 "unsupported CMP message type: {}",
865 cmp_req.message_type,
866 )));
867 }
868 };
869
870 let ca_subject_der = state
872 .config
873 .cas
874 .first()
875 .and_then(|ca_cfg| state.get_ca(&ca_cfg.id))
876 .and_then(|ca| {
877 synta_certificate::Certificate::from_der(&ca.cert_der)
878 .ok()
879 .map(|cert| cert.tbs_certificate.subject.as_bytes().to_vec())
880 });
881
882 let response_der = build_cmp_response(
884 &cmp_req,
885 response_type,
886 &response_body_der,
887 ca_subject_der.as_deref(),
888 )?;
889
890 let request_message =
892 PKIMessage::from_der(&body).map_err(|e| KipukaError::BadRequest(e.to_string()))?;
893 let mut response_message =
894 PKIMessage::from_der(&response_der).map_err(|e| KipukaError::Internal(e.to_string()))?;
895 response_message.header.recipient = request_message.header.sender;
896 let response_der = response_message
897 .to_der()
898 .map_err(|e| KipukaError::Internal(e.to_string()))?;
899
900 let ca_cfg = state.config.cas.first().ok_or(KipukaError::NotFound)?;
901 let ca = state.get_ca(&ca_cfg.id).ok_or(KipukaError::NotFound)?;
902 let key = crate::ca::issue::resolve_signing_key(ca_cfg, state.hsm.as_ref()).await?;
903 let response_der = crate::ca::protocol::protect_cmp(
904 &response_der,
905 &ca.cert_der,
906 key.as_signing_key(),
907 &ca.hash_algorithm,
908 )?;
909
910 state
911 .record_audit_event(
912 "cmp_success",
913 &format!("type={}, sender={}", cmp_req.message_type, cmp_req.sender),
914 )
915 .await;
916
917 let mut resp = (StatusCode::OK, response_der).into_response();
919 resp.headers_mut().insert(
920 header::CONTENT_TYPE,
921 HeaderValue::from_static(CONTENT_TYPE_CMP),
922 );
923 Ok(resp)
924}
925
926fn generate_nonce() -> Vec<u8> {
930 use rand::Rng;
931 let mut nonce = vec![0u8; 16];
932 rand::rng().fill_bytes(&mut nonce);
933 nonce
934}
935
936fn format_general_name(gn: &synta_certificate::GeneralName<'_>) -> String {
938 use synta_certificate::GeneralName;
939 match gn {
940 GeneralName::DirectoryName(name) => {
941 synta::ToDer::to_der(name)
944 .map(|der| synta_certificate::format_dn(&der))
945 .unwrap_or_else(|_| "<invalid DN>".to_string())
946 }
947 GeneralName::Rfc822Name(s) => s.as_str().to_string(),
948 GeneralName::DNSName(s) => s.as_str().to_string(),
949 GeneralName::UniformResourceIdentifier(s) => s.as_str().to_string(),
950 _ => "<other GeneralName>".to_string(),
951 }
952}
953
954async fn process_enrollment_request(
961 state: &Arc<AppState>,
962 cmp_req: &CmpRequest,
963 req_type: &str,
964) -> Result<Vec<u8>, KipukaError> {
965 let cert_req_msgs: Vec<CertReqMsg<'_>> =
967 synta::Decoder::new(&cmp_req.body_der, synta::Encoding::Der)
968 .decode()
969 .map_err(|e| {
970 KipukaError::BadRequest(format!(
971 "failed to parse CMP {req_type} CertReqMessages: {e}"
972 ))
973 })?;
974
975 if cert_req_msgs.len() != 1 {
976 return Err(KipukaError::BadRequest(
977 "CMP endpoint requires exactly one request per message".into(),
978 ));
979 }
980
981 let cert_req_msg = &cert_req_msgs[0];
983 let cert_req_id = cert_req_msg.cert_req.cert_req_id.clone();
984 let cert_template = &cert_req_msg.cert_req.cert_template;
985
986 let subject_der = cert_template
988 .subject
989 .as_ref()
990 .ok_or_else(|| KipukaError::BadRequest("CRMF CertTemplate missing subject name".into()))?
991 .to_der()
992 .map_err(|e| KipukaError::BadRequest(format!("failed to encode CRMF subject: {e}")))?;
993
994 let spki_der = cert_template
996 .public_key
997 .as_ref()
998 .ok_or_else(|| KipukaError::BadRequest("CRMF CertTemplate missing public key".into()))?
999 .to_der()
1000 .map_err(|e| KipukaError::BadRequest(format!("failed to encode CRMF SPKI: {e}")))?;
1001
1002 tracing::debug!(
1003 subject = %synta_certificate::format_dn(&subject_der),
1004 spki_len = spki_der.len(),
1005 "CMP {}: extracted certificate request template", req_type,
1006 );
1007
1008 let ca_id = state
1010 .config
1011 .cas
1012 .first()
1013 .map(|c| c.id.as_str())
1014 .unwrap_or("default");
1015
1016 let ca = state
1017 .get_ca(ca_id)
1018 .ok_or_else(|| KipukaError::Ca(format!("CA '{ca_id}' not found")))?;
1019
1020 let ca_cfg = state
1021 .config
1022 .cas
1023 .iter()
1024 .find(|c| c.id == ca_id)
1025 .ok_or_else(|| KipukaError::Ca(format!("CA config not found for id={ca_id}")))?;
1026
1027 let resolved_key = crate::ca::issue::resolve_signing_key(ca_cfg, state.hsm.as_ref()).await?;
1029
1030 let profile = crate::ca::issue::EnrollmentProfile {
1031 max_validity_days: ca
1032 .validity_days
1033 .min(crate::ca::issue::cab_forum_max_validity_days()),
1034 ..crate::ca::issue::EnrollmentProfile::default()
1035 };
1036
1037 let result = crate::ca::issue::issue_crmf_certificate(
1039 cert_req_msg,
1040 &profile,
1041 &ca.cert_der,
1042 resolved_key.as_signing_key(),
1043 &ca.hash_algorithm,
1044 ca.ocsp_url.as_deref(),
1045 ca.crl_url.as_deref(),
1046 )
1047 .map_err(|e| KipukaError::Ca(format!("CMP certificate issuance failed: {e}")))?;
1048
1049 tracing::info!(
1050 serial = %result.serial_number,
1051 subject = %result.subject_dn,
1052 "CMP {}: certificate issued successfully", req_type,
1053 );
1054
1055 crate::ca::issue::persist_certificate(state, ca_id, &profile.name, &result).await?;
1056
1057 let status_info = PKIStatusInfo {
1061 status: Integer::from_i64(0), status_string: None,
1063 fail_info: None,
1064 };
1065
1066 let cert_response = CertResponse {
1067 cert_req_id,
1068 status: status_info,
1069 certified_key_pair: Some(CertifiedKeyPair {
1070 cert_or_enc_cert: CertOrEncCert::Certificate(RawDer(&result.certificate_der)),
1071 private_key: None,
1072 publication_info: None,
1073 }),
1074 rsp_info: None,
1075 };
1076
1077 let cert_rep = CertRepMessage {
1078 ca_pubs: None,
1079 response: vec![cert_response],
1080 };
1081
1082 cert_rep
1083 .to_der()
1084 .map_err(|e| KipukaError::Internal(format!("failed to encode CMP CertRepMessage: {e}")))
1085}
1086
1087async fn process_revocation_request(
1093 state: &Arc<AppState>,
1094 cmp_req: &CmpRequest,
1095) -> Result<Vec<u8>, KipukaError> {
1096 if !matches!(cmp_req.protection, CmpProtectionType::Signature { .. }) {
1098 return Err(KipukaError::Forbidden(
1099 "CMP revocation requires certificate signature protection".into(),
1100 ));
1101 }
1102 use synta_certificate::cmp_types::RevDetails;
1103 use synta_certificate::crmf_types::CertTemplate;
1104
1105 let rev_details: Vec<RevDetails<'_>> =
1107 synta::Decoder::new(&cmp_req.body_der, synta::Encoding::Der)
1108 .decode()
1109 .map_err(|e| {
1110 KipukaError::BadRequest(format!("failed to parse CMP RevReqContent: {e}"))
1111 })?;
1112
1113 if rev_details.is_empty() {
1114 return Err(KipukaError::BadRequest(
1115 "CMP RevReqContent contains no revocation requests".into(),
1116 ));
1117 }
1118
1119 if let CmpProtectionType::Signature { cert_der, .. } = &cmp_req.protection {
1132 let signer_cert = synta_certificate::Certificate::from_der(cert_der).map_err(|e| {
1133 KipukaError::Auth(format!(
1134 "failed to parse signer certificate for revocation authorization: {e:?}"
1135 ))
1136 })?;
1137 let signer_subject_der = signer_cert.tbs_certificate.subject.as_bytes();
1138 let signer_subject_dn = synta_certificate::format_dn(signer_subject_der);
1139
1140 const CMC_RA_OID: &str = "1.3.6.1.5.5.7.3.28";
1142 let signer_is_ra = signer_cert
1143 .tbs_certificate
1144 .extensions
1145 .as_ref()
1146 .and_then(|ext_raw| {
1147 synta_certificate::find_extension_value(
1148 ext_raw.as_bytes(),
1149 synta_certificate::oids::EXTENDED_KEY_USAGE,
1150 )
1151 })
1152 .map(|eku_bytes| {
1153 let mut decoder = synta::Decoder::new(eku_bytes, synta::Encoding::Der);
1154 let oids: Vec<synta::ObjectIdentifier> = decoder.decode().unwrap_or_default();
1155 oids.iter().any(|oid| oid.to_string() == CMC_RA_OID)
1156 })
1157 .unwrap_or(false);
1158
1159 if signer_is_ra {
1160 tracing::info!(
1161 signer = %signer_subject_dn,
1162 "CMP rr: signer has id-kp-cmcRA EKU — RA revocation authorized"
1163 );
1164 } else {
1165 for detail in &rev_details {
1168 let cert_tmpl_check =
1169 CertTemplate::from_der(detail.cert_details.0).map_err(|e| {
1170 KipukaError::BadRequest(format!(
1171 "failed to parse RevDetails CertTemplate for authz: {e}"
1172 ))
1173 })?;
1174
1175 if let Some(serial_check) = &cert_tmpl_check.serial_number {
1176 let serial_hex_check = hex::encode(serial_check.as_bytes());
1177
1178 let row: Option<(String,)> = sqlx::query_as(crate::db::pg_sql(
1180 "SELECT subject_dn FROM certificates WHERE serial = ?",
1181 ))
1182 .bind(&serial_hex_check)
1183 .fetch_optional(&state.db)
1184 .await
1185 .map_err(|e| {
1186 KipukaError::Ca(format!(
1187 "database error checking revocation authorization: {e}"
1188 ))
1189 })?;
1190
1191 if let Some((revokee_subject,)) = row {
1192 if revokee_subject != signer_subject_dn {
1193 tracing::warn!(
1194 signer = %signer_subject_dn,
1195 revokee = %revokee_subject,
1196 serial = %serial_hex_check,
1197 "CMP rr: signer is not the certificate owner \
1198 and does not have RA privilege"
1199 );
1200 return Err(KipukaError::Forbidden(format!(
1201 "CMP revocation denied: signer '{signer_subject_dn}' is not authorized \
1202 to revoke certificate serial {serial_hex_check} (owner: '{revokee_subject}')"
1203 )));
1204 }
1205
1206 tracing::debug!(
1207 serial = %serial_hex_check,
1208 subject = %signer_subject_dn,
1209 "CMP rr: self-revocation authorized"
1210 );
1211 }
1212 }
1215 }
1216 }
1217 }
1218 let mut statuses = Vec::with_capacity(rev_details.len());
1223
1224 for detail in &rev_details {
1225 let cert_tmpl = CertTemplate::from_der(detail.cert_details.0).map_err(|e| {
1228 KipukaError::BadRequest(format!("failed to parse RevDetails CertTemplate: {e}"))
1229 })?;
1230
1231 let serial = cert_tmpl.serial_number.ok_or_else(|| {
1232 KipukaError::BadRequest("RevDetails CertTemplate missing serial number".into())
1233 })?;
1234
1235 let serial_hex = hex::encode(serial.as_bytes());
1236
1237 tracing::info!(
1238 serial = %serial_hex,
1239 "CMP rr: revoking certificate"
1240 );
1241
1242 let rows = sqlx::query(crate::db::pg_sql(
1244 "UPDATE certificates SET status = 'revoked' WHERE serial = ?",
1245 ))
1246 .bind(&serial_hex)
1247 .execute(&state.db)
1248 .await
1249 .map_err(|e| KipukaError::Ca(format!("database error revoking serial {serial_hex}: {e}")))?
1250 .rows_affected();
1251
1252 if rows == 0 {
1253 tracing::warn!(
1254 serial = %serial_hex,
1255 "CMP rr: certificate not found in database"
1256 );
1257 }
1258
1259 state
1260 .record_audit_event(
1261 "cmp_revocation",
1262 &format!("serial={serial_hex}, sender={}", cmp_req.sender),
1263 )
1264 .await;
1265
1266 statuses.push(PKIStatusInfo {
1268 status: Integer::from_i64(0), status_string: None,
1270 fail_info: None,
1271 });
1272 }
1273
1274 let rev_rep = RevRepContent {
1275 status: statuses,
1276 rev_certs: None,
1277 crls: None,
1278 };
1279
1280 rev_rep
1281 .to_der()
1282 .map_err(|e| KipukaError::Internal(format!("failed to encode CMP RevRepContent: {e}")))
1283}
1284
1285fn derive_pbm_key(
1296 secret: &[u8],
1297 salt: &[u8],
1298 owf_alg: &synta_certificate::AlgorithmIdentifier<'_>,
1299 iteration_count: u32,
1300) -> Result<Vec<u8>, KipukaError> {
1301 use sha2::{Digest, Sha256, Sha384, Sha512};
1302
1303 let owf_oid = owf_alg.algorithm.to_string();
1304
1305 enum OwfKind {
1315 Sha256,
1316 Sha384,
1317 Sha512,
1318 }
1319
1320 let owf_kind = if owf_oid.contains("2.16.840.1.101.3.4.2.1") {
1321 OwfKind::Sha256
1322 } else if owf_oid.contains("2.16.840.1.101.3.4.2.2") {
1323 OwfKind::Sha384
1324 } else if owf_oid.contains("2.16.840.1.101.3.4.2.3") {
1325 OwfKind::Sha512
1326 } else {
1327 return Err(KipukaError::Auth(format!(
1328 "CMP PBM: unsupported OWF algorithm OID: {owf_oid}"
1329 )));
1330 };
1331
1332 if iteration_count == 0 {
1333 return Err(KipukaError::Auth(
1334 "CMP PBM: iteration count must be positive".into(),
1335 ));
1336 }
1337
1338 if iteration_count > 100_000 {
1340 return Err(KipukaError::Auth(
1341 "CMP PBM: iteration count exceeds maximum (100000)".into(),
1342 ));
1343 }
1344
1345 let mut input = Vec::with_capacity(secret.len() + salt.len());
1347 input.extend_from_slice(secret);
1348 input.extend_from_slice(salt);
1349
1350 let mut key = match owf_kind {
1351 OwfKind::Sha256 => Sha256::digest(&input).to_vec(),
1352 OwfKind::Sha384 => Sha384::digest(&input).to_vec(),
1353 OwfKind::Sha512 => Sha512::digest(&input).to_vec(),
1354 };
1355
1356 for _ in 1..iteration_count {
1359 key = match owf_kind {
1360 OwfKind::Sha256 => Sha256::digest(&key).to_vec(),
1361 OwfKind::Sha384 => Sha384::digest(&key).to_vec(),
1362 OwfKind::Sha512 => Sha512::digest(&key).to_vec(),
1363 };
1364 }
1365
1366 Ok(key)
1367}
1368
1369fn compute_pbm_hmac(
1375 key: &[u8],
1376 data: &[u8],
1377 mac_alg: &synta_certificate::AlgorithmIdentifier<'_>,
1378) -> Result<Vec<u8>, KipukaError> {
1379 use hmac::{Hmac, KeyInit, Mac};
1380 use sha2::{Sha256, Sha384, Sha512};
1381
1382 let mac_oid = mac_alg.algorithm.to_string();
1383
1384 if mac_oid.contains("1.2.840.113549.2.9") || mac_oid.contains("2.16.840.1.101.3.4.2.1") {
1395 let mut mac = Hmac::<Sha256>::new_from_slice(key)
1396 .map_err(|e| KipukaError::Internal(format!("HMAC-SHA256 key init failed: {e}")))?;
1397 mac.update(data);
1398 Ok(mac.finalize().into_bytes().to_vec())
1399 } else if mac_oid.contains("1.2.840.113549.2.10") || mac_oid.contains("2.16.840.1.101.3.4.2.2")
1400 {
1401 let mut mac = Hmac::<Sha384>::new_from_slice(key)
1402 .map_err(|e| KipukaError::Internal(format!("HMAC-SHA384 key init failed: {e}")))?;
1403 mac.update(data);
1404 Ok(mac.finalize().into_bytes().to_vec())
1405 } else if mac_oid.contains("1.2.840.113549.2.11") || mac_oid.contains("2.16.840.1.101.3.4.2.3")
1406 {
1407 let mut mac = Hmac::<Sha512>::new_from_slice(key)
1408 .map_err(|e| KipukaError::Internal(format!("HMAC-SHA512 key init failed: {e}")))?;
1409 mac.update(data);
1410 Ok(mac.finalize().into_bytes().to_vec())
1411 } else {
1412 Err(KipukaError::Auth(format!(
1413 "CMP PBM: unsupported MAC algorithm OID: {mac_oid}"
1414 )))
1415 }
1416}
1417
1418fn process_general_message(
1423 _state: &Arc<AppState>,
1424 cmp_req: &CmpRequest,
1425) -> Result<Vec<u8>, KipukaError> {
1426 use synta_certificate::cmp_types::InfoTypeAndValue;
1427
1428 let gen_msg: Vec<InfoTypeAndValue<'_>> = if cmp_req.body_der.is_empty() {
1430 Vec::new()
1431 } else {
1432 synta::Decoder::new(&cmp_req.body_der, synta::Encoding::Der)
1433 .decode()
1434 .map_err(|e| {
1435 KipukaError::BadRequest(format!("failed to parse CMP GenMsgContent: {e}"))
1436 })?
1437 };
1438
1439 for itv in &gen_msg {
1449 tracing::debug!(
1450 oid = %itv.info_type,
1451 "CMP genm: client requested info type"
1452 );
1453 }
1454
1455 let gen_rep: Vec<InfoTypeAndValue<'_>> = Vec::new();
1457 let mut encoder = synta::Encoder::new(synta::Encoding::Der);
1458 synta::Encode::encode(&gen_rep, &mut encoder)
1459 .map_err(|e| KipukaError::Internal(format!("failed to encode CMP GenRepContent: {e}")))?;
1460 encoder.finish().map_err(|e| {
1461 KipukaError::Internal(format!("failed to finalize CMP GenRepContent DER: {e}"))
1462 })
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 #[test]
1470 fn from_tag_maps_request_types() {
1471 assert_eq!(CmpMessageType::from_tag(0), Some(CmpMessageType::Ir));
1472 assert_eq!(CmpMessageType::from_tag(2), Some(CmpMessageType::Cr));
1473 assert_eq!(CmpMessageType::from_tag(7), Some(CmpMessageType::Kur));
1474 assert_eq!(CmpMessageType::from_tag(11), Some(CmpMessageType::Rr));
1475 assert_eq!(CmpMessageType::from_tag(21), Some(CmpMessageType::GenM));
1476 }
1477
1478 #[test]
1479 fn from_tag_maps_response_types() {
1480 assert_eq!(CmpMessageType::from_tag(1), Some(CmpMessageType::Ip));
1481 assert_eq!(CmpMessageType::from_tag(3), Some(CmpMessageType::Cp));
1482 assert_eq!(CmpMessageType::from_tag(8), Some(CmpMessageType::Kup));
1483 assert_eq!(CmpMessageType::from_tag(12), Some(CmpMessageType::Rp));
1484 assert_eq!(CmpMessageType::from_tag(22), Some(CmpMessageType::GenP));
1485 }
1486
1487 #[test]
1488 fn from_tag_maps_special_types() {
1489 assert_eq!(CmpMessageType::from_tag(23), Some(CmpMessageType::Error));
1490 assert_eq!(CmpMessageType::from_tag(24), Some(CmpMessageType::CertConf));
1491 assert_eq!(CmpMessageType::from_tag(25), Some(CmpMessageType::PkiConf));
1492 }
1493
1494 #[test]
1495 fn from_tag_rejects_unknown() {
1496 assert_eq!(CmpMessageType::from_tag(4), None);
1497 assert_eq!(CmpMessageType::from_tag(10), None);
1498 assert_eq!(CmpMessageType::from_tag(50), None);
1499 assert_eq!(CmpMessageType::from_tag(255), None);
1500 }
1501
1502 #[test]
1503 fn is_request_identifies_requests() {
1504 assert!(CmpMessageType::Ir.is_request());
1505 assert!(CmpMessageType::Cr.is_request());
1506 assert!(CmpMessageType::Kur.is_request());
1507 assert!(CmpMessageType::Rr.is_request());
1508 assert!(CmpMessageType::GenM.is_request());
1509 assert!(CmpMessageType::CertConf.is_request());
1510 }
1511
1512 #[test]
1513 fn is_request_rejects_responses() {
1514 assert!(!CmpMessageType::Ip.is_request());
1515 assert!(!CmpMessageType::Cp.is_request());
1516 assert!(!CmpMessageType::Kup.is_request());
1517 assert!(!CmpMessageType::Rp.is_request());
1518 assert!(!CmpMessageType::GenP.is_request());
1519 assert!(!CmpMessageType::Error.is_request());
1520 assert!(!CmpMessageType::PkiConf.is_request());
1521 }
1522
1523 #[test]
1524 fn expected_response_maps_correctly() {
1525 assert_eq!(
1526 CmpMessageType::Ir.expected_response(),
1527 Some(CmpMessageType::Ip)
1528 );
1529 assert_eq!(
1530 CmpMessageType::Cr.expected_response(),
1531 Some(CmpMessageType::Cp)
1532 );
1533 assert_eq!(
1534 CmpMessageType::Kur.expected_response(),
1535 Some(CmpMessageType::Kup)
1536 );
1537 assert_eq!(
1538 CmpMessageType::Rr.expected_response(),
1539 Some(CmpMessageType::Rp)
1540 );
1541 assert_eq!(
1542 CmpMessageType::GenM.expected_response(),
1543 Some(CmpMessageType::GenP)
1544 );
1545 assert_eq!(
1546 CmpMessageType::CertConf.expected_response(),
1547 Some(CmpMessageType::PkiConf)
1548 );
1549 }
1550
1551 #[test]
1552 fn expected_response_none_for_responses() {
1553 assert_eq!(CmpMessageType::Ip.expected_response(), None);
1554 assert_eq!(CmpMessageType::Error.expected_response(), None);
1555 assert_eq!(CmpMessageType::PkiConf.expected_response(), None);
1556 }
1557
1558 #[test]
1559 fn display_formats_correctly() {
1560 assert_eq!(format!("{}", CmpMessageType::Ir), "ir");
1561 assert_eq!(format!("{}", CmpMessageType::Kur), "kur");
1562 assert_eq!(format!("{}", CmpMessageType::CertConf), "certConf");
1563 }
1564
1565 #[test]
1566 fn parse_rejects_empty_message() {
1567 let result = parse_cmp_message(&[]);
1568 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
1569 }
1570
1571 #[test]
1572 fn parse_rejects_short_message() {
1573 let result = parse_cmp_message(&[0x30, 0x03, 0x01, 0x01, 0x00]);
1574 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
1575 }
1576
1577 #[test]
1578 fn parse_rejects_non_sequence() {
1579 let result = parse_cmp_message(&[0x02; 100]);
1581 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
1582 }
1583
1584 #[test]
1585 fn build_response_rejects_empty_transaction_id() {
1586 let req = CmpRequest {
1587 message_type: CmpMessageType::Ir,
1588 transaction_id: Vec::new(),
1589 sender_nonce: vec![1, 2, 3],
1590 sender: "CN=test".into(),
1591 protection: CmpProtectionType::Mac {
1592 algorithm: "hmac-sha256".into(),
1593 },
1594 body_der: vec![0u8; 50],
1595 };
1596 let result = build_cmp_response(&req, CmpMessageType::Ip, &[1, 2, 3], None);
1597 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
1598 }
1599
1600 #[test]
1601 fn build_response_rejects_empty_body() {
1602 let req = CmpRequest {
1603 message_type: CmpMessageType::Cr,
1604 transaction_id: vec![1, 2, 3, 4],
1605 sender_nonce: vec![5, 6, 7],
1606 sender: "CN=test".into(),
1607 protection: CmpProtectionType::Signature {
1608 algorithm: "sha256WithRSAEncryption".into(),
1609 cert_der: vec![0u8; 200],
1610 },
1611 body_der: vec![0u8; 50],
1612 };
1613 let result = build_cmp_response(&req, CmpMessageType::Cp, &[], None);
1614 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
1615 }
1616
1617 #[test]
1620 fn parse_rejects_unprotected_pkiconf_message() {
1621 use synta_certificate::CMPMessageBuilder;
1622
1623 let msg_der = CMPMessageBuilder::new()
1624 .sender(GeneralNameSpec::rfc822("client@example.com"))
1625 .recipient(GeneralNameSpec::rfc822("ca@example.com"))
1626 .transaction_id(b"\x01\x02\x03\x04")
1627 .sender_nonce(b"\xaa\xbb\xcc\xdd")
1628 .body_pkiconf()
1629 .build()
1630 .expect("CMPMessageBuilder::build failed");
1631
1632 let result = parse_cmp_message(&msg_der);
1633 assert!(
1634 matches!(result, Err(KipukaError::Auth(_))),
1635 "unprotected CMP message should be rejected"
1636 );
1637 }
1638
1639 #[test]
1641 fn parse_rejects_unprotected_ir_message() {
1642 use synta_certificate::CMPMessageBuilder;
1643
1644 let empty_seq = vec![0x30, 0x00]; let msg_der = CMPMessageBuilder::new()
1648 .sender(GeneralNameSpec::rfc822("user@example.com"))
1649 .recipient(GeneralNameSpec::dns("ca.example.com"))
1650 .transaction_id(b"\x10\x20\x30\x40")
1651 .sender_nonce(b"\x01\x02\x03\x04\x05\x06\x07\x08")
1652 .body_ir(&empty_seq)
1653 .build()
1654 .expect("CMPMessageBuilder::build failed for ir");
1655
1656 let result = parse_cmp_message(&msg_der);
1657 assert!(
1658 matches!(result, Err(KipukaError::Auth(_))),
1659 "unprotected CMP message should be rejected"
1660 );
1661 }
1662
1663 #[test]
1665 fn parse_rejects_unprotected_genm_message() {
1666 use synta_certificate::CMPMessageBuilder;
1667
1668 let empty_seq = vec![0x30, 0x00];
1670
1671 let msg_der = CMPMessageBuilder::new()
1672 .sender(GeneralNameSpec::rfc822("client@test.com"))
1673 .recipient(GeneralNameSpec::rfc822("ca@test.com"))
1674 .transaction_id(b"\xff\xfe\xfd\xfc\x01\x02\x03\x04")
1675 .sender_nonce(b"\x10\x20\x30\x40\x50\x60\x70\x80")
1676 .body_genm(&empty_seq)
1677 .build()
1678 .expect("CMPMessageBuilder::build failed for genm");
1679
1680 let result = parse_cmp_message(&msg_der);
1681 assert!(
1682 matches!(result, Err(KipukaError::Auth(_))),
1683 "unprotected CMP message should be rejected"
1684 );
1685 }
1686
1687 #[test]
1690 fn build_response_produces_valid_der() {
1691 let req = CmpRequest {
1692 message_type: CmpMessageType::GenM,
1693 transaction_id: vec![0x01, 0x02, 0x03, 0x04],
1694 sender_nonce: vec![0xaa, 0xbb, 0xcc, 0xdd],
1695 sender: "client@example.com".into(),
1696 protection: CmpProtectionType::Mac {
1697 algorithm: "unprotected".into(),
1698 },
1699 body_der: vec![0x30, 0x00],
1700 };
1701
1702 let body_der = vec![0x30, 0x00]; let response_der = build_cmp_response(&req, CmpMessageType::GenP, &body_der, None)
1705 .expect("build_cmp_response should succeed");
1706
1707 let parsed =
1709 PKIMessage::from_der(&response_der).expect("response should parse as valid PKIMessage");
1710
1711 assert_eq!(
1713 parsed.header.transaction_id.map(|o| o.as_bytes().to_vec()),
1714 Some(vec![0x01, 0x02, 0x03, 0x04])
1715 );
1716 assert_eq!(
1717 parsed.header.recip_nonce.map(|o| o.as_bytes().to_vec()),
1718 Some(vec![0xaa, 0xbb, 0xcc, 0xdd])
1719 );
1720 assert!(parsed.header.sender_nonce.is_some());
1721 assert_eq!(parsed.header.sender_nonce.unwrap().as_bytes().len(), 16);
1723 }
1724
1725 #[test]
1727 fn build_pkiconf_response() {
1728 let req = CmpRequest {
1729 message_type: CmpMessageType::CertConf,
1730 transaction_id: vec![0x11, 0x22],
1731 sender_nonce: vec![0x33, 0x44],
1732 sender: "test".into(),
1733 protection: CmpProtectionType::Mac {
1734 algorithm: "unprotected".into(),
1735 },
1736 body_der: Vec::new(),
1737 };
1738
1739 let null_der = synta::ToDer::to_der(&Null).unwrap();
1743 let response_der = build_cmp_response(&req, CmpMessageType::PkiConf, &null_der, None)
1744 .expect("build_cmp_response should succeed for PkiConf");
1745
1746 let parsed = PKIMessage::from_der(&response_der).expect("PkiConf response should parse");
1747 assert!(matches!(parsed.body, PKIBody::Pkiconf(Null)));
1748 }
1749
1750 #[test]
1752 fn nonce_is_16_bytes() {
1753 let nonce = generate_nonce();
1754 assert_eq!(nonce.len(), 16);
1755 assert!(nonce.iter().any(|&b| b != 0));
1757 }
1758
1759 #[test]
1761 fn format_general_name_rfc822() {
1762 let spec = GeneralNameSpec::rfc822("test@example.com");
1763 let gn = spec.to_general_name().unwrap();
1764 assert_eq!(format_general_name(&gn), "test@example.com");
1765 }
1766
1767 #[test]
1769 fn format_general_name_dns() {
1770 let spec = GeneralNameSpec::dns("ca.example.com");
1771 let gn = spec.to_general_name().unwrap();
1772 assert_eq!(format_general_name(&gn), "ca.example.com");
1773 }
1774
1775 #[test]
1777 fn genrep_empty_encodes_as_sequence() {
1778 use synta_certificate::cmp_types::InfoTypeAndValue;
1779
1780 let gen_rep: Vec<InfoTypeAndValue<'_>> = Vec::new();
1782 let mut encoder = synta::Encoder::new(synta::Encoding::Der);
1783 synta::Encode::encode(&gen_rep, &mut encoder).unwrap();
1784 let der = encoder.finish().unwrap();
1785
1786 assert!(!der.is_empty());
1787 assert_eq!(der[0], 0x30); assert_eq!(der[1], 0x00); }
1790
1791 fn build_alg_id(oid_components: &[u32]) -> synta_certificate::AlgorithmIdentifier<'static> {
1796 synta_certificate::AlgorithmIdentifier {
1797 algorithm: synta::ObjectIdentifier::new(oid_components).unwrap(),
1798 parameters: None,
1799 }
1800 }
1801
1802 #[test]
1808 fn pbm_key_derivation_sha256() {
1809 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1811 let secret = b"shared-secret";
1812 let salt = b"random-salt-value";
1813
1814 let key = derive_pbm_key(secret, salt, &owf_alg, 1000).unwrap();
1815
1816 assert_eq!(key.len(), 32);
1818
1819 let key2 = derive_pbm_key(secret, salt, &owf_alg, 1000).unwrap();
1821 assert_eq!(key, key2);
1822
1823 let key_diff_secret = derive_pbm_key(b"other-secret", salt, &owf_alg, 1000).unwrap();
1825 assert_ne!(key, key_diff_secret);
1826
1827 let key_diff_salt = derive_pbm_key(secret, b"other-salt", &owf_alg, 1000).unwrap();
1829 assert_ne!(key, key_diff_salt);
1830
1831 let key_diff_iter = derive_pbm_key(secret, salt, &owf_alg, 500).unwrap();
1833 assert_ne!(key, key_diff_iter);
1834 }
1835
1836 #[test]
1838 fn pbm_key_derivation_rejects_zero_iterations() {
1839 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1840 let result = derive_pbm_key(b"secret", b"salt", &owf_alg, 0);
1841 assert!(result.is_err());
1842 }
1843
1844 #[test]
1846 fn pbm_key_derivation_rejects_excessive_iterations() {
1847 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1848 let result = derive_pbm_key(b"secret", b"salt", &owf_alg, 200_000);
1849 assert!(result.is_err());
1850 }
1851
1852 #[test]
1854 fn pbm_key_derivation_rejects_unknown_owf() {
1855 let owf_alg = build_alg_id(&[1, 2, 3, 4, 5, 6, 7]);
1857 let result = derive_pbm_key(b"secret", b"salt", &owf_alg, 100);
1858 assert!(result.is_err());
1859 }
1860
1861 #[test]
1868 fn pbm_mac_verification_roundtrip() {
1869 use subtle::ConstantTimeEq;
1870
1871 let secret = b"test-shared-secret";
1872 let salt = b"test-salt-1234";
1873 let iteration_count = 500u32;
1874 let data = b"this is the protected header || body data";
1875
1876 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1878 let mac_alg = build_alg_id(&[1, 2, 840, 113549, 2, 9]);
1880
1881 let key = derive_pbm_key(secret, salt, &owf_alg, iteration_count).unwrap();
1883
1884 let mac = compute_pbm_hmac(&key, data, &mac_alg).unwrap();
1886
1887 assert_eq!(mac.len(), 32);
1889
1890 let mac2 = compute_pbm_hmac(&key, data, &mac_alg).unwrap();
1892 assert!(bool::from(mac.ct_eq(&mac2)));
1893 }
1894
1895 #[test]
1897 fn pbm_mac_wrong_secret_differs() {
1898 use subtle::ConstantTimeEq;
1899
1900 let salt = b"salt-value";
1901 let iteration_count = 100u32;
1902 let data = b"protected-message-bytes";
1903
1904 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1905 let mac_alg = build_alg_id(&[1, 2, 840, 113549, 2, 9]);
1906
1907 let key_correct =
1909 derive_pbm_key(b"correct-secret", salt, &owf_alg, iteration_count).unwrap();
1910 let mac_correct = compute_pbm_hmac(&key_correct, data, &mac_alg).unwrap();
1911
1912 let key_wrong = derive_pbm_key(b"wrong-secret", salt, &owf_alg, iteration_count).unwrap();
1914 let mac_wrong = compute_pbm_hmac(&key_wrong, data, &mac_alg).unwrap();
1915
1916 assert!(!bool::from(mac_correct.ct_eq(&mac_wrong)));
1918 }
1919
1920 #[test]
1922 fn pbm_mac_rejects_unknown_mac_alg() {
1923 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1924 let key = derive_pbm_key(b"secret", b"salt", &owf_alg, 100).unwrap();
1925
1926 let bad_mac_alg = build_alg_id(&[1, 2, 3, 4, 5, 6, 7]);
1928 let result = compute_pbm_hmac(&key, b"data", &bad_mac_alg);
1929 assert!(result.is_err());
1930 }
1931
1932 #[test]
1934 fn pbm_key_derivation_sha384() {
1935 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 2]);
1937 let key = derive_pbm_key(b"secret", b"salt", &owf_alg, 10).unwrap();
1938
1939 assert_eq!(key.len(), 48);
1941 }
1942
1943 #[test]
1945 fn pbm_key_derivation_sha512() {
1946 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 3]);
1948 let key = derive_pbm_key(b"secret", b"salt", &owf_alg, 10).unwrap();
1949
1950 assert_eq!(key.len(), 64);
1952 }
1953
1954 #[test]
1956 fn pbm_mac_hmac_sha384() {
1957 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 2]);
1958 let key = derive_pbm_key(b"secret", b"salt", &owf_alg, 10).unwrap();
1959
1960 let mac_alg = build_alg_id(&[1, 2, 840, 113549, 2, 10]);
1962 let mac = compute_pbm_hmac(&key, b"test data", &mac_alg).unwrap();
1963
1964 assert_eq!(mac.len(), 48);
1966 }
1967
1968 #[test]
1970 fn pbm_mac_hmac_sha512() {
1971 let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 3]);
1972 let key = derive_pbm_key(b"secret", b"salt", &owf_alg, 10).unwrap();
1973
1974 let mac_alg = build_alg_id(&[1, 2, 840, 113549, 2, 11]);
1976 let mac = compute_pbm_hmac(&key, b"test data", &mac_alg).unwrap();
1977
1978 assert_eq!(mac.len(), 64);
1980 }
1981}