Skip to content

Commit bec5af8

Browse files
authored
Merge pull request #170
ci: added a signing script to sign build assets
2 parents 4e25a51 + 56b4430 commit bec5af8

File tree

1 file changed

+372
-0
lines changed

1 file changed

+372
-0
lines changed

build/sign.ps1

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
# Helper Functions
2+
function Sign-SingleFile {
3+
[CmdletBinding()]
4+
param(
5+
[Parameter(Mandatory = $true)]
6+
[string]$FilePath,
7+
8+
[Parameter(Mandatory = $true)]
9+
[string]$Thumbprint,
10+
11+
[Parameter(Mandatory = $true)]
12+
[string]$SignToolPath,
13+
14+
[Parameter(Mandatory = $true)]
15+
[string]$TimestampServer
16+
)
17+
18+
$signParams = @(
19+
"sign", "/fd", "SHA256",
20+
"/sha1", $Thumbprint,
21+
"/t", $TimestampServer,
22+
$FilePath
23+
)
24+
25+
$output = & $SignToolPath @signParams 2>&1
26+
if ($LASTEXITCODE -ne 0) {
27+
$output | ForEach-Object { Write-Host $_ }
28+
throw "Signing failed for file: $FilePath"
29+
}
30+
}
31+
32+
function Clean-Directory {
33+
[CmdletBinding()]
34+
param(
35+
[Parameter(Mandatory = $true)]
36+
[string]$BaseDirectory
37+
)
38+
39+
Write-Host "`nCleaning up working directories..." -ForegroundColor Yellow
40+
41+
# Only clean unsigned and signed directories
42+
$dirsToClean = @(
43+
Join-Path $BaseDirectory "unsigned"
44+
Join-Path $BaseDirectory "signed"
45+
)
46+
47+
foreach ($dir in $dirsToClean) {
48+
if (Test-Path $dir) {
49+
Write-Host "Removing: $dir"
50+
Remove-Item $dir -Recurse -Force
51+
}
52+
}
53+
Write-Host "✓ Cleanup completed"
54+
}
55+
56+
function Test-RequiredAssets {
57+
[CmdletBinding()]
58+
param(
59+
[Parameter(Mandatory = $true)]
60+
[string]$WorkingDirectory,
61+
62+
[Parameter(Mandatory = $true)]
63+
[string]$NuGetPackagesZip,
64+
65+
[Parameter(Mandatory = $true)]
66+
[string]$SymbolsPackagesZip
67+
)
68+
69+
Write-Host "`nValidating required build assets..."
70+
$requiredFiles = @{
71+
$NuGetPackagesZip = "NuGet packages"
72+
$SymbolsPackagesZip = "Symbol packages"
73+
}
74+
75+
foreach ($required in $requiredFiles.GetEnumerator()) {
76+
$found = Get-ChildItem -Path $WorkingDirectory -Filter $required.Key -ErrorAction SilentlyContinue
77+
if (-not $found) {
78+
throw "Required build asset not found: $($required.Key)`nThis file should contain $($required.Value)"
79+
}
80+
81+
Write-Host " ✅ Found $($required.Value) in: $($found.Name)" -ForegroundColor Green
82+
83+
# Verify GitHub attestation
84+
if (-not (Test-GithubAttestation -FilePath $found.FullName -RepoName "Yubico/Yubico.NET.SDK")) {
85+
throw "Attestation verification failed for: $($found.Name)"
86+
}
87+
}
88+
}
89+
90+
function Initialize-DirectoryStructure {
91+
[CmdletBinding()]
92+
param(
93+
[Parameter(Mandatory = $true)]
94+
[string]$BaseDirectory
95+
)
96+
97+
$directories = @{
98+
WorkingDir = $BaseDirectory
99+
Unsigned = Join-Path $BaseDirectory "unsigned"
100+
Signed = Join-Path $BaseDirectory "signed"
101+
Libraries = Join-Path $BaseDirectory "signed\libraries"
102+
Packages = Join-Path $BaseDirectory "signed\packages"
103+
}
104+
105+
Write-Host "`nCreating directory structure..."
106+
# Only create the directories we'll manage
107+
$directories.Keys | Where-Object { $_ -ne 'WorkingDir' } | ForEach-Object {
108+
$dir = $directories[$_]
109+
if (-not (Test-Path $dir)) {
110+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
111+
Write-Host "✓ Created: $dir"
112+
}
113+
}
114+
115+
return $directories
116+
}
117+
118+
function Test-GithubAttestation {
119+
[CmdletBinding()]
120+
param(
121+
[Parameter(Mandatory = $true)]
122+
[string]$FilePath,
123+
124+
[Parameter(Mandatory = $true)]
125+
[string]$RepoName
126+
)
127+
128+
Write-Host " 🔐 Verifying attestation for: $FilePath" -ForegroundColor Gray
129+
130+
try {
131+
# Check if gh CLI is available
132+
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
133+
throw "GitHub CLI (gh) is not installed or not in PATH"
134+
}
135+
136+
$output = gh attestation verify $FilePath --repo $RepoName 2>&1
137+
if ($LASTEXITCODE -ne 0) {
138+
Write-Host $output -ForegroundColor Red
139+
throw $output # This will trigger the catch block
140+
}
141+
142+
Write-Host " ✅ Attestation verified" -ForegroundColor Green
143+
return $true
144+
}
145+
catch {
146+
Write-Host " ❌ Attestation verification failed: $_" -ForegroundColor Red
147+
return $false
148+
}
149+
}
150+
151+
<#
152+
.SYNOPSIS
153+
Signs NuGet and Symbol packages using a smart card certificate.
154+
155+
.DESCRIPTION
156+
Signs NuGet packages (*.nupkg) and their corresponding symbol packages (*.snupkg) using a hardware-based certificate.
157+
The script processes the contents of two required zip files ('Nuget Packages.zip' and 'Symbols Packages.zip'),
158+
signs all assemblies within the NuGet packages, repacks them, and then signs both the NuGet and Symbol packages.
159+
160+
How to use:
161+
1. Create a release folder on your machine e.g. ../releases/1.12
162+
2. Download the build assets "Nuget Packages.zip" and "Symbols Packages.zip" from the latest SDK build action
163+
to the newly created folder.
164+
3. Start a Powershell terminal, and load the script by running the following command:
165+
> . \.Yubico.NET.SDK\build\sign.ps1
166+
4. The script can be invoked by following the examples below.
167+
168+
.PARAMETER Thumbprint
169+
The thumbprint of the signing certificate stored on the smart card.
170+
171+
.PARAMETER WorkingDirectory
172+
The directory containing the zip files and where the signing process will take place.
173+
174+
.PARAMETER SignToolPath
175+
Optional. Path to signtool.exe. Defaults to "signtool.exe" (expects it in PATH).
176+
177+
.PARAMETER NuGetPath
178+
Optional. Path to nuget.exe. Defaults to "nuget.exe" (expects it in PATH).
179+
180+
.PARAMETER TimestampServer
181+
Optional. URL of the timestamp server. Defaults to "http://timestamp.digicert.com".
182+
183+
.PARAMETER NuGetPackagesZip
184+
Optional. Name of the NuGet packages zip file. Defaults to "Nuget Packages.zip".
185+
186+
.PARAMETER SymbolsPackagesZip
187+
Optional. Name of the symbols packages zip file. Defaults to "Symbols Packages.zip".
188+
189+
.PARAMETER CleanWorkingDirectory
190+
Optional switch. If specified, cleans the working directories before processing.
191+
192+
.EXAMPLE
193+
Invoke-NuGetPackageSigning -Thumbprint "0123456789ABCDEF" -WorkingDirectory "C:\Signing"
194+
195+
.EXAMPLE
196+
Invoke-NuGetPackageSigning -Thumbprint "0123456789ABCDEF" -WorkingDirectory "C:\Signing" -CleanWorkingDirectory -NuGetPath "C:\Tools\nuget.exe"
197+
198+
.NOTES
199+
Requires:
200+
- A smart card with the signing certificate
201+
- signtool.exe (Windows SDK)
202+
- nuget.exe
203+
- PowerShell 5.1 or later
204+
#>
205+
function Invoke-NuGetPackageSigning {
206+
[CmdletBinding()]
207+
param(
208+
[Parameter(Mandatory = $true)]
209+
[string]$Thumbprint,
210+
211+
[Parameter(Mandatory = $true)]
212+
[string]$WorkingDirectory,
213+
214+
[Parameter(Mandatory = $false)]
215+
[string]$SignToolPath = "signtool.exe",
216+
217+
[Parameter(Mandatory = $false)]
218+
[string]$NuGetPath = "nuget.exe",
219+
220+
[Parameter(Mandatory = $false)]
221+
[string]$TimestampServer = "http://timestamp.digicert.com",
222+
223+
[Parameter(Mandatory = $false)]
224+
[string]$NuGetPackagesZip = "Nuget Packages.zip",
225+
226+
[Parameter(Mandatory = $false)]
227+
[string]$SymbolsPackagesZip = "Symbols Packages.zip",
228+
229+
[Parameter(Mandatory = $false)]
230+
[switch]$CleanWorkingDirectory
231+
)
232+
233+
try {
234+
Write-Host "`nInitializing NuGet package signing process..." -ForegroundColor Cyan
235+
236+
# Validate tools existence
237+
Write-Host "`nVerifying required tools..."
238+
if (-not (Get-Command $SignToolPath -ErrorAction SilentlyContinue)) {
239+
throw "SignTool not found at path: $SignToolPath"
240+
}
241+
Write-Host "✓ SignTool found at: $SignToolPath"
242+
243+
if (-not (Get-Command $NuGetPath -ErrorAction SilentlyContinue)) {
244+
throw "NuGet not found at path: $NuGetPath"
245+
}
246+
Write-Host "✓ NuGet found at: $NuGetPath"
247+
248+
# Verify certificate is available and log details
249+
$cert = Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $Thumbprint }
250+
if (-not $cert) {
251+
throw "Certificate with thumbprint $Thumbprint not found in current user store"
252+
}
253+
254+
Write-Host "`nCertificate Details:" -ForegroundColor Cyan
255+
Write-Host " Subject: $($cert.Subject)"
256+
Write-Host " Issuer: $($cert.Issuer)"
257+
Write-Host " Thumbprint: $($cert.Thumbprint)"
258+
Write-Host " Valid From: $($cert.NotBefore)"
259+
Write-Host " Valid To: $($cert.NotAfter)"
260+
261+
if ($cert.NotAfter -le (Get-Date).AddMonths(1)) {
262+
Write-Warning "Certificate will expire within one month on $($cert.NotAfter)"
263+
}
264+
265+
# Clean if requested
266+
if ($CleanWorkingDirectory) {
267+
Clean-Directory -BaseDirectory $WorkingDirectory
268+
}
269+
270+
# Initialize directory structure
271+
$directories = Initialize-DirectoryStructure -BaseDirectory $WorkingDirectory
272+
273+
# Validate required zip files
274+
Test-RequiredAssets -WorkingDirectory $WorkingDirectory -NuGetPackagesZip $NuGetPackagesZip -SymbolsPackagesZip $SymbolsPackagesZip
275+
276+
# Process each zip file
277+
Write-Host "`n📦 Processing ZIP files..." -ForegroundColor Yellow
278+
$zipFiles = Get-ChildItem -Path $WorkingDirectory -Filter "*.zip"
279+
foreach ($zip in $zipFiles) {
280+
Write-Host "`n 🔄 Processing: $($zip.Name)" -ForegroundColor Cyan
281+
282+
$extractPath = Join-Path $directories.Unsigned ([System.IO.Path]::GetFileNameWithoutExtension($zip.Name))
283+
Write-Host " 📂 Extracting to: $extractPath" -ForegroundColor Gray
284+
Expand-Archive -Path $zip.FullName -DestinationPath $extractPath -Force
285+
286+
Write-Host " 📋 Copying packages to unsigned directory" -ForegroundColor Gray
287+
$packages = Get-ChildItem -Path $extractPath -Recurse -Include *.nupkg, *.snupkg
288+
foreach ($package in $packages) {
289+
Write-Host " Copying: $($package.Name)"
290+
Copy-Item -Path $package.FullName -Destination $directories.Unsigned -Force
291+
}
292+
Write-Host "✓ Copied $($packages.Count) package(s)"
293+
}
294+
295+
# First process nupkg files to sign their contents
296+
Write-Host "`n📦 Processing NuGet packages..." -ForegroundColor Yellow
297+
$nugetPackages = Get-ChildItem -Path $directories.Unsigned -Filter "*.nupkg"
298+
foreach ($package in $nugetPackages) {
299+
Write-Host "`nSigning contents of: $($package.Name)"
300+
301+
$extractPath = Join-Path $directories.Libraries ([System.IO.Path]::GetFileNameWithoutExtension($package.Name))
302+
Write-Host "Extracting to: $extractPath"
303+
Expand-Archive -Path $package.FullName -DestinationPath $extractPath -Force
304+
305+
Write-Host "Cleaning package structure"
306+
Get-ChildItem -Path $extractPath -Recurse -Include "_rels", "package" | Remove-Item -Force -Recurse
307+
Get-ChildItem -Path $extractPath -Recurse -Filter '[Content_Types].xml' | Remove-Item -Force
308+
309+
Write-Host "Signing assemblies..."
310+
$dlls = Get-ChildItem -Path $extractPath -Include "*.dll" -Recurse
311+
foreach ($dll in $dlls) {
312+
# Get the parent directory name (framework target) and the file name
313+
$frameworkDir = Split-Path (Split-Path $dll.FullName -Parent) -Leaf
314+
$fileName = Split-Path $dll.FullName -Leaf
315+
Write-Host " ✍️ Signing: ..\$frameworkDir\$fileName" -ForegroundColor Gray
316+
Sign-SingleFile -FilePath $dll.FullName -Thumbprint $Thumbprint -SignToolPath $SignToolPath -TimestampServer $TimestampServer
317+
}
318+
319+
Write-Host "Repacking signed content..."
320+
Get-ChildItem -Path $extractPath -Recurse -Filter "*.nuspec" |
321+
ForEach-Object {
322+
Write-Host " Packing: $($_.Name)"
323+
& $NuGetPath pack $_.FullName -OutputDirectory $directories.Packages
324+
}
325+
}
326+
327+
# Copy symbol packages to output directory
328+
Write-Host "`nCopying symbol packages..."
329+
$symbolPackages = Get-ChildItem -Path $directories.Unsigned -Filter "*.snupkg"
330+
foreach ($package in $symbolPackages) {
331+
Write-Host " Copying: $($package.Name)"
332+
Copy-Item -Path $package.FullName -Destination $directories.Packages -Force
333+
}
334+
335+
# Sign all final packages (both nupkg and snupkg)
336+
Write-Host "`n🔏 Signing final packages..." -ForegroundColor Cyan
337+
$finalPackages = Get-ChildItem -Path $directories.Packages -Include *.nupkg, *.snupkg -Recurse
338+
foreach ($package in $finalPackages) {
339+
Write-Host " ✒️ Signing package: $($package.Name)" -ForegroundColor White
340+
$nugetSignParams = @(
341+
"sign", $package.FullName,
342+
"-CertificateFingerprint", $Thumbprint,
343+
"-Timestamper", $TimestampServer,
344+
"-NonInteractive"
345+
)
346+
& $NuGetPath @nugetSignParams
347+
}
348+
349+
# Print summary of signed packages
350+
Write-Host "`n📊 Signed Packages Summary:" -ForegroundColor Yellow
351+
Write-Host " NuGet Packages:" -ForegroundColor White
352+
Get-ChildItem -Path $directories.Packages -Filter "*.nupkg" | ForEach-Object {
353+
$size = "{0:N2}" -f ($_.Length / 1KB)
354+
Write-Host " 📦 $($_.Name) [$size KB]" -ForegroundColor Gray
355+
}
356+
357+
Write-Host " Symbol Packages:" -ForegroundColor White
358+
Get-ChildItem -Path $directories.Packages -Filter "*.snupkg" | ForEach-Object {
359+
$size = "{0:N2}" -f ($_.Length / 1KB)
360+
Write-Host " 🔍 $($_.Name) [$size KB]" -ForegroundColor Gray
361+
}
362+
363+
Write-Host "`n✨ Package signing process completed successfully! ✨" -ForegroundColor Green
364+
return $directories.Packages
365+
}
366+
catch {
367+
Write-Host "`n❌ Error occurred:" -ForegroundColor Red
368+
Write-Error $_.Exception.Message
369+
Clean-Directory -BaseDirectory $WorkingDirectory
370+
throw
371+
}
372+
}

0 commit comments

Comments
 (0)