#requires -Version 5.1 <# .SYNOPSIS Print the SPKI SHA-256 pin of a ShopDB server certificate, for building the Set-ShopDBServerPins.ps1 entry. .DESCRIPTION The pin is the lowercase hex SHA-256 of the leaf certificate's SubjectPublicKeyInfo: the same value the Server Certificate settings page shows and the endpoint agent compares. It depends only on the key pair, so a certificate renewed with the same key keeps its pin. Give it either a URL (the certificate the server actually presents is read from the TLS handshake; it is NOT trusted or validated, only hashed) or a certificate file (.cer, .crt, .pem, DER or PEM). Use the file form to pin a new certificate BEFORE it goes live on the server. Reading from a URL trusts the network path at that moment. Compare the printed pin with the one on the Server Certificate settings page before rolling it out. .PARAMETER Url https://[:port]. Only the host and port are used. .PARAMETER Path A certificate file. .EXAMPLE .\Get-ShopDBServerPin.ps1 -Url https:// .EXAMPLE .\Get-ShopDBServerPin.ps1 -Path .\server.crt #> [CmdletBinding(DefaultParameterSetName = 'Url')] param( [Parameter(Mandatory = $true, ParameterSetName = 'Url', Position = 0)] [string]$Url, [Parameter(Mandatory = $true, ParameterSetName = 'Path')] [string]$Path, [int]$TimeoutSec = 15 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $source = @' using System; using System.IO; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; public static class ShopDbServerPinReader { public static byte[] FetchLeafCertificate(string host, int port, int timeoutMilliseconds) { using (TcpClient client = new TcpClient()) { IAsyncResult pending = client.BeginConnect(host, port, null, null); if (!pending.AsyncWaitHandle.WaitOne(timeoutMilliseconds)) { throw new TimeoutException("Timed out connecting to " + host + ":" + port); } client.EndConnect(pending); client.ReceiveTimeout = timeoutMilliseconds; client.SendTimeout = timeoutMilliseconds; byte[] captured = null; RemoteCertificateValidationCallback capture = delegate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { if (certificate != null) { captured = certificate.GetRawCertData(); } return true; }; using (SslStream ssl = new SslStream(client.GetStream(), false, capture)) { ssl.AuthenticateAsClient(host, null, SslProtocols.Tls12, false); } if (captured == null) { throw new InvalidOperationException("The server presented no certificate"); } return captured; } } // Walks the DER: Certificate > TBSCertificate > [0] version (optional), // serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo. public static string SpkiSha256Hex(byte[] der) { int length; int position = EnterSequence(der, 0, out length); position = EnterSequence(der, position, out length); if (der[position] == 0xA0) { position = Skip(der, position); } for (int field = 0; field < 5; field++) { position = Skip(der, position); } if (der[position] != 0x30) { throw new FormatException("SubjectPublicKeyInfo not found"); } int end = Skip(der, position); using (SHA256 sha = SHA256.Create()) { byte[] hash = sha.ComputeHash(der, position, end - position); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } } private static int ReadLength(byte[] der, int position, out int length) { int first = der[position]; if (first < 0x80) { length = first; return position + 1; } int count = first & 0x7F; if (count == 0 || count > 4) { throw new FormatException("Unsupported DER length"); } length = 0; for (int i = 1; i <= count; i++) { length = (length << 8) | der[position + i]; } return position + 1 + count; } private static int EnterSequence(byte[] der, int position, out int length) { if (der[position] != 0x30) { throw new FormatException("Expected SEQUENCE"); } return ReadLength(der, position + 1, out length); } private static int Skip(byte[] der, int position) { int length; int content = ReadLength(der, position + 1, out length); return content + length; } } '@ if (-not ('ShopDbServerPinReader' -as [type])) { Add-Type -TypeDefinition $source } try { if ($PSCmdlet.ParameterSetName -eq 'Path') { $resolved = (Resolve-Path -LiteralPath $Path).ProviderPath $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $resolved $der = $certificate.RawData $label = "$resolved (subject $($certificate.Subject), expires $($certificate.NotAfter.ToString('yyyy-MM-dd')))" } else { $uri = [Uri]$Url if ($uri.Scheme -ne 'https') { throw "The URL must be https ($Url)" } $der = [ShopDbServerPinReader]::FetchLeafCertificate($uri.Host, $uri.Port, $TimeoutSec * 1000) $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList (,$der) $label = "$($uri.Host):$($uri.Port) (subject $($certificate.Subject), expires $($certificate.NotAfter.ToString('yyyy-MM-dd')))" } $pin = [ShopDbServerPinReader]::SpkiSha256Hex($der) } catch { Write-Error "Could not read a certificate: $($_.Exception.Message)" exit 1 } Write-Host "Certificate: $label" Write-Host "Pin (SPKI SHA-256):" Write-Output $pin exit 0