Skip to content

[Task] Migrate Windows code signing from DigiCert KeyLocker to Azure Trusted Signing #2034

@samuv

Description

@samuv

Description

Migrate the Windows code signing pipeline from DigiCert KeyLocker to Azure Trusted Signing, authenticated via GitHub OIDC workload identity federation, and decommission the DigiCert path entirely.

Today Windows artifacts are signed by shelling out to DigiCert's smctl.exe from a custom @electron/windows-sign hook, which requires storing a client certificate (.p12), its password, an API key, a host URL, and a SHA-1 fingerprint as long-lived repository secrets. The same flow is duplicated in both pr-build-test.yml (opt-in via /build-test --sign-windows) and on-release.yml (stable + prerelease tags).

Azure Trusted Signing replaces all of that with OIDC (no client secret stored anywhere), uses the standard signtool.exe + Azure Code Signing DLib, and is the path Electron Forge now documents as canonical for Windows signing. The migration also lets us scope signing credentials to a dedicated artifact-signing GitHub environment so unsigned PR builds and non-Windows matrix rows can never request them.

Technical Details

Target architecture

  • Signing backend: Azure Trusted Signing account + Certificate Profile, with a federated credential trusting repo:stacklok/toolhive-studio:environment:artifact-signing.
  • Auth: azure/login@v2 with id-token: write; no client secret.
  • Signer: signtool.exe (from the Windows SDK on the runner) + Azure.CodeSigning.Dlib (installed via nuget) + a generated metadata.json — wired through @electron/windows-sign's signWithParams option, following the official Electron Forge Trusted Signing guide.
  • GitHub environment: artifact-signing, holding six AZURE_ARTIFACT_SIGNING_* secrets (tenant id, client id, subscription id, endpoint, account name, certificate profile name). Activated only for the Windows matrix row when signing is requested.
  • Reusable composite action: .github/actions/setup-azure-trusted-signing encapsulates azure/login, nuget install, locating the latest x64 signtool.exe, writing metadata.json, and exporting AZURE_CODE_SIGNING_DLIB / AZURE_METADATA_JSON / SIGNTOOL_PATH to GITHUB_ENV.
  • Forge integration: a single getWindowsSignConfig() helper in forge.config.ts returns the Azure config when env vars are present; both packagerConfig.windowsSign and maker-squirrel's windowsSign consume it.

Rollout plan (staged to keep releases shipping)

The migration is broken into three PRs so we can burn in the new flow progressively without risking a broken stable release:

  1. PR builds (opt-in): migrate pr-build-test.yml / /build-test --sign-windows to Azure Trusted Signing. Keep DigiCert as a runtime fallback in forge.config.ts so on-release.yml is unaffected. Add utils/windows-sign-azure.ts, the composite action, and the artifact-signing environment.
  2. Prerelease tags: extend Azure signing to *-alpha, *-beta, *-rc tags in on-release.yml via a conditional step. Stable releases still sign via DigiCert until prereleases are proven.
  3. Stable releases + cleanup: flip stable Windows releases to Azure, then remove the entire DigiCert path — .github/actions/setup-windows-codesign, utils/digicert-hook.js, the DigiCert fallback in forge.config.ts, the eslint.config.mjs ignore entry, the DigiCert section in docs/README.md, and the SM_* repository/environment secrets.

AWS OIDC compatibility

The build-and-release job also assumes an AWS IAM role via OIDC (for S3 sync / CloudFront invalidation). Entering the artifact-signing environment changes the token subject from repo:.../ref:refs/tags/vX.Y.Z to repo:.../environment:artifact-signing. The existing AWS trust policy uses the wildcard repo:stacklok/toolhive-studio:*, so both subjects are accepted and no AWS-side changes are required.

Relevant files

  • .github/workflows/pr-build-test.yml
  • .github/workflows/on-release.yml
  • .github/actions/setup-azure-trusted-signing/action.yml (new)
  • .github/actions/setup-windows-codesign/action.yml (to be removed at the end)
  • utils/windows-sign-azure.ts (new)
  • utils/digicert-hook.js (to be removed at the end)
  • forge.config.ts (getWindowsSignConfig())
  • eslint.config.mjs
  • docs/README.md

Acceptance Criteria

  • A dedicated artifact-signing GitHub environment holds the six AZURE_ARTIFACT_SIGNING_* secrets and has a federated credential trusting the repo's environment subject
  • .github/actions/setup-azure-trusted-signing composite action is in place and used by both pr-build-test.yml and on-release.yml
  • utils/windows-sign-azure.ts produces a valid @electron/windows-sign config and is consumed by forge.config.ts for both packagerConfig.windowsSign and maker-squirrel's windowsSign
  • /build-test --sign-windows produces Azure-signed artifacts on PRs
  • Prerelease tags (*-alpha, *-beta, *-rc) are signed via Azure Trusted Signing from on-release.yml
  • Stable release tags are signed via Azure Trusted Signing from on-release.yml
  • Signed artifacts pass signtool verify /pa /v, install cleanly on a fresh Windows VM without SmartScreen blocking after reputation builds, and the Squirrel auto-updater handshake succeeds on an existing installed build
  • AWS S3 sync + CloudFront invalidation on the Windows release runner continue to work after switching the OIDC token subject to environment:artifact-signing
  • .github/actions/setup-windows-codesign/, utils/digicert-hook.js, the DigiCert fallback branch in forge.config.ts, and the utils/digicert-hook.js entry in eslint.config.mjs are removed
  • docs/README.md documents Azure Trusted Signing as the sole Windows signing path
  • The SM_HOST, SM_API_KEY, SM_CLIENT_CERT_FILE_B64, SM_CLIENT_CERT_PASSWORD, and SM_CODE_SIGNING_CERT_SHA1_HASH secrets are removed from the repo and any environments
  • Lint, type-check, and unit tests pass

Out of Scope

  • macOS / Flatpak signing (unchanged — Apple certificates and Flatpak remain as-is)
  • Reworking the AWS OIDC trust policy (the existing wildcard already covers both tag and environment subjects)
  • Changes to the auto-updater feed itself — only the signing identity on the artifacts changes
  • Cancelling the DigiCert KeyLocker subscription is tracked under this task but handled outside the repo

Priority

High — removes long-lived client certs + API keys from repository secrets, aligns with the Electron Forge recommended path, and unblocks decommissioning DigiCert KeyLocker.

Task Type

Chore / Infrastructure

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions