use std::collections::BTreeMap;
use std::str::FromStr;
use super::error::AlgorithmKind;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum AlgorithmId {
HashMd5,
HashSha1,
HashSha256,
HashSha384,
HashSha512,
CipherRc4,
CipherAes128Cbc,
CipherAes256Cbc,
SigRsaPkcs1v15Sha1,
SigRsaPkcs1v15Sha256,
SigRsaPkcs1v15Sha384,
SigRsaPkcs1v15Sha512,
SigRsaPssSha256,
SigRsaPssSha384,
SigRsaPssSha512,
SigEcdsaP256Sha256,
SigEcdsaP384Sha384,
SigMlDsa44,
SigMlDsa65,
SigMlDsa87,
KemMlKem512,
KemMlKem768,
KemMlKem1024,
}
impl AlgorithmId {
pub const ALL: [AlgorithmId; 23] = [
AlgorithmId::HashMd5,
AlgorithmId::HashSha1,
AlgorithmId::HashSha256,
AlgorithmId::HashSha384,
AlgorithmId::HashSha512,
AlgorithmId::CipherRc4,
AlgorithmId::CipherAes128Cbc,
AlgorithmId::CipherAes256Cbc,
AlgorithmId::SigRsaPkcs1v15Sha1,
AlgorithmId::SigRsaPkcs1v15Sha256,
AlgorithmId::SigRsaPkcs1v15Sha384,
AlgorithmId::SigRsaPkcs1v15Sha512,
AlgorithmId::SigRsaPssSha256,
AlgorithmId::SigRsaPssSha384,
AlgorithmId::SigRsaPssSha512,
AlgorithmId::SigEcdsaP256Sha256,
AlgorithmId::SigEcdsaP384Sha384,
AlgorithmId::SigMlDsa44,
AlgorithmId::SigMlDsa65,
AlgorithmId::SigMlDsa87,
AlgorithmId::KemMlKem512,
AlgorithmId::KemMlKem768,
AlgorithmId::KemMlKem1024,
];
pub const fn token(self) -> &'static str {
match self {
AlgorithmId::HashMd5 => "md5",
AlgorithmId::HashSha1 => "sha1",
AlgorithmId::HashSha256 => "sha256",
AlgorithmId::HashSha384 => "sha384",
AlgorithmId::HashSha512 => "sha512",
AlgorithmId::CipherRc4 => "rc4",
AlgorithmId::CipherAes128Cbc => "aes128",
AlgorithmId::CipherAes256Cbc => "aes256",
AlgorithmId::SigRsaPkcs1v15Sha1 => "rsa-pkcs1-sha1",
AlgorithmId::SigRsaPkcs1v15Sha256 => "rsa-pkcs1-sha256",
AlgorithmId::SigRsaPkcs1v15Sha384 => "rsa-pkcs1-sha384",
AlgorithmId::SigRsaPkcs1v15Sha512 => "rsa-pkcs1-sha512",
AlgorithmId::SigRsaPssSha256 => "rsa-pss-sha256",
AlgorithmId::SigRsaPssSha384 => "rsa-pss-sha384",
AlgorithmId::SigRsaPssSha512 => "rsa-pss-sha512",
AlgorithmId::SigEcdsaP256Sha256 => "ecdsa-p256-sha256",
AlgorithmId::SigEcdsaP384Sha384 => "ecdsa-p384-sha384",
AlgorithmId::SigMlDsa44 => "ml-dsa-44",
AlgorithmId::SigMlDsa65 => "ml-dsa-65",
AlgorithmId::SigMlDsa87 => "ml-dsa-87",
AlgorithmId::KemMlKem512 => "ml-kem-512",
AlgorithmId::KemMlKem768 => "ml-kem-768",
AlgorithmId::KemMlKem1024 => "ml-kem-1024",
}
}
pub fn from_token(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|a| a.token() == s)
}
pub fn index(self) -> usize {
Self::ALL
.iter()
.position(|&a| a == self)
.expect("every AlgorithmId is in ALL")
}
pub const fn kind(self) -> AlgorithmKind {
match self {
AlgorithmId::HashMd5
| AlgorithmId::HashSha1
| AlgorithmId::HashSha256
| AlgorithmId::HashSha384
| AlgorithmId::HashSha512 => AlgorithmKind::Hash,
AlgorithmId::CipherRc4
| AlgorithmId::CipherAes128Cbc
| AlgorithmId::CipherAes256Cbc => AlgorithmKind::SymmetricCipher,
AlgorithmId::SigRsaPkcs1v15Sha1
| AlgorithmId::SigRsaPkcs1v15Sha256
| AlgorithmId::SigRsaPkcs1v15Sha384
| AlgorithmId::SigRsaPkcs1v15Sha512
| AlgorithmId::SigRsaPssSha256
| AlgorithmId::SigRsaPssSha384
| AlgorithmId::SigRsaPssSha512
| AlgorithmId::SigEcdsaP256Sha256
| AlgorithmId::SigEcdsaP384Sha384
| AlgorithmId::SigMlDsa44
| AlgorithmId::SigMlDsa65
| AlgorithmId::SigMlDsa87 => AlgorithmKind::SignatureSign,
AlgorithmId::KemMlKem512 | AlgorithmId::KemMlKem768 | AlgorithmId::KemMlKem1024 => {
AlgorithmKind::KeyDerivation
},
}
}
pub const fn is_fips_approved(self) -> bool {
matches!(
self,
AlgorithmId::HashSha256
| AlgorithmId::HashSha384
| AlgorithmId::HashSha512
| AlgorithmId::CipherAes128Cbc
| AlgorithmId::CipherAes256Cbc
| AlgorithmId::SigRsaPkcs1v15Sha256
| AlgorithmId::SigRsaPkcs1v15Sha384
| AlgorithmId::SigRsaPkcs1v15Sha512
| AlgorithmId::SigRsaPssSha256
| AlgorithmId::SigRsaPssSha384
| AlgorithmId::SigRsaPssSha512
| AlgorithmId::SigEcdsaP256Sha256
| AlgorithmId::SigEcdsaP384Sha384
| AlgorithmId::SigMlDsa44
| AlgorithmId::SigMlDsa65
| AlgorithmId::SigMlDsa87
| AlgorithmId::KemMlKem512
| AlgorithmId::KemMlKem768
| AlgorithmId::KemMlKem1024
)
}
pub const fn min_security_bits(self) -> u16 {
match self {
AlgorithmId::HashMd5 | AlgorithmId::HashSha1 | AlgorithmId::CipherRc4 => 0,
AlgorithmId::SigRsaPkcs1v15Sha1 => 0,
AlgorithmId::HashSha256
| AlgorithmId::CipherAes128Cbc
| AlgorithmId::SigRsaPkcs1v15Sha256
| AlgorithmId::SigRsaPssSha256
| AlgorithmId::SigEcdsaP256Sha256 => 128,
AlgorithmId::HashSha384
| AlgorithmId::SigRsaPkcs1v15Sha384
| AlgorithmId::SigRsaPssSha384
| AlgorithmId::SigEcdsaP384Sha384 => 192,
AlgorithmId::HashSha512
| AlgorithmId::CipherAes256Cbc
| AlgorithmId::SigRsaPkcs1v15Sha512
| AlgorithmId::SigRsaPssSha512 => 256,
AlgorithmId::SigMlDsa44 | AlgorithmId::KemMlKem512 => 128,
AlgorithmId::SigMlDsa65 | AlgorithmId::KemMlKem768 => 192,
AlgorithmId::SigMlDsa87 | AlgorithmId::KemMlKem1024 => 256,
}
}
const fn is_sha1_hash(self) -> bool {
matches!(self, AlgorithmId::HashSha1)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum AlgorithmUse {
Read,
Write,
}
impl AlgorithmUse {
pub const fn token(self) -> &'static str {
match self {
AlgorithmUse::Read => "read",
AlgorithmUse::Write => "write",
}
}
pub fn from_token(s: &str) -> Option<Self> {
match s {
"read" => Some(AlgorithmUse::Read),
"write" => Some(AlgorithmUse::Write),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyMode {
Compat,
Strict,
FipsStrict,
Cnsa2,
PqcReady,
}
impl PolicyMode {
pub const fn token(self) -> &'static str {
match self {
PolicyMode::Compat => "compat",
PolicyMode::Strict => "strict",
PolicyMode::FipsStrict => "fips-strict",
PolicyMode::Cnsa2 => "cnsa2",
PolicyMode::PqcReady => "pqc-ready",
}
}
const fn default_min_bits(self) -> u16 {
match self {
PolicyMode::Compat => 0,
PolicyMode::Strict | PolicyMode::FipsStrict | PolicyMode::PqcReady => 128,
PolicyMode::Cnsa2 => 192,
}
}
const fn default_min_rsa_bits(self) -> u16 {
match self {
PolicyMode::Compat => 0,
PolicyMode::Strict | PolicyMode::PqcReady => 2048,
PolicyMode::FipsStrict | PolicyMode::Cnsa2 => 3072,
}
}
fn default_decision(self, alg: AlgorithmId, use_: AlgorithmUse) -> Decision {
match self {
PolicyMode::Compat => Decision::Allow,
PolicyMode::Strict => match use_ {
AlgorithmUse::Read => Decision::Allow,
AlgorithmUse::Write => {
if alg.is_fips_approved() {
Decision::Allow
} else {
Decision::Deny
}
},
},
PolicyMode::FipsStrict => {
if alg.is_fips_approved() {
Decision::Allow
} else {
match use_ {
AlgorithmUse::Read if alg.is_sha1_hash() => Decision::Allow,
_ => Decision::Deny,
}
}
},
PolicyMode::PqcReady => match use_ {
AlgorithmUse::Read => Decision::Allow,
AlgorithmUse::Write => {
if alg.is_fips_approved() {
Decision::Allow
} else {
Decision::Deny
}
},
},
PolicyMode::Cnsa2 => match use_ {
AlgorithmUse::Read => Decision::Allow,
AlgorithmUse::Write => {
if alg.is_fips_approved() && alg.min_security_bits() >= 192 {
Decision::Allow
} else {
Decision::Deny
}
},
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyParseError(String);
impl PolicyParseError {
pub fn detail(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for PolicyParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid crypto policy spec: {}", self.0)
}
}
impl std::error::Error for PolicyParseError {}
#[derive(Debug, Clone)]
pub struct SecurityPolicy {
mode: PolicyMode,
overrides: BTreeMap<(AlgorithmId, AlgorithmUse), Decision>,
unknown_algorithm: Decision,
min_security_bits: u16,
min_rsa_modulus_bits: u16,
}
impl SecurityPolicy {
pub fn compat() -> Self {
Self::builder(PolicyMode::Compat).build()
}
pub fn strict() -> Self {
Self::builder(PolicyMode::Strict).build()
}
pub fn fips_strict() -> Self {
Self::builder(PolicyMode::FipsStrict).build()
}
pub fn builder(mode: PolicyMode) -> SecurityPolicyBuilder {
SecurityPolicyBuilder {
mode,
overrides: BTreeMap::new(),
unknown_algorithm: Decision::Deny,
min_security_bits: mode.default_min_bits(),
min_rsa_modulus_bits: mode.default_min_rsa_bits(),
}
}
pub fn mode(&self) -> PolicyMode {
self.mode
}
pub fn min_security_bits(&self) -> u16 {
self.min_security_bits
}
pub fn min_rsa_modulus_bits(&self) -> u16 {
self.min_rsa_modulus_bits
}
pub fn rsa_modulus_allowed(&self, modulus_bits: u32) -> Decision {
if self.min_rsa_modulus_bits == 0 || modulus_bits >= u32::from(self.min_rsa_modulus_bits) {
Decision::Allow
} else {
Decision::Deny
}
}
pub fn evaluate(&self, alg: AlgorithmId, use_: AlgorithmUse) -> Decision {
if let Some(&d) = self.overrides.get(&(alg, use_)) {
match d {
Decision::Deny => return Decision::Deny,
Decision::Allow => {
if self.mode == PolicyMode::FipsStrict && !alg.is_fips_approved() {
} else {
return Decision::Allow;
}
},
}
}
match self.mode.default_decision(alg, use_) {
Decision::Deny => Decision::Deny,
Decision::Allow => {
if use_ == AlgorithmUse::Write && alg.min_security_bits() < self.min_security_bits {
Decision::Deny
} else {
Decision::Allow
}
},
}
}
pub fn allows(&self, alg: AlgorithmId, use_: AlgorithmUse) -> bool {
self.evaluate(alg, use_) == Decision::Allow
}
pub fn unknown_algorithm_decision(&self) -> Decision {
self.unknown_algorithm
}
pub fn evaluate_token(&self, alg_token: &str, use_: AlgorithmUse) -> Decision {
match AlgorithmId::from_token(alg_token) {
Some(a) => self.evaluate(a, use_),
None => self.unknown_algorithm,
}
}
}
impl Default for SecurityPolicy {
fn default() -> Self {
Self::compat()
}
}
#[derive(Debug, Clone)]
pub struct SecurityPolicyBuilder {
mode: PolicyMode,
overrides: BTreeMap<(AlgorithmId, AlgorithmUse), Decision>,
unknown_algorithm: Decision,
min_security_bits: u16,
min_rsa_modulus_bits: u16,
}
impl SecurityPolicyBuilder {
pub fn allow(mut self, alg: AlgorithmId, use_: AlgorithmUse) -> Self {
self.overrides.insert((alg, use_), Decision::Allow);
self
}
pub fn deny(mut self, alg: AlgorithmId, use_: AlgorithmUse) -> Self {
self.overrides.insert((alg, use_), Decision::Deny);
self
}
pub fn min_security_bits(mut self, bits: u16) -> Self {
self.min_security_bits = bits;
self
}
pub fn min_rsa_modulus_bits(mut self, bits: u16) -> Self {
self.min_rsa_modulus_bits = bits;
self
}
pub fn unknown_algorithm(mut self, decision: Decision) -> Self {
self.unknown_algorithm = decision;
self
}
pub fn build(self) -> SecurityPolicy {
SecurityPolicy {
mode: self.mode,
overrides: self.overrides,
unknown_algorithm: self.unknown_algorithm,
min_security_bits: self.min_security_bits,
min_rsa_modulus_bits: self.min_rsa_modulus_bits,
}
}
}
impl FromStr for SecurityPolicy {
type Err = PolicyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(';');
let mode_tok = parts
.next()
.map(str::trim)
.filter(|t| !t.is_empty())
.ok_or_else(|| PolicyParseError("empty policy spec".to_string()))?;
let mode = match mode_tok {
"compat" => PolicyMode::Compat,
"strict" => PolicyMode::Strict,
"fips-strict" => PolicyMode::FipsStrict,
"cnsa2" => PolicyMode::Cnsa2,
"pqc-ready" => PolicyMode::PqcReady,
other => {
return Err(PolicyParseError(format!(
"unknown mode '{other}' (expected compat|strict|fips-strict|cnsa2|pqc-ready)"
)))
},
};
let mut b = SecurityPolicy::builder(mode);
for raw in parts {
let clause = raw.trim();
if clause.is_empty() {
continue;
}
let (verb, rest) = clause.split_once(':').ok_or_else(|| {
PolicyParseError(format!("clause '{clause}' must be '<allow|deny>:<alg>@<use>'"))
})?;
let (alg_tok, use_tok) = rest.split_once('@').ok_or_else(|| {
PolicyParseError(format!("clause '{clause}' missing '@<read|write>'"))
})?;
let alg = AlgorithmId::from_token(alg_tok.trim())
.ok_or_else(|| PolicyParseError(format!("unknown algorithm token '{alg_tok}'")))?;
let use_ = AlgorithmUse::from_token(use_tok.trim()).ok_or_else(|| {
PolicyParseError(format!("unknown use token '{use_tok}' (expected read|write)"))
})?;
b = match verb.trim() {
"allow" => b.allow(alg, use_),
"deny" => b.deny(alg, use_),
other => {
return Err(PolicyParseError(format!(
"unknown verb '{other}' (expected allow|deny)"
)))
},
};
}
Ok(b.build())
}
}
impl std::fmt::Display for SecurityPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.mode.token())?;
for ((alg, use_), d) in &self.overrides {
let verb = match d {
Decision::Allow => "allow",
Decision::Deny => "deny",
};
write!(f, ";{verb}:{}@{}", alg.token(), use_.token())?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct AuditEvent {
pub algorithm: AlgorithmId,
pub use_: AlgorithmUse,
pub decision: Decision,
pub mode: PolicyMode,
}
pub trait AuditSink: Send + Sync {
fn record(&self, event: &AuditEvent);
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopAuditSink;
impl AuditSink for NoopAuditSink {
fn record(&self, _event: &AuditEvent) {}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LogAuditSink;
impl AuditSink for LogAuditSink {
fn record(&self, e: &AuditEvent) {
match e.decision {
Decision::Allow => log::debug!(
"crypto-policy allow: {} {} (mode={})",
e.algorithm.token(),
e.use_.token(),
e.mode.token()
),
Decision::Deny => log::warn!(
"crypto-policy DENY: {} {} (mode={})",
e.algorithm.token(),
e.use_.token(),
e.mode.token()
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compat_allows_everything() {
let p = SecurityPolicy::compat();
for &a in &AlgorithmId::ALL {
for u in [AlgorithmUse::Read, AlgorithmUse::Write] {
assert_eq!(p.evaluate(a, u), Decision::Allow, "compat {a:?} {u:?}");
}
}
}
#[test]
fn strict_read_all_write_fips_only() {
let p = SecurityPolicy::strict();
for &a in &AlgorithmId::ALL {
assert_eq!(p.evaluate(a, AlgorithmUse::Read), Decision::Allow, "strict R {a:?}");
let want = if a.is_fips_approved() {
Decision::Allow
} else {
Decision::Deny
};
assert_eq!(p.evaluate(a, AlgorithmUse::Write), want, "strict W {a:?}");
}
assert_eq!(p.evaluate(AlgorithmId::HashMd5, AlgorithmUse::Write), Decision::Deny);
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Write), Decision::Deny);
assert_eq!(
p.evaluate(AlgorithmId::SigRsaPkcs1v15Sha1, AlgorithmUse::Write),
Decision::Deny
);
assert_eq!(p.evaluate(AlgorithmId::CipherAes256Cbc, AlgorithmUse::Write), Decision::Allow);
assert_eq!(
p.evaluate(AlgorithmId::SigEcdsaP256Sha256, AlgorithmUse::Write),
Decision::Allow
);
}
#[test]
fn fips_strict_matrix() {
let p = SecurityPolicy::fips_strict();
for &a in &AlgorithmId::ALL {
if a.is_fips_approved() {
assert_eq!(p.evaluate(a, AlgorithmUse::Read), Decision::Allow, "fips R {a:?}");
assert_eq!(p.evaluate(a, AlgorithmUse::Write), Decision::Allow, "fips W {a:?}");
} else {
assert_eq!(p.evaluate(a, AlgorithmUse::Write), Decision::Deny, "fips W {a:?}");
let want_read = if a == AlgorithmId::HashSha1 {
Decision::Allow
} else {
Decision::Deny
};
assert_eq!(p.evaluate(a, AlgorithmUse::Read), want_read, "fips R {a:?}");
}
}
}
#[test]
fn deny_override_is_terminal_even_over_compat_allow() {
let p = SecurityPolicy::builder(PolicyMode::Compat)
.deny(AlgorithmId::CipherRc4, AlgorithmUse::Write)
.build();
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Write), Decision::Deny);
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Read), Decision::Allow);
}
#[test]
fn allow_override_beats_strict_write_deny() {
let p = SecurityPolicy::builder(PolicyMode::Strict)
.allow(AlgorithmId::CipherRc4, AlgorithmUse::Write)
.build();
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Write), Decision::Allow);
}
#[test]
fn fips_strict_ignores_allow_override_for_non_approved() {
let p = SecurityPolicy::builder(PolicyMode::FipsStrict)
.allow(AlgorithmId::CipherRc4, AlgorithmUse::Read)
.allow(AlgorithmId::CipherRc4, AlgorithmUse::Write)
.build();
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Read), Decision::Deny);
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Write), Decision::Deny);
let p2 = SecurityPolicy::builder(PolicyMode::FipsStrict)
.allow(AlgorithmId::CipherAes256Cbc, AlgorithmUse::Write)
.build();
assert_eq!(p2.evaluate(AlgorithmId::CipherAes256Cbc, AlgorithmUse::Write), Decision::Allow);
}
#[test]
fn strict_min_bits_256_forbids_aes128_write_allows_read() {
let p = SecurityPolicy::builder(PolicyMode::Strict)
.min_security_bits(256)
.build();
assert_eq!(p.evaluate(AlgorithmId::CipherAes128Cbc, AlgorithmUse::Write), Decision::Deny);
assert_eq!(p.evaluate(AlgorithmId::CipherAes128Cbc, AlgorithmUse::Read), Decision::Allow);
assert_eq!(p.evaluate(AlgorithmId::CipherAes256Cbc, AlgorithmUse::Write), Decision::Allow);
let d = SecurityPolicy::strict();
assert_eq!(d.evaluate(AlgorithmId::CipherAes128Cbc, AlgorithmUse::Write), Decision::Allow);
}
#[test]
fn parse_modes() {
assert_eq!("compat".parse::<SecurityPolicy>().unwrap().mode(), PolicyMode::Compat);
assert_eq!("strict".parse::<SecurityPolicy>().unwrap().mode(), PolicyMode::Strict);
assert_eq!("fips-strict".parse::<SecurityPolicy>().unwrap().mode(), PolicyMode::FipsStrict);
}
#[test]
fn parse_with_overrides_and_roundtrip() {
let spec = "compat;deny:rc4@write;deny:md5@write";
let p: SecurityPolicy = spec.parse().unwrap();
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Write), Decision::Deny);
assert_eq!(p.evaluate(AlgorithmId::HashMd5, AlgorithmUse::Write), Decision::Deny);
assert_eq!(p.evaluate(AlgorithmId::CipherRc4, AlgorithmUse::Read), Decision::Allow);
let rendered = p.to_string();
let reparsed: SecurityPolicy = rendered.parse().unwrap();
assert_eq!(reparsed.to_string(), rendered);
}
#[test]
fn parse_tolerates_whitespace() {
let p: SecurityPolicy = " strict ; deny: sha1 @ write ".parse().unwrap();
assert_eq!(p.mode(), PolicyMode::Strict);
assert_eq!(p.evaluate(AlgorithmId::HashSha1, AlgorithmUse::Write), Decision::Deny);
}
#[test]
fn parse_fails_closed_on_garbage() {
for bad in [
"",
"garbage-mode",
"compat;rc4@write", "compat;deny:rc4", "compat;deny:nope@write", "compat;deny:rc4@sideways", "compat;maybe:rc4@write", ] {
assert!(
bad.parse::<SecurityPolicy>().is_err(),
"spec {bad:?} must be a fail-closed parse error"
);
}
}
#[test]
fn log_audit_sink_does_not_panic() {
let s = LogAuditSink;
s.record(&AuditEvent {
algorithm: AlgorithmId::CipherRc4,
use_: AlgorithmUse::Write,
decision: Decision::Deny,
mode: PolicyMode::Strict,
});
NoopAuditSink.record(&AuditEvent {
algorithm: AlgorithmId::HashSha256,
use_: AlgorithmUse::Read,
decision: Decision::Allow,
mode: PolicyMode::Compat,
});
}
#[test]
fn token_roundtrips_for_every_algorithm() {
for &a in &AlgorithmId::ALL {
assert_eq!(AlgorithmId::from_token(a.token()), Some(a), "token roundtrip {a:?}");
}
assert_eq!(AlgorithmId::from_token("does-not-exist"), None);
}
#[test]
fn kind_and_fips_classification_are_consistent() {
assert_eq!(AlgorithmId::HashMd5.kind(), AlgorithmKind::Hash);
assert_eq!(AlgorithmId::CipherRc4.kind(), AlgorithmKind::SymmetricCipher);
assert_eq!(AlgorithmId::SigRsaPssSha256.kind(), AlgorithmKind::SignatureSign);
assert!(!AlgorithmId::HashMd5.is_fips_approved());
assert!(!AlgorithmId::HashSha1.is_fips_approved());
assert!(!AlgorithmId::CipherRc4.is_fips_approved());
assert!(!AlgorithmId::SigRsaPkcs1v15Sha1.is_fips_approved());
assert!(AlgorithmId::HashSha256.is_fips_approved());
assert!(AlgorithmId::CipherAes256Cbc.is_fips_approved());
assert!(AlgorithmId::SigEcdsaP384Sha384.is_fips_approved());
}
#[test]
fn default_policy_is_compat() {
assert_eq!(SecurityPolicy::default().mode(), PolicyMode::Compat);
}
#[test]
fn evaluate_token_fails_closed_on_unknown() {
let p = SecurityPolicy::compat();
assert_eq!(p.evaluate_token("aes256", AlgorithmUse::Write), Decision::Allow);
assert_eq!(p.evaluate_token("kyber768", AlgorithmUse::Read), Decision::Deny);
assert_eq!(p.unknown_algorithm_decision(), Decision::Deny);
}
#[test]
fn index_is_stable_and_unique() {
for (i, &a) in AlgorithmId::ALL.iter().enumerate() {
assert_eq!(a.index(), i, "index must match position in ALL for {a:?}");
}
let mut seen = [false; 64];
for &a in &AlgorithmId::ALL {
assert!(a.index() < 64);
assert!(!seen[a.index()], "duplicate index for {a:?}");
seen[a.index()] = true;
}
}
#[test]
fn unknown_algorithm_decision_is_configurable() {
let p = SecurityPolicy::builder(PolicyMode::Compat)
.unknown_algorithm(Decision::Allow)
.build();
assert_eq!(p.evaluate_token("future-pqc", AlgorithmUse::Read), Decision::Allow);
}
#[test]
fn pqc_algorithm_ids_round_trip_and_are_fips_classified() {
for (tok, sec) in [
("ml-dsa-44", 128u16),
("ml-dsa-65", 192),
("ml-dsa-87", 256),
("ml-kem-512", 128),
("ml-kem-768", 192),
("ml-kem-1024", 256),
] {
let a = AlgorithmId::from_token(tok).unwrap_or_else(|| panic!("{tok} known"));
assert_eq!(a.token(), tok, "token round-trips");
assert!(a.is_fips_approved(), "{tok} is FIPS 203/204 approved");
assert_eq!(a.min_security_bits(), sec, "{tok} NIST-level strength");
}
assert_eq!(
AlgorithmId::from_token("ml-dsa-65").unwrap().kind(),
AlgorithmKind::SignatureSign
);
assert_eq!(
AlgorithmId::from_token("ml-kem-768").unwrap().kind(),
AlgorithmKind::KeyDerivation
);
assert_eq!(AlgorithmId::HashMd5.index(), 0);
assert_eq!(AlgorithmId::SigEcdsaP384Sha384.index(), 16);
assert_eq!(AlgorithmId::SigMlDsa44.index(), 17);
assert_eq!(AlgorithmId::KemMlKem1024.index(), 22);
}
#[test]
fn cnsa2_and_pqc_ready_modes_parse_and_govern() {
for tok in ["cnsa2", "pqc-ready"] {
let p: SecurityPolicy = tok.parse().expect("mode parses");
assert_eq!(p.mode().token(), tok);
}
let cnsa2: SecurityPolicy = "cnsa2".parse().unwrap();
let pqc: SecurityPolicy = "pqc-ready".parse().unwrap();
assert_eq!(cnsa2.evaluate_token("rsa-pkcs1-sha256", AlgorithmUse::Read), Decision::Allow);
assert_eq!(cnsa2.evaluate_token("ml-dsa-65", AlgorithmUse::Write), Decision::Allow);
assert_eq!(cnsa2.evaluate_token("ml-dsa-87", AlgorithmUse::Write), Decision::Allow);
assert_eq!(cnsa2.evaluate_token("rsa-pss-sha256", AlgorithmUse::Write), Decision::Deny);
assert_eq!(cnsa2.evaluate_token("ml-dsa-44", AlgorithmUse::Write), Decision::Deny);
assert_eq!(cnsa2.evaluate_token("md5", AlgorithmUse::Write), Decision::Deny);
assert_eq!(pqc.evaluate_token("ml-dsa-44", AlgorithmUse::Write), Decision::Allow);
assert_eq!(pqc.evaluate_token("rsa-pss-sha256", AlgorithmUse::Write), Decision::Allow);
assert_eq!(pqc.evaluate_token("rc4", AlgorithmUse::Write), Decision::Deny);
}
#[test]
fn rsa_modulus_floor_defaults_and_enforcement() {
assert_eq!(SecurityPolicy::compat().min_rsa_modulus_bits(), 0);
assert_eq!(SecurityPolicy::strict().min_rsa_modulus_bits(), 2048);
assert_eq!(SecurityPolicy::fips_strict().min_rsa_modulus_bits(), 3072);
assert_eq!(
"pqc-ready"
.parse::<SecurityPolicy>()
.unwrap()
.min_rsa_modulus_bits(),
2048
);
assert_eq!(
"cnsa2"
.parse::<SecurityPolicy>()
.unwrap()
.min_rsa_modulus_bits(),
3072
);
assert_eq!(SecurityPolicy::compat().rsa_modulus_allowed(1024), Decision::Allow);
let strict = SecurityPolicy::strict();
assert_eq!(strict.rsa_modulus_allowed(1024), Decision::Deny);
assert_eq!(strict.rsa_modulus_allowed(2048), Decision::Allow);
assert_eq!(strict.rsa_modulus_allowed(4096), Decision::Allow);
let cnsa2: SecurityPolicy = "cnsa2".parse().unwrap();
assert_eq!(cnsa2.rsa_modulus_allowed(2048), Decision::Deny);
assert_eq!(cnsa2.rsa_modulus_allowed(3072), Decision::Allow);
let p = SecurityPolicy::builder(PolicyMode::Strict)
.min_rsa_modulus_bits(4096)
.build();
assert_eq!(p.rsa_modulus_allowed(3072), Decision::Deny);
assert_eq!(p.rsa_modulus_allowed(4096), Decision::Allow);
let none = SecurityPolicy::builder(PolicyMode::FipsStrict)
.min_rsa_modulus_bits(0)
.build();
assert_eq!(none.rsa_modulus_allowed(1024), Decision::Allow);
}
}