1use crate::{EstError, EstResult};
8use base64::Engine;
9use serde::{Deserialize, Serialize};
10
11pub mod ml_dsa_oids {
13 pub const ML_DSA_44: &str = "2.16.840.1.101.3.4.3.17";
15 pub const ML_DSA_65: &str = "2.16.840.1.101.3.4.3.18";
17 pub const ML_DSA_87: &str = "2.16.840.1.101.3.4.3.19";
19}
20
21pub mod ml_kem_oids {
23 pub const ML_KEM_512: &str = "2.16.840.1.101.3.4.4.1";
25 pub const ML_KEM_768: &str = "2.16.840.1.101.3.4.4.2";
27 pub const ML_KEM_1024: &str = "2.16.840.1.101.3.4.4.3";
29}
30
31pub mod composite_ml_dsa_oids {
35 pub const BASE: &str = "2.16.840.1.114027.80.5.2";
37
38 pub const ML_DSA_44_RSA_2048: &str = "2.16.840.1.114027.80.5.2.37";
40 pub const ML_DSA_65_RSA_3072: &str = "2.16.840.1.114027.80.5.2.38";
42 pub const ML_DSA_87_RSA_4096: &str = "2.16.840.1.114027.80.5.2.39";
44
45 pub const ML_DSA_44_ECDSA_P256: &str = "2.16.840.1.114027.80.5.2.40";
47 pub const ML_DSA_65_ECDSA_P384: &str = "2.16.840.1.114027.80.5.2.41";
49 pub const ML_DSA_87_ECDSA_P521: &str = "2.16.840.1.114027.80.5.2.42";
51
52 pub const ML_DSA_44_ED25519: &str = "2.16.840.1.114027.80.5.2.43";
54 pub const ML_DSA_65_ED448: &str = "2.16.840.1.114027.80.5.2.44";
56}
57
58pub mod x509_attr_oids {
60 pub const CHALLENGE_PASSWORD: &str = "1.2.840.113549.1.9.7";
62 pub const UNSTRUCTURED_NAME: &str = "1.2.840.113549.1.9.8";
64 pub const EXTENSION_REQUEST: &str = "1.2.840.113549.1.9.14";
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77pub struct CsrAttribute {
78 pub oid: String,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub description: Option<String>,
84}
85
86impl CsrAttribute {
87 pub fn new(oid: impl Into<String>) -> Self {
89 Self {
90 oid: oid.into(),
91 description: None,
92 }
93 }
94
95 pub fn with_description(oid: impl Into<String>, description: impl Into<String>) -> Self {
97 Self {
98 oid: oid.into(),
99 description: Some(description.into()),
100 }
101 }
102
103 pub fn ml_dsa_44() -> Self {
105 Self::with_description(ml_dsa_oids::ML_DSA_44, "ML-DSA-44 (FIPS 204)")
106 }
107
108 pub fn ml_dsa_65() -> Self {
110 Self::with_description(ml_dsa_oids::ML_DSA_65, "ML-DSA-65 (FIPS 204)")
111 }
112
113 pub fn ml_dsa_87() -> Self {
115 Self::with_description(ml_dsa_oids::ML_DSA_87, "ML-DSA-87 (FIPS 204)")
116 }
117
118 pub fn ml_kem_512() -> Self {
120 Self::with_description(ml_kem_oids::ML_KEM_512, "ML-KEM-512 (FIPS 203)")
121 }
122
123 pub fn ml_kem_768() -> Self {
125 Self::with_description(ml_kem_oids::ML_KEM_768, "ML-KEM-768 (FIPS 203)")
126 }
127
128 pub fn ml_kem_1024() -> Self {
130 Self::with_description(ml_kem_oids::ML_KEM_1024, "ML-KEM-1024 (FIPS 203)")
131 }
132
133 pub fn composite_ml_dsa_65_ecdsa_p384() -> Self {
135 Self::with_description(
136 composite_ml_dsa_oids::ML_DSA_65_ECDSA_P384,
137 "Composite ML-DSA-65 + ECDSA-P384",
138 )
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153pub struct CsrAttrsResponse {
154 attributes: Vec<CsrAttribute>,
156
157 #[serde(skip)]
159 der_cache: Option<Vec<u8>>,
160}
161
162impl CsrAttrsResponse {
163 pub fn new(attributes: Vec<CsrAttribute>) -> Self {
165 Self {
166 attributes,
167 der_cache: None,
168 }
169 }
170
171 pub fn empty() -> Self {
173 Self::new(vec![])
174 }
175
176 pub fn attributes(&self) -> &[CsrAttribute] {
178 &self.attributes
179 }
180
181 pub fn add_attribute(&mut self, attr: CsrAttribute) {
183 self.attributes.push(attr);
184 self.der_cache = None; }
186
187 pub fn to_der(&mut self) -> &[u8] {
197 if let Some(ref cached) = self.der_cache {
198 return cached;
199 }
200
201 let der = if self.attributes.is_empty() {
204 vec![0x30, 0x00] } else {
206 vec![0x30, 0x03, 0x06, 0x01, 0x00] };
209
210 self.der_cache = Some(der);
211 self.der_cache.as_ref().unwrap()
212 }
213
214 pub fn to_base64(&mut self) -> String {
216 let der = self.to_der();
217 base64::engine::general_purpose::STANDARD.encode(der)
218 }
219
220 pub fn from_base64(base64_data: &str) -> EstResult<Self> {
225 let der = base64::engine::general_purpose::STANDARD
226 .decode(base64_data)
227 .map_err(|e| EstError::InvalidBase64(e.to_string()))?;
228
229 if der.is_empty() {
231 return Ok(Self::empty());
232 }
233
234 if der[0] != 0x30 {
235 return Err(EstError::InvalidDer("Expected SEQUENCE tag".to_string()));
236 }
237
238 Ok(Self::empty())
241 }
242
243 pub fn validate(&self) -> EstResult<()> {
245 for attr in &self.attributes {
247 if attr.oid.is_empty() {
248 return Err(EstError::MissingField("OID".to_string()));
249 }
250
251 if !attr.oid.chars().all(|c| c.is_ascii_digit() || c == '.') {
253 return Err(EstError::Protocol(format!("Invalid OID: {}", attr.oid)));
254 }
255 }
256
257 Ok(())
258 }
259
260 pub fn builder() -> CsrAttrsBuilder {
262 CsrAttrsBuilder::new()
263 }
264}
265
266#[derive(Debug, Clone, Default)]
268pub struct CsrAttrsBuilder {
269 attributes: Vec<CsrAttribute>,
270}
271
272impl CsrAttrsBuilder {
273 pub fn new() -> Self {
275 Self::default()
276 }
277
278 pub fn add_attribute(mut self, attr: CsrAttribute) -> Self {
280 self.attributes.push(attr);
281 self
282 }
283
284 pub fn add_oid(mut self, oid: impl Into<String>) -> Self {
286 self.attributes.push(CsrAttribute::new(oid));
287 self
288 }
289
290 pub fn with_all_ml_dsa(mut self) -> Self {
292 self.attributes.push(CsrAttribute::ml_dsa_44());
293 self.attributes.push(CsrAttribute::ml_dsa_65());
294 self.attributes.push(CsrAttribute::ml_dsa_87());
295 self
296 }
297
298 pub fn with_all_ml_kem(mut self) -> Self {
300 self.attributes.push(CsrAttribute::ml_kem_512());
301 self.attributes.push(CsrAttribute::ml_kem_768());
302 self.attributes.push(CsrAttribute::ml_kem_1024());
303 self
304 }
305
306 pub fn with_all_pqc(self) -> Self {
308 self.with_all_ml_dsa().with_all_ml_kem()
309 }
310
311 pub fn with_composite_ml_dsa(mut self) -> Self {
313 self.attributes.push(CsrAttribute::with_description(
314 composite_ml_dsa_oids::ML_DSA_65_RSA_3072,
315 "Composite ML-DSA-65 + RSA-3072",
316 ));
317 self.attributes
318 .push(CsrAttribute::composite_ml_dsa_65_ecdsa_p384());
319 self
320 }
321
322 pub fn with_standard_attrs(mut self) -> Self {
324 self.attributes.push(CsrAttribute::with_description(
325 x509_attr_oids::CHALLENGE_PASSWORD,
326 "challengePassword",
327 ));
328 self.attributes.push(CsrAttribute::with_description(
329 x509_attr_oids::EXTENSION_REQUEST,
330 "extensionRequest",
331 ));
332 self
333 }
334
335 pub fn build(self) -> CsrAttrsResponse {
337 CsrAttrsResponse::new(self.attributes)
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_csr_attribute_creation() {
347 let attr = CsrAttribute::new("1.2.3.4.5");
348 assert_eq!(attr.oid, "1.2.3.4.5");
349 assert!(attr.description.is_none());
350
351 let attr = CsrAttribute::with_description("1.2.3.4.5", "Test OID");
352 assert_eq!(attr.oid, "1.2.3.4.5");
353 assert_eq!(attr.description.as_deref(), Some("Test OID"));
354 }
355
356 #[test]
357 fn test_ml_dsa_attributes() {
358 let attr = CsrAttribute::ml_dsa_44();
359 assert_eq!(attr.oid, ml_dsa_oids::ML_DSA_44);
360 assert!(attr.description.is_some());
361
362 let attr = CsrAttribute::ml_dsa_65();
363 assert_eq!(attr.oid, ml_dsa_oids::ML_DSA_65);
364
365 let attr = CsrAttribute::ml_dsa_87();
366 assert_eq!(attr.oid, ml_dsa_oids::ML_DSA_87);
367 }
368
369 #[test]
370 fn test_ml_kem_attributes() {
371 let attr = CsrAttribute::ml_kem_512();
372 assert_eq!(attr.oid, ml_kem_oids::ML_KEM_512);
373
374 let attr = CsrAttribute::ml_kem_768();
375 assert_eq!(attr.oid, ml_kem_oids::ML_KEM_768);
376
377 let attr = CsrAttribute::ml_kem_1024();
378 assert_eq!(attr.oid, ml_kem_oids::ML_KEM_1024);
379 }
380
381 #[test]
382 fn test_composite_attributes() {
383 let attr = CsrAttribute::composite_ml_dsa_65_ecdsa_p384();
384 assert_eq!(attr.oid, composite_ml_dsa_oids::ML_DSA_65_ECDSA_P384);
385 assert!(attr.description.is_some());
386 }
387
388 #[test]
389 fn test_builder_basic() {
390 let response = CsrAttrsResponse::builder()
391 .add_attribute(CsrAttribute::ml_dsa_65())
392 .add_oid("1.2.3.4.5")
393 .build();
394
395 assert_eq!(response.attributes().len(), 2);
396 assert_eq!(response.attributes()[0].oid, ml_dsa_oids::ML_DSA_65);
397 assert_eq!(response.attributes()[1].oid, "1.2.3.4.5");
398 }
399
400 #[test]
401 fn test_builder_all_ml_dsa() {
402 let response = CsrAttrsResponse::builder().with_all_ml_dsa().build();
403
404 assert_eq!(response.attributes().len(), 3);
405 assert!(
406 response
407 .attributes()
408 .iter()
409 .any(|a| a.oid == ml_dsa_oids::ML_DSA_44)
410 );
411 assert!(
412 response
413 .attributes()
414 .iter()
415 .any(|a| a.oid == ml_dsa_oids::ML_DSA_65)
416 );
417 assert!(
418 response
419 .attributes()
420 .iter()
421 .any(|a| a.oid == ml_dsa_oids::ML_DSA_87)
422 );
423 }
424
425 #[test]
426 fn test_builder_all_ml_kem() {
427 let response = CsrAttrsResponse::builder().with_all_ml_kem().build();
428
429 assert_eq!(response.attributes().len(), 3);
430 assert!(
431 response
432 .attributes()
433 .iter()
434 .any(|a| a.oid == ml_kem_oids::ML_KEM_512)
435 );
436 assert!(
437 response
438 .attributes()
439 .iter()
440 .any(|a| a.oid == ml_kem_oids::ML_KEM_768)
441 );
442 assert!(
443 response
444 .attributes()
445 .iter()
446 .any(|a| a.oid == ml_kem_oids::ML_KEM_1024)
447 );
448 }
449
450 #[test]
451 fn test_builder_all_pqc() {
452 let response = CsrAttrsResponse::builder().with_all_pqc().build();
453
454 assert_eq!(response.attributes().len(), 6);
455 assert!(
456 response
457 .attributes()
458 .iter()
459 .any(|a| a.oid == ml_dsa_oids::ML_DSA_44)
460 );
461 assert!(
462 response
463 .attributes()
464 .iter()
465 .any(|a| a.oid == ml_kem_oids::ML_KEM_512)
466 );
467 }
468
469 #[test]
470 fn test_builder_composite() {
471 let response = CsrAttrsResponse::builder().with_composite_ml_dsa().build();
472
473 assert_eq!(response.attributes().len(), 2);
474 assert!(
475 response
476 .attributes()
477 .iter()
478 .any(|a| a.oid == composite_ml_dsa_oids::ML_DSA_65_RSA_3072)
479 );
480 assert!(
481 response
482 .attributes()
483 .iter()
484 .any(|a| a.oid == composite_ml_dsa_oids::ML_DSA_65_ECDSA_P384)
485 );
486 }
487
488 #[test]
489 fn test_builder_standard_attrs() {
490 let response = CsrAttrsResponse::builder().with_standard_attrs().build();
491
492 assert_eq!(response.attributes().len(), 2);
493 assert!(
494 response
495 .attributes()
496 .iter()
497 .any(|a| a.oid == x509_attr_oids::CHALLENGE_PASSWORD)
498 );
499 assert!(
500 response
501 .attributes()
502 .iter()
503 .any(|a| a.oid == x509_attr_oids::EXTENSION_REQUEST)
504 );
505 }
506
507 #[test]
508 fn test_validate() {
509 let response = CsrAttrsResponse::builder().with_all_pqc().build();
510 assert!(response.validate().is_ok());
511
512 let mut invalid = CsrAttrsResponse::new(vec![CsrAttribute::new("")]);
513 assert!(matches!(invalid.validate(), Err(EstError::MissingField(_))));
514
515 invalid = CsrAttrsResponse::new(vec![CsrAttribute::new("not-an-oid!")]);
516 assert!(matches!(invalid.validate(), Err(EstError::Protocol(_))));
517 }
518
519 #[test]
520 fn test_to_der() {
521 let mut response = CsrAttrsResponse::empty();
522 let der = response.to_der();
523 assert_eq!(der, &[0x30, 0x00]); let mut response = CsrAttrsResponse::builder().add_oid("1.2.3.4.5").build();
526 let der = response.to_der();
527 assert_eq!(der[0], 0x30); }
529
530 #[test]
531 fn test_base64_roundtrip() {
532 let mut response = CsrAttrsResponse::empty();
533 let base64 = response.to_base64();
534 let decoded = CsrAttrsResponse::from_base64(&base64).unwrap();
535 assert_eq!(decoded.attributes().len(), 0);
536 }
537
538 #[test]
539 fn test_add_attribute() {
540 let mut response = CsrAttrsResponse::empty();
541 assert_eq!(response.attributes().len(), 0);
542
543 response.add_attribute(CsrAttribute::ml_dsa_65());
544 assert_eq!(response.attributes().len(), 1);
545
546 response.add_attribute(CsrAttribute::ml_kem_768());
547 assert_eq!(response.attributes().len(), 2);
548 }
549
550 #[test]
551 fn test_composite_oids() {
552 assert_eq!(
553 composite_ml_dsa_oids::ML_DSA_44_RSA_2048,
554 "2.16.840.1.114027.80.5.2.37"
555 );
556 assert_eq!(
557 composite_ml_dsa_oids::ML_DSA_65_ECDSA_P384,
558 "2.16.840.1.114027.80.5.2.41"
559 );
560 assert_eq!(
561 composite_ml_dsa_oids::ML_DSA_44_ED25519,
562 "2.16.840.1.114027.80.5.2.43"
563 );
564 }
565}