Skip to main content

kipuka_est/
enroll.rs

1//! Simple enrollment per RFC 7030 §4.2.
2//!
3//! The `/simpleenroll` operation accepts a PKCS#10 CSR and returns a PKCS#7
4//! certificate chain. Supports ML-DSA and ML-KEM CSRs with proof-of-possession.
5//!
6//! CSR wire format follows RFC 2986 (PKCS#10 Certification Request Syntax
7//! Specification v1.7). The [`CertificationRequest`] struct formalizes the
8//! three-part ASN.1 structure: CertificationRequestInfo, signatureAlgorithm,
9//! and signature.
10
11use crate::{EstError, EstResult};
12use base64::Engine;
13use serde::{Deserialize, Serialize};
14
15/// ML-DSA algorithm OIDs per FIPS 204.
16pub mod ml_dsa_oids {
17    /// ML-DSA-44 (2.16.840.1.101.3.4.3.17)
18    pub const ML_DSA_44: &str = "2.16.840.1.101.3.4.3.17";
19    /// ML-DSA-65 (2.16.840.1.101.3.4.3.18)
20    pub const ML_DSA_65: &str = "2.16.840.1.101.3.4.3.18";
21    /// ML-DSA-87 (2.16.840.1.101.3.4.3.19)
22    pub const ML_DSA_87: &str = "2.16.840.1.101.3.4.3.19";
23}
24
25/// ML-KEM algorithm OIDs per FIPS 203.
26pub mod ml_kem_oids {
27    /// ML-KEM-512 (2.16.840.1.101.3.4.4.1)
28    pub const ML_KEM_512: &str = "2.16.840.1.101.3.4.4.1";
29    /// ML-KEM-768 (2.16.840.1.101.3.4.4.2)
30    pub const ML_KEM_768: &str = "2.16.840.1.101.3.4.4.2";
31    /// ML-KEM-1024 (2.16.840.1.101.3.4.4.3)
32    pub const ML_KEM_1024: &str = "2.16.840.1.101.3.4.4.3";
33}
34
35/// Traditional key algorithm OIDs.
36pub mod traditional_oids {
37    /// RSA encryption (1.2.840.113549.1.1.1) per RFC 8017.
38    pub const RSA: &str = "1.2.840.113549.1.1.1";
39    /// EC public key (1.2.840.10045.2.1) per RFC 5480.
40    pub const EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
41}
42
43/// Named curve OIDs for ECDSA.
44pub mod named_curve_oids {
45    /// P-256 / secp256r1 (1.2.840.10045.3.1.7) per RFC 5480.
46    pub const P256: &str = "1.2.840.10045.3.1.7";
47    /// P-384 / secp384r1 (1.3.132.0.34) per RFC 5480.
48    pub const P384: &str = "1.3.132.0.34";
49}
50
51/// Composite ML-DSA OID base arc (2.16.840.1.114027.80.5.2).
52///
53/// Sub-arcs 37-54 define various composite ML-DSA + traditional combinations.
54pub const COMPOSITE_ML_DSA_BASE: &str = "2.16.840.1.114027.80.5.2";
55
56/// Key algorithm detected from SubjectPublicKeyInfo in a CSR.
57///
58/// Per RFC 2986 §4.1, the SubjectPublicKeyInfo field contains an
59/// AlgorithmIdentifier that specifies the key algorithm and any
60/// algorithm-specific parameters (e.g., named curves for ECDSA).
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum KeyAlgorithm {
63    /// RSA (OID 1.2.840.113549.1.1.1).
64    Rsa,
65    /// ECDSA P-256 (OID 1.2.840.10045.2.1 + namedCurve 1.2.840.10045.3.1.7).
66    EcdsaP256,
67    /// ECDSA P-384 (OID 1.2.840.10045.2.1 + namedCurve 1.3.132.0.34).
68    EcdsaP384,
69    /// ML-DSA-44 (OID 2.16.840.1.101.3.4.3.17) per FIPS 204.
70    MlDsa44,
71    /// ML-DSA-65 (OID 2.16.840.1.101.3.4.3.18) per FIPS 204.
72    MlDsa65,
73    /// ML-DSA-87 (OID 2.16.840.1.101.3.4.3.19) per FIPS 204.
74    MlDsa87,
75    /// ML-KEM-512 (OID 2.16.840.1.101.3.4.4.1) per FIPS 203.
76    MlKem512,
77    /// ML-KEM-768 (OID 2.16.840.1.101.3.4.4.2) per FIPS 203.
78    MlKem768,
79    /// ML-KEM-1024 (OID 2.16.840.1.101.3.4.4.3) per FIPS 203.
80    MlKem1024,
81    /// Unknown algorithm with the given OID string.
82    Unknown(String),
83}
84
85impl KeyAlgorithm {
86    /// Parse a key algorithm from its OID string.
87    ///
88    /// For EC keys, the caller must also supply the named curve OID
89    /// via [`KeyAlgorithm::from_ec_oid`].
90    pub fn from_oid(oid: &str) -> Self {
91        match oid {
92            "1.2.840.113549.1.1.1" => Self::Rsa,
93            "1.2.840.10045.2.1" => Self::EcdsaP256, // default; caller refines via from_ec_oid
94            "2.16.840.1.101.3.4.3.17" => Self::MlDsa44,
95            "2.16.840.1.101.3.4.3.18" => Self::MlDsa65,
96            "2.16.840.1.101.3.4.3.19" => Self::MlDsa87,
97            "2.16.840.1.101.3.4.4.1" => Self::MlKem512,
98            "2.16.840.1.101.3.4.4.2" => Self::MlKem768,
99            "2.16.840.1.101.3.4.4.3" => Self::MlKem1024,
100            other => Self::Unknown(other.to_string()),
101        }
102    }
103
104    /// Refine an EC public key algorithm using the named curve OID.
105    ///
106    /// Per RFC 5480 §2.1.1, the AlgorithmIdentifier for EC keys includes
107    /// the namedCurve parameter that identifies the specific curve.
108    pub fn from_ec_oid(curve_oid: &str) -> Self {
109        match curve_oid {
110            "1.2.840.10045.3.1.7" => Self::EcdsaP256,
111            "1.3.132.0.34" => Self::EcdsaP384,
112            other => Self::Unknown(format!("ec-unknown-curve:{other}")),
113        }
114    }
115
116    /// Returns the OID string for this algorithm.
117    pub fn oid(&self) -> &str {
118        match self {
119            Self::Rsa => "1.2.840.113549.1.1.1",
120            Self::EcdsaP256 | Self::EcdsaP384 => "1.2.840.10045.2.1",
121            Self::MlDsa44 => "2.16.840.1.101.3.4.3.17",
122            Self::MlDsa65 => "2.16.840.1.101.3.4.3.18",
123            Self::MlDsa87 => "2.16.840.1.101.3.4.3.19",
124            Self::MlKem512 => "2.16.840.1.101.3.4.4.1",
125            Self::MlKem768 => "2.16.840.1.101.3.4.4.2",
126            Self::MlKem1024 => "2.16.840.1.101.3.4.4.3",
127            Self::Unknown(oid) => oid.as_str(),
128        }
129    }
130}
131
132/// Parsed PKCS#10 Certification Request per RFC 2986 §4.
133///
134/// ```text
135/// CertificationRequest ::= SEQUENCE {
136///     certificationRequestInfo  CertificationRequestInfo,
137///     signatureAlgorithm        AlgorithmIdentifier{{ SignatureAlgorithms }},
138///     signature                 BIT STRING
139/// }
140/// ```
141///
142/// This struct represents the logical structure of a parsed CSR. The actual
143/// DER parsing is performed by the CA module using the `synta` crate; this
144/// struct captures the extracted fields for EST protocol-level processing.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct CertificationRequest {
147    /// CSR version (0 = v1 per RFC 2986 §4.1).
148    pub version: u8,
149    /// Subject distinguished name (e.g., "CN=example.com,O=ACME,C=US").
150    pub subject: String,
151    /// Key algorithm from SubjectPublicKeyInfo.
152    pub key_algorithm: KeyAlgorithm,
153    /// DER-encoded SubjectPublicKeyInfo.
154    pub subject_public_key_info: Vec<u8>,
155    /// Signature algorithm OID (e.g., ML-DSA-65, sha256WithRSAEncryption).
156    pub signature_algorithm: String,
157    /// DER-encoded signature BIT STRING value.
158    pub signature: Vec<u8>,
159    /// Subject Alternative Names extracted from the extensionRequest attribute
160    /// (OID 1.2.840.113549.1.9.14) per RFC 2986 §4.1 and RFC 5280 §4.2.1.6.
161    pub subject_alt_names: Vec<String>,
162    /// Key usage flags from the extensionRequest attribute, if present.
163    pub key_usage: Vec<String>,
164    /// ChallengePassword attribute (OID 1.2.840.113549.1.9.7) per RFC 2986 §4.1.
165    ///
166    /// When present, this carries a shared secret (e.g., OTP) for binding the
167    /// CSR to a pre-authorized enrollment. See also RFC 7030 §3.2.3.
168    pub challenge_password: Option<String>,
169    /// Raw DER of the CertificationRequestInfo for signature verification.
170    pub tbs_der: Vec<u8>,
171}
172
173impl CertificationRequest {
174    /// Verify the CSR self-signature over CertificationRequestInfo.
175    ///
176    /// RFC 2986 §3: "The signature process consists of two steps:
177    /// 1. The value of the certificationRequestInfo component is DER encoded,
178    ///    producing an octet string.
179    /// 2. The result of step 1 is signed with the certification request
180    ///    subject's private key under the specified signature algorithm."
181    ///
182    /// This method validates that the signature was produced by the private key
183    /// corresponding to the public key in `subject_public_key_info`. Full
184    /// cryptographic verification is delegated to the CA module.
185    pub fn verify_self_signature(&self) -> EstResult<()> {
186        if self.tbs_der.is_empty() {
187            return Err(EstError::InvalidPkcs10(
188                "empty CertificationRequestInfo for signature verification".to_string(),
189            ));
190        }
191        if self.signature.is_empty() {
192            return Err(EstError::InvalidPkcs10(
193                "empty signature in CSR".to_string(),
194            ));
195        }
196        // Cryptographic verification delegated to CA module which has access
197        // to the full ASN.1 parser and crypto primitives.
198        Ok(())
199    }
200
201    /// Validate the challengePassword attribute if present.
202    ///
203    /// Per RFC 2986 §4.1 the challengePassword attribute (OID 1.2.840.113549.1.9.7)
204    /// carries a password for identity verification. When used with EST OTP
205    /// binding, this password must match the pre-provisioned OTP.
206    pub fn validate_challenge_password(&self, expected: &str) -> EstResult<()> {
207        match &self.challenge_password {
208            Some(pw) if pw == expected => Ok(()),
209            Some(pw) => Err(EstError::InvalidPop(format!(
210                "challengePassword mismatch: expected {expected:?}, got {pw:?}",
211            ))),
212            None => Err(EstError::MissingField(
213                "challengePassword attribute".to_string(),
214            )),
215        }
216    }
217}
218
219/// Enrollment request containing a PKCS#10 CSR (RFC 7030 §4.2.1).
220///
221/// The CSR must include:
222/// - Subject public key (ML-DSA, ML-KEM, or traditional)
223/// - Subject distinguished name
224/// - Signature proving possession of the private key (POP)
225///
226/// For ML-DSA CSRs, the signature algorithm OID indicates the ML-DSA level.
227/// For ML-KEM CSRs, a separate ML-DSA signature is required for POP.
228///
229/// The wire format is a DER-encoded CertificationRequest per RFC 2986.
230/// Use [`CertificationRequest`] for parsed/structured access to CSR fields.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct EnrollRequest {
233    /// PKCS#10 CSR in DER encoding.
234    #[serde(with = "serde_bytes")]
235    csr_der: Vec<u8>,
236}
237
238impl EnrollRequest {
239    /// Creates a new enrollment request from a DER-encoded PKCS#10 CSR.
240    pub fn new(csr_der: Vec<u8>) -> Self {
241        Self { csr_der }
242    }
243
244    /// Returns the raw DER-encoded CSR.
245    pub fn csr_der(&self) -> &[u8] {
246        &self.csr_der
247    }
248
249    /// Consumes self and returns the DER-encoded CSR.
250    pub fn into_csr_der(self) -> Vec<u8> {
251        self.csr_der
252    }
253
254    /// Encodes the request as base64 for HTTP transport.
255    pub fn to_base64(&self) -> String {
256        base64::engine::general_purpose::STANDARD.encode(&self.csr_der)
257    }
258
259    /// Decodes a base64-encoded enrollment request.
260    pub fn from_base64(base64_data: &str) -> EstResult<Self> {
261        let csr_der = base64::engine::general_purpose::STANDARD
262            .decode(base64_data)
263            .map_err(|e| EstError::InvalidBase64(e.to_string()))?;
264
265        Ok(Self::new(csr_der))
266    }
267
268    /// Validates the CSR structure and proof-of-possession.
269    ///
270    /// This performs basic DER validation. Full cryptographic validation
271    /// (signature verification) is delegated to the CA module.
272    pub fn validate(&self) -> EstResult<()> {
273        if self.csr_der.is_empty() {
274            return Err(EstError::InvalidPkcs10("Empty CSR".to_string()));
275        }
276
277        // Basic DER sanity: must start with SEQUENCE tag (0x30)
278        if self.csr_der[0] != 0x30 {
279            return Err(EstError::InvalidPkcs10(
280                "Invalid DER: expected SEQUENCE tag".to_string(),
281            ));
282        }
283
284        // Minimum viable CSR is ~200 bytes
285        if self.csr_der.len() < 100 {
286            return Err(EstError::InvalidPkcs10(format!(
287                "CSR too small: {} bytes",
288                self.csr_der.len()
289            )));
290        }
291
292        Ok(())
293    }
294
295    /// Detects the signature algorithm OID from the CSR.
296    ///
297    /// Returns the OID string if found, or `None` if parsing fails.
298    /// This is used to route CSRs to the appropriate CA signing key
299    /// (ML-DSA-44/65/87, composite, or traditional).
300    ///
301    /// Note: This is a simple heuristic parser. Full ASN.1 parsing
302    /// is performed by the CA module.
303    pub fn detect_signature_algorithm(&self) -> Option<String> {
304        // Simplified OID detection - in production, use proper ASN.1 parser
305        // For now, return None to indicate "needs full parsing"
306        None
307    }
308
309    /// Checks if the CSR appears to contain an ML-DSA public key.
310    ///
311    /// Searches for ML-DSA OID prefixes in the DER structure.
312    pub fn contains_ml_dsa(&self) -> bool {
313        let ml_dsa_prefix = b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x03"; // OID prefix for ML-DSA
314        self.csr_der
315            .windows(ml_dsa_prefix.len())
316            .any(|w| w == ml_dsa_prefix)
317    }
318
319    /// Checks if the CSR appears to contain an ML-KEM public key.
320    ///
321    /// Searches for ML-KEM OID prefixes in the DER structure.
322    pub fn contains_ml_kem(&self) -> bool {
323        let ml_kem_prefix = b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x04"; // OID prefix for ML-KEM
324        self.csr_der
325            .windows(ml_kem_prefix.len())
326            .any(|w| w == ml_kem_prefix)
327    }
328
329    /// Returns a parsed [`CertificationRequest`] from the DER-encoded CSR.
330    ///
331    /// This is a placeholder that creates a `CertificationRequest` with
332    /// default/empty fields. Full ASN.1 parsing of the RFC 2986 structure
333    /// is performed by the CA module using `synta`.
334    ///
335    /// Callers should use this to get the struct, then have the CA module
336    /// populate the fields from actual DER parsing.
337    pub fn to_certification_request(&self) -> CertificationRequest {
338        CertificationRequest {
339            version: 0, // v1 per RFC 2986 §4.1
340            subject: String::new(),
341            key_algorithm: KeyAlgorithm::Unknown("unparsed".to_string()),
342            subject_public_key_info: Vec::new(),
343            signature_algorithm: String::new(),
344            signature: Vec::new(),
345            subject_alt_names: Vec::new(),
346            key_usage: Vec::new(),
347            challenge_password: None,
348            tbs_der: self.csr_der.clone(),
349        }
350    }
351}
352
353/// Enrollment response containing a PKCS#7 certificate chain (RFC 7030 §4.2.3).
354///
355/// The response includes:
356/// - End-entity certificate (signed by CA with ML-DSA or traditional key)
357/// - Intermediate CA certificates (optional)
358/// - Root CA certificate (optional)
359///
360/// The certificate chain is returned as a PKCS#7 `certs-only` structure.
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct EnrollResponse {
363    /// PKCS#7 certs-only message in DER encoding.
364    #[serde(with = "serde_bytes")]
365    pkcs7_der: Vec<u8>,
366}
367
368impl EnrollResponse {
369    /// Creates a new enrollment response from DER-encoded PKCS#7.
370    pub fn new(pkcs7_der: Vec<u8>) -> Self {
371        Self { pkcs7_der }
372    }
373
374    /// Returns the raw DER-encoded PKCS#7 data.
375    pub fn pkcs7_der(&self) -> &[u8] {
376        &self.pkcs7_der
377    }
378
379    /// Consumes self and returns the DER-encoded PKCS#7 data.
380    pub fn into_pkcs7_der(self) -> Vec<u8> {
381        self.pkcs7_der
382    }
383
384    /// Encodes the response as base64 for HTTP transport.
385    pub fn to_base64(&self) -> String {
386        base64::engine::general_purpose::STANDARD.encode(&self.pkcs7_der)
387    }
388
389    /// Decodes a base64-encoded enrollment response.
390    pub fn from_base64(base64_data: &str) -> EstResult<Self> {
391        let pkcs7_der = base64::engine::general_purpose::STANDARD
392            .decode(base64_data)
393            .map_err(|e| EstError::InvalidBase64(e.to_string()))?;
394
395        Ok(Self::new(pkcs7_der))
396    }
397
398    /// Validates the PKCS#7 structure.
399    pub fn validate(&self) -> EstResult<()> {
400        if self.pkcs7_der.is_empty() {
401            return Err(EstError::InvalidPkcs7("Empty PKCS#7 structure".to_string()));
402        }
403
404        if self.pkcs7_der[0] != 0x30 {
405            return Err(EstError::InvalidPkcs7(
406                "Invalid DER: expected SEQUENCE tag".to_string(),
407            ));
408        }
409
410        if self.pkcs7_der.len() < 100 {
411            return Err(EstError::InvalidPkcs7(format!(
412                "PKCS#7 too small: {} bytes",
413                self.pkcs7_der.len()
414            )));
415        }
416
417        Ok(())
418    }
419}
420
421/// Helper module for serde byte serialization.
422mod serde_bytes {
423    use serde::{Deserialize, Deserializer, Serializer};
424
425    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
426    where
427        S: Serializer,
428    {
429        serializer.serialize_bytes(bytes)
430    }
431
432    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
433    where
434        D: Deserializer<'de>,
435    {
436        Vec::<u8>::deserialize(deserializer)
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_enroll_request_roundtrip() {
446        let der = vec![0x30, 0x82, 0x01, 0x00]; // SEQUENCE
447        let mut full_der = der.clone();
448        full_der.extend(vec![0x00; 252]); // Pad to 256 bytes
449
450        let request = EnrollRequest::new(full_der.clone());
451        assert_eq!(request.csr_der(), &full_der);
452
453        let base64 = request.to_base64();
454        let decoded = EnrollRequest::from_base64(&base64).unwrap();
455        assert_eq!(decoded.csr_der(), &full_der);
456    }
457
458    #[test]
459    fn test_enroll_response_roundtrip() {
460        let der = vec![0x30, 0x82, 0x01, 0x00];
461        let mut full_der = der.clone();
462        full_der.extend(vec![0x00; 252]);
463
464        let response = EnrollResponse::new(full_der.clone());
465        assert_eq!(response.pkcs7_der(), &full_der);
466
467        let base64 = response.to_base64();
468        let decoded = EnrollResponse::from_base64(&base64).unwrap();
469        assert_eq!(decoded.pkcs7_der(), &full_der);
470    }
471
472    #[test]
473    fn test_validate_csr() {
474        let mut der = vec![0x30, 0x82, 0x01, 0x00];
475        der.extend(vec![0x00; 252]);
476        let request = EnrollRequest::new(der);
477        assert!(request.validate().is_ok());
478    }
479
480    #[test]
481    fn test_validate_empty_csr() {
482        let request = EnrollRequest::new(vec![]);
483        assert!(matches!(
484            request.validate(),
485            Err(EstError::InvalidPkcs10(_))
486        ));
487    }
488
489    #[test]
490    fn test_ml_dsa_oids() {
491        assert_eq!(ml_dsa_oids::ML_DSA_44, "2.16.840.1.101.3.4.3.17");
492        assert_eq!(ml_dsa_oids::ML_DSA_65, "2.16.840.1.101.3.4.3.18");
493        assert_eq!(ml_dsa_oids::ML_DSA_87, "2.16.840.1.101.3.4.3.19");
494    }
495
496    #[test]
497    fn test_ml_kem_oids() {
498        assert_eq!(ml_kem_oids::ML_KEM_512, "2.16.840.1.101.3.4.4.1");
499        assert_eq!(ml_kem_oids::ML_KEM_768, "2.16.840.1.101.3.4.4.2");
500        assert_eq!(ml_kem_oids::ML_KEM_1024, "2.16.840.1.101.3.4.4.3");
501    }
502
503    #[test]
504    fn test_contains_ml_dsa() {
505        // Mock DER with ML-DSA OID prefix
506        let mut der = vec![0x30, 0x82, 0x01, 0x00];
507        der.extend_from_slice(b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x03\x11"); // ML-DSA-44 OID
508        der.extend(vec![0x00; 240]);
509
510        let request = EnrollRequest::new(der);
511        assert!(request.contains_ml_dsa());
512        assert!(!request.contains_ml_kem());
513    }
514
515    #[test]
516    fn test_contains_ml_kem() {
517        // Mock DER with ML-KEM OID prefix
518        let mut der = vec![0x30, 0x82, 0x01, 0x00];
519        der.extend_from_slice(b"\x06\x0b\x60\x86\x48\x01\x65\x03\x04\x04\x01"); // ML-KEM-512 OID
520        der.extend(vec![0x00; 240]);
521
522        let request = EnrollRequest::new(der);
523        assert!(!request.contains_ml_dsa());
524        assert!(request.contains_ml_kem());
525    }
526
527    #[test]
528    fn test_key_algorithm_from_oid() {
529        assert_eq!(
530            KeyAlgorithm::from_oid("1.2.840.113549.1.1.1"),
531            KeyAlgorithm::Rsa
532        );
533        assert_eq!(
534            KeyAlgorithm::from_oid("2.16.840.1.101.3.4.3.17"),
535            KeyAlgorithm::MlDsa44
536        );
537        assert_eq!(
538            KeyAlgorithm::from_oid("2.16.840.1.101.3.4.3.18"),
539            KeyAlgorithm::MlDsa65
540        );
541        assert_eq!(
542            KeyAlgorithm::from_oid("2.16.840.1.101.3.4.3.19"),
543            KeyAlgorithm::MlDsa87
544        );
545        assert_eq!(
546            KeyAlgorithm::from_oid("2.16.840.1.101.3.4.4.1"),
547            KeyAlgorithm::MlKem512
548        );
549        assert_eq!(
550            KeyAlgorithm::from_oid("2.16.840.1.101.3.4.4.2"),
551            KeyAlgorithm::MlKem768
552        );
553        assert_eq!(
554            KeyAlgorithm::from_oid("2.16.840.1.101.3.4.4.3"),
555            KeyAlgorithm::MlKem1024
556        );
557        assert!(matches!(
558            KeyAlgorithm::from_oid("1.2.3"),
559            KeyAlgorithm::Unknown(_)
560        ));
561    }
562
563    #[test]
564    fn test_key_algorithm_ec_curves() {
565        assert_eq!(
566            KeyAlgorithm::from_ec_oid("1.2.840.10045.3.1.7"),
567            KeyAlgorithm::EcdsaP256
568        );
569        assert_eq!(
570            KeyAlgorithm::from_ec_oid("1.3.132.0.34"),
571            KeyAlgorithm::EcdsaP384
572        );
573        assert!(matches!(
574            KeyAlgorithm::from_ec_oid("1.2.3.4"),
575            KeyAlgorithm::Unknown(_)
576        ));
577    }
578
579    #[test]
580    fn test_certification_request_verify_empty_tbs() {
581        let cr = CertificationRequest {
582            version: 0,
583            subject: String::new(),
584            key_algorithm: KeyAlgorithm::Rsa,
585            subject_public_key_info: Vec::new(),
586            signature_algorithm: String::new(),
587            signature: vec![0x00],
588            subject_alt_names: Vec::new(),
589            key_usage: Vec::new(),
590            challenge_password: None,
591            tbs_der: Vec::new(),
592        };
593        assert!(matches!(
594            cr.verify_self_signature(),
595            Err(EstError::InvalidPkcs10(_))
596        ));
597    }
598
599    #[test]
600    fn test_certification_request_verify_empty_signature() {
601        let cr = CertificationRequest {
602            version: 0,
603            subject: String::new(),
604            key_algorithm: KeyAlgorithm::Rsa,
605            subject_public_key_info: Vec::new(),
606            signature_algorithm: String::new(),
607            signature: Vec::new(),
608            subject_alt_names: Vec::new(),
609            key_usage: Vec::new(),
610            challenge_password: None,
611            tbs_der: vec![0x30, 0x00],
612        };
613        assert!(matches!(
614            cr.verify_self_signature(),
615            Err(EstError::InvalidPkcs10(_))
616        ));
617    }
618
619    #[test]
620    fn test_challenge_password_validation() {
621        let cr = CertificationRequest {
622            version: 0,
623            subject: String::new(),
624            key_algorithm: KeyAlgorithm::Rsa,
625            subject_public_key_info: Vec::new(),
626            signature_algorithm: String::new(),
627            signature: vec![0x00],
628            subject_alt_names: Vec::new(),
629            key_usage: Vec::new(),
630            challenge_password: Some("secret123".to_string()),
631            tbs_der: vec![0x30, 0x00],
632        };
633        assert!(cr.validate_challenge_password("secret123").is_ok());
634        assert!(matches!(
635            cr.validate_challenge_password("wrong"),
636            Err(EstError::InvalidPop(_))
637        ));
638    }
639
640    #[test]
641    fn test_challenge_password_missing() {
642        let cr = CertificationRequest {
643            version: 0,
644            subject: String::new(),
645            key_algorithm: KeyAlgorithm::Rsa,
646            subject_public_key_info: Vec::new(),
647            signature_algorithm: String::new(),
648            signature: vec![0x00],
649            subject_alt_names: Vec::new(),
650            key_usage: Vec::new(),
651            challenge_password: None,
652            tbs_der: vec![0x30, 0x00],
653        };
654        assert!(matches!(
655            cr.validate_challenge_password("anything"),
656            Err(EstError::MissingField(_))
657        ));
658    }
659
660    #[test]
661    fn test_to_certification_request() {
662        let mut der = vec![0x30, 0x82, 0x01, 0x00];
663        der.extend(vec![0x00; 252]);
664        let request = EnrollRequest::new(der.clone());
665        let cr = request.to_certification_request();
666        assert_eq!(cr.version, 0);
667        assert_eq!(cr.tbs_der, der);
668    }
669
670    #[test]
671    fn test_traditional_oids() {
672        assert_eq!(traditional_oids::RSA, "1.2.840.113549.1.1.1");
673        assert_eq!(traditional_oids::EC_PUBLIC_KEY, "1.2.840.10045.2.1");
674    }
675
676    #[test]
677    fn test_named_curve_oids() {
678        assert_eq!(named_curve_oids::P256, "1.2.840.10045.3.1.7");
679        assert_eq!(named_curve_oids::P384, "1.3.132.0.34");
680    }
681}