ci(windows): optionally sign builder executables with Authenticode #98
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!98
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:ci/windows-authenticode"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Signed release checksums do not give the Windows builder a verified publisher. After #99 moved Windows packaging to Linux, this adds optional Linux-native Authenticode without choosing a legal publisher, exporting a private key or promising that SmartScreen warnings disappear.
What changed
HumanBean authored and pushed commit
b784ce0e108428560a3aeda515eec3af0b06e46a. He first rebased the four earlier commits onto mainfbd420241a, then added his own review fixes. The changes below describe that head, not the earlier implementation.The Linux package job signs three project files after
dotnet publish: the compiledLongshotBuilder-Setup.exe, the publishedLongshotBuilder.exeapphost, andLongshotBuilder.dll, which contains the Builder's own code. Every file must verify before any original is replaced or packaging starts. Third-party binaries remain untouched.ARCHIVE_FILESandcheck_windows_files.py --authenticodeverify four paths in the finished zip:LongshotBuilder.exeLongshotBuilder/LongshotBuilder-Setup.exeLongshotBuilder/LongshotBuilder.exeLongshotBuilder/LongshotBuilder.dllThe package check receives public certificate/root values, never the key selector or PIN. It precedes artifact upload and release checksums. The setup is also copied unchanged as the small game-folder launch wrapper; the locally compiled game/renderer and separate launcher remain outside this signing scope.
AUTHENTICODE_ROOT_PEM, a repository variable, pins the issuing CA's public code-signing root, optionally followed by intermediates, for signer-chain verification through-CAfile. The configured leaf is still pinned separately fromAUTHENTICODE_CERTIFICATE_PEM. Unset or blank root: use and log the system bundle, which may lack code-signing-only roots. Set: use the supplied public certificates and log the first certificate's SHA-256 fingerprint. A non-empty malformed value fails instead of falling back. Timestamp-chain and HTTPS trust stay on the system bundle, not this variable.RFC 3161 requests use
https://timestamp.digicert.comfirst, thenhttp://timestamp.sectigo.comif the first request fails. Both are passed as ordered-tsoptions. If neither succeeds, signing fails; a returned token must still pass signature, chain and message-imprint verification. Missing or invalid timestamps fail before originals are replaced. The Sectigo chain has not yet been confirmed against the runner's bundle.A new secret-free Signing runner step runs
authenticode.py runnerunder the same guard as signing, after publishing and before the PIN-holding step. It checks that both configured PKCS#11 library paths are absolute existing files; failure stops subsequent signing.sign_builderchecks the paths again before reading the PIN. This is a presence check, not proof of runner identity or key isolation.Signing still shares the package job with build-time code. HumanBean added
dotnet publish --disable-build-serversand documented the limitation, not a separate signing job. The intended follow-up and remaining deployment checks are in Risk and rollback below.The pinned osslsigncode 2.15 download, matching-release guards, named secret forwarding, stdin PIN and redacted tool output remain. Without a signing certificate, the job logs
not signed: no certificate configured, downloads no signer and packages unsigned. Retired nightlies still skip signing configuration;checkednever skips signing of release files that are rebuilt.The stand-in and its export now also include
signed-library-standin.dlland check the four archive paths. That library fixture is the same tiny authored PE under a DLL name, not the real managed Builder assembly. The export keeps the unsigned fixture, signed setup/apphost/library fixtures, positive zip, public certificates and hashes, never keys. It still refuses non-empty output folders.Flags and settings
AUTHENTICODE_CERTIFICATE_PEMAUTHENTICODE_ROOT_PEM-CAfileAUTHENTICODE_KEY_URIAUTHENTICODE_TOKEN_PINAUTHENTICODE_PKCS11_PROVIDER,AUTHENTICODE_PKCS11_MODULEbuild-windows.ymlinputauthenticodeAUTHENTICODE_BUILD_REFauthenticode.py plan/runner/sign --setup ... --publish ... --cache ...check_windows_files.py --authenticode --signer-cache ...dotnet publish --disable-build-serversauthenticode_standin.py --osslsigncode ...authenticode_standin.py --out DIRSeparate trust choices:
AUTHENTICODE_ROOT_PEMaffects only the signer's chain in signing verification and the packaged-file check. Unset or whitespace-only uses the system bundle and logs that choice. A supplied public root is written temporarily, its first certificate fingerprint is logged, and it is removed after use. Private-key material, trailing text or malformed certificate data stops the operation; there is no silent fallback from a bad supplied root. The leaf pin remains unchanged.-TSA-CAfilealways uses the system bundle in the production callers.-HTTPS-CAfilealso stays on that bundle for HTTPS connections. The ordered timestamp endpoints are DigiCert over HTTPS, then Sectigo over HTTP; the HTTP response still needs a valid signed RFC 3161 token and matching imprint. Either total request failure or later verification failure stops publication. No live fallback success is claimed.Measurements
Branch state: on main
fbd420241afdba1d5ba2936971eb48549edb85ae; headb784ce0e108428560a3aeda515eec3af0b06e46a; whole game-free suite on this head:516 passed, 0 failed, 15 skipped(mainfbd420241afdba1d5ba2936971eb48549edb85ae:514 passed, 0 failed, 15 skipped). The rebase onto this main and the last commit of the branch are HumanBean's; the suite was run on his head unchanged, on Arch Linux. Main has gained 10 commits since; the branch still merges cleanly.Historical measurements, not results for
b784ce0eBaseline: baseline-2a2da67f2b
The comparison below predates HumanBean's changes.
baseline-2a2da67f2bis the earlier main;earlier-93e3bc3cis the earlier signing implementation. These numbers are retained history, not a measurement of the DLL-inclusive path, pinned signer root, two-server fallback or runner check now in this PR.Hardware: Linux x86-64, kernel 7.2.15-1-omarchy, Python 3.14.8; clang/LLD 23.1.3, openssl and osslsigncode 2.15
Scene: game-free source/packaging checks
Method: baseline / earlier implementation / baseline / earlier implementation; command wall time and the median of two samples. The stand-in used the earlier implementation's signer with each source's packaging/checksum code and temporary software keys/local TSA. No signing-speed or game-speed conclusion follows.
How to reproduce current source checks:
python -I -B tests/test_authenticode.py,python -I -B tests/test_authenticode_standin.pyandpython -I -B tests/test_windows_on_linux.py. Current results must nameb784ce0e; rerunning these now is not a reproduction of the old counts below.Cells retain run 1 / run 2 counts and wall time, then the median. Stand-in counts are scenarios.
The baseline lacked the optional workflow step, giving the expected missing-feature lookup error, not a Windows failure. The earlier signing file passed its then-14 cases twice. The old fixture's scope did not include the Builder DLL; its log lines and sizes are not reused as results for today's four-path package check. Historical export-only checks at
e2a27e94likewise predate the DLL addition and do not establish the current export's execution result.Testing
Tested: earlier results are retained only at their tested commits. The source/export update at
e2a27e94passed 14 signing cases, 6 workflow cases and 8 pure-Python export cases; the export comparison ran before/after/before/after against the earlier stand-in. The real software-key/loopback-TSA measurements were at93e3bc3c. Those checks predate HumanBean's DLL, trust-root, timestamp-list and runner-check changes and are not a pass forb784ce0e. Use the branch-state block for the separately recorded current-head suite result.HumanBean said he parsed the edited Python files but ran no tests, signer, build or workflow for his commit. The updated tests now describe the three signed inputs, four archive paths, distinct signer/TSA trust, timestamp order, runner guard and library fixture. Reading those tests is not an execution result. No signed release or new export bundle is claimed here.
Reported separately, not a test of this branch: d4nkn3ss's Windows 11 follow-up used main
316f9319and PowerShell signing. Its unsigned and self-signed setup copies scanned clean on demand and returned S_OK throughIAttachmentExecute::Save; Windows reported the self-signed chain as untrusted. His earlier download history does not establish why verdicts changed or predict a new release's verdict. A self-signed fixture remains mechanics evidence, not public publisher trust or a detection-rate improvement.Not tested: real PKCS#11 provider/key access, live Forgejo runner selection and the canary, live DigiCert/Sectigo requests, the Sectigo timestamp chain against the runner's bundle, Windows acceptance of this branch's setup/apphost/managed DLL, or browser/Defender/SmartScreen outcomes for its release files. The library stand-in is an authored PE with a DLL filename, not a working managed Builder library. No game-speed claim is made.
Needs other testers:
Screenshots
Nothing on screen. Windows prompts have not been observed.
Risk and rollback
Open before signing is enabled:
AUTHENTICODE_ROOT_PEM. The review could not confirm whether the token supplies the USERTrust cross-signature needed for Sectigo Public Time Stamping Root R46. The fallback may therefore fail every verification even when it answers. Confirm it with a canary in which DigiCert is unavailable, or review the timestamp trust configuration before relying on it. It still fails closed; this description update does not fix or confirm the chain.--disable-build-serversis a mitigation, not key isolation. The documented follow-up is a build job that uploads unsigned setup/publish files and their SHA-256 values, a separate guarded signing job onlinux-signingthat checks those hashes, signs, verifies and uploads, then a packaging job that uses only the signed files. This is not implemented. The two extra artifact transfers and Forgejo's skipped-job handling inneedsrequire a real workflow test.The Signing runner step has no secrets and must run under the same guard immediately before signing. A harmless canary must confirm the job is actually on
linux-signingand printssigning runner: the PKCS#11 provider and module are present. Missing libraries must producenot on the signing runnerand stop before PIN use. Keep real credentials disabled until the routing and authorization checks are settled.Any configured signing, root, timestamp, leaf or packaged-file verification failure stops publication. Reverting the signing commits removes the optional path. Removing the signing-certificate secret returns to explicitly unsigned output, which must not be done silently after promising signed releases. Linux verification is not Windows trust or SmartScreen reputation.
Reviewer notes
Commit
b784ce0e108428560a3aeda515eec3af0b06e46ais HumanBean's own commit on top of the four commits he rebased ontofbd420241a. His re-review says the code can merge with signing off; it does not certify deployment or live-provider behaviour. This update changes only the description.Read
trust_anchor,signing_runner,PUBLISH_FILES,ARCHIVE_FILES,sign_filesand the two production callers that keep timestamp/HTTPS trust on the system bundle. The Signing runner step, the DLL-inclusive stand-in/export and the same-job follow-up are now described above. The two remaining should-change points are explicitly open in Risk, not presented as fixed.Maintainers still choose the legal publisher, service/token, root certificate, custody and approval policy, and eventual signing-required channels. A proprietary service API is not automatically supported by the PKCS#11 route. The separate launcher and setup metadata remain parts of issue 46. Do not import fixture roots or disable Defender; T-RSMT remains the live deployment/consumer testing scope.
Issues
See #46
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.tests/that reads changed source updated in this PR, not weakened.New on this forge; not opened on GitHub.
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra
Adapted to #99's Linux packaging: pinned osslsigncode now signs through a PKCS#11 token/service interface, and the Windows-file checker also verifies the completed zip before upload and checksums. The real command-path stand-in passes with temporary software keys and a loopback RFC 3161 authority, 14 signing unit tests pass twice, and T-RSMT now needs Linux provider/key-access checks plus Windows trust checks; neither the stand-in nor Linux verification proves public publisher trust. The test request for the reworked flow is open (T-RSMT; it replaces the earlier one). Rebased onto main
b9cdc4326; whole suite on the new headeb510a3cb: 512 passed, 0 failed, 19 skipped. Ready for review.57698f8b0feb510a3cbaWIP: ci(windows): optionally sign builder executables with Authenticodeto ci(windows): optionally sign builder executables with AuthenticodeAn offer of Windows-side checks for this, whenever they're useful. I have a Windows 11 PC with Defender fully on (real-time and cloud protection, tamper protection) and its detection history for Hookshot's releases: #46 (comment).
What I can run here and post back, each against an unsigned build of the same commit:
Get-AuthenticodeSignature/signtool verify /pa /von the setup, the apphost and the Builder exe, the timestamp, and whether the chain is accepted. That checks the osslsigncode path's output against Windows rather than against itself.No decisions needed from anyone for any of this, and nothing gets signed here. Just point me at the artifacts when there's a build to try.
Re-fitted signing onto main's current command-line Builder and release flow: checked-release inputs, quick jobs, artifact retries and the retired-nightly skip are preserved, while every package that proceeds is signed and verified before upload. A separate commit adds
--out DIRfor tiny public signature fixtures with no keys; 14 signing, 6 workflow and 8 export checks pass; no real fixture was rebuilt for this update. Headc2e7d2287on maindd02c6cca; the whole game-free suite on it: 507 passed, 2 failed, 15 skipped.eb510a3cbac2e7d22877Thanks: your download-time checks and the untrusted self-signed result reinforce the distinction here. The branch now has an export for your first check: Windows' own reading of the signature and timestamp, not a Defender/reputation comparison.
From this PR's checkout, on a PC with Python 3.12+, OpenSSL 3, clang, lld-link and osslsigncode 2.15 for that OS (Linux/WSL is one route):
The new/empty output folder gets:
unsigned-standin.exe,signed-setup-standin.exe,signed-apphost-standin.exe;standin-package.zip, with the three release-style EXE paths (two are copies of the setup);signer.pemandtimestamp.pem, plusSHA256SUMS, never a private key.These are tiny authored fixtures, not working Builder binaries. Please record the hashes,
Get-AuthenticodeSignature/signtool verify /pa /vresults and timestamp details; untrusted roots are expected. Do not import these certificates as trusted roots or disable Defender.No CI build is needed to produce those fixtures yourself. Your browser-download and next-release checks need real release files; those builds remain the maintainers' to start, and these fixtures make no promise about their verdicts.
Reviewed with security in mind, against main
fbd42024. Main's release workflows (release.yml,build-windows.yml),builder_package.pyandcheck_windows_files.pyhaven't changed since this branch's basedd02c6cc, so it is current and still fits the Linux-only, command-line Builder release flow (no MSI, Python Builder or Windows runner comes back). I only read the code: I didn't run the checks or the signer.What's good
release.yml(push to main or av*tag) passes the three secrets, by name and not withsecrets: inherit.build-windows.ymlhas nopull_requesttrigger, and the signing step checks push, this repository,inputs.ref == github.shaand main or a tag, withcertificate_forchecking the same again in Python. The PIN goes in on stdin (-readpass -, which osslsigncode 2.15 supports) and is removed from the child's environment. The child's output is never printed, so provider errors can't leak anything. The PKCS#11 URI is restricted to selector fields, andpin-*, query strings and file keys are refused. The public chain goes into a 0600 temporary file that is deleted afterwards.tools.fetch, and its size and SHA-256 match GitHub's asset digest for the 2.15 release (5d2890c1…, 2335349 bytes). It is unpacked fresh from that checked archive on every run.-ts). If the timestamp server is down, osslsigncode fails and the job stops, so nothing ships unsigned by mistake. Each file is verified with the leaf pinned (-require-leaf-hash), and the check requires the lineTimestamp Server Signature verification: okinstead of trusting exit 0. The originals are only replaced once both files have verified.dotnet publishand beforebuilder_package.py package/zip. The finished zip is checked for all three EXE paths before upload, andrelease.ymlcomputes SHA256SUMS and its signature from that zip. So the published hashes cover the signed bytes.make_game.pycopies the signed setup into the game folder unchanged.not signed: no certificate configured, downloads nothing and goes on as before. A malformed certificate fails the version job.Must change
Should change (before anyone turns signing on)
verify_filechecks the signer chain against/etc/ssl/certs/ca-certificates.crt. On Ubuntu/Debian that bundle holds only the Mozilla roots trusted for TLS, and Mozilla carries no code-signing-only roots. A certificate that chains to a code-signing root such as Sectigo's Public Code Signing Root R46, GlobalSign's code-signing R45, SSL.com's code-signing roots or Microsoft's Identity Verification Root (Artifact Signing) will most likely fail verification, safely but with only the generic "failed" message. DigiCert (Trusted Root G4) would pass. Better to let maintainers pin the root, for example anAUTHENTICODE_ROOT_PEMvariable or the last certificate of the chain, and use it as-CAfile. Keep the system bundle for-HTTPS-CAfileand the timestamp chain.dotnet publish(MSBuild targets from NuGet packages restored withNuGetAudit=false, and build-server processes that stay running),apt-get installand Wine. A small job that only downloads the unsigned files, signs and verifies them, then packages, would keep build-time code away from the key. That job would be the only one needinglinux-signing, instead of the whole package build with Wine and dav1d.LongshotBuilder/LongshotBuilder.dllis not signed. It is the Builder's own code (PublishSingleFileis false, so the apphost only loads it) and is scanned as much as the EXE. osslsigncode can sign it in the samesign_filescall, andARCHIVE_FILEScan check it.-tsoptions and tries them in turn. A second RFC 3161 server (Sectigo's, say) would stop a DigiCert outage from blocking a release, and it would still fail closed if all of them are down.What I couldn't check
inputs.*in a called workflow'sruns-on. If it doesn't, the expression falls back toubuntu-24.04, so unsigned releases are unaffected. A signed run, though, would put the PIN on an ordinary runner, where it would fail for lack of the PKCS#11 module. Check this with the canary that docs/releases.md already asks for.@ -59,1 +73,3 @@runs-on: ubuntu-24.04# Both paths build on Linux. Runner admission/provider authorization must deny PR access to the signing identity;# a label alone is not an access control (docs/releases.md, Windows Authenticode).runs-on: ${{ inputs.authenticode && github.event_name == 'push' && github.repository == 'MagicBeanDev/Longshot' && inputs.ref == github.sha && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'linux-signing' || 'ubuntu-24.04' }}Worth confirming on the canary: does Forgejo evaluate
inputs.*in a called workflow'sruns-on? If not, this falls back toubuntu-24.04. Unsigned releases are fine either way, but a signed run would then hand the PIN to an ordinary runner (where it would fail for lack of the PKCS#11 module).@ -128,1 +144,4 @@# Sign the two project executables before either copy of the setup is packaged, and before release.yml hashes the zip.# No signing secret is passed to a PR step, including same-repository PRs and pull_request_target.- name: Authenticode (optional)The token PIN and key access arrive in the same job that has already run
dotnet publish(MSBuild targets from NuGet packages restored withNuGetAudit=false, plus build-server processes that stay running) andapt-get install, and that will run Wine afterwards. A small separate signing job (download the unsigned setup and publish folder, sign and verify, then package) would keep build-time code away from the key. It would also mean only that job needslinux-signing, not the whole package build.@ -0,0 +28,4 @@REPOSITORY = "MagicBeanDev/Longshot"CERTIFICATE = "AUTHENTICODE_CERTIFICATE_PEM"TIMESTAMP = "https://timestamp.digicert.com"CA_FILE = "/etc/ssl/certs/ca-certificates.crt"This bundle is Debian/Ubuntu's TLS (serverAuth) roots from Mozilla, which carries no code-signing-only roots. A certificate chaining to, for example, Sectigo Public Code Signing Root R46, GlobalSign Code Signing Root R45, SSL.com's code-signing roots or Microsoft Identity Verification Root (Artifact Signing) will most likely fail
verifyhere (safely, but with only the generic failure message). DigiCert's Trusted Root G4 would pass. Suggest a maintainer-pinned signer root (a variable, or the chain's last certificate) for-CAfile, keeping the system bundle for-HTTPS-CAfileand the timestamp chain.@ -0,0 +140,4 @@for file in files:tmp = Path(stack.enter_context(tempfile.TemporaryDirectory(prefix=".authenticode-", dir=file.parent)))signed = tmp / "signed.exe"run_tool([tool, "sign", "-certs", certificate, *key_args, "-h", "sha256", "-ts", timestamp,osslsigncode 2.15 accepts several
-tsoptions and tries each in turn. A second RFC 3161 server (Sectigo's, say) would keep a DigiCert outage from blocking a release, and it still fails closed when every server is down.@ -0,0 +160,4 @@cert.write_text(pem, encoding="ascii")cert.chmod(0o600)with pinned_tool(cache) as tool:sign_files([setup, Path(publish) / "LongshotBuilder.exe"], tool, cert, args, password=pin + "\n")LongshotBuilder/LongshotBuilder.dll(PublishSingleFileis false) is the Builder's own code: the apphost only loads it. It stays unsigned, and Defender scans it as much as the EXE. Could it be signed in this same call and added toARCHIVE_FILES?c2e7d22877b784ce0e10New commits pushed, approval review dismissed automatically according to repository settings
I rebased this onto main @
fbd42024(no conflicts) and addressed my review's five comments in one commit on top of yours. It's pushed asb784ce0e, and your four commits are unchanged apart from the rebase. No tests, builds or workflows were run here; only the edited Python files were parsed.My commit, "pin the signer's root, add a second timestamp server, sign LongshotBuilder.dll":
AUTHENTICODE_ROOT_PEM, holds the issuing CA's public root, optionally followed by intermediates. It is used as-CAfilefor the signer chain in both the signing step and the packaged check (check_windows_files.pythroughauth.trust_anchor).http://timestamp.sectigo.com), each passed as its own-ts. It still fails closed when both are down, and verification still requires a verified timestamp.LongshotBuilder/LongshotBuilder.dllis signed in the same call as the setup and the apphost.ARCHIVE_FILESnow has four paths, so the zip check covers it.tests/test_authenticode.py,tests/authenticode_standin.pyandtests/test_authenticode_standin.pyare updated to match.release.ymltakes for every release. It needs two more artifact transfers of the publish folder, and Forgejo's handling of a skipped job inneedsis something only a real run can confirm. So RELEASING.md now has a "known limitation and follow-up" paragraph, before signing is turned on for players, describing the target design: build, then a hash-checked signing job, then packaging from the signed files. There's a comment at the step too.dotnet publishnow passes--disable-build-servers, so no MSBuild or compiler server outlives the build.inputs.*inruns-on. A new "Signing runner" step, holding no secrets, runsauthenticode.py runnerbefore signing. It fails with "not on the signing runner … the token PIN is not used" unless the PKCS#11 provider and module exist.sign_builderchecks again before reading the PIN. The docs describe the canary to look for onlinux-signing, and say the token must still be bound to the signing machine, since this check stops the PIN being used elsewhere, not sent there.Please update the description with:
AUTHENTICODE_ROOT_PEM;Also re-check the testing boxes:
run_allhasn't run since these changes. The live checks (T-RSMT) are still open.Re-review at
b784ce0e, on main@originfbd42024(current, so "Rebased on current main" holds). I read the four commits and the fix commit with security in mind. I ran nothing: no checks, no signer, no build. For osslsigncode I read the 2.15 source (osslsigncode.c).Earlier points (the approving review, id 1140, at
c2e7d228)Trust anchor: resolved.
AUTHENTICODE_ROOT_PEMis avars.value, never placed beside a secret on its own. The signing step and the packaged check both use it as-CAfile.-TSA-CAfileand-HTTPS-CAfilestay on the system bundle (tsa_ca_file=CA_FILEinsign_builderand incheck_windows_files.authenticode).fingerprint(), so it must hold certificates only: anything malformed, a private key or trailing text raisesSigningErrorbefore any file is written. Both callers turn that into a failure; there's no fallback.TemporaryDirectorythat is removed on every exit path, including an exception at theyield. The file itself takes the umask, which is fine for a public root.Separate signing job: resolved as a documented limitation. It's documented in RELEASING.md, docs/releases.md and a comment at the step, and
--disable-build-serversis added.--disable-build-serversexists since .NET SDK 7.0.100, the workflow pins8.0.x, andtests/test_portable_builder_startup.pyalready passes it todotnet publish. It's harmless for unsigned builds.LongshotBuilder.dll: resolved. It is listed the same way everywhere:PUBLISH_FILES, signed in the samesign_filescall;ARCHIVE_FILES, four paths, whichbuilder_package.packageproduces throughcopytreeof the publish folder;Signing still comes before
Package folder/The zip, andrelease.ymlstill hashes the finished zip, so SHA256SUMS covers the signed bytes.Second timestamp server: resolved.
-tsintsurl[], andadd_timestamp_rfc3161returns at the first server that succeeds. It fails ("RFC 3161 timestamping failed") only when all of them fail.-HTTPS-CAfileapplies only tohttpsURLs. A plain-HTTP RFC 3161 token is acceptable: it is signed, and signing doesn't check it, butverifydoes.verify_timestampchecks the token against-TSA-CAfileand the message imprint.verify_signatureprintsTimestamp Server Signature verification: ok|failed, and for a file with no timestamp it printsTimestamp is not available, whichverify_filestill rejects.inputs.*inruns-on: resolved as far as a review can go. The "Signing runner" step has the sameif:as the signing step (the test compares them), and it has nosecrets.. A failing step skips the later steps through the implicitsuccess(), and it can't be skipped while signing runs.sign_buildercallskey_arguments(the provider and module files) before the PIN is touched, and the test covers that. The docs say the secrets still reach whichever runner takes the job.The tests, read but not run. They match the code as far as I can tell:
sign_files,verify_file,verify_archiveandsign_builder;-tsorder fromTIMESTAMPS;[2:6]of the verify arguments;.dllas the fourth suffix;dotnet publish builder/first appears at the publish step, and the "Signing runner" section has nosecrets.;ARCHIVE_FILES, with byte 256 still0xc3in the DLL.Must change
The description and checklist describe
c2e7d228, not this head. Out of date:AUTHENTICODE_ROOT_PEMrow in the flags table;"
run_all.pyrun and passing" is ticked, but nothing has run since the fix commit changedauthenticode.py,check_windows_files.pyand three test files. Runpython -I -B tests/run_all.py(at leasttest_authenticode.py,test_authenticode_standin.pyandtest_windows_on_linux.py) atb784ce0e, then update the description and boxes as asked in the maintainer's comment. The code itself needs no change before merging with signing off.Should change (before anyone turns signing on)
What I couldn't check
runs-onevaluation, a real provider, the live DigiCert and Sectigo servers, and Windows trust. These are T-RSMT and the canary.osslsigncode.cI read is byte-for-byte the 2.15 tag. It matches the 2.15 raw file's size within a few bytes.@ -102,0 +207,4 @@Mozilla's TLS roots only, so a certificate chaining to a code-signing-only root fails verification there (safely);DigiCert Trusted Root G4 is one that would pass. A set but malformed value stops the job rather than falling back.The timestamp chains and the timestamp servers' HTTPS are always checked against the system bundle (DigiCert's andSectigo's timestamp roots are in it). Take the root from the CA's own repository, compare its fingerprint with the"DigiCert's and Sectigo's timestamp roots are in it": true for DigiCert (Trusted Root G4). For Sectigo it holds only if the token carries the USERTrust cross-signature of Public Time Stamping Root R46 (see the comment on
TIMESTAMPS). Please confirm it, or soften the claim until a canary has exercised the fallback.@ -0,0 +36,4 @@CERTIFICATE = "AUTHENTICODE_CERTIFICATE_PEM"ROOT_CERTIFICATE = "AUTHENTICODE_ROOT_PEM"# RFC 3161 servers, tried in turn (several -ts): one server's outage does not stop a release, all of them down does.TIMESTAMPS = ("https://timestamp.digicert.com", "http://timestamp.sectigo.com")The multi-
-tsfallback is right: osslsigncode 2.15'sadd_timestamp_rfc3161stops at the first server that succeeds. A plain-HTTP RFC 3161 server is acceptable too, sinceverifychecks the token's chain and imprint.One thing to confirm before relying on it: the token's chain must reach a root in
/etc/ssl/certs/ca-certificates.crt, because that's the-TSA-CAfilehere. Sectigo's current TSA chain ends at Sectigo Public Time Stamping Root R46. That root isn't a Mozilla TLS root, so the chain only verifies if the token carries R46 cross-signed by USERTrust RSA. I couldn't check what the token carries. If it doesn't, a release timestamped by Sectigo failsverify. That is safe, but the fallback would never actually work.Worth one canary run with DigiCert blocked, or else add the timestamp roots to the trust bundle explicitly.
@ -0,0 +106,4 @@"""True when this runner has the PKCS#11 libraries. Run in a step that holds no secret, before the one that does."""for name in PKCS11_PATHS:path = Path(env.get(name, ""))if not env.get(name) or not path.is_absolute() or not path.is_file():Nit: this only checks that the files exist. A provider or module path that also exists on a stock Ubuntu runner, for example a distro package's
pkcs11-providerorp11-kitpath, would pass this check off the signing runner, and then the PIN goes to the module. Worth a sentence in docs/releases.md step 2: install the libraries at a path only the signing runner has (e.g. under/opt/...).Thanks for the rebase and your own commit
b784ce0e. The description now describes that head, including the DLL, pinned signer root, ordered timestamp fallback, runner canary and same-job limitation.Source suite at
b784ce0e: 516 passed, 0 failed, 15 skipped (mainfbd42024: 514 passed, 0 failed, 15 skipped).The Sectigo fallback chain is still unconfirmed against the runner's bundle, and the runner check only tests file presence. Both remain open before signing is enabled and are named in Risk; neither is fixed by this description update.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.