test(crypto): pin the dev-mode acknowledgement gate
Extracts the dev-mode acknowledgement decision into a pure check_dev_acknowledgement(secret, dev_mode, dev_ack) called by from_env before key resolution, and pins that dev mode without a secret and without PICLOUD_DEV_INSECURE_KEY is refused. Behavior-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -277,10 +277,30 @@ impl MasterKey {
|
||||
let dev_ack = std::env::var("PICLOUD_DEV_INSECURE_KEY")
|
||||
.map(|v| v == "i-understand-this-is-insecure")
|
||||
.unwrap_or(false);
|
||||
if dev_mode && secret.as_deref().map_or("", str::trim).is_empty() && !dev_ack {
|
||||
Self::check_dev_acknowledgement(secret.as_deref(), dev_mode, dev_ack)?;
|
||||
Self::resolve(secret.as_deref(), dev_mode)
|
||||
}
|
||||
|
||||
/// The dev-mode acknowledgement gate (F-S-009), factored out of [`Self::from_env`]
|
||||
/// so it is testable without mutating process-global env vars — the same
|
||||
/// reason [`Self::resolve`] is a separate pure function. With no real secret,
|
||||
/// `dev_mode` alone is NOT enough to boot on the world-known deterministic dev
|
||||
/// key; the caller must also acknowledge the risk (`dev_ack`). A present,
|
||||
/// non-empty secret makes the gate a no-op (a real key needs no acknowledgement).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`MasterKeyError::DevModeUnacknowledged`] when `dev_mode` is set and no
|
||||
/// secret is present and the risk was not acknowledged.
|
||||
pub fn check_dev_acknowledgement(
|
||||
secret: Option<&str>,
|
||||
dev_mode: bool,
|
||||
dev_ack: bool,
|
||||
) -> Result<(), MasterKeyError> {
|
||||
let secret_empty = secret.map_or("", str::trim).is_empty();
|
||||
if dev_mode && secret_empty && !dev_ack {
|
||||
return Err(MasterKeyError::DevModeUnacknowledged);
|
||||
}
|
||||
Self::resolve(secret.as_deref(), dev_mode)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pure resolution logic, factored out of [`Self::from_env`] so it's
|
||||
@@ -501,4 +521,29 @@ mod tests {
|
||||
assert_eq!(real.as_bytes(), &raw);
|
||||
assert_ne!(real.as_bytes(), a.as_bytes());
|
||||
}
|
||||
|
||||
/// F-S-009: dev mode alone must NOT be enough to boot on the world-known dev
|
||||
/// key. This is the mitigation for a dev docker-compose copied into prod
|
||||
/// encrypting every secret with a public value. `check_dev_acknowledgement`
|
||||
/// pins the gate; a defaulted-true ack or an inverted comparison fails here.
|
||||
#[test]
|
||||
fn dev_mode_without_acknowledgement_is_refused() {
|
||||
// dev_mode + no secret + NOT acknowledged → refused.
|
||||
assert!(matches!(
|
||||
MasterKey::check_dev_acknowledgement(None, true, false),
|
||||
Err(MasterKeyError::DevModeUnacknowledged)
|
||||
));
|
||||
// An empty/whitespace secret is treated as absent.
|
||||
assert!(matches!(
|
||||
MasterKey::check_dev_acknowledgement(Some(" "), true, false),
|
||||
Err(MasterKeyError::DevModeUnacknowledged)
|
||||
));
|
||||
|
||||
// Acknowledged → allowed (from_env then resolves onto the dev key).
|
||||
assert!(MasterKey::check_dev_acknowledgement(None, true, true).is_ok());
|
||||
// A real secret makes the gate a no-op regardless of the ack.
|
||||
assert!(MasterKey::check_dev_acknowledgement(Some("realkey"), true, false).is_ok());
|
||||
// No dev mode → the gate never fires (resolve then returns Missing).
|
||||
assert!(MasterKey::check_dev_acknowledgement(None, false, false).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user