Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 6 additions & 46 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,16 @@ name: Release (commercial)
# enforces "no unsigned binary is ever published to customers".
#
# Required repository secrets:
# Windows (one of three modes, auto-detected by the preflight job):
# a) Azure Artifact Signing (preferred — no certificate to buy, no token):
# AZURE_SIGNING_ENDPOINT region URI, e.g. https://eus.codesigning.azure.net
# AZURE_SIGNING_ACCOUNT Artifact Signing account name
# AZURE_SIGNING_PROFILE certificate profile name
# AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET
# b) SSL.com eSigner (cloud HSM, for a traditional CA certificate):
# Windows (one of two modes, auto-detected by the preflight job):
# a) SSL.com eSigner with an OV code signing certificate (the production
# route — cloud HSM, no USB token, works unattended in CI):
# ESIGNER_USERNAME
# ESIGNER_PASSWORD
# ESIGNER_CREDENTIAL_ID
# ESIGNER_TOTP_SECRET
# plus the ESIGNER_TOOL_URL variable so CodeSignTool can be installed
# c) PFX file:
# b) PFX file (only for a certificate already held — no CA issues these
# for code signing any more):
# CSC_LINK base64 .pfx
# CSC_KEY_PASSWORD pfx password
# macOS:
Expand Down Expand Up @@ -96,18 +93,15 @@ jobs:
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
ESIGNER_USERNAME: ${{ secrets.ESIGNER_USERNAME }}
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
run: |
# Precedence matches the signer's own auto-detect, so the mode chosen
# here and the mode the signer would pick can never disagree.
if [ -n "$ESIGNER_USERNAME" ]; then
echo "windows_mode=ssl_esigner" >> $GITHUB_OUTPUT
elif [ -n "$AZURE_SIGNING_ACCOUNT" ]; then
echo "windows_mode=azure" >> $GITHUB_OUTPUT
elif [ -n "$CSC_LINK" ]; then
echo "windows_mode=pfx" >> $GITHUB_OUTPUT
else
echo "::error::No Windows code-signing credentials present. Set AZURE_SIGNING_*, ESIGNER_* or CSC_LINK secrets before tagging a release."
echo "::error::No Windows code-signing credentials present. Set the ESIGNER_* secrets (or CSC_LINK) before tagging a release."
exit 1
fi

Expand Down Expand Up @@ -145,15 +139,6 @@ jobs:
# CodeSignTool is not present on the runner image; the step below installs
# it here. Overridable for self-hosted runners that pre-provision it.
ESIGNER_TOOL_PATH: ${{ vars.ESIGNER_TOOL_PATH || 'D:\CodeSignTool\CodeSignTool.bat' }}
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
AZURE_SIGNING_PROFILE: ${{ secrets.AZURE_SIGNING_PROFILE }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
# The client tools installer puts the dlib here. Overridable for
# self-hosted runners that provision it elsewhere.
AZURE_SIGNING_DLIB: ${{ vars.AZURE_SIGNING_DLIB || 'C:\Program Files\Microsoft\Azure Artifact Signing Client Tools\bin\x64\Azure.CodeSigning.Dlib.dll' }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
steps:
Expand Down Expand Up @@ -193,27 +178,6 @@ jobs:
env:
ESIGNER_TOOL_URL: ${{ vars.ESIGNER_TOOL_URL }}

# The dlib, the .NET 8 runtime and a new-enough signtool all have to be
# present before signtool can talk to Azure, and none of them are on the
# runner image. The client tools MSI installs all three together.
- name: Install Azure Artifact Signing client tools
if: needs.preflight.outputs.windows_mode == 'azure'
shell: pwsh
run: |
if (Test-Path $env:AZURE_SIGNING_DLIB) {
Write-Host "Artifact Signing dlib already present at $env:AZURE_SIGNING_DLIB"
exit 0
}
winget install -e --id Microsoft.Azure.ArtifactSigningClientTools `
--accept-source-agreements --accept-package-agreements --disable-interactivity
if (-not (Test-Path $env:AZURE_SIGNING_DLIB)) {
Write-Host "::error::Client tools installed but $env:AZURE_SIGNING_DLIB does not exist."
Write-Host "::error::Set the AZURE_SIGNING_DLIB repository variable to the actual path of x64\Azure.CodeSigning.Dlib.dll."
Get-ChildItem -Recurse -Filter 'Azure.CodeSigning.Dlib.dll' 'C:\Program Files' -ErrorAction SilentlyContinue |
ForEach-Object { Write-Host $_.FullName }
exit 1
}

- name: Install npm dependencies
run: npm ci

Expand Down Expand Up @@ -308,10 +272,6 @@ jobs:
ESIGNER_CREDENTIAL_ID: ${{ secrets.ESIGNER_CREDENTIAL_ID }}
ESIGNER_TOTP_SECRET: ${{ secrets.ESIGNER_TOTP_SECRET }}
ESIGNER_TOOL_PATH: '/usr/local/bin/codesigntool'
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
AZURE_SIGNING_PROFILE: ${{ secrets.AZURE_SIGNING_PROFILE }}
AZURE_SIGNING_DLIB: '/usr/local/lib/Azure.CodeSigning.Dlib.dll'
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
Expand Down
69 changes: 46 additions & 23 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,49 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added — Azure Artifact Signing

- **`TRANSTRACK_SIGN_MODE=azure`.** Microsoft's cloud signing service, roughly
$10/month, with no certificate to buy, no hardware token, and no annual
re-issue. `signtool` loads `Azure.CodeSigning.Dlib.dll`, which authenticates
to Azure and has the signature produced server-side, so no private key is
ever on the build machine. The release workflow detects the mode from the
secrets present and installs the client tools on the runner.
- Two behaviours worth knowing rather than discovering. Artifact Signing
certificates are valid for **three days**, so the signer always timestamps
against Microsoft's authority — an untimestamped installer verifies for three
days and then starts failing on customer machines with nothing about the file
having changed. And `DefaultAzureCredential`'s chain includes a browser
prompt that would hang a headless build, so the signer narrows the chain to
the service principal when one is present, and otherwise excludes only the
browser so federated identity still works.
- The two common Azure failures both surface from `signtool` as a generic
`SignerSign()` error. A 403 now says it is probably a region mismatch or a
missing signer role; a dlib load failure now says it is probably a missing
.NET 8 runtime or an x64/x86 mismatch.
### Fixed — SSL.com eSigner signing (`ssl_esigner` mode)

The eSigner path had never been exercised against a real certificate. Audited
against SSL.com's CodeSignTool command guide, it would have failed on the first
attempt for three independent reasons, each of which presented as something
other than what it was.

- **The TOTP secret was being consumed as if it were a code generator seed the
signer owned.** `-totp_secret` takes the secret from the SSL.com dashboard;
CodeSignTool derives the six-digit code itself. The signer was deriving a code
and passing *that*, so CodeSignTool tried to derive an OTP from six digits.
The failure would have read `Error: invalid otp` and looked like a broken 2FA
enrolment. The now-unused RFC 6238 helpers have been removed rather than left
as an attractive nuisance.
- **`-overwrite` is not a CodeSignTool option**, so every invocation would have
been rejected for an unknown argument. The signer now writes to a temporary
output directory and moves the result over the original, which also avoids
the interactive overwrite confirmation that would hang a CI build outright.
- **Arguments were passed as `-name value`.** SSL.com documents `-name=value`
throughout; the space-separated form is not documented to work.

Two further problems in the same path:

- **A password containing shell metacharacters was silently corrupted.**
CodeSignTool is a batch file, so its arguments are re-parsed by `cmd.exe` —
and SSL.com's own documentation uses `P!@^^ssword12` as its example password.
Values are now quoted, and the two characters that cannot survive quoting
(`"` and `%`) are refused up front with an explanation, rather than sending a
different password than the one configured and surfacing as an authentication
failure. The PFX path no longer uses a shell at all, so Node quotes for it.
- **A zero exit status was treated as proof of signing.** CodeSignTool has been
observed to print a failure and exit 0. Every mode now re-reads the artifact
afterwards and fails the build unless it actually carries an embedded
signature.

### Removed — Azure Artifact Signing

- **`TRANSTRACK_SIGN_MODE=azure` and its workflow, gate and documentation
support have been removed.** Artifact Signing requires an organisation
verifiable for three years or more, which TransTrack Medical Software does not
meet, so the mode could not have been used. Keeping it would have left the
validation package describing a signing path that cannot be demonstrated on
request. Documented in `docs/CODE_SIGNING.md` as worth revisiting at renewal.

### Fixed — release signing

Expand Down Expand Up @@ -71,9 +94,9 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **EV is no longer recommended by default.** The guidance to buy EV rested on
it granting immediate SmartScreen reputation; Microsoft removed that
behaviour, and OV now gives the same first-download experience. The docs now
recommend Azure Artifact Signing (~$10/month) or an OV certificate with cloud
HSM signing, and note that since June 2023 all code signing keys — OV
included — must live in hardware, so a copyable `.pfx` is no longer issuable.
recommend an OV certificate with cloud HSM signing, and note that since June
2023 all code signing keys — OV included — must live in hardware, so a
copyable `.pfx` is no longer issuable.

### Fixed

Expand Down
Loading
Loading