lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

security_contract.rs (6939B)


      1 use radroots_secrets::context::{
      2     EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId,
      3 };
      4 use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest};
      5 use radroots_secrets::error::{Operation, SecretIdError};
      6 use radroots_secrets::id::{BackendKind, KeyVersion};
      7 use radroots_secrets::wrapping::{SecretMaterial, WrappedSecret};
      8 use radroots_secrets::{Error, SecretId, SecretRef};
      9 use std::fs;
     10 use std::path::{Path, PathBuf};
     11 
     12 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_secrets.txt");
     13 
     14 #[test]
     15 fn reviewed_api_forbids_secret_bearing_clone_serialize_and_byte_access() {
     16     for required in [
     17         "pub mod radroots_secrets::context",
     18         "pub mod radroots_secrets::envelope",
     19         "pub mod radroots_secrets::error",
     20         "pub mod radroots_secrets::id",
     21         "pub mod radroots_secrets::provider",
     22         "pub mod radroots_secrets::wrapping",
     23         "pub struct radroots_secrets::wrapping::SecretMaterial(_)",
     24         "pub struct radroots_secrets::id::SecretRef",
     25         "pub trait radroots_secrets::provider::SecretProvider",
     26         "pub trait radroots_secrets::wrapping::KeyWrapping",
     27     ] {
     28         assert!(
     29             PUBLIC_API.contains(required),
     30             "reviewed public API is missing `{required}`"
     31         );
     32     }
     33 
     34     for secret_bearing_type in [
     35         "radroots_secrets::wrapping::SecretMaterial",
     36         "radroots_secrets::id::SecretRef",
     37         "radroots_secrets::envelope::SealMaterial",
     38         "radroots_secrets::envelope::SealRequest",
     39         "radroots_secrets::envelope::LegacyV1ResealAuthority",
     40         "radroots_secrets::wrapping::LegacyV1UnwrapRequest",
     41     ] {
     42         for forbidden_trait in ["core::clone::Clone", "serde_core::ser::Serialize"] {
     43             let forbidden = format!("impl {forbidden_trait} for {secret_bearing_type}");
     44             assert!(
     45                 !PUBLIC_API.contains(&forbidden),
     46                 "secret-bearing public type exposes forbidden trait: {forbidden}"
     47             );
     48         }
     49     }
     50 
     51     for forbidden in [
     52         "SecretMaterial::as_bytes",
     53         "SecretMaterial::as_slice",
     54         "SecretMaterial::into_bytes",
     55         "SecretMaterial::to_vec",
     56         "SecretRef::clone",
     57         "EncryptedEnvelope::open(&self, &dyn radroots_secrets::wrapping::KeyWrapping) ->",
     58         "SealRequest<'a>::new(radroots_secrets::id::SecretRef, &'a radroots_secrets::wrapping::SecretMaterial",
     59     ] {
     60         assert!(
     61             !PUBLIC_API.contains(forbidden),
     62             "reviewed API exposes forbidden plaintext or duplication surface `{forbidden}`"
     63         );
     64     }
     65 
     66     for forbidden_dependency in [
     67         "chacha20poly1305",
     68         "futures_executor",
     69         "keyring",
     70         "serde_json",
     71         "tempfile",
     72         "zeroize",
     73     ] {
     74         assert!(
     75             !exposes_crate_path(PUBLIC_API, forbidden_dependency),
     76             "reviewed API leaks implementation dependency `{forbidden_dependency}`"
     77         );
     78     }
     79 }
     80 
     81 fn exposes_crate_path(public_api: &str, crate_name: &str) -> bool {
     82     public_api
     83         .split(|character: char| {
     84             !(character.is_ascii_alphanumeric() || matches!(character, '_' | ':'))
     85         })
     86         .any(|token| {
     87             token
     88                 .strip_prefix(crate_name)
     89                 .is_some_and(|remainder| remainder.starts_with("::"))
     90         })
     91 }
     92 
     93 #[test]
     94 fn diagnostics_snapshot_is_redacted_and_plaintext_free() {
     95     const SECRET_ID_SENTINEL: &str = "plaintext-secret-id-sentinel";
     96     const PLAINTEXT_SENTINEL: &[u8] = b"plaintext-material-sentinel";
     97 
     98     let id = SecretId::parse(SECRET_ID_SENTINEL).expect("valid secret id");
     99     let reference = SecretRef::new(
    100         id,
    101         BackendKind::External,
    102         KeyVersion::new(7).expect("valid key version"),
    103     );
    104     let plaintext = SecretMaterial::from_slice(PLAINTEXT_SENTINEL).expect("valid material");
    105     let wrapped = WrappedSecret::from_bytes(b"wrapped-material-sentinel".to_vec())
    106         .expect("valid wrapped material");
    107     let sealing_key = SecretMaterial::from_slice(&[0x42; 32]).expect("valid sealing key");
    108     let context = EnvelopeContext::new(
    109         EnvelopePurpose::parse("radroots.security_test").expect("purpose"),
    110         EnvelopeSubject::parse("security_test", "plaintext-secret-id-sentinel").expect("subject"),
    111         PayloadSchemaId::parse("radroots.security_test.v1").expect("schema"),
    112     );
    113     let request = SealRequest::new(
    114         reference,
    115         context,
    116         &plaintext,
    117         SealMaterial::new(sealing_key, Nonce::new([0x24; 24])),
    118     );
    119 
    120     let diagnostics = [
    121         format!("{plaintext:?}"),
    122         format!("{wrapped:?}"),
    123         format!("{request:?}"),
    124         format!("{:?}", Error::DecryptFailed),
    125         Error::BackendFailure {
    126             backend: BackendKind::External,
    127             operation: Operation::Unwrap,
    128         }
    129         .to_string(),
    130         Error::SecretNotFound {
    131             backend: BackendKind::External,
    132             key_version: 7,
    133         }
    134         .to_string(),
    135         Error::InvalidSecretId(SecretIdError::InvalidCharacter { byte_offset: 9 }).to_string(),
    136     ];
    137 
    138     assert_eq!(diagnostics[0], "SecretMaterial(<redacted>)");
    139     assert_eq!(diagnostics[1], "WrappedSecret(<redacted>)");
    140     assert_eq!(diagnostics[2], "SealRequest(<redacted>)");
    141     for diagnostic in diagnostics {
    142         assert!(!diagnostic.contains(SECRET_ID_SENTINEL));
    143         assert!(!diagnostic.contains("plaintext-material-sentinel"));
    144         assert!(!diagnostic.contains("wrapped-material-sentinel"));
    145     }
    146 }
    147 
    148 #[test]
    149 fn envelope_and_private_artifact_sources_have_no_plaintext_logging_surface() {
    150     let crates_root = Path::new(env!("CARGO_MANIFEST_DIR"))
    151         .parent()
    152         .expect("secrets crate has a crates directory parent");
    153     let mut paths = Vec::new();
    154     for crate_name in ["secrets", "storage", "storage_sqlite"] {
    155         collect_rust_sources(&crates_root.join(crate_name).join("src"), &mut paths);
    156     }
    157     assert!(!paths.is_empty(), "audited production sources must exist");
    158 
    159     for path in paths {
    160         let source = fs::read_to_string(&path).expect("read audited source");
    161         let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source);
    162         for forbidden in ["tracing::", "log::", "println!(", "eprintln!(", "dbg!("] {
    163             assert!(
    164                 !production.contains(forbidden),
    165                 "envelope or private-artifact source contains logging surface `{forbidden}`: {}",
    166                 path.display()
    167             );
    168         }
    169     }
    170 }
    171 
    172 fn collect_rust_sources(root: &Path, paths: &mut Vec<PathBuf>) {
    173     for entry in fs::read_dir(root).expect("read source directory") {
    174         let path = entry.expect("source entry").path();
    175         if path.is_dir() {
    176             collect_rust_sources(&path, paths);
    177         } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") {
    178             paths.push(path);
    179         }
    180     }
    181 }