Found in the Windows VM re-test. - A CA-flagged server certificate (common with a plain openssl self-signed cert) produced an installer package with no pin at all, so agent 1.20 refused the server. The package now always carries the SPKI pin of the certificate PCs receive; the CA flag only skips the trusted-root import. When an uploaded CA-flagged certificate differs from the server's own, both pins are included. The build response carries X-ShopDB-Build-Warning and the installer logs what older agents need. - Pins written into appsettings.json by the installer were merged with the registry, so removing a pin from HKLM never took effect. When the ServerPins value exists in the registry it is now authoritative, even when empty; appsettings pins apply only when it is absent. Set-ShopDBServerPins.ps1 -Clear deletes the value; new -None writes an empty value (no pins). - A script or package download sent with a credential revoked moments earlier got 401 CREDENTIAL_INVALID and failed the command. The agent now retries that download once with the shared key and records the fallback in the result.
213 lines
7.8 KiB
PowerShell
213 lines
7.8 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Write the ShopDB server public key pins to HKLM:\SOFTWARE\GE\ShopDB
|
|
ServerPins, so the endpoint agent and the SYSTEM reporters trust only this
|
|
site's server key.
|
|
|
|
.DESCRIPTION
|
|
Each pin is the lowercase hex SHA-256 of the server leaf certificate's
|
|
SubjectPublicKeyInfo. The ShopDB Server Certificate settings page shows the
|
|
current one, and Get-ShopDBServerPin.ps1 computes one from a URL or a
|
|
certificate file.
|
|
|
|
Written as REG_MULTI_SZ in BOTH registry views (64-bit and WOW6432Node),
|
|
the same way CollectorKey is, so a 32-bit reader sees the same set.
|
|
|
|
Meant to run as a GE-Enforce PS1 entry under the SYSTEM task, before agent
|
|
1.20 reaches the PC. Idempotent: the set on the PC is compared with the
|
|
requested set first, and nothing is written when they already match, so it
|
|
is safe every cycle.
|
|
|
|
THE REGISTRY WINS. When ServerPins exists in either view, even empty, the
|
|
agent uses it alone and ignores serverpins / servercertthumbprint in
|
|
appsettings.json (the installer and store upgrade write those). Only when
|
|
the value is absent in both views do the appsettings pins apply.
|
|
|
|
ROTATING THE SERVER KEY: pass the old pin AND the new pin until every PC has
|
|
both, switch the server certificate, then write ONLY the new pin to retire
|
|
the old one. A PC holding only the old pin refuses the new certificate.
|
|
|
|
.PARAMETER Pins
|
|
One or more pins separated by commas, semicolons or spaces. Colons and case
|
|
are ignored. Every value must be 64 hex characters or nothing is written.
|
|
|
|
.PARAMETER Clear
|
|
Delete ServerPins from both views. The agent then falls back to the pins
|
|
in appsettings.json; with none there, normal Windows certificate checks.
|
|
|
|
.PARAMETER None
|
|
Write an EMPTY ServerPins in both views. The registry stays authoritative
|
|
with no pins, so appsettings.json pins are ignored and the normal Windows
|
|
certificate checks apply.
|
|
|
|
.PARAMETER LogPath
|
|
Append a transcript here as well as writing to the pipeline.
|
|
|
|
.EXAMPLE
|
|
.\Set-ShopDBServerPins.ps1 -Pins <pin>
|
|
Pin one server key.
|
|
|
|
.EXAMPLE
|
|
.\Set-ShopDBServerPins.ps1 -Pins "<oldpin>,<newpin>" -WhatIf
|
|
Show what a key rotation would write without touching the registry.
|
|
|
|
.EXAMPLE
|
|
.\Set-ShopDBServerPins.ps1 -Pins <newpin>
|
|
After the server switched certificates: retire the old pin.
|
|
|
|
.EXAMPLE
|
|
.\Set-ShopDBServerPins.ps1 -None
|
|
Turn pinning off on a PC whose appsettings.json still carries pins.
|
|
|
|
.NOTES
|
|
Exit codes:
|
|
0 written, or already correct
|
|
1 bad input or an error stopped the write
|
|
#>
|
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
param(
|
|
[string]$Pins,
|
|
[switch]$Clear,
|
|
[switch]$None,
|
|
[string]$LogPath
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$SubKey = 'SOFTWARE\GE\ShopDB'
|
|
$ValueName = 'ServerPins'
|
|
|
|
function Log {
|
|
param([string]$Message)
|
|
$line = "{0} [Set-ShopDBServerPins] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
|
Write-Output $line
|
|
if ($LogPath) {
|
|
try { Add-Content -LiteralPath $LogPath -Value $line -ErrorAction Stop } catch { }
|
|
}
|
|
}
|
|
|
|
# Returns @{ Present; Pins }. Present = the value exists, even empty.
|
|
function Get-CurrentPins {
|
|
param([Microsoft.Win32.RegistryView]$View)
|
|
$result = @{ Present = $false; Pins = @() }
|
|
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $View)
|
|
try {
|
|
$key = $base.OpenSubKey($SubKey)
|
|
if (-not $key) { return $result }
|
|
try {
|
|
$value = $key.GetValue($ValueName)
|
|
if ($null -eq $value) { return $result }
|
|
$result.Present = $true
|
|
$result.Pins = @(@($value) | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ })
|
|
return $result
|
|
} finally { $key.Dispose() }
|
|
} finally { $base.Dispose() }
|
|
}
|
|
|
|
function Test-SamePins {
|
|
param([string[]]$Left, [string[]]$Right)
|
|
$a = @($Left | Sort-Object -Unique)
|
|
$b = @($Right | Sort-Object -Unique)
|
|
if ($a.Count -ne $b.Count) { return $false }
|
|
for ($i = 0; $i -lt $a.Count; $i++) {
|
|
if ($a[$i] -ne $b[$i]) { return $false }
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# --- 1. validate the request ------------------------------------------------
|
|
|
|
$requested = @()
|
|
if (@(@($Clear.IsPresent, $None.IsPresent, [bool]$Pins) | Where-Object { $_ }).Count -gt 1) {
|
|
Log 'ERROR use only one of -Pins, -Clear, -None'
|
|
exit 1
|
|
}
|
|
if ($None) {
|
|
Log 'requested an empty ServerPins (registry authoritative, no pins)'
|
|
} elseif (-not $Clear) {
|
|
if (-not $Pins -or -not $Pins.Trim()) {
|
|
Log 'ERROR no -Pins given (-Clear deletes the value, -None writes it empty)'
|
|
exit 1
|
|
}
|
|
foreach ($raw in ($Pins -split '[,;\s]+')) {
|
|
if (-not $raw) { continue }
|
|
$pin = ($raw -replace ':', '').ToLowerInvariant()
|
|
if ($pin -notmatch '^[0-9a-f]{64}$') {
|
|
Log "ERROR '$raw' is not a SHA-256 pin (64 hex characters); nothing written"
|
|
exit 1
|
|
}
|
|
if ($requested -notcontains $pin) { $requested += $pin }
|
|
}
|
|
if ($requested.Count -eq 0) {
|
|
Log 'ERROR -Pins held no values; nothing written'
|
|
exit 1
|
|
}
|
|
Log "requested $($requested.Count) pin(s): $($requested -join ', ')"
|
|
}
|
|
|
|
# --- 2. write each view -----------------------------------------------------
|
|
|
|
$views = @(
|
|
@{ Name = '64-bit'; View = [Microsoft.Win32.RegistryView]::Registry64 },
|
|
@{ Name = '32-bit'; View = [Microsoft.Win32.RegistryView]::Registry32 }
|
|
)
|
|
|
|
$failed = $false
|
|
foreach ($entry in $views) {
|
|
try {
|
|
$state = Get-CurrentPins -View $entry.View
|
|
$current = @($state.Pins)
|
|
|
|
if ($Clear) {
|
|
if (-not $state.Present) {
|
|
Log "$($entry.Name): no ServerPins; nothing to do"
|
|
continue
|
|
}
|
|
if ($PSCmdlet.ShouldProcess("HKLM\$SubKey ($($entry.Name))", "Remove $ValueName")) {
|
|
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $entry.View)
|
|
try {
|
|
$key = $base.OpenSubKey($SubKey, $true)
|
|
if ($key) {
|
|
try { $key.DeleteValue($ValueName, $false) } finally { $key.Dispose() }
|
|
}
|
|
} finally { $base.Dispose() }
|
|
Log "$($entry.Name): deleted ServerPins (appsettings.json pins apply)"
|
|
}
|
|
continue
|
|
}
|
|
|
|
if ($state.Present -and (Test-SamePins -Left $current -Right $requested)) {
|
|
Log "$($entry.Name): ServerPins already correct; nothing to do"
|
|
continue
|
|
}
|
|
|
|
$action = "Set $ValueName to $($requested.Count) pin(s) (was $($current.Count))"
|
|
if ($PSCmdlet.ShouldProcess("HKLM\$SubKey ($($entry.Name))", $action)) {
|
|
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $entry.View)
|
|
try {
|
|
$key = $base.CreateSubKey($SubKey)
|
|
try {
|
|
$key.SetValue($ValueName, [string[]]$requested, [Microsoft.Win32.RegistryValueKind]::MultiString)
|
|
} finally { $key.Dispose() }
|
|
} finally { $base.Dispose() }
|
|
|
|
# Read back rather than trust SetValue.
|
|
$after = Get-CurrentPins -View $entry.View
|
|
if (-not $after.Present -or -not (Test-SamePins -Left @($after.Pins) -Right $requested)) {
|
|
Log "ERROR $($entry.Name): ServerPins did not read back as written"
|
|
$failed = $true
|
|
continue
|
|
}
|
|
Log "$($entry.Name): wrote ServerPins ($($requested.Count) pin(s), was $($current.Count))"
|
|
}
|
|
} catch {
|
|
Log "ERROR $($entry.Name): $($_.Exception.Message)"
|
|
$failed = $true
|
|
}
|
|
}
|
|
|
|
if ($failed) { exit 1 }
|
|
exit 0
|