Skip to content

Commit 3cf730f

Browse files
committed
demo GHA
1 parent 04c75f6 commit 3cf730f

43 files changed

Lines changed: 779 additions & 857 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Builds a demo .NET exe, pulls signing inputs from Key Vault, ensures the cert profile exists,
2+
# then signs and verifies the binary using SignTool + the Azure Artifact Signing dlib.
3+
4+
name: Artifact Signing Demo
5+
6+
on:
7+
push:
8+
branches: [ main ]
9+
workflow_dispatch: {}
10+
11+
permissions:
12+
contents: read
13+
id-token: write
14+
15+
jobs:
16+
sign:
17+
runs-on: windows-latest
18+
19+
env:
20+
BUILD_CONFIGURATION: Release
21+
RUNTIME_IDENTIFIER: win-x64
22+
23+
# Non-secret inputs (configure as GitHub repo variables or secrets):
24+
KEYVAULT_NAME: ${{ secrets.KEYVAULT_NAME }}
25+
ARTIFACT_SIGNING_RESOURCE_GROUP: ${{ secrets.ARTIFACT_SIGNING_RESOURCE_GROUP }}
26+
27+
# Optional override; only needed when creating the cert profile:
28+
CERT_PROFILE_TYPE: PublicTrust
29+
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
34+
- name: Setup .NET
35+
uses: actions/setup-dotnet@v4
36+
with:
37+
dotnet-version: '8.0.x'
38+
39+
- name: Publish (unsigned)
40+
shell: pwsh
41+
run: |
42+
dotnet publish .\SigningDemo\SigningDemo.csproj -c $env:BUILD_CONFIGURATION -r $env:RUNTIME_IDENTIFIER --self-contained false
43+
44+
- name: Azure Login (OIDC)
45+
uses: azure/login@v2
46+
with:
47+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
48+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
49+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
50+
51+
- name: Load signing variables from Key Vault
52+
shell: pwsh
53+
run: |
54+
$ErrorActionPreference = 'Stop'
55+
56+
if ([string]::IsNullOrWhiteSpace($env:KEYVAULT_NAME)) { throw "Missing KEYVAULT_NAME (set as a GitHub secret)." }
57+
if ([string]::IsNullOrWhiteSpace($env:ARTIFACT_SIGNING_RESOURCE_GROUP)) { throw "Missing ARTIFACT_SIGNING_RESOURCE_GROUP (set as a GitHub secret)." }
58+
59+
function Get-KvSecret([string]$name) {
60+
$value = az keyvault secret show --vault-name $env:KEYVAULT_NAME --name $name --query value -o tsv
61+
if ($LASTEXITCODE -ne 0) { throw "Failed to read Key Vault secret '$name' from vault '$($env:KEYVAULT_NAME)'" }
62+
if ($null -eq $value) { return '' }
63+
return "$value".Trim()
64+
}
65+
66+
$endpoint = Get-KvSecret 'artifactSigningEndpoint'
67+
$account = Get-KvSecret 'artifactSigningAccountName'
68+
$profile = Get-KvSecret 'artifactSigningCertificateProfileName'
69+
$idvId = Get-KvSecret 'artifactSigningIdentityValidationId'
70+
71+
if ([string]::IsNullOrWhiteSpace($endpoint)) { throw "Key Vault secret artifactSigningEndpoint is empty." }
72+
if ([string]::IsNullOrWhiteSpace($account)) { throw "Key Vault secret artifactSigningAccountName is empty." }
73+
if ([string]::IsNullOrWhiteSpace($profile)) { throw "Key Vault secret artifactSigningCertificateProfileName is empty." }
74+
75+
# Mask sensitive-ish values in logs.
76+
Write-Host "::add-mask::$endpoint"
77+
Write-Host "::add-mask::$account"
78+
Write-Host "::add-mask::$profile"
79+
if (-not [string]::IsNullOrWhiteSpace($idvId)) { Write-Host "::add-mask::$idvId" }
80+
81+
"ARTIFACT_SIGNING_ENDPOINT=$endpoint" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
82+
"ARTIFACT_SIGNING_ACCOUNT_NAME=$account" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
83+
"ARTIFACT_SIGNING_CERT_PROFILE_NAME=$profile" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
84+
"ARTIFACT_SIGNING_IDENTITY_VALIDATION_ID=$idvId" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
85+
86+
- name: Ensure certificate profile exists
87+
shell: pwsh
88+
run: |
89+
$ErrorActionPreference = 'Stop'
90+
91+
$rg = "$env:ARTIFACT_SIGNING_RESOURCE_GROUP".Trim()
92+
if ([string]::IsNullOrWhiteSpace($rg)) { throw "Missing ARTIFACT_SIGNING_RESOURCE_GROUP" }
93+
94+
$endpoint = "$env:ARTIFACT_SIGNING_ENDPOINT".Trim()
95+
$accountName = "$env:ARTIFACT_SIGNING_ACCOUNT_NAME".Trim()
96+
$profileName = "$env:ARTIFACT_SIGNING_CERT_PROFILE_NAME".Trim()
97+
$profileType = "$env:CERT_PROFILE_TYPE".Trim()
98+
if ([string]::IsNullOrWhiteSpace($profileType)) { $profileType = 'PublicTrust' }
99+
100+
if ([string]::IsNullOrWhiteSpace($endpoint)) { throw "Missing ARTIFACT_SIGNING_ENDPOINT" }
101+
if ([string]::IsNullOrWhiteSpace($accountName)) { throw "Missing ARTIFACT_SIGNING_ACCOUNT_NAME" }
102+
if ([string]::IsNullOrWhiteSpace($profileName)) { throw "Missing ARTIFACT_SIGNING_CERT_PROFILE_NAME" }
103+
104+
$identityValidationId = "$env:ARTIFACT_SIGNING_IDENTITY_VALIDATION_ID".Trim()
105+
106+
$subId = (az account show --query id -o tsv)
107+
if ([string]::IsNullOrWhiteSpace($subId)) { throw "Unable to determine subscription id from Azure CLI context." }
108+
109+
$profileResourceId = "/subscriptions/$subId/resourceGroups/$rg/providers/Microsoft.CodeSigning/codeSigningAccounts/$accountName/certificateProfiles/$profileName"
110+
$profileUrl = "https://management.azure.com$profileResourceId?api-version=2025-10-13"
111+
112+
Write-Host "Ensuring certificate profile exists: $profileResourceId"
113+
114+
$profileExists = $false
115+
try {
116+
az rest --method get --url $profileUrl --only-show-errors | Out-Null
117+
$profileExists = $true
118+
} catch {
119+
$profileExists = $false
120+
}
121+
122+
if (-not $profileExists) {
123+
if ([string]::IsNullOrWhiteSpace($identityValidationId)) {
124+
throw "Certificate profile is missing. Complete identity validation in the portal and set Key Vault secret 'artifactSigningIdentityValidationId'."
125+
}
126+
127+
$body = @{
128+
properties = @{
129+
identityValidationId = $identityValidationId
130+
profileType = $profileType
131+
includeStreetAddress = $false
132+
includePostalCode = $false
133+
}
134+
} | ConvertTo-Json -Depth 10
135+
136+
az rest --method put --url $profileUrl --body $body --only-show-errors | Out-Null
137+
Write-Host "Created certificate profile: $profileName"
138+
}
139+
140+
- name: Sign with Azure Artifact Signing (SignTool + dlib)
141+
shell: pwsh
142+
run: |
143+
$ErrorActionPreference = 'Stop'
144+
145+
$tempRoot = Join-Path "$env:RUNNER_TEMP" "artifact-signing"
146+
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
147+
148+
$nugetExe = Join-Path $tempRoot "nuget.exe"
149+
Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nugetExe
150+
151+
$sdkOut = Join-Path $tempRoot "sdk"
152+
& $nugetExe install Microsoft.Windows.SDK.BuildTools -x -OutputDirectory $sdkOut | Out-Host
153+
154+
$signtool = Get-ChildItem -Path $sdkOut -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match "\\x64\\signtool\\.exe$" } | Select-Object -First 1
155+
if (-not $signtool) {
156+
$signtool = Get-ChildItem -Path $sdkOut -Recurse -Filter signtool.exe | Where-Object { $_.FullName -match "\\x64\\signtool\\.exe" } | Select-Object -First 1
157+
}
158+
if (-not $signtool) { throw "signtool.exe not found after extracting Microsoft.Windows.SDK.BuildTools" }
159+
160+
$dlibOut = Join-Path $tempRoot "dlib"
161+
& $nugetExe install Microsoft.ArtifactSigning.Client -x -OutputDirectory $dlibOut | Out-Host
162+
163+
$dlib = Get-ChildItem -Path $dlibOut -Recurse -Filter Azure.CodeSigning.Dlib.dll | Where-Object { $_.FullName -match "\\x64\\Azure\\.CodeSigning\\.Dlib\\.dll$" } | Select-Object -First 1
164+
if (-not $dlib) {
165+
$dlib = Get-ChildItem -Path $dlibOut -Recurse -Filter Azure.CodeSigning.Dlib.dll | Select-Object -First 1
166+
}
167+
if (-not $dlib) { throw "Azure.CodeSigning.Dlib.dll not found after extracting Microsoft.ArtifactSigning.Client" }
168+
169+
$metadataPath = Join-Path $tempRoot "metadata.json"
170+
$metadata = @{
171+
Endpoint = "$env:ARTIFACT_SIGNING_ENDPOINT"
172+
CodeSigningAccountName = "$env:ARTIFACT_SIGNING_ACCOUNT_NAME"
173+
CertificateProfileName = "$env:ARTIFACT_SIGNING_CERT_PROFILE_NAME"
174+
CorrelationId = "gha-${{ github.run_id }}"
175+
} | ConvertTo-Json -Depth 4
176+
177+
$metadata | Out-File -FilePath $metadataPath -Encoding utf8
178+
179+
$unsigned = "$env:GITHUB_WORKSPACE\SigningDemo\bin\$env:BUILD_CONFIGURATION\net8.0\$env:RUNTIME_IDENTIFIER\publish\SigningDemo.exe"
180+
if (-not (Test-Path $unsigned)) { throw "Unsigned binary not found at $unsigned" }
181+
182+
$signedDir = "$env:GITHUB_WORKSPACE\artifacts"
183+
New-Item -ItemType Directory -Force -Path $signedDir | Out-Null
184+
$signed = Join-Path $signedDir "SigningDemo.signed.exe"
185+
Copy-Item $unsigned $signed -Force
186+
187+
Write-Host "Using signtool: $($signtool.FullName)"
188+
Write-Host "Using dlib: $($dlib.FullName)"
189+
190+
& $signtool.FullName sign /v /debug /fd SHA256 /tr "http://timestamp.acs.microsoft.com" /td SHA256 /dlib "$($dlib.FullName)" /dmdf "$metadataPath" "$signed"
191+
192+
- name: Verify signature
193+
shell: pwsh
194+
run: |
195+
$ErrorActionPreference = 'Stop'
196+
$signed = "$env:GITHUB_WORKSPACE\artifacts\SigningDemo.signed.exe"
197+
Get-AuthenticodeSignature $signed | Format-List
198+
199+
- name: Upload signed artifact
200+
uses: actions/upload-artifact@v4
201+
with:
202+
name: signed-drop
203+
path: artifacts\

.gitignore

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Local .terraform directories
22
.terraform/
3+
*.terraform.lock.hcl
4+
.terraform.lock.hcl
35
*src/.env
46
app-logs.zip
57
LogFiles
@@ -43,11 +45,3 @@ override.tf.json
4345
# Ignore CLI configuration files
4446
.terraformrc
4547
terraform.rc
46-
47-
# .NET build output
48-
**/bin/
49-
**/obj/
50-
51-
# Visual Studio / VS Code
52-
.vs/
53-
.vscode/

0 commit comments

Comments
 (0)