In previous post I talked about weirdness in CNG support in .NET and showed an example how to fill the gaps in .NET. That was just an example. Today I will show how to perform basic cryptographic operations with CNG in PowerShell and other CLR languages (C#, VB.NET). PowerShell is built on top of CLR, so the techniques are almost identical, despite a bit different syntax.

So, today I will show how to sign the data with CNG certificate. The easiest way is to use NCrypt* unmanaged function family exposed by NCRYPT.DLL.

.NET contains a ECDsaCng class which is a wrapper over NCrypt* functions. However, it is not easy to instantiate this object by using X509Certificate2 object. If you read my previous post, you may notice the difficulties to obtain provider and key container information. Therefore, it will require a lot of unmanaged functions calls to get the ECDsaCng object, so fully unmanaged approach will be easier and less code involved than managed approach (and fully managed approach is not possible in the current .NET 4.5.

I want to provider an easier way. Not all developers like p/invoke and will write tons of managed code to accomplish the same. Since, I’m not a developer, this way is acceptable for me as long as it solves the goal.

At first, let’s recap what is the signing process? This process consist of the following actions:

  1. Calculate cryptographic hash over the data to be signed
  2. Sign the hash with a certificate’s private key.
  3. Use certificate’s public key to verify signature.

In overall we will use only three (excluding cleanup extra function) unmanaged functions:

  1. CryptAcquireCertificatePrivateKey – this function is used to obtain a handle to a private key (key pair). In addition, this function returns a hint which denotes what type of key is retrieved: legacy CSP or CNG/KSP (key storage provider). And note that this function allows us to acquire private key associated (if exist) with a particular certificate. This is very useful feature.
  2. NCryptSignHash – this function signs the hash with a private key.
  3. NCryptVerifySignature – this function is used to verify the signature.
  4. NCryptFreeObject – this function is used to free cryptographic handles.

So, we can start with unmanaged function definitions:

$signature = @"
[DllImport("crypt32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CryptAcquireCertificatePrivateKey(
    IntPtr pCert,
    int dwFlags,
    IntPtr pvReserved,
    ref IntPtr phCryptProv,
    ref uint pdwKeySpec,
    ref bool pfCallerFreeProv
);
[DllImport("ncrypt.dll", SetLastError=false)]
public static extern int NCryptSignHash(
    IntPtr hKey,
    IntPtr pPaddingInfo,
    [MarshalAs(UnmanagedType.LPArray)]
    byte[] pbHashValue,
    int cbHashValue,
    [MarshalAs(UnmanagedType.LPArray)]
    byte[] pbSignature,
    int cbSignature,
    ref int pcbResult,
    int dwFlags
);
[DllImport("ncrypt.dll", SetLastError=false)]
public static extern int NCryptVerifySignature(
    IntPtr hKey,
    IntPtr pPaddingInfo,
    [MarshalAs(UnmanagedType.LPArray)]
    byte[] pbHashValue,
    int cbHashValue,
    [MarshalAs(UnmanagedType.LPArray)]
    byte[] pbSignature,
    int cbSignature,
    int dwFlags
);
[DllImport("ncrypt.dll",  SetLastError=false)]
public static extern int NCryptFreeObject(
    IntPtr hObject
);
"@
Add-Type -MemberDefinition $signature -Namespace PKI -Name PfxTools

Now we need to pick a ECC/CNG certificate. Say, it is installed in the personal store:

$cert = gi cert:\CurrentUser\My\85CB30CE8B0B49F0FD8D51FA4461D48F097F7B72

In a current example we will sign the certificate itself (we do not care that it is already signed), so we have a data to sign:

$hasher = [Security.Cryptography.SHA256]::Create()
[Byte[]]$hash = $hasher.ComputeHash($cert.RawData)
$hasher.Dispose()

$hash stores a SHA256 hash value and it’s length is 256 bits. Now we need to acquire private key context:

# initialize variables
$CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG = 0x00010000 # from Wincrypt.h header file
$phCryptProv = [IntPtr]::Zero
$pdwKeySpec = 0
$pfCallerFreeProv = $false
# call the CryptAcquireCertificatePrivateKey function to get the handle to a private key
[PKI.PfxTools]::CryptAcquireCertificatePrivateKey(
    $cert.Handle,
    $CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG,
    0,
    [ref]$phCryptProv,
    [ref]$pdwKeySpec,
    [ref]$pfCallerFreeProv
)

By default, the function attempts to retrieve private key from legacy CSP. In order to allow CNG key storage providers, we need to pass CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG flag in the dwFlags parameter.

PS C:\> $CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG = 0x00010000 # from Wincrypt.h header file
PS C:\> $phCryptProv = [IntPtr]::Zero
PS C:\> $pdwKeySpec = 0
PS C:\> $pfCallerFreeProv = $false
PS C:\> [PKI.PfxTools]::CryptAcquireCertificatePrivateKey(
>> $cert.Handle,
>> $CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG,
>> 0,
>> [ref]$phCryptProv,
>> [ref]$pdwKeySpec,
>> [ref]$pfCallerFreeProv
>> )
>>
True
PS C:\> # $pdwKeySpec must be [UInt32]::MaxValue
PS C:\> $pdwKeySpec
4294967295
PS C:\> $pdwKeySpec -eq [UInt32]::MaxValue
True
PS C:\>

The function succeeded and $pdwKeySpec returned UInt32.MaxValue, or 0xFFFFFF. This value indicates that $phCryptProv contains pointer of NCRYPT_KEY_HANDLE type. This type is the only private key type supported by CNG. Ok, now we can sign the data:

# initialize variable
$pcbResult = 0
# call NCryptSignHash function by passing private key handle and hash data
[PKI.PfxTools]::NCryptSignHash($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$null,$pcbResult,[ref]$pcbResult,0)
# display how many bytes required to store the signature
Write-Host pcbResult: $pcbResult
# allocate managed buffer to store signature
$pbSignature = New-Object byte[] -ArgumentList $pcbResult
# call NCryptSignHash function by passing allocated buffer to store signature.
# signature will be computed and written to $pbSignature only during this call
[PKI.PfxTools]::NCryptSignHash($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.length,[ref]$pcbResult,0)

As you can see, we need to call NCryptSignHash function twice. Once again, it is due to Retrieving Data of Unknown Length. First call calculates the size required to store signature value. The caller is responsible to create large enough buffer and pass it to the next function call. Both calls should return zero (0), which indicates that the function succeeded.

PS C:\> $pcbResult = 0
PS C:\> [PKI.PfxTools]::NCryptSignHash($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$null,$pcbResult,[ref]$pcbResult,0
)
0
PS C:\> Write-Host pcbResult: $pcbResult
pcbResult: 64
PS C:\> $pbSignature = New-Object byte[] -ArgumentList $pcbResult
PS C:\> [PKI.PfxTools]::NCryptSignHash($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.length,[
ref]$pcbResult,0)
0
PS C:\>

We see that both function calls succeeded and signature size is 64 bytes. I forgot to mention, my certificate uses ECC public key with ECDSA_P256 curve. Therefore, resulting signature will be named: sha256ecdsa:

PS C:\> [System.Security.Cryptography.Oid]"sha256ecdsa"

Value                                                       FriendlyName
-----                                                       ------------
1.2.840.10045.4.3.2                                         sha256ECDSA


PS C:\>

Actually, ECDSA signature consist of two integers, called r and s. So, in order to encode the signature with ASN.1 DER encoding, you will have to divide the signature by two and encode each part with integer tag.

Ok, we signed data, but what about integrity? We need to verify the signature:

# function requires single call. Return value indicates the status
[PKI.PfxTools]::NCryptVerifySignature($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.length,0)
PS C:\> [PKI.PfxTools]::NCryptVerifySignature($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.l
ength,0)
0
PS C:\>

The function returned zero, which means valid signature. If we modify at least one byte in the signature:

PS C:\> $pbSignature[5] = 0
PS C:\> [PKI.PfxTools]::NCryptVerifySignature($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.l
ength,0)
-2146893818
PS C:\> Get-ErrorMessage -2146893818
Invalid Signature.
PS C:\>

Feel the difference, no fooling :)

And the last, release private key handle when completed:

[PKI.PfxTools]::NCryptFreeObject($phCryptProv)

Trivia

I almost never asked questions to my readers (if they exist), but give a try by asking a trivia question: when you call NCryptSignHash against the same hash value and by using the same key, resulting signature will never be the same:

PS C:\> [PKI.PfxTools]::NCryptSignHash($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.length,[
ref]$pcbResult,0)
0
PS C:\> $pbSignature[0..5]
103
42
38
189
93
6
PS C:\> [PKI.PfxTools]::NCryptSignHash($phCryptProv,[IntPtr]::Zero,$hash,$hash.length,$pbSignature,$pbSignature.length,[
ref]$pcbResult,0)
0
PS C:\> $pbSignature[0..5]
17
107
245
170
52
243
PS C:\>

The trivia question: why signature each time is different? Good luck and thanks for attention! :)


Share this article:

Comments:

Carl

Guess but does the signature use a timestamp server when called ?

Vadims Podans

Ecdsa is using random padding. That is why the signature is different every time. It is not related to timestamping service, because it is non involved here. Timestamping is a separate procedure.

Istvan Zollai

First, I'd like to thanks for your example. I must work in .NET Framework 3.5, and your PowerShell example converted to C# equivalent, helped me a lot, because I've never used NCrypt native libraries before.

I found a problem however, that I can't resolve. When I try to use ECDsa key from USB token through the certificate store, all the parts of your example gives same result, only the verifrication returns whith NTE_NOT_SUPPORTED. If I use a software key imported to CertStore from PFX, verification succeeds, but I have to use hardware keys. Could you help, what am I missing?


Post your comment:

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