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 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
45/// Content-Type for CMP messages (RFC 9810 §6.2).
46const CONTENT_TYPE_CMP: &str = "application/pkixcmp";
47
48/// CMP message type, identified by the implicit tag on the PKIBody
49/// choice within PKIMessage (RFC 9810 §5.3).
50///
51/// Each variant corresponds to a specific CMP operation.  Request
52/// types have matching response types (e.g., `Ir` → `Ip`).
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum CmpMessageType {
55    /// Initialization request (tag 0) — new certificate enrollment
56    /// with no prior credential.
57    Ir,
58    /// Initialization response (tag 1).
59    Ip,
60    /// Certification request (tag 2) — standard enrollment with an
61    /// existing credential.
62    Cr,
63    /// Certification response (tag 3).
64    Cp,
65    /// Key update request (tag 7) — re-enrollment / key rollover.
66    Kur,
67    /// Key update response (tag 8).
68    Kup,
69    /// Revocation request (tag 11).
70    Rr,
71    /// Revocation response (tag 12).
72    Rp,
73    /// General message (tag 21) — CA information, supported algorithms.
74    GenM,
75    /// General response (tag 22).
76    GenP,
77    /// Error message (tag 23).
78    Error,
79    /// Certificate confirmation (tag 24).
80    CertConf,
81    /// PKI confirmation (tag 25).
82    PkiConf,
83}
84
85impl CmpMessageType {
86    /// Map an ASN.1 implicit tag value to the corresponding message type.
87    ///
88    /// RFC 9810 §5.3 defines the PKIBody CHOICE tags:
89    ///
90    /// ```text
91    /// ir       [0]  CertReqMessages
92    /// ip       [1]  CertRepMessage
93    /// cr       [2]  CertReqMessages
94    /// cp       [3]  CertRepMessage
95    /// ...
96    /// kur      [7]  CertReqMessages
97    /// kup      [8]  CertRepMessage
98    /// ...
99    /// rr       [11] RevReqContent
100    /// rp       [12] RevRepContent
101    /// ...
102    /// genm     [21] GenMsgContent
103    /// genp     [22] GenRepContent
104    /// error    [23] ErrorMsgContent
105    /// certConf [24] CertConfirmContent
106    /// pkiConf  [25] PKIConfirmContent
107    /// ```
108    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    /// Returns `true` if this message type is a client request.
128    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    /// Return the expected response type for a given request type.
136    ///
137    /// Returns `None` for response types or types that do not expect
138    /// a specific response (e.g., `CertConf` expects `PkiConf`, but
139    /// error messages do not expect a response).
140    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/// CMP message protection mechanism.
175///
176/// RFC 9810 §5.1.3: PKIMessage `protection` is computed over the
177/// `header` and `body` fields.  Two modes are defined:
178///
179/// - **Signature-based**: the sender signs with a certificate-bound key.
180/// - **MAC-based**: a shared secret protects the message; used for
181///   initial enrollment when no certificate exists yet.
182#[derive(Debug, Clone)]
183pub enum CmpProtectionType {
184    /// Signature-based protection (RFC 9810 §5.1.3.3).
185    ///
186    /// The sender's certificate is included in the `extraCerts` field
187    /// of the PKIMessage.
188    Signature {
189        /// Signature algorithm OID or name.
190        algorithm: String,
191        /// DER-encoded signer certificate.
192        cert_der: Vec<u8>,
193    },
194    /// MAC-based protection (RFC 9810 §5.1.3.1).
195    ///
196    /// Uses a shared secret (reference number + passphrase) to compute
197    /// a MAC over the message.  Typically used for initial enrollment
198    /// (`ir`) before the client has a certificate.
199    Mac {
200        /// MAC algorithm name (e.g., `"hmac-sha256"`).
201        algorithm: String,
202    },
203}
204
205/// Parsed CMP request message.
206///
207/// Represents the essential fields extracted from a PKIMessage DER
208/// encoding for dispatch and processing.
209#[derive(Debug, Clone)]
210pub struct CmpRequest {
211    /// The request message type (ir, cr, kur, rr, genm, certConf).
212    pub message_type: CmpMessageType,
213
214    /// Transaction identifier (RFC 9810 §5.1.1).
215    ///
216    /// Used to correlate request-response pairs.  The server copies
217    /// this value into the response.
218    pub transaction_id: Vec<u8>,
219
220    /// Sender nonce (RFC 9810 §5.1.1).
221    ///
222    /// Provides replay protection.  The server returns this as
223    /// `recipNonce` in the response.
224    pub sender_nonce: Vec<u8>,
225
226    /// Sender general name (RFC 9810 §5.1.1).
227    ///
228    /// For signature-protected messages: the certificate subject DN.
229    /// For MAC-protected messages: the reference number.
230    pub sender: String,
231
232    /// Protection mechanism and credentials.
233    pub protection: CmpProtectionType,
234
235    /// DER-encoded PKIBody content for the specific message type.
236    pub body_der: Vec<u8>,
237}
238
239/// CMP response message under construction.
240///
241/// The handler builds this struct and passes it to [`build_cmp_response`]
242/// to produce the DER-encoded PKIMessage for the HTTP response.
243#[derive(Debug, Clone)]
244pub struct CmpResponse {
245    /// The response message type (ip, cp, kup, rp, genp, pkiConf, error).
246    pub message_type: CmpMessageType,
247
248    /// Transaction identifier copied from the request.
249    pub transaction_id: Vec<u8>,
250
251    /// Recipient nonce — copied from the request's sender nonce.
252    pub recip_nonce: Vec<u8>,
253
254    /// Sender nonce — freshly generated by the server.
255    pub sender_nonce: Vec<u8>,
256
257    /// Server sender name (CA subject DN).
258    pub sender: String,
259
260    /// DER-encoded response body content.
261    pub body_der: Vec<u8>,
262}
263
264/// Parse a DER-encoded CMP PKIMessage into a [`CmpRequest`].
265///
266/// RFC 9810 §5: PKIMessage is an ASN.1 SEQUENCE:
267///
268/// ```text
269/// PKIMessage ::= SEQUENCE {
270///     header     PKIHeader,
271///     body       PKIBody,
272///     protection [0] PKIProtection OPTIONAL,
273///     extraCerts [1] SEQUENCE SIZE (1..MAX) OF CMPCertificate OPTIONAL
274/// }
275/// ```
276///
277/// This function performs minimal structural validation and extracts
278/// the fields needed for request dispatch.
279///
280/// # Errors
281///
282/// - `KipukaError::BadRequest` — malformed DER, unknown message type,
283///   missing required header fields.
284/// - `KipukaError::Internal` — full ASN.1 parsing not yet implemented.
285pub 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    // A minimal PKIMessage (header + body) is at least ~50 bytes.
291    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    // Verify outer SEQUENCE tag (0x30).
298    if der[0] != 0x30 {
299        return Err(KipukaError::BadRequest(
300            "CMP message does not start with a SEQUENCE tag".into(),
301        ));
302    }
303
304    // Parse the DER-encoded PKIMessage using synta-certificate's
305    // auto-generated CMP types (RFC 9810 / RFC 4210).
306    let pki_msg = PKIMessage::from_der(der)
307        .map_err(|e| KipukaError::BadRequest(format!("failed to parse CMP PKIMessage: {e}")))?;
308
309    // Extract header fields.
310    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    // Format sender GeneralName for logging / correlation.
323    let sender = format_general_name(&pki_msg.header.sender);
324
325    // Determine message type from the PKIBody CHOICE variant.
326    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    // Extract protection information.
349    let protection = if let Some(prot_bits) = &pki_msg.protection {
350        // Check if the protection algorithm indicates MAC-based or signature-based.
351        if let Some(ref alg_id) = pki_msg.header.protection_alg {
352            let alg_oid_str = alg_id.algorithm.to_string();
353            // id-PasswordBasedMac (1.2.840.113533.7.66.13) and
354            // id-DHBasedMac (1.2.840.113533.7.66.30) indicate MAC-based protection.
355            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                // Signature-based protection — extract the signer certificate
364                // from extraCerts if present.
365                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            // Protection bits present but no algorithm — treat as unprotected
378            // (malformed, but don't crash).
379            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        // Unprotected messages are not acceptable — reject them outright.
386        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
399/// Build a DER-encoded CMP PKIMessage response.
400///
401/// Constructs a PKIMessage with:
402/// - `header`: sender, recipient, transactionID, senderNonce, recipNonce
403/// - `body`: the response content tagged with the response type
404/// - `protection`: signature computed with the CA signing key
405/// - `extraCerts`: CA certificate chain for validation
406///
407/// # Errors
408///
409/// - `KipukaError::BadRequest` — empty transaction ID or body.
410/// - `KipukaError::Internal` — ASN.1 encoding not yet implemented.
411pub 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    // Verify the response type is appropriate for the request.
430    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    // Generate a fresh sender nonce for replay protection.
442    let server_nonce = generate_nonce();
443
444    // Build the response PKIBody tagged with the appropriate CHOICE variant.
445    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    // Build PKIHeader with:
461    //   - pvno = 2 (cmp2000, compatible with both RFC 4210 and RFC 9810)
462    //   - sender = CA subject DN (extracted from the default CA certificate)
463    //   - recipient = original sender
464    //   - transactionID = echoed from request
465    //   - senderNonce = fresh random nonce
466    //   - recipNonce = request's senderNonce (replay protection)
467    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    // Use the request's sender as the recipient, echoing it back per CMP protocol.
473    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    // Assemble the PKIMessage (unprotected for now; a full implementation
502    // would compute a signature over header || body using the CA key).
503    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
573/// `POST /.well-known/cmp` — process a CMP PKIMessage.
574///
575/// RFC 9810 §6.2: CMP messages are transported over HTTP using
576/// `Content-Type: application/pkixcmp`.  The request and response
577/// bodies are DER-encoded PKIMessage values.
578///
579/// # Processing
580///
581/// 1. Validate Content-Type is `application/pkixcmp`.
582/// 2. Parse the PKIMessage to extract message type and protection.
583/// 3. Verify message protection (signature or MAC).
584/// 4. Dispatch based on message type:
585///    - `ir` / `cr` → enrollment (certificate issuance)
586///    - `kur` → key update (re-enrollment)
587///    - `rr` → revocation
588///    - `genm` → general message (CA info, algorithms)
589///    - `certConf` → certificate confirmation
590/// 5. Build and return the response PKIMessage.
591///
592/// # Errors
593///
594/// - `400 Bad Request` — malformed PKIMessage, unsupported type
595/// - `403 Forbidden` — MAC verification failure, untrusted signer
596/// - `415 Unsupported Media Type` — wrong Content-Type
597/// - `500 Internal Server Error` — CA backend failure
598pub async fn post_cmp(
599    State(state): State<Arc<AppState>>,
600    body: Bytes,
601) -> Result<Response, KipukaError> {
602    // Check that CMP is enabled.
603    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    // Parse the PKIMessage.
615    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    // Reject non-request message types.
625    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    // RFC 4210 ProtectedPart is a SEQUENCE containing the header and body.
633    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    // Extract protection bits (signature or MAC value).
638    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    // Extract DER-encoded AlgorithmIdentifier for the protection algorithm.
645    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            // 1. Extract the signer certificate's SPKI for signature verification.
670            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            // 2. Verify the signature over (header || body) using the signer's
678            //    public key.  protection_alg_der is the DER-encoded
679            //    AlgorithmIdentifier; protection_bits is the raw signature bytes.
680            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            // 1. Extract PBMParameter from the protectionAlg parameters.
708            //
709            // RFC 4210 §5.1.3.1: the AlgorithmIdentifier for
710            // id-PasswordBasedMac has PBMParameter as its parameters.
711            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            // The parameters field is an Element — DER-encode it to get the
723            // raw bytes, then parse as PBMParameter.
724            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            // 2. Look up the shared secret by reference number (sender field).
737            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            // 3. Derive the MAC key via iterated OWF (RFC 4210 §5.1.3.1).
770            //
771            // basekey = OWF(secret || salt)
772            // for i in 1..iterationCount:
773            //     basekey = OWF(basekey)
774            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            // 4. Compute HMAC over protected_bytes using the MAC algorithm
782            //    from PBMParameter and the derived key.
783            let computed_mac = compute_pbm_hmac(&derived_key, &protected_bytes, &pbm.mac)?;
784
785            // 5. Constant-time compare with the protection bits.
786            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    // Determine the expected response type.
798    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    // Dispatch based on message type.
808    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            // KUR is treated identically to CR for certificate issuance;
835            // additional validation (old cert not revoked) is a policy check
836            // that the protection verification above enforces.
837            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            // CertConf acknowledges receipt of the issued certificate.
855            // The response is PKIConfirm which is an empty NULL body.
856            // The Null is encoded directly in the PKIBody::Pkiconf variant
857            // by build_cmp_response, so we return an empty placeholder here.
858            // RFC 9810 §5.3.18: PKIConfirmContent ::= NULL
859            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    // Extract the CA subject Name DER for the response sender field.
871    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    // Build the response PKIMessage.
883    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    // Preserve the request's GeneralName choice and encoding in the response.
891    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    // RFC 9810 §6.2: Response Content-Type is application/pkixcmp.
918    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
926// ── Helper functions ──────────────────────────────────────────────────────────
927
928/// Generate a 16-byte random nonce for CMP replay protection.
929fn 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
936/// Format a `GeneralName` to a human-readable string for logging.
937fn format_general_name(gn: &synta_certificate::GeneralName<'_>) -> String {
938    use synta_certificate::GeneralName;
939    match gn {
940        GeneralName::DirectoryName(name) => {
941            // Encode the Name to DER, then format using synta-certificate's
942            // RFC 4514 DN formatter.
943            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
954/// Process a CMP enrollment request (ir, cr, or kur).
955///
956/// Parses the `CertReqMessages` body, extracts the first `CertReqMsg`,
957/// builds a minimal CSR from the CRMF `CertTemplate`, issues a certificate
958/// via `ca::issue::issue_certificate`, and returns the DER-encoded
959/// `CertRepMessage` for the response body.
960async fn process_enrollment_request(
961    state: &Arc<AppState>,
962    cmp_req: &CmpRequest,
963    req_type: &str,
964) -> Result<Vec<u8>, KipukaError> {
965    // Parse CertReqMessages (SEQUENCE OF CertReqMsg) from the body DER.
966    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    // Process the first CertReqMsg.
982    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    // Extract the subject Name DER from the CertTemplate.
987    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    // Extract the SubjectPublicKeyInfo DER from the CertTemplate.
995    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    // Select the default CA for enrollment.
1009    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    // Resolve signing key (PEM or HSM).
1028    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    // Issue the certificate.
1038    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    // Build CertRepMessage with a single successful CertResponse.
1058    //
1059    // PKIStatusInfo: status = 0 (accepted), no status string, no failInfo.
1060    let status_info = PKIStatusInfo {
1061        status: Integer::from_i64(0), // accepted
1062        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
1087/// Process a CMP revocation request (rr).
1088///
1089/// Parses the `RevReqContent` (SEQUENCE OF RevDetails), extracts the
1090/// certificate serial number from the cert template, marks the certificate
1091/// as revoked in the database, and returns the DER-encoded `RevRepContent`.
1092async fn process_revocation_request(
1093    state: &Arc<AppState>,
1094    cmp_req: &CmpRequest,
1095) -> Result<Vec<u8>, KipukaError> {
1096    // Enrollment MAC credentials carry no certificate ownership or RA privilege.
1097    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    // Parse RevReqContent (SEQUENCE OF RevDetails).
1106    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    // ── Revocation authorization ──────────────────────────────────────
1120    //
1121    // For signature-protected messages, verify that the signer is
1122    // authorized to revoke the requested certificate(s):
1123    //
1124    // - **RA privilege**: if the signer cert has id-kp-cmcRA EKU
1125    //   (1.3.6.1.5.5.7.3.28), allow revocation of any certificate.
1126    // - **Self-revocation**: otherwise, the signer cert's subject DN
1127    //   must match the subject of the certificate being revoked.
1128    //
1129    // MAC-protected revocation requests are rejected above, so we
1130    // only handle the signature case here.
1131    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        // Check if the signer holds id-kp-cmcRA EKU (RA privilege).
1141        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            // Self-revocation: check each RevDetails entry to verify the
1166            // signer's subject matches the revokee's subject.
1167            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                    // Query the database for the certificate's subject DN.
1179                    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                    // If the certificate is not in the database, the revocation
1213                    // will produce a "not found" result below — not an authz error.
1214                }
1215            }
1216        }
1217    }
1218    // MAC-protected revocations: already rejected by the MAC verification
1219    // check above, so we do not need to handle them here.
1220
1221    // Process each RevDetails entry and collect status responses.
1222    let mut statuses = Vec::with_capacity(rev_details.len());
1223
1224    for detail in &rev_details {
1225        // The cert_details field is a CertTemplate containing the
1226        // serial number and issuer of the certificate to revoke.
1227        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        // Update the certificate status to 'revoked' in the database.
1243        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        // Build a successful PKIStatusInfo for this revocation.
1267        statuses.push(PKIStatusInfo {
1268            status: Integer::from_i64(0), // accepted
1269            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
1285/// Derive a MAC key using the Password-Based MAC scheme (RFC 4210 §5.1.3.1).
1286///
1287/// The derivation is:
1288///   1. `basekey = OWF(secret || salt)`
1289///   2. For `i` in `1..iteration_count`: `basekey = OWF(basekey)`
1290///
1291/// where `OWF` is the one-way function specified in the `PBMParameter`.
1292///
1293/// The `owf_alg` parameter is the `AlgorithmIdentifier` from the `owf` field
1294/// of the `PBMParameter`, whose `.algorithm` OID identifies the hash function.
1295fn 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    // Map the OWF OID to a hash function.
1306    //
1307    // Common OIDs:
1308    //   SHA-1:   1.3.14.3.2.26  (id-sha1, NOT recommended)
1309    //   SHA-256: 2.16.840.1.101.3.4.2.1  (id-sha256)
1310    //   SHA-384: 2.16.840.1.101.3.4.2.2  (id-sha384)
1311    //   SHA-512: 2.16.840.1.101.3.4.2.3  (id-sha512)
1312    //
1313    // We intentionally exclude SHA-1 as it is deprecated for new deployments.
1314    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    // Cap iteration count to prevent DoS via absurdly large values.
1339    if iteration_count > 100_000 {
1340        return Err(KipukaError::Auth(
1341            "CMP PBM: iteration count exceeds maximum (100000)".into(),
1342        ));
1343    }
1344
1345    // Step 1: basekey = OWF(secret || salt)
1346    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    // Step 2: iterate OWF (iteration_count - 1 more times,
1357    // since first application was step 1).
1358    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
1369/// Compute an HMAC over `data` using the MAC algorithm from `PBMParameter`
1370/// and the derived key.
1371///
1372/// The `mac_alg` parameter is the `AlgorithmIdentifier` from the `mac` field
1373/// of the `PBMParameter`, whose `.algorithm` OID identifies the HMAC variant.
1374fn 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    // Map the MAC OID to an HMAC variant.
1385    //
1386    // Common OIDs:
1387    //   hmac-SHA256: 1.2.840.113549.2.9
1388    //   hmac-SHA384: 1.2.840.113549.2.10
1389    //   hmac-SHA512: 1.2.840.113549.2.11
1390    //   hmac-SHA1:   1.2.840.113549.2.7  (deprecated)
1391    //
1392    // The OWF hash OIDs (id-sha256 etc.) are also accepted as MAC algorithm
1393    // identifiers since some CMP implementations use them interchangeably.
1394    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
1418/// Process a CMP general message (genm).
1419///
1420/// Parses the `GenMsgContent` (SEQUENCE OF InfoTypeAndValue) and returns
1421/// appropriate information.  Currently supports returning CA certificates.
1422fn 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    // Parse GenMsgContent to determine what information is being requested.
1429    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    // Build GenRepContent (SEQUENCE OF InfoTypeAndValue) response.
1440    //
1441    // For now, return an empty GenRepContent.  Known OIDs that could be
1442    // supported:
1443    //   - id-it-caCerts (1.3.6.1.5.5.7.4.17): return CA certificate chain
1444    //   - id-it-rootCaKeyUpdate (1.3.6.1.5.5.7.4.18): CA key rollover info
1445    //   - id-it-certReqTemplate (1.3.6.1.5.5.7.4.19): certificate template
1446    //
1447    // Log the requested OIDs so we can add support incrementally.
1448    for itv in &gen_msg {
1449        tracing::debug!(
1450            oid = %itv.info_type,
1451            "CMP genm: client requested info type"
1452        );
1453    }
1454
1455    // Return an empty SEQUENCE as the GenRepContent.
1456    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        // Tag 0x02 = INTEGER, not SEQUENCE.
1580        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    /// Build an unprotected PKIMessage DER and verify that
1618    /// `parse_cmp_message` rejects it (unprotected messages must be refused).
1619    #[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    /// Build an unprotected IR message and verify parse rejects it.
1640    #[test]
1641    fn parse_rejects_unprotected_ir_message() {
1642        use synta_certificate::CMPMessageBuilder;
1643
1644        // Build a minimal CertReqMessages body (empty SEQUENCE).
1645        let empty_seq = vec![0x30, 0x00]; // SEQUENCE {}
1646
1647        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    /// Build an unprotected GENM message and verify parse rejects it.
1664    #[test]
1665    fn parse_rejects_unprotected_genm_message() {
1666        use synta_certificate::CMPMessageBuilder;
1667
1668        // Empty GenMsgContent (SEQUENCE OF InfoTypeAndValue).
1669        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 build_cmp_response produces valid DER that round-trips
1688    /// through PKIMessage::from_der.
1689    #[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        // Build a GenP response with an empty SEQUENCE body.
1703        let body_der = vec![0x30, 0x00]; // empty GenRepContent
1704        let response_der = build_cmp_response(&req, CmpMessageType::GenP, &body_der, None)
1705            .expect("build_cmp_response should succeed");
1706
1707        // Verify it parses as a valid PKIMessage.
1708        let parsed =
1709            PKIMessage::from_der(&response_der).expect("response should parse as valid PKIMessage");
1710
1711        // Verify the header fields were set correctly.
1712        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        // senderNonce should be 16 bytes (generated by generate_nonce).
1722        assert_eq!(parsed.header.sender_nonce.unwrap().as_bytes().len(), 16);
1723    }
1724
1725    /// Test build_cmp_response for PkiConf (NULL body).
1726    #[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        // For PkiConf, the body is NULL — we still need to pass non-empty
1740        // bytes to satisfy the pre-check, but the actual encoding uses
1741        // PKIBody::Pkiconf(Null).
1742        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 that generate_nonce produces 16 bytes.
1751    #[test]
1752    fn nonce_is_16_bytes() {
1753        let nonce = generate_nonce();
1754        assert_eq!(nonce.len(), 16);
1755        // Verify it's not all zeros (probabilistic but reliable).
1756        assert!(nonce.iter().any(|&b| b != 0));
1757    }
1758
1759    /// Test format_general_name with various GeneralName types.
1760    #[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 format_general_name with DNS name.
1768    #[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    /// Verify that an empty GenRepContent encodes as a valid SEQUENCE.
1776    #[test]
1777    fn genrep_empty_encodes_as_sequence() {
1778        use synta_certificate::cmp_types::InfoTypeAndValue;
1779
1780        // Encode an empty GenRepContent the same way process_general_message does.
1781        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); // SEQUENCE tag
1788        assert_eq!(der[1], 0x00); // length 0 = empty sequence
1789    }
1790
1791    // ── MAC verification tests ──────────────────────────────────────────
1792
1793    /// Helper: build an AlgorithmIdentifier with the given OID components
1794    /// and no parameters (NULL).
1795    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 derive_pbm_key with SHA-256 OWF and known inputs.
1803    ///
1804    /// Verifies that:
1805    /// - Key derivation produces a 32-byte output (SHA-256 digest size)
1806    /// - The output changes when any input (secret, salt, iteration count) changes
1807    #[test]
1808    fn pbm_key_derivation_sha256() {
1809        // SHA-256 OID: 2.16.840.1.101.3.4.2.1
1810        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        // SHA-256 produces 32 bytes.
1817        assert_eq!(key.len(), 32);
1818
1819        // Verify determinism: same inputs → same key.
1820        let key2 = derive_pbm_key(secret, salt, &owf_alg, 1000).unwrap();
1821        assert_eq!(key, key2);
1822
1823        // Different secret → different key.
1824        let key_diff_secret = derive_pbm_key(b"other-secret", salt, &owf_alg, 1000).unwrap();
1825        assert_ne!(key, key_diff_secret);
1826
1827        // Different salt → different key.
1828        let key_diff_salt = derive_pbm_key(secret, b"other-salt", &owf_alg, 1000).unwrap();
1829        assert_ne!(key, key_diff_salt);
1830
1831        // Different iteration count → different key.
1832        let key_diff_iter = derive_pbm_key(secret, salt, &owf_alg, 500).unwrap();
1833        assert_ne!(key, key_diff_iter);
1834    }
1835
1836    /// Test that derive_pbm_key rejects zero iteration count.
1837    #[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 that derive_pbm_key rejects excessively large iteration count.
1845    #[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 that derive_pbm_key rejects unsupported OWF algorithms.
1853    #[test]
1854    fn pbm_key_derivation_rejects_unknown_owf() {
1855        // Use a bogus OID.
1856        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    /// End-to-end MAC verification: derive key + compute HMAC + verify.
1862    ///
1863    /// This simulates the complete PBM verification flow:
1864    /// 1. Derive the MAC key from a shared secret using SHA-256 OWF
1865    /// 2. Compute HMAC-SHA-256 over the protected bytes
1866    /// 3. Verify the MAC matches via constant-time comparison
1867    #[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        // SHA-256 OWF
1877        let owf_alg = build_alg_id(&[2, 16, 840, 1, 101, 3, 4, 2, 1]);
1878        // HMAC-SHA-256 MAC (OID 1.2.840.113549.2.9)
1879        let mac_alg = build_alg_id(&[1, 2, 840, 113549, 2, 9]);
1880
1881        // Derive key.
1882        let key = derive_pbm_key(secret, salt, &owf_alg, iteration_count).unwrap();
1883
1884        // Compute MAC.
1885        let mac = compute_pbm_hmac(&key, data, &mac_alg).unwrap();
1886
1887        // HMAC-SHA-256 produces 32 bytes.
1888        assert_eq!(mac.len(), 32);
1889
1890        // Verify: same inputs produce the same MAC.
1891        let mac2 = compute_pbm_hmac(&key, data, &mac_alg).unwrap();
1892        assert!(bool::from(mac.ct_eq(&mac2)));
1893    }
1894
1895    /// MAC verification with wrong secret produces a different MAC.
1896    #[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        // Correct secret.
1908        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        // Wrong secret.
1913        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        // MACs must differ.
1917        assert!(!bool::from(mac_correct.ct_eq(&mac_wrong)));
1918    }
1919
1920    /// MAC verification rejects unsupported MAC algorithms.
1921    #[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        // Use a bogus MAC OID.
1927        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 SHA-384 OWF key derivation.
1933    #[test]
1934    fn pbm_key_derivation_sha384() {
1935        // SHA-384 OID: 2.16.840.1.101.3.4.2.2
1936        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        // SHA-384 produces 48 bytes.
1940        assert_eq!(key.len(), 48);
1941    }
1942
1943    /// Test SHA-512 OWF key derivation.
1944    #[test]
1945    fn pbm_key_derivation_sha512() {
1946        // SHA-512 OID: 2.16.840.1.101.3.4.2.3
1947        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        // SHA-512 produces 64 bytes.
1951        assert_eq!(key.len(), 64);
1952    }
1953
1954    /// Test HMAC-SHA-384 computation.
1955    #[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        // HMAC-SHA-384 OID: 1.2.840.113549.2.10
1961        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        // HMAC-SHA-384 produces 48 bytes.
1965        assert_eq!(mac.len(), 48);
1966    }
1967
1968    /// Test HMAC-SHA-512 computation.
1969    #[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        // HMAC-SHA-512 OID: 1.2.840.113549.2.11
1975        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        // HMAC-SHA-512 produces 64 bytes.
1979        assert_eq!(mac.len(), 64);
1980    }
1981}