Hello S-1-1-0!

Recently I noticed that PowerShell in Windows Server 2012 R2 ships two new cmdlets: Backup-CARoleService and Restore-CARoleService which are used to backup and restore CA database and CA keys. Today I want to talk about CryptoAPI functions utilization to backup CA database in PowerShell.

Backup step-by-step guide

Although, backup process isn’t looking very complex, however CryptoAPI implements a number of detailed (low-level) functions which must be called in a certain sequence. Here is a copy of the article that explains the correct sequence:

  1. Call CertSrvIsServerOnline to determine whether Certificate Services is online. Certificate Services must be online for the backup operations to be successful.
  2. Call CertSrvBackupPrepare to start a backup session. The resulting Certificate Services backup context handle will be used by many of the other backup functions.
  3. Call CertSrvRestoreGetDatabaseLocations to determine the restore map. The restore map contains the paths to be used when restoring the backup. Save the information retrieved by CertSrvRestoreGetDatabaseLocations to an application-specific location.
  4. Call CertSrvBackupGetDatabaseNames to determine the names of the database files to backup. For each of these files, execute steps 7 through 9.
  5. Call CertSrvBackupOpenFile to open the file for backup.
  6. Call CertSrvBackupRead to read a portion of bytes from the file, then call an application-specific routine to store the bytes on a backup medium. Repeat this step until all of the bytes in the file are backed up.
  7. Call CertSrvBackupClose to close the file.
  8. Call CertSrvBackupGetBackupLogs to determine the names of the log files to backup. For each of these files, execute steps 7 through 9.
  9. Call CertSrvBackupTruncateLogs to truncate the log files which were backed up in steps 6 and 10. This step is optional; however, call CertSrvBackupTruncateLogs only if all files returned by CertSrvBackupGetDatabaseNames and CertSrvBackupGetBackupLogs have been backed up (otherwise, the restore operation will fail). Consult the CertSrvBackupTruncateLogs reference page for details.
  10. Call CertSrvBackupGetDynamicFileList to determine the names of the non-database files to backup. These files are only identified by the function, and must be backed up by some other means.
  11. Backup the dynamic files identified in step 12, using routines separate from Certadm.dll.
  12. Call CertSrvBackupEnd to end the backup session.
  13. Call CertSrvBackupFree as needed to release buffers allocated by certain Certificate Services backup functions. Calls to CertSrvBackupGetBackupLogs, CertSrvBackupGetDatabaseNames, and CertSrvBackupGetDynamicFileList will allocate buffers that can be freed by a call to CertSrvBackupFree.

You see that the process isn’t a one-line solution :) I removed unnecessary (in our case) parts, so we have a full step-by-step guide to perform CA database backup.

Unmanaged function signatures

Since we are dealing with unmanaged functions in PowerShell, we have to use them via p/invoke and create c#-style interop definitions/signatures. These function definitions are quite simple for translation from c++ to c#, so here we start:

$cadmsignature = @"
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool CertSrvIsServerOnline(
    string pwszServerName,
    ref bool pfServerOnline
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupPrepare(
    string pwszServerName,
    uint grbitJet,
    uint dwBackupFlags,
    ref IntPtr phbc
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvRestoreGetDatabaseLocations(
    IntPtr hbc,
    ref IntPtr ppwszzDatabaseLocationList,
    ref uint pcbSize
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupGetDatabaseNames(
    IntPtr hbc,
    ref IntPtr ppwszzAttachmentInformation,
    ref uint pcbSize
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupGetBackupLogs(
    IntPtr hbc,
    ref IntPtr ppwszzBackupLogFiles,
    ref uint pcbSize
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupGetDynamicFileList(
    IntPtr hbc,
    ref IntPtr ppwszzFileList,
    ref uint pcbSize
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupOpenFile(
    IntPtr hbc,
    string pwszAttachmentName,
    uint cbReadHintSize,
    ref ulong pliFileSize
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupRead(
    IntPtr hbc,
    IntPtr pvBuffer,
    uint cbBuffer,
    ref uint pcbRead
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupClose(
    IntPtr hbc
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupTruncateLogs(
    IntPtr hbc
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupEnd(
    IntPtr phbc
);
[DllImport("Certadm.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern int CertSrvBackupFree(
    IntPtr pv
);
"@
Add-Type -MemberDefinition $cadmsignature -Namespace PKI -Name Certadm

The only note here is pliFileSize parameter in CertSrvBackupOpenFile function. This parameter should be at least long (Int64) or ulong (UInt64), while the rest integers (DWORDs) are translated to unsigned integer (uint).

Check whether CA is online

When function signatures are loaded, we can start with backup process. At first, we need to check whether CA is online:

$Server = $Env:COMPUTERNAME
$ServerStatus = $false
$hresult = [PKI.CertAdm]::CertSrvIsServerOnline($Server,[ref]$ServerStatus)
if (!$ServerStatus) {
    # 0x800706ba stands for "The RPC server is unavailable" error.
    throw New-Object ComponentModel.Win32Exception 0x800706ba
}

I never experienced that this function would return a non-zero hresult, so we do not check it. The function just writes $true or $false to $ServerStatus variable. $ServerStatus is $false, we should stop the whole process and proceed only when server is online.

Register backup operation

Write-Debug "Instantiate backup context handle"
# initialize backup handle variable
[IntPtr]$phbc = [IntPtr]::Zero
# register Full backup
$hresult = [PKI.CertAdm]::CertSrvBackupPrepare($Server,0,1,[ref]$phbc)
if ($hresult -ne 0) {
    throw New-Object ComponentModel.Win32Exception $hresult
}
Write-Debug "Backup context handle is: $phbc"

$phbc variable contains current backup process handle and will be used throughout whole backup process, so do not lose it.

Important: Once backup handle is retrieved, CA server is switched to backup state and will not respond to any request and query. In order to move CA to a working state we have to call CertSrvBackupEnd function. Although, the documentation states that we need to call CertSrvBackupFree function, but it looks like that this process is not necessary. At least, when I call this function, console is crashed. Also, CA returns to a operable state just by calling CertSrvBackupEnd function. Therefore we will create a helper function to end backup process. This function should be called any time when any subsequent function returns non-zero hresult. Of course, the function must be called at the end if the backup operation succeeded.

function End-Backup ($phbc) {
    $hresult = [PKI.CertAdm]::CertSrvBackupEnd($phbc)
    Write-Debug "Backup sent to end state: $(!$hresult)"
}

Retrieve restore map

During CA backup we have to create a restore map and save it in the certbkxp.dat file (the file name is constant). This file is used by CA database restore process and must be included in the backup set. File contents is retrieved by calling CertSrvRestoreGetDatabaseLocations function:

# initialize variables
$ppwszzDatabaseLocationList = [IntPtr]::Zero
$pcbSize = 0
# retrieve database and log files locations. These locations are written to a unmanaged
# buffer. The buffer is located at $ppwszzDatabaseLocationList pointed and is $pcbSize
# bytes in size
$hresult = [PKI.CertAdm]::CertSrvRestoreGetDatabaseLocations($phbc,[ref]$ppwszzDatabaseLocationList,[ref]$pcbSize)
if ($hresult -ne 0) {
    End-Backup $phbc
    throw New-Object ComponentModel.Win32Exception $hresult
}
Write-Debug "Restore map handle: $ppwszzDatabaseLocationList"
Write-Debug "Restore map size in bytes: $pcbSize"
# allocate managed buffer to store restore map
$Bytes = New-Object byte[] -ArgumentList $pcbSize
# copy restore map from unmanaged to managed buffer
[Runtime.InteropServices.Marshal]::Copy($ppwszzDatabaseLocationList,$Bytes,0,$pcbSize)
# save managed buffer to a file in the backup destination directory
Write-Verbose "Writing restore map to: $BackupDir\certbkxp.dat"
[IO.File]::WriteAllBytes("$BackupDir\certbkxp.dat",$Bytes)
Remove-Variable Bytes -Force

Retrieve CA database location

When restore map is created, we need to retrieve CA database location:

# initialize variables
$ppwszzAttachmentInformation = [IntPtr]::Zero
$pcbSize = 0
# retieve database locations. Locations are written to a unmanaged buffer
$hresult = [PKI.CertAdm]::CertSrvBackupGetDatabaseNames($phbc,[ref]$ppwszzAttachmentInformation,[ref]$pcbSize)
if ($hresult -ne 0) {
    End-Backup $phbc
    throw New-Object ComponentModel.Win32Exception $hresult
}
if ($pcbSize -eq 0) {
    End-Backup $phbc
    # 0x80070012 error code stands for "There are no more files"
    throw New-Object ComponentModel.Win32Exception 0x80070012
}
# allocate managed buffer
$Bytes = New-Object byte[] -ArgumentList $pcbSize
# copy bytes between unmanaged and managed buffers
[Runtime.InteropServices.Marshal]::Copy($ppwszzAttachmentInformation,$Bytes,0,$pcbSize)
# another helper function. Database locations are stored (at this point) in a little-endian
# Unicode format. This function reverts array to a big-endian byte order and gets
# appropriate unicode character.
function Split-BackupPath ([Byte[]]$Bytes) {
    $SB = New-Object System.Text.StringBuilder
    $bytes1 = $bytes | ForEach-Object {"{0:X2}" -f $_}
    for ($n = 0; $n -lt $bytes1.count; $n = $n + 2) {
        [void]$SB.Append([char](Invoke-Expression 0x$(($bytes1[$n+1]) + ($bytes1[$n]))))
    }
    # each database path is separated by null-character.
    $SB.ToString().Split("`0",[StringSplitOptions]::RemoveEmptyEntries)
}
# save CA database 
$DBPaths = Split-BackupPath $Bytes
Remove-Variable Bytes

At this point we now have CA database locations.

Historical note: why locations? Originally Microsoft designed Certification Authority architecture to support multiple CA instances on the same server. This is why you are required to use both, CA server and CA certificate names when using ICertAdmin, ICertRequest, ICertView, etc. interfaces. And a dedicated node exist in registry. Eventually Microsoft ended with single-instance service. Windows PKI team pays a lot of attention to backward compatibility with scripts written in early 2000 or even prior, therefore a lot of unobvious (by now) things are still in place.

There is no built-in routine to write backup data to backup media. This part is leaved to developers. However, backup API allows to read backup data as a byte array, so write routine can be easily implemented in a way you prefer. In the next part I will continue the CA backup story where we will talk about actual database and log file copy. Stay connected!


Share this article:

Comments:


Post your comment:

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