1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[non_exhaustive]
18pub struct CryptoProperties {
19 pub asset_type: CryptoAssetType,
21 pub oid: Option<String>,
23 pub algorithm_properties: Option<AlgorithmProperties>,
25 pub certificate_properties: Option<CertificateProperties>,
27 pub related_crypto_material_properties: Option<RelatedCryptoMaterialProperties>,
29 pub protocol_properties: Option<ProtocolProperties>,
31}
32
33impl CryptoProperties {
34 #[must_use]
36 pub fn new(asset_type: CryptoAssetType) -> Self {
37 Self {
38 asset_type,
39 oid: None,
40 algorithm_properties: None,
41 certificate_properties: None,
42 related_crypto_material_properties: None,
43 protocol_properties: None,
44 }
45 }
46
47 #[must_use]
48 pub fn with_oid(mut self, oid: String) -> Self {
49 self.oid = Some(oid);
50 self
51 }
52
53 #[must_use]
54 pub fn with_algorithm_properties(mut self, props: AlgorithmProperties) -> Self {
55 self.algorithm_properties = Some(props);
56 self
57 }
58
59 #[must_use]
60 pub fn with_certificate_properties(mut self, props: CertificateProperties) -> Self {
61 self.certificate_properties = Some(props);
62 self
63 }
64
65 #[must_use]
66 pub fn with_related_crypto_material_properties(
67 mut self,
68 props: RelatedCryptoMaterialProperties,
69 ) -> Self {
70 self.related_crypto_material_properties = Some(props);
71 self
72 }
73
74 #[must_use]
75 pub fn with_protocol_properties(mut self, props: ProtocolProperties) -> Self {
76 self.protocol_properties = Some(props);
77 self
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
85#[non_exhaustive]
86pub enum CryptoAssetType {
87 Algorithm,
88 Certificate,
89 RelatedCryptoMaterial,
90 Protocol,
91 Other(String),
92}
93
94impl std::fmt::Display for CryptoAssetType {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::Algorithm => write!(f, "algorithm"),
98 Self::Certificate => write!(f, "certificate"),
99 Self::RelatedCryptoMaterial => write!(f, "related-crypto-material"),
100 Self::Protocol => write!(f, "protocol"),
101 Self::Other(s) => write!(f, "{s}"),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110#[non_exhaustive]
111pub struct AlgorithmProperties {
112 pub primitive: CryptoPrimitive,
114 pub algorithm_family: Option<String>,
116 pub parameter_set_identifier: Option<String>,
118 pub mode: Option<CryptoMode>,
120 pub padding: Option<CryptoPadding>,
122 pub crypto_functions: Vec<CryptoFunction>,
124 pub execution_environment: Option<ExecutionEnvironment>,
126 pub implementation_platform: Option<ImplementationPlatform>,
128 pub certification_level: Vec<CertificationLevel>,
130 pub classical_security_level: Option<u32>,
132 pub nist_quantum_security_level: Option<u8>,
134 pub elliptic_curve: Option<String>,
136}
137
138impl AlgorithmProperties {
139 #[must_use]
141 pub fn new(primitive: CryptoPrimitive) -> Self {
142 Self {
143 primitive,
144 algorithm_family: None,
145 parameter_set_identifier: None,
146 mode: None,
147 padding: None,
148 crypto_functions: Vec::new(),
149 execution_environment: None,
150 implementation_platform: None,
151 certification_level: Vec::new(),
152 classical_security_level: None,
153 nist_quantum_security_level: None,
154 elliptic_curve: None,
155 }
156 }
157
158 #[must_use]
161 pub fn is_quantum_safe(&self) -> bool {
162 self.nist_quantum_security_level.is_some_and(|l| l > 0)
163 }
164
165 #[must_use]
167 pub fn is_hybrid_pqc(&self) -> bool {
168 self.primitive == CryptoPrimitive::Combiner
169 }
170
171 #[must_use]
180 pub fn is_classical_quantum_vulnerable(&self) -> bool {
181 const CLASSICAL_PK: &[&str] = &[
182 "RSA", "DSA", "DH", "DHE", "ECDH", "ECDHE", "ECDSA", "EDDSA", "ED25519", "ED448",
183 "X25519", "X448", "ELGAMAL", "ECIES", "ECMQV",
184 ];
185 self.algorithm_family.as_deref().is_some_and(|f| {
186 let upper = f.to_uppercase();
187 CLASSICAL_PK.iter().any(|c| upper == *c)
188 })
189 }
190
191 #[must_use]
195 pub fn is_weak(&self) -> bool {
196 const WEAK_FAMILIES: &[&str] = &[
198 "MD5", "MD4", "MD2", "SHA-1", "DES", "3DES", "TDEA", "RC2", "RC4", "BLOWFISH", "IDEA",
199 "CAST5",
200 ];
201
202 if let Some(family) = &self.algorithm_family {
203 let upper = family.to_uppercase();
204 if WEAK_FAMILIES.iter().any(|w| upper == *w) {
205 return true;
206 }
207 }
208 false
209 }
210
211 #[must_use]
214 pub fn is_weak_by_name(&self, component_name: &str) -> bool {
215 if self.is_weak() {
216 return true;
217 }
218 let upper = component_name.to_uppercase();
220 upper.starts_with("MD5")
221 || upper.starts_with("MD4")
222 || upper.starts_with("SHA-1")
223 || upper.starts_with("DES")
224 || upper.starts_with("3DES")
225 || upper.starts_with("RC4")
226 || upper.starts_with("RC2")
227 || upper.starts_with("BLOWFISH")
228 }
229
230 #[must_use]
232 pub fn effective_security_bits(&self) -> Option<u32> {
233 self.classical_security_level
234 }
235
236 #[must_use]
237 pub fn with_algorithm_family(mut self, family: String) -> Self {
238 self.algorithm_family = Some(family);
239 self
240 }
241
242 #[must_use]
243 pub fn with_parameter_set_identifier(mut self, id: String) -> Self {
244 self.parameter_set_identifier = Some(id);
245 self
246 }
247
248 #[must_use]
249 pub fn with_mode(mut self, mode: CryptoMode) -> Self {
250 self.mode = Some(mode);
251 self
252 }
253
254 #[must_use]
255 pub fn with_padding(mut self, padding: CryptoPadding) -> Self {
256 self.padding = Some(padding);
257 self
258 }
259
260 #[must_use]
261 pub fn with_crypto_functions(mut self, funcs: Vec<CryptoFunction>) -> Self {
262 self.crypto_functions = funcs;
263 self
264 }
265
266 #[must_use]
267 pub fn with_execution_environment(mut self, env: ExecutionEnvironment) -> Self {
268 self.execution_environment = Some(env);
269 self
270 }
271
272 #[must_use]
273 pub fn with_implementation_platform(mut self, platform: ImplementationPlatform) -> Self {
274 self.implementation_platform = Some(platform);
275 self
276 }
277
278 #[must_use]
279 pub fn with_certification_level(mut self, levels: Vec<CertificationLevel>) -> Self {
280 self.certification_level = levels;
281 self
282 }
283
284 #[must_use]
285 pub fn with_classical_security_level(mut self, bits: u32) -> Self {
286 self.classical_security_level = Some(bits);
287 self
288 }
289
290 #[must_use]
291 pub fn with_nist_quantum_security_level(mut self, level: u8) -> Self {
292 self.nist_quantum_security_level = Some(level);
293 self
294 }
295
296 #[must_use]
297 pub fn with_elliptic_curve(mut self, curve: String) -> Self {
298 self.elliptic_curve = Some(curve);
299 self
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307#[non_exhaustive]
308pub struct CertificateProperties {
309 pub subject_name: Option<String>,
311 pub issuer_name: Option<String>,
313 pub not_valid_before: Option<DateTime<Utc>>,
315 pub not_valid_after: Option<DateTime<Utc>>,
317 pub signature_algorithm_ref: Option<String>,
319 pub subject_public_key_ref: Option<String>,
321 pub certificate_format: Option<String>,
323 pub certificate_extension: Option<String>,
325}
326
327impl CertificateProperties {
328 #[must_use]
329 pub fn new() -> Self {
330 Self {
331 subject_name: None,
332 issuer_name: None,
333 not_valid_before: None,
334 not_valid_after: None,
335 signature_algorithm_ref: None,
336 subject_public_key_ref: None,
337 certificate_format: None,
338 certificate_extension: None,
339 }
340 }
341
342 #[must_use]
344 pub fn is_expired(&self) -> bool {
345 self.not_valid_after
346 .is_some_and(|expiry| expiry < Utc::now())
347 }
348
349 #[must_use]
351 pub fn is_expiring_soon(&self, days: u32) -> bool {
352 self.not_valid_after.is_some_and(|expiry| {
353 let threshold = Utc::now() + chrono::Duration::days(i64::from(days));
354 expiry <= threshold && expiry > Utc::now()
355 })
356 }
357
358 #[must_use]
361 pub fn validity_days(&self) -> Option<i64> {
362 self.not_valid_after
363 .map(|expiry| (expiry - Utc::now()).num_days())
364 }
365
366 #[must_use]
367 pub fn with_subject_name(mut self, name: String) -> Self {
368 self.subject_name = Some(name);
369 self
370 }
371
372 #[must_use]
373 pub fn with_issuer_name(mut self, name: String) -> Self {
374 self.issuer_name = Some(name);
375 self
376 }
377
378 #[must_use]
379 pub fn with_not_valid_before(mut self, dt: DateTime<Utc>) -> Self {
380 self.not_valid_before = Some(dt);
381 self
382 }
383
384 #[must_use]
385 pub fn with_not_valid_after(mut self, dt: DateTime<Utc>) -> Self {
386 self.not_valid_after = Some(dt);
387 self
388 }
389
390 #[must_use]
391 pub fn with_signature_algorithm_ref(mut self, r: String) -> Self {
392 self.signature_algorithm_ref = Some(r);
393 self
394 }
395
396 #[must_use]
397 pub fn with_subject_public_key_ref(mut self, r: String) -> Self {
398 self.subject_public_key_ref = Some(r);
399 self
400 }
401
402 #[must_use]
403 pub fn with_certificate_format(mut self, fmt: String) -> Self {
404 self.certificate_format = Some(fmt);
405 self
406 }
407
408 #[must_use]
409 pub fn with_certificate_extension(mut self, ext: String) -> Self {
410 self.certificate_extension = Some(ext);
411 self
412 }
413}
414
415impl Default for CertificateProperties {
416 fn default() -> Self {
417 Self::new()
418 }
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425#[non_exhaustive]
426pub struct RelatedCryptoMaterialProperties {
427 pub material_type: CryptoMaterialType,
429 pub id: Option<String>,
431 pub state: Option<CryptoMaterialState>,
433 pub size: Option<u32>,
435 pub algorithm_ref: Option<String>,
437 pub secured_by: Option<SecuredBy>,
439 pub format: Option<String>,
441 pub creation_date: Option<DateTime<Utc>>,
443 pub activation_date: Option<DateTime<Utc>>,
445 pub update_date: Option<DateTime<Utc>>,
447 pub expiration_date: Option<DateTime<Utc>>,
449}
450
451impl RelatedCryptoMaterialProperties {
452 #[must_use]
453 pub fn new(material_type: CryptoMaterialType) -> Self {
454 Self {
455 material_type,
456 id: None,
457 state: None,
458 size: None,
459 algorithm_ref: None,
460 secured_by: None,
461 format: None,
462 creation_date: None,
463 activation_date: None,
464 update_date: None,
465 expiration_date: None,
466 }
467 }
468
469 #[must_use]
470 pub fn with_id(mut self, id: String) -> Self {
471 self.id = Some(id);
472 self
473 }
474
475 #[must_use]
476 pub fn with_state(mut self, state: CryptoMaterialState) -> Self {
477 self.state = Some(state);
478 self
479 }
480
481 #[must_use]
482 pub fn with_size(mut self, bits: u32) -> Self {
483 self.size = Some(bits);
484 self
485 }
486
487 #[must_use]
488 pub fn with_algorithm_ref(mut self, r: String) -> Self {
489 self.algorithm_ref = Some(r);
490 self
491 }
492
493 #[must_use]
494 pub fn with_secured_by(mut self, secured: SecuredBy) -> Self {
495 self.secured_by = Some(secured);
496 self
497 }
498
499 #[must_use]
500 pub fn with_format(mut self, fmt: String) -> Self {
501 self.format = Some(fmt);
502 self
503 }
504
505 #[must_use]
506 pub fn with_creation_date(mut self, dt: DateTime<Utc>) -> Self {
507 self.creation_date = Some(dt);
508 self
509 }
510
511 #[must_use]
512 pub fn with_activation_date(mut self, dt: DateTime<Utc>) -> Self {
513 self.activation_date = Some(dt);
514 self
515 }
516
517 #[must_use]
518 pub fn with_expiration_date(mut self, dt: DateTime<Utc>) -> Self {
519 self.expiration_date = Some(dt);
520 self
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528#[non_exhaustive]
529pub struct ProtocolProperties {
530 pub protocol_type: ProtocolType,
532 pub version: Option<String>,
534 pub cipher_suites: Vec<CipherSuite>,
536 pub ikev2_transform_types: Option<Ikev2TransformTypes>,
538 pub crypto_ref_array: Vec<String>,
540}
541
542impl ProtocolProperties {
543 #[must_use]
544 pub fn new(protocol_type: ProtocolType) -> Self {
545 Self {
546 protocol_type,
547 version: None,
548 cipher_suites: Vec::new(),
549 ikev2_transform_types: None,
550 crypto_ref_array: Vec::new(),
551 }
552 }
553
554 #[must_use]
555 pub fn with_version(mut self, version: String) -> Self {
556 self.version = Some(version);
557 self
558 }
559
560 #[must_use]
561 pub fn with_cipher_suites(mut self, suites: Vec<CipherSuite>) -> Self {
562 self.cipher_suites = suites;
563 self
564 }
565
566 #[must_use]
567 pub fn with_ikev2_transform_types(mut self, types: Ikev2TransformTypes) -> Self {
568 self.ikev2_transform_types = Some(types);
569 self
570 }
571
572 #[must_use]
573 pub fn with_crypto_ref_array(mut self, refs: Vec<String>) -> Self {
574 self.crypto_ref_array = refs;
575 self
576 }
577}
578
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
583pub struct CipherSuite {
584 pub name: Option<String>,
586 pub algorithms: Vec<String>,
588 pub identifiers: Vec<String>,
590}
591
592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
594pub struct Ikev2TransformTypes {
595 pub encr: Vec<String>,
597 pub prf: Vec<String>,
599 pub integ: Vec<String>,
601 pub ke: Vec<String>,
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
607pub struct SecuredBy {
608 pub mechanism: String,
610 pub algorithm_ref: Option<String>,
612}
613
614#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
618#[non_exhaustive]
619pub enum CryptoPrimitive {
620 Ae,
622 BlockCipher,
624 StreamCipher,
626 Hash,
628 Mac,
630 Signature,
632 Pke,
634 Kem,
636 Kdf,
638 KeyAgree,
640 Xof,
642 Drbg,
644 Combiner,
646 Other(String),
647 Unknown,
648}
649
650impl std::fmt::Display for CryptoPrimitive {
651 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
652 match self {
653 Self::Ae => write!(f, "ae"),
654 Self::BlockCipher => write!(f, "block-cipher"),
655 Self::StreamCipher => write!(f, "stream-cipher"),
656 Self::Hash => write!(f, "hash"),
657 Self::Mac => write!(f, "mac"),
658 Self::Signature => write!(f, "signature"),
659 Self::Pke => write!(f, "pke"),
660 Self::Kem => write!(f, "kem"),
661 Self::Kdf => write!(f, "kdf"),
662 Self::KeyAgree => write!(f, "key-agree"),
663 Self::Xof => write!(f, "xof"),
664 Self::Drbg => write!(f, "drbg"),
665 Self::Combiner => write!(f, "combiner"),
666 Self::Other(s) => write!(f, "{s}"),
667 Self::Unknown => write!(f, "unknown"),
668 }
669 }
670}
671
672#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
674#[non_exhaustive]
675pub enum CryptoMode {
676 Ecb,
677 Cbc,
678 Ofb,
679 Cfb,
680 Ctr,
681 Gcm,
682 Ccm,
683 Xts,
684 Other(String),
685}
686
687impl std::fmt::Display for CryptoMode {
688 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689 match self {
690 Self::Ecb => write!(f, "ecb"),
691 Self::Cbc => write!(f, "cbc"),
692 Self::Ofb => write!(f, "ofb"),
693 Self::Cfb => write!(f, "cfb"),
694 Self::Ctr => write!(f, "ctr"),
695 Self::Gcm => write!(f, "gcm"),
696 Self::Ccm => write!(f, "ccm"),
697 Self::Xts => write!(f, "xts"),
698 Self::Other(s) => write!(f, "{s}"),
699 }
700 }
701}
702
703#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
705#[non_exhaustive]
706pub enum CryptoPadding {
707 Pkcs5,
708 Oaep,
709 Pss,
710 Other(String),
711}
712
713impl std::fmt::Display for CryptoPadding {
714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 match self {
716 Self::Pkcs5 => write!(f, "pkcs5"),
717 Self::Oaep => write!(f, "oaep"),
718 Self::Pss => write!(f, "pss"),
719 Self::Other(s) => write!(f, "{s}"),
720 }
721 }
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
726#[non_exhaustive]
727pub enum CryptoFunction {
728 Keygen,
729 Encrypt,
730 Decrypt,
731 Sign,
732 Verify,
733 Digest,
734 Tag,
735 KeyDerive,
736 Encapsulate,
737 Decapsulate,
738 Wrap,
739 Unwrap,
740 Other(String),
741}
742
743impl std::fmt::Display for CryptoFunction {
744 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
745 match self {
746 Self::Keygen => write!(f, "keygen"),
747 Self::Encrypt => write!(f, "encrypt"),
748 Self::Decrypt => write!(f, "decrypt"),
749 Self::Sign => write!(f, "sign"),
750 Self::Verify => write!(f, "verify"),
751 Self::Digest => write!(f, "digest"),
752 Self::Tag => write!(f, "tag"),
753 Self::KeyDerive => write!(f, "keyderive"),
754 Self::Encapsulate => write!(f, "encapsulate"),
755 Self::Decapsulate => write!(f, "decapsulate"),
756 Self::Wrap => write!(f, "wrap"),
757 Self::Unwrap => write!(f, "unwrap"),
758 Self::Other(s) => write!(f, "{s}"),
759 }
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
765#[non_exhaustive]
766pub enum ExecutionEnvironment {
767 SoftwarePlainRam,
768 SoftwareEncryptedRam,
769 SoftwareTee,
770 Hardware,
771 Other(String),
772}
773
774impl std::fmt::Display for ExecutionEnvironment {
775 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776 match self {
777 Self::SoftwarePlainRam => write!(f, "software-plain-ram"),
778 Self::SoftwareEncryptedRam => write!(f, "software-encrypted-ram"),
779 Self::SoftwareTee => write!(f, "software-tee"),
780 Self::Hardware => write!(f, "hardware"),
781 Self::Other(s) => write!(f, "{s}"),
782 }
783 }
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
788#[non_exhaustive]
789pub enum ImplementationPlatform {
790 X86_32,
791 X86_64,
792 Armv7A,
793 Armv7M,
794 Armv8A,
795 S390x,
796 Generic,
797 Other(String),
798}
799
800impl std::fmt::Display for ImplementationPlatform {
801 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802 match self {
803 Self::X86_32 => write!(f, "x86_32"),
804 Self::X86_64 => write!(f, "x86_64"),
805 Self::Armv7A => write!(f, "armv7-a"),
806 Self::Armv7M => write!(f, "armv7-m"),
807 Self::Armv8A => write!(f, "armv8-a"),
808 Self::S390x => write!(f, "s390x"),
809 Self::Generic => write!(f, "generic"),
810 Self::Other(s) => write!(f, "{s}"),
811 }
812 }
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
817#[non_exhaustive]
818pub enum CertificationLevel {
819 None,
820 Fips140_1L1,
821 Fips140_1L2,
822 Fips140_1L3,
823 Fips140_1L4,
824 Fips140_2L1,
825 Fips140_2L2,
826 Fips140_2L3,
827 Fips140_2L4,
828 Fips140_3L1,
829 Fips140_3L2,
830 Fips140_3L3,
831 Fips140_3L4,
832 CcEal1,
833 CcEal2,
834 CcEal3,
835 CcEal4,
836 CcEal5,
837 CcEal6,
838 CcEal7,
839 Other(String),
840}
841
842impl std::fmt::Display for CertificationLevel {
843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
844 match self {
845 Self::None => write!(f, "none"),
846 Self::Fips140_1L1 => write!(f, "fips140-1-l1"),
847 Self::Fips140_1L2 => write!(f, "fips140-1-l2"),
848 Self::Fips140_1L3 => write!(f, "fips140-1-l3"),
849 Self::Fips140_1L4 => write!(f, "fips140-1-l4"),
850 Self::Fips140_2L1 => write!(f, "fips140-2-l1"),
851 Self::Fips140_2L2 => write!(f, "fips140-2-l2"),
852 Self::Fips140_2L3 => write!(f, "fips140-2-l3"),
853 Self::Fips140_2L4 => write!(f, "fips140-2-l4"),
854 Self::Fips140_3L1 => write!(f, "fips140-3-l1"),
855 Self::Fips140_3L2 => write!(f, "fips140-3-l2"),
856 Self::Fips140_3L3 => write!(f, "fips140-3-l3"),
857 Self::Fips140_3L4 => write!(f, "fips140-3-l4"),
858 Self::CcEal1 => write!(f, "cc-eal1"),
859 Self::CcEal2 => write!(f, "cc-eal2"),
860 Self::CcEal3 => write!(f, "cc-eal3"),
861 Self::CcEal4 => write!(f, "cc-eal4"),
862 Self::CcEal5 => write!(f, "cc-eal5"),
863 Self::CcEal6 => write!(f, "cc-eal6"),
864 Self::CcEal7 => write!(f, "cc-eal7"),
865 Self::Other(s) => write!(f, "{s}"),
866 }
867 }
868}
869
870#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
872#[non_exhaustive]
873pub enum CryptoMaterialType {
874 PublicKey,
875 PrivateKey,
876 SymmetricKey,
877 SecretKey,
878 KeyPair,
879 Ciphertext,
880 Signature,
881 Digest,
882 Iv,
883 Nonce,
884 Seed,
885 Salt,
886 SharedSecret,
887 Tag,
888 Password,
889 Credential,
890 Token,
891 Other(String),
892 Unknown,
893}
894
895impl std::fmt::Display for CryptoMaterialType {
896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897 match self {
898 Self::PublicKey => write!(f, "public-key"),
899 Self::PrivateKey => write!(f, "private-key"),
900 Self::SymmetricKey => write!(f, "symmetric-key"),
901 Self::SecretKey => write!(f, "secret-key"),
902 Self::KeyPair => write!(f, "key-pair"),
903 Self::Ciphertext => write!(f, "ciphertext"),
904 Self::Signature => write!(f, "signature"),
905 Self::Digest => write!(f, "digest"),
906 Self::Iv => write!(f, "initialization-vector"),
907 Self::Nonce => write!(f, "nonce"),
908 Self::Seed => write!(f, "seed"),
909 Self::Salt => write!(f, "salt"),
910 Self::SharedSecret => write!(f, "shared-secret"),
911 Self::Tag => write!(f, "tag"),
912 Self::Password => write!(f, "password"),
913 Self::Credential => write!(f, "credential"),
914 Self::Token => write!(f, "token"),
915 Self::Other(s) => write!(f, "{s}"),
916 Self::Unknown => write!(f, "unknown"),
917 }
918 }
919}
920
921#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
923#[non_exhaustive]
924pub enum CryptoMaterialState {
925 PreActivation,
926 Active,
927 Suspended,
928 Deactivated,
929 Compromised,
930 Destroyed,
931}
932
933impl std::fmt::Display for CryptoMaterialState {
934 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
935 match self {
936 Self::PreActivation => write!(f, "pre-activation"),
937 Self::Active => write!(f, "active"),
938 Self::Suspended => write!(f, "suspended"),
939 Self::Deactivated => write!(f, "deactivated"),
940 Self::Compromised => write!(f, "compromised"),
941 Self::Destroyed => write!(f, "destroyed"),
942 }
943 }
944}
945
946#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
948#[non_exhaustive]
949pub enum ProtocolType {
950 Tls,
951 Dtls,
952 Ipsec,
953 Ssh,
954 Srtp,
955 Wireguard,
956 Ikev1,
957 Ikev2,
958 Zrtp,
959 Mikey,
960 Other(String),
961 Unknown,
962}
963
964impl std::fmt::Display for ProtocolType {
965 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
966 match self {
967 Self::Tls => write!(f, "tls"),
968 Self::Dtls => write!(f, "dtls"),
969 Self::Ipsec => write!(f, "ipsec"),
970 Self::Ssh => write!(f, "ssh"),
971 Self::Srtp => write!(f, "srtp"),
972 Self::Wireguard => write!(f, "wireguard"),
973 Self::Ikev1 => write!(f, "ikev1"),
974 Self::Ikev2 => write!(f, "ikev2"),
975 Self::Zrtp => write!(f, "zrtp"),
976 Self::Mikey => write!(f, "mikey"),
977 Self::Other(s) => write!(f, "{s}"),
978 Self::Unknown => write!(f, "unknown"),
979 }
980 }
981}
982
983#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
998pub enum PqcKind {
999 MlKem,
1001 MlDsa,
1003 SlhDsa,
1005 FnDsa,
1007 Lms,
1009 Xmss,
1011 Hss,
1013}
1014
1015#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1017pub enum AlgorithmClass {
1018 Broken,
1021 ClassicalQuantumVulnerable,
1025 Symmetric,
1028 Sha2,
1030 Sha3,
1032 OtherHash,
1036 PostQuantum(PqcKind),
1039 Unknown,
1042}
1043
1044impl AlgorithmClass {
1045 #[must_use]
1053 pub const fn severity_rank(self) -> u8 {
1054 match self {
1055 Self::Broken => 5,
1056 Self::ClassicalQuantumVulnerable => 4,
1057 Self::Symmetric | Self::Sha2 | Self::Sha3 | Self::OtherHash => 2,
1058 Self::PostQuantum(_) => 1,
1059 Self::Unknown => 0,
1060 }
1061 }
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Eq)]
1067pub struct AlgorithmClassification {
1068 pub family: Option<String>,
1070 pub parameter: Option<String>,
1072 pub class: AlgorithmClass,
1074}
1075
1076impl AlgorithmClassification {
1077 #[must_use]
1079 pub const fn unknown() -> Self {
1080 Self {
1081 family: None,
1082 parameter: None,
1083 class: AlgorithmClass::Unknown,
1084 }
1085 }
1086
1087 #[must_use]
1089 pub fn parameter_bits(&self) -> Option<u32> {
1090 self.parameter.as_deref().and_then(|p| p.parse().ok())
1091 }
1092
1093 #[must_use]
1095 pub fn label(&self) -> String {
1096 let Some(family) = self.family.as_deref() else {
1097 return "unclassified".to_string();
1098 };
1099 match (family, self.parameter.as_deref()) {
1100 ("SHA-2", Some(p)) => format!("SHA-{p}"),
1102 ("SHA-3", Some(p)) => format!("SHA3-{p}"),
1103 (f, Some(p)) => format!("{f}-{p}"),
1104 (f, None) => f.to_string(),
1105 }
1106 }
1107}
1108
1109fn family_class(canonical: &str) -> AlgorithmClass {
1111 use AlgorithmClass as C;
1112 match canonical {
1113 "MD2" | "MD4" | "MD5" | "SHA-1" | "DES" | "3DES" | "RC2" | "RC4" | "BLOWFISH" | "IDEA"
1114 | "CAST5" | "SKIPJACK" => C::Broken,
1115 "RSA" | "DSA" | "DH" | "ECDH" | "ECDSA" | "EDDSA" | "ED25519" | "ED448" | "X25519"
1116 | "X448" | "ELGAMAL" | "ECIES" | "ECMQV" | "EC" | "SM2" | "SM9" | "GOST-R-34.10" => {
1117 C::ClassicalQuantumVulnerable
1118 }
1119 "AES" | "CHACHA20" | "CAMELLIA" | "ARIA" | "SEED" | "SERPENT" | "TWOFISH" | "SM4"
1120 | "MAGMA" | "KUZNYECHIK" => C::Symmetric,
1121 "SHA-2" => C::Sha2,
1122 "SHA-3" => C::Sha3,
1123 "SM3" | "GOST-R-34.11" => C::OtherHash,
1124 "ML-KEM" => C::PostQuantum(PqcKind::MlKem),
1125 "ML-DSA" => C::PostQuantum(PqcKind::MlDsa),
1126 "SLH-DSA" => C::PostQuantum(PqcKind::SlhDsa),
1127 "FN-DSA" => C::PostQuantum(PqcKind::FnDsa),
1128 "LMS" => C::PostQuantum(PqcKind::Lms),
1129 "XMSS" => C::PostQuantum(PqcKind::Xmss),
1130 "HSS" => C::PostQuantum(PqcKind::Hss),
1131 _ => C::Unknown,
1132 }
1133}
1134
1135fn alias_lookup(token: &str) -> Option<(&'static str, Option<&'static str>, bool)> {
1139 let hit: (&'static str, Option<&'static str>) = match token {
1140 "MD2" => ("MD2", None),
1142 "MD4" => ("MD4", None),
1143 "MD5" => ("MD5", None),
1144 "SHA-1" | "SHA1" | "SHA" => ("SHA-1", None),
1146 "DES" => ("DES", None),
1147 "3DES" | "TDES" | "TDEA" | "DES3" | "DESEDE" | "DESEDE3" | "DES-EDE" | "DES-EDE2"
1148 | "DES-EDE3" | "3DES-EDE" | "TRIPLE-DES" | "TRIPLEDES" => ("3DES", None),
1149 "RC2" => ("RC2", None),
1150 "RC4" | "ARC4" | "ARCFOUR" => ("RC4", None),
1151 "BLOWFISH" => ("BLOWFISH", None),
1152 "IDEA" => ("IDEA", None),
1153 "CAST5" | "CAST-128" | "CAST128" => ("CAST5", None),
1154 "SKIPJACK" => ("SKIPJACK", None),
1155 "RSA" | "RSAES" | "RSASSA" | "RSA-PSS" | "RSA-OAEP" | "RSAES-OAEP" | "RSASSA-PSS" => {
1157 ("RSA", None)
1158 }
1159 "DSA" | "DSS" => ("DSA", None),
1160 "DH" | "DHE" | "FFDHE" | "EDH" | "ADH" | "DIFFIE-HELLMAN" => ("DH", None),
1161 "ECDH" | "ECDHE" | "XDH" => ("ECDH", None),
1162 "ECDSA" => ("ECDSA", None),
1163 "EDDSA" => ("EDDSA", None),
1164 "ED25519" => ("ED25519", None),
1165 "ED448" => ("ED448", None),
1166 "X25519" => ("X25519", None),
1167 "X448" => ("X448", None),
1168 "ELGAMAL" | "EL-GAMAL" => ("ELGAMAL", None),
1169 "ECIES" => ("ECIES", None),
1170 "ECMQV" => ("ECMQV", None),
1171 "EC" | "ECC" => ("EC", None),
1172 "SM2" => ("SM2", None),
1178 "SM9" => ("SM9", None),
1179 "GOST" | "GOST3410" | "GOSTR3410" | "GOST-R-34-10" => ("GOST-R-34.10", None),
1180 "AES" | "RIJNDAEL" => ("AES", None),
1182 "CHACHA" | "CHACHA20" | "XCHACHA20" | "CHACHA20-POLY1305" => ("CHACHA20", None),
1183 "CAMELLIA" => ("CAMELLIA", None),
1184 "ARIA" => ("ARIA", None),
1185 "SEED" => ("SEED", None),
1186 "SERPENT" => ("SERPENT", None),
1187 "TWOFISH" => ("TWOFISH", None),
1188 "SM4" => ("SM4", None),
1191 "MAGMA" => ("MAGMA", None),
1192 "KUZNYECHIK" | "KUZNECHIK" => ("KUZNYECHIK", None),
1193 "SM3" => ("SM3", None),
1195 "GOST3411" | "GOSTR3411" | "GOST-R-34-11" | "STREEBOG" => ("GOST-R-34.11", None),
1196 "SHA-2" | "SHA2" => ("SHA-2", None),
1198 "SHA-224" | "SHA224" => ("SHA-2", Some("224")),
1199 "SHA-256" | "SHA256" => ("SHA-2", Some("256")),
1200 "SHA-384" | "SHA384" => ("SHA-2", Some("384")),
1201 "SHA-512" | "SHA512" => ("SHA-2", Some("512")),
1202 "SHA-512-256" | "SHA512-256" => ("SHA-2", Some("256")),
1208 "SHA-512-224" | "SHA512-224" => ("SHA-2", Some("224")),
1209 "SHA-3" | "SHA3" | "KECCAK" | "SHAKE" | "SHAKE128" | "SHAKE256" => ("SHA-3", None),
1211 "SHA3-224" => ("SHA-3", Some("224")),
1212 "SHA3-256" => ("SHA-3", Some("256")),
1213 "SHA3-384" => ("SHA-3", Some("384")),
1214 "SHA3-512" => ("SHA-3", Some("512")),
1215 "ML-KEM" | "MLKEM" | "KYBER" | "CRYSTALS-KYBER" => ("ML-KEM", None),
1217 "ML-DSA" | "MLDSA" => ("ML-DSA", None),
1218 "DILITHIUM" | "CRYSTALS-DILITHIUM" => {
1219 return Some(("ML-DSA", None, true));
1220 }
1221 "SLH-DSA" | "SLHDSA" | "SPHINCS" | "SPHINCS+" | "SPHINCSPLUS" => ("SLH-DSA", None),
1222 "FALCON" | "FN-DSA" | "FNDSA" => ("FN-DSA", None),
1223 "LMS" | "HSS-LMS" | "LMS-HSS" => ("LMS", None),
1224 "XMSS" | "XMSS-MT" | "XMSSMT" => ("XMSS", None),
1225 "HSS" => ("HSS", None),
1226 _ => return None,
1227 };
1228 Some((hit.0, hit.1, false))
1229}
1230
1231fn normalize_algo_token(s: &str) -> String {
1233 s.trim()
1234 .chars()
1235 .map(|c| match c {
1236 '_' | ' ' | '/' | '.' => '-',
1237 other => other.to_ascii_uppercase(),
1238 })
1239 .collect()
1240}
1241
1242fn map_dilithium_param(p: &str) -> String {
1244 match p {
1245 "2" => "44".to_string(),
1246 "3" => "65".to_string(),
1247 "5" => "87".to_string(),
1248 other => other.to_string(),
1249 }
1250}
1251
1252fn classify_token(token: &str) -> Option<(&'static str, Option<String>)> {
1256 let t = normalize_algo_token(token);
1257
1258 let finish = |family: &'static str, param: Option<String>, dilithium: bool| {
1259 let param = if dilithium {
1260 param.map(|p| map_dilithium_param(&p))
1261 } else {
1262 param
1263 };
1264 Some((family, param))
1265 };
1266
1267 if let Some((family, param, dilithium)) = alias_lookup(&t) {
1268 return finish(family, param.map(str::to_string), dilithium);
1269 }
1270
1271 if t.starts_with("BRAINPOOL") {
1274 return Some(("EC", None));
1275 }
1276
1277 if let Some((base, digits)) = t.rsplit_once('-')
1279 && !digits.is_empty()
1280 && digits.bytes().all(|b| b.is_ascii_digit())
1281 && let Some((family, param, dilithium)) = alias_lookup(base)
1282 {
1283 let param = param
1284 .map(str::to_string)
1285 .or_else(|| Some(digits.to_string()));
1286 return finish(family, param, dilithium);
1287 }
1288
1289 let digit_start = t.len() - t.bytes().rev().take_while(u8::is_ascii_digit).count();
1293 if digit_start > 0 && digit_start < t.len() {
1294 let (alpha, digits) = t.split_at(digit_start);
1295 if let Some((family, param, dilithium)) = alias_lookup(alpha.trim_end_matches('-')) {
1296 let param = param
1297 .map(str::to_string)
1298 .or_else(|| Some(digits.to_string()));
1299 return finish(family, param, dilithium);
1300 }
1301 }
1302
1303 None
1304}
1305
1306#[must_use]
1316pub fn classify_algorithm_names(name: &str) -> Vec<AlgorithmClassification> {
1317 classify_names_impl(name, false)
1318}
1319
1320#[must_use]
1328pub fn classify_algorithm_names_guarded(name: &str) -> Vec<AlgorithmClassification> {
1329 classify_names_impl(name, true)
1330}
1331
1332fn is_overgeneric_span(span: &str) -> bool {
1336 let base = span
1337 .trim_end_matches(|c: char| c.is_ascii_digit())
1338 .trim_end_matches('-');
1339 matches!(base, "SEED" | "EC" | "ECC")
1340}
1341
1342fn classify_names_impl(name: &str, guarded: bool) -> Vec<AlgorithmClassification> {
1343 let upper = name.to_uppercase();
1344 let tokens: Vec<&str> = upper
1345 .split(|c: char| !(c.is_ascii_alphanumeric() || c == '+'))
1346 .filter(|t| !t.is_empty())
1347 .collect();
1348
1349 let mut out: Vec<AlgorithmClassification> = Vec::new();
1350 let mut i = 0;
1351 while i < tokens.len() {
1352 if tokens[i].bytes().all(|b| b.is_ascii_digit()) {
1354 i += 1;
1355 continue;
1356 }
1357 let max_span = 3.min(tokens.len() - i);
1358 let mut advanced = false;
1359 for span in (1..=max_span).rev() {
1360 let joined = tokens[i..i + span].join("-");
1361 if let Some((family, parameter)) = classify_token(&joined) {
1362 if guarded && is_overgeneric_span(&joined) {
1363 continue;
1364 }
1365 let cls = AlgorithmClassification {
1366 family: Some(family.to_string()),
1367 parameter,
1368 class: family_class(family),
1369 };
1370 if !out.contains(&cls) {
1371 out.push(cls);
1372 }
1373 i += span;
1374 advanced = true;
1375 break;
1376 }
1377 }
1378 if !advanced {
1379 i += 1;
1380 }
1381 }
1382 out
1383}
1384
1385#[must_use]
1391pub fn worst_classification(
1392 mentions: Vec<AlgorithmClassification>,
1393) -> Option<AlgorithmClassification> {
1394 let mut worst: Option<AlgorithmClassification> = None;
1395 for m in mentions {
1396 if worst
1397 .as_ref()
1398 .is_none_or(|w| m.class.severity_rank() > w.class.severity_rank())
1399 {
1400 worst = Some(m);
1401 }
1402 }
1403 worst
1404}
1405
1406fn classify_oid(oid: &str) -> Option<(&'static str, Option<String>)> {
1410 let o = oid.trim();
1411
1412 let exact: Option<(&'static str, Option<&'static str>)> = match o {
1414 "1.3.14.3.2.26" => Some(("SHA-1", None)),
1415 "1.2.840.113549.2.2" => Some(("MD2", None)),
1416 "1.2.840.113549.2.4" => Some(("MD4", None)),
1417 "1.2.840.113549.2.5" => Some(("MD5", None)),
1418 "1.3.14.3.2.7" => Some(("DES", None)),
1419 "1.2.840.113549.3.7" => Some(("3DES", None)),
1420 "1.2.840.113549.3.2" => Some(("RC2", None)),
1421 "1.2.840.113549.3.4" => Some(("RC4", None)),
1422 "1.2.840.10040.4.1" | "1.2.840.10040.4.3" => Some(("DSA", None)),
1423 "1.2.840.113549.1.3.1" | "1.2.840.10046.2.1" => Some(("DH", None)),
1424 "1.3.101.110" => Some(("X25519", None)),
1425 "1.3.101.111" => Some(("X448", None)),
1426 "1.3.101.112" => Some(("ED25519", None)),
1427 "1.3.101.113" => Some(("ED448", None)),
1428 "2.16.840.1.101.3.4.2.1" => Some(("SHA-2", Some("256"))),
1430 "2.16.840.1.101.3.4.2.2" => Some(("SHA-2", Some("384"))),
1431 "2.16.840.1.101.3.4.2.3" => Some(("SHA-2", Some("512"))),
1432 "2.16.840.1.101.3.4.2.4" => Some(("SHA-2", Some("224"))),
1433 "2.16.840.1.101.3.4.2.5" => Some(("SHA-2", Some("224"))), "2.16.840.1.101.3.4.2.6" => Some(("SHA-2", Some("256"))), "2.16.840.1.101.3.4.2.7" => Some(("SHA-3", Some("224"))),
1436 "2.16.840.1.101.3.4.2.8" => Some(("SHA-3", Some("256"))),
1437 "2.16.840.1.101.3.4.2.9" => Some(("SHA-3", Some("384"))),
1438 "2.16.840.1.101.3.4.2.10" => Some(("SHA-3", Some("512"))),
1439 "2.16.840.1.101.3.4.2.11" | "2.16.840.1.101.3.4.2.12" => Some(("SHA-3", None)), "1.2.840.113549.1.9.16.3.17" => Some(("LMS", None)), "0.4.0.127.0.15.1.1.13.0" => Some(("XMSS", None)),
1443 _ => None,
1444 };
1445 if let Some((family, param)) = exact {
1446 return Some((family, param.map(str::to_string)));
1447 }
1448
1449 if o.starts_with("1.2.840.113549.1.1.") {
1451 return Some(("RSA", None));
1452 }
1453 if o.starts_with("1.2.840.10045.4.") {
1455 return Some(("ECDSA", None));
1456 }
1457 if o.starts_with("1.2.840.10045.") {
1458 return Some(("EC", None));
1459 }
1460 if o.starts_with("1.3.132.") {
1462 return Some(("EC", None));
1463 }
1464 if let Some(rest) = o.strip_prefix("2.16.840.1.101.3.4.1.")
1466 && let Ok(n) = rest.parse::<u32>()
1467 {
1468 let bits = match n {
1469 1..=10 => Some("128"),
1470 21..=30 => Some("192"),
1471 41..=50 => Some("256"),
1472 _ => None,
1473 };
1474 return Some(("AES", bits.map(str::to_string)));
1475 }
1476 if let Some(rest) = o.strip_prefix("2.16.840.1.101.3.4.4.")
1478 && let Ok(n) = rest.parse::<u32>()
1479 {
1480 let param = match n {
1481 1 => Some("512"),
1482 2 => Some("768"),
1483 3 => Some("1024"),
1484 _ => None,
1485 };
1486 return Some(("ML-KEM", param.map(str::to_string)));
1487 }
1488 if let Some(rest) = o.strip_prefix("2.16.840.1.101.3.4.3.")
1490 && let Ok(n) = rest.parse::<u32>()
1491 {
1492 return match n {
1493 1..=8 => Some(("DSA", None)), 9..=12 => Some(("ECDSA", None)), 13..=16 => Some(("RSA", None)), 17 => Some(("ML-DSA", Some("44".to_string()))),
1497 18 => Some(("ML-DSA", Some("65".to_string()))),
1498 19 => Some(("ML-DSA", Some("87".to_string()))),
1499 20..=31 => Some(("SLH-DSA", None)),
1503 32 => Some(("ML-DSA", Some("44".to_string()))),
1504 33 => Some(("ML-DSA", Some("65".to_string()))),
1505 34 => Some(("ML-DSA", Some("87".to_string()))),
1506 35..=46 => Some(("SLH-DSA", None)),
1507 _ => None,
1508 };
1509 }
1510 if o == "1.2.156.10197.1.301" || o.starts_with("1.2.156.10197.1.301.") {
1514 return Some(("SM2", None));
1515 }
1516 if o == "1.2.156.10197.1.401" || o.starts_with("1.2.156.10197.1.401.") {
1517 return Some(("SM3", None));
1518 }
1519 if o == "1.2.156.10197.1.104" || o.starts_with("1.2.156.10197.1.104.") {
1520 return Some(("SM4", None));
1521 }
1522 if matches!(
1526 o,
1527 "1.2.643.2.2.19" | "1.2.643.2.2.20" | "1.2.643.2.2.3" | "1.2.643.2.2.4"
1528 ) || o.starts_with("1.2.643.7.1.1.1.")
1529 || o.starts_with("1.2.643.7.1.1.3.")
1530 {
1531 return Some(("GOST-R-34.10", None));
1532 }
1533 if o == "1.2.643.2.2.9" || o.starts_with("1.2.643.7.1.1.2.") {
1535 return Some(("GOST-R-34.11", None));
1536 }
1537 if o.starts_with("1.3.36.3.3.2.8.1.") {
1539 return Some(("EC", None));
1540 }
1541
1542 None
1543}
1544
1545#[must_use]
1577pub fn classify_algorithm(
1578 family: Option<&str>,
1579 name: Option<&str>,
1580 oid: Option<&str>,
1581 parameter_set: Option<&str>,
1582 elliptic_curve: Option<&str>,
1583) -> AlgorithmClassification {
1584 let fill_parameter = |mut cls: AlgorithmClassification| {
1585 if cls.parameter.is_none() {
1586 cls.parameter = parameter_set
1587 .map(str::trim)
1588 .filter(|p| !p.is_empty())
1589 .map(str::to_string);
1590 }
1591 if cls.parameter.is_none()
1594 && let Some(n) = name
1595 && let Some(named) = classify_algorithm_names(n)
1596 .into_iter()
1597 .find(|c| c.family == cls.family)
1598 {
1599 cls.parameter = named.parameter;
1600 }
1601 cls
1602 };
1603
1604 if let Some(f) = family.map(str::trim).filter(|f| !f.is_empty()) {
1606 if normalize_algo_token(f) == "SHA"
1611 && let Some(p @ ("224" | "256" | "384" | "512")) = parameter_set.map(str::trim)
1612 {
1613 return AlgorithmClassification {
1614 family: Some("SHA-2".to_string()),
1615 parameter: Some(p.to_string()),
1616 class: AlgorithmClass::Sha2,
1617 };
1618 }
1619 if let Some((canonical, parameter)) = classify_token(f) {
1620 return fill_parameter(AlgorithmClassification {
1621 family: Some(canonical.to_string()),
1622 parameter,
1623 class: family_class(canonical),
1624 });
1625 }
1626 } else if let Some(o) = oid.map(str::trim).filter(|o| !o.is_empty()) {
1627 if let Some((canonical, parameter)) = classify_oid(o) {
1630 return fill_parameter(AlgorithmClassification {
1631 family: Some(canonical.to_string()),
1632 parameter,
1633 class: family_class(canonical),
1634 });
1635 }
1636 }
1637
1638 if family.is_some()
1640 && let Some(o) = oid.map(str::trim).filter(|o| !o.is_empty())
1641 && let Some((canonical, parameter)) = classify_oid(o)
1642 {
1643 return fill_parameter(AlgorithmClassification {
1644 family: Some(canonical.to_string()),
1645 parameter,
1646 class: family_class(canonical),
1647 });
1648 }
1649
1650 if let Some(f) = family.map(str::trim).filter(|f| !f.is_empty())
1658 && let Some(cls) = worst_classification(classify_algorithm_names(f))
1659 {
1660 return fill_parameter(cls);
1661 }
1662
1663 if let Some(curve) = elliptic_curve.map(str::trim).filter(|c| !c.is_empty()) {
1665 return AlgorithmClassification {
1666 family: Some("EC".to_string()),
1667 parameter: Some(curve.to_string()),
1668 class: AlgorithmClass::ClassicalQuantumVulnerable,
1669 };
1670 }
1671
1672 if family.is_none()
1676 && oid.is_none()
1677 && let Some(n) = name
1678 && let Some(cls) = worst_classification(classify_algorithm_names_guarded(n))
1679 {
1680 return fill_parameter(cls);
1681 }
1682
1683 AlgorithmClassification {
1684 family: None,
1685 parameter: parameter_set.map(str::to_string),
1686 class: AlgorithmClass::Unknown,
1687 }
1688}
1689
1690#[cfg(test)]
1693mod tests {
1694 use super::*;
1695
1696 #[test]
1697 fn algorithm_is_quantum_safe() {
1698 let algo =
1699 AlgorithmProperties::new(CryptoPrimitive::Kem).with_nist_quantum_security_level(5);
1700 assert!(algo.is_quantum_safe());
1701
1702 let classical =
1703 AlgorithmProperties::new(CryptoPrimitive::Pke).with_nist_quantum_security_level(0);
1704 assert!(!classical.is_quantum_safe());
1705
1706 let unknown = AlgorithmProperties::new(CryptoPrimitive::Pke);
1707 assert!(!unknown.is_quantum_safe());
1708 }
1709
1710 #[test]
1711 fn algorithm_is_hybrid_pqc() {
1712 let hybrid = AlgorithmProperties::new(CryptoPrimitive::Combiner);
1713 assert!(hybrid.is_hybrid_pqc());
1714
1715 let normal = AlgorithmProperties::new(CryptoPrimitive::Kem);
1716 assert!(!normal.is_hybrid_pqc());
1717 }
1718
1719 #[test]
1720 fn algorithm_is_weak() {
1721 let md5 = AlgorithmProperties::new(CryptoPrimitive::Hash)
1722 .with_algorithm_family("MD5".to_string());
1723 assert!(md5.is_weak());
1724
1725 let sha1 = AlgorithmProperties::new(CryptoPrimitive::Hash)
1726 .with_algorithm_family("SHA-1".to_string());
1727 assert!(sha1.is_weak());
1728
1729 let des = AlgorithmProperties::new(CryptoPrimitive::BlockCipher)
1730 .with_algorithm_family("DES".to_string());
1731 assert!(des.is_weak());
1732
1733 let rc4 = AlgorithmProperties::new(CryptoPrimitive::StreamCipher)
1734 .with_algorithm_family("RC4".to_string());
1735 assert!(rc4.is_weak());
1736
1737 let aes =
1738 AlgorithmProperties::new(CryptoPrimitive::Ae).with_algorithm_family("AES".to_string());
1739 assert!(!aes.is_weak());
1740
1741 let ml_kem = AlgorithmProperties::new(CryptoPrimitive::Kem)
1742 .with_algorithm_family("ML-KEM".to_string());
1743 assert!(!ml_kem.is_weak());
1744 }
1745
1746 #[test]
1747 fn certificate_expiry() {
1748 let expired = CertificateProperties::new()
1749 .with_not_valid_after(Utc::now() - chrono::Duration::days(1));
1750 assert!(expired.is_expired());
1751 assert!(!expired.is_expiring_soon(90));
1752
1753 let valid = CertificateProperties::new()
1754 .with_not_valid_after(Utc::now() + chrono::Duration::days(365));
1755 assert!(!valid.is_expired());
1756 assert!(!valid.is_expiring_soon(90));
1757
1758 let expiring = CertificateProperties::new()
1759 .with_not_valid_after(Utc::now() + chrono::Duration::days(30));
1760 assert!(!expiring.is_expired());
1761 assert!(expiring.is_expiring_soon(90));
1762 }
1763
1764 #[test]
1765 fn certificate_validity_days() {
1766 let no_expiry = CertificateProperties::new();
1767 assert!(no_expiry.validity_days().is_none());
1768
1769 let expired = CertificateProperties::new()
1770 .with_not_valid_after(Utc::now() - chrono::Duration::days(10));
1771 assert!(expired.validity_days().unwrap() < 0);
1772
1773 let future = CertificateProperties::new()
1774 .with_not_valid_after(Utc::now() + chrono::Duration::days(100));
1775 let days = future.validity_days().unwrap();
1776 assert!(days >= 99 && days <= 100);
1777 }
1778
1779 #[test]
1780 fn crypto_properties_builder() {
1781 let props = CryptoProperties::new(CryptoAssetType::Algorithm)
1782 .with_oid("2.16.840.1.101.3.4.1.46".to_string())
1783 .with_algorithm_properties(
1784 AlgorithmProperties::new(CryptoPrimitive::Ae)
1785 .with_algorithm_family("AES".to_string())
1786 .with_mode(CryptoMode::Gcm)
1787 .with_classical_security_level(256)
1788 .with_nist_quantum_security_level(1),
1789 );
1790
1791 assert_eq!(props.asset_type, CryptoAssetType::Algorithm);
1792 assert_eq!(props.oid.as_deref(), Some("2.16.840.1.101.3.4.1.46"));
1793 let algo = props.algorithm_properties.unwrap();
1794 assert_eq!(algo.primitive, CryptoPrimitive::Ae);
1795 assert_eq!(algo.algorithm_family.as_deref(), Some("AES"));
1796 assert_eq!(algo.mode, Some(CryptoMode::Gcm));
1797 assert_eq!(algo.classical_security_level, Some(256));
1798 assert!(algo.is_quantum_safe());
1799 assert!(!algo.is_weak());
1800 }
1801
1802 #[test]
1803 fn display_impls() {
1804 assert_eq!(CryptoAssetType::Algorithm.to_string(), "algorithm");
1805 assert_eq!(
1806 CryptoAssetType::RelatedCryptoMaterial.to_string(),
1807 "related-crypto-material"
1808 );
1809 assert_eq!(CryptoPrimitive::Kem.to_string(), "kem");
1810 assert_eq!(CryptoPrimitive::Combiner.to_string(), "combiner");
1811 assert_eq!(CryptoMode::Gcm.to_string(), "gcm");
1812 assert_eq!(CryptoFunction::Encapsulate.to_string(), "encapsulate");
1813 assert_eq!(CryptoMaterialType::PublicKey.to_string(), "public-key");
1814 assert_eq!(CryptoMaterialState::Compromised.to_string(), "compromised");
1815 assert_eq!(ProtocolType::Tls.to_string(), "tls");
1816 assert_eq!(CertificationLevel::Fips140_3L1.to_string(), "fips140-3-l1");
1817 assert_eq!(ExecutionEnvironment::Hardware.to_string(), "hardware");
1818 assert_eq!(ImplementationPlatform::X86_64.to_string(), "x86_64");
1819 }
1820
1821 #[test]
1822 fn protocol_builder() {
1823 let proto = ProtocolProperties::new(ProtocolType::Tls)
1824 .with_version("1.3".to_string())
1825 .with_cipher_suites(vec![CipherSuite {
1826 name: Some("TLS_AES_256_GCM_SHA384".to_string()),
1827 algorithms: vec!["algo/aes-256-gcm".to_string()],
1828 identifiers: vec!["0x13".to_string(), "0x02".to_string()],
1829 }]);
1830
1831 assert_eq!(proto.protocol_type, ProtocolType::Tls);
1832 assert_eq!(proto.version.as_deref(), Some("1.3"));
1833 assert_eq!(proto.cipher_suites.len(), 1);
1834 }
1835
1836 fn cls(
1839 family: Option<&str>,
1840 name: Option<&str>,
1841 oid: Option<&str>,
1842 param: Option<&str>,
1843 curve: Option<&str>,
1844 ) -> AlgorithmClassification {
1845 classify_algorithm(family, name, oid, param, curve)
1846 }
1847
1848 #[test]
1849 fn classify_family_spelling_variants() {
1850 for (input, family, class) in [
1852 ("SHA1", "SHA-1", AlgorithmClass::Broken),
1853 ("sha-1", "SHA-1", AlgorithmClass::Broken),
1854 ("TDES", "3DES", AlgorithmClass::Broken),
1855 ("DES-EDE3", "3DES", AlgorithmClass::Broken),
1856 ("3DES-EDE", "3DES", AlgorithmClass::Broken),
1857 ("ARC4", "RC4", AlgorithmClass::Broken),
1858 ("ARCFOUR", "RC4", AlgorithmClass::Broken),
1859 (
1860 "Ed25519",
1861 "ED25519",
1862 AlgorithmClass::ClassicalQuantumVulnerable,
1863 ),
1864 ("ECIES", "ECIES", AlgorithmClass::ClassicalQuantumVulnerable),
1865 ("ECDHE", "ECDH", AlgorithmClass::ClassicalQuantumVulnerable),
1866 ("EC", "EC", AlgorithmClass::ClassicalQuantumVulnerable),
1867 ("ChaCha20", "CHACHA20", AlgorithmClass::Symmetric),
1868 ("Camellia", "CAMELLIA", AlgorithmClass::Symmetric),
1869 ] {
1870 let c = cls(Some(input), None, None, None, None);
1871 assert_eq!(c.family.as_deref(), Some(family), "family for {input}");
1872 assert_eq!(c.class, class, "class for {input}");
1873 }
1874 }
1875
1876 #[test]
1877 fn classify_size_in_family_string() {
1878 let c = cls(Some("ML-KEM-768"), None, None, None, None);
1879 assert_eq!(c.family.as_deref(), Some("ML-KEM"));
1880 assert_eq!(c.parameter.as_deref(), Some("768"));
1881 assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::MlKem));
1882
1883 let c = cls(Some("AES-128"), None, None, None, None);
1884 assert_eq!(c.family.as_deref(), Some("AES"));
1885 assert_eq!(c.parameter.as_deref(), Some("128"));
1886
1887 let c = cls(Some("AES128"), None, None, None, None);
1888 assert_eq!(c.parameter.as_deref(), Some("128"));
1889
1890 let c = cls(Some("RSA-2048"), None, None, None, None);
1891 assert_eq!(c.family.as_deref(), Some("RSA"));
1892 assert_eq!(c.parameter.as_deref(), Some("2048"));
1893 assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
1894
1895 let c = cls(Some("SHA-256"), None, None, None, None);
1897 assert_eq!(c.family.as_deref(), Some("SHA-2"));
1898 assert_eq!(c.parameter.as_deref(), Some("256"));
1899 assert_eq!(c.class, AlgorithmClass::Sha2);
1900 assert_eq!(c.label(), "SHA-256");
1901 }
1902
1903 #[test]
1904 fn classify_round3_pqc_names() {
1905 let c = cls(Some("Kyber"), None, None, Some("768"), None);
1906 assert_eq!(c.family.as_deref(), Some("ML-KEM"));
1907 assert_eq!(c.parameter.as_deref(), Some("768"));
1908
1909 let c = cls(Some("Kyber-1024"), None, None, None, None);
1910 assert_eq!(c.parameter.as_deref(), Some("1024"));
1911
1912 let c = cls(Some("Dilithium-3"), None, None, None, None);
1914 assert_eq!(c.family.as_deref(), Some("ML-DSA"));
1915 assert_eq!(c.parameter.as_deref(), Some("65"));
1916
1917 let c = cls(Some("SPHINCS+"), None, None, None, None);
1918 assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::SlhDsa));
1919
1920 let c = cls(Some("Falcon"), None, None, None, None);
1921 assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::FnDsa));
1922 }
1923
1924 #[test]
1925 fn classify_by_oid() {
1926 let c = cls(None, None, Some("1.2.840.113549.1.1.1"), Some("2048"), None);
1928 assert_eq!(c.family.as_deref(), Some("RSA"));
1929 assert_eq!(c.parameter.as_deref(), Some("2048"));
1930 assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
1931
1932 assert_eq!(
1934 cls(None, None, Some("1.3.14.3.2.26"), None, None).class,
1935 AlgorithmClass::Broken
1936 );
1937 assert_eq!(
1938 cls(None, None, Some("1.2.840.113549.2.5"), None, None).class,
1939 AlgorithmClass::Broken
1940 );
1941
1942 let c = cls(None, None, Some("2.16.840.1.101.3.4.1.2"), None, None);
1944 assert_eq!(c.family.as_deref(), Some("AES"));
1945 assert_eq!(c.parameter.as_deref(), Some("128"));
1946 let c = cls(None, None, Some("2.16.840.1.101.3.4.1.46"), None, None);
1947 assert_eq!(c.parameter.as_deref(), Some("256"));
1948
1949 let c = cls(None, None, Some("2.16.840.1.101.3.4.2.2"), None, None);
1951 assert_eq!(c.class, AlgorithmClass::Sha2);
1952 assert_eq!(c.parameter.as_deref(), Some("384"));
1953
1954 let c = cls(None, None, Some("2.16.840.1.101.3.4.4.3"), None, None);
1956 assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::MlKem));
1957 assert_eq!(c.parameter.as_deref(), Some("1024"));
1958 let c = cls(None, None, Some("2.16.840.1.101.3.4.3.19"), None, None);
1959 assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::MlDsa));
1960 assert_eq!(c.parameter.as_deref(), Some("87"));
1961 let c = cls(None, None, Some("2.16.840.1.101.3.4.3.24"), None, None);
1962 assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::SlhDsa));
1963
1964 assert_eq!(
1966 cls(None, None, Some("1.3.101.112"), None, None)
1967 .family
1968 .as_deref(),
1969 Some("ED25519")
1970 );
1971
1972 assert_eq!(
1974 cls(None, None, Some("1.2.840.10045.4.3.2"), None, None).class,
1975 AlgorithmClass::ClassicalQuantumVulnerable
1976 );
1977 assert_eq!(
1978 cls(None, None, Some("1.2.840.10045.3.1.7"), None, None).class,
1979 AlgorithmClass::ClassicalQuantumVulnerable
1980 );
1981 }
1982
1983 #[test]
1984 fn classify_name_fallback_only_without_family_and_oid() {
1985 let c = cls(None, Some("AES-128-CBC"), None, None, None);
1987 assert_eq!(c.family.as_deref(), Some("AES"));
1988 assert_eq!(c.parameter.as_deref(), Some("128"));
1989
1990 let c = cls(None, Some("RSA-2048-PKCS1"), None, None, None);
1991 assert_eq!(c.family.as_deref(), Some("RSA"));
1992
1993 let c = cls(
1995 Some("proprietary-frobnicator"),
1996 Some("RSA-2048"),
1997 None,
1998 None,
1999 None,
2000 );
2001 assert_eq!(c.class, AlgorithmClass::Unknown);
2002 let c = cls(None, Some("RSA-2048"), Some("9.9.9.9"), None, None);
2003 assert_eq!(c.class, AlgorithmClass::Unknown);
2004
2005 let c = cls(None, Some("DESCRIPTOR-HANDLER"), None, None, None);
2007 assert_eq!(c.class, AlgorithmClass::Unknown);
2008 }
2009
2010 #[test]
2011 fn classify_elliptic_curve_field() {
2012 let c = cls(None, None, None, None, Some("secg/secp256r1"));
2015 assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
2016 assert_eq!(c.family.as_deref(), Some("EC"));
2017 assert_eq!(c.parameter.as_deref(), Some("secg/secp256r1"));
2018 }
2019
2020 #[test]
2021 fn classify_name_enriches_missing_parameter() {
2022 let c = cls(Some("AES"), Some("AES-256-GCM"), None, None, None);
2024 assert_eq!(c.parameter.as_deref(), Some("256"));
2025 }
2026
2027 #[test]
2028 fn classify_cipher_suite_names() {
2029 let found = classify_algorithm_names("TLS_RSA_WITH_RC4_128_SHA");
2030 let families: Vec<_> = found.iter().filter_map(|c| c.family.as_deref()).collect();
2031 assert!(families.contains(&"RSA"), "{families:?}");
2032 assert!(families.contains(&"RC4"), "{families:?}");
2033 assert!(families.contains(&"SHA-1"), "{families:?}");
2034
2035 let found = classify_algorithm_names("TLS_AES_256_GCM_SHA384_ML_KEM_1024");
2038 assert!(
2039 found.iter().any(
2040 |c| c.family.as_deref() == Some("AES") && c.parameter.as_deref() == Some("256")
2041 )
2042 );
2043 assert!(
2044 found
2045 .iter()
2046 .any(|c| c.family.as_deref() == Some("SHA-2")
2047 && c.parameter.as_deref() == Some("384"))
2048 );
2049 assert!(found.iter().any(
2050 |c| c.family.as_deref() == Some("ML-KEM") && c.parameter.as_deref() == Some("1024")
2051 ));
2052 assert!(!found.iter().any(|c| c.class == AlgorithmClass::Broken));
2053 }
2054
2055 #[test]
2059 fn classify_compound_family_mode_suffixes() {
2060 for (family, canonical, param, class) in [
2061 ("DES-CBC", "DES", None, AlgorithmClass::Broken),
2062 ("3DES-EDE-CBC", "3DES", None, AlgorithmClass::Broken),
2063 ("AES-128-CBC", "AES", Some("128"), AlgorithmClass::Symmetric),
2064 ("AES-256-GCM", "AES", Some("256"), AlgorithmClass::Symmetric),
2065 (
2066 "RSA/ECB/PKCS1Padding",
2067 "RSA",
2068 None,
2069 AlgorithmClass::ClassicalQuantumVulnerable,
2070 ),
2071 ] {
2072 let c = cls(Some(family), None, None, None, None);
2073 assert_eq!(c.family.as_deref(), Some(canonical), "family for {family}");
2074 assert_eq!(c.parameter.as_deref(), param, "parameter for {family}");
2075 assert_eq!(c.class, class, "class for {family}");
2076 }
2077 let c = cls(Some("DES-CBC-HMAC-SHA384"), None, None, None, None);
2080 assert_eq!(c.family.as_deref(), Some("DES"));
2081 assert_eq!(c.class, AlgorithmClass::Broken);
2082 for family in ["Hybrid-KEM", "proprietary-frobnicator"] {
2084 let c = cls(Some(family), None, None, None, None);
2085 assert_eq!(c.class, AlgorithmClass::Unknown, "class for {family}");
2086 }
2087 }
2088
2089 #[test]
2092 fn classify_truncated_sha2_variants() {
2093 for (family, param) in [
2094 ("SHA-512/256", "256"),
2095 ("SHA-512/224", "224"),
2096 ("SHA512/256", "256"),
2097 ("sha-512/224", "224"),
2098 ] {
2099 let c = cls(Some(family), None, None, None, None);
2100 assert_eq!(c.family.as_deref(), Some("SHA-2"), "family for {family}");
2101 assert_eq!(c.parameter.as_deref(), Some(param), "param for {family}");
2102 assert_eq!(c.class, AlgorithmClass::Sha2);
2103 }
2104 let c = cls(Some("SHA-512"), None, None, None, None);
2107 assert_eq!(c.parameter.as_deref(), Some("512"));
2108 let by_oid = cls(None, None, Some("2.16.840.1.101.3.4.2.6"), None, None);
2109 assert_eq!(by_oid.parameter.as_deref(), Some("256"));
2110 }
2111
2112 #[test]
2116 fn classify_name_fallback_picks_most_severe() {
2117 let hash_first = cls(None, Some("sha384-rsa-signature"), None, None, None);
2118 let rsa_first = cls(None, Some("rsa-sha384-signature"), None, None, None);
2119 for c in [&hash_first, &rsa_first] {
2120 assert_eq!(c.family.as_deref(), Some("RSA"), "{c:?}");
2121 assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
2122 }
2123 let c = cls(None, Some("rsa-md5-legacy-signer"), None, None, None);
2125 assert_eq!(c.family.as_deref(), Some("MD5"));
2126 assert_eq!(c.class, AlgorithmClass::Broken);
2127 let c = cls(None, Some("sha384-digest"), None, None, None);
2129 assert_eq!(c.class, AlgorithmClass::Sha2);
2130 }
2131
2132 #[test]
2137 fn classify_bare_sha_with_parameter_set() {
2138 for param in ["224", "256", "384", "512"] {
2139 let c = cls(Some("SHA"), None, None, Some(param), None);
2140 assert_eq!(c.family.as_deref(), Some("SHA-2"), "family for SHA/{param}");
2141 assert_eq!(c.parameter.as_deref(), Some(param));
2142 assert_eq!(c.class, AlgorithmClass::Sha2);
2143 }
2144 let c = cls(Some("SHA"), None, None, None, None);
2145 assert_eq!(c.family.as_deref(), Some("SHA-1"));
2146 assert_eq!(c.class, AlgorithmClass::Broken);
2147 let c = cls(Some("SHA"), None, None, Some("160"), None);
2148 assert_eq!(c.family.as_deref(), Some("SHA-1"));
2149 assert_eq!(c.class, AlgorithmClass::Broken);
2150 }
2151
2152 #[test]
2157 fn classify_national_algorithms() {
2158 for (family, canonical) in [
2160 ("SM2", "SM2"),
2161 ("sm9", "SM9"),
2162 ("GOST", "GOST-R-34.10"),
2163 ("GOST R 34.10", "GOST-R-34.10"),
2164 ("GOST-R-34.10-2012", "GOST-R-34.10"),
2165 ("brainpoolP256r1", "EC"),
2166 ] {
2167 let c = cls(Some(family), None, None, None, None);
2168 assert_eq!(c.family.as_deref(), Some(canonical), "family for {family}");
2169 assert_eq!(
2170 c.class,
2171 AlgorithmClass::ClassicalQuantumVulnerable,
2172 "class for {family}"
2173 );
2174 }
2175 for (oid, canonical, class) in [
2177 (
2178 "1.2.156.10197.1.301",
2179 "SM2",
2180 AlgorithmClass::ClassicalQuantumVulnerable,
2181 ),
2182 (
2183 "1.2.643.2.2.19",
2184 "GOST-R-34.10",
2185 AlgorithmClass::ClassicalQuantumVulnerable,
2186 ),
2187 (
2188 "1.2.643.7.1.1.1.1",
2189 "GOST-R-34.10",
2190 AlgorithmClass::ClassicalQuantumVulnerable,
2191 ),
2192 (
2193 "1.3.36.3.3.2.8.1.1.7",
2194 "EC",
2195 AlgorithmClass::ClassicalQuantumVulnerable,
2196 ),
2197 ("1.2.156.10197.1.104", "SM4", AlgorithmClass::Symmetric),
2198 (
2199 "1.2.643.7.1.1.2.2",
2200 "GOST-R-34.11",
2201 AlgorithmClass::OtherHash,
2202 ),
2203 ] {
2204 let c = cls(None, None, Some(oid), None, None);
2205 assert_eq!(c.family.as_deref(), Some(canonical), "family for {oid}");
2206 assert_eq!(c.class, class, "class for {oid}");
2207 }
2208 assert_eq!(
2210 cls(Some("SM4"), None, None, None, None).class,
2211 AlgorithmClass::Symmetric
2212 );
2213 assert_eq!(
2214 cls(Some("Kuznyechik"), None, None, None, None).class,
2215 AlgorithmClass::Symmetric
2216 );
2217 assert_eq!(
2218 cls(Some("Streebog"), None, None, None, None).class,
2219 AlgorithmClass::OtherHash
2220 );
2221 }
2222
2223 #[test]
2226 fn classify_hash_ml_dsa_oids() {
2227 for (oid, param) in [
2228 ("2.16.840.1.101.3.4.3.32", "44"),
2229 ("2.16.840.1.101.3.4.3.33", "65"),
2230 ("2.16.840.1.101.3.4.3.34", "87"),
2231 ] {
2232 let c = cls(None, None, Some(oid), None, None);
2233 assert_eq!(
2234 c.class,
2235 AlgorithmClass::PostQuantum(PqcKind::MlDsa),
2236 "class for {oid}"
2237 );
2238 assert_eq!(c.parameter.as_deref(), Some(param), "param for {oid}");
2239 }
2240 for oid in [
2243 "2.16.840.1.101.3.4.3.20",
2244 "2.16.840.1.101.3.4.3.31",
2245 "2.16.840.1.101.3.4.3.35",
2246 ] {
2247 assert_eq!(
2248 cls(None, None, Some(oid), None, None).class,
2249 AlgorithmClass::PostQuantum(PqcKind::SlhDsa),
2250 "class for {oid}"
2251 );
2252 }
2253 }
2254
2255 #[test]
2260 fn guarded_name_scan_drops_overgeneric_tokens() {
2261 for name in ["seed-expander", "ec2-instance-agent", "ecc-memory-check"] {
2262 assert!(
2263 classify_algorithm_names_guarded(name).is_empty(),
2264 "guarded scan must ignore {name}"
2265 );
2266 assert_eq!(
2267 cls(None, Some(name), None, None, None).class,
2268 AlgorithmClass::Unknown,
2269 "name fallback must not classify {name}"
2270 );
2271 }
2272 assert_eq!(
2274 cls(None, Some("brainpoolP256r1-signer"), None, None, None).class,
2275 AlgorithmClass::ClassicalQuantumVulnerable
2276 );
2277 assert!(
2280 classify_algorithm_names("TLS_RSA_WITH_SEED_CBC_SHA")
2281 .iter()
2282 .any(|c| c.family.as_deref() == Some("SEED"))
2283 );
2284 assert_eq!(
2285 cls(Some("SEED"), None, None, None, None).class,
2286 AlgorithmClass::Symmetric
2287 );
2288 }
2289
2290 #[test]
2291 fn related_material_builder() {
2292 let key = RelatedCryptoMaterialProperties::new(CryptoMaterialType::PublicKey)
2293 .with_id("test-id".to_string())
2294 .with_state(CryptoMaterialState::Active)
2295 .with_size(2048)
2296 .with_algorithm_ref("algo/rsa-2048".to_string())
2297 .with_secured_by(SecuredBy {
2298 mechanism: "HSM".to_string(),
2299 algorithm_ref: Some("algo/aes-256".to_string()),
2300 });
2301
2302 assert_eq!(key.material_type, CryptoMaterialType::PublicKey);
2303 assert_eq!(key.state, Some(CryptoMaterialState::Active));
2304 assert_eq!(key.size, Some(2048));
2305 assert!(key.secured_by.is_some());
2306 }
2307}