#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 one server key. .EXAMPLE .\Set-ShopDBServerPins.ps1 -Pins "," -WhatIf Show what a key rotation would write without touching the registry. .EXAMPLE .\Set-ShopDBServerPins.ps1 -Pins 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