Skip to content

Commit e7fcbc1

Browse files
committed
fix(components): verify CBS package integrity by content hash before install
Assert-AtlasCbsCertificate only checked for an EKU, so a tampered or attacker re-signed CAB could pass. Thumbprint-pinning (the obvious fix) is unworkable: the sxsc builder regenerates the signing cert on every build (make-cert.ps1 -> New-SelfSignedCertificate), so no thumbprint is stable — which is also why the 8A334AA8 value in the ROOT-store key no longer matches the shipped CABs' signer. Instead verify each CAB's SHA256 against a committed Atlas-CbsHashes.psd1 before Add-WindowsPackage (loaded via the AST so it doesn't depend on PSModulePath under TrustedInstaller). A PayloadLayout test fails CI if the manifest drifts from the shipped CABs. The existing EKU check and ROOT-store key are left untouched.
1 parent 69c7c0f commit e7fcbc1

3 files changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SHA256 of every CBS package shipped in this folder, verified before install by
2+
# Atlas.Software\Domain\CbsPackages.ps1 (Assert-AtlasCbsHash). The signing cert is
3+
# regenerated on every CAB rebuild, so its thumbprint is not stable and cannot be
4+
# pinned; the content hash is. REGENERATE THIS FILE WHENEVER THE CABS ARE REBUILT:
5+
# Get-ChildItem *.cab | Sort-Object Name | ForEach-Object {
6+
# " '$($_.Name)' = '$((Get-FileHash $_ -Algorithm SHA256).Hash)'" }
7+
# A PayloadLayout test fails CI if this drifts from the shipped CABs.
8+
@{
9+
'Z-Atlas-NoDefender-Package31bf3856ad364e35amd645.0.0.0.cab' = '6A8D1F8788277B425766FAD069A8192EDC33EEA2D6F7F9B061A42C941A21D2EC'
10+
'Z-Atlas-NoDefender-Package31bf3856ad364e35arm645.0.0.0.cab' = '39500FA6DF403F162C02A9C36D37D2C787BF459C05376872B9962FA649F4E081'
11+
'Z-Atlas-NoTelemetry-Package31bf3856ad364e35amd645.0.0.0.cab' = '11C5E3502DCB962F2FAE456712C7982DC1864D686B76194628CBFF8D1CD77ECA'
12+
'Z-Atlas-NoTelemetry-Package31bf3856ad364e35arm645.0.0.0.cab' = 'C6526330F660E654B249B63A974793BAE0E39F6714A0B79E03942145195E29A3'
13+
}

playbook/Executables/AtlasModules/Scripts/Modules/Atlas.Software/Domain/CbsPackages.ps1

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,59 @@ function Assert-AtlasCbsCertificate {
9898
}
9999
}
100100

101+
function Get-AtlasCbsExpectedHashes {
102+
<#
103+
.SYNOPSIS
104+
Loads Atlas-CbsHashes.psd1 (the SHA256 of every shipped CAB) from the packages
105+
folder. The CAB signing cert is regenerated on every build, so its thumbprint is
106+
not stable and cannot be pinned - the content hash is what we verify against.
107+
#>
108+
param(
109+
[Parameter(Mandatory = $true)]
110+
[ValidateNotNullOrEmpty()]
111+
[string]$PackagesPath
112+
)
113+
114+
if ($null -ne $script:AtlasCbsExpectedHashes) {
115+
return $script:AtlasCbsExpectedHashes
116+
}
117+
118+
$hashFile = Join-Path -Path $PackagesPath -ChildPath 'Atlas-CbsHashes.psd1'
119+
if (-not (Test-Path -LiteralPath $hashFile -PathType Leaf)) {
120+
throw "The CBS package hash manifest '$hashFile' is missing; refusing to install unverified packages."
121+
}
122+
123+
# Parse via the AST (SafeGetValue) so loading never depends on a possibly-polluted
124+
# PSModulePath under TrustedInstaller (see docs/testing.md).
125+
$tableAst = [System.Management.Automation.Language.Parser]::ParseFile($hashFile, [ref]$null, [ref]$null).Find(
126+
{ param($node) $node -is [System.Management.Automation.Language.HashtableAst] }, $false)
127+
if ($null -eq $tableAst) {
128+
throw "The CBS package hash manifest '$hashFile' is not a valid data file."
129+
}
130+
131+
$script:AtlasCbsExpectedHashes = $tableAst.SafeGetValue()
132+
return $script:AtlasCbsExpectedHashes
133+
}
134+
135+
function Assert-AtlasCbsHash {
136+
param(
137+
[Parameter(Mandatory = $true)]
138+
[ValidateNotNullOrEmpty()]
139+
[string]$CabPath
140+
)
141+
142+
$fileName = Split-Path -Path $CabPath -Leaf
143+
$expected = Get-AtlasCbsExpectedHashes -PackagesPath (Split-Path -Path $CabPath -Parent)
144+
if (-not $expected.ContainsKey($fileName)) {
145+
throw "No expected SHA256 is recorded for '$fileName' in the CBS hash manifest; refusing to install an unlisted package."
146+
}
147+
148+
$actual = (Get-FileHash -LiteralPath $CabPath -Algorithm SHA256).Hash
149+
if ($actual -ne $expected[$fileName]) {
150+
throw "SHA256 mismatch for '$fileName' (got '$actual'); the package may have been modified. Refusing to install it."
151+
}
152+
}
153+
101154
function Install-AtlasCbsCab {
102155
param(
103156
[Parameter(Mandatory = $true)]
@@ -109,6 +162,15 @@ function Install-AtlasCbsCab {
109162
Write-Host "`nInstalling $fileName..." -ForegroundColor Cyan
110163
Write-Host ('-' * 84) -ForegroundColor Magenta
111164

165+
Write-Host '[INFO] Verifying package hash...'
166+
try {
167+
Assert-AtlasCbsHash -CabPath $CabPath
168+
}
169+
catch {
170+
Write-Host "[ERROR] Hash verification failed for '$CabPath': $($_.Exception.Message)" -ForegroundColor Red
171+
return $false
172+
}
173+
112174
Write-Host '[INFO] Checking certificate...'
113175
try {
114176
Assert-AtlasCbsCertificate -CabPath $CabPath

tests/Atlas.PayloadLayout.Tests.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,26 @@ Describe 'Paired registry assets stay in lockstep' {
4040
$hashB | Should -Be $hashA -Because 'the Toolbox copy must match its Scripts/Registry source; edit both together'
4141
}
4242
}
43+
44+
Describe 'CBS package hash manifest stays in lockstep with the shipped CABs' {
45+
# Atlas-CbsHashes.psd1 is verified before install by Assert-AtlasCbsHash. If a CAB is
46+
# rebuilt without regenerating the manifest, this fails CI instead of shipping a gate
47+
# that would reject every real package at install time.
48+
It 'records the correct SHA256 for every shipped .cab and lists no stale entries' {
49+
$pkgDir = Join-Path -Path $script:executablesRoot -ChildPath 'AtlasModules\Packages'
50+
$manifestPath = Join-Path -Path $pkgDir -ChildPath 'Atlas-CbsHashes.psd1'
51+
Test-Path -LiteralPath $manifestPath -PathType Leaf | Should -BeTrue
52+
$manifest = Import-PowerShellDataFile -LiteralPath $manifestPath
53+
54+
$cabs = @(Get-ChildItem -LiteralPath $pkgDir -Filter '*.cab' -File)
55+
$cabs.Count | Should -BeGreaterThan 0
56+
57+
foreach ($cab in $cabs) {
58+
$manifest.Keys | Should -Contain $cab.Name
59+
(Get-FileHash -LiteralPath $cab.FullName -Algorithm SHA256).Hash | Should -Be $manifest[$cab.Name]
60+
}
61+
foreach ($listed in $manifest.Keys) {
62+
$cabs.Name | Should -Contain $listed
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)