Files
shopdb-flask/plugins/endpoints/client/Set-ShopDBServerPins.ps1
cproudlock 8b144a17bb
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
Add the enrollment page, move scripts to per-PC credentials, and document the rollout
- Settings > Endpoints > Enrollment. Migration tab: readiness counts and
  progress, each PC's state with the reasons it is blocked, "Upgrade
  selected" and "Upgrade next 25", and live wave progress. Credentials tab:
  enrolled PCs with revoke, pending requests with approve and reject, and the
  shared key and auto-enroll switches. Turning the shared key off asks for
  confirmation and shows how many PCs are not enrolled.
- The server certificate page shows the SPKI pin to put in the GE-Enforce
  pin entry.
- The agent upgrade script and the asset and printer reporters send the PC's
  credential when it has one (over https, only to the issuing server), fall
  back to the shared key, check server pins, and log refusals clearly.
- New Set-ShopDBServerPins.ps1 (GE-Enforce entry) and Get-ShopDBServerPin.ps1.
- AGENT-CONTRACT.md documents per-PC credentials; the rollout runbook covers
  phases, gates, rollback, pin and wave entries, and every place the shared
  key lives and how to clear it at cutover.
- Removed the 09-14 certificate design notes and dated session reports, and
  two VM notes that carried a live collector key and advised plain http.
- Corrected the GE-Enforce collector note, which claimed a plain registry
  token is protected by ACL.
2026-09-15 12:04:12 -04:00

183 lines
6.4 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.
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>
Pin one server key.
.EXAMPLE
.\Set-ShopDBServerPins.ps1 -Pins "<oldpin>,<newpin>" -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