Before I will discuss the subject, I want to share my thoughts about the Windows cryptography problems. You can skip this section if you need only solution for the subject.
Cryptography in general is not something new, it is actual for a long time, the problem appeared in very ancient ages. Julius Caesar was one of the notable modern persons who created the problem. It is cryptography. Caesar created so-called Caesar cipher which was enough secure during his life. However, people enough quickly figured out how to decrypt this cipher. Cipher method become more complex to break. For example, middle ages Vigenère cipher was much better than Caesar cipher. There were a lot, but all of them were relatively easy to break. Arthur Scherbius in 20th century invented famous Enigma machine. Americans invented SIGABA which was supposed to fix Enigma’s vulnerability. Time goes forward, cryptography become more complex, stronger against attacks.
In the 2nd half of 20th century started semiconductor computer era which set a new level for application cryptography. Newer processors, more operations per second, more chances to break a cryptoalgorithm that was considered almost unbreakable yesterday. And this process continues constantly.
Many years ago three good guys, Ron Rivest, Adi Shamir and Leonard Adleman invented a cryptographic algorithm (RSA) which become a de facto standard for computer cryptography for many years. Most operating systems implemented cryptographic service providers (CSP) that support this algorithm. As time goes forward, cryptography requires stronger algorithms. Nowadays 512-bit RSA key cannot be considered secure. Even 1024-bit RSA keys are under serious attack. In 2004 computers widely started to use elliptic curve cryptography (ECC) which is stronger than RSA with shorter key lengths.
Microsoft did a huge work by rewriting almost all cryptography platform in Windows operating system family. They introduced ECC support and introduced new key storage provider (KSP) which provides additional security mechanisms, like key isolation. New cryptography is called Cryptography Next Generation (CNG). It is implemented in a set of native C++ functions. However, most modern applications, even clouds, are written in Common Language Runtime (CLR), which is completely different from native functions. The good thing in CLR is interoperability support with native functions implemented via platform invocation (p/invoke) service. Therefore, you can use native functions and their functionality in CLR. However, CNG support in CLR (.NET) is awful. X.509 certificates (X509Certificate2 class) do not support non-RSA public keys and do not support private keys protected by new key storage providers, instead of legacy service providers. As the result, CNG support by client applications is very poor. Cloud products, System Center do not support CNG at all. And the only reason is .NET. An inability to perform basic tasks with CNG in .NET makes CNG almost useless. And it looks like, there are no plans to deliver missing keys. Shame on .NET!
Sometimes you need to get an unique container name of the private key (this name identifies the key file on a file system). For sure, you can use certutil:
PS C:\> certutil -user -store my C541C66F490413302C845A440AFA24E98A231C3C
my "Personal"
================ Certificate 1 ================
Serial Number: 29daadda4a4e3b944bd479a37401bc75
Issuer: CN=CNG test
NotBefore: 20.08.2014. 23:12
NotAfter: 20.08.2015. 23:12
Subject: CN=CNG test
Signature matches Public Key
Root Certificate: Subject matches Issuer
Cert Hash(sha1): c5 41 c6 6f 49 04 13 30 2c 84 5a 44 0a fa 24 e9 8a 23 1c 3c
Key Container = lp-73a773a7-dcf3-489b-afc7-bcdd4048ad8b
Unique container name: 540132813278bee633c18081ca40a5d5_bdeec052-f05f-41fc-87b3-1240aa213252
Provider = microsoft software key storage provider
Private key is NOT exportable
Encryption test passed
CertUtil: -store command completed successfully.
PS C:\>
plain and simple. But what if you need to do this programmatically? Let’s go:
PS C:\> $cert = gi cert:\CurrentUser\My\888CB51FCB7D6915EE4F61A9C741B0663CB503DF PS C:\> $cert | fl * PSPath : Microsoft.PowerShell.Security\Certificate::CurrentUser\My\888CB51FCB7D6915EE4F61A9C741B0663C B503DF PSParentPath : Microsoft.PowerShell.Security\Certificate::CurrentUser\My PSChildName : 888CB51FCB7D6915EE4F61A9C741B0663CB503DF PSDrive : Cert PSProvider : Microsoft.PowerShell.Security\Certificate PSIsContainer : False EnhancedKeyUsageList : {Client Authentication (1.3.6.1.5.5.7.3.2)} DnsNameList : {bb1419a2cfc1e008} SendAsTrustedIssuer : False EnrollmentPolicyEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty EnrollmentServerEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty PolicyId : Archived : False Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptog raphy.Oid, System.Security.Cryptography.Oid...} FriendlyName : IssuerName : System.Security.Cryptography.X509Certificates.X500DistinguishedName NotAfter : 06.07.2014. 12:36:05 NotBefore : 29.06.2014. 12:36:05 HasPrivateKey : True PrivateKey : System.Security.Cryptography.RSACryptoServiceProvider PublicKey : System.Security.Cryptography.X509Certificates.PublicKey RawData : {48, 130, 4, 34...} SerialNumber : 6DB97F8D65F2D27E08B3E44774843A6253956592 SubjectName : System.Security.Cryptography.X509Certificates.X500DistinguishedName SignatureAlgorithm : System.Security.Cryptography.Oid Thumbprint : 888CB51FCB7D6915EE4F61A9C741B0663CB503DF Version : 3 Handle : 156552367984 Issuer : CN=Token Signing Public Key Subject : CN=bb1419a2cfc1e008 PS C:\> $cert.PrivateKey PublicOnly : False CspKeyContainerInfo : System.Security.Cryptography.CspKeyContainerInfo KeySize : 1024 KeyExchangeAlgorithm : RSA-PKCS1-KeyEx SignatureAlgorithm : http://www.w3.org/2000/09/xmldsig#rsa-sha1 PersistKeyInCsp : True LegalKeySizes : {System.Security.Cryptography.KeySizes} PS C:\> $cert.PrivateKey.CspKeyContainerInfo MachineKeyStore : False ProviderName : Microsoft Enhanced Cryptographic Provider v1.0 ProviderType : 1 KeyContainerName : IDENTITYCRL_CERT_CONTAINER_abca8416-2a08-4aba-83f4-b6942e48ab11 UniqueKeyContainerName : 9db4dfa3a5a4f304efcb3fb0603798db_bdeec052-f05f-41fc-87b3-1240aa213252 KeyNumber : Exchange Exportable : True HardwareDevice : False Removable : False Accessible : True Protected : False CryptoKeySecurity : RandomlyGenerated : False PS C:\>
very easy! Yes, it is because, private key of this certificate is stored in a legacy cryptographic service provider. And what about CNG certificate?
PS C:\> $cert = gi cert:\CurrentUser\My\C541C66F490413302C845A440AFA24E98A231C3C
PS C:\> $cert | fl *
PSPath : Microsoft.PowerShell.Security\Certificate::CurrentUser\My\C541C66F490413302C845A440AFA24E98A
231C3C
PSParentPath : Microsoft.PowerShell.Security\Certificate::CurrentUser\My
PSChildName : C541C66F490413302C845A440AFA24E98A231C3C
PSDrive : Cert
PSProvider : Microsoft.PowerShell.Security\Certificate
PSIsContainer : False
EnhancedKeyUsageList : {Code Signing (1.3.6.1.5.5.7.3.3)}
DnsNameList : {CNG test}
SendAsTrustedIssuer : False
EnrollmentPolicyEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
EnrollmentServerEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
PolicyId :
Archived : False
Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptog
raphy.Oid}
FriendlyName :
IssuerName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter : 20.08.2015. 23:12:04
NotBefore : 20.08.2014. 23:12:04
HasPrivateKey : True
PrivateKey :
PublicKey : System.Security.Cryptography.X509Certificates.PublicKey
RawData : {48, 130, 2, 246...}
SerialNumber : 29DAADDA4A4E3B944BD479A37401BC75
SubjectName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
SignatureAlgorithm : System.Security.Cryptography.Oid
Thumbprint : C541C66F490413302C845A440AFA24E98A231C3C
Version : 3
Handle : 156552370800
Issuer : CN=CNG test
Subject : CN=CNG test
PS C:\> $cert.PrivateKey
PS C:\>
And what we see? The certificate has associated private key, but private key property is EMPTY!!! So, we cannot use this certificate directly for decryption and signing operations. In order to use it, we need to use some hacks and tricks.
At this point we have to move on and get advantage of unmanaged functions which don’t such limitation. In short, we need to:
If we talk about function we need to call, then we will need:
So, start with these functions p/invoke definitions:
$signature = @" [DllImport("Crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CertGetCertificateContextProperty( IntPtr pCertContext, uint dwPropId, IntPtr pvData, ref uint pcbData ); [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] public struct CRYPT_KEY_PROV_INFO { [MarshalAs(UnmanagedType.LPWStr)] public string pwszContainerName; [MarshalAs(UnmanagedType.LPWStr)] public string pwszProvName; public uint dwProvType; public uint dwFlags; public uint cProvParam; public IntPtr rgProvParam; public uint dwKeySpec; } [DllImport("ncrypt.dll", SetLastError = true)] public static extern int NCryptOpenStorageProvider( ref IntPtr phProvider, [MarshalAs(UnmanagedType.LPWStr)] string pszProviderName, uint dwFlags ); [DllImport("ncrypt.dll", SetLastError = true)] public static extern int NCryptOpenKey( IntPtr hProvider, ref IntPtr phKey, [MarshalAs(UnmanagedType.LPWStr)] string pszKeyName, uint dwLegacyKeySpec, uint dwFlags ); [DllImport("ncrypt.dll", SetLastError = true)] public static extern int NCryptGetProperty( IntPtr hObject, [MarshalAs(UnmanagedType.LPWStr)] string pszProperty, byte[] pbOutput, int cbOutput, ref int pcbResult, int dwFlags ); [DllImport("ncrypt.dll", CharSet=CharSet.Auto, SetLastError=true)] public static extern int NCryptFreeObject( IntPtr hObject ); "@ Add-Type -MemberDefinition $signature -Namespace PKI -Name Tools
NCryptFreeObject is used to release unmanaged resources after calling NCrypt* functions.
And here is a code that will retrieve key provider information from certificate property.
$CERT_KEY_PROV_INFO_PROP_ID = 0x2 # from Wincrypt.h header file $cert = dir cert:\currentuser\my\C541C66F490413302C845A440AFA24E98A231C3C # initialize variables $pcbData = 0 # get buffer size that will contain provider information [void][PKI.Tools]::CertGetCertificateContextProperty($cert.Handle,$CERT_KEY_PROV_INFO_PROP_ID,[IntPtr]::Zero,[ref]$pcbData) # allocate this buffer in unmanaged memory $pvData = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbData) # call the function again to copy provider information to a pointer. [PKI.Tools]::CertGetCertificateContextProperty($cert.Handle,$CERT_KEY_PROV_INFO_PROP_ID,$pvData,[ref]$pcbData) # copy structure from unmanaged memory to a managed structure $keyProv = [Runtime.InteropServices.Marshal]::PtrToStructure($pvData,[type][PKI.Tools+CRYPT_KEY_PROV_INFO]) # we don't need unmanaged buffer, so release it [Runtime.InteropServices.Marshal]::FreeHGlobal($pvData) # display the key provider information $keyProv
If you are unsure about passed parameters, please, check the function description on MSDN. If you want to understand why we call the function twice, then there is a great article that explains this: Retrieving Data of Unknown Length. And in CryptoAPI functions it is usual to call the function twice. Look at the output:
PS C:\> $CERT_KEY_PROV_INFO_PROP_ID = 0x2 # from Wincrypt.h header file PS C:\> $cert = dir cert:\currentuser\my\C541C66F490413302C845A440AFA24E98A231C3C PS C:\> $pcbData = 0 PS C:\> [void][PKI.Tools]::CertGetCertificateContextProperty($cert.Handle,$CERT_KEY_PROV_INFO_PROP_ID,[IntPtr]::Ze ro,[ref]$pcbData) PS C:\> $pvData = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbData) PS C:\> [void][PKI.Tools]::CertGetCertificateContextProperty($cert.Handle,$CERT_KEY_PROV_INFO_PROP_ID,$pvData,[ref] $pcbData) PS C:\> $keyProv = [Runtime.InteropServices.Marshal]::PtrToStructure($pvData,[type][PKI.Tools+CRYPT_KEY_PROV_INFO] ) PS C:\> [Runtime.InteropServices.Marshal]::FreeHGlobal($pvData) PS C:\> $keyProv pwszContainerName : lp-73a773a7-dcf3-489b-afc7-bcdd4048ad8b pwszProvName : microsoft software key storage provider dwProvType : 0 dwFlags : 0 cProvParam : 0 rgProvParam : 0 dwKeySpec : 0 PS C:\>
Ok, we completed first step. What we see here? We see key name (name that uniquely identifies the key within a cryptographic provider) and provider name that protects the key. Now, we need to open the provider and open the named key:
$phProvider = [IntPtr]::Zero [PKI.Tools]::NCryptOpenStorageProvider([ref]$phProvider,$keyProv.pwszProvName,0) $phKey = [IntPtr]::Zero [PKI.Tools]::NCryptOpenKey($phProvider,[ref]$phKey,$keyProv.pwszContainerName,0,0)
in the first method call we open key storage provider and pass received handle to a second method call to open the key in a user store. If the certificate is installed in the local machine store, the last parameter must be 0x20. After second method call we will have a handle to a private key, the handle is stored in the $phKey variable. Now we need to call NCryptGetProperty to get the “unique container name” property. This function will be called twice:
$pcbResult = 0 # calculate the size of the unique container name [PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$null,0,[ref]$pcbResult,0) # allocate the buffer to store unique container name. $pbOutput = New-Object byte[] -ArgumentList $pcbResult # copy unique container name to a buffer. [PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$pbOutput,$pbOutput.length,[ref]$pcbResult,0)
Returned byte array will contain a unicode string:
PS C:\> $pbOutput[0..5] 53 0 52 0 48 0 PS C:\> [text.encoding]::Unicode.GetString($pboutput) 540132813278bee633c18081ca40a5d5_bdeec052-f05f-41fc-87b3-1240aa213252 PS C:\> # compare unique container name with certutil: PS C:\> certutil -user -store my C541C66F490413302C845A440AFA24E98A231C3C my "Personal" ================ Certificate 1 ================ Serial Number: 29daadda4a4e3b944bd479a37401bc75 Issuer: CN=CNG test NotBefore: 20.08.2014. 23:12 NotAfter: 20.08.2015. 23:12 Subject: CN=CNG test Signature matches Public Key Root Certificate: Subject matches Issuer Cert Hash(sha1): c5 41 c6 6f 49 04 13 30 2c 84 5a 44 0a fa 24 e9 8a 23 1c 3c Key Container = lp-73a773a7-dcf3-489b-afc7-bcdd4048ad8b Unique container name: 540132813278bee633c18081ca40a5d5_bdeec052-f05f-41fc-87b3-1240aa213252 Provider = microsoft software key storage provider Private key is NOT exportable Encryption test passed CertUtil: -store command completed successfully. PS C:\>
from my perspective these values are equals, the one returned by our PowerShell method and the one returned by certutil. Now you can use returned value to locate private key file. And one of the most important: clear unmanaged handles:
# close provider [PKI.Tools]::NCryptFreeObject($phProvider) # close key [PKI.Tools]::NCryptFreeObject($phKey)
As you can see, there are more talks, than actual code. The whole code (excluding unmanaged functions signatures) is just 19 lines:
$CERT_KEY_PROV_INFO_PROP_ID = 0x2 # from Wincrypt.h header file $cert = dir cert:\currentuser\my\C541C66F490413302C845A440AFA24E98A231C3C $pcbData = 0 [void][PKI.Tools]::CertGetCertificateContextProperty($cert.Handle,$CERT_KEY_PROV_INFO_PROP_ID,[IntPtr]::Zero,[ref]$pcbData) $pvData = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbData) [PKI.Tools]::CertGetCertificateContextProperty($cert.Handle,$CERT_KEY_PROV_INFO_PROP_ID,$pvData,[ref]$pcbData) $keyProv = [Runtime.InteropServices.Marshal]::PtrToStructure($pvData,[type][PKI.Tools+CRYPT_KEY_PROV_INFO]) [Runtime.InteropServices.Marshal]::FreeHGlobal($pvData) $phProvider = [IntPtr]::Zero [void][PKI.Tools]::NCryptOpenStorageProvider([ref]$phProvider,$keyProv.pwszProvName,0) $phKey = [IntPtr]::Zero [void][PKI.Tools]::NCryptOpenKey($phProvider,[ref]$phKey,$keyProv.pwszContainerName,0,0) $pcbResult = 0 [void][PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$null,0,[ref]$pcbResult,0) $pbOutput = New-Object byte[] -ArgumentList $pcbResult [void][PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$pbOutput,$pbOutput.length,[ref]$pcbResult,0) [Text.Encoding]::Unicode.GetString($pbOutput) [void][PKI.Tools]::NCryptFreeObject($phProvider) [void][PKI.Tools]::NCryptFreeObject($phKey)
But we did what wasn’t able to do .NET! In the next post I will show how you can sign random data with CNG keys.
HTH.
I had this problem for sometime and struggled to find a solution to it. I do not know enough to write the code you demonstrated here however I managed to find a library in codeplex called CLRSecurity https://clrsecurity.codeplex.com/ In the Security.Cryptography assembly there are some extension methods which you can use to access the CNG container names. e.g. #Does the certificate have a CNG key? [Security.Cryptography.X509Certificates.X509CertificateExtensionMethods]::HasCngKey($Certificate) #Get the private key of a CNG key $privateKey = [Security.Cryptography.X509Certificates.X509Certificate2ExtensionMethods]::GetCngPrivateKey($Certificate) #Get the container name $keyContainerName = $privateKey.UniqueName I then feed the container name and a boolean property indicating whether the container is CNG or not into this function which returns the path to the container to me. Function Get-KeyContainerPath() { [CmdletBinding(PositionalBinding=$false)] Param( [Parameter(Mandatory=$True)][string][ValidateNotNullOrEmpty()] $Name, [Parameter(Mandatory=$True)][boolean] $IsCNG ) If ($IsCNG) { $searchDirectories = @("Microsoft\Crypto\Keys","Microsoft\Crypto\SystemKeys") } else { $searchDirectories = @("Microsoft\Crypto\RSA\MachineKeys","Microsoft\Crypto\RSA\S-1-5-18","Microsoft\Crypto\RSA\S-1-5-19","Crypto\DSS\S-1-5-20") } foreach ($searchDirectory in $searchDirectories) { $machineKeyDirectory = Join-Path -Path $([Environment]::GetFolderPath("CommonApplicationData")) -ChildPath $searchDirectory $privateKeyFile = Get-ChildItem -Path $machineKeyDirectory -Filter $Name -Recurse if ($privateKeyFile -ne $null) { return $privateKeyFile.FullName } } Throw "Cannot find private key file path for key container ""$Name""" }
> Using CLR Security - Security.Cryptography assembly to do the same I'm not owning CLR Security on CodePlex and I didn't knew that they already implemented this. Thanks for pointing this. But in any way, conceptually they do the same.
This looks great. I've been looking for something to give the CNG provider name. I'm really a novice. Since this article was written in 2014 I imagine the approach or API may have changed. My task is conceptually simple but I'm having a hard time gathering enough knowledge to see a simple solution.
Given a certificate thumbprint and a server name I need to return the Signature Algorithm and wether its provider is a CSP or a KSP. I don't really need the provider name for my project but would like to learn how to get that as well. Q: If I can get a X509Certificate2 instance of a certificate then is its provider always a CSP? Q: Is the CNG KSP easier to find than in 2014. I feel like I need to read more. If you could point me to something that will help I would be grateful. Your article here is the first thing I've seen that talks about getting the KSP name. I hope all is well. Here is a link to a question that I posted: https://social.technet.microsoft.com/Forums/en-US/415ba914-333f-437e-8382-cf578ae3147d/i-need-a-ps-script-that-will-return-a-remote-server-certificates-cryptographic-service-provider?forum=ITCG
Thank you,
Scott
Hello, we were looking at this for our network documentation tool which was breaking when reading the Public Key size for certificates.
It seems though that .NET has a new GetECDsaPublicKey() method in .NET 4.6.1 if you can use this version....
MessageBox.Show((cert.GetECDsaPublicKey().KeySize).ToString());
Thanks!
Dave
> Since this article was written in 2014 I imagine the approach or API may have changed
no, it haven't changed. However, as David pointed, with new .NET versions, you can retrieve CNG public/private keys directly from X509Certificate2 object.
The code example shows how to get the value for the Current User certificate store. If you want the Local Machine store, replace
[void][PKI.Tools]::NCryptOpenKey($phProvider,[ref]$phKey,$keyProv.pwszContainerName,0,0)
with
[void][PKI.Tools]::NCryptOpenKey($phProvider,[ref]$phKey,$keyProv.pwszContainerName,0,$keyProv.dwFlags)
The last chunk of 19 line code does not actually output ContainerName
so jus slot in $keyProv
Can you do the reverse? I.e. given a file name in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\ or C:\Users\<name>\AppData\Roaming\Microsoft\Crypto\RSA\<SID>\, find which certificate is associated with this key(container)file? Or is it just a one-way thing?
> Can you do the reverse?
with some degree of accuracy it is possible. The idea is the same: get the container name from a file and enumerate all certificates in the store and check if particular certificate contains key information that points to specified file name. Though, I would go in a bit different way: load the key in provider and extract public key. Again, enumerate certificate in the store and check if there is matching public key in certificate. This is how "certutil -repairstore" works.
Very insightful article, Vadims! Learnt a big deal. Thank you.
Is it possible to rettrive the key container name in the KSP provider, without going to certificate route?
It is possible if you know how to identify the right key.
Really nice article. Although I encountered some problems and don't really know how to fix it.
This is my output for the $keyProv. All good so far.
pwszContainerName : F8DDB365B9386C1490034EC2DDB183EBA201FE8
pwszProvName : Microsoft Base Smart Card Crypto Provider
dwProvType : 1
dwFlags : 0
cProvParam : 0
rgProvParam : 0
dwKeySpec : 1
When I try to open the provider and the named key, I get this result:
$phProvider = [IntPtr]::Zero
[PKI.Tools]::NCryptOpenStorageProvider([ref]$phProvider,$keyProv.pwszProvName,0)
0
$phKey = [IntPtr]::Zero
[PKI.Tools]::NCryptOpenKey($phProvider,[ref]$phKey,$keyProv.pwszContainerName,0,0)
-2146893802
And after that it's almost the same;
$pcbResult = 0
# calculate the size of the unique container name
[PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$null,0,[ref]$pcbResult,0)
-2146893786
# allocate the buffer to store unique container name.
$pbOutput = New-Object byte[] -ArgumentList $pcbResult
# copy unique container name to a buffer.
PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$pbOutput,$pbOutput.length,[ref]$pcbResult,0) -2146893786
I keep getting this negative value (or should I call it an error).
What could I do to progress from here? I really need this unique container name value in order to automate the process of EV Code Signing with this token certificate.
The error says that keyset does not exist. Maybe smart card wasn't inserted or you didn't authenticate on a smart card with PIN.
The smart card is inserted. If i try to sign something with signtool.exe it works. It asks for the PIN and after I enter it, the file is signed. Any idea what else I could try? I was thinking of reissuing the certificate (was wondering if something is wrong with the private key) but I'm not sure if it has to do anything with that.
Got any other ideas I could try? :/
Just reissued the certificate. Again, it works fine with the signtool.exe; But I get the same errors as above, when I try to find the unique container name.
Your post helped me solve a problem with some old PowerShell code that used the old CryptoApi and had quit working. When looking up a couple of other bits that I needed (my situation is slightly different) I discovered that there's a new managed wrapper for the CNG API (which probably didn't exist when you first solved this problem).
I think this chunk of code:
[void][PKI.Tools]::NCryptOpenStorageProvider([ref]$phProvider,$keyProv.pwszProvName,0)
$phKey = [IntPtr]::Zero
[void][PKI.Tools]::NCryptOpenKey($phProvider,[ref]$phKey,$keyProv.pwszContainerName,0,0)
$pcbResult = 0
[void][PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$null,0,[ref]$pcbResult,0)
$pbOutput = New-Object byte[] -ArgumentList $pcbResult
[void][PKI.Tools]::NCryptGetProperty($phKey,"Unique Name",$pbOutput,$pbOutput.length,[ref]$pcbResult,0)
[Text.Encoding]::Unicode.GetString($pbOutput)
[void][PKI.Tools]::NCryptFreeObject($phProvider)
[void][PKI.Tools]::NCryptFreeObject($phKey)
can be replaced with something like
$cngProvider = [System.Security.Cryptography.CngProvider]::new($keyProv.pwszProvName)
$machineKey = [System.Security.Cryptography.CngKeyOpenOptions]::MachineKey # you may not need this
$cngKey = [System.Security.Cryptography.CngKey]::Open($keyProv.pwszContainerName, $cngProvider, $machineKey)
($uniqueName = $cngKey.UniqueName)
$cngKey.Dispose()
(I haven't investigated the certificate part, because I don't need that. But I wouldn't be surprised if this also has a managed wrapper now)
The blog post was written when no such wrappers existed in .NET.
Hi Vadims,
I am facing difficulty when programatically exporting EDCSA certificates from my windows certificate local machine store.
I ran the below powershell commands:
$cert = gi cert:\LocalMachine\My\12e6c44662ec8ed202d716958d8b3e09cdedaaad
$cert | fl *
The output I get is as below:
PSPath : Microsoft.PowerShell.Security\Certificate::LocalMachine\My\12e6c44662ec8ed202d716958d8b3e09cdedaaad
PSParentPath : Microsoft.PowerShell.Security\Certificate::LocalMachine\My
PSChildName : 12e6c44662ec8ed202d716958d8b3e09cdedaaad
PSDrive : Cert
PSProvider : Microsoft.PowerShell.Security\Certificate
PSIsContainer : False
EnhancedKeyUsageList : {}
DnsNameList : {EDCSA}
SendAsTrustedIssuer : False
EnrollmentPolicyEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
EnrollmentServerEndPoint : Microsoft.CertificateServices.Commands.EnrollmentEndPointProperty
PolicyId :
Archived : False
Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid}
FriendlyName :
IssuerName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter : 6/5/2022 7:00:07 PM
NotBefore : 5/6/2022 7:00:07 PM
HasPrivateKey : True
PrivateKey :
PublicKey : System.Security.Cryptography.X509Certificates.PublicKey
RawData : {48, 130, 2, 62...}
SerialNumber : 204FD3003E5E660EB4D0D538D5232701E6E88BE1
SubjectName : System.Security.Cryptography.X509Certificates.X500DistinguishedName
SignatureAlgorithm : System.Security.Cryptography.Oid
Thumbprint : 12E6C44662EC8ED202D716958D8B3E09CDEDAAAD
Version : 3
Handle : 2495051271328
Issuer : E=test@EDCSA, CN=EDCSA, OU=EDCSA, O=EDCSA, L=BLR, S=KTK, C=IN
Subject : E=test@EDCSA, CN=EDCSA, OU=EDCSA, O=EDCSA, L=BLR, S=KTK, C=IN
HasPrivateKey has value True but PrivateKey field is blank.
What could be the issue?
Using the export function of certificate manager UI, I am able to properly export the same.
Also, the certificate works as I have tested using a client and server.
But programatically exporting ECDSA is failing.
I use C++ with WinCrypt APIs for export.
Thanks in advance.
@Amal
> HasPrivateKey has value True but PrivateKey field is blank.
it is expected because X509Certificate2.PrivateKey property supports only legacy CSPs, while ECC keys are stored in modern KSP.
> I use C++ with WinCrypt APIs for export.
You need to use PFXExportCertStoreEx function to export the certificate to PFX using WinCrypt.
@Vadims
I was using PFXExportCertStoreEx for export, but it did not work for ECDSA.
If I call PFXExportCertStoreEx with REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY, the function fails.
If I do not set that flag, PFXExportCertStoreEx works fine but when I call PKCS12_parse API to extract public and private keys, they are returned as NULL.
The PKCS12_parse output parameter for CA certificate has value for but the public and private key parameters are NULL.
Below is a code snippet showing what I tried:
CRYPT_DATA_BLOB pfxBlob;
pfxBlob.pbData = NULL;
pfxBlob.cbData = 0;
DWORD dwFlags = EXPORT_PRIVATE_KEYS | REPORT_NO_PRIVATE_KEY;
PFXExportCertStoreEx(hMemoryStore, &pfxBlob, L"testpassword", 0, dwFlags);
BIO* pBio = NULL;
PKCS12* pPKCS12 = NULL;
X509 *pCert;
EVP_PKEY* pEvpKey = NULL;
STACK_OF(X509) *certs = NULL;
BIO* pBio = BIO_new(BIO_s_mem());
BIO_write(pBio, pfxBlob.pbData, pfxBlob.cbData);
pPKCS12 = d2i_PKCS12_bio(pBio, NULL);
int ret = PKCS12_parse(pPKCS12, "testpassword", &pEvpKey, &pCert, &certs);
ret is > 1 but only certs is valid.
@Amal
your question is outside the scope of the blog post. I would recommend to post your question on developer forums, Microsoft Q&A or StackOverflow.
@Vadims
I did, but there is no help.
Anyway, thanks and keep up the good work.
@Vadmins
One query related to the blog:
I tried NCryptOpenStorageProvider() but it returns only one key. So I think it is for user.
How do I call NCryptOpenStorageProvider() for local machine?
I see the line "If the certificate is installed in the local machine store, the last parameter must be 0x20." in this blog.
Does that mean NCryptOpenStorageProvider([ref]$phProvider,$keyProv.pwszProvName,0x20) will return keys of local machine store?
@Vadims
Please discard my previous comment. I got the hang of it.
In between, I made an observation, the command certutil -csp "Microsoft Software Key Storage Provider" -key retruns NULL when run in normal mode.
The same returns a list of keys, when run in Admin mode.
So I assume my issue is with privileges and not with API usage.
After days of analysis and discussions, finally I was able to identify the root cause. It is related to privileges. If I run with Admin privilege, I can extract keys for ECDSA certificate as well from the Local Machine certificate store.
If you do not intend to use Admin privilege, just take the certificate manager or mmc and select the certificate, take All tasks > Manage Private Keys give privileges as required.
Post your comment:
Comments: