Skip to main content

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}