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