From 6e433aa4607cd91539d5e8d257cd0f50a1883ff3 Mon Sep 17 00:00:00 2001 From: Kunal Mehta Date: Thu, 14 Nov 2024 18:58:29 -0500 Subject: [PATCH] Script to upgrade from focal to noble The script is split into various stages where progress is tracked on-disk. The script is able to resume where it was at any point, and needs to, given multiple reboots in the middle. Given that we want to invoke the check script during the upgrade path, most of the code is moved into a common lib.rs that can be imported by both check.rs and upgrade.rs. The new noble-upgrade.json file shipped in the securedrop-config package is used to control the upgrade process. A systemd timer runs every 3 minutes to trigger the upgrade script, which in most cases will do nothing. We need to run it so frequently since this is how the script will be restarted after it pauses for a reboot. Fixes #7332. --- Cargo.lock | 99 +++- noble-migration/Cargo.toml | 3 + noble-migration/files/apt_freedom_press.list | 1 + noble-migration/files/sources.list | 13 + noble-migration/files/ubuntu.sources | 11 + noble-migration/src/bin/check.rs | 303 +---------- noble-migration/src/bin/upgrade.rs | 477 ++++++++++++++++++ noble-migration/src/lib.rs | 307 +++++++++++ ...securedrop-noble-migration-upgrade.service | 8 + .../securedrop-noble-migration-upgrade.timer | 10 + .../usr/share/securedrop/noble-upgrade.json | 10 + securedrop/debian/rules | 4 +- supply-chain/audits.toml | 14 + supply-chain/imports.lock | 120 ++++- 14 files changed, 1068 insertions(+), 312 deletions(-) create mode 100644 noble-migration/files/apt_freedom_press.list create mode 100644 noble-migration/files/sources.list create mode 100644 noble-migration/files/ubuntu.sources create mode 100644 noble-migration/src/bin/upgrade.rs create mode 100644 noble-migration/src/lib.rs create mode 100644 securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.service create mode 100644 securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.timer create mode 100644 securedrop/debian/config/usr/share/securedrop/noble-upgrade.json diff --git a/Cargo.lock b/Cargo.lock index 3e6b176c92..c99157d1c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,12 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bzip2" version = "0.4.4" @@ -263,6 +269,26 @@ dependencies = [ "log", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -360,6 +386,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -637,9 +669,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" @@ -682,6 +714,9 @@ name = "noble-migration" version = "0.1.0" dependencies = [ "anyhow", + "env_logger", + "log", + "rand", "rustix", "serde", "serde_json", @@ -796,6 +831,15 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -880,6 +924,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1550,6 +1624,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "zerofrom" version = "0.1.4" diff --git a/noble-migration/Cargo.toml b/noble-migration/Cargo.toml index 23b92fd926..5c01ffc790 100644 --- a/noble-migration/Cargo.toml +++ b/noble-migration/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" [dependencies] anyhow = "1.0.93" +env_logger = { version = "0.11.5", features = ["humantime"] , default-features = false } +log = "0.4.22" +rand = "0.8.5" rustix = { version = "0.38.40", features = ["process"] } serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.132" diff --git a/noble-migration/files/apt_freedom_press.list b/noble-migration/files/apt_freedom_press.list new file mode 100644 index 0000000000..12e72a6778 --- /dev/null +++ b/noble-migration/files/apt_freedom_press.list @@ -0,0 +1 @@ +deb [arch=amd64] https://apt.freedom.press noble main diff --git a/noble-migration/files/sources.list b/noble-migration/files/sources.list new file mode 100644 index 0000000000..df96219dc8 --- /dev/null +++ b/noble-migration/files/sources.list @@ -0,0 +1,13 @@ +## newer versions of the distribution. +deb http://archive.ubuntu.com/ubuntu/ noble main + +## newer versions of the distribution. +deb http://archive.ubuntu.com/ubuntu/ noble universe + +## Major bug fix updates produced after the final release of the +## distribution. +deb http://archive.ubuntu.com/ubuntu/ noble-updates main + +### Security fixes for distribution packages +deb http://security.ubuntu.com/ubuntu noble-security main +deb http://security.ubuntu.com/ubuntu noble-security universe diff --git a/noble-migration/files/ubuntu.sources b/noble-migration/files/ubuntu.sources new file mode 100644 index 0000000000..bcd1f501fe --- /dev/null +++ b/noble-migration/files/ubuntu.sources @@ -0,0 +1,11 @@ +Types: deb +URIs: http://archive.ubuntu.com/ubuntu/ +Suites: noble noble-updates +Components: main universe restricted multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +Types: deb +URIs: http://security.ubuntu.com/ubuntu/ +Suites: noble-security +Components: main universe restricted multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg diff --git a/noble-migration/src/bin/check.rs b/noble-migration/src/bin/check.rs index 0a92ac74b0..0244165b84 100644 --- a/noble-migration/src/bin/check.rs +++ b/noble-migration/src/bin/check.rs @@ -4,16 +4,9 @@ //! //! It is typically run by a systemd service/timer, but we also //! support admins running it manually to get more detailed output. -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use rustix::process::geteuid; -use serde::Serialize; -use std::{ - fs, - path::Path, - process::{self, ExitCode}, -}; -use url::{Host, Url}; -use walkdir::WalkDir; +use std::{fs, process::ExitCode}; /// This file contains the state of the pre-migration checks. /// @@ -24,253 +17,8 @@ use walkdir::WalkDir; /// * JSON object with boolean values for each check (see `State` struct) const STATE_PATH: &str = "/etc/securedrop-noble-migration.json"; -#[derive(Serialize)] -struct State { - ssh: bool, - ufw: bool, - free_space: bool, - apt: bool, - systemd: bool, -} - -impl State { - fn is_ready(&self) -> bool { - self.ssh && self.ufw && self.free_space && self.apt && self.systemd - } -} - -/// Parse the OS codename from /etc/os-release -fn os_codename() -> Result { - let contents = fs::read_to_string("/etc/os-release") - .context("reading /etc/os-release failed")?; - for line in contents.lines() { - if line.starts_with("VERSION_CODENAME=") { - // unwrap: Safe because we know the line contains "=" - let (_, codename) = line.split_once("=").unwrap(); - return Ok(codename.trim().to_string()); - } - } - - bail!("Could not find VERSION_CODENAME in /etc/os-release") -} - -/// Check that the UNIX "ssh" group has no members -/// -/// See . -fn check_ssh_group() -> Result { - // There are no clean bindings to getgrpname in rustix, - // so jut shell out to getent to get group members - let output = process::Command::new("getent") - .arg("group") - .arg("ssh") - .output() - .context("spawning getent failed")?; - if output.status.code() == Some(2) { - println!("ssh OK: group does not exist"); - return Ok(true); - } else if !output.status.success() { - bail!( - "running getent failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let stdout = String::from_utf8(output.stdout) - .context("getent stdout is not utf-8")?; - let members = parse_getent_output(&stdout)?; - if members.is_empty() { - println!("ssh OK: group is empty"); - Ok(true) - } else { - println!("ssh ERROR: group is not empty: {members:?}"); - Ok(false) - } -} - -/// Parse the output of `getent group ssh`, return true if empty -fn parse_getent_output(stdout: &str) -> Result> { - let stdout = stdout.trim(); - // The format looks like `ssh:x:123:member1,member2` - if !stdout.contains(":") { - bail!("unexpected output from getent: '{stdout}'"); - } - - // unwrap: safe, we know the line contains ":" - let (_, members) = stdout.rsplit_once(':').unwrap(); - if members.is_empty() { - Ok(vec![]) - } else { - Ok(members.split(',').collect()) - } -} - -/// Check that ufw was removed -/// -/// See . -fn check_ufw_removed() -> bool { - if Path::new("/usr/sbin/ufw").exists() { - println!("ufw ERROR: ufw is still installed"); - false - } else { - println!("ufw OK: ufw was removed"); - true - } -} - -/// Estimate the size of the backup so we know how much free space we'll need. -/// -/// We just check the size of `/var/lib/securedrop` since that's really the -/// data that'll take up space; everything else is just config files that are -/// negligible post-compression. We also don't estimate compression benefits. -fn estimate_backup_size() -> Result { - let path = Path::new("/var/lib/securedrop"); - if !path.exists() { - // mon server - return Ok(0); - } - let mut total: u64 = 0; - let walker = WalkDir::new(path); - for entry in walker { - let entry = entry.context("walking /var/lib/securedrop failed")?; - if entry.file_type().is_dir() { - continue; - } - let metadata = entry.metadata().context("getting metadata failed")?; - total += metadata.len(); - } - - Ok(total) -} - -/// We want to have enough space for a backup, the upgrade (~4GB of packages, -/// conservatively), and not take up more than 90% of the disk. -fn check_free_space() -> Result { - // Also no simple bindings to get disk size, so shell out to df - // Explicitly specify -B1 for bytes (not kilobytes) - let output = process::Command::new("df") - .args(["-B1", "/"]) - .output() - .context("spawning df failed")?; - if !output.status.success() { - bail!( - "running df failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let stdout = - String::from_utf8(output.stdout).context("df stdout is not utf-8")?; - let parsed = parse_df_output(&stdout)?; - - let backup_needs = estimate_backup_size()?; - let upgrade_needs: u64 = 4 * 1024 * 1024 * 1024; // 4GB - let headroom = parsed.total / 10; // 10% headroom - let total_needs = backup_needs + upgrade_needs + headroom; - - if parsed.free < total_needs { - println!( - "free space ERROR: not enough free space, have {} free bytes, need {total_needs} bytes", - parsed.free - ); - Ok(false) - } else { - println!("free space OK: enough free space"); - Ok(true) - } -} - -/// Sizes are in bytes -struct DfOutput { - total: u64, - free: u64, -} - -fn parse_df_output(stdout: &str) -> Result { - let line = match stdout.split_once('\n') { - Some((_, line)) => line, - None => bail!("df output didn't have a newline"), - }; - let parts: Vec<_> = line.split_whitespace().collect(); - - if parts.len() < 4 { - bail!("df output didn't have enough columns"); - } - - // vec indexing is safe because we did the bounds check above - let total = parts[1] - .parse::() - .context("parsing total space failed")?; - let free = parts[3] - .parse::() - .context("parsing free space failed")?; - - Ok(DfOutput { total, free }) -} - -const EXPECTED_DOMAINS: [&str; 3] = [ - "archive.ubuntu.com", - "security.ubuntu.com", - "apt.freedom.press", -]; - -const TEST_DOMAINS: [&str; 2] = - ["apt-qa.freedom.press", "apt-test.freedom.press"]; - -/// Verify only expected sources are configured for apt -fn check_apt() -> Result { - let output = process::Command::new("apt-get") - .arg("indextargets") - .output() - .context("spawning apt-get indextargets failed")?; - if !output.status.success() { - bail!( - "running apt-get indextargets failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let stdout = String::from_utf8(output.stdout) - .context("apt-get stdout is not utf-8")?; - for line in stdout.lines() { - if line.starts_with("URI:") { - let uri = line.strip_prefix("URI: ").unwrap(); - let parsed = Url::parse(uri)?; - if let Some(Host::Domain(domain)) = parsed.host() { - if TEST_DOMAINS.contains(&domain) { - println!("apt: WARNING test source found ({domain})"); - } else if !EXPECTED_DOMAINS.contains(&domain) { - println!("apt ERROR: unexpected source: {domain}"); - return Ok(false); - } - } else { - println!("apt ERROR: unexpected source: {uri}"); - return Ok(false); - } - } - } - - println!("apt OK: all sources are expected"); - Ok(true) -} - -/// Check that systemd has no failed units -fn check_systemd() -> Result { - let output = process::Command::new("systemctl") - .arg("is-failed") - .output() - .context("spawning systemctl failed")?; - if output.status.success() { - // success means some units are failed - println!("systemd ERROR: some units are failed"); - Ok(false) - } else { - println!("systemd OK: all units are happy"); - Ok(true) - } -} - fn run() -> Result<()> { - let codename = os_codename()?; + let codename = noble_migration::os_codename()?; if codename != "focal" { println!("Unsupported Ubuntu version: {codename}"); // nothing to do, write an empty JSON blob @@ -278,13 +26,7 @@ fn run() -> Result<()> { return Ok(()); } - let state = State { - ssh: check_ssh_group()?, - ufw: check_ufw_removed(), - free_space: check_free_space()?, - apt: check_apt()?, - systemd: check_systemd()?, - }; + let state = noble_migration::run_checks()?; fs::write( STATE_PATH, @@ -326,40 +68,3 @@ fn main() -> Result { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_getent_output() { - // no members - assert_eq!( - parse_getent_output("ssh:x:123:\n").unwrap(), - Vec::<&str>::new() - ); - // one member - assert_eq!( - parse_getent_output("ssh:x:123:member1\n").unwrap(), - vec!["member1"] - ); - // two members - assert_eq!( - parse_getent_output("ssh:x:123:member1,member2\n").unwrap(), - vec!["member1", "member2"] - ); - } - - #[test] - fn test_parse_df_output() { - let output = parse_df_output( - "Filesystem 1B-blocks Used Available Use% Mounted on -/dev/mapper/ubuntu--vg-ubuntu--lv 105089261568 8573784064 91129991168 9% / -", - ) - .unwrap(); - - assert_eq!(output.total, 105089261568); - assert_eq!(output.free, 91129991168); - } -} diff --git a/noble-migration/src/bin/upgrade.rs b/noble-migration/src/bin/upgrade.rs new file mode 100644 index 0000000000..4493dd1699 --- /dev/null +++ b/noble-migration/src/bin/upgrade.rs @@ -0,0 +1,477 @@ +//! Migrate a SecureDrop server from focal to noble +//! +//! This script should never be run directly, only via the +//! systemd service. +use anyhow::{bail, Context, Result}; +use log::{debug, error, info}; +use rand::{thread_rng, Rng}; +use rustix::process::geteuid; +use serde::{Deserialize, Serialize}; +use std::{ + env, + fs::{self, Permissions}, + os::unix::{fs::PermissionsExt, process::ExitStatusExt}, + path::Path, + process::{self, Command, ExitCode}, +}; + +const CONFIG_PATH: &str = "/usr/share/securedrop/noble-upgrade.json"; +const STATE_PATH: &str = "/etc/securedrop-noble-migration-state.json"; +const MON_OSSEC_CONFIG: &str = "/var/ossec/etc/ossec.conf"; +/// Environment variable to allow developers to inject an extra APT source +const EXTRA_APT_SOURCE: &str = "EXTRA_APT_SOURCE"; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +enum Stage { + None, + PendingUpdates, + MigrationCheck, + DisableApache2, + Backup, + Marker, + SuspendOSSEC, + ChangeAptSources, + AptGetUpdate, + AptGetUpgradeNoNew, + AptGetFullUpgrade, + ReenableUnattendedUpdates, + ReenableOSSEC, + Reboot, + SwitchUbuntuSources, + IntegrityCheck, + ReenableApache2, + RemoveBackup, + Done, +} + +#[derive(Serialize, Deserialize, Debug)] +struct State { + finished: Stage, + bucket: usize, +} + +impl State { + fn load() -> Result { + if !Path::new(STATE_PATH).exists() { + debug!("State file, {}, doesn't exist; state=None", STATE_PATH); + let mut rng = thread_rng(); + let state = State { + finished: Stage::None, + bucket: rng.gen_range(1..=5), + }; + // Persist the randomly selected bucket + state.save()?; + return Ok(state); + } + debug!("Loading state from {}", STATE_PATH); + // If this fails we're basically stuck. + let state = serde_json::from_str( + &fs::read_to_string(STATE_PATH) + .context("Unable to read STATE_PATH")?, + ) + .context("Deserializing STATE_PATH failed")?; + debug!("Loaded state: {state:?}"); + Ok(state) + } + + fn set(&mut self, stage: Stage) -> Result<()> { + debug!("Finished stage {stage:?}"); + self.finished = stage; + self.save() + } + + fn save(&self) -> Result<()> { + fs::write( + STATE_PATH, + serde_json::to_string(self).context("Failed to serialize state")?, + ) + .context("Failed to write state") + } +} + +fn run_next_stage(state: &mut State) -> Result<()> { + match state.finished { + Stage::None => { + pending_updates(state)?; + // n.b. this is unreachable because we've already rebooted + state.set(Stage::PendingUpdates)?; + } + Stage::PendingUpdates => { + migration_check()?; + state.set(Stage::MigrationCheck)?; + } + Stage::MigrationCheck => { + disable_apache2()?; + state.set(Stage::DisableApache2)?; + } + Stage::DisableApache2 => { + backup()?; + state.set(Stage::Backup)?; + } + Stage::Backup => { + suspend_ossec()?; + state.set(Stage::Marker)?; + } + Stage::Marker => { + marker()?; + state.set(Stage::SuspendOSSEC)?; + } + Stage::SuspendOSSEC => { + change_apt_sources()?; + state.set(Stage::ChangeAptSources)?; + } + Stage::ChangeAptSources => { + apt_get_update()?; + state.set(Stage::AptGetUpdate)?; + } + Stage::AptGetUpdate => { + apt_get_upgrade_no_new()?; + state.set(Stage::AptGetUpgradeNoNew)?; + } + Stage::AptGetUpgradeNoNew => { + apt_get_full_upgrade()?; + state.set(Stage::AptGetFullUpgrade)?; + } + Stage::AptGetFullUpgrade => { + reenable_unattended_updates()?; + state.set(Stage::ReenableUnattendedUpdates)?; + } + Stage::ReenableUnattendedUpdates => { + reenable_ossec()?; + state.set(Stage::ReenableOSSEC)?; + } + Stage::ReenableOSSEC => { + reboot(state)?; + // n.b. this is unreachable because we've already rebooted + state.set(Stage::Reboot)?; + } + Stage::Reboot => { + switch_ubuntu_sources()?; + state.set(Stage::SwitchUbuntuSources)?; + } + Stage::SwitchUbuntuSources => { + integrity_check()?; + state.set(Stage::IntegrityCheck)?; + } + Stage::IntegrityCheck => { + reenable_apache2()?; + state.set(Stage::ReenableApache2)?; + } + Stage::ReenableApache2 => { + remove_backup()?; + state.set(Stage::RemoveBackup)?; + } + Stage::RemoveBackup => { + state.set(Stage::Done)?; + } + Stage::Done => {} + } + Ok(()) +} + +/// A wrapper to roughly implement Python's subprocess.check_call/check_output +fn check_call(binary: &str, args: &[&str]) -> Result { + debug!("Running: {binary} {}", args.join(" ")); + let output = Command::new(binary) + .args(args) + .env("DEBIAN_FRONTEND", "noninteractive") + .output() + .context(format!("failed to spawn/execute '{binary}'"))?; + if output.status.success() { + debug!("Finished running: {binary} {}", args.join(" ")); + // In theory we could use from_utf8_lossy here as we're not expecting + // any non-UTF-8 output, but let's error in that case. + let stdout = String::from_utf8(output.stdout) + .context("stdout contains non-utf8 bytes")?; + debug!("{stdout}"); + Ok(stdout) + } else { + debug!("Errored running: {binary} {}", args.join(" ")); + // Figure out why it failed by looking at the exit code, and if none, + // look at if it was a signal + let exit = match output.status.code() { + Some(code) => format!("exit code {code}"), + None => match output.status.signal() { + Some(signal) => format!("terminated by signal {signal}"), + None => "for an unknown reason".to_string(), + }, + }; + error!("{}", String::from_utf8_lossy(&output.stderr)); + bail!("running '{binary}' failed; {exit}") + } +} + +/// Check if the current server is the mon server by +/// looking for the securedrop-ossec-server package +fn is_mon_server() -> bool { + Path::new("/usr/share/doc/securedrop-ossec-server/copyright").exists() +} + +fn pending_updates(state: &mut State) -> Result<()> { + info!("Applying any pending updates..."); + check_call("apt-get", &["update"])?; + check_call("unattended-upgrade", &[])?; + // Disable all background updates pre-reboot so we know it's fully + // disabled when we come back. + info!("Temporarily disabling background updates..."); + check_call("systemctl", &["mask", "unattended-upgrades"])?; + check_call("systemctl", &["mask", "apt-daily"])?; + check_call("systemctl", &["mask", "apt-daily-upgrade"])?; + state.set(Stage::PendingUpdates)?; + check_call("systemctl", &["reboot"])?; + // Because we've initiated the reboot, do a hard stop here to ensure that + // we don't keep moving forward if the reboot doesn't happen instantly + process::exit(0); +} + +fn migration_check() -> Result<()> { + info!("Checking pre-migration steps..."); + if noble_migration::os_codename()? != "focal" { + bail!("not a focal system"); + } + let state = + noble_migration::run_checks().context("migration check errored")?; + + if env::var(EXTRA_APT_SOURCE).is_ok() { + // If we're injecting an extra APT source, then allow that check to fail + if !state.is_ready_except_apt() { + bail!("Migration check failed") + } + } else if !state.is_ready() { + bail!("Migration check failed") + } + + Ok(()) +} + +fn disable_apache2() -> Result<()> { + if is_mon_server() { + return Ok(()); + } + info!("Stopping web server for duration of upgrade..."); + check_call("systemctl", &["mask", "apache2"])?; + Ok(()) +} + +fn backup() -> Result<()> { + info!("Taking a backup..."); + // Create a root-only directory to store the backup + fs::create_dir("/var/lib/securedrop-backups")?; + let permissions = Permissions::from_mode(0o700); + fs::set_permissions("/var/lib/securedrop-backups", permissions)?; + check_call( + "/usr/bin/securedrop-app-backup.py", + &["--dest", "/var/lib/securedrop-backups"], + )?; + Ok(()) +} + +fn marker() -> Result<()> { + info!("Writing upgrade marker file..."); + fs::write("/etc/securedrop-upgraded-from-focal", "yes") + .context("failed to write upgrade marker file") +} + +fn suspend_ossec() -> Result<()> { + if !is_mon_server() { + return Ok(()); + } + info!("Temporarily suspending most OSSEC notifications..."); + let current = fs::read_to_string(MON_OSSEC_CONFIG)?; + let new = current.replace( + "7", + "15", + ); + fs::write(MON_OSSEC_CONFIG, new)?; + check_call("systemctl", &["restart", "ossec"])?; + Ok(()) +} + +fn change_apt_sources() -> Result<()> { + info!("Switching APT sources to noble..."); + fs::write( + "/etc/apt/sources.list", + include_str!("../../files/sources.list"), + )?; + let mut contents = + include_str!("../../files/apt_freedom_press.list").to_string(); + // Allow developers to inject an extra APT source + if let Ok(extra) = env::var(EXTRA_APT_SOURCE) { + contents.push_str(format!("\n{extra}\n").as_str()); + } + fs::write("/etc/apt/sources.list.d/apt_freedom_press.list", contents)?; + Ok(()) +} + +fn apt_get_update() -> Result<()> { + info!("Updating APT cache..."); + check_call("apt-get", &["update"])?; + Ok(()) +} + +fn apt_get_upgrade_no_new() -> Result<()> { + info!("Upgrading APT packages (first pass)..."); + // FIXME: args don't work + check_call( + "apt-get", + &[ + "upgrade", + "--without-new-pkgs", + "--force-confold", + "--force-confdef", + ], + )?; + Ok(()) +} + +fn apt_get_full_upgrade() -> Result<()> { + info!("Upgrading APT packages (second pass)..."); + check_call( + "apt-get", + &["full-upgrade", "--force-confold", "--force-confdef"], + )?; + Ok(()) +} + +fn reenable_unattended_updates() -> Result<()> { + info!("Re-enabling background updates..."); + check_call("systemctl", &["unmask", "unattended-upgrades"])?; + check_call("systemctl", &["unmask", "apt-daily"])?; + check_call("systemctl", &["unmask", "apt-daily-upgrade"])?; + Ok(()) +} + +fn reenable_ossec() -> Result<()> { + if !is_mon_server() { + return Ok(()); + } + info!("Re-enabling OSSEC notifications..."); + let current = fs::read_to_string(MON_OSSEC_CONFIG)?; + let new = current.replace( + "15", + "7", + ); + fs::write(MON_OSSEC_CONFIG, new)?; + check_call("systemctl", &["restart", "ossec"])?; + Ok(()) +} + +fn reboot(state: &mut State) -> Result<()> { + info!("Rebooting!"); + state.set(Stage::Reboot)?; + check_call("systemctl", &["reboot"])?; + // Because we've initiated the reboot, do a hard stop here to ensure that + // we don't keep moving forward if the reboot doesn't happen instantly + process::exit(0); +} + +fn switch_ubuntu_sources() -> Result<()> { + info!("Switching APT sources format..."); + fs::write( + "/etc/apt/sources.list.d/ubuntu.sources", + include_str!("../../files/ubuntu.sources"), + ) + .context("failed to write ubuntu.sources")?; + fs::remove_file("/etc/apt/sources.list") + .context("failed to remove sources.list")?; + Ok(()) +} + +fn integrity_check() -> Result<()> { + info!("Running integrity check post-upgrade..."); + // Check systemd units are happy + if !noble_migration::check_systemd()? { + bail!("some systemd units are not happy"); + } + // Very simple check that the iptables firewall is up + let iptables = check_call("iptables", &["-S"])?; + if !iptables.contains("INPUT DROP") { + bail!("iptables firewall is not up"); + } + Ok(()) +} + +fn reenable_apache2() -> Result<()> { + if is_mon_server() { + return Ok(()); + } + info!("Starting web server..."); + check_call("systemctl", &["unmask", "apache2"])?; + Ok(()) +} + +fn remove_backup() -> Result<()> { + info!("Deleting backup..."); + fs::remove_dir_all("/var/lib/securedrop-backups")?; + Ok(()) +} + +#[derive(Deserialize)] +struct UpgradeConfig { + app: HostUpgradeConfig, + mon: HostUpgradeConfig, +} + +#[derive(Deserialize)] +struct HostUpgradeConfig { + enabled: bool, + bucket: usize, +} + +fn should_upgrade(state: &State) -> Result { + let config: UpgradeConfig = serde_json::from_str( + &fs::read_to_string(CONFIG_PATH) + .context("failed to read CONFIG_PATH")?, + ) + .context("failed to deserialize CONFIG_PATH")?; + // If we've already started the upgrade, keep going + if state.finished != Stage::None { + info!("Upgrade has already started; will keep going"); + return Ok(true); + } + let for_host = if is_mon_server() { + &config.mon + } else { + &config.app + }; + if !for_host.enabled { + info!("Auto-upgrades are disabled"); + return Ok(false); + } + if for_host.bucket > state.bucket { + info!( + "Auto-upgrades are enabled, but our bucket hasn't been enabled yet" + ); + return Ok(false); + } + + Ok(true) +} + +fn main() -> Result { + env_logger::init(); + + if !geteuid().is_root() { + error!("This script must be run as root"); + return Ok(ExitCode::FAILURE); + } + + if env::var("LAUNCHED_BY_SYSTEMD").is_err() { + error!("This script must be run from the systemd unit"); + return Ok(ExitCode::FAILURE); + } + + let mut state = State::load()?; + if !should_upgrade(&state)? { + return Ok(ExitCode::SUCCESS); + } + info!("Starting migration from state: {:?}", state.finished); + loop { + run_next_stage(&mut state)?; + if state.finished == Stage::Done { + break; + } + } + + Ok(ExitCode::SUCCESS) +} diff --git a/noble-migration/src/lib.rs b/noble-migration/src/lib.rs new file mode 100644 index 0000000000..7cfd7b09e2 --- /dev/null +++ b/noble-migration/src/lib.rs @@ -0,0 +1,307 @@ +//! Common code for the noble-migration that is used by check and upgrade +use anyhow::{bail, Context, Result}; +use serde::Serialize; +use std::{ + fs, + path::Path, + process::{self}, +}; +use url::{Host, Url}; +use walkdir::WalkDir; + +#[derive(Serialize)] +pub struct State { + ssh: bool, + ufw: bool, + free_space: bool, + apt: bool, + systemd: bool, +} + +impl State { + pub fn is_ready(&self) -> bool { + self.is_ready_except_apt() && self.apt + } + + /// For when developers inject extra APT sources for testing + pub fn is_ready_except_apt(&self) -> bool { + self.ssh && self.ufw && self.free_space && self.systemd + } +} + +/// Parse the OS codename from /etc/os-release +pub fn os_codename() -> Result { + let contents = fs::read_to_string("/etc/os-release") + .context("reading /etc/os-release failed")?; + for line in contents.lines() { + if line.starts_with("VERSION_CODENAME=") { + // unwrap: Safe because we know the line contains "=" + let (_, codename) = line.split_once("=").unwrap(); + return Ok(codename.trim().to_string()); + } + } + + bail!("Could not find VERSION_CODENAME in /etc/os-release") +} + +/// Check that the UNIX "ssh" group has no members +/// +/// See . +fn check_ssh_group() -> Result { + // There are no clean bindings to getgrpname in rustix, + // so jut shell out to getent to get group members + let output = process::Command::new("getent") + .arg("group") + .arg("ssh") + .output() + .context("spawning getent failed")?; + if output.status.code() == Some(2) { + println!("ssh OK: group does not exist"); + return Ok(true); + } else if !output.status.success() { + bail!( + "running getent failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let stdout = String::from_utf8(output.stdout) + .context("getent stdout is not utf-8")?; + let members = parse_getent_output(&stdout)?; + if members.is_empty() { + println!("ssh OK: group is empty"); + Ok(true) + } else { + println!("ssh ERROR: group is not empty: {members:?}"); + Ok(false) + } +} + +/// Parse the output of `getent group ssh`, return true if empty +fn parse_getent_output(stdout: &str) -> Result> { + let stdout = stdout.trim(); + // The format looks like `ssh:x:123:member1,member2` + if !stdout.contains(":") { + bail!("unexpected output from getent: '{stdout}'"); + } + + // unwrap: safe, we know the line contains ":" + let (_, members) = stdout.rsplit_once(':').unwrap(); + if members.is_empty() { + Ok(vec![]) + } else { + Ok(members.split(',').collect()) + } +} + +/// Check that ufw was removed +/// +/// See . +fn check_ufw_removed() -> bool { + if Path::new("/usr/sbin/ufw").exists() { + println!("ufw ERROR: ufw is still installed"); + false + } else { + println!("ufw OK: ufw was removed"); + true + } +} + +/// Estimate the size of the backup so we know how much free space we'll need. +/// +/// We just check the size of `/var/lib/securedrop` since that's really the +/// data that'll take up space; everything else is just config files that are +/// negligible post-compression. We also don't estimate compression benefits. +fn estimate_backup_size() -> Result { + let path = Path::new("/var/lib/securedrop"); + if !path.exists() { + // mon server + return Ok(0); + } + let mut total: u64 = 0; + let walker = WalkDir::new(path); + for entry in walker { + let entry = entry.context("walking /var/lib/securedrop failed")?; + if entry.file_type().is_dir() { + continue; + } + let metadata = entry.metadata().context("getting metadata failed")?; + total += metadata.len(); + } + + Ok(total) +} + +/// We want to have enough space for a backup, the upgrade (~4GB of packages, +/// conservatively), and not take up more than 90% of the disk. +fn check_free_space() -> Result { + // Also no simple bindings to get disk size, so shell out to df + // Explicitly specify -B1 for bytes (not kilobytes) + let output = process::Command::new("df") + .args(["-B1", "/"]) + .output() + .context("spawning df failed")?; + if !output.status.success() { + bail!( + "running df failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let stdout = + String::from_utf8(output.stdout).context("df stdout is not utf-8")?; + let parsed = parse_df_output(&stdout)?; + + let backup_needs = estimate_backup_size()?; + let upgrade_needs: u64 = 4 * 1024 * 1024 * 1024; // 4GB + let headroom = parsed.total / 10; // 10% headroom + let total_needs = backup_needs + upgrade_needs + headroom; + + if parsed.free < total_needs { + println!( + "free space ERROR: not enough free space, have {} free bytes, need {total_needs} bytes", + parsed.free + ); + Ok(false) + } else { + println!("free space OK: enough free space"); + Ok(true) + } +} + +/// Sizes are in bytes +struct DfOutput { + total: u64, + free: u64, +} + +fn parse_df_output(stdout: &str) -> Result { + let line = match stdout.split_once('\n') { + Some((_, line)) => line, + None => bail!("df output didn't have a newline"), + }; + let parts: Vec<_> = line.split_whitespace().collect(); + + if parts.len() < 4 { + bail!("df output didn't have enough columns"); + } + + // vec indexing is safe because we did the bounds check above + let total = parts[1] + .parse::() + .context("parsing total space failed")?; + let free = parts[3] + .parse::() + .context("parsing free space failed")?; + + Ok(DfOutput { total, free }) +} + +const EXPECTED_DOMAINS: [&str; 3] = [ + "archive.ubuntu.com", + "security.ubuntu.com", + "apt.freedom.press", +]; + +const TEST_DOMAINS: [&str; 2] = + ["apt-qa.freedom.press", "apt-test.freedom.press"]; + +/// Verify only expected sources are configured for apt +fn check_apt() -> Result { + let output = process::Command::new("apt-get") + .arg("indextargets") + .output() + .context("spawning apt-get indextargets failed")?; + if !output.status.success() { + bail!( + "running apt-get indextargets failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let stdout = String::from_utf8(output.stdout) + .context("apt-get stdout is not utf-8")?; + for line in stdout.lines() { + if line.starts_with("URI:") { + let uri = line.strip_prefix("URI: ").unwrap(); + let parsed = Url::parse(uri)?; + if let Some(Host::Domain(domain)) = parsed.host() { + if TEST_DOMAINS.contains(&domain) { + println!("apt: WARNING test source found ({domain})"); + } else if !EXPECTED_DOMAINS.contains(&domain) { + println!("apt ERROR: unexpected source: {domain}"); + return Ok(false); + } + } else { + println!("apt ERROR: unexpected source: {uri}"); + return Ok(false); + } + } + } + + println!("apt OK: all sources are expected"); + Ok(true) +} + +/// Check that systemd has no failed units +pub fn check_systemd() -> Result { + let output = process::Command::new("systemctl") + .arg("is-failed") + .output() + .context("spawning systemctl failed")?; + if output.status.success() { + // success means some units are failed + println!("systemd ERROR: some units are failed"); + Ok(false) + } else { + println!("systemd OK: all units are happy"); + Ok(true) + } +} + +pub fn run_checks() -> Result { + Ok(State { + ssh: check_ssh_group()?, + ufw: check_ufw_removed(), + free_space: check_free_space()?, + apt: check_apt()?, + systemd: check_systemd()?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_getent_output() { + // no members + assert_eq!( + parse_getent_output("ssh:x:123:\n").unwrap(), + Vec::<&str>::new() + ); + // one member + assert_eq!( + parse_getent_output("ssh:x:123:member1\n").unwrap(), + vec!["member1"] + ); + // two members + assert_eq!( + parse_getent_output("ssh:x:123:member1,member2\n").unwrap(), + vec!["member1", "member2"] + ); + } + + #[test] + fn test_parse_df_output() { + let output = parse_df_output( + "Filesystem 1B-blocks Used Available Use% Mounted on +/dev/mapper/ubuntu--vg-ubuntu--lv 105089261568 8573784064 91129991168 9% / +", + ) + .unwrap(); + + assert_eq!(output.total, 105089261568); + assert_eq!(output.free, 91129991168); + } +} diff --git a/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.service b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.service new file mode 100644 index 0000000000..d70b80ec93 --- /dev/null +++ b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.service @@ -0,0 +1,8 @@ +[Unit] +Description=Run noble migration + +[Service] +Type=exec +Environment=LAUNCHED_BY_SYSTEMD=1 +ExecStart=/usr/bin/securedrop-noble-migration-upgrade +User=root diff --git a/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.timer b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.timer new file mode 100644 index 0000000000..2f5892d34d --- /dev/null +++ b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-upgrade.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Run noble migration + +[Timer] +OnBootSec=3m +OnUnitInactiveSec=3m +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/securedrop/debian/config/usr/share/securedrop/noble-upgrade.json b/securedrop/debian/config/usr/share/securedrop/noble-upgrade.json new file mode 100644 index 0000000000..4c0a630183 --- /dev/null +++ b/securedrop/debian/config/usr/share/securedrop/noble-upgrade.json @@ -0,0 +1,10 @@ +{ + "app": { + "enabled": false, + "bucket": 0 + }, + "mon": { + "enabled": false, + "bucket": 0 + } +} diff --git a/securedrop/debian/rules b/securedrop/debian/rules index 548660c328..a509271a2f 100755 --- a/securedrop/debian/rules +++ b/securedrop/debian/rules @@ -22,7 +22,8 @@ override_dh_auto_install: cd /srv/rust/noble-migration && cargo build --release --locked && \ cd /srv/securedrop && \ mkdir -p ./debian/securedrop-config/usr/bin && \ - mv /srv/rust/target/release/check ./debian/securedrop-config/usr/bin/securedrop-noble-migration-check + mv /srv/rust/target/release/check ./debian/securedrop-config/usr/bin/securedrop-noble-migration-check && \ + mv /srv/rust/target/release/upgrade ./debian/securedrop-config/usr/bin/securedrop-noble-migration-upgrade # Build redwood wheel python3 /srv/rust/redwood/build-wheel.py --release --redwood /srv/rust/redwood --target /srv/rust/target # Set up virtualenv and install dependencies @@ -104,4 +105,5 @@ override_dh_systemd_start: dh_systemd_start --no-start securedrop-cleanup-ossec.service dh_systemd_start --no-start securedrop-reboot-required.service dh_systemd_start --no-start securedrop-noble-migration-check.service + dh_systemd_start --no-start securedrop-noble-migration-upgrade.service dh_systemd_start diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index a420308c26..876a482cca 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -300,6 +300,20 @@ start = "2019-03-19" end = "2024-05-02" notes = "Rust Project member" +[[trusted.env_filter]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2024-01-19" +end = "2025-06-02" +notes = "Rust Project member" + +[[trusted.env_logger]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-11-24" +end = "2025-06-02" +notes = "Rust Project member" + [[trusted.equivalent]] criteria = "safe-to-deploy" user-id = 539 # Josh Stone (cuviper) diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index ce168f3c3e..eaa22c75af 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -64,6 +64,20 @@ user-id = 539 user-login = "cuviper" user-name = "Josh Stone" +[[publisher.env_filter]] +version = "0.1.2" +when = "2024-07-25" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.env_logger]] +version = "0.11.5" +when = "2024-07-25" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + [[publisher.equivalent]] version = "1.0.1" when = "2023-07-10" @@ -448,6 +462,13 @@ criteria = "safe-to-run" delta = "2.3.2 -> 2.4.0" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.byteorder]] +who = "danakj " +criteria = "safe-to-deploy" +version = "1.5.0" +notes = "Unsafe review in https://crrev.com/c/5838022" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.cc]] who = "George Burgess IV " criteria = "safe-to-run" @@ -520,6 +541,12 @@ crypto implementations. Hence, this crate does not implement crypto. """ aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.humantime]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "2.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.itertools]] who = "ChromeOS" criteria = "safe-to-run" @@ -562,16 +589,16 @@ version = "1.4.0" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" [[audits.google.audits.log]] -who = "ChromeOS" -criteria = "safe-to-run" -version = "0.4.17" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +who = "danakj " +criteria = "safe-to-deploy" +version = "0.4.22" +notes = """ +Unsafe review in https://docs.google.com/document/d/1IXQbD1GhTRqNHIGxq6yy7qHqxeO4CwN5noMFXnqyDIM/edit?usp=sharing -[[audits.google.audits.log]] -who = "George Burgess IV " -criteria = "safe-to-run" -delta = "0.4.17 -> 0.4.20" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +Unsafety is generally very well-documented, with one exception, which we +describe in the review doc. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" [[audits.google.audits.memoffset]] who = "Dennis Kempin " @@ -609,6 +636,24 @@ criteria = "safe-to-run" version = "0.3.26" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.ppv-lite86]] +who = "danakj@chromium.org" +criteria = "safe-to-run" +version = "0.2.17" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.ppv-lite86]] +who = "danakj " +criteria = "safe-to-run" +delta = "0.2.17 -> 0.2.20" +notes = "Using zerocopy to reduce unsafe usage." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.proc-macro2]] who = "Adrian Taylor " criteria = "safe-to-deploy" @@ -704,6 +749,29 @@ The delta just 1) inlines/expands `impl ToTokens` that used to be handled via """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.rand]] +who = "danakj@chromium.org" +criteria = "safe-to-run" +version = "0.8.5" +notes = """ +Reviewed in https://crrev.com/c/5171063 + +Previously reviewed during security review and the audit is grandparented in. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.rand_chacha]] +who = "Android Legacy" +criteria = "safe-to-run" +version = "0.3.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.rand_core]] +who = "Android Legacy" +criteria = "safe-to-run" +version = "0.6.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.same-file]] who = "Android Legacy" criteria = "safe-to-run" @@ -1421,7 +1489,7 @@ aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-ch [[audits.mozilla.audits.url]] who = "Valentin Gosu " criteria = "safe-to-deploy" -delta = "2.5.1 -> 2.5.3" +delta = "2.5.1 -> 2.5.4" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" [[audits.mozilla.audits.utf16_iter]] @@ -1488,6 +1556,26 @@ criteria = "safe-to-deploy" delta = "0.7.3 -> 0.7.4" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.zerocopy]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.7.32" +notes = """ +This crate is `no_std` so doesn't use any side-effectful std functions. It +contains quite a lot of `unsafe` code, however. I verified portions of this. It +also has a large, thorough test suite. The project claims to run tests with +Miri to have stronger soundness checks, and also claims to use formal +verification tools to prove correctness. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.zerocopy-derive]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.7.32" +notes = "Clean, safe macros for zerocopy." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.zerofrom]] who = "Makoto Kato " criteria = "safe-to-deploy" @@ -1580,3 +1668,15 @@ who = "Jack Grigg " criteria = "safe-to-deploy" delta = "1.16.0 -> 1.17.0" aggregated-from = "https://raw.githubusercontent.com/zcash/zcash/master/qa/supply-chain/audits.toml" + +[[audits.zcash.audits.zerocopy]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.32 -> 0.7.34" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml" + +[[audits.zcash.audits.zerocopy-derive]] +who = "Daira-Emma Hopwood " +criteria = "safe-to-deploy" +delta = "0.7.32 -> 0.7.34" +aggregated-from = "https://raw.githubusercontent.com/zcash/librustzcash/main/supply-chain/audits.toml"