Automatic Certificate Revocation

HYPR Passwordless: Advanced Configuration: Windows

If you're using Active Directory for workstation authentication with HYPR, deregistering a user's mobile device will remove the certificate information from the HYPR platform. However, the certificate is still present in AD CS and must be removed separately.

To help AD Administrators manage certificate revocation, HYPR internally tracks the various deregistration events and makes a list of deregistered certificate serial numbers available via an API call.

This document demonstrates how to create a PowerShell script that uses this API call to automate the certificate revocation workflow.

πŸ“˜

Event Tracking

The following HYPR deregistration events are tracked:

  • The device or workstation has been deregistered
  • The user has been deleted in the HYPR Control Center, triggering a deregister
  • The workstation has renewed the certificate
  • The Smart Key has been unenrolled

Script Code

This example script demonstrates how to retrieve and process deregistered certificates. The, the certificates are output to the screen and can be optionally revoked using the standard Microsoft Certificate Authority COM object.

Note that the script is provided as an example only. Depending on your production environment, you may need to implement the workflow differently.

🚧

Check Yourself

This example provides no error handling, logging, or other facilities essential for a production quality script. Please thoroughly test any script based on this example before using it in your production environment!

# Usage examples ...
#   .\revoke-dereg-certs-example.ps1 -CC <xxx.com> -RpApp <yyy> -Token '<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>' -From "11/22/2020 12:00:00 AM" 
#   .\revoke-dereg-certs-example.ps1 -CC <xxx.com> -RpApp <yyy> -Token '<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>' -From 1623876047
#   .\revoke-dereg-certs-example.ps1 -CC <xxx.com> -RpApp <yyy> -Token '<xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>' -From 1623876047 -Revoke $true
#

param(
  [Parameter(Mandatory=$true)]
  [string]$CC,                    # (required) Domain name of Control Center 
  [Parameter(Mandatory=$true)]
  [string]$RpApp,                 # (required) RpApp 
  [Parameter(Mandatory=$true)]
  [string]$Token,                 # (required) Bearer token (required)
  [Parameter(Mandatory=$true)]
  $From,                          # (required) UTC DateTime as regular DateTime string or Unix time (as number of seconds from Jan 1, 1970)
  [bool]$Revoke = $false          # (optional) If true, deregistered certificates will be revoked
)

# Script Entry Point
$Main = {
  $filter = ConvertToUnixMillisec $From
  $deregCerts = GetDeregisteredCertificates $CC $RpApp $Token $filter

  # Show certificates
  ShowCertificates $deregCerts

  # Optionally revoke certificates
  if ($Revoke) {
    RevokeCertificates $deregCerts
  }
}
# ------------------------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------------------------
#
# G e t   D e r e g i s t e r e d    C e r t i f i c a t e s
#
# This is a workhorse/core function, which makes a REST API call to provided the endpoint to get a
# list of deregistered certificates, including machine name, certificate serial number and time
# of the deregistration (in UNIX time format in milliseconds). More details, including example
# of JSON "shape" are provided below. 
#
function GetDeregisteredCertificates([string]$server, [string]$app, [string]$bearer, [int64]$timeFilter) {
  # Alternative way to retrive/test JSON via command line command ("from" is UTC UNIX-time in milliseconds)
  # curl --location --request GET 'https://<$CC>/rp/api/rpapp/<$RpApp>/certificate/deregistered[?from=xxxxxxxx]' \
  #     --header 'Authorization: Bearer xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx' \
  #     --header 'Content-Type: application/json'


  # This is an example of possible JSON output
  #
  # {
  #   "certificates": [
  #     {
  #       "machineUserName": "DESKTOP-ghopper",
  #       "deviceId": "20C3F05D-368D-4C4F-BA24-17E6577FB014",
  #       "machineId": "29910810-5183-40f0-7bfd-5456b1ae5776",
  #       "certificateSerialNumber": "172E255F0000000062DE",
  #       "deleteDate": 1623876047000
  #     },
  #     {
  #       "machineUserName": "DESKTOP-jclarke",
  #       "deviceId": "72A16C90-F2CF-481F-BD3B-3A7330EC0FE1",
  #       "machineId": "95745D81-64BD-4374-B2EB-11ADA729F04E",
  #       "certificateSerialNumber": "1749B3D200000000437E1",
  #       "deleteDate": 1623877444000
  #     }
  #   ]
  # }
  #
  
  # If $timeFilter is provided (non-zero) request all certificates which have been deregistered
  # starting from $timeFilter, otherwise request all deregistered certificate HYPR keeps (currently
  # for the last 30 days)
  [string]$uri = ""
  if ($timeFilter -gt 0) {
    $uri = "https://$($server)/rp/api/rpapp/$($app)/certificate/deregistered?from=$($timeFilter)"
  }
  else {
    $uri = "https://$($server)/rp/api/rpapp/$($app)/certificate/deregistered"
  }

  $params = @{
      Uri         = $uri
      Headers     = @{ 'Authorization' = "Bearer $bearer" }
      Method      = 'GET'
      ContentType = 'application/json'
  }

  # Get JSON object
  $webData = Invoke-RestMethod @params
 
  return $webData.certificates
}


# ------------------------------------------------------------------------------------------------------------
#
# S h o w    C e r t i f i c a t e s
#
# Example of what can be done with the JSON of deregistered certificates. In this case they
# are simply displayed on the screen. In a production script, you can adapt to save them to a log
# file, or save to a network share for external application consumption, or do nothing and proceed
# immediately to their revocation. 
#
function ShowCertificates($certs) {
  # We can show nicely formatted output using PS one-liner. It is commented out to show
  # more procedural column access/manipulation further down
  #
  # $certs | Format-Table -Property machineUserName,@{Label="Deleted(UTC)"; Expression={ConvertFromUnixTime($_.deleteDate/1000)}},certificateSerialNumber
  #
  # If you uncomment the line above your output should be simular to this
  #
  #     machineUserName Deleted(UTC)         certificateSerialNumber
  #     --------------- ------------         -----------------------
  #     DESKTOP-ghopper 6/16/2021 8:40:47 PM 172E255F0000000062DE
  #     DESKTOP-jclarke 6/16/2021 9:04:04 PM 1749B3D200000000437E1
  #

  # ... but if you want to adopt a more flexible output or other actions:
  $counter = 0
  foreach($cert in $certs) {
    $counter = $counter + 1
    write-output "$($counter)."
    write-output "Machine:      $($cert.machineUserName)"
    write-output "Deleted(UTC): $(ConvertFromUnixTime($cert.deleteDate/1000))"
    write-output "Serial#:      $($cert.certificateSerialNumber)"
    write-output ""
  }
}

# ------------------------------------------------------------------------------------------------------------
#
# R e v o k e    C e r t i f i c a t e s
#
# This is an example of a certificate revocation using two built-in (on Windows Server) COM 
# objects created using their ProgIDs.
#
# "CertificateAuthority.Config" ProgID represents the ICertConfig interface and this script uses its
# ICertConfig::GetConfig() method documented here:
#    https://docs.microsoft.com/en-us/windows/win32/api/certcli/nf-certcli-icertconfig-getconfig
# It's used to get CertificateAuthority configuration, which is a parameter for the other COM object's
# RevokeCertificate() method. This is a "COMPUTERNAME\CANAME" string, where COMPUTERNAME is the network
# name of the Certificate Services server and CANAME is the common name of the certification authority,
# as entered during Certificate Services setup.
#
# "CertificateAuthority.Admin" ProgID represents the ICertAdmin interface and this script uses its
# ICertAdmin::RevokeCertificate() and ICertAdmin::PublishCRL() methods documented here respectively:
#    https://docs.microsoft.com/en-us/windows/win32/api/certadm/nf-certadm-icertadmin-revokecertificate
#    https://docs.microsoft.com/en-us/windows/win32/api/certadm/nf-certadm-icertadmin-publishcrl 
#
function RevokeCertificates($certs) {
  # Get COM object with ProgID "CertificateAuthority.Config"
  $CertConfig = New-Object -ComObject CertificateAuthority.Config
  # Get COM object with ProgID "CertificateAuthority.Admin"
  $CertAdmin = New-Object -ComObject CertificateAuthority.Admin

  # Get "COMPUTERNAME\CANAME" configuration
  $CC_LOCALACTIVECONFIG = 4
  $caConfig = $CertConfig.GetConfig($CC_LOCALACTIVECONFIG)

  # Loop through the certificates list
  $CRL_REASON_UNSPECIFIED = 0
  $REVOCATION_DATE_TIME = 0
  foreach($cert in $certs) {
    # Revoke certificate
    $CertAdmin.RevokeCertificate($caConfig, $cert.certificateSerialNumber, $CRL_REASON_UNSPECIFIED, $REVOCATION_DATE_TIME)
  }

  # Publish CRL
  $CertAdmin.PublishCRL($caConfig, $REVOCATION_DATE_TIME)
}

# -----------------------------------
#
# H e l p e r s
#

# Convert from Unix time to DateTime
function ConvertFromUnixTime ([int64]$unixTime) {
  [datetime]$zeroUnixTime = '1970-01-01 00:00:00'    
  [datetime]$result = $zeroUnixTime.AddSeconds($unixTime)
  return $result
}

# Convert from DateTime to Unix time
function ConvertToUnixTime ([datetime]$dateTime) {
  [datetime]$zeroDateTime = '1970-01-01 00:00:00'
  [int64]$unixTime = 0

  $unixTime = (New-TimeSpan -Start $zeroDateTime -End $dateTime).TotalSeconds
  return $unixTime
}

# Convert to Unix time (millisecond resolution) parameter which is either
# Unix time as seconds or DateTime as a string
function ConvertToUnixMillisec($dt) {
  [int64]$unixTimeMillisec = 0

  $isNum = $dt -is [int]    
  if ($isNum) {
    # Unix time as seconds provided
    $unixTimeMillisec = [int64]$dt
    # Scale up to milliseconds (because endpoint API requires that)
    $unixTimeMillisec *= 1000
  }
  else {
    # DateTime as string provided
    [datetime]$dateTime = [datetime]::Parse($dt)
    $unixTimeMillisec = ConvertToUnixTime $dateTime
    # Scale up to milliseconds (because endpoint API requires that)
    $unixTimeMillisec *= 1000
  }

  return $unixTimeMillisec
}

# Execute Main function (entry point)
& $Main

Script Parameters

Run the script in a PowerShell window as follows:

.\revoke-dereg-certs-example.ps1 -CC <domain> -RpApp <appID> -Token
 <xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx> -From <date> -Revoke $True

CC (string, required)
The base domain of your Control Center instance.

RpApp (string, required)
The ID of your Relying Party application in the Control Center.

Token (string, required)
A valid Control Center access token. For more information on generating a token, please see Access Tokens.

From (date, required)
UTC date and time, either as regular DateTime string or a Unix time (number of seconds from Jan 1, 1970).

Revoke (boolean, optional)
If $true, deregistered certificates will be revoked in ADCS. Note that if you set the Revoke parameter, the script must be executed on a Windows Server machine with elevated privileges. The required COM objects aren't available on non-server Windows installs.

Usage Examples

.\revoke-dereg-certs-example.ps1 -CC highlandsbank.com -RpApp WorkstationAccess -Token
 110f7826-d95e-12d2-42ce-8bd67fb20bb9 -From "07/01/2021 12:00:00 AM"

.\revoke-dereg-certs-example.ps1 -CC highlandsbank.com -RpApp WorkstationAccess -Token
 110f7826-d95e-12d2-42ce-8bd67fb20bb9 -From 1625097600

.\revoke-dereg-certs-example.ps1 -CC highlandsbank.com -RpApp WorkstationAccess -Token
 110f7826-d95e-12d2-42ce-8bd67fb20bb9 -From 1625097600 -Revoke $true

Sample Output:

1.
Machine:      DESKTOP-ghopper
Deleted(UTC): 07/01/2021 12:34:18
Serial#:      172E255F0000000062DE

2.
Machine:      DESKTOP-jclarke
Deleted(UTC): 07/01/2021 12:42:30
Serial#:      1749B3D200000000437E1