use crate::secrets::transit::{
TransitCreateKeyRequest, TransitHashAlgorithm, TransitKeyInfo, TransitKeyType,
TransitSignatureAlgorithm,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default)]
pub struct FipsPosture {
report: FipsPostureReport,
}
impl FipsPosture {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn note_checked(
&mut self,
subject: impl Into<String>,
detail: impl Into<String>,
) -> &mut Self {
self.report.checked.push(FipsPostureNote {
subject: subject.into(),
detail: detail.into(),
});
self
}
pub fn note_unverified(
&mut self,
subject: impl Into<String>,
detail: impl Into<String>,
) -> &mut Self {
self.report.unverified.push(FipsPostureNote {
subject: subject.into(),
detail: detail.into(),
});
self
}
pub fn assume_hsm_or_kms_seal(&mut self, subject: impl Into<String>) -> &mut Self {
self.note_checked(
subject,
"caller supplied an HSM/KMS-backed seal assumption; the SDK did not verify it",
)
}
pub fn assume_unknown_or_non_hsm_seal(&mut self, subject: impl Into<String>) -> &mut Self {
self.add_finding(
FipsPostureSeverity::Warning,
subject,
"seal mechanism is not verified as HSM/KMS-backed by this SDK",
)
}
pub fn check_transit_create_key(
&mut self,
subject: impl Into<String>,
request: &TransitCreateKeyRequest,
) -> &mut Self {
let subject = subject.into();
let key_type = request.key_type.unwrap_or(TransitKeyType::Aes256Gcm96);
self.check_transit_key_type(subject.clone(), key_type);
if request.convergent_encryption == Some(true) {
self.add_finding(
FipsPostureSeverity::Warning,
format!("{subject}.convergent_encryption"),
"convergent encryption is enabled; review deterministic encryption requirements",
);
}
if request.exportable == Some(true) {
self.add_finding(
FipsPostureSeverity::Warning,
format!("{subject}.exportable"),
"Transit key material is exportable",
);
}
if request.allow_plaintext_backup == Some(true) {
self.add_finding(
FipsPostureSeverity::Warning,
format!("{subject}.allow_plaintext_backup"),
"Transit plaintext backup is enabled",
);
}
self
}
pub fn check_transit_key_info(
&mut self,
subject: impl Into<String>,
info: &TransitKeyInfo,
) -> &mut Self {
let subject = subject.into();
self.check_transit_key_type_name(subject.clone(), &info.key_type);
if info.exportable {
self.add_finding(
FipsPostureSeverity::Warning,
format!("{subject}.exportable"),
"Transit key metadata reports exportable key material",
);
}
if info.allow_plaintext_backup {
self.add_finding(
FipsPostureSeverity::Warning,
format!("{subject}.allow_plaintext_backup"),
"Transit key metadata reports plaintext backup is enabled",
);
}
if info.imported {
self.note_unverified(
format!("{subject}.imported"),
"imported Transit key provenance is outside what this SDK can verify",
);
}
self
}
pub fn check_transit_hash_algorithm(
&mut self,
subject: impl Into<String>,
algorithm: TransitHashAlgorithm,
) -> &mut Self {
let subject = subject.into();
match algorithm {
#[cfg(feature = "allow-sha1")]
#[allow(deprecated)]
TransitHashAlgorithm::Sha1 => self.add_finding(
FipsPostureSeverity::Reject,
subject,
"SHA-1 is not accepted by this FIPS-oriented helper",
),
TransitHashAlgorithm::Sha2_256
| TransitHashAlgorithm::Sha2_384
| TransitHashAlgorithm::Sha2_512 => {
self.note_checked(subject, "SHA-2 algorithm is in the conservative allowlist")
}
TransitHashAlgorithm::Sha2_224 => self.add_finding(
FipsPostureSeverity::Warning,
subject,
"SHA2-224 is FIPS-defined but outside this helper's conservative allowlist",
),
TransitHashAlgorithm::Sha3_224
| TransitHashAlgorithm::Sha3_256
| TransitHashAlgorithm::Sha3_384
| TransitHashAlgorithm::Sha3_512 => self.add_finding(
FipsPostureSeverity::Warning,
subject,
"SHA-3 requires provider/deployment validation outside this SDK",
),
TransitHashAlgorithm::None => self.add_finding(
FipsPostureSeverity::Warning,
subject,
"unhashed signing mode requires caller-side hash and protocol review",
),
}
}
pub fn check_transit_signature_algorithm(
&mut self,
subject: impl Into<String>,
algorithm: TransitSignatureAlgorithm,
) -> &mut Self {
match algorithm {
TransitSignatureAlgorithm::Pss => {
self.note_checked(subject, "RSA-PSS is in the conservative allowlist")
}
TransitSignatureAlgorithm::Pkcs1v15 => self.add_finding(
FipsPostureSeverity::Warning,
subject,
"RSASSA-PKCS1-v1_5 is legacy; prefer RSA-PSS where possible",
),
}
}
#[must_use]
pub fn finish(self) -> FipsPostureReport {
self.report
}
fn check_transit_key_type(&mut self, subject: String, key_type: TransitKeyType) {
match key_type {
TransitKeyType::Aes128Gcm96
| TransitKeyType::Aes256Gcm96
| TransitKeyType::EcdsaP256
| TransitKeyType::EcdsaP384
| TransitKeyType::Rsa3072
| TransitKeyType::Rsa4096
| TransitKeyType::Hmac => {
self.note_checked(subject, "Transit key type is in the conservative allowlist");
}
TransitKeyType::Rsa2048 => {
self.add_finding(
FipsPostureSeverity::Warning,
subject,
"RSA-2048 is accepted by some profiles but below this helper's conservative RSA size",
);
}
TransitKeyType::EcdsaP521 => {
self.add_finding(
FipsPostureSeverity::Warning,
subject,
"ECDSA P-521 requires deployment-specific validation outside this SDK",
);
}
TransitKeyType::ChaCha20Poly1305 | TransitKeyType::XChaCha20Poly1305 => {
self.add_finding(
FipsPostureSeverity::Warning,
subject,
"ChaCha20/XChaCha20 is outside this helper's conservative FIPS-oriented allowlist",
);
}
TransitKeyType::Ed25519 => {
self.add_finding(
FipsPostureSeverity::Warning,
subject,
"Ed25519 is outside this helper's conservative FIPS-oriented allowlist",
);
}
}
}
fn check_transit_key_type_name(&mut self, subject: String, key_type: &str) {
match key_type {
"aes128-gcm96" => self.check_transit_key_type(subject, TransitKeyType::Aes128Gcm96),
"aes256-gcm96" => self.check_transit_key_type(subject, TransitKeyType::Aes256Gcm96),
"chacha20-poly1305" => {
self.check_transit_key_type(subject, TransitKeyType::ChaCha20Poly1305);
}
"xchacha20-poly1305" => {
self.check_transit_key_type(subject, TransitKeyType::XChaCha20Poly1305);
}
"ed25519" => self.check_transit_key_type(subject, TransitKeyType::Ed25519),
"ecdsa-p256" => self.check_transit_key_type(subject, TransitKeyType::EcdsaP256),
"ecdsa-p384" => self.check_transit_key_type(subject, TransitKeyType::EcdsaP384),
"ecdsa-p521" => self.check_transit_key_type(subject, TransitKeyType::EcdsaP521),
"rsa-2048" => self.check_transit_key_type(subject, TransitKeyType::Rsa2048),
"rsa-3072" => self.check_transit_key_type(subject, TransitKeyType::Rsa3072),
"rsa-4096" => self.check_transit_key_type(subject, TransitKeyType::Rsa4096),
"hmac" => self.check_transit_key_type(subject, TransitKeyType::Hmac),
other => {
self.add_finding(
FipsPostureSeverity::Warning,
subject,
format!("Transit key type `{other}` is unknown to this SDK version"),
);
}
}
}
fn add_finding(
&mut self,
severity: FipsPostureSeverity,
subject: impl Into<String>,
message: impl Into<String>,
) -> &mut Self {
self.report.findings.push(FipsPostureFinding {
severity,
subject: subject.into(),
message: message.into(),
});
self
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct FipsPostureReport {
pub checked: Vec<FipsPostureNote>,
pub findings: Vec<FipsPostureFinding>,
pub unverified: Vec<FipsPostureNote>,
}
impl FipsPostureReport {
#[must_use]
pub fn has_rejects(&self) -> bool {
self.findings
.iter()
.any(|finding| finding.severity == FipsPostureSeverity::Reject)
}
#[must_use]
pub fn has_findings(&self) -> bool {
!self.findings.is_empty()
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FipsPostureNote {
pub subject: String,
pub detail: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FipsPostureFinding {
pub severity: FipsPostureSeverity,
pub subject: String,
pub message: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum FipsPostureSeverity {
Warning,
Reject,
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use super::{FipsPosture, FipsPostureSeverity};
use crate::secrets::transit::{TransitCreateKeyRequest, TransitHashAlgorithm, TransitKeyType};
#[test]
fn transit_create_key_posture_flags_risky_options() {
let mut posture = FipsPosture::new();
posture.check_transit_create_key(
"transit/app",
&TransitCreateKeyRequest {
key_type: Some(TransitKeyType::ChaCha20Poly1305),
convergent_encryption: Some(true),
exportable: Some(true),
allow_plaintext_backup: Some(true),
..Default::default()
},
);
let report = posture.finish();
assert_eq!(report.findings.len(), 4);
assert!(report.findings.iter().any(|finding| {
finding.subject == "transit/app.exportable"
&& finding.severity == FipsPostureSeverity::Warning
}));
}
#[test]
fn transit_hash_posture_accepts_sha2_and_warns_sha3() {
let mut posture = FipsPosture::new();
posture.check_transit_hash_algorithm("hash/ok", TransitHashAlgorithm::Sha2_256);
posture.check_transit_hash_algorithm("hash/review", TransitHashAlgorithm::Sha3_256);
let report = posture.finish();
assert_eq!(report.checked.len(), 1);
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].subject, "hash/review");
}
#[test]
fn seal_assumptions_are_reported_without_verification_claims() {
let mut posture = FipsPosture::new();
posture.assume_hsm_or_kms_seal("seal").note_unverified(
"provider",
"cryptographic module validation is deployment-owned",
);
let report = posture.finish();
assert_eq!(report.checked.len(), 1);
assert_eq!(report.unverified.len(), 1);
assert!(!report.has_findings());
}
}