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 => tracing::debug!(
algorithm = e.algorithm.token(),
r#use = e.use_.token(),
mode = e.mode.token(),
"crypto-policy allow"
),
Decision::Deny => tracing::warn!(
algorithm = e.algorithm.token(),
r#use = e.use_.token(),
mode = e.mode.token(),
"crypto-policy deny"
),
}
}
}
#[cfg(test)]
mod tests;