kipuka/routes/cmp.rs
1//! CMP v3 endpoint (RFC 9810).
2//!
3//! Certificate Management Protocol version 3 provides a comprehensive
4//! certificate lifecycle management protocol. Unlike EST which uses
5//! HTTP semantics, CMP uses its own ASN.1 message format (PKIMessage)
6//! transported over HTTP.
7//!
8//! RFC 9810 §3: CMP messages are encoded as DER and transported via
9//! HTTP POST to `/.well-known/cmp`.
10//!
11//! # Supported message types
12//!
13//! | Type | Body | Description |
14//! |------|------------|--------------------------------|
15//! | ir | CertReqMessages | Initialization request |
16//! | cr | CertReqMessages | Certification request |
17//! | kur | CertReqMessages | Key update request |
18//! | rr | RevReqContent | Revocation request |
19//! | genm | GenMsgContent | General message |
20//!
21//! # Protection
22//!
23//! CMP messages are protected by either:
24//! - **Signature-based** — the sender signs with their certificate
25//! - **MAC-based** — using a shared secret (for initial enrollment)
26
27use 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 crate::error::KipukaError;
35use crate::state::AppState;
36
37/// Content-Type for CMP messages (RFC 9810 §6.2).
38const CONTENT_TYPE_CMP: &str = "application/pkixcmp";
39
40/// CMP message type, identified by the implicit tag on the PKIBody
41/// choice within PKIMessage (RFC 9810 §5.3).
42///
43/// Each variant corresponds to a specific CMP operation. Request
44/// types have matching response types (e.g., `Ir` → `Ip`).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum CmpMessageType {
47 /// Initialization request (tag 0) — new certificate enrollment
48 /// with no prior credential.
49 Ir,
50 /// Initialization response (tag 1).
51 Ip,
52 /// Certification request (tag 2) — standard enrollment with an
53 /// existing credential.
54 Cr,
55 /// Certification response (tag 3).
56 Cp,
57 /// Key update request (tag 7) — re-enrollment / key rollover.
58 Kur,
59 /// Key update response (tag 8).
60 Kup,
61 /// Revocation request (tag 11).
62 Rr,
63 /// Revocation response (tag 12).
64 Rp,
65 /// General message (tag 21) — CA information, supported algorithms.
66 GenM,
67 /// General response (tag 22).
68 GenP,
69 /// Error message (tag 23).
70 Error,
71 /// Certificate confirmation (tag 24).
72 CertConf,
73 /// PKI confirmation (tag 25).
74 PkiConf,
75}
76
77impl CmpMessageType {
78 /// Map an ASN.1 implicit tag value to the corresponding message type.
79 ///
80 /// RFC 9810 §5.3 defines the PKIBody CHOICE tags:
81 ///
82 /// ```text
83 /// ir [0] CertReqMessages
84 /// ip [1] CertRepMessage
85 /// cr [2] CertReqMessages
86 /// cp [3] CertRepMessage
87 /// ...
88 /// kur [7] CertReqMessages
89 /// kup [8] CertRepMessage
90 /// ...
91 /// rr [11] RevReqContent
92 /// rp [12] RevRepContent
93 /// ...
94 /// genm [21] GenMsgContent
95 /// genp [22] GenRepContent
96 /// error [23] ErrorMsgContent
97 /// certConf [24] CertConfirmContent
98 /// pkiConf [25] PKIConfirmContent
99 /// ```
100 pub fn from_tag(tag: u8) -> Option<Self> {
101 match tag {
102 0 => Some(Self::Ir),
103 1 => Some(Self::Ip),
104 2 => Some(Self::Cr),
105 3 => Some(Self::Cp),
106 7 => Some(Self::Kur),
107 8 => Some(Self::Kup),
108 11 => Some(Self::Rr),
109 12 => Some(Self::Rp),
110 21 => Some(Self::GenM),
111 22 => Some(Self::GenP),
112 23 => Some(Self::Error),
113 24 => Some(Self::CertConf),
114 25 => Some(Self::PkiConf),
115 _ => None,
116 }
117 }
118
119 /// Returns `true` if this message type is a client request.
120 pub fn is_request(&self) -> bool {
121 matches!(
122 self,
123 Self::Ir | Self::Cr | Self::Kur | Self::Rr | Self::GenM | Self::CertConf
124 )
125 }
126
127 /// Return the expected response type for a given request type.
128 ///
129 /// Returns `None` for response types or types that do not expect
130 /// a specific response (e.g., `CertConf` expects `PkiConf`, but
131 /// error messages do not expect a response).
132 pub fn expected_response(&self) -> Option<Self> {
133 match self {
134 Self::Ir => Some(Self::Ip),
135 Self::Cr => Some(Self::Cp),
136 Self::Kur => Some(Self::Kup),
137 Self::Rr => Some(Self::Rp),
138 Self::GenM => Some(Self::GenP),
139 Self::CertConf => Some(Self::PkiConf),
140 _ => None,
141 }
142 }
143}
144
145impl std::fmt::Display for CmpMessageType {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 let name = match self {
148 Self::Ir => "ir",
149 Self::Ip => "ip",
150 Self::Cr => "cr",
151 Self::Cp => "cp",
152 Self::Kur => "kur",
153 Self::Kup => "kup",
154 Self::Rr => "rr",
155 Self::Rp => "rp",
156 Self::GenM => "genm",
157 Self::GenP => "genp",
158 Self::Error => "error",
159 Self::CertConf => "certConf",
160 Self::PkiConf => "pkiConf",
161 };
162 f.write_str(name)
163 }
164}
165
166/// CMP message protection mechanism.
167///
168/// RFC 9810 §5.1.3: PKIMessage `protection` is computed over the
169/// `header` and `body` fields. Two modes are defined:
170///
171/// - **Signature-based**: the sender signs with a certificate-bound key.
172/// - **MAC-based**: a shared secret protects the message; used for
173/// initial enrollment when no certificate exists yet.
174#[derive(Debug, Clone)]
175pub enum CmpProtectionType {
176 /// Signature-based protection (RFC 9810 §5.1.3.3).
177 ///
178 /// The sender's certificate is included in the `extraCerts` field
179 /// of the PKIMessage.
180 Signature {
181 /// Signature algorithm OID or name.
182 algorithm: String,
183 /// DER-encoded signer certificate.
184 cert_der: Vec<u8>,
185 },
186 /// MAC-based protection (RFC 9810 §5.1.3.1).
187 ///
188 /// Uses a shared secret (reference number + passphrase) to compute
189 /// a MAC over the message. Typically used for initial enrollment
190 /// (`ir`) before the client has a certificate.
191 Mac {
192 /// MAC algorithm name (e.g., `"hmac-sha256"`).
193 algorithm: String,
194 },
195}
196
197/// Parsed CMP request message.
198///
199/// Represents the essential fields extracted from a PKIMessage DER
200/// encoding for dispatch and processing.
201#[derive(Debug, Clone)]
202pub struct CmpRequest {
203 /// The request message type (ir, cr, kur, rr, genm, certConf).
204 pub message_type: CmpMessageType,
205
206 /// Transaction identifier (RFC 9810 §5.1.1).
207 ///
208 /// Used to correlate request-response pairs. The server copies
209 /// this value into the response.
210 pub transaction_id: Vec<u8>,
211
212 /// Sender nonce (RFC 9810 §5.1.1).
213 ///
214 /// Provides replay protection. The server returns this as
215 /// `recipNonce` in the response.
216 pub sender_nonce: Vec<u8>,
217
218 /// Sender general name (RFC 9810 §5.1.1).
219 ///
220 /// For signature-protected messages: the certificate subject DN.
221 /// For MAC-protected messages: the reference number.
222 pub sender: String,
223
224 /// Protection mechanism and credentials.
225 pub protection: CmpProtectionType,
226
227 /// DER-encoded PKIBody content for the specific message type.
228 pub body_der: Vec<u8>,
229}
230
231/// CMP response message under construction.
232///
233/// The handler builds this struct and passes it to [`build_cmp_response`]
234/// to produce the DER-encoded PKIMessage for the HTTP response.
235#[derive(Debug, Clone)]
236pub struct CmpResponse {
237 /// The response message type (ip, cp, kup, rp, genp, pkiConf, error).
238 pub message_type: CmpMessageType,
239
240 /// Transaction identifier copied from the request.
241 pub transaction_id: Vec<u8>,
242
243 /// Recipient nonce — copied from the request's sender nonce.
244 pub recip_nonce: Vec<u8>,
245
246 /// Sender nonce — freshly generated by the server.
247 pub sender_nonce: Vec<u8>,
248
249 /// Server sender name (CA subject DN).
250 pub sender: String,
251
252 /// DER-encoded response body content.
253 pub body_der: Vec<u8>,
254}
255
256/// Parse a DER-encoded CMP PKIMessage into a [`CmpRequest`].
257///
258/// RFC 9810 §5: PKIMessage is an ASN.1 SEQUENCE:
259///
260/// ```text
261/// PKIMessage ::= SEQUENCE {
262/// header PKIHeader,
263/// body PKIBody,
264/// protection [0] PKIProtection OPTIONAL,
265/// extraCerts [1] SEQUENCE SIZE (1..MAX) OF CMPCertificate OPTIONAL
266/// }
267/// ```
268///
269/// This function performs minimal structural validation and extracts
270/// the fields needed for request dispatch.
271///
272/// # Errors
273///
274/// - `KipukaError::BadRequest` — malformed DER, unknown message type,
275/// missing required header fields.
276/// - `KipukaError::Internal` — full ASN.1 parsing not yet implemented.
277pub fn parse_cmp_message(der: &[u8]) -> Result<CmpRequest, KipukaError> {
278 if der.is_empty() {
279 return Err(KipukaError::BadRequest("empty CMP message".into()));
280 }
281
282 // A minimal PKIMessage (header + body) is at least ~50 bytes.
283 if der.len() < 50 {
284 return Err(KipukaError::BadRequest(
285 "CMP message is too short to be a valid PKIMessage".into(),
286 ));
287 }
288
289 // Verify outer SEQUENCE tag (0x30).
290 if der[0] != 0x30 {
291 return Err(KipukaError::BadRequest(
292 "CMP message does not start with a SEQUENCE tag".into(),
293 ));
294 }
295
296 // Extract the PKIBody tag to determine message type.
297 //
298 // The PKIBody is a CHOICE type with implicit context-class tags.
299 // After the PKIHeader SEQUENCE, the body appears as a
300 // context-tagged element: [tag] IMPLICIT.
301 //
302 // For a stub implementation, we attempt to find the body tag by
303 // scanning past the header SEQUENCE. A full implementation would
304 // use a proper ASN.1 DER parser.
305
306 // TODO: Implement full ASN.1 PKIMessage parsing.
307 //
308 // Implementation plan using `der` + `x509-cert` crates:
309 //
310 // 1. Parse outer SEQUENCE:
311 // let pki_msg = PkiMessage::from_der(der)?;
312 //
313 // 2. Extract header fields:
314 // let header = &pki_msg.header;
315 // let transaction_id = header.transaction_id.as_bytes().to_vec();
316 // let sender_nonce = header.sender_nonce.as_bytes().to_vec();
317 // let sender = header.sender.to_string();
318 //
319 // 3. Determine body type from the CHOICE tag:
320 // let (tag, body_der) = pki_msg.body.tag_and_content();
321 // let message_type = CmpMessageType::from_tag(tag)?;
322 //
323 // 4. Extract protection:
324 // let protection = match pki_msg.protection {
325 // Some(sig) => CmpProtectionType::Signature { ... },
326 // None => return Err("unprotected message"),
327 // };
328 //
329 // 5. Return CmpRequest { message_type, transaction_id, ... }
330
331 Err(KipukaError::Internal(
332 "CMP PKIMessage parsing not yet implemented".into(),
333 ))
334}
335
336/// Build a DER-encoded CMP PKIMessage response.
337///
338/// Constructs a PKIMessage with:
339/// - `header`: sender, recipient, transactionID, senderNonce, recipNonce
340/// - `body`: the response content tagged with the response type
341/// - `protection`: signature computed with the CA signing key
342/// - `extraCerts`: CA certificate chain for validation
343///
344/// # Errors
345///
346/// - `KipukaError::BadRequest` — empty transaction ID or body.
347/// - `KipukaError::Internal` — ASN.1 encoding not yet implemented.
348pub fn build_cmp_response(
349 req: &CmpRequest,
350 response_type: CmpMessageType,
351 body: &[u8],
352) -> Result<Vec<u8>, KipukaError> {
353 if req.transaction_id.is_empty() {
354 return Err(KipukaError::BadRequest(
355 "cannot build CMP response: empty transaction ID".into(),
356 ));
357 }
358
359 if body.is_empty() {
360 return Err(KipukaError::BadRequest(
361 "cannot build CMP response: empty body".into(),
362 ));
363 }
364
365 // Verify the response type is appropriate for the request.
366 if let Some(expected) = req.message_type.expected_response()
367 && expected != response_type
368 {
369 tracing::warn!(
370 request_type = %req.message_type,
371 response_type = %response_type,
372 expected = %expected,
373 "CMP response type mismatch"
374 );
375 }
376
377 // TODO: Implement CMP PKIMessage response construction.
378 //
379 // Implementation plan:
380 //
381 // 1. Build PKIHeader:
382 // let header = PkiHeader {
383 // pvno: Pvno::Cmp2021,
384 // sender: GeneralName::directoryName(ca_subject_dn),
385 // recipient: GeneralName::from_str(&req.sender),
386 // message_time: Some(GeneralizedTime::now()),
387 // protection_alg: Some(sha256_with_rsa()),
388 // transaction_id: OctetString::new(req.transaction_id.clone()),
389 // sender_nonce: OctetString::new(random_nonce()),
390 // recip_nonce: Some(OctetString::new(req.sender_nonce.clone())),
391 // };
392 //
393 // 2. Build PKIBody with the response tag:
394 // let pki_body = PkiBody::new(response_type.tag(), body);
395 //
396 // 3. Compute protection (signature over header + body):
397 // let to_protect = concat_der(&header, &pki_body);
398 // let signature = ca_key.sign(&to_protect)?;
399 // let protection = BitString::new(signature);
400 //
401 // 4. Assemble PKIMessage:
402 // let pki_msg = PkiMessage { header, body: pki_body, protection, extra_certs };
403 //
404 // 5. Encode:
405 // Ok(pki_msg.to_der()?)
406
407 Err(KipukaError::Internal(
408 "CMP PKIMessage response construction not yet implemented".into(),
409 ))
410}
411
412/// `POST /.well-known/cmp` — process a CMP PKIMessage.
413///
414/// RFC 9810 §6.2: CMP messages are transported over HTTP using
415/// `Content-Type: application/pkixcmp`. The request and response
416/// bodies are DER-encoded PKIMessage values.
417///
418/// # Processing
419///
420/// 1. Validate Content-Type is `application/pkixcmp`.
421/// 2. Parse the PKIMessage to extract message type and protection.
422/// 3. Verify message protection (signature or MAC).
423/// 4. Dispatch based on message type:
424/// - `ir` / `cr` → enrollment (certificate issuance)
425/// - `kur` → key update (re-enrollment)
426/// - `rr` → revocation
427/// - `genm` → general message (CA info, algorithms)
428/// - `certConf` → certificate confirmation
429/// 5. Build and return the response PKIMessage.
430///
431/// # Errors
432///
433/// - `400 Bad Request` — malformed PKIMessage, unsupported type
434/// - `403 Forbidden` — MAC verification failure, untrusted signer
435/// - `415 Unsupported Media Type` — wrong Content-Type
436/// - `500 Internal Server Error` — CA backend failure
437pub async fn post_cmp(
438 State(state): State<Arc<AppState>>,
439 body: Bytes,
440) -> Result<Response, KipukaError> {
441 // Check that CMP is enabled.
442 let cmp_config = match state.config.cmp {
443 Some(ref cfg) if cfg.enabled => cfg,
444 _ => return Err(KipukaError::Est("CMP is not enabled".into())),
445 };
446
447 tracing::info!("CMP request received ({} bytes)", body.len());
448
449 if body.is_empty() {
450 return Err(KipukaError::BadRequest("empty CMP message".into()));
451 }
452
453 // Parse the PKIMessage.
454 let cmp_req = parse_cmp_message(&body)?;
455
456 tracing::info!(
457 message_type = %cmp_req.message_type,
458 sender = %cmp_req.sender,
459 transaction_id_len = cmp_req.transaction_id.len(),
460 "CMP message parsed"
461 );
462
463 // Reject non-request message types.
464 if !cmp_req.message_type.is_request() {
465 return Err(KipukaError::BadRequest(format!(
466 "unexpected CMP message type '{}' — only request types are accepted",
467 cmp_req.message_type,
468 )));
469 }
470
471 // Verify message protection.
472 match &cmp_req.protection {
473 CmpProtectionType::Signature {
474 algorithm,
475 cert_der,
476 } => {
477 tracing::debug!(
478 algorithm = %algorithm,
479 cert_len = cert_der.len(),
480 "verifying signature-based CMP protection"
481 );
482
483 // TODO: Verify the signature over (header || body) using
484 // the signer's public key from cert_der, then validate
485 // the signer's certificate chain against the CA truststore.
486 //
487 // let signer_cert = x509::Certificate::from_der(cert_der)?;
488 // let to_verify = concat_header_body(&cmp_req);
489 // signer_cert.verify_signature(algorithm, &to_verify, &protection_bits)?;
490 // x509::verify_chain(&signer_cert, &[], &truststore)?;
491 }
492 CmpProtectionType::Mac { algorithm } => {
493 if !cmp_config.allow_mac_protection {
494 return Err(KipukaError::Auth(
495 "MAC-based CMP protection is not allowed by policy".into(),
496 ));
497 }
498
499 tracing::debug!(
500 algorithm = %algorithm,
501 "verifying MAC-based CMP protection"
502 );
503
504 // TODO: Look up the shared secret by reference number
505 // (from the sender field), compute the MAC over
506 // (header || body), and compare with the protection value.
507 //
508 // let secret = otp_store.lookup_cmp_secret(&cmp_req.sender)?;
509 // let expected_mac = compute_mac(algorithm, &secret, &to_protect)?;
510 // if expected_mac != protection_bits {
511 // return Err(KipukaError::Auth("MAC verification failed"));
512 // }
513 }
514 }
515
516 // Determine the expected response type.
517 let response_type = cmp_req.message_type.expected_response().ok_or_else(|| {
518 KipukaError::BadRequest(format!(
519 "CMP message type '{}' has no defined response",
520 cmp_req.message_type,
521 ))
522 })?;
523
524 // Dispatch based on message type.
525 let response_body_der = match cmp_req.message_type {
526 CmpMessageType::Ir => {
527 if !cmp_config.allow_ir {
528 return Err(KipukaError::Est(
529 "CMP initialization requests (ir) are not allowed".into(),
530 ));
531 }
532 tracing::info!("CMP: processing initialization request (ir)");
533
534 // TODO: Parse CertReqMessages from body_der, extract the
535 // certificate template, issue the certificate, and build
536 // a CertRepMessage response.
537 //
538 // let cert_req = CertReqMessages::from_der(&cmp_req.body_der)?;
539 // let cert_der = kipuka_est::issue::sign_cmp_request(ca, &cert_req).await?;
540 // let cert_rep = CertRepMessage::success(cert_der);
541 // cert_rep.to_der()?
542 Vec::new()
543 }
544 CmpMessageType::Cr => {
545 if !cmp_config.allow_cr {
546 return Err(KipukaError::Est(
547 "CMP certification requests (cr) are not allowed".into(),
548 ));
549 }
550 tracing::info!("CMP: processing certification request (cr)");
551
552 // TODO: Same as ir but the sender has an existing certificate.
553 Vec::new()
554 }
555 CmpMessageType::Kur => {
556 if !cmp_config.allow_kur {
557 return Err(KipukaError::Est(
558 "CMP key update requests (kur) are not allowed".into(),
559 ));
560 }
561 tracing::info!("CMP: processing key update request (kur)");
562
563 // TODO: Verify the old certificate is valid and not revoked,
564 // issue a new certificate with the updated key.
565 Vec::new()
566 }
567 CmpMessageType::Rr => {
568 if !cmp_config.allow_rr {
569 return Err(KipukaError::Est(
570 "CMP revocation requests (rr) are not allowed".into(),
571 ));
572 }
573 tracing::info!("CMP: processing revocation request (rr)");
574
575 // TODO: Parse RevReqContent, look up the certificate by
576 // serial number, revoke it, build RevRepContent response.
577 Vec::new()
578 }
579 CmpMessageType::GenM => {
580 tracing::info!("CMP: processing general message (genm)");
581
582 // TODO: Parse GenMsgContent InfoTypeAndValue sequence.
583 // Return CA certificates, supported algorithms, etc.
584 Vec::new()
585 }
586 CmpMessageType::CertConf => {
587 tracing::info!("CMP: processing certificate confirmation (certConf)");
588
589 // TODO: Verify the certificate hash in the confirmation
590 // matches the issued certificate. Return PKIConfirm (empty).
591 Vec::new()
592 }
593 _ => {
594 return Err(KipukaError::BadRequest(format!(
595 "unsupported CMP message type: {}",
596 cmp_req.message_type,
597 )));
598 }
599 };
600
601 if response_body_der.is_empty() {
602 return Err(KipukaError::Ca("CMP processing not yet implemented".into()));
603 }
604
605 // Build the response PKIMessage.
606 let response_der = build_cmp_response(&cmp_req, response_type, &response_body_der)?;
607
608 state
609 .record_audit_event(
610 "cmp_success",
611 &format!("type={}, sender={}", cmp_req.message_type, cmp_req.sender),
612 )
613 .await;
614
615 // RFC 9810 §6.2: Response Content-Type is application/pkixcmp.
616 let mut resp = (StatusCode::OK, response_der).into_response();
617 resp.headers_mut().insert(
618 header::CONTENT_TYPE,
619 HeaderValue::from_static(CONTENT_TYPE_CMP),
620 );
621 Ok(resp)
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn from_tag_maps_request_types() {
630 assert_eq!(CmpMessageType::from_tag(0), Some(CmpMessageType::Ir));
631 assert_eq!(CmpMessageType::from_tag(2), Some(CmpMessageType::Cr));
632 assert_eq!(CmpMessageType::from_tag(7), Some(CmpMessageType::Kur));
633 assert_eq!(CmpMessageType::from_tag(11), Some(CmpMessageType::Rr));
634 assert_eq!(CmpMessageType::from_tag(21), Some(CmpMessageType::GenM));
635 }
636
637 #[test]
638 fn from_tag_maps_response_types() {
639 assert_eq!(CmpMessageType::from_tag(1), Some(CmpMessageType::Ip));
640 assert_eq!(CmpMessageType::from_tag(3), Some(CmpMessageType::Cp));
641 assert_eq!(CmpMessageType::from_tag(8), Some(CmpMessageType::Kup));
642 assert_eq!(CmpMessageType::from_tag(12), Some(CmpMessageType::Rp));
643 assert_eq!(CmpMessageType::from_tag(22), Some(CmpMessageType::GenP));
644 }
645
646 #[test]
647 fn from_tag_maps_special_types() {
648 assert_eq!(CmpMessageType::from_tag(23), Some(CmpMessageType::Error));
649 assert_eq!(CmpMessageType::from_tag(24), Some(CmpMessageType::CertConf));
650 assert_eq!(CmpMessageType::from_tag(25), Some(CmpMessageType::PkiConf));
651 }
652
653 #[test]
654 fn from_tag_rejects_unknown() {
655 assert_eq!(CmpMessageType::from_tag(4), None);
656 assert_eq!(CmpMessageType::from_tag(10), None);
657 assert_eq!(CmpMessageType::from_tag(50), None);
658 assert_eq!(CmpMessageType::from_tag(255), None);
659 }
660
661 #[test]
662 fn is_request_identifies_requests() {
663 assert!(CmpMessageType::Ir.is_request());
664 assert!(CmpMessageType::Cr.is_request());
665 assert!(CmpMessageType::Kur.is_request());
666 assert!(CmpMessageType::Rr.is_request());
667 assert!(CmpMessageType::GenM.is_request());
668 assert!(CmpMessageType::CertConf.is_request());
669 }
670
671 #[test]
672 fn is_request_rejects_responses() {
673 assert!(!CmpMessageType::Ip.is_request());
674 assert!(!CmpMessageType::Cp.is_request());
675 assert!(!CmpMessageType::Kup.is_request());
676 assert!(!CmpMessageType::Rp.is_request());
677 assert!(!CmpMessageType::GenP.is_request());
678 assert!(!CmpMessageType::Error.is_request());
679 assert!(!CmpMessageType::PkiConf.is_request());
680 }
681
682 #[test]
683 fn expected_response_maps_correctly() {
684 assert_eq!(
685 CmpMessageType::Ir.expected_response(),
686 Some(CmpMessageType::Ip)
687 );
688 assert_eq!(
689 CmpMessageType::Cr.expected_response(),
690 Some(CmpMessageType::Cp)
691 );
692 assert_eq!(
693 CmpMessageType::Kur.expected_response(),
694 Some(CmpMessageType::Kup)
695 );
696 assert_eq!(
697 CmpMessageType::Rr.expected_response(),
698 Some(CmpMessageType::Rp)
699 );
700 assert_eq!(
701 CmpMessageType::GenM.expected_response(),
702 Some(CmpMessageType::GenP)
703 );
704 assert_eq!(
705 CmpMessageType::CertConf.expected_response(),
706 Some(CmpMessageType::PkiConf)
707 );
708 }
709
710 #[test]
711 fn expected_response_none_for_responses() {
712 assert_eq!(CmpMessageType::Ip.expected_response(), None);
713 assert_eq!(CmpMessageType::Error.expected_response(), None);
714 assert_eq!(CmpMessageType::PkiConf.expected_response(), None);
715 }
716
717 #[test]
718 fn display_formats_correctly() {
719 assert_eq!(format!("{}", CmpMessageType::Ir), "ir");
720 assert_eq!(format!("{}", CmpMessageType::Kur), "kur");
721 assert_eq!(format!("{}", CmpMessageType::CertConf), "certConf");
722 }
723
724 #[test]
725 fn parse_rejects_empty_message() {
726 let result = parse_cmp_message(&[]);
727 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
728 }
729
730 #[test]
731 fn parse_rejects_short_message() {
732 let result = parse_cmp_message(&[0x30, 0x03, 0x01, 0x01, 0x00]);
733 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
734 }
735
736 #[test]
737 fn parse_rejects_non_sequence() {
738 // Tag 0x02 = INTEGER, not SEQUENCE.
739 let result = parse_cmp_message(&[0x02; 100]);
740 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
741 }
742
743 #[test]
744 fn build_response_rejects_empty_transaction_id() {
745 let req = CmpRequest {
746 message_type: CmpMessageType::Ir,
747 transaction_id: Vec::new(),
748 sender_nonce: vec![1, 2, 3],
749 sender: "CN=test".into(),
750 protection: CmpProtectionType::Mac {
751 algorithm: "hmac-sha256".into(),
752 },
753 body_der: vec![0u8; 50],
754 };
755 let result = build_cmp_response(&req, CmpMessageType::Ip, &[1, 2, 3]);
756 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
757 }
758
759 #[test]
760 fn build_response_rejects_empty_body() {
761 let req = CmpRequest {
762 message_type: CmpMessageType::Cr,
763 transaction_id: vec![1, 2, 3, 4],
764 sender_nonce: vec![5, 6, 7],
765 sender: "CN=test".into(),
766 protection: CmpProtectionType::Signature {
767 algorithm: "sha256WithRSAEncryption".into(),
768 cert_der: vec![0u8; 200],
769 },
770 body_der: vec![0u8; 50],
771 };
772 let result = build_cmp_response(&req, CmpMessageType::Cp, &[]);
773 assert!(matches!(result, Err(KipukaError::BadRequest(_))));
774 }
775}