use crate::rules::common::make_finding_from_offsets;
use crate::{Finding, Severity, PQ_READY_TAG};
pub struct PqAlgorithm {
pub canonical: &'static str,
pub standard: &'static str,
pub aka: &'static str,
pub primitive: &'static str,
pub spellings: &'static [&'static str],
}
pub const PQ_ALGORITHMS: &[PqAlgorithm] = &[
PqAlgorithm {
canonical: "X25519MLKEM768",
standard: "FIPS 203 hybrid (RFC 9370)",
aka: "X25519 + ML-KEM-768",
primitive: "kem",
spellings: &[
"x25519mlkem768",
"x25519_mlkem768",
"x25519-mlkem768",
"secp256r1mlkem768",
"x25519mlkem",
],
},
PqAlgorithm {
canonical: "X25519Kyber768",
standard: "FIPS 203 hybrid (pre-standard draft)",
aka: "X25519 + Kyber-768",
primitive: "kem",
spellings: &[
"x25519kyber768draft00",
"x25519kyber768",
"x25519_kyber768",
"x25519-kyber768",
"p256_kyber768",
"p256-kyber768",
],
},
PqAlgorithm {
canonical: "ML-KEM",
standard: "FIPS 203",
aka: "Kyber",
primitive: "kem",
spellings: &[
"ml_kem",
"ml-kem",
"mlkem",
"kyber",
"crystals-kyber",
"crystals_kyber",
"fips203",
],
},
PqAlgorithm {
canonical: "ML-DSA",
standard: "FIPS 204",
aka: "Dilithium",
primitive: "signature",
spellings: &[
"ml_dsa",
"ml-dsa",
"mldsa",
"dilithium",
"crystals-dilithium",
"crystals_dilithium",
"fips204",
],
},
PqAlgorithm {
canonical: "SLH-DSA",
standard: "FIPS 205",
aka: "SPHINCS+",
primitive: "signature",
spellings: &[
"slh_dsa",
"slh-dsa",
"slhdsa",
"sphincsplus",
"sphincs_plus",
"sphincs+",
"sphincs",
"fips205",
],
},
PqAlgorithm {
canonical: "FN-DSA",
standard: "FIPS 206 (draft)",
aka: "Falcon",
primitive: "signature",
spellings: &[
"fn_dsa",
"fn-dsa",
"fndsa",
"falcon512",
"falcon-512",
"falcon_512",
"falcon1024",
"falcon-1024",
"falcon_1024",
],
},
PqAlgorithm {
canonical: "HQC",
standard: "NIST 5th selection (draft)",
aka: "",
primitive: "kem",
spellings: &[
"hqc-128", "hqc-192", "hqc-256", "hqc128", "hqc192", "hqc256", "hqc_128", "hqc",
],
},
PqAlgorithm {
canonical: "liboqs",
standard: "Open Quantum Safe (PQC library)",
aka: "OQS",
primitive: "",
spellings: &[
"liboqs",
"oqs-provider",
"oqsprovider",
"oqs_provider",
"open-quantum-safe",
"pqcrystals",
"pq-crystals",
"pqclean",
],
},
];
pub struct PqMatch {
pub start_byte: usize,
pub end_byte: usize,
pub algo: &'static PqAlgorithm,
}
fn is_boundary_ident(b: u8) -> bool {
b.is_ascii_alphanumeric()
}
fn boundary_find(haystack_lower: &str, needle: &str) -> Option<usize> {
let bytes = haystack_lower.as_bytes();
let mut start = 0;
while let Some(pos) = haystack_lower[start..].find(needle) {
let idx = start + pos;
let end = idx + needle.len();
let before_ok = idx == 0 || !is_boundary_ident(bytes[idx - 1]);
let after_ok = end >= bytes.len() || !is_boundary_ident(bytes[end]);
if before_ok && after_ok {
return Some(idx);
}
start = idx + needle.len().max(1);
}
None
}
fn is_comment_line(trimmed: &str) -> bool {
const MARKERS: &[&str] = &["#", "//", "/*", "*/", "*", "--", "<!--", "%", ";;"];
MARKERS.iter().any(|m| trimmed.starts_with(m))
}
pub fn scan(source: &str) -> Vec<PqMatch> {
let mut matches = Vec::new();
let mut line_start = 0usize;
for line in source.split_inclusive('\n') {
if is_comment_line(line.trim_start()) {
line_start += line.len();
continue;
}
let lower = line.to_ascii_lowercase();
let mut seen: Vec<&'static str> = Vec::new();
for algo in PQ_ALGORITHMS {
if seen.contains(&algo.canonical) {
continue;
}
for spelling in algo.spellings {
if let Some(off) = boundary_find(&lower, spelling) {
matches.push(PqMatch {
start_byte: line_start + off,
end_byte: line_start + off + spelling.len(),
algo,
});
seen.push(algo.canonical);
break;
}
}
}
line_start += line.len();
}
matches
}
pub fn pq_ready_findings(rule_id: &str, source: &str) -> Vec<Finding> {
scan(source)
.into_iter()
.map(|m| {
let aka = if m.algo.aka.is_empty() {
String::new()
} else {
format!(", aka {}", m.algo.aka)
};
let desc = format!(
"Post-quantum algorithm in use: {} ({}{}) — quantum-resistant; no migration required",
m.algo.canonical, m.algo.standard, aka
);
let mut f = make_finding_from_offsets(
rule_id,
Severity::Low,
None,
&desc,
source,
m.start_byte,
m.end_byte,
);
f.tags = vec![PQ_READY_TAG.to_string()];
f.crypto_algorithm = Some(m.algo.canonical.to_string());
f
})
.collect()
}
pub fn algorithm_by_canonical(canonical: &str) -> Option<&'static PqAlgorithm> {
PQ_ALGORITHMS.iter().find(|a| a.canonical == canonical)
}
#[cfg(test)]
mod tests {
use super::*;
fn canonicals(source: &str) -> Vec<&'static str> {
let mut v: Vec<&'static str> = scan(source).into_iter().map(|m| m.algo.canonical).collect();
v.sort_unstable();
v.dedup();
v
}
#[test]
fn detects_ml_kem_spellings() {
assert!(canonicals("use ml_kem::MlKem768;").contains(&"ML-KEM"));
assert!(canonicals("from kyber_py.ml_kem import ML_KEM_768").contains(&"ML-KEM"));
assert!(canonicals("import \"crypto/mlkem\"").contains(&"ML-KEM"));
assert!(canonicals("let k = crystals_kyber::keypair();").contains(&"ML-KEM"));
}
#[test]
fn detects_signature_families() {
assert!(canonicals("dilithium.Sign(msg)").contains(&"ML-DSA"));
assert!(canonicals("ml_dsa_65_keypair()").contains(&"ML-DSA"));
assert!(canonicals("sphincsplus.sign()").contains(&"SLH-DSA"));
assert!(canonicals("slh_dsa_sha2_128s()").contains(&"SLH-DSA"));
assert!(canonicals("falcon512_keygen()").contains(&"FN-DSA"));
assert!(canonicals("fn_dsa_sign()").contains(&"FN-DSA"));
}
#[test]
fn detects_hybrids_without_double_counting_base() {
let c = canonicals("ssl_ecdh_curve X25519MLKEM768;");
assert!(c.contains(&"X25519MLKEM768"));
assert!(!c.contains(&"ML-KEM"));
}
#[test]
fn detects_library_markers() {
assert!(canonicals("#include <oqs/oqs.h>\nliboqs_version();").contains(&"liboqs"));
assert!(canonicals("import pqcrystals").contains(&"liboqs"));
}
#[test]
fn ignores_classical_and_unrelated_tokens() {
assert!(canonicals("rsa.generate_private_key()").is_empty());
assert!(canonicals("let falcon = SpaceX::launch();").is_empty());
assert!(canonicals("xmlkemper = parse_xml();").is_empty());
}
#[test]
fn pq_ready_findings_are_informational() {
let findings = pq_ready_findings("py/pq-ready-crypto", "from kyber_py import ml_kem\n");
assert_eq!(findings.len(), 1);
let f = &findings[0];
assert!(f.is_pq_ready());
assert_eq!(f.severity, Severity::Low);
assert_eq!(f.crypto_algorithm.as_deref(), Some("ML-KEM"));
assert!(f.cnsa2_deadline.is_none());
}
}