Exporting a non-exportable private key from the Windows Certificate Store



Last modified on September 20, 2026

I recently switched laptops while traveling, and I had to migrate a client certificate, used for mTLS, that I had saved in the Windows Certificate Store. Of course, when I imported it initially, I didn’t select “Mark this key as exportable”, and Windows was (logically) refusing to let me export it now. While I could have waited to get back home, where I stored an offline copy of the private key, I was ready to spend some time to find if there was a way to do it sooner.

After a quick Kagi search, I found many solutions, especially on the usual knowledge sources: Stack Overflow and Super User. After excluding the stupid responses, I tried the remaining solutions: mimikatz/ mimicertz, exportrsa, looking for the private key in the registry. Nothing worked.

It turns out my certificate’s private key was not CAPI-backed, but CNG-backed, which is the newer stack (“Cryptography API: Next Generation”). If you want to check which stack a certificate uses, certutil -store -user My lists the provider for each one.

CNG is supported (in theory) by exportrsa, and the accompanying paper describes how to modify the flag in-memory and which API to call to export the key.

With a bit of help from Claude, I ended up with the following PowerShell script that does just the same: list the certificates with a private-key, present them to the user for selection, load the private key in memory, set the export policy to NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG and export it as PKCS#8. It worked for my private key, mission accomplished!

powershell Exporting a CNG-backed private key as PKCS#8 PEM
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# Prints the private key of a certificate from the current user's store as an
# unencrypted PEM (PKCS#8). The key must be CNG-backed.

# Add-Type compiles inline C# with [DllImport] P/Invoke stubs, exposing native
# Win32 ncrypt.dll (CNG) calls to PowerShell.
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class NCrypt {
    // https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptsetproperty
    [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
    public static extern int NCryptSetProperty(IntPtr hObject, string pszProperty,
        byte[] pbInput, int cbInput, int dwFlags);

    // https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptexportkey
    [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
    public static extern int NCryptExportKey(
        IntPtr hKey, IntPtr hExportKey, string pszBlobType,
        IntPtr pParameterList, byte[] pbOutput, int cbOutput,
        out int pcbResult, int dwFlags);

    public const string EXPORT_POLICY = "Export Policy";
    public const string PKCS8_BLOB    = "PKCS8_PRIVATEKEY";
    public static readonly byte[] ALLOW = BitConverter.GetBytes(3); // ALLOW_EXPORT | ALLOW_PLAINTEXT_EXPORT
}
"@

function ConvertToPem {
    param([byte[]]$Der)
    $b64 = [Convert]::ToBase64String($Der)
    $lines = for ($i = 0; $i -lt $b64.Length; $i += 64) {
        $b64.Substring($i, [Math]::Min(64, $b64.Length - $i))
    }
    "-----BEGIN PRIVATE KEY-----`n" + ($lines -join "`n") + "`n-----END PRIVATE KEY-----`n"
}

# Pick a certificate that has a private key
$cert = Get-ChildItem Cert:\CurrentUser\My |
        Where-Object { $_.HasPrivateKey } |
        Select-Object Subject, Thumbprint |
        Out-GridView -PassThru | # This will show all certificates found, asking the user to select one
        ForEach-Object { Get-ChildItem "Cert:\CurrentUser\My\$($_.Thumbprint)" }

if (-not $cert) { throw 'No certificate selected.' }

$cngKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
if (-not $cngKey.Key) { throw 'The selected key is not CNG-backed; this export method does not apply.' }
$hKey = $cngKey.Key.Handle.DangerousGetHandle()

# Flip the in-memory export policy to allow plaintext export
[NCrypt]::NCryptSetProperty($hKey, [NCrypt]::EXPORT_POLICY, [NCrypt]::ALLOW, 4, 0) | Out-Null

# The private key can now be exported, let's find its size
$keySize = 0
[NCrypt]::NCryptExportKey($hKey, [IntPtr]::Zero, [NCrypt]::PKCS8_BLOB, [IntPtr]::Zero, $null, 0, [ref]$keySize, 0) | Out-Null

# And perform the actual export now
$keyBytes = New-Object byte[] $keySize
$result   = [NCrypt]::NCryptExportKey($hKey, [IntPtr]::Zero, [NCrypt]::PKCS8_BLOB, [IntPtr]::Zero, $keyBytes, $keySize, [ref]$keySize, 0)
if ($result -ne 0) {throw ("NCryptExportKey failed: 0x{0:X8}" -f $result) }

Write-Host "`n# PKCS#8 private key for: $($cert.Subject)`n" -ForegroundColor Cyan
Write-Host (ConvertToPem $keyBytes)

A few things are worth keeping in mind: