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.
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:
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.
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).
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.
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)" }
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
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) { $FullString = [Text.Encoding]::Unicode.GetString($Bytes) # each database path is separated by null-character. $FullString.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!
Post your comment:
Comments: