Foreword

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!

About the problem

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.

The solution

At this point we have to move on and get advantage of unmanaged functions which don’t such limitation. In short, we need to:

  1. Extract certificate property that contains information about cryptographic provider that protects the private key. Unique container name is not available here.
  2. Open CNG key storage provider and open key name. Key name will be returned in previous step.
  3. Read attached to key property that holds unique container name.

If we talk about function we need to call, then we will need:

  • CertGetCertificateContextProperty – this function returns key provider that protects private key. We will use this information for subsequent NCrypt* function calls.
  • NCryptOpenStorageProvider – this function is used to open a handle to a key storage provider that protects our private key. Provider handle will be used in the next function call.
  • NCryptOpenKey – this function is used to open the key from a provider obtained in a previous step. Key name to open is returned by calling CertGetCertificateContextProperty function.
  • NCryptGetProperty – this function will read requested “unique container name” property from a key property.

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)

Afterword

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.


Share this article:

Comments:

Carl

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""" }

Vadims Podans

> 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.

Scott

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

 

David Homer

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

Vadims Podāns

> 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.

Shawn Sesna

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)

sebus

The last chunk of 19 line code does not actually output ContainerName

so jus slot in $keyProv

AndrePKI

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?

Vadims Podāns

> 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.

Juris

Very insightful article, Vadims! Learnt a big deal. Thank you.

Mayank Bansal

Is it possible to rettrive the key container name in the KSP provider, without going to certificate route?

Vadims Podāns

It is possible if you know how to identify the right key.

Bob

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.
 

Vadims Podāns

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.

Bob

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? :/ 

Bob

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.

Robert Praetorius

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)

Vadims Podāns

The blog post was written when no such wrappers existed in .NET.

Amal

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.

 

Vadims Podāns

@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.

Amal

@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.

 

 

Vadims Podāns

@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.

Amal

@Vadims

I did, but there is no help.
Anyway, thanks and keep up the good work.

Amal

@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?

 

Amal

@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.

Amal

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:

Please, solve this little equation and enter result below. Captcha