| // Copyright 2023 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use crate::arch::Arch; |
| use crate::config::Config; |
| use crate::gen_disk::{ |
| corrupt_kern_a, corrupt_pubkey_section, SignAfterCorrupt, VerboseRuntimeLogs, |
| }; |
| use crate::network::HttpsResource; |
| use crate::qemu::{Display, QemuOpts}; |
| use crate::{copy_file, run_bootloader_build}; |
| use anyhow::{bail, Result}; |
| use command_run::Command; |
| use fs_err as fs; |
| use std::fs::Permissions; |
| use std::io::{BufRead, BufReader}; |
| use std::os::unix::fs::PermissionsExt; |
| use std::thread; |
| use std::time::{Duration, Instant}; |
| |
| /// Timeout used for error tests. |
| /// |
| /// This can be relatively short since error tests fail early in boot |
| /// (we don't have to wait for SSH to come up like in the success |
| /// tests). |
| const VM_ERROR_TIMEOUT: Duration = Duration::from_secs(30); |
| |
| /// Download the well-known testing_rsa key for ChromeOS test images. |
| fn download_test_key(conf: &Config) -> Result<()> { |
| let mut resource = HttpsResource::new("https://chromium.googlesource.com/chromiumos/chromite/+/HEAD/ssh_keys/testing_rsa?format=TEXT"); |
| resource.enable_base64_decode(); |
| resource |
| .set_expected_sha256("ebd33984c3b671f8aa82f73ab12dd1fe5af6af7080bd6361e3ec814f60c335be"); |
| let key = resource.download_to_vec()?; |
| fs::write(conf.ssh_key_path(), key)?; |
| |
| // Set key permissions. |
| fs::set_permissions(conf.ssh_key_path(), Permissions::from_mode(0o600)).unwrap(); |
| |
| Ok(()) |
| } |
| |
| /// Wait for SSH to come up on the VM (indicating a successful |
| /// boot). Times out after one minute. |
| fn wait_for_ssh(conf: &Config) -> Result<()> { |
| println!("waiting for SSH"); |
| let mut cmd = Command::with_args( |
| "ssh", |
| [ |
| "-oStrictHostKeyChecking=no", |
| "-oUserKnownHostsFile=/dev/null", |
| // Enable batch mode to prevent password prompts if the SSH |
| // key is misconfigured. |
| "-oBatchMode=yes", |
| "-i", |
| conf.ssh_key_path().as_str(), |
| "-p", |
| &Config::ssh_port().to_string(), |
| "root@localhost", |
| "true", |
| ], |
| ); |
| cmd.check = false; |
| cmd.capture = true; |
| cmd.log_command = false; |
| |
| // Wait up to one minute for SSH to come up. |
| let start_time = Instant::now(); |
| while start_time.elapsed() < Duration::from_secs(120) { |
| let output = cmd.run()?; |
| if output.status.success() { |
| return Ok(()); |
| } |
| |
| thread::sleep(Duration::from_secs(1)); |
| } |
| |
| bail!("SSH didn't come up"); |
| } |
| |
| /// Test successful boots on both ia32 and x64. |
| fn test_successful_boot(conf: &Config) -> Result<()> { |
| for arch in Arch::all() { |
| println!("test successful boot with arch={arch:?}"); |
| let opts = QemuOpts { |
| capture_output: true, |
| display: Display::None, |
| // No need to copy the disk for this test since we aren't |
| // modifying it. |
| image_path: conf.disk_path().to_path_buf(), |
| ovmf: conf.ovmf_paths(arch), |
| secure_boot: true, |
| snapshot: true, |
| timeout: None, |
| tpm_version: None, |
| }; |
| let _vm = opts.spawn_disk_image(conf)?; |
| |
| // Check that SSH comes up, indicating a successful boot. |
| wait_for_ssh(conf)?; |
| } |
| |
| Ok(()) |
| } |
| |
| /// Make a copy of the original disk image so that we can modify it for |
| /// the test. |
| fn create_test_disk(conf: &Config) -> Result<()> { |
| copy_file(conf.disk_path(), conf.test_disk_path()) |
| } |
| |
| /// Helper for testing vboot errors. |
| /// |
| /// This launches the test disk in a VM and monitors the output, looking |
| /// for each error string in `expected_errors` (in order). Once all |
| /// expected errors have been output by the VM, the VM is killed and |
| /// `Ok` is returned. |
| /// |
| /// If the expected errors do not occur within `VM_ERROR_TIMEOUT`, the |
| /// VM is killed and an error is returned. |
| fn launch_test_disk_and_expect_errors(conf: &Config, expected_errors: &[&str]) -> Result<()> { |
| // At least one expected error is required. |
| assert!(!expected_errors.is_empty()); |
| |
| let opts = QemuOpts { |
| capture_output: true, |
| display: Display::None, |
| image_path: conf.test_disk_path(), |
| ovmf: conf.ovmf_paths(Arch::X64), |
| secure_boot: true, |
| snapshot: true, |
| timeout: Some(VM_ERROR_TIMEOUT), |
| tpm_version: None, |
| }; |
| let vm = opts.spawn_disk_image(conf)?; |
| |
| let stdout = vm.qemu.lock().unwrap().stdout.take().unwrap(); |
| let mut reader = BufReader::new(stdout); |
| let mut expected_errors = expected_errors.to_vec(); |
| |
| while let Some(next_expected_error) = expected_errors.first() { |
| let mut line = String::new(); |
| if reader.read_line(&mut line)? == 0 { |
| // EOF reached, which means the VM has stopped. |
| bail!("QEMU no longer running"); |
| } |
| print!(">>> {line}"); |
| |
| if line.contains(next_expected_error) { |
| expected_errors.remove(0); |
| } |
| } |
| |
| // The expected errors have all occurred, test is successful. Kill |
| // the VM. |
| vm.qemu.lock().unwrap().kill().unwrap(); |
| Ok(()) |
| } |
| |
| /// Test failed boot due to corrupt KERN-A. |
| /// |
| /// This test generates an intentionally corrupt disk, where a single |
| /// byte in the kernel data has been changed so that the signature is no |
| /// longer valid. |
| /// |
| /// The test checks that vboot rejects that kernel with a specific |
| /// error, and that crdyboot fails to boot. |
| fn test_corrupt_kern_a(conf: &Config) -> Result<()> { |
| println!("test if boot correctly fails when KERN-A is corrupt"); |
| |
| create_test_disk(conf)?; |
| |
| corrupt_kern_a(&conf.test_disk_path())?; |
| |
| let expected_errors = &[ |
| "Kernel data verification failed", |
| "boot failed: failed to load kernel", |
| ]; |
| launch_test_disk_and_expect_errors(conf, expected_errors) |
| } |
| |
| /// This test modifies a byte in the `.vbpubk` section of the |
| /// bootloader, then verifies that shim refuses to launch crdyboot due |
| /// to the executable's signature no longer being valid. |
| /// |
| /// This validates that the `.vbpubk` section is properly included in |
| /// the authenticode hash, and shim is correctly validating the |
| /// signature. |
| fn test_vbpubk_mod_breaks_signature(conf: &Config) -> Result<()> { |
| println!("test that modifying the vbpubk section prevents crdyboot from launching"); |
| |
| create_test_disk(conf)?; |
| |
| corrupt_pubkey_section(conf, &conf.test_disk_path(), SignAfterCorrupt(false))?; |
| |
| let expected_errors = &["boot failed: signature verification failed"]; |
| launch_test_disk_and_expect_errors(conf, expected_errors) |
| } |
| |
| /// This test modifies a byte in the `.vbpubk` section of crdyboot and |
| /// then re-signs crdyboot. It then verifies that crdyboot refuses to |
| /// launch the kernel since the pubkey is no longer valid. |
| /// |
| /// This validates two things: |
| /// 1. Crdyboot is reading the pubkey from the expected place in the binary. |
| /// 2. Vboot will not load a kernel if the pubkey is invalid. |
| fn test_signed_vbpubk_mod_breaks_vboot(conf: &Config) -> Result<()> { |
| println!( |
| "test that modifying the vbpubk section and re-signing prevents the kernel from launching" |
| ); |
| |
| create_test_disk(conf)?; |
| |
| corrupt_pubkey_section(conf, &conf.test_disk_path(), SignAfterCorrupt(true))?; |
| |
| let expected_errors = &[ |
| "vb2api_inject_kernel_subkey failed", |
| "boot failed: failed to load kernel", |
| ]; |
| launch_test_disk_and_expect_errors(conf, expected_errors) |
| } |
| |
| pub fn run_vm_tests(conf: &Config) -> Result<()> { |
| run_bootloader_build(conf, VerboseRuntimeLogs(true))?; |
| download_test_key(conf)?; |
| |
| test_signed_vbpubk_mod_breaks_vboot(conf)?; |
| test_vbpubk_mod_breaks_signature(conf)?; |
| test_corrupt_kern_a(conf)?; |
| test_successful_boot(conf)?; |
| |
| Ok(()) |
| } |