Skip to content

Instantly share code, notes, and snippets.

@ChrisLynchHPE
Created July 31, 2020 17:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ChrisLynchHPE/a99fe45d48f427ef4a45c4d44623f262 to your computer and use it in GitHub Desktop.
Save ChrisLynchHPE/a99fe45d48f427ef4a45c4d44623f262 to your computer and use it in GitHub Desktop.
Add-Type @"
namespace System.Security.Cryptography.X509Certificates {
public enum X509KeySpecFlags {
None = 0,
AT_KEYEXCHANGE = 1,
AT_SIGNATURE = 2
}
}
"@
function Convert-PemToPfx {
[OutputType('[System.Security.Cryptography.X509Certificates.X509Certificate2]')]
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$InputPath,
[string]$KeyPath,
[string]$OutputPath,
[Security.Cryptography.X509Certificates.X509KeySpecFlags]$KeySpec = "AT_KEYEXCHANGE",
[Security.SecureString]$Password,
[string]$ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider",
[Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = "CurrentUser",
[switch]$Install
)
if ($PSBoundParameters.Verbose) {$VerbosePreference = "continue"}
if ($PSBoundParameters.Debug) {
$Host.PrivateData.DebugForegroundColor = "Cyan"
$DebugPreference = "continue"
}
#region helper functions
function __normalizeAsnInteger ($array) {
$padding = $array.Length % 8
if ($padding) {
$array = $array[$padding..($array.Length - 1)]
}
[array]::Reverse($array)
[Byte[]]$array
}
function __extractCert([string]$Text) {
if ($Text -match "(?msx).*-{5}BEGIN\sCERTIFICATE-{5}(.+)-{5}END\sCERTIFICATE-{5}") {
$keyFlags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
if ($Install) {
if ($StoreLocation -eq "CurrentUser") {
$keyFlags = $keyFlags -bor [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet
} else {
$keyFlags = $keyFlags -bor [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet
}
}
$RawData = [Convert]::FromBase64String($matches[1])
try {
New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $RawData, "", $keyFlags
} catch {throw "The data is not valid security certificate."}
Write-Debug "X.509 certificate is correct."
} else {throw "Missing certificate file."}
}
# returns [byte[]]
function __composePRIVATEKEYBLOB($modulus, $PublicExponent, $PrivateExponent, $Prime1, $Prime2, $Exponent1, $Exponent2, $Coefficient) {
Write-Debug "Calculating key length."
$bitLen = "{0:X4}" -f $($modulus.Length * 8)
Write-Debug "Key length is $($modulus.Length * 8) bits."
[byte[]]$bitLen1 = Invoke-Expression 0x$([int]$bitLen.Substring(0,2))
[byte[]]$bitLen2 = Invoke-Expression 0x$([int]$bitLen.Substring(2,2))
[Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32,0x00
[Byte[]]$PrivateKey = $PrivateKey + $bitLen1 + $bitLen2 + $PublicExponent + ,0x00 + `
$modulus + $Prime1 + $Prime2 + $Exponent1 + $Exponent2 + $Coefficient + $PrivateExponent
$PrivateKey
}
# returns RSACryptoServiceProvider for dispose purposes
function __attachPrivateKey($_Cert, [Byte[]]$PrivateKey) {
$cspParams = New-Object Security.Cryptography.CspParameters -Property @{
ProviderName = $ProviderName
KeyContainerName = "pspki-" + [Guid]::NewGuid().ToString()
KeyNumber = [int]$KeySpec
}
if ($Install -and $StoreLocation -eq "LocalMachine") {
$cspParams.Flags = $cspParams.Flags -bor [Security.Cryptography.CspProviderFlags]::UseMachineKeyStore
}
$rsa = New-Object Security.Cryptography.RSACryptoServiceProvider $cspParams
$rsa.ImportCspBlob($PrivateKey)
# This is here due to .NetCore changes to the X509Certificate2 API. The PrivateKey property cannot be set, and getter is only available for backwards compat. (https://github.com/dotnet/runtime/issues/27346)
if ($PSVersionTable.PSEdition -eq "Core") {
[void][Reflection.Assembly]::Load("System.Security.Cryptography.X509Certificates")
$script:Cert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::CopyWithPrivateKey($_Cert.RawData, $rsa)
}
else {
$script:Cert.PrivateKey = $rsa
}
$rsa
}
# returns Asn1Reader
function __decodePkcs1($base64) {
Write-Debug "Processing PKCS#1 RSA KEY module."
$asn = New-Object SysadminsLV.Asn1Parser.Asn1Reader @(,[Convert]::FromBase64String($base64))
if ($asn.Tag -ne 48) {throw "The data is invalid."}
$asn
}
# returns Asn1Reader
function __decodePkcs8($base64) {
Write-Debug "Processing PKCS#8 Private Key module."
$asn = New-Object SysadminsLV.Asn1Parser.Asn1Reader @(,[Convert]::FromBase64String($base64))
if ($asn.Tag -ne 48) {throw "The data is invalid."}
# version
if (!$asn.MoveNext()) {throw "The data is invalid."}
# algorithm identifier
if (!$asn.MoveNext()) {throw "The data is invalid."}
# octet string
if (!$asn.MoveNextCurrentLevel()) {throw "The data is invalid."}
if ($asn.Tag -ne 4) {throw "The data is invalid."}
if (!$asn.MoveNext()) {throw "The data is invalid."}
$asn
}
#endregion
$ErrorActionPreference = "Stop"
$File = Get-Item $InputPath -Force -ErrorAction Stop
if ($KeyPath) {$Key = Get-Item $KeyPath -Force -ErrorAction Stop}
# parse content
$Text = Get-Content -Path $InputPath -Raw -ErrorAction Stop
Write-Debug "Extracting certificate information..."
$script:Cert = __extractCert $Text
if ($Key) {$Text = Get-Content -Path $KeyPath -Raw -ErrorAction Stop}
$asn = if ($Text -match "(?msx).*-{5}BEGIN\sPRIVATE\sKEY-{5}(.+)-{5}END\sPRIVATE\sKEY-{5}") {
__decodePkcs8 $matches[1]
} elseif ($Text -match "(?msx).*-{5}BEGIN\sRSA\sPRIVATE\sKEY-{5}(.+)-{5}END\sRSA\sPRIVATE\sKEY-{5}") {
__decodePkcs1 $matches[1]
} else {throw "The data is invalid."}
# private key version
if (!$asn.MoveNext()) {throw "The data is invalid."}
# modulus n
if (!$asn.MoveNext()) {throw "The data is invalid."}
$modulus = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "Modulus length: $($modulus.Length)"
# public exponent e
if (!$asn.MoveNext()) {throw "The data is invalid."}
# public exponent must be 4 bytes exactly.
$PublicExponent = if ($asn.GetPayload().Length -eq 3) {
,0 + $asn.GetPayload()
} else {
$asn.GetPayload()
}
Write-Debug "PublicExponent length: $($PublicExponent.Length)"
# private exponent d
if (!$asn.MoveNext()) {throw "The data is invalid."}
$PrivateExponent = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "PrivateExponent length: $($PrivateExponent.Length)"
# prime1 p
if (!$asn.MoveNext()) {throw "The data is invalid."}
$Prime1 = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "Prime1 length: $($Prime1.Length)"
# prime2 q
if (!$asn.MoveNext()) {throw "The data is invalid."}
$Prime2 = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "Prime2 length: $($Prime2.Length)"
# exponent1 d mod (p-1)
if (!$asn.MoveNext()) {throw "The data is invalid."}
$Exponent1 = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "Exponent1 length: $($Exponent1.Length)"
# exponent2 d mod (q-1)
if (!$asn.MoveNext()) {throw "The data is invalid."}
$Exponent2 = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "Exponent2 length: $($Exponent2.Length)"
# coefficient (inverse of q) mod p
if (!$asn.MoveNext()) {throw "The data is invalid."}
$Coefficient = __normalizeAsnInteger $asn.GetPayload()
Write-Debug "Coefficient length: $($Coefficient.Length)"
# creating Private Key BLOB structure
$PrivateKey = __composePRIVATEKEYBLOB $modulus $PublicExponent $PrivateExponent $Prime1 $Prime2 $Exponent1 $Exponent2 $Coefficient
#region key attach and export routine
$rsaKey = __attachPrivateKey $Cert $PrivateKey
if (![string]::IsNullOrEmpty($OutputPath)) {
if (!$Password) {
$Password = Read-Host -Prompt "Enter PFX password" -AsSecureString
}
$pfxBytes = $Cert.Export("pfx", $Password)
Set-Content -Path $OutputPath -Value $pfxBytes -Encoding Byte
}
#endregion
if ($Install) {
$store = New-Object Security.Cryptography.X509Certificates.X509Store "my", $StoreLocation
$store.Open("ReadWrite")
$store.Add($Cert)
$store.Close()
}
$rsaKey.Dispose()
$Cert
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment