Hi everyone,
while working in a PKI project we had the requirement to recover all archived private keys from a certification authority. The customer used SMIME mail encryption with two different certificates for signing and encryption.
From a security point of view it is good to user two different certificates for signing and encryption. On the other hand there are applications and devices that can only use a single certificate for signing and encryption. E.g. SAP C4C or any Android/iOS mobile device and mail client.
Therefore the customer decided to get rid of two different certificates and enroll a combined certificate to all end users.
Due to the fact that the customer didn't know who in his environment was actually using SMIME encryption and to make the reconfiguration of the users mail client easier, he decided to remove the two certificates from the client and replace (supersed) it with the new combined one.
In addition to that the customer wasn't even sure how many of his users actually make use of SMIME encryption. He expected less than 5 percent. In case a use has received an ecrypted email with his former certificate the customer needs to be able to provide the certificate to the user to be able to read the encrypted mail.
This was not very nice but had to be done in order to achive a supportable and stabel state.
Therefore we had to develope a simple script to recover all existing private keys from the CA and save them in a PFX file using standard key recovery processes and existing KRA certificates. Without the KRA certificate it is not possible to recovery and decrypt the private keys.
Of course the IT Security was involved in the whole process and additional security measures have been taken to protect the PFX files and passwords.
I would like to share the simple script we devolped to automatically recover all private keys from the CA database. This could also be helpfull if you would like to decomission your CA.
As a result you will receive all certificates including the private key enrolled based on the given template. You also receive a CSV file including the requester, serial number, PFX file name and password of each recovered key.
I used a Password generator script from the technet gallery. Thanks to Simon Wahlin.
https://gallery.technet.microsoft.com/scriptcenter/Generate-a-random-and-5c879ed5
I also used the PSPKI Powershell Module from Codeplex. https://pspki.codeplex.com/
Here's the script we used:
#### Variables ####
$TemplateName = "PKIW-User"
$CAName = "PWSUBCA.pkiw.lab\PWSUBCA"
$NetBiosName = "PKIW"
$ExportFolder = "C:\Export\"
#### Required Modules ####
Import-Module pspki
#### Functions ####
#### RandomPassword function from https://gallery.technet.microsoft.com/scriptcenter/Generate-a-random-and-5c879ed5 #####
function New-SWRandomPassword {
[CmdletBinding(ConfirmImpact='Low')]
[OutputType([String[]])]
Param
(
# Specifies minimum password length
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromRemainingArguments=$false,
Position=0)]
[ValidateScript({$_ -gt 0})]
[Alias("Min")]
[int]$MinPasswordLength = 10,
# Specifies maximum password length
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromRemainingArguments=$false,
Position=1)]
[ValidateScript({$_ -ge $MinPasswordLength})]
[Alias("Max")]
[int]$MaxPasswordLength = 10,
# Specifies an array of strings containing charactergroups from which the password will be generated.
# At least one char from each group (string) will be used.
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromRemainingArguments=$false,
Position=2)]
[String[]]$InputStrings = @("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789", '!@#$%&*_-+=\(){}[]:"<>.?/'''),
# Specifies number of passwords to generate.
[Parameter(Mandatory=$false,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$true,
ValueFromRemainingArguments=$false,
Position=3)]
[ValidateScript({$_ -gt 0})]
[int]$Count = 1
)
Begin {
Function Get-Seed{
# Generate a seed for future randomization
$RandomBytes = New-Object -TypeName "System.Byte[]" 4
$Random = New-Object -TypeName "System.Security.Cryptography.RNGCryptoServiceProvider"
$Random.GetBytes($RandomBytes)
[BitConverter]::ToInt32($RandomBytes, 0)
}
}
Process {
For($iteration = 1;$iteration -le $Count; $iteration++){
# Create char arrays containing possible chars
[char[][]]$CharGroups = $InputStrings
# Set counter of used groups
[int[]]$UsedGroups = for($i=0;$i -lt $CharGroups.Count;$i++){0}
# Create new char-array to hold generated password
if($MinPasswordLength -eq $MaxPasswordLength) {
# If password length is set, use set lenth
$password = New-Object -TypeName "System.Char[]" $MinPasswordLength
}
else {
# Otherwise randomize password length
$password = New-Object -TypeName "System.Char[]" (Get-Random -SetSeed $(Get-Seed) -Minimum $MinPasswordLength -Maximum $($MaxPasswordLength+1))
}
for($i=0;$i -lt $password.Length;$i++){
if($i -ge ($password.Length - ($UsedGroups | Where-Object {$_ -eq 0}).Count)) {
# Check if number of unused groups are equal of less than remaining chars
# Select first unused CharGroup
$CharGroupIndex = 0
while(($UsedGroups[$CharGroupIndex] -ne 0) -and ($CharGroupIndex -lt $CharGroups.Length)) {
$CharGroupIndex++
}
}
else {
#Select Random Group
$CharGroupIndex = Get-Random -SetSeed $(Get-Seed) -Minimum 0 -Maximum $CharGroups.Length
}
# Set current position in password to random char from selected group using a random seed
$password[$i] = Get-Random -SetSeed $(Get-Seed) -InputObject $CharGroups[$CharGroupIndex]
# Update count of used groups.
$UsedGroups[$CharGroupIndex] = $UsedGroups[$CharGroupIndex] + 1
}
$password -join ""
}
}
}
###### Main ######
"Requester;ValidFrom;ValidTo;SerialNumber;Filename;Password" | Out-File C:\Install\PFXPassword.csv
$certs = Get-CertificationAuthority pwsubca.pkiw.lab | Get-IssuedRequest -Filter "CertificateTemplate -eq $TemplateName"
Foreach ($Cert in $certs) {
$serial = $cert.SerialNumber
$serialNew = ([regex]::matches($cert.SerialNumber, '.{1,2}') | %{$_.value}) -join ' '
$Requester = $cert.'Request.RequesterName'.Replace("$NetBiosName\","")
$ValidTo = $cert.NotAfter
$ValidFrom = $cert.NotBefore
Certutil -config "$CAName" -getkey $serialNew "$ExportFolder$Requester-$serial.key"
If (Test-Path "$ExportFolder$Requester-$serial.key") {
$Password = New-SWRandomPassword
certutil -p "$Password" -recoverkey -user "$ExportFolder$Requester-$serial.key" "$ExportFolder$Requester-$serial.pfx"
If (Test-Path "$Requester-$serial.pfx") {
"$Requester;$ValidFrom;$ValidTo;$serial;$Requester-$serial.pfx;$Password" | Out-File C:\Install\PFXPassword.csv -Append
}
Else {
"$Requester;$ValidFrom;$ValidTo;$serial;Private key could not be decrypted. PFX file not created.;" | Out-File C:\Install\PFXPassword.csv -Append
}
}
Else {
"$Requester;$ValidFrom;$ValidTo;$serial;Certificate private key could not be pulled from the CA database;" | Out-File C:\Install\PFXPassword.csv -Append
}
}
I hope this is helpfull for some of you. (At least a hacker maybe will find this helpfull :-))
Best regards
Chris