Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created June 30, 2025 20:21
Show Gist options
  • Save jborean93/24e346156fa4d19d66361b4cd27545d5 to your computer and use it in GitHub Desktop.
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
# 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