#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. ROTATING THE SERVER KEY: pass the old pin AND the new pin until every PC has both, switch the server certificate, then drop the old pin. 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 Remove ServerPins from both views. Pinning is then off 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. .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, [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 { } } } function Get-CurrentPins { param([Microsoft.Win32.RegistryView]$View) $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $View) try { $key = $base.OpenSubKey($SubKey) if (-not $key) { return @() } try { $value = $key.GetValue($ValueName) if ($null -eq $value) { return @() } return @(@($value) | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } 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 (-not $Clear) { if (-not $Pins -or -not $Pins.Trim()) { Log 'ERROR no -Pins given (use -Clear to remove pinning)' 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 { $current = @(Get-CurrentPins -View $entry.View) if ($Clear) { if ($current.Count -eq 0) { 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): removed ServerPins" } continue } if (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 (Test-SamePins -Left $after -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