Skip to content

Commit 2bd6125

Browse files
authored
ci (authenticity): Strong-name signing for assemblies (#32)
* Strong-name signing for the assemblies * ci(publish): split release workflow into build, sign, pack, sbom generation and publish jobs (#38)
2 parents a4d1ec5 + 976db8b commit 2bd6125

5 files changed

Lines changed: 647 additions & 154 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<#
2+
.SYNOPSIS
3+
Verifies that assemblies are strong-name signed with the approved Infragistics key.
4+
5+
.DESCRIPTION
6+
'sn.exe -vf' proves only that an assembly's strong name is internally consistent, so any valid
7+
private key passes it. This script additionally compares each assembly's public key against a
8+
value pinned in the repository and established out of band from the signing key.
9+
#>
10+
[CmdletBinding()]
11+
param(
12+
[Parameter(Mandatory)]
13+
[string[]]$Path,
14+
15+
[Parameter(Mandatory)]
16+
[string]$ExpectedPublicKeyPath,
17+
18+
[string]$SnPath
19+
)
20+
21+
$ErrorActionPreference = 'Stop'
22+
# Failures are aggregated per assembly, so sn.exe exit codes must not throw on their own.
23+
$PSNativeCommandUseErrorActionPreference = $false
24+
25+
function ConvertTo-HexString([byte[]]$Bytes) {
26+
return (-join ($Bytes | ForEach-Object { $_.ToString('x2') }))
27+
}
28+
29+
if (-not (Test-Path -LiteralPath $ExpectedPublicKeyPath)) {
30+
throw "Pinned public key file not found: $ExpectedPublicKeyPath"
31+
}
32+
33+
$hexLines = @(
34+
Get-Content -LiteralPath $ExpectedPublicKeyPath |
35+
ForEach-Object { $_.Trim() } |
36+
Where-Object { $_ -and -not $_.StartsWith('#') }
37+
)
38+
39+
# A blank or malformed pin must fail loudly; otherwise the whole check silently becomes a no-op.
40+
if ($hexLines.Count -ne 1) {
41+
throw "$ExpectedPublicKeyPath must contain exactly one non-comment line, but contains $($hexLines.Count)."
42+
}
43+
44+
$expectedPublicKeyHex = $hexLines[0].ToLowerInvariant()
45+
if ($expectedPublicKeyHex -notmatch '^[0-9a-f]{320,}$' -or $expectedPublicKeyHex.Length % 2 -ne 0) {
46+
throw "$ExpectedPublicKeyPath does not hold a public key blob (expected an even number of at least 320 hex characters)."
47+
}
48+
49+
$expectedPublicKey = [byte[]]::new($expectedPublicKeyHex.Length / 2)
50+
for ($index = 0; $index -lt $expectedPublicKey.Length; $index++) {
51+
$expectedPublicKey[$index] = [Convert]::ToByte($expectedPublicKeyHex.Substring($index * 2, 2), 16)
52+
}
53+
54+
# SHA-1 is not a security choice here; it is the algorithm that defines a strong-name token.
55+
$digest = [System.Security.Cryptography.SHA1]::Create().ComputeHash($expectedPublicKey)
56+
$tokenBytes = $digest[-8..-1]
57+
[array]::Reverse($tokenBytes)
58+
$expectedToken = ConvertTo-HexString $tokenBytes
59+
60+
if ($SnPath) {
61+
if (-not (Test-Path -LiteralPath $SnPath -PathType Leaf)) {
62+
throw "The specified sn.exe path does not exist: $SnPath"
63+
}
64+
65+
$strongNameTool = Get-Item -LiteralPath $SnPath
66+
}
67+
else {
68+
$strongNameCommand = Get-Command sn.exe -CommandType Application -ErrorAction SilentlyContinue |
69+
Select-Object -First 1
70+
71+
if ($null -ne $strongNameCommand) {
72+
$strongNameTool = Get-Item -LiteralPath $strongNameCommand.Path
73+
}
74+
else {
75+
$windowsSdkRoot = Join-Path ${env:ProgramFiles(x86)} 'Microsoft SDKs\Windows'
76+
$strongNameTool = Get-ChildItem -Path $windowsSdkRoot -Filter 'sn.exe' -Recurse -ErrorAction SilentlyContinue |
77+
Sort-Object -Property @{
78+
Expression = {
79+
$match = [regex]::Match($_.FullName, '\\v(?<version>\d+(?:\.\d+)*)A?\\', 'IgnoreCase')
80+
if ($match.Success) { [version]$match.Groups['version'].Value } else { [version]'0.0' }
81+
}
82+
Descending = $true
83+
}, @{
84+
Expression = { $_.FullName }
85+
Descending = $true
86+
} |
87+
Select-Object -First 1
88+
}
89+
}
90+
91+
if ($null -eq $strongNameTool) {
92+
throw 'Could not find sn.exe on PATH or under the Windows SDK directory. Pass -SnPath explicitly.'
93+
}
94+
95+
Write-Verbose "Using sn.exe from '$($strongNameTool.FullName)'."
96+
97+
$assemblies = @(Get-ChildItem -Path $Path -Filter '*.dll' -Recurse -File)
98+
if ($assemblies.Count -eq 0) {
99+
throw "No assemblies were found under '$($Path -join ', ')'. Refusing to report success."
100+
}
101+
102+
$problems = @()
103+
foreach ($assembly in $assemblies) {
104+
$output = & $strongNameTool.FullName -vf $assembly.FullName
105+
if ($LASTEXITCODE -ne 0) {
106+
$problems += "$($assembly.FullName): strong-name verification failed. $(($output | Where-Object { $_ }) -join ' ')"
107+
continue
108+
}
109+
110+
$assemblyName = [System.Reflection.AssemblyName]::GetAssemblyName($assembly.FullName)
111+
$token = $assemblyName.GetPublicKeyToken()
112+
if ($null -eq $token -or $token.Length -eq 0) {
113+
$problems += "$($assembly.FullName): not strong named."
114+
continue
115+
}
116+
117+
$actualToken = ConvertTo-HexString $token
118+
if ($actualToken -ne $expectedToken) {
119+
$problems += "$($assembly.FullName): public key token is $actualToken, expected $expectedToken."
120+
continue
121+
}
122+
123+
# Best effort: the token is a truncated hash, so compare the whole key when it is available.
124+
$publicKey = $assemblyName.GetPublicKey()
125+
if ($null -ne $publicKey -and $publicKey.Length -gt 0) {
126+
$actualPublicKey = ConvertTo-HexString $publicKey
127+
if ($actualPublicKey -ne $expectedPublicKeyHex) {
128+
$problems += "$($assembly.FullName): public key does not match $ExpectedPublicKeyPath despite a matching token."
129+
}
130+
}
131+
}
132+
133+
if ($problems.Count -gt 0) {
134+
throw "Strong-name validation failed:`n$($problems -join "`n")"
135+
}
136+
137+
Write-Host "Verified $($assemblies.Count) assemblies against public key token $expectedToken."

0 commit comments

Comments
 (0)