Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save JohnLBevan/57cd36de0f5fd5e6b629c21b58e0a992 to your computer and use it in GitHub Desktop.
Save JohnLBevan/57cd36de0f5fd5e6b629c21b58e0a992 to your computer and use it in GitHub Desktop.
Fetches a certificate from Azure Key Vault and exports its full chain (client>intermediate>root) and private key to a PFX with a private key export password.
Function Export-AzKeyVaultCertificateToPfx {
[CmdletBinding()]
Param (
[Parameter(Mandatory)]
[string]$Subscription
,
[Parameter(Mandatory)]
[string]$VaultName
,
[Parameter(Mandatory)]
[string]$CertificateName
,
[Parameter(Mandatory)]
[string]$Path
,
[Parameter(Mandatory)]
[SecureString]$Password
)
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]$privateKeyIsExportable = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
[Security.Cryptography.X509Certificates.X509ContentType]$pfxFormat = [Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12
$originalContext = Get-AzContext
Set-AzContext -Subscription $Subscription | Out-Null
try {
# fetch the cert info from KV
[string]$certSecret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $CertificateName -AsPlainText -ErrorAction Stop
[byte[]]$certBytes = [Convert]::FromBase64String($certSecret)
# note: A lot of the below can be avoided via `$fullChain.Import($certBytes, $null, $privateKeyIsExportable)`... though that only gets part of the chain, and doesn't guarantee the correct order.
[System.Security.Cryptography.X509Certificates.X509Certificate2]$clientCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certBytes, $null, $privateKeyIsExportable)
# Build the full chain
[System.Security.Cryptography.X509Certificates.X509Chain]$chainBuilder = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chainBuilder.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
if (!$chainBuilder.Build($clientCert)) {
throw 'Error building certificate chain: certificate is invalid'
}
[System.Security.Cryptography.X509Certificates.X509Certificate2Collection]$fullChain = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new()
$certs = $chainBuilder.ChainElements | Select-Object -ExpandProperty 'Certificate' | Sort-Object 'NotAfter' -Descending # hacky way to ensure our certs are in order; works on the assumption that a cert will never expire before its issuer; could improve this by comparing subject vs issuer thumbprints, but for my needs the quick and dirty ws good enough. For a more complex version, see https://github.com/PKISharp/ACME-PS/pull/129/files. Note; we load the certs in reverse order!
foreach ($cert in $certs) {
Write-Verbose "Processing $($cert.Subject) by $($cert.Issuer)"
if ($cert.Thumbprint -eq $clientCert.Thumbprint) {
$fullChain.Add($clientCert) | Out-Null # add this one rather than the one from our chain, as this one contains the private key
} else {
$fullChain.Add($cert) | Out-Null
}
}
# export to file
[byte[]]$pfxBytes = $fullChain.Export($pfxFormat, ($Password | ConvertFrom-SecureString -AsPlainText)) # Note: using the SecureString parameter of this method seemed to have a bug, whilst converting to plaintext avoided it
[System.IO.File]::WriteAllBytes($Path, $pfxBytes)
} finally {
# revert to original subscription to avoid causing side effects
Set-AzContext -Subscription ($originalContext.Subscription) | Out-Null
}
}
# Example Usage:
# Export-AzKeyVaultCertificateToPfx -Subscription 'MySubscription' -VaultName 'MyKeyVault' -CertificateName 'MyCertificate' -Path 'c:\temp\certs\myCertificate.pfx' -Password ('demoN0t$ecure' | ConvertTo-SecureString -AsPlainText -Force) -Verbose
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment