diff --git a/crates/cargo-wdk/src/actions/build/mod.rs b/crates/cargo-wdk/src/actions/build/mod.rs index ad1d9159c..84d904dd8 100644 --- a/crates/cargo-wdk/src/actions/build/mod.rs +++ b/crates/cargo-wdk/src/actions/build/mod.rs @@ -29,7 +29,7 @@ use wdk_build::{ metadata::{TryFromCargoMetadataError, Wdk}, }; -use crate::actions::Profile; +use crate::actions::{Profile, SignMode}; #[double] use crate::providers::{exec::CommandExec, fs::Fs, metadata::Metadata, wdk_build::WdkBuild}; @@ -38,6 +38,7 @@ pub struct BuildActionParams<'a> { pub profile: Option<&'a Profile>, pub target_arch: Option, pub verify_signature: bool, + pub sign_mode: SignMode, pub is_sample_class: bool, pub verbosity_level: clap_verbosity_flag::Verbosity, } @@ -49,6 +50,7 @@ pub struct BuildAction<'a> { profile: Option<&'a Profile>, target_arch: Option, verify_signature: bool, + sign_mode: SignMode, is_sample_class: bool, verbosity_level: clap_verbosity_flag::Verbosity, @@ -90,6 +92,7 @@ impl<'a> BuildAction<'a> { profile: params.profile, target_arch: params.target_arch, verify_signature: params.verify_signature, + sign_mode: params.sign_mode, is_sample_class: params.is_sample_class, verbosity_level: params.verbosity_level, wdk_build, @@ -386,6 +389,7 @@ impl<'a> BuildAction<'a> { target_dir: &target_dir, target_arch: &target_arch, verify_signature: self.verify_signature, + sign_mode: self.sign_mode, sample_class: self.is_sample_class, driver_model, }, diff --git a/crates/cargo-wdk/src/actions/build/package_task.rs b/crates/cargo-wdk/src/actions/build/package_task.rs index 6162405aa..806b69477 100644 --- a/crates/cargo-wdk/src/actions/build/package_task.rs +++ b/crates/cargo-wdk/src/actions/build/package_task.rs @@ -28,7 +28,10 @@ use windows::{ #[double] use crate::providers::{exec::CommandExec, fs::Fs, wdk_build::WdkBuild}; -use crate::{actions::build::error::PackageTaskError, providers::error::FileError}; +use crate::{ + actions::{SignMode, build::error::PackageTaskError}, + providers::error::FileError, +}; // FIXME: This range is inclusive of 25798. Update with range end after /sample // flag is added to InfVerif CLI @@ -44,6 +47,7 @@ pub struct PackageTaskParams<'a> { pub target_dir: &'a Path, pub target_arch: &'a CpuArchitecture, pub verify_signature: bool, + pub sign_mode: SignMode, pub sample_class: bool, pub driver_model: DriverConfig, } @@ -52,6 +56,7 @@ pub struct PackageTaskParams<'a> { pub struct PackageTask<'a> { package_name: String, verify_signature: bool, + sign_mode: SignMode, sample_class: bool, // src paths @@ -162,6 +167,7 @@ impl<'a> PackageTask<'a> { Self { package_name, verify_signature: params.verify_signature, + sign_mode: params.sign_mode, sample_class: params.sample_class, src_inx_file_path, src_driver_binary_file_path, @@ -236,24 +242,36 @@ impl<'a> PackageTask<'a> { self.copy(&self.src_map_file_path, &self.dest_map_file_path)?; self.run_stampinf()?; self.run_inf2cat()?; - self.generate_certificate()?; - self.copy(&self.src_cert_file_path, &self.dest_cert_file_path)?; - self.run_signtool_sign( - &self.dest_driver_binary_path, - WDR_TEST_CERT_STORE, - WDR_LOCAL_TEST_CERT, - )?; - self.run_signtool_sign( - &self.dest_cat_file_path, - WDR_TEST_CERT_STORE, - WDR_LOCAL_TEST_CERT, - )?; + match self.sign_mode { + SignMode::Test => { + self.generate_certificate()?; + self.copy(&self.src_cert_file_path, &self.dest_cert_file_path)?; + self.run_signtool_sign( + &self.dest_driver_binary_path, + WDR_TEST_CERT_STORE, + WDR_LOCAL_TEST_CERT, + )?; + self.run_signtool_sign( + &self.dest_cat_file_path, + WDR_TEST_CERT_STORE, + WDR_LOCAL_TEST_CERT, + )?; + } + SignMode::Off => { + info!("Sign mode is 'off'; skipping certificate generation and signing"); + } + } self.run_infverif()?; // Verify signatures only when --verify-signature flag = true is passed + // and signing was done (sign mode is not 'off'). if self.verify_signature { - info!("Verifying signatures for driver binary and cat file using signtool"); - self.run_signtool_verify(&self.dest_driver_binary_path)?; - self.run_signtool_verify(&self.dest_cat_file_path)?; + if matches!(self.sign_mode, SignMode::Off) { + warn!("Skipping signature verification because sign mode is 'off'"); + } else { + info!("Verifying signatures for driver binary and cat file using signtool"); + self.run_signtool_verify(&self.dest_driver_binary_path)?; + self.run_signtool_verify(&self.dest_cat_file_path)?; + } } Ok(()) } @@ -636,6 +654,7 @@ mod tests { driver_model: DriverConfig::Kmdf(KmdfConfig::default()), sample_class: false, verify_signature: false, + sign_mode: SignMode::Test, }; let dest_root = target_dir.join(format!("{package_name}_package")); @@ -699,6 +718,7 @@ mod tests { driver_model: DriverConfig::Kmdf(KmdfConfig::default()), sample_class: false, verify_signature: false, + sign_mode: SignMode::Test, }; let command_exec = CommandExec::default(); @@ -725,6 +745,7 @@ mod tests { driver_model: DriverConfig::Kmdf(KmdfConfig::default()), sample_class: false, verify_signature: false, + sign_mode: SignMode::Test, }; let command_exec = CommandExec::default(); @@ -760,6 +781,7 @@ mod tests { driver_model: DriverConfig::Kmdf(KmdfConfig::default()), sample_class: false, verify_signature: false, + sign_mode: SignMode::Test, }; let wdk_build = WdkBuild::default(); diff --git a/crates/cargo-wdk/src/actions/build/tests.rs b/crates/cargo-wdk/src/actions/build/tests.rs index 6bb4f9973..e4af27584 100644 --- a/crates/cargo-wdk/src/actions/build/tests.rs +++ b/crates/cargo-wdk/src/actions/build/tests.rs @@ -29,6 +29,7 @@ use crate::providers::{ use crate::{ actions::{ Profile, + SignMode, build::{BuildAction, BuildActionParams, error::BuildActionError}, to_target_triple, }, @@ -265,6 +266,118 @@ pub fn given_a_driver_project_when_verify_signature_is_true_then_it_builds_succe ); } +#[test] +pub fn given_a_driver_project_when_sign_mode_is_off_then_signing_and_verification_steps_are_skipped() + { + // Input CLI args + let cwd = PathBuf::from("C:\\tmp"); + let profile = None; + let target_arch = CpuArchitecture::Amd64; + let verify_signature = false; + let sample_class = false; + + // Driver project data + let driver_type = "KMDF"; + let driver_name = "sample-kmdf"; + let driver_version = "0.0.1"; + let wdk_metadata = get_cargo_metadata_wdk_metadata(driver_type, 1, 33); + let (workspace_member, package) = + get_cargo_metadata_package(&cwd, driver_name, driver_version, Some(&wdk_metadata)); + + let cargo_build_output = + create_cargo_build_output_json(driver_name, driver_version, &cwd, None, profile); + let test_build_action = &TestBuildAction::new(cwd.clone(), profile, None, sample_class) + .with_sign_mode(SignMode::Off) + .set_up_standalone_driver_project((workspace_member, package)) + .expect_default_build_task_steps(driver_name, Some(cargo_build_output)) + .expect_probe_target_arch_using_cargo_rustc(&cwd, target_arch, None) + .expect_package_task_steps_with_sign_mode_off(driver_name, driver_type, target_arch); + + assert_build_action_run_with_env_is_success( + &cwd, + profile, + None, + verify_signature, + sample_class, + test_build_action, + ); +} + +#[test] +pub fn given_a_sample_class_driver_project_when_sign_mode_is_off_then_signing_is_skipped_and_sample_infverif_still_runs() + { + // Input CLI args + let cwd = PathBuf::from("C:\\tmp"); + let profile = None; + let target_arch = CpuArchitecture::Amd64; + let verify_signature = false; + let sample_class = true; + + // Driver project data + let driver_type = "KMDF"; + let driver_name = "sample-kmdf"; + let driver_version = "0.0.1"; + let wdk_metadata = get_cargo_metadata_wdk_metadata(driver_type, 1, 33); + let (workspace_member, package) = + get_cargo_metadata_package(&cwd, driver_name, driver_version, Some(&wdk_metadata)); + + let cargo_build_output = + create_cargo_build_output_json(driver_name, driver_version, &cwd, None, profile); + let test_build_action = &TestBuildAction::new(cwd.clone(), profile, None, sample_class) + .with_sign_mode(SignMode::Off) + .set_up_standalone_driver_project((workspace_member, package)) + .expect_default_build_task_steps(driver_name, Some(cargo_build_output)) + .expect_probe_target_arch_using_cargo_rustc(&cwd, target_arch, None) + .expect_package_task_steps_with_sign_mode_off(driver_name, driver_type, target_arch) + .expect_detect_wdk_build_number(25100u32); + + assert_build_action_run_with_env_is_success( + &cwd, + profile, + None, + verify_signature, + sample_class, + test_build_action, + ); +} + +#[test] +pub fn given_a_driver_project_when_sign_mode_is_off_and_verify_signature_is_true_then_verification_is_skipped() + { + // Input CLI args + let cwd = PathBuf::from("C:\\tmp"); + let profile = None; + let target_arch = CpuArchitecture::Amd64; + let verify_signature = true; + let sample_class = false; + + // Driver project data + let driver_type = "KMDF"; + let driver_name = "sample-kmdf"; + let driver_version = "0.0.1"; + let wdk_metadata = get_cargo_metadata_wdk_metadata(driver_type, 1, 33); + let (workspace_member, package) = + get_cargo_metadata_package(&cwd, driver_name, driver_version, Some(&wdk_metadata)); + + let cargo_build_output = + create_cargo_build_output_json(driver_name, driver_version, &cwd, None, profile); + let test_build_action = &TestBuildAction::new(cwd.clone(), profile, None, sample_class) + .with_sign_mode(SignMode::Off) + .set_up_standalone_driver_project((workspace_member, package)) + .expect_default_build_task_steps(driver_name, Some(cargo_build_output)) + .expect_probe_target_arch_using_cargo_rustc(&cwd, target_arch, None) + .expect_package_task_steps_with_sign_mode_off(driver_name, driver_type, target_arch); + + assert_build_action_run_with_env_is_success( + &cwd, + profile, + None, + verify_signature, + sample_class, + test_build_action, + ); +} + #[test] pub fn given_a_driver_project_when_self_signed_exists_then_it_should_skip_calling_makecert() { // Input CLI args @@ -1600,6 +1713,7 @@ fn initialize_build_action<'a>( profile, target_arch, verify_signature, + sign_mode: test_build_action.sign_mode, is_sample_class: sample_class, verbosity_level: clap_verbosity_flag::Verbosity::new(1, 0), }, @@ -1662,6 +1776,7 @@ struct TestBuildAction { profile: Option, target_arch: Option, sample_class: bool, + sign_mode: SignMode, cargo_metadata: Option, // mocks @@ -1688,6 +1803,7 @@ impl TestBuildAction { profile, target_arch, sample_class, + sign_mode: SignMode::Test, mock_run_command, mock_wdk_build_provider, mock_fs_provider, @@ -1696,6 +1812,11 @@ impl TestBuildAction { } } + fn with_sign_mode(mut self, sign_mode: SignMode) -> Self { + self.sign_mode = sign_mode; + self + } + fn set_up_standalone_driver_project( mut self, package_metadata: (TestMetadataWorkspaceMemberId, TestMetadataPackage), @@ -1826,6 +1947,28 @@ impl TestBuildAction { .expect_signtool_verify_cat_file(driver_name, &cwd, None) } + /// Sets up package-task expectations for `SignMode::Off`: stampinf, + /// inf2cat, and infverif are still expected, but all certificate + /// generation, signing, and signature-verification steps are skipped. + fn expect_package_task_steps_with_sign_mode_off( + self, + driver_name: &str, + driver_type: &str, + target_arch: CpuArchitecture, + ) -> Self { + let cwd = self.cwd.clone(); + self.expect_final_package_dir_exists(driver_name, &cwd, true) + .expect_inx_file_exists(driver_name, &cwd, true) + .expect_rename_driver_binary_dll_to_sys(driver_name, &cwd) + .expect_copy_driver_binary_sys_to_package_folder(driver_name, &cwd, true) + .expect_copy_pdb_file_to_package_folder(driver_name, &cwd, true) + .expect_copy_inx_file_to_package_folder(driver_name, &cwd, true, &cwd) + .expect_copy_map_file_to_package_folder(driver_name, &cwd, true) + .expect_stampinf(driver_name, &cwd, target_arch, None) + .expect_inf2cat(driver_name, &cwd, target_arch, None) + .expect_infverif(driver_name, &cwd, driver_type, None) + } + fn expect_default_package_task_steps_for_workspace( self, driver_name: &str, diff --git a/crates/cargo-wdk/src/actions/mod.rs b/crates/cargo-wdk/src/actions/mod.rs index 92197b781..9f89f108c 100644 --- a/crates/cargo-wdk/src/actions/mod.rs +++ b/crates/cargo-wdk/src/actions/mod.rs @@ -50,6 +50,27 @@ impl Display for Profile { } } +/// Driver signing mode used during the package phase. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)] +#[value(rename_all = "lower")] +pub enum SignMode { + /// Skip signing entirely. + Off, + /// Use test-signing with an auto-generated self-signed certificate. + #[default] + Test, +} + +impl Display for SignMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::Off => "off", + Self::Test => "test", + }; + write!(f, "{s}") + } +} + /// Converts `CpuArchitecture` to its corresponding target triple name. #[must_use] pub fn to_target_triple(cpu_arch: CpuArchitecture) -> String { diff --git a/crates/cargo-wdk/src/cli.rs b/crates/cargo-wdk/src/cli.rs index 907588363..e542cbcef 100644 --- a/crates/cargo-wdk/src/cli.rs +++ b/crates/cargo-wdk/src/cli.rs @@ -15,6 +15,7 @@ use crate::actions::{ DriverType, KMDF_STR, Profile, + SignMode, UMDF_STR, WDM_STR, build::{BuildAction, BuildActionParams}, @@ -85,10 +86,13 @@ pub struct BuildArgs { #[arg(long, ignore_case = true)] pub target_arch: Option, + /// Driver signing mode. + #[arg(long, value_enum, ignore_case = true, default_value_t = SignMode::Test)] + pub sign_mode: SignMode, + /// Verify the signature #[arg(long)] pub verify_signature: bool, - /// Build sample class driver project #[arg(long)] pub sample: bool, @@ -161,12 +165,18 @@ impl Cli { Ok(()) } Subcmd::Build(cli_args) => { + if cli_args.sign_mode == SignMode::Off && cli_args.verify_signature { + return Err(anyhow::anyhow!( + "`--verify-signature` cannot be used with `--sign-mode=off`." + )); + } BuildAction::new( &BuildActionParams { working_dir: Path::new("."), // Using current dir as working dir profile: cli_args.profile.as_ref(), target_arch: cli_args.target_arch, verify_signature: cli_args.verify_signature, + sign_mode: cli_args.sign_mode, is_sample_class: cli_args.sample, verbosity_level: self.verbose, }, @@ -244,4 +254,32 @@ mod tests { "Extended/Verbatim paths (i.e. paths starting with '\\?') are not currently supported" ); } + + #[test] + fn build_rejects_verify_signature_when_sign_mode_is_off() { + use crate::{ + actions::SignMode, + cli::{BuildArgs, Subcmd}, + }; + + let cli = Cli { + cargo_command: "wdk".to_string(), + sub_cmd: Subcmd::Build(BuildArgs { + profile: None, + target_arch: None, + verify_signature: true, + sign_mode: SignMode::Off, + sample: false, + }), + verbose: clap_verbosity_flag::Verbosity::default(), + }; + + let result = cli.run(); + assert!(result.is_err()); + let err = result.err().unwrap().to_string(); + assert!( + err.contains("--verify-signature") && err.contains("--sign-mode=off"), + "unexpected error: {err}" + ); + } } diff --git a/crates/cargo-wdk/tests/build_command_test.rs b/crates/cargo-wdk/tests/build_command_test.rs index 9aed17ff1..dfac54587 100644 --- a/crates/cargo-wdk/tests/build_command_test.rs +++ b/crates/cargo-wdk/tests/build_command_test.rs @@ -287,6 +287,112 @@ mod kmdf_driver_with_target_override { } } +#[test] +fn kmdf_driver_builds_successfully_with_sign_mode_off() { + let driver = "kmdf-driver"; + let project_path = format!("tests/{driver}"); + with_mutex(&project_path, || { + run_cargo_clean(&project_path); + + let stderr = run_build_cmd(&project_path, Some(&["--sign-mode", "off"]), None); + assert!(stderr.contains(&format!("Building package {driver}"))); + assert!(stderr.contains(&format!("Finished building {driver}"))); + + let driver_name = driver.replace('-', "_"); + let target_dir = format!("{project_path}/target/debug"); + let package_dir = format!("{target_dir}/{driver_name}_package"); + + // Standard package contents are still produced. + assert_dir_exists(&package_dir); + for ext in ["cat", "inf", "map", "pdb", "sys"] { + assert_file_exists(&format!("{package_dir}/{driver_name}.{ext}")); + } + + // No certificate is copied into the package folder. + let cert_in_package = PathBuf::from(format!("{package_dir}/WDRLocalTestCert.cer")); + assert!( + !cert_in_package.exists(), + "Expected no cert file in package folder when --sign-mode=off, but found {}", + cert_in_package.display() + ); + }); +} + +#[test] +fn sign_mode_off_does_not_produce_cert_file_in_target_dir() { + let driver = "kmdf-driver"; + let project_path = format!("tests/{driver}"); + with_mutex(&project_path, || { + run_cargo_clean(&project_path); + + let _ = run_build_cmd(&project_path, Some(&["--sign-mode", "off"]), None); + + let staged_cert = + PathBuf::from(format!("{project_path}/target/debug/WDRLocalTestCert.cer")); + assert!( + !staged_cert.exists(), + "Expected no staged cert file under target dir when --sign-mode=off, but found {}", + staged_cert.display() + ); + }); +} + +/// `--sign-mode=off` together with `--verify-signature` is rejected at the CLI +/// layer +#[test] +fn sign_mode_off_with_verify_signature_is_rejected() { + let driver = "kmdf-driver"; + let project_path = format!("tests/{driver}"); + let mut cmd = create_cargo_wdk_cmd( + "build", + Some(&["--sign-mode", "off", "--verify-signature"]), + None, + Some(&project_path), + ); + let assertion = cmd.assert().failure(); + let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string(); + assert!( + stderr.contains("--verify-signature") && stderr.contains("--sign-mode=off"), + "expected validation error mentioning both flags, got: {stderr}" + ); +} + +/// `cargo wdk build --help` must advertise every supported option. Adding a +/// new option to the `build` subcommand should also be reflected here so that +/// future regressions (e.g., a flag accidentally hidden or removed) are caught +/// at the CLI surface. +#[test] +fn build_help_advertises_all_options() { + let mut cmd = create_cargo_wdk_cmd("build", Some(&["--help"]), None, None::<&str>); + let assertion = cmd.assert().success(); + let stdout = String::from_utf8_lossy(&assertion.get_output().stdout).to_string(); + + // (flag, optional substring that must also appear on/near the flag's help + // line, e.g. its default value) + let expected_options: &[(&str, Option<&str>)] = &[ + ("--profile", None), + ("--target-arch", None), + ("--verify-signature", None), + ("--sign-mode", Some("[default: test]")), + ("--sample", None), + ("--help", None), + ]; + + for (flag, extra) in expected_options { + assert!( + stdout.contains(flag), + "expected `{flag}` in `cargo wdk build --help` output:\n{stdout}" + ); + if let Some(extra) = extra { + assert!( + stdout.contains(extra), + "expected `{extra}` (associated with `{flag}`) in `cargo wdk build --help` \ + output:\n{stdout}" + ); + } + } +} + #[allow(clippy::too_many_arguments)] fn clean_build_and_verify_project( driver_type: &str,