The following steps work on Windows PowerShell 5.1. This issue is present on both x86 and x64 versions and can be reproduced on multiple machines.
I can export the certificate successfully using X509 .Net classes in PowerShell 7.
$TestCertificate = New-SelfSignedCertificate -Subject 'TestCertificate' -KeyExportPolicy 'Exportable'
Export-PfxCertificate -Cert $TestCertificate -FilePath .\TestCertificate.pfx -Password (ConvertTo-SecureString 'TestPassword' -AsPlainText -Force)
Creation of PFX file.
Export-PfxCertificate: Cannot export non-exportable private key.
Name Value
---- -----
PSVersion 7.0.0
PSEdition Core
GitCommitId 7.0.0
OS Microsoft Windows 10.0.18363
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
/cc @anmenaga for information
Looks like this is run on a system with an older version of PKI module.
This older version will engage WinCompat feature and each certificate cmdlet in repro steps will be doing PS Remoting Serialization twice.
Looks like X509Certificate2 is losing its PrivateKey when going over Serialization boundary.
Here is a repro in Windows PowerShell 5.1 with WinRM Remoting session:
PS C:\> $PSVersionTable.PSVersion.ToString()
5.1.14393.3471
PS C:\> $s = New-PSSession localhost
PS C:\> # original X509Certificate2 in remote session has PrivateKey
PS C:\> icm $s {(New-SelfSignedCertificate -Subject 'TestCertFromWinPS' -KeyExportPolicy 'Exportable').HasPrivateKey}
True
PS C:\> # deserialized local copy does Not have PrivateKey
PS C:\> $TestCertificate = icm $s {New-SelfSignedCertificate -Subject 'TestCertFromWinPS' -KeyExportPolicy 'Exportable'}
PS C:\> $TestCertificate.HasPrivateKey
False
PS C:\> # attempt to export deserialized local copy of X509Certificate2 will fail
PS C:\> Export-PfxCertificate -Cert $TestCertificate -FilePath .\TestCertificate.pfx -Password (ConvertTo-SecureString 'Test
Password' -AsPlainText -Force)
Export-PfxCertificate : Cannot export non-exportable private key.
At line:1 char:1
+ Export-PfxCertificate -Cert $TestCertificate -FilePath .\TestCertific ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Export-PfxCertificate], Win32Exception
+ FullyQualifiedErrorId : System.ComponentModel.Win32Exception,Microsoft.CertificateServices.Commands.ExportPfxCertifi
cate
As a workaround in PS 7 the entire operation can be done in WinCompat session:
PS C:\A> $PSVersionTable.PSVersion.ToString()
7.0.0
PS C:\A> Import-Module PKI -UseWindowsPowerShell # this will create WinPSCompatSesion PS Remoting session
WARNING: Module PKI is loaded in Windows PowerShell using WinPSCompatSession remoting session; please note that all input and output of commands from this module will be deserialized objects. If you want to load this module into PowerShell Core please use 'Import-Module -SkipEditionCheck' syntax.
PS C:\A> $s = Get-PSSession -Name WinPSCompatSession
PS C:\A> icm $s {$TestCertificate = New-SelfSignedCertificate -Subject 'TestCertificate' -KeyExportPolicy 'Exportable'}
PS C:\A> $pass = ConvertTo-SecureString 'TestPassword' -AsPlainText -Force
PS C:\A> icm $s {Export-PfxCertificate -Cert $TestCertificate -FilePath .\TestCertificate.pfx -Password $using:pass | Out-Null}
@PaulHigin can you please confirm that this is expected behavior of private keys for de/serialized certificate objects? Thank you.
Yes, looking at the code, PowerShell remoting rehydrates the dotNet certificate object but without the private key.