Private/Get-SqlCertFromTargetStore.ps1
|
# ============================================================================= # Script : Private/Get-SqlCertFromTargetStore.ps1 # Author : Keith Ramsey # Created : 2026-08-23 # ============================================================================= # Change Log # ----------------------------------------------------------------------------- # 2026-08-23 Keith Ramsey Initial: look up a certificate in the TARGET # machine's LocalMachine\My store. Extracted because # Set-RsCertBinding and Test-RsCertBinding both read # the LOCAL store while their help and error text # claimed the -ComputerName target -- so on a remote # target both could report "not found" for a cert # that was present, or pass a pre-flight for a cert # the target did not have. # ============================================================================= # Decision Contract (see Docs/DECISION_REGISTER.md) # ----------------------------------------------------------------------------- # Must : read the store on the machine named by -ComputerName, not the # caller's. Never throw (DR-005). Distinguish "checked and absent" # from "could not check" -- the RS path needs only CIM/WS-Man, so a # machine without PowerShell remoting must still be bindable: the # caller skips its pre-flight rather than failing, and lets the RS # provider report the authoritative error. # ============================================================================= function Get-SqlCertFromTargetStore { <# .SYNOPSIS Finds a certificate by thumbprint in LocalMachine\My on the target computer. .DESCRIPTION Returns a small result describing whether the lookup could be performed and, if so, what it found. Never throws. Local targets read the store directly. Remote targets read it over PowerShell remoting. The RS surface otherwise needs only CIM (WS-Man), so remoting may be unavailable on a machine where binding still works -- in that case Checked is $false and the caller treats the pre-flight as indeterminate rather than failing the operation. .PARAMETER Thumbprint 40-character hex thumbprint to find. .PARAMETER ComputerName Machine whose LocalMachine\My store is searched. Defaults to the local machine. .PARAMETER Credential Credential for the remote lookup, when the target needs one. .OUTPUTS PSCustomObject: Checked $true when the store was actually read. Found $true when a matching certificate was present. NotAfter expiry of the matched certificate, else $null. Reason why Checked is $false, else ''. Deliberately narrow: only what the callers consume. The module runs Set-StrictMode -Version Latest, so reading a property a certificate object may not carry would throw and be swallowed as an indeterminate result -- which is exactly how a speculative Subject field masked a working lookup during development. .EXAMPLE $r = Get-SqlCertFromTargetStore -Thumbprint $t -ComputerName 'RS01' if ($r.Checked -and -not $r.Found) { 'definitely absent on RS01' } Shows the three-state contract: only treat an absent certificate as a hard error when the store was actually read. .NOTES Steps: 1. Build the store-search scriptblock (thumbprint passed as a bound argument, never interpolated into the script text). 2. Run it locally, or via Invoke-Command against a remote target. 3. On a remoting failure, return Checked = $false with the reason -- never throw, and never report a false "not found". #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'The thumbprint is passed via param() + -ArgumentList, not closure capture; the Using: modifier is not applicable.')] param( [Parameter(Mandatory)] [string] $Thumbprint, [string] $ComputerName = $env:COMPUTERNAME, [System.Management.Automation.PSCredential] $Credential ) # Remote lookups need a scriptblock for Invoke-Command. The LOCAL lookup is # deliberately NOT routed through that scriptblock: calling Get-ChildItem # inline keeps the store read in this function's own scope, which is what # the existing unit tests mock. $sb = { param($Tp) $c = Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue | Where-Object { $_.Thumbprint -ieq $Tp } | Select-Object -First 1 if ($c) { [PSCustomObject]@{ Found = $true; NotAfter = $c.NotAfter } } else { [PSCustomObject]@{ Found = $false; NotAfter = $null } } } try { if ($ComputerName -eq $env:COMPUTERNAME -or $ComputerName -eq 'localhost' -or $ComputerName -eq '.') { $c = Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue | Where-Object { $_.Thumbprint -ieq $Thumbprint } | Select-Object -First 1 $hit = if ($c) { [PSCustomObject]@{ Found = $true; NotAfter = $c.NotAfter } } else { [PSCustomObject]@{ Found = $false; NotAfter = $null } } } else { $icArgs = @{ ComputerName = $ComputerName ScriptBlock = $sb ArgumentList = @($Thumbprint) ErrorAction = 'Stop' } if ($Credential) { $icArgs['Credential'] = $Credential } $hit = Invoke-Command @icArgs } [PSCustomObject]@{ Checked = $true Found = [bool]$hit.Found NotAfter = $hit.NotAfter Reason = '' } } catch { # Remoting unavailable or refused. The RS bind path needs only CIM, so # this must not fail the operation -- report indeterminate and let the # caller fall through to the RS provider's own authoritative error. [PSCustomObject]@{ Checked = $false Found = $false NotAfter = $null Reason = "Certificate pre-flight on '$ComputerName' was skipped: $($_.Exception.Message)" } } } |