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

Closed
KofTwentyTwo wants to merge 3 commits from feat/GH-57-release-signing into main
pull from: feat/GH-57-release-signing
7 changed files with 369 additions and 8 deletions

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
# Copyright (c) 2026 James Maes (KofTwentyTwo)
# SPDX-License-Identifier: MIT
<#
.SYNOPSIS
Configures the Azure side of gclo's release signing (ADR 0008) and prints the
GitHub `release`-environment variables the workflow needs.
.DESCRIPTION
Idempotent. Run interactively by the owner after `Connect-AzAccount` into the
KofTwentyTwo tenant (Az.Accounts, Az.Resources and Az.CodeSigning modules).
It never creates a secret, key or certificate and never touches the signing
account, the publisher validation or the certificate profile themselves:
1. Reads the Artifact Signing account and its certificate profile and fails if
either is missing or the profile is not Active.
2. Ensures an Entra application + service principal for this repository.
3. Ensures a federated credential on it that trusts GitHub's OIDC issuer for
subject `repo:<owner/repo>:environment:<environment>` only.
4. Ensures the service principal holds "Artifact Signing Certificate Profile
Signer" scoped to the certificate profile, and nothing broader.
5. Prints the variable values to set with `gh variable set --env release`.
.PARAMETER Repository
The GitHub repository the credential is bound to (owner/name).
.PARAMETER Environment
The GitHub environment whose jobs may sign (its deployment rule limits it to
v* tags).
.PARAMETER ResourceGroup
Resource group that holds the signing account.
.PARAMETER AccountName
The Artifact Signing account name.
.PARAMETER ProfileName
The certificate profile name on that account.
.PARAMETER ApplicationName
Display name of the Entra application that represents this repository's
release job.
.EXAMPLE
Connect-AzAccount -Tenant koftwentytwooutlook.onmicrosoft.com
./.github/scripts/Set-ReleaseSigning.ps1
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[string] $Repository = 'KofTwentyTwo/gclo',
[string] $Environment = 'release',
[string] $ResourceGroup = 'rg-code-signing',
[string] $AccountName = 'kof22signing',
[string] $ProfileName = 'releases',
[string] $ApplicationName = 'gclo-release-signing'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$signerRole = 'Artifact Signing Certificate Profile Signer'
$issuer = 'https://token.actions.githubusercontent.com'
$subject = "repo:$Repository`:environment:$Environment"
$context = Get-AzContext
if($null -eq $context)
{
throw 'Not signed in. Run Connect-AzAccount into the KofTwentyTwo tenant first.'
}
Write-Output "Tenant $($context.Tenant.Id), subscription '$($context.Subscription.Name)' ($($context.Subscription.Id))"
# 1. The signing account and profile must already exist; this script never creates them.
# Az.CodeSigning has no account/profile cmdlets, so this reads the ARM resources.
$apiVersion = '2024-09-30-preview'
$accountPath = "/subscriptions/$($context.Subscription.Id)/resourceGroups/$ResourceGroup/providers/Microsoft.CodeSigning/codeSigningAccounts/$AccountName"
$accountResponse = Invoke-AzRestMethod -Method GET -Path "$accountPath`?api-version=$apiVersion"
if($accountResponse.StatusCode -ne 200)
{
throw "Signing account '$AccountName' not found in '$ResourceGroup' (HTTP $($accountResponse.StatusCode)): $($accountResponse.Content)"
}
$account = $accountResponse.Content | ConvertFrom-Json
$certProfileResponse = Invoke-AzRestMethod -Method GET -Path "$accountPath/certificateProfiles/$ProfileName`?api-version=$apiVersion"
if($certProfileResponse.StatusCode -ne 200)
{
throw "Certificate profile '$ProfileName' not found on '$AccountName' (HTTP $($certProfileResponse.StatusCode)): $($certProfileResponse.Content)"
}
$certProfile = $certProfileResponse.Content | ConvertFrom-Json
if($certProfile.properties.status -ne 'Active')
{
throw "Certificate profile '$ProfileName' is '$($certProfile.properties.status)', not Active."
}
$endpoint = $account.properties.accountUri.TrimEnd('/')
# The validated publisher is on the issued certificate (subjectName), not on the profile.
$certificate = $certProfile.properties.certificates | Where-Object { $_.status -eq 'Active' } | Select-Object -First 1
if($null -eq $certificate)
{
throw "Certificate profile '$ProfileName' has no active certificate."
}
$subjectName = (($certificate.subjectName -split ',\s*') | Where-Object { $_ -like 'CN=*' } | Select-Object -First 1) -replace '^CN='
Write-Output "Signing account '$AccountName' in $($account.location): endpoint $endpoint."
Write-Output "Profile '$ProfileName' is Active (type $($certProfile.properties.profileType)); certificate subject CN='$subjectName'."
# 2. One application per repository, so the trust can be revoked per repository.
$app = Get-AzADApplication -DisplayName $ApplicationName | Select-Object -First 1
if($null -eq $app)
{
if($PSCmdlet.ShouldProcess($ApplicationName, 'Create Entra application'))
{
$app = New-AzADApplication -DisplayName $ApplicationName -SignInAudience AzureADMyOrg
}
}
$sp = Get-AzADServicePrincipal -ApplicationId $app.AppId
if($null -eq $sp)
{
if($PSCmdlet.ShouldProcess($ApplicationName, 'Create service principal'))
{
$sp = New-AzADServicePrincipal -ApplicationId $app.AppId
}
}
Write-Output "Application '$ApplicationName': client id $($app.AppId), service principal $($sp.Id)."
# 3. Federated credential: GitHub OIDC, this repository's release environment only.
$credentialName = "github-$($Repository.Replace('/', '-'))-$Environment"
$existing = Get-AzADAppFederatedCredential -ApplicationObjectId $app.Id | Where-Object { $_.Subject -eq $subject -and $_.Issuer -eq $issuer }
if($null -eq $existing)
{
if($PSCmdlet.ShouldProcess($credentialName, "Create federated credential for $subject"))
{
New-AzADAppFederatedCredential -ApplicationObjectId $app.Id -Name $credentialName -Issuer $issuer -Subject $subject -Audience 'api://AzureADTokenExchange' | Out-Null
}
}
Write-Output "Federated credential trusts $issuer for subject '$subject'."
# 4. Signer role on the certificate profile only. Owner/Contributor grant no signing right.
$profileScope = $certProfile.Id
$assignment = Get-AzRoleAssignment -ObjectId $sp.Id -Scope $profileScope -RoleDefinitionName $signerRole -ErrorAction SilentlyContinue
if($null -eq $assignment)
{
if($PSCmdlet.ShouldProcess($profileScope, "Assign '$signerRole'"))
{
New-AzRoleAssignment -ObjectId $sp.Id -Scope $profileScope -RoleDefinitionName $signerRole | Out-Null
}
}
$broader = Get-AzRoleAssignment -ObjectId $sp.Id | Where-Object { $_.Scope -ne $profileScope }
if($broader)
{
Write-Warning "The service principal also holds: $(($broader | ForEach-Object { "$($_.RoleDefinitionName) @ $($_.Scope)" }) -join '; '). Remove anything it does not need."
}
Write-Output "'$signerRole' is assigned at $profileScope."
# 5. The values the workflow reads (none of them is a secret).
Write-Output ''
Write-Output "gh variable set AZURE_TENANT_ID --env $Environment --repo $Repository --body '$($context.Tenant.Id)'"
Write-Output "gh variable set AZURE_CLIENT_ID --env $Environment --repo $Repository --body '$($app.AppId)'"
Write-Output "gh variable set AZURE_SUBSCRIPTION_ID --env $Environment --repo $Repository --body '$($context.Subscription.Id)'"
Write-Output "gh variable set TRUSTED_SIGNING_ENDPOINT --env $Environment --repo $Repository --body '$endpoint'"
Write-Output "gh variable set TRUSTED_SIGNING_ACCOUNT --env $Environment --repo $Repository --body '$AccountName'"
Write-Output "gh variable set TRUSTED_SIGNING_PROFILE --env $Environment --repo $Repository --body '$ProfileName'"
Write-Output "gh variable set TRUSTED_SIGNING_SUBJECT --env $Environment --repo $Repository --body '$subjectName'"
+71 -1
View File
@@ -164,7 +164,77 @@ jobs:
gclo.Engine.Tests
gclo.Cli.Tests
cli-project: gclo.Cli/gclo.Cli.csproj
sign: false # standards exception EX-0002 (#57): true once the Azure Trusted Signing identity is validated
# Authenticode (Azure Artifact Signing, formerly Trusted Signing) over OIDC: the
# shared workflow signs every published exe/dll and vpk signs Setup.exe and the
# update stub with the same identity. The identity lives in the `release`
# environment's variables (AZURE_CLIENT_ID, AZURE_TENANT_ID,
# AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT, TRUSTED_SIGNING_ACCOUNT,
# TRUSTED_SIGNING_PROFILE, docs/RELEASING.md) and is granted only to this
# repository's v* tags through a federated credential (#57, closes EX-0002).
sign: true
# The shipped artifacts carry valid Authenticode signatures: every exe inside the
# installer, the portable zip and the CLI zip, and Setup.exe itself, verify with
# `signtool verify /pa /all` and show the publisher the certificate profile was
# validated for. Runs on what was actually published, so a signing step that
# silently produced unsigned files fails the run (and the release is then
# handled per "A broken release" in docs/RELEASING.md), not a later user.
verify-signatures:
name: Verify signatures on the published assets
needs: release
runs-on: windows-latest
timeout-minutes: 15
permissions:
contents: read
env:
TAG: ${{ github.ref_name }}
K22_VERSION: ${{ needs.release.outputs.version }}
K22_CHANNEL: ${{ needs.release.outputs.prerelease == 'true' && 'dev' || 'stable' }}
EXPECTED_SUBJECT: ${{ vars.TRUSTED_SIGNING_SUBJECT }}
steps:
- name: Download the published assets
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
New-Item -ItemType Directory -Force assets | Out-Null
gh release download $env:TAG --dir assets --pattern "gclo-$env:K22_CHANNEL-Setup.exe" --pattern "gclo-$env:K22_CHANNEL-Portable.zip" --pattern "gclo-cli-win-x64-$env:K22_VERSION.zip"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Expand-Archive assets/gclo-$env:K22_CHANNEL-Portable.zip -DestinationPath assets/portable
Expand-Archive assets/gclo-cli-win-x64-$env:K22_VERSION.zip -DestinationPath assets/cli
- name: signtool verify /pa /all on every shipped executable
shell: pwsh
run: |
if ([string]::IsNullOrEmpty($env:EXPECTED_SUBJECT))
{
Write-Output '::error::TRUSTED_SIGNING_SUBJECT is not set in the release environment (docs/RELEASING.md).'
exit 1
}
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1
$files = @(Get-Item assets/gclo-$env:K22_CHANNEL-Setup.exe) + @(Get-ChildItem assets/portable, assets/cli -Recurse -Include *.exe, *.dll |
Where-Object { $_.Name -like 'gclo*' -or $_.Name -in 'Update.exe' })
$failed = 0
foreach ($f in $files)
{
& $signtool.FullName verify /pa /all /v $f.FullName | Out-Null
$sig = Get-AuthenticodeSignature -LiteralPath $f.FullName
$subject = $sig.SignerCertificate.Subject
$timestamped = $null -ne $sig.TimeStamperCertificate
if ($LASTEXITCODE -ne 0 -or $sig.Status -ne 'Valid' -or -not $timestamped -or $subject -notlike "*$env:EXPECTED_SUBJECT*")
{
Write-Output "::error file=$($f.Name)::signature status '$($sig.Status)', timestamped=$timestamped, subject '$subject' (expected '*$env:EXPECTED_SUBJECT*', signtool exit $LASTEXITCODE)"
$failed++
}
else
{
Write-Output "ok $($f.Name) $subject (timestamped)"
}
}
if ($failed -gt 0) { exit 1 }
Write-Output "All $($files.Count) shipped executables carry a valid, timestamped signature by '$env:EXPECTED_SUBJECT'."
# Scoop and Chocolatey (#9), stable releases only. The repository is its own
# Scoop bucket (bucket/gclo.json) and ships the Chocolatey package source
+7 -1
View File
@@ -62,9 +62,15 @@ gh attestation verify .\gclo-stable-Setup.exe --repo KofTwentyTwo/gclo --signer-
# 3. Dependencies: the SBOM is attested too
gh attestation verify .\gclo-cli-win-x64-<version>.zip --repo KofTwentyTwo/gclo --signer-repo KofTwentyTwo/standards --predicate-type https://cyclonedx.org/bom
# 4. Publisher: Setup.exe and every gclo executable are Authenticode-signed (Azure
# Artifact Signing, SHA-256, RFC 3161 timestamp) by KofTwentyTwo
signtool verify /pa /all /v .\gclo-stable-Setup.exe
Get-AuthenticodeSignature .\gclo-stable-Setup.exe | Format-List Status, SignerCertificate, TimeStamperCertificate
```
The binaries are not yet Authenticode-signed (standards exception [EX-0002](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0002), [#57](https://github.com/KofTwentyTwo/gclo/issues/57)), so step 2 is the origin check until `Get-AuthenticodeSignature` can be.
Windows checks step 4 for you: a signed installer carries the publisher name in the
SmartScreen and UAC prompts instead of "Unknown publisher".
## Getting a token
+7 -5
View File
@@ -110,8 +110,10 @@ is a vulnerability: please report it through the channel above.
[README](README.md#verifying-a-release).
- Self-update downloads only from this repository's GitHub Releases and verifies each
package against the release's `releases.<channel>.json` feed.
- The binaries are **not yet Authenticode-signed**: KofTwentyTwo standards exception
[EX-0002](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0002)
(tracked here as [#57](https://github.com/KofTwentyTwo/gclo/issues/57)). Until they
are, expect a SmartScreen prompt on first install, and prefer the checksum and
attestation checks over trusting a download by its name.
- Setup.exe, the update stub and every gclo executable and library are
**Authenticode-signed** with Azure Artifact Signing (SHA-256, RFC 3161 timestamp) in
the release workflow, over OIDC with an identity that only this repository's release
tags can use; no signing key or secret exists outside Azure. The workflow verifies
the published assets with `signtool verify /pa /all` and fails if any shipped
executable is unsigned or signed by another publisher. Check the publisher yourself
with `Get-AuthenticodeSignature` (README, "Verifying a release").
+64 -1
View File
@@ -64,7 +64,8 @@ its first submission cannot be automated; see below.
| Job | Runs for | Needs | Publishes |
| --- | -------- | ----- | --------- |
| `release` (shared workflow) | every tag | nothing beyond `GITHUB_TOKEN` | the GitHub Release: Setup, portable, full/delta packages, feed, CLI zip, SBOMs, `SHA256SUMS`, provenance and SBOM attestations |
| `release` (shared workflow) | every tag | the signing identity, as `release`-environment **variables** (no secret exists): `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_SUBSCRIPTION_ID`, `TRUSTED_SIGNING_ENDPOINT`, `TRUSTED_SIGNING_ACCOUNT`, `TRUSTED_SIGNING_PROFILE` (see "Code signing") | the GitHub Release: Setup, portable, full/delta packages, feed, CLI zip, SBOMs, `SHA256SUMS`, provenance and SBOM attestations; every exe/dll and Setup.exe Authenticode-signed |
| `verify-signatures` | every tag | `TRUSTED_SIGNING_SUBJECT` (`release`-environment variable: the publisher name on the certificate, as validated by Azure) | nothing; fails the run when a shipped executable is unsigned, untimestamped or signed by another publisher |
| `packages` | stable only | `RELEASE_APP_PRIVATE_KEY` (private key of the release GitHub App, see below) plus the repository variable `RELEASE_APP_CLIENT_ID`, `CHOCO_API_KEY` (chocolatey.org) | the Chocolatey package pushed to chocolatey.org, and a `build(packaging): …` pull request against `main` with `bucket/gclo.json` and `packaging/chocolatey` pointed at the release |
| `winget` | stable only, once the repository variable `WINGET_PACKAGE_ID` is set (`KofTwentyTwo.gclo`, after the one-time manual submission below) | `WINGET_TOKEN` (fine-grained token that can fork and open pull requests on public repositories, ≤ 90 days); missing → the job fails | a manifest update PR on [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) |
| `nuget` | only when the repository variable `NUGET_PUBLISH` is `true` | `NUGET_API_KEY` (nuget.org, push rights for `gclo.Engine`) | `gclo.Engine` on nuget.org |
@@ -80,6 +81,64 @@ requests **write**, no ruleset bypass. Store its client id as the Actions variab
`RELEASE_APP_CLIENT_ID` and its private key (`.pem`) as `RELEASE_APP_PRIVATE_KEY`
in the `release` environment; never as a repository secret or a committed file.
## Code signing
Every release is Authenticode-signed with **Azure Artifact Signing** (formerly Trusted
Signing): the shared workflow signs every published `.exe`/`.dll` (app and CLI) before
packing, and `vpk pack` signs `Setup.exe` and the update stub with the same identity
(`--azureTrustedSignFile`). Always SHA-256 with an RFC 3161 timestamp from
`http://timestamp.acs.microsoft.com`. `SHA256SUMS`, the Velopack feed and the
attestations are produced after signing, so they describe the signed files. The
`verify-signatures` job then downloads what was published and runs
`signtool verify /pa /all` on Setup.exe and every gclo executable (ADR
[0008](adr/0008-azure-artifact-signing.md)).
There is **no signing secret**: the release job logs in to Azure over GitHub OIDC
(`azure/login`) as an Entra application whose federated credential trusts only this
repository's `release` environment, and that application holds only the
*Artifact Signing Certificate Profile Signer* role on the certificate profile.
The identity is configuration, not a secret, so it lives in `release`-environment
variables:
| Variable | Value |
| --- | --- |
| `AZURE_TENANT_ID` | the Entra tenant GUID (not the directory domain) |
| `AZURE_CLIENT_ID` | the application (client) id of `gclo-release-signing` |
| `AZURE_SUBSCRIPTION_ID` | the subscription that holds the signing account |
| `TRUSTED_SIGNING_ENDPOINT` | the account's regional endpoint, e.g. `https://eus.codesigning.azure.net` |
| `TRUSTED_SIGNING_ACCOUNT` | the signing account name (`kof22signing`) |
| `TRUSTED_SIGNING_PROFILE` | the certificate profile name (`releases`) |
| `TRUSTED_SIGNING_SUBJECT` | the validated publisher name on the certificate, checked by `verify-signatures` |
A missing variable fails the Azure login, and the run with it; nothing unsigned is
published. The Azure side (application, federated credential, role assignment) is
administered interactively by the owner; `.github/scripts/Set-ReleaseSigning.ps1`
records the exact steps and is idempotent.
### Signing locally
Only for a one-off (for example re-signing a hotfix build by hand); releases are
signed in CI. You need SignTool (Windows SDK), the .NET 8 runtime, the
[Microsoft.ArtifactSigning.Client](https://www.nuget.org/packages/Microsoft.ArtifactSigning.Client)
package (the `dlib`), and an identity holding the *Certificate Profile Signer* role:
```powershell
# One-time: the dlib, and a metadata file with the same values as the variables above
nuget install Microsoft.ArtifactSigning.Client -x -OutputDirectory $env:LOCALAPPDATA\ArtifactSigning
@{ Endpoint = 'https://eus.codesigning.azure.net'; CodeSigningAccountName = 'kof22signing'; CertificateProfileName = 'releases' } |
ConvertTo-Json | Set-Content $env:LOCALAPPDATA\ArtifactSigning\metadata.json
# Sign in as an authorized identity (the dlib uses the Azure CLI / Azure PowerShell credential)
az login --tenant <tenant GUID>
# Sign and verify
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe | Where-Object FullName -match '\\x64\\' | Sort-Object FullName -Descending | Select-Object -First 1
& $signtool sign /v /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 `
/dlib "$env:LOCALAPPDATA\ArtifactSigning\Microsoft.ArtifactSigning.Client\bin\x64\Azure.CodeSigning.Dlib.dll" `
/dmdf "$env:LOCALAPPDATA\ArtifactSigning\metadata.json" .\gclo.exe
& $signtool verify /pa /all /v .\gclo.exe
```
## First winget submission (one-time, manual)
`wingetcreate update` can only update a package that already exists in
@@ -166,6 +225,10 @@ Both install the same asset, `gclo-cli-win-x64-<version>.zip`, by URL and SHA-25
`SHA256SUMS`, provenance, a **draft** GitHub Release with every asset and
generated notes, then **publish** — releases are immutable, so everything
lands while the release is still a draft.
- **Verify signatures on the published assets** — `signtool verify /pa /all`
on Setup.exe and every gclo executable inside the portable and CLI zips,
checking the publisher name and the timestamp. A failure here means a
published release is unsigned: handle it as "A broken release".
- **Scoop and Chocolatey**, **winget**, **NuGet** — the publishers described
above; each fails loudly when it cannot publish.
+60
View File
@@ -0,0 +1,60 @@
# 0008. Authenticode-sign releases with Azure Artifact Signing over GitHub OIDC
- Status: accepted
- Date: 2026-10-10
- Supersedes: KofTwentyTwo standards exception EX-0002 for this repository
## Context
K22-REL-05 requires Windows executables and installers to be Authenticode-signed.
Until now gclo shipped unsigned (exception EX-0002, #57): users saw "Unknown
publisher" in SmartScreen and UAC, and the only origin check was the build-provenance
attestation, which proves which workflow built a file, not who publishes it.
KofTwentyTwo now has an Azure Artifact Signing (formerly Trusted Signing) account
with a completed publisher identity validation and an active Public Trust
certificate profile. The standards' shared `release-velopack.yml` already carries
the signing steps behind its `sign` input: `azure/login` over OIDC, then
`azure/artifact-signing-action` over every published `.exe`/`.dll`, then `vpk pack
--azureTrustedSignFile` so Setup.exe and the update stub are signed with the same
identity.
## Decision
- Turn the shared workflow's `sign` input on. Signing happens after publish and
before packing; `SHA256SUMS`, the Velopack feed and the attestations are
computed over the signed files. SHA-256 digests and RFC 3161 timestamps
(`http://timestamp.acs.microsoft.com`) always.
- Authenticate with **GitHub OIDC**, never a secret: an Entra application
(`gclo-release-signing`) with a federated credential whose subject is
`repo:KofTwentyTwo/gclo:environment:release`, so only a job running in the
tag-restricted `release` environment can obtain a token. The application holds
exactly one role, *Artifact Signing Certificate Profile Signer*, scoped to the
certificate profile. Owner or Contributor on the subscription grants no signing
right and is not given.
- The identity is configuration, not a secret: tenant, client and subscription ids,
endpoint, account and profile names are `release`-environment variables. Missing
values fail the Azure login, and nothing unsigned is published.
- A caller-side `verify-signatures` job downloads the published Setup.exe, portable
and CLI zips and runs `signtool verify /pa /all` plus `Get-AuthenticodeSignature`
on every gclo executable, checking status, timestamp and the validated publisher
name (`TRUSTED_SIGNING_SUBJECT`). It cannot stop a publish that already happened,
but it turns a silently unsigned release into a failed run handled by the broken-
release runbook; the shared workflow is asked to verify before publishing
(standards#22 follow-up).
- Local signing is documented for one-off needs only (SignTool + the
`Microsoft.ArtifactSigning.Client` dlib + an authorized Azure sign-in); releases
are signed in CI.
- The Azure configuration is scripted in `.github/scripts/Set-ReleaseSigning.ps1`
(idempotent, run interactively by the owner) so it can be audited and repeated.
## Consequences
- SmartScreen and UAC show the validated publisher; Velopack's installer and update
stub are signed, so silent self-update no longer rests on TLS plus the GitHub
account alone.
- Releases depend on Azure being reachable and the certificate profile being
active; a signing outage fails the release rather than shipping unsigned.
- No PFX, private key or client secret exists anywhere; nothing to rotate except
the federated credential if the repository or environment is renamed.
- EX-0002 closes for gclo once the first signed release verifies.
+1
View File
@@ -112,3 +112,4 @@ flowchart LR
| 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | Initial model, covering the path-recovery, WSL clone, vault scoping, package-manager, and SBOM changes of 1.0.1 |
| 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | T11 updated: releases build in the shared standards workflow; publishing tokens moved to the `release` environment (#76) |
| 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | T11: the packaging PR is opened with a short-lived release GitHub App token (contents + pull requests on this repository, no bypass) instead of a long-lived PAT; the commit is GitHub-signed (#85) |
| 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | T2/T11: releases are Authenticode-signed with Azure Artifact Signing over GitHub OIDC (no signing secret; federated credential bound to this repository's `release` environment; signer role scoped to the certificate profile) and the published assets are verified with `signtool verify /pa /all` (#57, ADR 0008) |