Certificates, part 2: encryption and decryption, and some about the cert store

To do the encryption and decryption with pubic/private keys, you need to start with getting a certificate. The easiest way is to generate a self-signed cert.

The first thing to know is that the cert from the PowerShell command New-SelfSignedCertificate won't work. It hardcodes the wrong crypto provider into the certs it generates. The wrong thing is that it's the provider of the "key store" type, and you won't be able to get the private key out of this certificate. Instead you need to generate the cert with the command makecert.exe that can be downloaded from MSDN as a part of Windows SDK (it will be in a location like C:\Program Files (x86)\Windows Kits\8.1\bin\x64):

makecert.exe -r -pe -a sha1 -n "CN=MyCert" -ss My -sr CurrentUser -len 2048 -sky exchange -sp "Microsoft Enhanced RSA and AES Cryptographic Provider" -sy 24

I've found this recipe somewhere I think on Stackoverflow but I've lost the link to where it is. You can also immediately save the cert together with its private key into a PFX file:

makecert.exe -r -pe -a sha1 -n "CN=MyCert" -ss My -sr CurrentUser -len 2048 -sky exchange -sp "Microsoft Enhanced RSA and AES Cryptographic Provider" -sy 24 MyCert.pfx

There also are options to save the public key separately into a .cer (PKCS#7) file and the private key into a .pvk (PKCS#8) file if you wish.

This certificate will be deposited into the cert store, in its part that PowerShell shows as the pseudo-filesystem directory cert:\CurrentUser\My. If you run the command multiple times, you will see that there are multiple certs with the same name:

PS C:\WINDOWS\system32> dir Cert:\CurrentUser\My\ | ? { $_.Subject -eq "CN=MyCert" }

    Directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\My

Thumbprint Subject
---------- -------
DD9831FBE5DFCF8486BD5148285F361014A55D82 CN=MyCert
667BAE79C659D59C2A6E22FAC769A801EFA04CFB CN=MyCert

That's by design. There may be multiple certificates with the same name (AKA subject) but each certificate is identified by its unique thumbprint, which is a cryptographic hash value of its data. To get a cert object, it must be identified by its thumbprint:

PS C:\WINDOWS\system32> $mycert = get-Item Cert:\CurrentUser\My\DD9831FBE5DFCF8486BD5148285F361014A55D82

We can check that this cert is installed along with its private key:

PS C:\WINDOWS\system32> $mycert.HasPrivateKey
True
PS C:\WINDOWS\system32> $mycert.PrivateKey

PublicOnly : False
CspKeyContainerInfo : System.Security.Cryptography.CspKeyContainerInfo
KeySize : 2048
KeyExchangeAlgorithm : RSA-PKCS1-KeyEx
SignatureAlgorithm : https://www.w3.org/2000/09/xmldsig#rsa-sha1
PersistKeyInCsp : True
LegalKeySizes : {System.Security.Cryptography.KeySizes}

If we were to create a cert with New-SelfSignedCertificate, HasPrivateKey will still be True but PrivateKey will be null, because the applications get no access to the private key of the certs with the key store providers.

If you create a cert from a script, and some certs with the same cert might already exist, how can you find the new one? One way is to remember the thumbprints beforehand and then look for the new ones. To quote from another cmdlet that I'll show in its entirety later:

        $oldThumbs = (dir Cert:\CurrentUser\My | ? { $_.Subject -eq $subj }).Thumbprint
        $out = &"$Makecert" -r -pe -a sha1 -n "$subj" -ss My -sr CurrentUser -len 2048 -sky exchange -sp "Microsoft Enhanced RSA and AES Cryptographic Provider" -sy 24
        if (!$?) {
            throw "makecert.exe failed: $out"
        } else {
            Write-Verbose $out
        }
        $newThumbs = (dir Cert:\CurrentUser\My | ? { $_.Subject -eq $subj }).Thumbprint
        $thumb = @(foreach ($x in $newThumbs) { if ($x -notin $oldThumbs) { $x } })
        if ($thumb.Count -ne 1) {
            throw ("Unable to discover the thumbprint of the newly created certificate, found: " + ($thumb -join ", "))
        }
        $cert = Get-Item "Cert:\CurrentUser\My\$thumb"

The encryption with an asymmetric key is actually a complicated deal. Encrypting with a public key is difficult and generally can only be used to encrypt a small chunk of data. So instead the data is encrypted with a randomly-generated symmetric key, then the symmetric key is encrypted with the public key, and they both are placed together into an "envelope" data structure. There is a class that helps with it. I've found the basic recipe on the Internet and modified it for PowerShell:

function EncryptBytesCms
{
<#
.SYNOPSIS
Encrypt a bunch of bytes on a certificate.

.OUTPUT
A hash table with the element "bytes" containing the encrypted byte array.
#>
    param(
        ## Bytes to encrypt
        [byte[]] $Bytes,
        ## Cert to encrypt with the public key.
        [System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert
    )

    $ci = New-Object System.Security.Cryptography.Pkcs.ContentInfo @(,$Bytes)
    $env = New-Object Security.Cryptography.Pkcs.EnvelopedCms $ci
    $recip = New-Object Security.Cryptography.Pkcs.CmsRecipient $Cert
    $env.Encrypt($recip)
    @{ bytes=$env.Encode(); }
}

function DecryptBytesCms
{
<#
.SYNOPSIS
Decrypt a bunch of bytes on a certificate. The same certificate with the
private key must be already installed locally, the decryption will
find it by the data in the envelope.

.OUTPUT
A hash table with the element "bytes" containing the decrypted byte array.
#>
    param(
        ## Bytes to encrypt
        [byte[]] $Bytes,
        ## Cert to decrypt with the private key.
        [System.Security.Cryptography.X509Certificates.X509Certificate2] $Cert
    )

    $env = New-Object Security.Cryptography.Pkcs.EnvelopedCms
    $env.Decode($Bytes)
    $col = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection $Cert
    $env.Decrypt($col)
    @{ bytes=$env.ContentInfo.Content; }
}

It's used like this:

PS C:\WINDOWS\system32> [byte[]]$bytes = @(1,2,3,4,5)
PS C:\WINDOWS\system32> $bytes
1
2
3
4
5
PS C:\WINDOWS\system32> $enc = EncryptBytesCms -Bytes $bytes -cert $mycert
PS C:\WINDOWS\system32> $enc

Name Value
---- -----
bytes {48, 130, 1, 136...}

PS C:\WINDOWS\system32> $enc.bytes.Count
396
PS C:\WINDOWS\system32> $dec = DecryptBytesCms -Bytes $enc.bytes
PS C:\WINDOWS\system32> $dec.bytes
1
2
3
4
5

This encrypted a bunch of bytes and then decrypted them back. You can see that 5 bytes of the data became 396 bytes in the encrypted envelope. Note that the decryption didn't require to specify the certificate. That's because the envelope contains the thumbprint of the cert used for encryption, and the envelope object is smart enough to find the cert in the store that has a matching thumbprint. It's possible to have a cert object that's not installed in the store, then you have to use it explicitly.

By the way, there is also the cmdlet Protect-CmsMessage but so far I haven't figured out how to use it. It's unhappy with the certs I make, complaining that the permissions aren't set right on them.

<<Part1 Part3>>