feat(cargo-wdk): add --signtool-args passthrough to customize driver signing - #699
feat(cargo-wdk): add --signtool-args passthrough to customize driver signing#699svasista-ms wants to merge 35 commits into
--signtool-args passthrough to customize driver signing#699Conversation
There was a problem hiding this comment.
Pull request overview
Adds a cargo wdk build --signtool-args passthrough so driver signing can be customized (certificate selection, digest, timestamping, extra operands), and adjusts packaging to stage artifacts in a fresh directory and assemble the final package folder last—preventing stale signing artifacts from persisting across rebuilds.
Changes:
- Add
--signtool-argsto the CLI and plumb it through tosigntool signinvocation. - Rework packaging to build in a clean per-build staging directory and rename into the final package folder at the end (fixes stale cert artifact scenarios).
- Expand integration/unit tests and documentation to cover the new signing behavior and staging semantics.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/cargo-wdk/tests/build_command_test.rs | Adds regression + functional integration tests for staging behavior and --signtool-args. |
| crates/cargo-wdk/src/providers/mod.rs | Extends filesystem error enum to cover directory removal failures. |
| crates/cargo-wdk/src/providers/fs.rs | Adds remove_dir_all wrapper to the FS provider for testable directory cleanup. |
| crates/cargo-wdk/src/cli.rs | Introduces --signtool-args and validates signing flag combinations via TryFrom<&BuildArgs> for SignMode. |
| crates/cargo-wdk/src/actions/build/tests.rs | Updates build action unit test expectations for staging-dir + final assembly flow and new SignMode shape. |
| crates/cargo-wdk/src/actions/build/package_task.rs | Implements staging directory flow, package folder assembly, signtool argument tokenization + passthrough, and updates signing behavior. |
| crates/cargo-wdk/src/actions/build/mod.rs | Adjusts BuildAction to clone SignMode (now contains owned data). |
| crates/cargo-wdk/README.md | Documents --signtool-args, quoting/tokenization rules, and updated signing/staging semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #699 +/- ##
==========================================
+ Coverage 80.47% 81.66% +1.18%
==========================================
Files 26 25 -1
Lines 5722 6107 +385
Branches 5722 6107 +385
==========================================
+ Hits 4605 4987 +382
Misses 989 989
- Partials 128 131 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
| /// Builds a `clap::Error` with the given message, rendered with the standard | ||
| /// `cargo wdk build` usage for a consistent CLI experience. | ||
| fn build_error(message: impl std::fmt::Display) -> clap::Error { |
There was a problem hiding this comment.
nit: we could inline this at call site, no need of a separate function
There was a problem hiding this comment.
Yes, it was inline initially. But I added the function because ArgumentConflict error can be constructed from multiple places (2 in try_from for SignMode). Once args for all tools are added we could use anyhow to construct CLI errors.
There was a problem hiding this comment.
Move build_error into the function in which it is used and declare it as a nested function. That is a good pattern to follow whenever you need a function that is used only inside one function.
|
|
||
| /// Arguments to `signtool sign` for signing the driver binary and catalog file. | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct SigntoolArgs(pub Vec<String>); |
There was a problem hiding this comment.
Move this declaration to the top to keep all arg type declarations at one place.
| .expect("args parse"); | ||
| let err = SignMode::try_from(&args).expect_err("should be rejected"); | ||
| assert!( | ||
| err.to_string().contains("`--sign-mode=off`"), |
There was a problem hiding this comment.
- use the exact message for assertion
`--signtool-args` cannot be used with `--sign-mode=off`.
- Can we not match exactly and avoid using contains?
There was a problem hiding this comment.
Fixed ✅, using the full message for assertion 👍
| } | ||
|
|
||
| #[test] | ||
| fn build_off_mode_maps_to_off() { |
There was a problem hiding this comment.
| fn build_off_mode_maps_to_off() { | |
| fn build_sign_mode_off_maps_correctly() { |
| let err = parse_build_args(&["--signtool-args", "/n \"CN=Contoso"]) | ||
| .expect_err("unterminated quote should be rejected"); | ||
| assert!( | ||
| err.to_string().contains("unterminated"), |
There was a problem hiding this comment.
Try and use exact message assertion
Similar suggestion as this: https://github.com/microsoft/windows-drivers-rs/pull/699/changes#r3579259207
| } | ||
|
|
||
| #[test] | ||
| fn build_signtool_args_with_verify_signature_maps_both() { |
There was a problem hiding this comment.
| fn build_signtool_args_with_verify_signature_maps_both() { | |
| fn build_verify_signature_works_with_signtool_args() { |
| @@ -304,25 +402,119 @@ mod tests { | |||
|
|
|||
| #[test] | |||
| fn build_rejects_verify_signature_when_sign_mode_is_off() { | |||
There was a problem hiding this comment.
As we have multiple tests for build sub command, We should move all the build tests to a submodule here and avoid using build as prefix for each test.
There was a problem hiding this comment.
Done ✅ , moved to a separate mod
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
crates/cargo-wdk/tests/build_command_test.rs:653
ensure_cert_in_storehard-asserts thatcertmgr.exe -s <store>succeeds. For a custom store (e.g.WDRCustomTestStore), that query can fail if the store doesn’t exist yet, which makes these functional tests unnecessarily flaky. The production code path treats a non-successcertmgrstatus as “cert not found” and proceeds tomakecert; this helper should do the same.
let output = Command::new("certmgr.exe")
.args(["-s", store])
.output()
.expect("failed to query certificate store");
assert!(output.status.success(), "certmgr query failed for {store}");
crates/cargo-wdk/README.md:160
- The README example recommends passing a PFX password inline via
/p <password>. Even with redaction in cargo-wdk logs, command-line arguments are often exposed via process listings and shell history. Add an explicit warning here so users don’t accidentally leak secrets when copy/pasting this example.
- To test-sign with a PFX file whose password is supplied inline, run:
|
|
||
| /// Builds a `clap::Error` with the given message, rendered with the standard | ||
| /// `cargo wdk build` usage for a consistent CLI experience. | ||
| fn build_error(message: impl std::fmt::Display) -> clap::Error { |
There was a problem hiding this comment.
Move build_error into the function in which it is used and declare it as a nested function. That is a good pattern to follow whenever you need a function that is used only inside one function.
…sign_mode` and drop qualified `Result/Ok` paths
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
crates/cargo-wdk/src/actions/build/package_task.rs:560
- Redaction index calculation can incorrectly redact the final file operand when the caller passes
/pwithout a password value (e.g.--signtool-args '/p'). Since the package code appends the file path as the last argument, the currenti + 1 < arg_refs.len()condition treats that file operand as the password value and hides it in logs/errors, making failures harder to diagnose.
.filter_map(|(i, arg)| {
(arg.eq_ignore_ascii_case("/p") && i + 1 < arg_refs.len()).then_some(i + 1)
})
There was a problem hiding this comment.
🟡 Not ready to approve
The new signtool password redaction logic can inadvertently redact the final file operand when /p is provided without a value, making failures unnecessarily hard to diagnose.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Comments suppressed due to low confidence (1)
crates/cargo-wdk/src/actions/build/package_task.rs:560
- The
/predaction logic can end up redacting the final file operand when the user accidentally supplies/pwithout a value (because cargo-wdk appends the file path after user args). That makes the resulting error/log output much harder to diagnose (the file being signed becomes<hidden>), even though the file path isn’t secret. Consider skipping redaction when the would-be redacted index is the final file operand.
.enumerate()
.filter_map(|(i, arg)| {
(arg.eq_ignore_ascii_case("/p") && i + 1 < arg_refs.len()).then_some(i + 1)
})
.collect();
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Low
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Not ready to approve
A new unit-test helper in package_task.rs compares &[&str] directly to Vec<String> (won’t compile), and the README currently doesn’t reflect the implemented “empty/whitespace args => default signing” behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Comments suppressed due to low confidence (1)
crates/cargo-wdk/README.md:126
- This section says that whenever
--signtool-argsis provided, the caller owns the fullsigntool signoption set, but the CLI parser treats empty/whitespace (and empty quotes) as no args and therefore falls back to the default auto-generated test certificate behavior. Please document this special-case (or alternatively reject empty values) so scripting mistakes don’t silently change signing behavior.
- When `--signtool-args` is **omitted**, cargo-wdk signs with the auto-generated WDR test certificate as described above.
- When `--signtool-args` is **provided**, you own the full `signtool sign` option set (certificate selection, digest algorithm, etc.). `cargo-wdk` will prepend the `sign` verb to your arguments and append the trailing file operand so you should not provide them.
`--signtool-args` applies only when signing is enabled; supplying it with `--sign-mode=off` is an error.
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Low
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| /// Signs the specified file using signtool command using certificate from | ||
| /// certificate store. | ||
| /// Signs the file with `signtool` by executing the following command: | ||
| /// `sign <signtool_args...> <file>` |
There was a problem hiding this comment.
| /// `sign <signtool_args...> <file>` | |
| /// `sign <signtool_args...> <file_path>` |
| self.generate_certificate()?; | ||
| self.copy(&self.src_cert_file_path, &self.dest_cert_file_path)?; | ||
| // Default WDR test-cert switches. | ||
| vec![ |
There was a problem hiding this comment.
Instead of calling to_string on each item. We could
["/v", "/s", WDR_TEST_CERT_STORE, "/n", WDR_LOCAL_TEST_CERT,
"/t", DEFAULT_TIMESTAMP_URL, "/fd", "SHA256"]
.map(ToString::to_string)
.to_vec()| } | ||
|
|
||
| #[test] | ||
| fn sign_redacts_password_value() { |
There was a problem hiding this comment.
| fn sign_redacts_password_value() { | |
| fn sign_redacts_password_value_arg_by_redaction_index() { |
| ) -> Self { | ||
| let cwd = self.cwd.clone(); | ||
| self.expect_final_package_dir_exists(driver_name, &cwd, true) | ||
| self.expect_final_package_dir_exists(driver_name, &cwd, false) |
There was a problem hiding this comment.
The name of the function and its responsibility is confusing now. Earlier expect_final_package_dir_exists with true indicates that the setup sets up only the mock_fs_provider.expect_exists() to return true.
Now with new changes, the function name and boolean value not communicating the intent as it is doing multiple things.
.expect_final_package_dir_exists(...true) -> expects dir, removal and creation
.expect_final_package_dir_exists(...false) -> expects no dir, no removal but there will be creation
Ideally, I would have liked to keep the assertions separate. But if we decide to keep it as single function to reduce verbosity, .expect_final_package_dir_removal(driver_name, &cwd, true) could be a better name which communicates what the function would do differently with the provided boolean value. Assuming the call to exists and create_dir will always be called.
There was a problem hiding this comment.
I think expect_final_package_dir_creation is better as:
- creation is always done in both cases
- really creation is the main thing we want to verify (we care less about deletion etc.)
There was a problem hiding this comment.
IMO, Removal is equally important, as it represents the new behaviour we introduced.
If we go with expect_final_package_dir_creation, I would also like to see a separate test that explicitly verifies the deletion behaviour.
There was a problem hiding this comment.
I'm actually fine with the deletion check being just a part of the creation test. Deletion is only a secondary detail. The main goal is to get a package created which the user can deploy and use. Deletion is there just to avoid staleness in that process. So yeah, one test is perhaps okay. We can discuss over a call as well if you feel strongly about this.
| /// Signing mode. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub enum SignMode { |
There was a problem hiding this comment.
The Profile type is declared in src/actions/mod.rs, but it is only used by the BuildTask module.
In contrast, SignMode and TargetPlatform are declared directly within the PackageTask module, as they are only used there. This feels inconsistent.
|
|
||
| // First build produces a signed driver binary we can reuse as an | ||
| // independent second file operand. | ||
| run_build_cmd(&project_path, Some(&["--signtool-args", common_args]), None); |
There was a problem hiding this comment.
Since the goal was to sign an additional file passed through --signtool-args, we can create a random file ourselves and let the build sign it. There is no need to find a DLL, copy it, strip its signature, and so on.
There was a problem hiding this comment.
This is what I felt too. That extra build seems wasteful. But signtool cannot sign arbitrary types of files. It is limited to things like DLLs, EXEs, CABs etc. None of them are easy to create by hand. We could carry one in the codebase but that also seems awkward.
@svasista-ms can you please research and find out which file type amongst those supported by signtool can we easily create on the fly?
Relocate shared types out of `actions/mod.rs` so each lives in the action module that uses it, leaving `actions/mod.rs` as a pure module-tree root: - `Profile`, `to_target_triple` (+ target-triple consts) -> `actions/build` - `DriverType`, `KMDF_STR`/`UMDF_STR`/`WDM_STR` -> `actions/new` Update imports in `cli.rs`, `build/mod.rs`, `build/build_task.rs`, `build/tests.rs`, and `new/mod.rs` accordingly. Matches the existing placement of `SignMode`/`TargetPlatform` in the `build` module.
- build the default signtool switches via `[...].map(ToString::to_string)` instead of per-item `.to_string()` pushes - add a comment explaining the `/p` password redaction index logic - doc: `sign <signtool_args...> <file_path>` - rename test to `sign_redacts_password_value_arg_by_redaction_index`
There was a problem hiding this comment.
🟡 Not ready to approve
A newly added unit test helper compares &[&str] to Vec<String> directly in a mock predicate (won’t compile) and needs a small fix before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
crates/cargo-wdk/src/actions/build/package_task.rs:956
expect_signtool_argscomparesargs(type&[&str]fromCommandExec::run_with_redaction) directly toexpected(typeVec<String>), which will not compile. Convert the observedargsintoVec<String>(or changeexpectedtoVec<&str>) before comparing.
.withf(move |command, args, redaction_indices, _env, _cwd| {
command == "signtool"
&& args == expected
&& redaction_indices == expected_redaction_indices.as_slice()
})
- Files reviewed: 13/13 changed files
- Comments generated: 0 new
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Adds
--signtool-argspassthrough so driver signing can be customized (certificate selection, digest algorithm, timestamping, extra file operands).When
--signtool-argsis omitted,cargo-wdksigns with the auto-generated WDR test certificate and default switches.When
--signtool-argsis provided, the caller owns the fullsigntool signoption set except thesignverb and the trailing file operand (spplied bycargo-wdk). Supplying it with--sign-mode=offis rejected.Packaging removes any existing
<target>/<profile>/<name>_packagefolder at the start of each build and recreates it, so stale signing artifacts (e.g. aWDRLocalTestCert.cerfrom a previous--sign-mode=testbuild) don't persist across rebuilds.Screenshots
Resolves #605