Created
June 30, 2025 20:21
-
-
Save jborean93/24e346156fa4d19d66361b4cd27545d5 to your computer and use it in GitHub Desktop.
Imports a PEM encoded RSA private key in PowerShell, supports PowerShell 5.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright: (c) 2025, Jordan Borean (@jborean93) <[email protected]> | |
# MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
using namespace System.IO | |
using namespace System.Management.Automation | |
using namespace System.Net | |
using namespace System.Security.Cryptography | |
Function Import-PemEncodedRsaKey { | |
<# | |
.SYNOPSIS | |
Imports a PEM encoded RSA key file. | |
.DESCRIPTION | |
Imports the RSA key located in a PEM encoded file. This cmdlets supports | |
the following key formats: | |
PKCS#1 - '-----BEGIN RSA PRIVATE KEY-----' | |
PKCS#8 - '-----BEGIN PRIVATE KEY-----' | |
Encrypted PKCS#8 private keys are also supported on PowerShell 7.x using | |
the -Password parameter. | |
.PARAMETER Path | |
The path to the PEM encoded RSA key file. This supports wildcard characters, | |
use -LiteralPath to specify a path without any wildcard expansion. | |
.PARAMETER LiteralPath | |
The path to the PEM encoded RSA key file without any wildcard expansion. | |
.PARAMETER Password | |
Password to use for decrypting a PKCS#8 encrypted key file. If no password | |
is specified and a key is encrypted, pwsh will prompt for the key password. | |
This parameter is only supported on PowerShell 7.x and only for PKCS#8 | |
keys. | |
.EXAMPLE | |
An example | |
#> | |
[OutputType([RSA])] | |
[CmdletBinding(DefaultParameterSetName = 'Path')] | |
param ( | |
[Parameter( | |
Mandatory = $true, | |
Position = 0, | |
ValueFromPipeline = $true, | |
ValueFromPipelineByPropertyName = $true, | |
ParameterSetName = "Path" | |
)] | |
[SupportsWildcards()] | |
[ValidateNotNullOrEmpty()] | |
[String[]] | |
$Path, | |
[Parameter( | |
Mandatory = $true, | |
Position = 0, | |
ValueFromPipelineByPropertyName = $true, | |
ParameterSetName = "LiteralPath" | |
)] | |
[Alias('PSPath')] | |
[ValidateNotNullOrEmpty()] | |
[String[]] | |
$LiteralPath, | |
[Parameter( | |
ValueFromPipelineByPropertyName = $true | |
)] | |
[SecureString] | |
$Password | |
) | |
begin { | |
$ErrorActionPreference = 'Stop' | |
} | |
process { | |
$allpaths = if ($PSCmdlet.ParameterSetName -eq 'Path') { | |
$Path | ForEach-Object -Process { | |
$provider = $null | |
try { | |
$PSCmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath($_, [ref]$provider) | |
} | |
catch [System.Management.Automation.ItemNotFoundException] { | |
$PSCmdlet.WriteError($_) | |
} | |
} | |
} | |
elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { | |
$LiteralPath | ForEach-Object -Process { | |
$resolvedPath = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_) | |
if (-not (Test-Path -LiteralPath $resolvedPath)) { | |
$msg = "Cannot find path '$resolvedPath' because it does not exist" | |
$err = [ErrorRecord]::new( | |
[ItemNotFoundException]::new($msg), | |
"PathNotFound", | |
[ErrorCategory]::ObjectNotFound, | |
$resolvedPath) | |
$PSCmdlet.WriteError($err) | |
return | |
} | |
$resolvedPath | |
} | |
} | |
$tempPass = $Password | |
foreach ($filePath in $allPaths) { | |
$keyData = Get-Content -LiteralPath $filePath | |
$keyFormat, $isEncrypted = switch ($keyData[0]) { | |
'-----BEGIN RSA PRIVATE KEY-----' { | |
'PKCS1' | |
'Proc-Type: 4,ENCRYPTED' -in $keyData | |
} | |
'-----BEGIN PRIVATE KEY-----' { 'PKCS8', $false } | |
'-----BEGIN ENCRYPTED PRIVATE KEY-----' { 'PKCS8', $true } | |
default { '', $false } | |
} | |
if (-not $keyFormat) { | |
$msg = "Key at '$filePath' has an unknown format '$($keyData[0])'" | |
$err = [ErrorRecord]::new( | |
[ArgumentException]::new($msg), | |
"UnsupportedKeyFormat", | |
[ErrorCategory]::InvalidData, | |
$filePath) | |
$PSCmdlet.WriteError($err) | |
continue | |
} | |
$keyPassword = $null | |
if ($isEncrypted) { | |
if ($keyFormat -eq 'PKCS1') { | |
# .NET does not support PKCS#1 encryption even in .NET 5+. | |
# We could theoretically decrypt the key data ourselves but | |
# it's weak and people shouldn't be using it in the first | |
# place. | |
$msg = "Cannot decrypt key at '$filePath': PKCS1 encryption is not supported" | |
$err = [ErrorRecord]::new( | |
[ArgumentException]::new($msg), | |
"Pkcs1EncryptKeyUnsupported", | |
[ErrorCategory]::InvalidData, | |
$filePath) | |
$PSCmdlet.WriteError($err) | |
continue | |
} | |
elseif (-not $IsCoreCLR) { | |
$msg = "Encrypted keys are only supported on PowerShell 7+." | |
$err = [ErrorRecord]::new( | |
[ArgumentException]::new($msg), | |
"EncryptionUnsupportedOnWinPS", | |
[ErrorCategory]::InvalidData, | |
$filePath) | |
$PSCmdlet.WriteError($err) | |
continue | |
} | |
if (-not $tempPass) { | |
$tempPass = Read-Host -AsSecureString -Prompt "Enter password for $filePath" | |
} | |
$keyPassword = [NetworkCredential]::new('', $tempPass) | |
} | |
if ($IsCoreCLR) { | |
$rsa = [RSA]::Create() | |
$keyPem = $keyData -join "`n" | |
if ($isEncrypted) { | |
$rsa.ImportFromEncryptedPem($keyPem, $keyPassword.Password) | |
} | |
else { | |
$rsa.ImportFromPem($keyPem) | |
} | |
$rsa | |
} | |
elseif ($keyFormat -eq 'PKCS1') { | |
# .NET Framework and Win32 CNG does not support PKCS#1 blobs so | |
# we need to manually extract the RSA parameters from the ASN.1 | |
# data ourselves. This isn't pretty as the ASN.1 parser in .NET | |
# is also .NET 5+ so not available for WinPS. This is missing | |
# a lot of validation and could be wrong for different numbers | |
$keyB64 = ($keyData | Where-Object { -not $_.StartsWith('-') }) -join "" | |
$keyBytes = [Convert]::FromBase64String($keyB64) | |
$ms = [MemoryStream]::new($keyBytes) | |
$reader = [BinaryReader]::new($ms) | |
try { | |
$readHeader = { | |
$tag = $reader.ReadByte() | |
$length = $reader.ReadByte() | |
if ($length -band 0x80) { | |
$lengthOctets = $length -band 0x7F | |
$length = 0 | |
for ($i = 0; $i -lt $lengthOctets; $i++) { | |
$octet = $reader.ReadByte() | |
$length = ($length -shl 8) -bor $octet | |
} | |
} | |
$tag, $length | |
} | |
$readInteger = { | |
$null, $length = & $readHeader | |
$intBytes = $reader.ReadBytes($length) | |
# Strip off the 0 byte padding if present | |
if ($length -gt 1 -and $intBytes[0] -eq 0) { | |
$intBytes = $intBytes[1..($length - 1)] | |
} | |
, $intBytes | |
} | |
# RSAPrivateKey ::= SEQUENCE { | |
# version Version, | |
# modulus INTEGER, -- n | |
# publicExponent INTEGER, -- e | |
# privateExponent INTEGER, -- d | |
# prime1 INTEGER, -- p | |
# prime2 INTEGER, -- q | |
# exponent1 INTEGER, -- d mod (p-1) | |
# exponent2 INTEGER, -- d mod (q-1) | |
# coefficient INTEGER, -- (inverse of q) mod p | |
# otherPrimeInfos OtherPrimeInfos OPTIONAL | |
# } | |
# Read the outer SEQUENCE and ignore the version field | |
$null = & $readHeader | |
$null, $versionLength = & $readHeader | |
$null = $reader.ReadBytes($versionLength) | |
# Read the remaining INTEGER values | |
$rsaParams = [RSAParameters]@{ | |
Modulus = & $readInteger | |
Exponent = & $readInteger | |
D = & $readInteger | |
P = & $readInteger | |
Q = & $readInteger | |
DP = & $readInteger | |
DQ = & $readInteger | |
InverseQ = & $readInteger | |
} | |
[RSA]::Create($rsaParams) | |
} | |
finally { | |
$reader.Dispose() | |
$ms.Dispose() | |
} | |
} | |
elseif ($keyFormat -eq 'PKCS8') { | |
$keyB64 = ($keyData | Where-Object { -not $_.StartsWith('-') }) -join "" | |
$keyBytes = [Convert]::FromBase64String($keyB64) | |
$cngKey = [CngKey]::Import( | |
$keyBytes, | |
[CngKeyBlobFormat]::Pkcs8PrivateBlob) | |
[RSACng]::new($cngKey) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment