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 " `n Cleaning 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 " `n Validating 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 ) `n This 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 " `n Creating 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 " `n Initializing NuGet package signing process..." - ForegroundColor Cyan
235
+
236
+ # Validate tools existence
237
+ Write-Host " `n Verifying 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 " `n Certificate 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 " `n Signing 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 " `n Copying 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 / 1 KB )
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 / 1 KB )
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