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
Carl 16.12.2014 03:55 (GMT+2) Retrieve CNG key container name and unique name

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
Vadims Podans 16.12.2014 05:09 (GMT+2) Retrieve CNG key container name and unique 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.

Scott
Scott 22.01.2017 23:48 (GMT+2) Retrieve CNG key container name and unique name

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
David Homer 24.01.2017 18:30 (GMT+2) Retrieve CNG key container name and unique name

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
Vadims Podāns 24.01.2017 20:01 (GMT+2) Retrieve CNG key container name and unique name

> 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
Shawn Sesna 22.08.2018 19:56 (GMT+2) Retrieve CNG key container name and unique name

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)


Post your comment:

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