use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CryptoProperties {
pub asset_type: CryptoAssetType,
pub oid: Option<String>,
pub algorithm_properties: Option<AlgorithmProperties>,
pub certificate_properties: Option<CertificateProperties>,
pub related_crypto_material_properties: Option<RelatedCryptoMaterialProperties>,
pub protocol_properties: Option<ProtocolProperties>,
}
impl CryptoProperties {
#[must_use]
pub fn new(asset_type: CryptoAssetType) -> Self {
Self {
asset_type,
oid: None,
algorithm_properties: None,
certificate_properties: None,
related_crypto_material_properties: None,
protocol_properties: None,
}
}
#[must_use]
pub fn with_oid(mut self, oid: String) -> Self {
self.oid = Some(oid);
self
}
#[must_use]
pub fn with_algorithm_properties(mut self, props: AlgorithmProperties) -> Self {
self.algorithm_properties = Some(props);
self
}
#[must_use]
pub fn with_certificate_properties(mut self, props: CertificateProperties) -> Self {
self.certificate_properties = Some(props);
self
}
#[must_use]
pub fn with_related_crypto_material_properties(
mut self,
props: RelatedCryptoMaterialProperties,
) -> Self {
self.related_crypto_material_properties = Some(props);
self
}
#[must_use]
pub fn with_protocol_properties(mut self, props: ProtocolProperties) -> Self {
self.protocol_properties = Some(props);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoAssetType {
Algorithm,
Certificate,
RelatedCryptoMaterial,
Protocol,
Other(String),
}
impl std::fmt::Display for CryptoAssetType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Algorithm => write!(f, "algorithm"),
Self::Certificate => write!(f, "certificate"),
Self::RelatedCryptoMaterial => write!(f, "related-crypto-material"),
Self::Protocol => write!(f, "protocol"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AlgorithmProperties {
pub primitive: CryptoPrimitive,
pub algorithm_family: Option<String>,
pub parameter_set_identifier: Option<String>,
pub mode: Option<CryptoMode>,
pub padding: Option<CryptoPadding>,
pub crypto_functions: Vec<CryptoFunction>,
pub execution_environment: Option<ExecutionEnvironment>,
pub implementation_platform: Option<ImplementationPlatform>,
pub certification_level: Vec<CertificationLevel>,
pub classical_security_level: Option<u32>,
pub nist_quantum_security_level: Option<u8>,
pub elliptic_curve: Option<String>,
}
impl AlgorithmProperties {
#[must_use]
pub fn new(primitive: CryptoPrimitive) -> Self {
Self {
primitive,
algorithm_family: None,
parameter_set_identifier: None,
mode: None,
padding: None,
crypto_functions: Vec::new(),
execution_environment: None,
implementation_platform: None,
certification_level: Vec::new(),
classical_security_level: None,
nist_quantum_security_level: None,
elliptic_curve: None,
}
}
#[must_use]
pub fn is_quantum_safe(&self) -> bool {
self.nist_quantum_security_level.is_some_and(|l| l > 0)
}
#[must_use]
pub fn is_hybrid_pqc(&self) -> bool {
self.primitive == CryptoPrimitive::Combiner
}
#[must_use]
pub fn is_classical_quantum_vulnerable(&self) -> bool {
const CLASSICAL_PK: &[&str] = &[
"RSA", "DSA", "DH", "DHE", "ECDH", "ECDHE", "ECDSA", "EDDSA", "ED25519", "ED448",
"X25519", "X448", "ELGAMAL", "ECIES", "ECMQV",
];
self.algorithm_family.as_deref().is_some_and(|f| {
let upper = f.to_uppercase();
CLASSICAL_PK.iter().any(|c| upper == *c)
})
}
#[must_use]
pub fn is_weak(&self) -> bool {
const WEAK_FAMILIES: &[&str] = &[
"MD5", "MD4", "MD2", "SHA-1", "DES", "3DES", "TDEA", "RC2", "RC4", "BLOWFISH", "IDEA",
"CAST5",
];
if let Some(family) = &self.algorithm_family {
let upper = family.to_uppercase();
if WEAK_FAMILIES.iter().any(|w| upper == *w) {
return true;
}
}
false
}
#[must_use]
pub fn is_weak_by_name(&self, component_name: &str) -> bool {
if self.is_weak() {
return true;
}
let upper = component_name.to_uppercase();
upper.starts_with("MD5")
|| upper.starts_with("MD4")
|| upper.starts_with("SHA-1")
|| upper.starts_with("DES")
|| upper.starts_with("3DES")
|| upper.starts_with("RC4")
|| upper.starts_with("RC2")
|| upper.starts_with("BLOWFISH")
}
#[must_use]
pub fn effective_security_bits(&self) -> Option<u32> {
self.classical_security_level
}
#[must_use]
pub fn with_algorithm_family(mut self, family: String) -> Self {
self.algorithm_family = Some(family);
self
}
#[must_use]
pub fn with_parameter_set_identifier(mut self, id: String) -> Self {
self.parameter_set_identifier = Some(id);
self
}
#[must_use]
pub fn with_mode(mut self, mode: CryptoMode) -> Self {
self.mode = Some(mode);
self
}
#[must_use]
pub fn with_padding(mut self, padding: CryptoPadding) -> Self {
self.padding = Some(padding);
self
}
#[must_use]
pub fn with_crypto_functions(mut self, funcs: Vec<CryptoFunction>) -> Self {
self.crypto_functions = funcs;
self
}
#[must_use]
pub fn with_execution_environment(mut self, env: ExecutionEnvironment) -> Self {
self.execution_environment = Some(env);
self
}
#[must_use]
pub fn with_implementation_platform(mut self, platform: ImplementationPlatform) -> Self {
self.implementation_platform = Some(platform);
self
}
#[must_use]
pub fn with_certification_level(mut self, levels: Vec<CertificationLevel>) -> Self {
self.certification_level = levels;
self
}
#[must_use]
pub fn with_classical_security_level(mut self, bits: u32) -> Self {
self.classical_security_level = Some(bits);
self
}
#[must_use]
pub fn with_nist_quantum_security_level(mut self, level: u8) -> Self {
self.nist_quantum_security_level = Some(level);
self
}
#[must_use]
pub fn with_elliptic_curve(mut self, curve: String) -> Self {
self.elliptic_curve = Some(curve);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CertificateProperties {
pub subject_name: Option<String>,
pub issuer_name: Option<String>,
pub not_valid_before: Option<DateTime<Utc>>,
pub not_valid_after: Option<DateTime<Utc>>,
pub signature_algorithm_ref: Option<String>,
pub subject_public_key_ref: Option<String>,
pub certificate_format: Option<String>,
pub certificate_extension: Option<String>,
}
impl CertificateProperties {
#[must_use]
pub fn new() -> Self {
Self {
subject_name: None,
issuer_name: None,
not_valid_before: None,
not_valid_after: None,
signature_algorithm_ref: None,
subject_public_key_ref: None,
certificate_format: None,
certificate_extension: None,
}
}
#[must_use]
pub fn is_expired(&self) -> bool {
self.not_valid_after
.is_some_and(|expiry| expiry < Utc::now())
}
#[must_use]
pub fn is_expiring_soon(&self, days: u32) -> bool {
self.not_valid_after.is_some_and(|expiry| {
let threshold = Utc::now() + chrono::Duration::days(i64::from(days));
expiry <= threshold && expiry > Utc::now()
})
}
#[must_use]
pub fn validity_days(&self) -> Option<i64> {
self.not_valid_after
.map(|expiry| (expiry - Utc::now()).num_days())
}
#[must_use]
pub fn with_subject_name(mut self, name: String) -> Self {
self.subject_name = Some(name);
self
}
#[must_use]
pub fn with_issuer_name(mut self, name: String) -> Self {
self.issuer_name = Some(name);
self
}
#[must_use]
pub fn with_not_valid_before(mut self, dt: DateTime<Utc>) -> Self {
self.not_valid_before = Some(dt);
self
}
#[must_use]
pub fn with_not_valid_after(mut self, dt: DateTime<Utc>) -> Self {
self.not_valid_after = Some(dt);
self
}
#[must_use]
pub fn with_signature_algorithm_ref(mut self, r: String) -> Self {
self.signature_algorithm_ref = Some(r);
self
}
#[must_use]
pub fn with_subject_public_key_ref(mut self, r: String) -> Self {
self.subject_public_key_ref = Some(r);
self
}
#[must_use]
pub fn with_certificate_format(mut self, fmt: String) -> Self {
self.certificate_format = Some(fmt);
self
}
#[must_use]
pub fn with_certificate_extension(mut self, ext: String) -> Self {
self.certificate_extension = Some(ext);
self
}
}
impl Default for CertificateProperties {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RelatedCryptoMaterialProperties {
pub material_type: CryptoMaterialType,
pub id: Option<String>,
pub state: Option<CryptoMaterialState>,
pub size: Option<u32>,
pub algorithm_ref: Option<String>,
pub secured_by: Option<SecuredBy>,
pub format: Option<String>,
pub creation_date: Option<DateTime<Utc>>,
pub activation_date: Option<DateTime<Utc>>,
pub update_date: Option<DateTime<Utc>>,
pub expiration_date: Option<DateTime<Utc>>,
}
impl RelatedCryptoMaterialProperties {
#[must_use]
pub fn new(material_type: CryptoMaterialType) -> Self {
Self {
material_type,
id: None,
state: None,
size: None,
algorithm_ref: None,
secured_by: None,
format: None,
creation_date: None,
activation_date: None,
update_date: None,
expiration_date: None,
}
}
#[must_use]
pub fn with_id(mut self, id: String) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn with_state(mut self, state: CryptoMaterialState) -> Self {
self.state = Some(state);
self
}
#[must_use]
pub fn with_size(mut self, bits: u32) -> Self {
self.size = Some(bits);
self
}
#[must_use]
pub fn with_algorithm_ref(mut self, r: String) -> Self {
self.algorithm_ref = Some(r);
self
}
#[must_use]
pub fn with_secured_by(mut self, secured: SecuredBy) -> Self {
self.secured_by = Some(secured);
self
}
#[must_use]
pub fn with_format(mut self, fmt: String) -> Self {
self.format = Some(fmt);
self
}
#[must_use]
pub fn with_creation_date(mut self, dt: DateTime<Utc>) -> Self {
self.creation_date = Some(dt);
self
}
#[must_use]
pub fn with_activation_date(mut self, dt: DateTime<Utc>) -> Self {
self.activation_date = Some(dt);
self
}
#[must_use]
pub fn with_expiration_date(mut self, dt: DateTime<Utc>) -> Self {
self.expiration_date = Some(dt);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ProtocolProperties {
pub protocol_type: ProtocolType,
pub version: Option<String>,
pub cipher_suites: Vec<CipherSuite>,
pub ikev2_transform_types: Option<Ikev2TransformTypes>,
pub crypto_ref_array: Vec<String>,
}
impl ProtocolProperties {
#[must_use]
pub fn new(protocol_type: ProtocolType) -> Self {
Self {
protocol_type,
version: None,
cipher_suites: Vec::new(),
ikev2_transform_types: None,
crypto_ref_array: Vec::new(),
}
}
#[must_use]
pub fn with_version(mut self, version: String) -> Self {
self.version = Some(version);
self
}
#[must_use]
pub fn with_cipher_suites(mut self, suites: Vec<CipherSuite>) -> Self {
self.cipher_suites = suites;
self
}
#[must_use]
pub fn with_ikev2_transform_types(mut self, types: Ikev2TransformTypes) -> Self {
self.ikev2_transform_types = Some(types);
self
}
#[must_use]
pub fn with_crypto_ref_array(mut self, refs: Vec<String>) -> Self {
self.crypto_ref_array = refs;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CipherSuite {
pub name: Option<String>,
pub algorithms: Vec<String>,
pub identifiers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ikev2TransformTypes {
pub encr: Vec<String>,
pub prf: Vec<String>,
pub integ: Vec<String>,
pub ke: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecuredBy {
pub mechanism: String,
pub algorithm_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoPrimitive {
Ae,
BlockCipher,
StreamCipher,
Hash,
Mac,
Signature,
Pke,
Kem,
Kdf,
KeyAgree,
Xof,
Drbg,
Combiner,
Other(String),
Unknown,
}
impl std::fmt::Display for CryptoPrimitive {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ae => write!(f, "ae"),
Self::BlockCipher => write!(f, "block-cipher"),
Self::StreamCipher => write!(f, "stream-cipher"),
Self::Hash => write!(f, "hash"),
Self::Mac => write!(f, "mac"),
Self::Signature => write!(f, "signature"),
Self::Pke => write!(f, "pke"),
Self::Kem => write!(f, "kem"),
Self::Kdf => write!(f, "kdf"),
Self::KeyAgree => write!(f, "key-agree"),
Self::Xof => write!(f, "xof"),
Self::Drbg => write!(f, "drbg"),
Self::Combiner => write!(f, "combiner"),
Self::Other(s) => write!(f, "{s}"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoMode {
Ecb,
Cbc,
Ofb,
Cfb,
Ctr,
Gcm,
Ccm,
Xts,
Other(String),
}
impl std::fmt::Display for CryptoMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ecb => write!(f, "ecb"),
Self::Cbc => write!(f, "cbc"),
Self::Ofb => write!(f, "ofb"),
Self::Cfb => write!(f, "cfb"),
Self::Ctr => write!(f, "ctr"),
Self::Gcm => write!(f, "gcm"),
Self::Ccm => write!(f, "ccm"),
Self::Xts => write!(f, "xts"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoPadding {
Pkcs5,
Oaep,
Pss,
Other(String),
}
impl std::fmt::Display for CryptoPadding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pkcs5 => write!(f, "pkcs5"),
Self::Oaep => write!(f, "oaep"),
Self::Pss => write!(f, "pss"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoFunction {
Keygen,
Encrypt,
Decrypt,
Sign,
Verify,
Digest,
Tag,
KeyDerive,
Encapsulate,
Decapsulate,
Wrap,
Unwrap,
Other(String),
}
impl std::fmt::Display for CryptoFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Keygen => write!(f, "keygen"),
Self::Encrypt => write!(f, "encrypt"),
Self::Decrypt => write!(f, "decrypt"),
Self::Sign => write!(f, "sign"),
Self::Verify => write!(f, "verify"),
Self::Digest => write!(f, "digest"),
Self::Tag => write!(f, "tag"),
Self::KeyDerive => write!(f, "keyderive"),
Self::Encapsulate => write!(f, "encapsulate"),
Self::Decapsulate => write!(f, "decapsulate"),
Self::Wrap => write!(f, "wrap"),
Self::Unwrap => write!(f, "unwrap"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ExecutionEnvironment {
SoftwarePlainRam,
SoftwareEncryptedRam,
SoftwareTee,
Hardware,
Other(String),
}
impl std::fmt::Display for ExecutionEnvironment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SoftwarePlainRam => write!(f, "software-plain-ram"),
Self::SoftwareEncryptedRam => write!(f, "software-encrypted-ram"),
Self::SoftwareTee => write!(f, "software-tee"),
Self::Hardware => write!(f, "hardware"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ImplementationPlatform {
X86_32,
X86_64,
Armv7A,
Armv7M,
Armv8A,
S390x,
Generic,
Other(String),
}
impl std::fmt::Display for ImplementationPlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::X86_32 => write!(f, "x86_32"),
Self::X86_64 => write!(f, "x86_64"),
Self::Armv7A => write!(f, "armv7-a"),
Self::Armv7M => write!(f, "armv7-m"),
Self::Armv8A => write!(f, "armv8-a"),
Self::S390x => write!(f, "s390x"),
Self::Generic => write!(f, "generic"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CertificationLevel {
None,
Fips140_1L1,
Fips140_1L2,
Fips140_1L3,
Fips140_1L4,
Fips140_2L1,
Fips140_2L2,
Fips140_2L3,
Fips140_2L4,
Fips140_3L1,
Fips140_3L2,
Fips140_3L3,
Fips140_3L4,
CcEal1,
CcEal2,
CcEal3,
CcEal4,
CcEal5,
CcEal6,
CcEal7,
Other(String),
}
impl std::fmt::Display for CertificationLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Fips140_1L1 => write!(f, "fips140-1-l1"),
Self::Fips140_1L2 => write!(f, "fips140-1-l2"),
Self::Fips140_1L3 => write!(f, "fips140-1-l3"),
Self::Fips140_1L4 => write!(f, "fips140-1-l4"),
Self::Fips140_2L1 => write!(f, "fips140-2-l1"),
Self::Fips140_2L2 => write!(f, "fips140-2-l2"),
Self::Fips140_2L3 => write!(f, "fips140-2-l3"),
Self::Fips140_2L4 => write!(f, "fips140-2-l4"),
Self::Fips140_3L1 => write!(f, "fips140-3-l1"),
Self::Fips140_3L2 => write!(f, "fips140-3-l2"),
Self::Fips140_3L3 => write!(f, "fips140-3-l3"),
Self::Fips140_3L4 => write!(f, "fips140-3-l4"),
Self::CcEal1 => write!(f, "cc-eal1"),
Self::CcEal2 => write!(f, "cc-eal2"),
Self::CcEal3 => write!(f, "cc-eal3"),
Self::CcEal4 => write!(f, "cc-eal4"),
Self::CcEal5 => write!(f, "cc-eal5"),
Self::CcEal6 => write!(f, "cc-eal6"),
Self::CcEal7 => write!(f, "cc-eal7"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoMaterialType {
PublicKey,
PrivateKey,
SymmetricKey,
SecretKey,
KeyPair,
Ciphertext,
Signature,
Digest,
Iv,
Nonce,
Seed,
Salt,
SharedSecret,
Tag,
Password,
Credential,
Token,
Other(String),
Unknown,
}
impl std::fmt::Display for CryptoMaterialType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PublicKey => write!(f, "public-key"),
Self::PrivateKey => write!(f, "private-key"),
Self::SymmetricKey => write!(f, "symmetric-key"),
Self::SecretKey => write!(f, "secret-key"),
Self::KeyPair => write!(f, "key-pair"),
Self::Ciphertext => write!(f, "ciphertext"),
Self::Signature => write!(f, "signature"),
Self::Digest => write!(f, "digest"),
Self::Iv => write!(f, "initialization-vector"),
Self::Nonce => write!(f, "nonce"),
Self::Seed => write!(f, "seed"),
Self::Salt => write!(f, "salt"),
Self::SharedSecret => write!(f, "shared-secret"),
Self::Tag => write!(f, "tag"),
Self::Password => write!(f, "password"),
Self::Credential => write!(f, "credential"),
Self::Token => write!(f, "token"),
Self::Other(s) => write!(f, "{s}"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CryptoMaterialState {
PreActivation,
Active,
Suspended,
Deactivated,
Compromised,
Destroyed,
}
impl std::fmt::Display for CryptoMaterialState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PreActivation => write!(f, "pre-activation"),
Self::Active => write!(f, "active"),
Self::Suspended => write!(f, "suspended"),
Self::Deactivated => write!(f, "deactivated"),
Self::Compromised => write!(f, "compromised"),
Self::Destroyed => write!(f, "destroyed"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ProtocolType {
Tls,
Dtls,
Ipsec,
Ssh,
Srtp,
Wireguard,
Ikev1,
Ikev2,
Zrtp,
Mikey,
Other(String),
Unknown,
}
impl std::fmt::Display for ProtocolType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tls => write!(f, "tls"),
Self::Dtls => write!(f, "dtls"),
Self::Ipsec => write!(f, "ipsec"),
Self::Ssh => write!(f, "ssh"),
Self::Srtp => write!(f, "srtp"),
Self::Wireguard => write!(f, "wireguard"),
Self::Ikev1 => write!(f, "ikev1"),
Self::Ikev2 => write!(f, "ikev2"),
Self::Zrtp => write!(f, "zrtp"),
Self::Mikey => write!(f, "mikey"),
Self::Other(s) => write!(f, "{s}"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PqcKind {
MlKem,
MlDsa,
SlhDsa,
FnDsa,
Lms,
Xmss,
Hss,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AlgorithmClass {
Broken,
ClassicalQuantumVulnerable,
Symmetric,
Sha2,
Sha3,
OtherHash,
PostQuantum(PqcKind),
Unknown,
}
impl AlgorithmClass {
#[must_use]
pub const fn severity_rank(self) -> u8 {
match self {
Self::Broken => 5,
Self::ClassicalQuantumVulnerable => 4,
Self::Symmetric | Self::Sha2 | Self::Sha3 | Self::OtherHash => 2,
Self::PostQuantum(_) => 1,
Self::Unknown => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlgorithmClassification {
pub family: Option<String>,
pub parameter: Option<String>,
pub class: AlgorithmClass,
}
impl AlgorithmClassification {
#[must_use]
pub const fn unknown() -> Self {
Self {
family: None,
parameter: None,
class: AlgorithmClass::Unknown,
}
}
#[must_use]
pub fn parameter_bits(&self) -> Option<u32> {
self.parameter.as_deref().and_then(|p| p.parse().ok())
}
#[must_use]
pub fn label(&self) -> String {
let Some(family) = self.family.as_deref() else {
return "unclassified".to_string();
};
match (family, self.parameter.as_deref()) {
("SHA-2", Some(p)) => format!("SHA-{p}"),
("SHA-3", Some(p)) => format!("SHA3-{p}"),
(f, Some(p)) => format!("{f}-{p}"),
(f, None) => f.to_string(),
}
}
}
fn family_class(canonical: &str) -> AlgorithmClass {
use AlgorithmClass as C;
match canonical {
"MD2" | "MD4" | "MD5" | "SHA-1" | "DES" | "3DES" | "RC2" | "RC4" | "BLOWFISH" | "IDEA"
| "CAST5" | "SKIPJACK" => C::Broken,
"RSA" | "DSA" | "DH" | "ECDH" | "ECDSA" | "EDDSA" | "ED25519" | "ED448" | "X25519"
| "X448" | "ELGAMAL" | "ECIES" | "ECMQV" | "EC" | "SM2" | "SM9" | "GOST-R-34.10" => {
C::ClassicalQuantumVulnerable
}
"AES" | "CHACHA20" | "CAMELLIA" | "ARIA" | "SEED" | "SERPENT" | "TWOFISH" | "SM4"
| "MAGMA" | "KUZNYECHIK" => C::Symmetric,
"SHA-2" => C::Sha2,
"SHA-3" => C::Sha3,
"SM3" | "GOST-R-34.11" => C::OtherHash,
"ML-KEM" => C::PostQuantum(PqcKind::MlKem),
"ML-DSA" => C::PostQuantum(PqcKind::MlDsa),
"SLH-DSA" => C::PostQuantum(PqcKind::SlhDsa),
"FN-DSA" => C::PostQuantum(PqcKind::FnDsa),
"LMS" => C::PostQuantum(PqcKind::Lms),
"XMSS" => C::PostQuantum(PqcKind::Xmss),
"HSS" => C::PostQuantum(PqcKind::Hss),
_ => C::Unknown,
}
}
fn alias_lookup(token: &str) -> Option<(&'static str, Option<&'static str>, bool)> {
let hit: (&'static str, Option<&'static str>) = match token {
"MD2" => ("MD2", None),
"MD4" => ("MD4", None),
"MD5" => ("MD5", None),
"SHA-1" | "SHA1" | "SHA" => ("SHA-1", None),
"DES" => ("DES", None),
"3DES" | "TDES" | "TDEA" | "DES3" | "DESEDE" | "DESEDE3" | "DES-EDE" | "DES-EDE2"
| "DES-EDE3" | "3DES-EDE" | "TRIPLE-DES" | "TRIPLEDES" => ("3DES", None),
"RC2" => ("RC2", None),
"RC4" | "ARC4" | "ARCFOUR" => ("RC4", None),
"BLOWFISH" => ("BLOWFISH", None),
"IDEA" => ("IDEA", None),
"CAST5" | "CAST-128" | "CAST128" => ("CAST5", None),
"SKIPJACK" => ("SKIPJACK", None),
"RSA" | "RSAES" | "RSASSA" | "RSA-PSS" | "RSA-OAEP" | "RSAES-OAEP" | "RSASSA-PSS" => {
("RSA", None)
}
"DSA" | "DSS" => ("DSA", None),
"DH" | "DHE" | "FFDHE" | "EDH" | "ADH" | "DIFFIE-HELLMAN" => ("DH", None),
"ECDH" | "ECDHE" | "XDH" => ("ECDH", None),
"ECDSA" => ("ECDSA", None),
"EDDSA" => ("EDDSA", None),
"ED25519" => ("ED25519", None),
"ED448" => ("ED448", None),
"X25519" => ("X25519", None),
"X448" => ("X448", None),
"ELGAMAL" | "EL-GAMAL" => ("ELGAMAL", None),
"ECIES" => ("ECIES", None),
"ECMQV" => ("ECMQV", None),
"EC" | "ECC" => ("EC", None),
"SM2" => ("SM2", None),
"SM9" => ("SM9", None),
"GOST" | "GOST3410" | "GOSTR3410" | "GOST-R-34-10" => ("GOST-R-34.10", None),
"AES" | "RIJNDAEL" => ("AES", None),
"CHACHA" | "CHACHA20" | "XCHACHA20" | "CHACHA20-POLY1305" => ("CHACHA20", None),
"CAMELLIA" => ("CAMELLIA", None),
"ARIA" => ("ARIA", None),
"SEED" => ("SEED", None),
"SERPENT" => ("SERPENT", None),
"TWOFISH" => ("TWOFISH", None),
"SM4" => ("SM4", None),
"MAGMA" => ("MAGMA", None),
"KUZNYECHIK" | "KUZNECHIK" => ("KUZNYECHIK", None),
"SM3" => ("SM3", None),
"GOST3411" | "GOSTR3411" | "GOST-R-34-11" | "STREEBOG" => ("GOST-R-34.11", None),
"SHA-2" | "SHA2" => ("SHA-2", None),
"SHA-224" | "SHA224" => ("SHA-2", Some("224")),
"SHA-256" | "SHA256" => ("SHA-2", Some("256")),
"SHA-384" | "SHA384" => ("SHA-2", Some("384")),
"SHA-512" | "SHA512" => ("SHA-2", Some("512")),
"SHA-512-256" | "SHA512-256" => ("SHA-2", Some("256")),
"SHA-512-224" | "SHA512-224" => ("SHA-2", Some("224")),
"SHA-3" | "SHA3" | "KECCAK" | "SHAKE" | "SHAKE128" | "SHAKE256" => ("SHA-3", None),
"SHA3-224" => ("SHA-3", Some("224")),
"SHA3-256" => ("SHA-3", Some("256")),
"SHA3-384" => ("SHA-3", Some("384")),
"SHA3-512" => ("SHA-3", Some("512")),
"ML-KEM" | "MLKEM" | "KYBER" | "CRYSTALS-KYBER" => ("ML-KEM", None),
"ML-DSA" | "MLDSA" => ("ML-DSA", None),
"DILITHIUM" | "CRYSTALS-DILITHIUM" => {
return Some(("ML-DSA", None, true));
}
"SLH-DSA" | "SLHDSA" | "SPHINCS" | "SPHINCS+" | "SPHINCSPLUS" => ("SLH-DSA", None),
"FALCON" | "FN-DSA" | "FNDSA" => ("FN-DSA", None),
"LMS" | "HSS-LMS" | "LMS-HSS" => ("LMS", None),
"XMSS" | "XMSS-MT" | "XMSSMT" => ("XMSS", None),
"HSS" => ("HSS", None),
_ => return None,
};
Some((hit.0, hit.1, false))
}
fn normalize_algo_token(s: &str) -> String {
s.trim()
.chars()
.map(|c| match c {
'_' | ' ' | '/' | '.' => '-',
other => other.to_ascii_uppercase(),
})
.collect()
}
fn map_dilithium_param(p: &str) -> String {
match p {
"2" => "44".to_string(),
"3" => "65".to_string(),
"5" => "87".to_string(),
other => other.to_string(),
}
}
fn classify_token(token: &str) -> Option<(&'static str, Option<String>)> {
let t = normalize_algo_token(token);
let finish = |family: &'static str, param: Option<String>, dilithium: bool| {
let param = if dilithium {
param.map(|p| map_dilithium_param(&p))
} else {
param
};
Some((family, param))
};
if let Some((family, param, dilithium)) = alias_lookup(&t) {
return finish(family, param.map(str::to_string), dilithium);
}
if t.starts_with("BRAINPOOL") {
return Some(("EC", None));
}
if let Some((base, digits)) = t.rsplit_once('-')
&& !digits.is_empty()
&& digits.bytes().all(|b| b.is_ascii_digit())
&& let Some((family, param, dilithium)) = alias_lookup(base)
{
let param = param
.map(str::to_string)
.or_else(|| Some(digits.to_string()));
return finish(family, param, dilithium);
}
let digit_start = t.len() - t.bytes().rev().take_while(u8::is_ascii_digit).count();
if digit_start > 0 && digit_start < t.len() {
let (alpha, digits) = t.split_at(digit_start);
if let Some((family, param, dilithium)) = alias_lookup(alpha.trim_end_matches('-')) {
let param = param
.map(str::to_string)
.or_else(|| Some(digits.to_string()));
return finish(family, param, dilithium);
}
}
None
}
#[must_use]
pub fn classify_algorithm_names(name: &str) -> Vec<AlgorithmClassification> {
classify_names_impl(name, false)
}
#[must_use]
pub fn classify_algorithm_names_guarded(name: &str) -> Vec<AlgorithmClassification> {
classify_names_impl(name, true)
}
fn is_overgeneric_span(span: &str) -> bool {
let base = span
.trim_end_matches(|c: char| c.is_ascii_digit())
.trim_end_matches('-');
matches!(base, "SEED" | "EC" | "ECC")
}
fn classify_names_impl(name: &str, guarded: bool) -> Vec<AlgorithmClassification> {
let upper = name.to_uppercase();
let tokens: Vec<&str> = upper
.split(|c: char| !(c.is_ascii_alphanumeric() || c == '+'))
.filter(|t| !t.is_empty())
.collect();
let mut out: Vec<AlgorithmClassification> = Vec::new();
let mut i = 0;
while i < tokens.len() {
if tokens[i].bytes().all(|b| b.is_ascii_digit()) {
i += 1;
continue;
}
let max_span = 3.min(tokens.len() - i);
let mut advanced = false;
for span in (1..=max_span).rev() {
let joined = tokens[i..i + span].join("-");
if let Some((family, parameter)) = classify_token(&joined) {
if guarded && is_overgeneric_span(&joined) {
continue;
}
let cls = AlgorithmClassification {
family: Some(family.to_string()),
parameter,
class: family_class(family),
};
if !out.contains(&cls) {
out.push(cls);
}
i += span;
advanced = true;
break;
}
}
if !advanced {
i += 1;
}
}
out
}
#[must_use]
pub fn worst_classification(
mentions: Vec<AlgorithmClassification>,
) -> Option<AlgorithmClassification> {
let mut worst: Option<AlgorithmClassification> = None;
for m in mentions {
if worst
.as_ref()
.is_none_or(|w| m.class.severity_rank() > w.class.severity_rank())
{
worst = Some(m);
}
}
worst
}
fn classify_oid(oid: &str) -> Option<(&'static str, Option<String>)> {
let o = oid.trim();
let exact: Option<(&'static str, Option<&'static str>)> = match o {
"1.3.14.3.2.26" => Some(("SHA-1", None)),
"1.2.840.113549.2.2" => Some(("MD2", None)),
"1.2.840.113549.2.4" => Some(("MD4", None)),
"1.2.840.113549.2.5" => Some(("MD5", None)),
"1.3.14.3.2.7" => Some(("DES", None)),
"1.2.840.113549.3.7" => Some(("3DES", None)),
"1.2.840.113549.3.2" => Some(("RC2", None)),
"1.2.840.113549.3.4" => Some(("RC4", None)),
"1.2.840.10040.4.1" | "1.2.840.10040.4.3" => Some(("DSA", None)),
"1.2.840.113549.1.3.1" | "1.2.840.10046.2.1" => Some(("DH", None)),
"1.3.101.110" => Some(("X25519", None)),
"1.3.101.111" => Some(("X448", None)),
"1.3.101.112" => Some(("ED25519", None)),
"1.3.101.113" => Some(("ED448", None)),
"2.16.840.1.101.3.4.2.1" => Some(("SHA-2", Some("256"))),
"2.16.840.1.101.3.4.2.2" => Some(("SHA-2", Some("384"))),
"2.16.840.1.101.3.4.2.3" => Some(("SHA-2", Some("512"))),
"2.16.840.1.101.3.4.2.4" => Some(("SHA-2", Some("224"))),
"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"))),
"2.16.840.1.101.3.4.2.8" => Some(("SHA-3", Some("256"))),
"2.16.840.1.101.3.4.2.9" => Some(("SHA-3", Some("384"))),
"2.16.840.1.101.3.4.2.10" => Some(("SHA-3", Some("512"))),
"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)),
_ => None,
};
if let Some((family, param)) = exact {
return Some((family, param.map(str::to_string)));
}
if o.starts_with("1.2.840.113549.1.1.") {
return Some(("RSA", None));
}
if o.starts_with("1.2.840.10045.4.") {
return Some(("ECDSA", None));
}
if o.starts_with("1.2.840.10045.") {
return Some(("EC", None));
}
if o.starts_with("1.3.132.") {
return Some(("EC", None));
}
if let Some(rest) = o.strip_prefix("2.16.840.1.101.3.4.1.")
&& let Ok(n) = rest.parse::<u32>()
{
let bits = match n {
1..=10 => Some("128"),
21..=30 => Some("192"),
41..=50 => Some("256"),
_ => None,
};
return Some(("AES", bits.map(str::to_string)));
}
if let Some(rest) = o.strip_prefix("2.16.840.1.101.3.4.4.")
&& let Ok(n) = rest.parse::<u32>()
{
let param = match n {
1 => Some("512"),
2 => Some("768"),
3 => Some("1024"),
_ => None,
};
return Some(("ML-KEM", param.map(str::to_string)));
}
if let Some(rest) = o.strip_prefix("2.16.840.1.101.3.4.3.")
&& let Ok(n) = rest.parse::<u32>()
{
return match n {
1..=8 => Some(("DSA", None)), 9..=12 => Some(("ECDSA", None)), 13..=16 => Some(("RSA", None)), 17 => Some(("ML-DSA", Some("44".to_string()))),
18 => Some(("ML-DSA", Some("65".to_string()))),
19 => Some(("ML-DSA", Some("87".to_string()))),
20..=31 => Some(("SLH-DSA", None)),
32 => Some(("ML-DSA", Some("44".to_string()))),
33 => Some(("ML-DSA", Some("65".to_string()))),
34 => Some(("ML-DSA", Some("87".to_string()))),
35..=46 => Some(("SLH-DSA", None)),
_ => None,
};
}
if o == "1.2.156.10197.1.301" || o.starts_with("1.2.156.10197.1.301.") {
return Some(("SM2", None));
}
if o == "1.2.156.10197.1.401" || o.starts_with("1.2.156.10197.1.401.") {
return Some(("SM3", None));
}
if o == "1.2.156.10197.1.104" || o.starts_with("1.2.156.10197.1.104.") {
return Some(("SM4", None));
}
if matches!(
o,
"1.2.643.2.2.19" | "1.2.643.2.2.20" | "1.2.643.2.2.3" | "1.2.643.2.2.4"
) || o.starts_with("1.2.643.7.1.1.1.")
|| o.starts_with("1.2.643.7.1.1.3.")
{
return Some(("GOST-R-34.10", None));
}
if o == "1.2.643.2.2.9" || o.starts_with("1.2.643.7.1.1.2.") {
return Some(("GOST-R-34.11", None));
}
if o.starts_with("1.3.36.3.3.2.8.1.") {
return Some(("EC", None));
}
None
}
#[must_use]
pub fn classify_algorithm(
family: Option<&str>,
name: Option<&str>,
oid: Option<&str>,
parameter_set: Option<&str>,
elliptic_curve: Option<&str>,
) -> AlgorithmClassification {
let fill_parameter = |mut cls: AlgorithmClassification| {
if cls.parameter.is_none() {
cls.parameter = parameter_set
.map(str::trim)
.filter(|p| !p.is_empty())
.map(str::to_string);
}
if cls.parameter.is_none()
&& let Some(n) = name
&& let Some(named) = classify_algorithm_names(n)
.into_iter()
.find(|c| c.family == cls.family)
{
cls.parameter = named.parameter;
}
cls
};
if let Some(f) = family.map(str::trim).filter(|f| !f.is_empty()) {
if normalize_algo_token(f) == "SHA"
&& let Some(p @ ("224" | "256" | "384" | "512")) = parameter_set.map(str::trim)
{
return AlgorithmClassification {
family: Some("SHA-2".to_string()),
parameter: Some(p.to_string()),
class: AlgorithmClass::Sha2,
};
}
if let Some((canonical, parameter)) = classify_token(f) {
return fill_parameter(AlgorithmClassification {
family: Some(canonical.to_string()),
parameter,
class: family_class(canonical),
});
}
} else if let Some(o) = oid.map(str::trim).filter(|o| !o.is_empty()) {
if let Some((canonical, parameter)) = classify_oid(o) {
return fill_parameter(AlgorithmClassification {
family: Some(canonical.to_string()),
parameter,
class: family_class(canonical),
});
}
}
if family.is_some()
&& let Some(o) = oid.map(str::trim).filter(|o| !o.is_empty())
&& let Some((canonical, parameter)) = classify_oid(o)
{
return fill_parameter(AlgorithmClassification {
family: Some(canonical.to_string()),
parameter,
class: family_class(canonical),
});
}
if let Some(f) = family.map(str::trim).filter(|f| !f.is_empty())
&& let Some(cls) = worst_classification(classify_algorithm_names(f))
{
return fill_parameter(cls);
}
if let Some(curve) = elliptic_curve.map(str::trim).filter(|c| !c.is_empty()) {
return AlgorithmClassification {
family: Some("EC".to_string()),
parameter: Some(curve.to_string()),
class: AlgorithmClass::ClassicalQuantumVulnerable,
};
}
if family.is_none()
&& oid.is_none()
&& let Some(n) = name
&& let Some(cls) = worst_classification(classify_algorithm_names_guarded(n))
{
return fill_parameter(cls);
}
AlgorithmClassification {
family: None,
parameter: parameter_set.map(str::to_string),
class: AlgorithmClass::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn algorithm_is_quantum_safe() {
let algo =
AlgorithmProperties::new(CryptoPrimitive::Kem).with_nist_quantum_security_level(5);
assert!(algo.is_quantum_safe());
let classical =
AlgorithmProperties::new(CryptoPrimitive::Pke).with_nist_quantum_security_level(0);
assert!(!classical.is_quantum_safe());
let unknown = AlgorithmProperties::new(CryptoPrimitive::Pke);
assert!(!unknown.is_quantum_safe());
}
#[test]
fn algorithm_is_hybrid_pqc() {
let hybrid = AlgorithmProperties::new(CryptoPrimitive::Combiner);
assert!(hybrid.is_hybrid_pqc());
let normal = AlgorithmProperties::new(CryptoPrimitive::Kem);
assert!(!normal.is_hybrid_pqc());
}
#[test]
fn algorithm_is_weak() {
let md5 = AlgorithmProperties::new(CryptoPrimitive::Hash)
.with_algorithm_family("MD5".to_string());
assert!(md5.is_weak());
let sha1 = AlgorithmProperties::new(CryptoPrimitive::Hash)
.with_algorithm_family("SHA-1".to_string());
assert!(sha1.is_weak());
let des = AlgorithmProperties::new(CryptoPrimitive::BlockCipher)
.with_algorithm_family("DES".to_string());
assert!(des.is_weak());
let rc4 = AlgorithmProperties::new(CryptoPrimitive::StreamCipher)
.with_algorithm_family("RC4".to_string());
assert!(rc4.is_weak());
let aes =
AlgorithmProperties::new(CryptoPrimitive::Ae).with_algorithm_family("AES".to_string());
assert!(!aes.is_weak());
let ml_kem = AlgorithmProperties::new(CryptoPrimitive::Kem)
.with_algorithm_family("ML-KEM".to_string());
assert!(!ml_kem.is_weak());
}
#[test]
fn certificate_expiry() {
let expired = CertificateProperties::new()
.with_not_valid_after(Utc::now() - chrono::Duration::days(1));
assert!(expired.is_expired());
assert!(!expired.is_expiring_soon(90));
let valid = CertificateProperties::new()
.with_not_valid_after(Utc::now() + chrono::Duration::days(365));
assert!(!valid.is_expired());
assert!(!valid.is_expiring_soon(90));
let expiring = CertificateProperties::new()
.with_not_valid_after(Utc::now() + chrono::Duration::days(30));
assert!(!expiring.is_expired());
assert!(expiring.is_expiring_soon(90));
}
#[test]
fn certificate_validity_days() {
let no_expiry = CertificateProperties::new();
assert!(no_expiry.validity_days().is_none());
let expired = CertificateProperties::new()
.with_not_valid_after(Utc::now() - chrono::Duration::days(10));
assert!(expired.validity_days().unwrap() < 0);
let future = CertificateProperties::new()
.with_not_valid_after(Utc::now() + chrono::Duration::days(100));
let days = future.validity_days().unwrap();
assert!(days >= 99 && days <= 100);
}
#[test]
fn crypto_properties_builder() {
let props = CryptoProperties::new(CryptoAssetType::Algorithm)
.with_oid("2.16.840.1.101.3.4.1.46".to_string())
.with_algorithm_properties(
AlgorithmProperties::new(CryptoPrimitive::Ae)
.with_algorithm_family("AES".to_string())
.with_mode(CryptoMode::Gcm)
.with_classical_security_level(256)
.with_nist_quantum_security_level(1),
);
assert_eq!(props.asset_type, CryptoAssetType::Algorithm);
assert_eq!(props.oid.as_deref(), Some("2.16.840.1.101.3.4.1.46"));
let algo = props.algorithm_properties.unwrap();
assert_eq!(algo.primitive, CryptoPrimitive::Ae);
assert_eq!(algo.algorithm_family.as_deref(), Some("AES"));
assert_eq!(algo.mode, Some(CryptoMode::Gcm));
assert_eq!(algo.classical_security_level, Some(256));
assert!(algo.is_quantum_safe());
assert!(!algo.is_weak());
}
#[test]
fn display_impls() {
assert_eq!(CryptoAssetType::Algorithm.to_string(), "algorithm");
assert_eq!(
CryptoAssetType::RelatedCryptoMaterial.to_string(),
"related-crypto-material"
);
assert_eq!(CryptoPrimitive::Kem.to_string(), "kem");
assert_eq!(CryptoPrimitive::Combiner.to_string(), "combiner");
assert_eq!(CryptoMode::Gcm.to_string(), "gcm");
assert_eq!(CryptoFunction::Encapsulate.to_string(), "encapsulate");
assert_eq!(CryptoMaterialType::PublicKey.to_string(), "public-key");
assert_eq!(CryptoMaterialState::Compromised.to_string(), "compromised");
assert_eq!(ProtocolType::Tls.to_string(), "tls");
assert_eq!(CertificationLevel::Fips140_3L1.to_string(), "fips140-3-l1");
assert_eq!(ExecutionEnvironment::Hardware.to_string(), "hardware");
assert_eq!(ImplementationPlatform::X86_64.to_string(), "x86_64");
}
#[test]
fn protocol_builder() {
let proto = ProtocolProperties::new(ProtocolType::Tls)
.with_version("1.3".to_string())
.with_cipher_suites(vec![CipherSuite {
name: Some("TLS_AES_256_GCM_SHA384".to_string()),
algorithms: vec!["algo/aes-256-gcm".to_string()],
identifiers: vec!["0x13".to_string(), "0x02".to_string()],
}]);
assert_eq!(proto.protocol_type, ProtocolType::Tls);
assert_eq!(proto.version.as_deref(), Some("1.3"));
assert_eq!(proto.cipher_suites.len(), 1);
}
fn cls(
family: Option<&str>,
name: Option<&str>,
oid: Option<&str>,
param: Option<&str>,
curve: Option<&str>,
) -> AlgorithmClassification {
classify_algorithm(family, name, oid, param, curve)
}
#[test]
fn classify_family_spelling_variants() {
for (input, family, class) in [
("SHA1", "SHA-1", AlgorithmClass::Broken),
("sha-1", "SHA-1", AlgorithmClass::Broken),
("TDES", "3DES", AlgorithmClass::Broken),
("DES-EDE3", "3DES", AlgorithmClass::Broken),
("3DES-EDE", "3DES", AlgorithmClass::Broken),
("ARC4", "RC4", AlgorithmClass::Broken),
("ARCFOUR", "RC4", AlgorithmClass::Broken),
(
"Ed25519",
"ED25519",
AlgorithmClass::ClassicalQuantumVulnerable,
),
("ECIES", "ECIES", AlgorithmClass::ClassicalQuantumVulnerable),
("ECDHE", "ECDH", AlgorithmClass::ClassicalQuantumVulnerable),
("EC", "EC", AlgorithmClass::ClassicalQuantumVulnerable),
("ChaCha20", "CHACHA20", AlgorithmClass::Symmetric),
("Camellia", "CAMELLIA", AlgorithmClass::Symmetric),
] {
let c = cls(Some(input), None, None, None, None);
assert_eq!(c.family.as_deref(), Some(family), "family for {input}");
assert_eq!(c.class, class, "class for {input}");
}
}
#[test]
fn classify_size_in_family_string() {
let c = cls(Some("ML-KEM-768"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("ML-KEM"));
assert_eq!(c.parameter.as_deref(), Some("768"));
assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::MlKem));
let c = cls(Some("AES-128"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("AES"));
assert_eq!(c.parameter.as_deref(), Some("128"));
let c = cls(Some("AES128"), None, None, None, None);
assert_eq!(c.parameter.as_deref(), Some("128"));
let c = cls(Some("RSA-2048"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("RSA"));
assert_eq!(c.parameter.as_deref(), Some("2048"));
assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
let c = cls(Some("SHA-256"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("SHA-2"));
assert_eq!(c.parameter.as_deref(), Some("256"));
assert_eq!(c.class, AlgorithmClass::Sha2);
assert_eq!(c.label(), "SHA-256");
}
#[test]
fn classify_round3_pqc_names() {
let c = cls(Some("Kyber"), None, None, Some("768"), None);
assert_eq!(c.family.as_deref(), Some("ML-KEM"));
assert_eq!(c.parameter.as_deref(), Some("768"));
let c = cls(Some("Kyber-1024"), None, None, None, None);
assert_eq!(c.parameter.as_deref(), Some("1024"));
let c = cls(Some("Dilithium-3"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("ML-DSA"));
assert_eq!(c.parameter.as_deref(), Some("65"));
let c = cls(Some("SPHINCS+"), None, None, None, None);
assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::SlhDsa));
let c = cls(Some("Falcon"), None, None, None, None);
assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::FnDsa));
}
#[test]
fn classify_by_oid() {
let c = cls(None, None, Some("1.2.840.113549.1.1.1"), Some("2048"), None);
assert_eq!(c.family.as_deref(), Some("RSA"));
assert_eq!(c.parameter.as_deref(), Some("2048"));
assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
assert_eq!(
cls(None, None, Some("1.3.14.3.2.26"), None, None).class,
AlgorithmClass::Broken
);
assert_eq!(
cls(None, None, Some("1.2.840.113549.2.5"), None, None).class,
AlgorithmClass::Broken
);
let c = cls(None, None, Some("2.16.840.1.101.3.4.1.2"), None, None);
assert_eq!(c.family.as_deref(), Some("AES"));
assert_eq!(c.parameter.as_deref(), Some("128"));
let c = cls(None, None, Some("2.16.840.1.101.3.4.1.46"), None, None);
assert_eq!(c.parameter.as_deref(), Some("256"));
let c = cls(None, None, Some("2.16.840.1.101.3.4.2.2"), None, None);
assert_eq!(c.class, AlgorithmClass::Sha2);
assert_eq!(c.parameter.as_deref(), Some("384"));
let c = cls(None, None, Some("2.16.840.1.101.3.4.4.3"), None, None);
assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::MlKem));
assert_eq!(c.parameter.as_deref(), Some("1024"));
let c = cls(None, None, Some("2.16.840.1.101.3.4.3.19"), None, None);
assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::MlDsa));
assert_eq!(c.parameter.as_deref(), Some("87"));
let c = cls(None, None, Some("2.16.840.1.101.3.4.3.24"), None, None);
assert_eq!(c.class, AlgorithmClass::PostQuantum(PqcKind::SlhDsa));
assert_eq!(
cls(None, None, Some("1.3.101.112"), None, None)
.family
.as_deref(),
Some("ED25519")
);
assert_eq!(
cls(None, None, Some("1.2.840.10045.4.3.2"), None, None).class,
AlgorithmClass::ClassicalQuantumVulnerable
);
assert_eq!(
cls(None, None, Some("1.2.840.10045.3.1.7"), None, None).class,
AlgorithmClass::ClassicalQuantumVulnerable
);
}
#[test]
fn classify_name_fallback_only_without_family_and_oid() {
let c = cls(None, Some("AES-128-CBC"), None, None, None);
assert_eq!(c.family.as_deref(), Some("AES"));
assert_eq!(c.parameter.as_deref(), Some("128"));
let c = cls(None, Some("RSA-2048-PKCS1"), None, None, None);
assert_eq!(c.family.as_deref(), Some("RSA"));
let c = cls(
Some("proprietary-frobnicator"),
Some("RSA-2048"),
None,
None,
None,
);
assert_eq!(c.class, AlgorithmClass::Unknown);
let c = cls(None, Some("RSA-2048"), Some("9.9.9.9"), None, None);
assert_eq!(c.class, AlgorithmClass::Unknown);
let c = cls(None, Some("DESCRIPTOR-HANDLER"), None, None, None);
assert_eq!(c.class, AlgorithmClass::Unknown);
}
#[test]
fn classify_elliptic_curve_field() {
let c = cls(None, None, None, None, Some("secg/secp256r1"));
assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
assert_eq!(c.family.as_deref(), Some("EC"));
assert_eq!(c.parameter.as_deref(), Some("secg/secp256r1"));
}
#[test]
fn classify_name_enriches_missing_parameter() {
let c = cls(Some("AES"), Some("AES-256-GCM"), None, None, None);
assert_eq!(c.parameter.as_deref(), Some("256"));
}
#[test]
fn classify_cipher_suite_names() {
let found = classify_algorithm_names("TLS_RSA_WITH_RC4_128_SHA");
let families: Vec<_> = found.iter().filter_map(|c| c.family.as_deref()).collect();
assert!(families.contains(&"RSA"), "{families:?}");
assert!(families.contains(&"RC4"), "{families:?}");
assert!(families.contains(&"SHA-1"), "{families:?}");
let found = classify_algorithm_names("TLS_AES_256_GCM_SHA384_ML_KEM_1024");
assert!(
found.iter().any(
|c| c.family.as_deref() == Some("AES") && c.parameter.as_deref() == Some("256")
)
);
assert!(
found
.iter()
.any(|c| c.family.as_deref() == Some("SHA-2")
&& c.parameter.as_deref() == Some("384"))
);
assert!(found.iter().any(
|c| c.family.as_deref() == Some("ML-KEM") && c.parameter.as_deref() == Some("1024")
));
assert!(!found.iter().any(|c| c.class == AlgorithmClass::Broken));
}
#[test]
fn classify_compound_family_mode_suffixes() {
for (family, canonical, param, class) in [
("DES-CBC", "DES", None, AlgorithmClass::Broken),
("3DES-EDE-CBC", "3DES", None, AlgorithmClass::Broken),
("AES-128-CBC", "AES", Some("128"), AlgorithmClass::Symmetric),
("AES-256-GCM", "AES", Some("256"), AlgorithmClass::Symmetric),
(
"RSA/ECB/PKCS1Padding",
"RSA",
None,
AlgorithmClass::ClassicalQuantumVulnerable,
),
] {
let c = cls(Some(family), None, None, None, None);
assert_eq!(c.family.as_deref(), Some(canonical), "family for {family}");
assert_eq!(c.parameter.as_deref(), param, "parameter for {family}");
assert_eq!(c.class, class, "class for {family}");
}
let c = cls(Some("DES-CBC-HMAC-SHA384"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("DES"));
assert_eq!(c.class, AlgorithmClass::Broken);
for family in ["Hybrid-KEM", "proprietary-frobnicator"] {
let c = cls(Some(family), None, None, None, None);
assert_eq!(c.class, AlgorithmClass::Unknown, "class for {family}");
}
}
#[test]
fn classify_truncated_sha2_variants() {
for (family, param) in [
("SHA-512/256", "256"),
("SHA-512/224", "224"),
("SHA512/256", "256"),
("sha-512/224", "224"),
] {
let c = cls(Some(family), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("SHA-2"), "family for {family}");
assert_eq!(c.parameter.as_deref(), Some(param), "param for {family}");
assert_eq!(c.class, AlgorithmClass::Sha2);
}
let c = cls(Some("SHA-512"), None, None, None, None);
assert_eq!(c.parameter.as_deref(), Some("512"));
let by_oid = cls(None, None, Some("2.16.840.1.101.3.4.2.6"), None, None);
assert_eq!(by_oid.parameter.as_deref(), Some("256"));
}
#[test]
fn classify_name_fallback_picks_most_severe() {
let hash_first = cls(None, Some("sha384-rsa-signature"), None, None, None);
let rsa_first = cls(None, Some("rsa-sha384-signature"), None, None, None);
for c in [&hash_first, &rsa_first] {
assert_eq!(c.family.as_deref(), Some("RSA"), "{c:?}");
assert_eq!(c.class, AlgorithmClass::ClassicalQuantumVulnerable);
}
let c = cls(None, Some("rsa-md5-legacy-signer"), None, None, None);
assert_eq!(c.family.as_deref(), Some("MD5"));
assert_eq!(c.class, AlgorithmClass::Broken);
let c = cls(None, Some("sha384-digest"), None, None, None);
assert_eq!(c.class, AlgorithmClass::Sha2);
}
#[test]
fn classify_bare_sha_with_parameter_set() {
for param in ["224", "256", "384", "512"] {
let c = cls(Some("SHA"), None, None, Some(param), None);
assert_eq!(c.family.as_deref(), Some("SHA-2"), "family for SHA/{param}");
assert_eq!(c.parameter.as_deref(), Some(param));
assert_eq!(c.class, AlgorithmClass::Sha2);
}
let c = cls(Some("SHA"), None, None, None, None);
assert_eq!(c.family.as_deref(), Some("SHA-1"));
assert_eq!(c.class, AlgorithmClass::Broken);
let c = cls(Some("SHA"), None, None, Some("160"), None);
assert_eq!(c.family.as_deref(), Some("SHA-1"));
assert_eq!(c.class, AlgorithmClass::Broken);
}
#[test]
fn classify_national_algorithms() {
for (family, canonical) in [
("SM2", "SM2"),
("sm9", "SM9"),
("GOST", "GOST-R-34.10"),
("GOST R 34.10", "GOST-R-34.10"),
("GOST-R-34.10-2012", "GOST-R-34.10"),
("brainpoolP256r1", "EC"),
] {
let c = cls(Some(family), None, None, None, None);
assert_eq!(c.family.as_deref(), Some(canonical), "family for {family}");
assert_eq!(
c.class,
AlgorithmClass::ClassicalQuantumVulnerable,
"class for {family}"
);
}
for (oid, canonical, class) in [
(
"1.2.156.10197.1.301",
"SM2",
AlgorithmClass::ClassicalQuantumVulnerable,
),
(
"1.2.643.2.2.19",
"GOST-R-34.10",
AlgorithmClass::ClassicalQuantumVulnerable,
),
(
"1.2.643.7.1.1.1.1",
"GOST-R-34.10",
AlgorithmClass::ClassicalQuantumVulnerable,
),
(
"1.3.36.3.3.2.8.1.1.7",
"EC",
AlgorithmClass::ClassicalQuantumVulnerable,
),
("1.2.156.10197.1.104", "SM4", AlgorithmClass::Symmetric),
(
"1.2.643.7.1.1.2.2",
"GOST-R-34.11",
AlgorithmClass::OtherHash,
),
] {
let c = cls(None, None, Some(oid), None, None);
assert_eq!(c.family.as_deref(), Some(canonical), "family for {oid}");
assert_eq!(c.class, class, "class for {oid}");
}
assert_eq!(
cls(Some("SM4"), None, None, None, None).class,
AlgorithmClass::Symmetric
);
assert_eq!(
cls(Some("Kuznyechik"), None, None, None, None).class,
AlgorithmClass::Symmetric
);
assert_eq!(
cls(Some("Streebog"), None, None, None, None).class,
AlgorithmClass::OtherHash
);
}
#[test]
fn classify_hash_ml_dsa_oids() {
for (oid, param) in [
("2.16.840.1.101.3.4.3.32", "44"),
("2.16.840.1.101.3.4.3.33", "65"),
("2.16.840.1.101.3.4.3.34", "87"),
] {
let c = cls(None, None, Some(oid), None, None);
assert_eq!(
c.class,
AlgorithmClass::PostQuantum(PqcKind::MlDsa),
"class for {oid}"
);
assert_eq!(c.parameter.as_deref(), Some(param), "param for {oid}");
}
for oid in [
"2.16.840.1.101.3.4.3.20",
"2.16.840.1.101.3.4.3.31",
"2.16.840.1.101.3.4.3.35",
] {
assert_eq!(
cls(None, None, Some(oid), None, None).class,
AlgorithmClass::PostQuantum(PqcKind::SlhDsa),
"class for {oid}"
);
}
}
#[test]
fn guarded_name_scan_drops_overgeneric_tokens() {
for name in ["seed-expander", "ec2-instance-agent", "ecc-memory-check"] {
assert!(
classify_algorithm_names_guarded(name).is_empty(),
"guarded scan must ignore {name}"
);
assert_eq!(
cls(None, Some(name), None, None, None).class,
AlgorithmClass::Unknown,
"name fallback must not classify {name}"
);
}
assert_eq!(
cls(None, Some("brainpoolP256r1-signer"), None, None, None).class,
AlgorithmClass::ClassicalQuantumVulnerable
);
assert!(
classify_algorithm_names("TLS_RSA_WITH_SEED_CBC_SHA")
.iter()
.any(|c| c.family.as_deref() == Some("SEED"))
);
assert_eq!(
cls(Some("SEED"), None, None, None, None).class,
AlgorithmClass::Symmetric
);
}
#[test]
fn related_material_builder() {
let key = RelatedCryptoMaterialProperties::new(CryptoMaterialType::PublicKey)
.with_id("test-id".to_string())
.with_state(CryptoMaterialState::Active)
.with_size(2048)
.with_algorithm_ref("algo/rsa-2048".to_string())
.with_secured_by(SecuredBy {
mechanism: "HSM".to_string(),
algorithm_ref: Some("algo/aes-256".to_string()),
});
assert_eq!(key.material_type, CryptoMaterialType::PublicKey);
assert_eq!(key.state, Some(CryptoMaterialState::Active));
assert_eq!(key.size, Some(2048));
assert!(key.secured_by.is_some());
}
}