feat(release): Authenticode-sign releases with Azure Artifact Signing over OIDC #96

Merged
KofTwentyTwo merged 2 commits from feat/GH-57-release-signing-3 into main 2026-10-10 19:39:27 +00:00
KofTwentyTwo commented 2026-10-10 19:31:40 +00:00 (Migrated from github.com)

What

Releases are now Authenticode-signed (K22-REL-05) through the standards' shared workflow, which already carries the signing steps behind sign (azure/login v3.1.0 and azure/artifact-signing-action v2.0.0, both SHA-pinned and current; vpk pack --azureTrustedSignFile). ADR 0008.

  • release.yml: sign: true; new verify-signatures job after release downloads the published Setup.exe, portable zip and CLI zip and runs signtool verify /pa /all + Get-AuthenticodeSignature on every gclo executable, failing on an invalid, untimestamped or foreign-publisher signature (TRUSTED_SIGNING_SUBJECT).
  • Authentication is GitHub OIDC: an Entra application gclo-release-signing with a federated credential for repo:KofTwentyTwo/gclo:environment:release and only Artifact Signing Certificate Profile Signer on the certificate profile. No secret, key or PFX anywhere. The identity is release-environment variables: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT, TRUSTED_SIGNING_ACCOUNT, TRUSTED_SIGNING_PROFILE, TRUSTED_SIGNING_SUBJECT.
  • .github/scripts/Set-ReleaseSigning.ps1: idempotent Azure configuration (reads the existing account and Active profile through ARM, ensures app + service principal + federated credential + profile-scoped role, prints the gh variable set lines). Run interactively by the owner; never creates secrets or touches the account, validation or profile.
  • Docs: RELEASING.md ("Code signing", local signing with SignTool + the Microsoft.ArtifactSigning.Client dlib, the verify job), SECURITY.md, README verify step 4, threat-model revision row.

Closes #57 (part of #62). After the first signed release verifies, EX-0002 closes for gclo in the standards register.

Azure configuration status

Pending the owner's interactive sign-in; the script output and signtool verify results of the first signed prerelease will be posted here before merge.

How it was tested

  • actionlint clean; PSScriptAnalyzer 0 findings.
  • Verification of a real signed build: pending the Azure configuration (recorded here when done).

🤖 Generated with Claude Code

## What Releases are now Authenticode-signed (K22-REL-05) through the standards' shared workflow, which already carries the signing steps behind `sign` (`azure/login` v3.1.0 and `azure/artifact-signing-action` v2.0.0, both SHA-pinned and current; `vpk pack --azureTrustedSignFile`). ADR 0008. - `release.yml`: `sign: true`; new `verify-signatures` job after `release` downloads the published Setup.exe, portable zip and CLI zip and runs `signtool verify /pa /all` + `Get-AuthenticodeSignature` on every gclo executable, failing on an invalid, untimestamped or foreign-publisher signature (`TRUSTED_SIGNING_SUBJECT`). - Authentication is GitHub OIDC: an Entra application `gclo-release-signing` with a federated credential for `repo:KofTwentyTwo/gclo:environment:release` and only *Artifact Signing Certificate Profile Signer* on the certificate profile. No secret, key or PFX anywhere. The identity is `release`-environment **variables**: `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_SUBSCRIPTION_ID`, `TRUSTED_SIGNING_ENDPOINT`, `TRUSTED_SIGNING_ACCOUNT`, `TRUSTED_SIGNING_PROFILE`, `TRUSTED_SIGNING_SUBJECT`. - `.github/scripts/Set-ReleaseSigning.ps1`: idempotent Azure configuration (reads the existing account and Active profile through ARM, ensures app + service principal + federated credential + profile-scoped role, prints the `gh variable set` lines). Run interactively by the owner; never creates secrets or touches the account, validation or profile. - Docs: RELEASING.md ("Code signing", local signing with SignTool + the `Microsoft.ArtifactSigning.Client` dlib, the verify job), SECURITY.md, README verify step 4, threat-model revision row. Closes #57 (part of #62). After the first signed release verifies, EX-0002 closes for gclo in the standards register. ## Azure configuration status Pending the owner's interactive sign-in; the script output and `signtool verify` results of the first signed prerelease will be posted here before merge. ## How it was tested - `actionlint` clean; PSScriptAnalyzer 0 findings. - Verification of a real signed build: pending the Azure configuration (recorded here when done). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
KofTwentyTwo commented 2026-10-10 19:31:42 +00:00 (Migrated from github.com)

Azure configuration done (2026-10-10, run by the owner's interactive session)

Verified against the deployed resources, not the setup notes:

Item Value
Tenant 4ebeb0ad-5782-487a-a1b4-5080255774cf (koftwentytwooutlook.onmicrosoft.com)
Subscription 8ef2479e-3346-4cba-93ad-101c6cd237a0 "Kof22 Signing Account"
Signing account kof22signing, rg-code-signing, eastus, endpoint https://eus.codesigning.azure.net
Certificate profile releases, PublicTrust, Active; certificate subject CN=James Maes, O=James Maes, L=Chester, S=il, C=US (thumbprint BAAA6D7D…31D3)
Entra application gclo-release-signing, client id 5fc7f572-6ffa-4538-bcf4-65282dd0859a (new; no existing identity existed in the tenant)
Federated credential issuer https://token.actions.githubusercontent.com, subject repo:KofTwentyTwo/gclo:environment:release, audience api://AzureADTokenExchange
Role Artifact Signing Certificate Profile Signer on …/certificateProfiles/releases only, for the service principal and (for local one-offs) the owner user. No subscription-level role for the application.

release environment variables set: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT, TRUSTED_SIGNING_ACCOUNT, TRUSTED_SIGNING_PROFILE, TRUSTED_SIGNING_SUBJECT=James Maes. No secret was created anywhere.

Signing test (local, same account and profile)

signtool sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib Azure.CodeSigning.Dlib.dll /dmdf metadata.json gclo.exe gclo.dll → 2 signed, 0 warnings, 0 errors.

signtool verify /pa /all /v gclo.exe → exit 0; chain James Maes ← Microsoft ID Verified CS EOC CA 03 ← Microsoft ID Verified Code Signing PCA 2021 ← Microsoft Identity Verification Root CA 2020. Get-AuthenticodeSignature: Valid, subject CN=James Maes, O=James Maes, L=Chester, S=il, C=US, timestamper Microsoft Public RSA Time Stamping Authority.

The verify-signatures job expects the subject to contain "James Maes" (the validated publisher), which is what Windows will show.

Next: merge → v1.0.2-beta.2 (first CI-signed build, OIDC path exercised for real) → verify the published assets with signtool → v1.0.2 → close EX-0002 in the standards register.

## Azure configuration done (2026-10-10, run by the owner's interactive session) Verified against the deployed resources, not the setup notes: | Item | Value | | --- | --- | | Tenant | `4ebeb0ad-5782-487a-a1b4-5080255774cf` (`koftwentytwooutlook.onmicrosoft.com`) | | Subscription | `8ef2479e-3346-4cba-93ad-101c6cd237a0` "Kof22 Signing Account" | | Signing account | `kof22signing`, `rg-code-signing`, **eastus**, endpoint `https://eus.codesigning.azure.net` | | Certificate profile | `releases`, PublicTrust, **Active**; certificate subject `CN=James Maes, O=James Maes, L=Chester, S=il, C=US` (thumbprint `BAAA6D7D…31D3`) | | Entra application | `gclo-release-signing`, client id `5fc7f572-6ffa-4538-bcf4-65282dd0859a` (new; no existing identity existed in the tenant) | | Federated credential | issuer `https://token.actions.githubusercontent.com`, subject `repo:KofTwentyTwo/gclo:environment:release`, audience `api://AzureADTokenExchange` | | Role | *Artifact Signing Certificate Profile Signer* on `…/certificateProfiles/releases` only, for the service principal and (for local one-offs) the owner user. No subscription-level role for the application. | `release` environment variables set: `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_SUBSCRIPTION_ID`, `TRUSTED_SIGNING_ENDPOINT`, `TRUSTED_SIGNING_ACCOUNT`, `TRUSTED_SIGNING_PROFILE`, `TRUSTED_SIGNING_SUBJECT=James Maes`. No secret was created anywhere. ## Signing test (local, same account and profile) `signtool sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib Azure.CodeSigning.Dlib.dll /dmdf metadata.json gclo.exe gclo.dll` → 2 signed, 0 warnings, 0 errors. `signtool verify /pa /all /v gclo.exe` → exit 0; chain *James Maes* ← Microsoft ID Verified CS EOC CA 03 ← Microsoft ID Verified Code Signing PCA 2021 ← Microsoft Identity Verification Root CA 2020. `Get-AuthenticodeSignature`: `Valid`, subject `CN=James Maes, O=James Maes, L=Chester, S=il, C=US`, timestamper Microsoft Public RSA Time Stamping Authority. The `verify-signatures` job expects the subject to contain **"James Maes"** (the validated publisher), which is what Windows will show. Next: merge → `v1.0.2-beta.2` (first CI-signed build, OIDC path exercised for real) → verify the published assets with signtool → `v1.0.2` → close EX-0002 in the standards register.
KofTwentyTwo commented 2026-10-10 19:31:43 +00:00 (Migrated from github.com)

Replaces #93 and #95 (rebuilt on main after #94; same commits, signed and signed-off; the threat-model revision table keeps both the #91 and the signing rows). The thumbprint quoted above is that day's certificate only: Azure Artifact Signing rotates certificates every few days, nothing pins it, and timestamped signatures stay valid.

Replaces #93 and #95 (rebuilt on main after #94; same commits, signed and signed-off; the threat-model revision table keeps both the #91 and the signing rows). The thumbprint quoted above is that day's certificate only: Azure Artifact Signing rotates certificates every few days, nothing pins it, and timestamped signatures stay valid.
Sign in to join this conversation.