Core/Save-TestCertificate.ps1
|
function Save-TestCertificate { <# .SYNOPSIS Installs a generated certificate into the current user's store, with its private key .DESCRIPTION Exists as its own function for two reasons, and the second is the more important. The first is correctness. A certificate from CertificateRequest.CreateSelfSigned holds an EPHEMERAL private key - it lives in memory attached to that object and is not backed by a key container, so adding the object straight to the store persists only the public half. The result is a certificate with the right thumbprint that cannot sign anything, and the failure appears one connection later as "has no private key". Exporting to PFX and re-importing with PersistKeySet is what actually writes the key somewhere the store can reference it. The second is that this is the only part of the bootstrap that touches the machine rather than the tenant, and a unit test must be able to stop it. Inline X509Store calls cannot be mocked, so a suite exercising the bootstrap would silently install a certificate into the developer's personal store on every run - which is exactly what happened before this function existed, nine times. .PARAMETER Certificate The certificate to install, with its private key attached .OUTPUTS System.Boolean, true when the stored certificate has a usable private key. .EXAMPLE PS> Save-TestCertificate -Certificate $certificate DESCRIPTION: Installs the certificate into Cert:\CurrentUser\My OUTPUT: True USE CASE: Called by New-TestServiceApp after minting the key pair .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ #> [CmdletBinding(SupportsShouldProcess)] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) if (-not $PSCmdlet.ShouldProcess("Cert:\CurrentUser\My", "Install certificate $($Certificate.Thumbprint)")) { return $false } # The password protects the PFX only while it is in memory between these two calls, and # the bytes are cleared immediately afterwards. $transitPassword = New-TestPassword -Length 48 $pfx = $Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $transitPassword) try { $persistable = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( $pfx, $transitPassword, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet) try { $store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'CurrentUser') $store.Open('ReadWrite') try { $store.Add($persistable) } finally { $store.Close() } $usable = $persistable.HasPrivateKey Write-Verbose "Installed $($Certificate.Thumbprint) (private key usable: $usable)" return $usable } finally { $persistable.Dispose() } } finally { [Array]::Clear($pfx, 0, $pfx.Length) } } |