use std::path::{Path, PathBuf};
use crate::error::Error;
use crate::vault::Vault;
#[derive(Debug, Clone)]
pub struct CtfSetupResult {
pub hash: String,
pub hash_path: PathBuf,
}
#[derive(Debug, Clone)]
pub enum CtfVerifyResult {
Correct {
flag: String,
hash: String,
},
Incorrect {
submitted_hash: String,
expected_hash: String,
},
}
#[derive(Debug, Clone)]
pub enum CtfStatus {
Active {
hash: String,
tier: String,
},
Partial,
Inactive,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DoctorCheck {
Ok(String),
Fail(String),
NotApplicable(String),
}
impl DoctorCheck {
#[must_use]
pub fn is_fail(&self) -> bool {
matches!(self, Self::Fail(_))
}
}
#[derive(Debug, Clone)]
pub struct CtfDoctorReport {
pub backend: DoctorCheck,
pub ptrace_scope: DoctorCheck,
pub windows_dacl: DoctorCheck,
pub macos_hardened_runtime: DoctorCheck,
pub no_ld_preload: DoctorCheck,
pub memfd_secret: DoctorCheck,
pub challenge_active: DoctorCheck,
pub is_ready: bool,
}
impl CtfDoctorReport {
#[must_use]
pub fn checks(&self) -> [(&'static str, &DoctorCheck); 7] {
[
("hardware-sealed master key", &self.backend),
("Linux ptrace_scope", &self.ptrace_scope),
("Windows process DACL", &self.windows_dacl),
("macOS hardened runtime", &self.macos_hardened_runtime),
("LD_PRELOAD / LD_AUDIT", &self.no_ld_preload),
("Linux memfd_secret", &self.memfd_secret),
("CTF challenge active", &self.challenge_active),
]
}
}
#[allow(clippy::too_many_lines)]
pub fn ctf_doctor(root: &Path) -> CtfDoctorReport {
crate::guard::harden_process();
let backend = {
let b = crate::keychain::active_backend();
if b == crate::vault::hardware::Backend::None {
DoctorCheck::Fail(format!(
"no hardware backend ({}) — master.key is decryptable on any \
machine that copies it. Install TPM 2.0 + tpm2-tools (Linux), \
use the default DPAPI (Windows), or sign with Secure Enclave \
entitlement (macOS).",
b.name()
))
} else {
DoctorCheck::Ok(format!("backend = {}", b.name()))
}
};
let ptrace_scope = if cfg!(target_os = "linux") {
match crate::guard::check_ptrace_scope() {
None => DoctorCheck::Ok("ptrace_scope ≥ 1 (same-UID ptrace blocked)".to_string()),
Some(detail) => DoctorCheck::Fail(detail),
}
} else {
DoctorCheck::NotApplicable("ptrace_scope is a Linux concept".to_string())
};
let windows_dacl = if cfg!(windows) {
if crate::guard::vm_read_access_blocked() {
DoctorCheck::Ok(
"PROCESS_VM_READ denied — same-user ReadProcessMemory is blocked".to_string(),
)
} else {
DoctorCheck::Fail(
"process DACL still grants PROCESS_VM_READ to the owner — \
a same-user attacker can ReadProcessMemory the unlocked \
master key. harden_process() may have failed at startup."
.to_string(),
)
}
} else {
DoctorCheck::NotApplicable("Windows DACL hardening is Windows-only".to_string())
};
#[cfg(target_os = "macos")]
let macos_hardened_runtime = match crate::guard::check_macos_hardened_runtime() {
Ok(()) => {
DoctorCheck::Ok("hardened runtime engaged — task_for_pid denied by default".to_string())
}
Err(detail) => DoctorCheck::Fail(detail),
};
#[cfg(not(target_os = "macos"))]
let macos_hardened_runtime =
DoctorCheck::NotApplicable("hardened runtime is a macOS concept".to_string());
let no_ld_preload = match crate::guard::check_self_preload() {
Ok(()) => DoctorCheck::Ok("no LD_PRELOAD / LD_AUDIT in startup environ".to_string()),
Err(e) => DoctorCheck::Fail(e.to_string()),
};
let memfd_secret = if cfg!(target_os = "linux") {
if crate::guard::test_memfd_secret() {
DoctorCheck::Ok("memfd_secret available — master key in unmapped pages".to_string())
} else {
DoctorCheck::Fail(
"memfd_secret unavailable (kernel < 5.14 or CONFIG_SECRETMEM=n) — \
master key falls back to mlock-only protection."
.to_string(),
)
}
} else {
DoctorCheck::NotApplicable("memfd_secret is a Linux 5.14+ feature".to_string())
};
let challenge_active = match ctf_status(root) {
Ok(CtfStatus::Active { hash, tier }) => {
DoctorCheck::Ok(format!("challenge active (tier={tier}, hash={hash})"))
}
Ok(CtfStatus::Partial) => DoctorCheck::Fail(
"ctf-flag exists but ctf-hash.txt is missing — verifier cannot \
check submissions. Run `envseal ctf reset` and `envseal ctf start`."
.to_string(),
),
Ok(CtfStatus::Inactive) => {
DoctorCheck::Fail("no challenge set up — run `envseal ctf start` first.".to_string())
}
Err(e) => DoctorCheck::Fail(format!("status check failed: {e}")),
};
let is_ready = ![
&backend,
&ptrace_scope,
&windows_dacl,
&macos_hardened_runtime,
&no_ld_preload,
&memfd_secret,
]
.iter()
.any(|c| c.is_fail());
CtfDoctorReport {
backend,
ptrace_scope,
windows_dacl,
macos_hardened_runtime,
no_ld_preload,
memfd_secret,
challenge_active,
is_ready,
}
}
pub fn ctf_doctor_default() -> Result<CtfDoctorReport, Error> {
let root = super::vault_root()?;
Ok(ctf_doctor(&root))
}
pub fn ctf_status(root: &Path) -> Result<CtfStatus, Error> {
let hash_path = root.join("ctf-hash.txt");
crate::guard::verify_not_symlink(&hash_path)?;
let has_flag = super::secret_exists(root, "ctf-flag")?;
if has_flag && hash_path.exists() {
let hash = std::fs::read_to_string(&hash_path)?.trim().to_string();
let sealed_path = root.join("security.sealed");
let legacy_path = root.join("security.toml");
let tier = if legacy_path.exists() {
let content = std::fs::read_to_string(&legacy_path).unwrap_or_default();
if content.contains("Lockdown") {
"Lockdown".to_string()
} else if content.contains("Hardened") {
"Hardened".to_string()
} else {
"Standard".to_string()
}
} else if sealed_path.exists() {
"(sealed — unlock with `envseal security show` to view)".to_string()
} else {
"Standard (default — no config saved)".to_string()
};
Ok(CtfStatus::Active { hash, tier })
} else if has_flag {
Ok(CtfStatus::Partial)
} else {
Ok(CtfStatus::Inactive)
}
}
pub fn ctf_verify(root: &Path, submitted: &str) -> Result<CtfVerifyResult, Error> {
use sha2::Digest;
let hash_path = root.join("ctf-hash.txt");
crate::guard::verify_not_symlink(&hash_path)?;
if !hash_path.exists() {
return Err(Error::CryptoFailure(
"no CTF challenge is currently active. Run `envseal ctf start` to set one up."
.to_string(),
));
}
let expected = std::fs::read_to_string(&hash_path)?.trim().to_string();
let submitted_hash = format!("{:x}", sha2::Sha256::digest(submitted.as_bytes()));
let correct = crate::guard::constant_time_eq(submitted_hash.as_bytes(), expected.as_bytes());
let _ = crate::audit::log_required_at(
root,
&crate::audit::AuditEvent::SignalRecorded {
tier: "Lockdown".to_string(),
classification: format!(
"info [ctf.verify.{verdict}] submitted_hash={submitted_hash}",
verdict = if correct { "correct" } else { "incorrect" }
),
},
);
std::thread::sleep(VERIFY_RATE_LIMIT);
if correct {
Ok(CtfVerifyResult::Correct {
flag: submitted.to_string(),
hash: submitted_hash,
})
} else {
Ok(CtfVerifyResult::Incorrect {
submitted_hash,
expected_hash: expected,
})
}
}
const VERIFY_RATE_LIMIT: std::time::Duration = std::time::Duration::from_millis(250);
pub fn ctf_status_default() -> Result<CtfStatus, Error> {
let root = super::vault_root()?;
ctf_status(&root)
}
pub fn ctf_verify_default(submitted: &str) -> Result<CtfVerifyResult, Error> {
let root = super::vault_root()?;
ctf_verify(&root, submitted)
}
pub fn ctf_setup_default() -> Result<CtfSetupResult, Error> {
let vault = Vault::open_default()?;
if vault
.list()?
.iter()
.any(|n| n.eq_ignore_ascii_case("ctf-flag"))
{
return Err(Error::CryptoFailure(
"a CTF challenge already exists. Run `envseal ctf reset` first.".to_string(),
));
}
ctf_setup(&vault)
}
pub fn ctf_reset_default() -> Result<bool, Error> {
let root = super::vault_root()?;
let hash_path = root.join("ctf-hash.txt");
let flag_path = root.join("vault").join("ctf-flag.seal");
let prev_tier_path = root.join("ctf-prev-tier.txt");
crate::guard::verify_not_symlink(&hash_path)?;
crate::guard::verify_not_symlink(&flag_path)?;
crate::guard::verify_not_symlink(&prev_tier_path)?;
let mut removed = false;
if prev_tier_path.exists() {
if let Ok(content) = std::fs::read_to_string(&prev_tier_path) {
let prev = content.trim();
let restore_to = match prev {
"Standard" => Some(crate::security_config::SecurityTier::Standard),
"Hardened" => Some(crate::security_config::SecurityTier::Hardened),
"Lockdown" => Some(crate::security_config::SecurityTier::Lockdown),
_ => None,
};
if let Some(tier) = restore_to {
if let Ok(vault) = Vault::open_default() {
if let Ok(mut config) =
crate::security_config::load_config(vault.root(), vault.master_key_bytes())
{
config.apply_preset(tier);
let _ = crate::security_config::save_config(
vault.root(),
&config,
vault.master_key_bytes(),
);
}
}
}
}
let _ = std::fs::remove_file(&prev_tier_path);
removed = true;
}
if hash_path.exists() {
std::fs::remove_file(&hash_path).map_err(Error::StorageIo)?;
removed = true;
}
if flag_path.exists() {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
if let Ok(mut file) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(&flag_path)
{
if let Ok(meta) = file.metadata() {
let len = usize::try_from(meta.len()).unwrap_or(0);
if len > 0 {
use std::io::{Seek, SeekFrom, Write};
let _ = file.seek(SeekFrom::Start(0));
let buf = [0u8; 8192];
let mut remaining = len;
while remaining > 0 {
let n = remaining.min(buf.len());
let _ = file.write_all(&buf[..n]);
remaining -= n;
}
let _ = file.sync_all();
}
}
}
}
#[cfg(not(unix))]
{
if let Ok(meta) = std::fs::metadata(&flag_path) {
let len = usize::try_from(meta.len()).unwrap_or(0);
if len > 0 {
let _ = std::fs::write(&flag_path, vec![0u8; len]);
}
}
}
std::fs::remove_file(&flag_path).map_err(Error::StorageIo)?;
removed = true;
}
Ok(removed)
}
pub fn ctf_setup(vault: &Vault) -> Result<CtfSetupResult, Error> {
use rand::RngCore;
use sha2::Digest;
use std::fmt::Write;
let backend = crate::keychain::active_backend();
if backend == crate::vault::hardware::Backend::None {
return Err(Error::CryptoFailure(format!(
"ctf start refused: vault is using tier 3 (passphrase-only) sealing — \
no hardware backend ({}). The CTF integrity claim depends on \
the master key being non-decryptable on any other machine. \
Install the platform hardware backend (TPM 2.0 + tpm2-tools on \
Linux, Secure Enclave on macOS, DPAPI is automatic on Windows) \
and re-run.",
backend.name()
)));
}
if let Some(detail) = crate::guard::check_ptrace_scope() {
return Err(Error::CryptoFailure(format!(
"ctf start refused: ptrace scope is permissive — {detail}. \
A permissive ptrace lets any same-UID process attach to envseal \
and read the unlocked master key from memory. Run \
`sudo sysctl kernel.yama.ptrace_scope=1` (or higher), confirm \
with `cat /proc/sys/kernel/yama/ptrace_scope`, then re-run."
)));
}
let mut bytes = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut bytes);
let mut hex = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(&mut hex, "{b:02x}");
}
let flag = format!("ENVSEAL_CTF{{{hex}}}");
let hash = format!("{:x}", sha2::Sha256::digest(flag.as_bytes()));
vault
.store("ctf-flag", flag.as_bytes(), false)
.map_err(|e| match e {
Error::SecretAlreadyExists(_) => Error::CryptoFailure(
"a CTF challenge already exists. Run `envseal ctf reset` first.".to_string(),
),
other => other,
})?;
let mut config = crate::security_config::load_config(vault.root(), vault.master_key_bytes())
.unwrap_or_default();
let old_tier = format!("{:?}", config.tier);
let prev_tier_path = vault.root().join("ctf-prev-tier.txt");
crate::guard::verify_not_symlink(&prev_tier_path)?;
let prev_tmp = prev_tier_path.with_extension("tmp");
std::fs::write(&prev_tmp, format!("{old_tier}\n")).map_err(Error::StorageIo)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&prev_tmp, std::fs::Permissions::from_mode(0o600))
.map_err(Error::StorageIo)?;
}
std::fs::rename(&prev_tmp, &prev_tier_path).map_err(Error::StorageIo)?;
config.apply_preset(crate::security_config::SecurityTier::Lockdown);
crate::security_config::save_config(vault.root(), &config, vault.master_key_bytes())?;
crate::audit::log_required_at(
vault.root(),
&crate::audit::AuditEvent::TierChanged {
from: old_tier,
to: format!("{:?}", config.tier),
},
)?;
crate::audit::log_required_at(
vault.root(),
&crate::audit::AuditEvent::SecretStored {
name: "ctf-flag".to_string(),
},
)?;
let hash_path = vault.root().join("ctf-hash.txt");
crate::guard::verify_not_symlink(&hash_path)?;
let hash_tmp = hash_path.with_extension("tmp");
std::fs::write(&hash_tmp, format!("{hash}\n")).map_err(Error::StorageIo)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&hash_tmp, std::fs::Permissions::from_mode(0o600))
.map_err(Error::StorageIo)?;
}
std::fs::rename(&hash_tmp, &hash_path).map_err(Error::StorageIo)?;
Ok(CtfSetupResult { hash, hash_path })
}