fix: Make Sign.SkipCertificateValidation actually skip validation - #194
Merged
Conversation
The setting was documented for the Store and Thumbprint sources and worked for neither. Get-PSBuildCertificate consulted -SkipValidation only for the EnvVar and PfxFile sources, which load exactly one certificate; Store and Thumbprint checked HasPrivateKey and NotAfter inside the Where-Object that selects the certificate, where the switch could not reach them. An expired store certificate was dropped before validation was ever considered and the build failed with NoCertificateFound, which points at a missing certificate rather than at the expiry that caused it. Naively hoisting those checks out of the filter would have changed selection: Select-Object -First 1 could then return an expired certificate even when a valid one exists further down the store. So the relaxation is a fallback instead. A valid certificate is preferred always, and a merely-present one is accepted only when nothing valid was found and the consumer explicitly opted out. The Thumbprint fallback keeps the thumbprint match and drops only the expiry check, because returning a certificate other than the one the consumer named would sign with a different identity than the build asked for. HasPrivateKey stays required in both: a certificate without a private key cannot sign anything, so skipping that check would buy nothing and only defer the failure to Set-AuthenticodeSignature with a worse error. Falling back now warns, since the consumer is about to sign with an expired certificate. Separately, IB.tasks.ps1 never passed the setting to Get-PSBuildCertificate in either of its signing tasks, so it was dead on all four sources for Invoke-Build consumers regardless. That is the same divergence between the two task files as #178, and no test could see it, so IBTasks.tests.ps1 now compares the $PSBPreference.Sign settings each task file reads. The comparison is scoped to Sign because the two files legitimately differ elsewhere. SkipCertificateValidation was the only signing setting missing from either. Closes #193 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE
tablackburn
added a commit
that referenced
this pull request
Aug 27, 2026
Publish-PSBuildModule validates -Path against $LocalizedData.PathDoesNotExist in a ValidateScript on a mandatory parameter. en-US/Messages.psd1 never defined that key, so the lookup returned $null, $null -f $argument is an empty string rather than an error, and passing a path that does not exist failed validation with no text at all -- while the very next check in the same script block reported itself properly. Unlike the drift this branch already fixes, that one was not confined to Windows PowerShell on a non-English machine. A key the file never defines is missing on every host and every culture. The new assertion is the other half of the promise this branch makes. The existing guard keeps a second copy of the strings from existing; this one keeps the single copy complete, by scanning the module source for every $LocalizedData reference and requiring Messages.psd1 to define it. The sweep found exactly one missing key and no unused ones, and it is worth having standing: #194 is adding a new string right now. Caught in review of #187. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE
…ficate-validation # Conflicts: # CHANGELOG.md # tests/IBTasks.tests.ps1
This was referenced Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #193. Two defects, one root cause each.
Get-PSBuildCertificatenever let-SkipValidationreach theStoreandThumbprintsources. Those two branches checkedHasPrivateKeyandNotAfterinside theWhere-Objectthat selects the certificate, where theswitch cannot reach them. The relaxation is now a fallback: a valid
certificate is preferred always, and a merely-present one is accepted only
when nothing valid was found and the consumer explicitly opted out.
IB.tasks.ps1never passed the setting at all. Both of its$certParamsblocks omitted
SkipValidation, so$PSBPreference.Sign.SkipCertificateValidationwas dead on all four sources for Invoke-Build consumers. Both blocks now pass it.
build.properties.ps1describes what the code now actually does, includingthe fallback semantics.
tests/IBTasks.tests.ps1gains a comparison of the$PSBPreference.Signsettings each task file reads, so this class of divergence cannot recur.
Why a fallback rather than a hoist
StoreandThumbprintselect one certificate from many, so filtering ishow the right one gets picked.
EnvVarandPfxFileload exactly one, sovalidation there is a gate on the only candidate. Hoisting the checks out of the
filter would have changed selection —
Select-Object -First 1could then returnan expired certificate even when a valid one exists further down the store. The
fallback preserves the selection semantics and still serves the CI case the
setting exists for.
The
Thumbprintfallback keeps the thumbprint match and drops only the expirycondition. Selecting a different certificate than the one the consumer named
would sign with a different identity than the build asked for.
HasPrivateKeystays required — confirmedThe snippet in the issue keeps requiring it, and that is right. A certificate
with no private key cannot sign anything. Relaxing that check buys nothing: it
does not turn a failing build into a passing one, it only moves the failure from
Get-PSBuildCertificate, which can say this certificate has no private key,to
Set-AuthenticodeSignature, which cannot.SkipCertificateValidationexiststo unblock a build whose certificate has just expired or is mid-rotation — a
real, recoverable situation. A certificate with no private key is not that.
Two tests pin this down, one per source: Does not return a certificate without
a private key even when SkipValidation is set.
The fallback warns
Yes. The consumer is about to sign with an expired certificate, and the whole
reason the current behavior is a bug is that the failure mode was silent —
StoreandThumbprintdropped the certificate and fell through to the genericNoCertificateFound, saying nothing about the expiry. Trading one silence foranother would be a poor fix.
The message follows the module's convention: a new
CertificateValidationRelaxedkey in
en-US/Messages.psd1, looked up through$LocalizedData. It reports theNotAfterand the subject, so the build log names the certificate and the date.Write-Warning, notWrite-Verbose— this should be visible in a default CI log.Per the task note, #187 changes how
$LocalizedDataloads; nothing here touchesthat loading, only the data file, so the new key should not conflict.
Task-file divergence sweep
Every
$PSBPreference.Sign.*read in both files, compared:psakeFile.ps1IB.tasks.ps1(before)Enabled,CertificateSource,CertStoreLocation,CertificateEnvVar,CertificatePasswordEnvVar,Thumbprint,PfxFilePath,PfxFilePassword,Certificate,TimestampServer,HashAlgorithm,FilesToSign,Catalog.Enabled,Catalog.Version,Catalog.FileNameSkipCertificateValidationSkipCertificateValidationwas the only one missing, in both theSignModuleand
SignCatalogbodies. Nothing was missing in the other direction.One non-
$PSBPreferencedifference is legitimate and was left alone:psakeFile.ps1addsVerbose = $VerbosePreference -eq 'Continue'to everyparameter hashtable and
IB.tasks.ps1does not. That is a task-runnerdifference, not a setting, and it is consistent across all four of the psake
signing hashtables rather than a one-off omission.
How the certificate store is mocked
The existing 17 tests do
Mock Get-ChildItem { }without-ModuleName, so themock never reaches the call inside the module — those tests assert
$nullandwould pass against a real, empty store, which is why they never caught this. The
new tests use
Mock -ModuleName PowerShellBuild -CommandName Get-ChildItem,which does reach it, and return
[PSCustomObject]certificates carrying justSubject,Thumbprint,HasPrivateKey, andNotAfter. That was verified towork,
-CodeSigningCertdynamic parameter included, before any test was written.They are
-Skip:(-not $IsWindows), matching every existingStore/Thumbprinttest:
-CodeSigningCertcomes from the certificate provider, which exists onlyon Windows, and the
Storesource throws on other platforms by design.Red before green
Against the unfixed code, with the new tests in place
(
Invoke-Pester ./tests/Get-PSBuildCertificate.tests.ps1,./tests/IBTasks.tests.ps1):Every behavior fixed by this PR fails first. All five pass after.
The sixth new behavior — Prefers a valid certificate over an expired one even
when SkipValidation is set — is the regression the fallback design exists to
prevent, so it is green against the unfixed code by construction. Green-before
proves nothing there, so it was validated against the naive hoist instead:
replacing the
Storefilter withGet-ChildItem ... | Select-Object -First 1and moving validation to the shared block, which is the obvious alternative fix.
The mock deliberately returns the expired certificate first so the hoisted
version picks it. The hoist was reverted before the real fix was written.
The two
SkipValidation-off tests and the twoHasPrivateKeytests pass in bothdirections; they pin behavior this PR must not change.
Test Plan
./build.ps1 -Task Test— 493 passed, 0 failed, 3 skipped. Baseline onmain(a64ebc8) is 483/0/3; the ten added tests are the nine newSkipValidationcases plus the signing-settings comparison.Analyze(PSScriptAnalyzer) clean — the four surviving warnings arepre-existing, in
New-PSBuildFileCatalog,Publish-PSBuildModule, andTest-PSBuildPester; none in the changed files.the naive-hoist alternative for the sixth.
LFand no\r\r\nintroduced (byte-counted).
README.mdwas not touched — but #190's row needs a follow-upOpen PR #190 documents
Sign.SkipCertificateValidationas:That was accurate against the implemented behavior when it was written, and #193
explicitly noted that #190 would stay correct whichever way this issue was
resolved. Resolving it by fixing the code makes that no longer true: the row now
under-reports, telling a consumer the setting does nothing for
StoreandThumbprintwhen it does. Per instructions the row was not edited here — itneeds a follow-up, either a rewording commit on #190 before it merges or a small
docs PR after. #190's
Settings documented in the READMEtest compares settingnames, not descriptions, so nothing breaks in the meantime and the two PRs do
not conflict.
tests/IBTasks.tests.ps1is also touched by #186 and #190, so the newDescribeblock is appended at the end of the file and nothing already there was
restructured.
Breaking Changes
None in the strict sense —
SkipCertificateValidationdefaults to$falseandthe behavior with it off is unchanged. Consumers who had it set to
$truewith aStoreorThumbprintsource were getting nothing from it before and now getthe documented fallback, which is the point of the fix.
🤖 Generated with Claude Code
https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE