Exporting an Active Directory Users Public Digital Certificate

Have you ever needed to export the public digital certificate of a user stored in Active Directory to allow that user secure access to an application or internal website not integrated into AD/LDAP?

Background and Overview

I have an internal web application not integrated into AD/LDAP to pull user or group information. This is not a web app limitation but a capability limitation as the web app isn't able to pull AD/LDAP Public Key Infrastructure (PKI) information into the client-side application. However, I require the intranet public-facing page to be secure still. The web app enforces the PKI environment to the web by prompting the user for his private key pin; however, the user is placed in a public group and not a secured group or elevated to a web app administrator role.

For the use case of this PowerShell script, I am exporting the AD user public certificate and assigning that to certain roles or groups within the web apps user management tool. This is done via the certificate's digital thumbprint and the certificate trust stores within the web app. This PowerShell script also allows me to pull all the public certificates from all the users within an organizational unit (OU) if the parameter is met. By default, if no user is identified it will query the current user or can be targeted to pull an individual's public certificate.

Prerequisites

To use this PowerShell function script the executing computer must have the Remote Server Administrator Tools (RSAT) - ActiveDirectory module installed. Depending on your company's software or security policy this may not be available to the domain user accounts. To install the RSAT - ActiveDirectory module copy the following into an elevated PowerShell window:

Add-WindowsCapability –online –Name “Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0”

Get-ADUser

The PowerShell script is going to rely on this "command let" or cmdlet to find the target user or query the OU for a list of users to export. The basic syntax that we are going to use is:

Get-ADUser -Identity <Identity> -Properties sAMAccountName, usercertificate

This is going to allow us to query the target user and pull the user's sAMAccountName and public certificate which is going to be used as the base of our script. We are going to assign this to the variable $users and query $username later in our script.

$users = Get-ADUser -Identity $username -Properties sAMAccountName, usercertificate

X509Certificates

Now that we have the user we need to pull the user's public certificate out of AD. To do so we will want to pipe the output of the $users variable to pull out the x509Certificate using the PowerShell class System.Security.Cryptography. This command will look like this:

$users | Select-Object -ExpandProperty usercertificate | ForEach-Object {
	$cert = [System.Security.Cryptograpahy.X509Certificate.X509Certificate2]$_
}

If done successfully the user's public certificate should be displayed in PKCS12 or X509 format. Since we want to export this certificate we will need to use the Export-Certificate command within our script block to execute this task to our Downloads directory.

$users | Select-Object -ExpandProperty usercertificate | ForEach-Object {
	$cert = [System.Security.Cryptograpahy.X509Certificate.X509Certificate2]$_
    Export-Certificate -Cert $cert -FilePath "$ENV:USERPROFILE\Downloads\$($users.sAMAccountName).cert"
}

The "$ENV:USERPROFILE" is a PowerShell environmental variable that will point to the current user's home directory. Additionally, we want to call the variable $users.sAMAccountName property to name our exported certificate.

Creating the Function Cmdlet

Now that we have the initial premise of what commands we will be using and how to use them we need to put them all together into a PowerShell function with parameters. To do this we are going to start with the basic PowerShell function and parameter snippet.

Function Get-ADUserCertificate {
	
    param (
        [string]$Username="$ENV:USERNAME",
        [string]$FilePath="$ENV:USERPROFILE\Downloads",
        [string]$SearchBase
    }
    
    begin {
    }
    
    process {
    }
    
    end {
    }
}

In our Function parameter block, we identify that our variables $Username, $FilePath, and $SearchBase are strings using the [string] as the type. By using the $Username="$ENV:USERNAME" or $FilePath="$ENV:USERPROFILE\Downloads" we can set the default values if the function is called with no parameters. Additionally, we would want to enforce that $Username is always in the first position using the "Position=0" attribute.

[Parameter(Position=0)][string]$Username = "$env:USERNAME",

The Begin Sequence

To ensure our function executes a set of commands in the sequence we will populate the begin script block to gather our initial variables. To do this we want to declare the $users variable but first, look for the $SearchBase parameter as this will change the method we export the public certificates. For this, we are going to use some logic statements If ... else, and some try ... catch in our execution.

begin {
	if (! $SearchBase) {
    	try {
        	$users = Get-ADUser -Identity $Username -Properties SamAccountName, usercertificate -ErrorAction Stop
            }
		catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
        	Write-Host "ERROR: User not found." -ForegroundColor Red
        $status = $false
            }
     }
     else {
     	try {
     		$users = Get-ADUser -Filter * -SearchBase $SearchBase -Properties SamAccountName, usercertificate -ErrorAction Stop
        }
        catch {
        	Write-Host "ERROR: Specified SearchBase not found" -ForegroundColor Red
            $status = $false
        }
     }
}

This is going to search if the parameter $SearchBase is being called. By placing an exclamation (!) we are saying if not $SearchBase do this otherwise do this which is identified by the action following the else statement. The try-catch blocks are telling our script to try this command if it fails stop and start from the catch block.

In the catch block, we are targeting a specific error type [Microsoft.ActiveDirectory.Management.ADIdentifyNotFoundException] which is a user-not-found error type. The $status variable is something we want to set and display a particular error message at the end of our function that will be covered later.

If the $SearchBase parameter is called we are going to attempt to use the Get-ADUsers cmdlet but execute it on a target OU instead. Using the try-catch blocks we want to ensure that the command executes as intended or displays an error to the user in the catch block.

The Process Sequence

In our process sequence block, we are going to take the values and information from our begin block and start to process them. For this, we are going to query the $users variable and start the Export-Certificate process we discussed in the X509Certificates section. To make use of this step we are going to use a ForEach loop since our $users variable will contain an array if the $SearchBase parameter is called.

process {
	foreach ($user in $users) {
		$user | Select-Object -ExpandProperty usercertificate | ForEach-Object {
			$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$_
			try {
				Export-Certificate -Cert $cert -FilePath "$FilePath\$($user.SamAccountName).cer" -ErrorAction Stop
				Write-Host "Successfully Exported the User Certificate to $FilePath\$($user.SamAccountName).cer"
				$status = $true
			}
			catch {
				Write-Host "ERROR: Unable to Save to Default Location $FilePath"
			}
		}
	}
}

We have additionally added another try-catch block to do some error checking for us. If the $FilePath parameter is declared and does not exist the script will stop and display an error.

The End Sequence

In this sequence, we want to call the $status variable in a switch block or select-case function to display a success or failure message to the user.

end {
	switch ($status) {
		$true { Write-Host "Task Complete." -ForegroundColor Cyan }
		$false { Write-Host "Task Completed with Errors." -ForegroundColor Yellow }
	}
}

Putting it all together

Now that we have the basic sequence let's put it all together in one complete script but add some comment blocks to remind us why we are performing a certain task. If you followed everything so far your end PowerShell script should look like this:

function Get-ADUserCertificate {
    # Permit the use of -WhatIf, -Force, and -Verbose
    [CmdletBinding(SupportsShouldProcess)]
    param (
        # Create the parameters, Username, FilePath, and SearchBase; optionally set the defaults if not declared
        [Parameter(Position=0)][string]$Username = "$env:USERNAME",
        [string]$FilePath = "$env:USERPROFILE\Downloads",
        [string]$SearchBase
    )
    # Gather the prerequisites
    begin {
        # Determine if SearchBase is declared, if not continue with the default execution
        if (! $SearchBase) {
            try {
                # Search Active Directory for the target user
                $users = Get-ADUser -Identity $Username -Properties SamAccountName, usercertificate -ErrorAction Stop
            }
            # User does not exist, display the error and exit the script
            catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
                Write-Host "ERROR: User not found." -ForegroundColor Red
                $status = $false
            }
        }
        else {
            try {
            # SearchBase is declared, gather all users within the SearchBase
            $users = Get-ADUser -Filter * -SearchBase $SearchBase -Properties SamAccountName, usercertificate -ErrorAction Stop
            }
            catch {
                Write-Host "ERROR: Specified SearchBase not found." -ForegroundColor Red
                $status = $false
            }
        }
    }
    # Begin the execution process
    process {
        # Simplified, export the user or users certificates to the declared or default directory
        foreach ($user in $users) {
            $user | Select-Object -ExpandProperty usercertificate | ForEach-Object {
                $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$_
                try {
                    Export-Certificate -Cert $cert -FilePath "$FilePath\$($user.SamAccountName).cer" -ErrorAction Stop
                    Write-Host "Successfully Exported the User Certificate to $FilePath\$($user.SamAccountName).cer"
                    $status = $true
                }
                # Display a genearic error if the destination file path does not exist if FilePath is declared
                catch {
                    Write-Host "ERROR: Unable to Save to Default Location $FilePath"
                }
            }
        }
    }
    # Close out the function    
    end {
        # Make it easy select-case
        switch ($status) {
            $true { Write-Host "Task Complete." -ForegroundColor Cyan }
            $false { Write-Host "Task Completed with Errors." -ForegroundColor Yellow }
        }
    }
}

Closing Remarks

Hopefully, this was able to help you or provide some snippets for your own script. In my environment, this works so that I can export the required user's public certificate and import it to my web app user management tool to allow the end user access to either perform administrative functions or access certain groups using the PKI certificate trust chains.