Windows PKI blog

News and information for public key infrastructure (PKI) and Active Directory Certificate Services (AD CS) professionals

Powershell CRL Copy

Powershell CRL Copy

  • Comments 7
  • Likes

This script writes a Certification Authority's Certificate Revocation List to HTTP based CRL Distribution Points via a UNC path. It checks to make sure that the copy was successful and that the CDPs have not and are not about to expire. Alerts/status messages are sent via SMTP and eventlog entries.

Performs the following steps:

  1. Determines if the Active Directory Certificate Services are running on the system. In the case of a cluster make sure to set the $Cluster variable to '$TRUE'
  2. Reads the CA's CRL from %windir%\system32\certsrv\certenroll (defined by $crl_master_path + $crl_name variables). I'll refer to this CRL as "Master CRL."
  3. Checks the NextUpdate value of the Master CRL to make sure is has not expired. (Note that the Mono library adds hours to the NextUpdate and ThisUpdate values, control this time difference with the $creep variable)
  4. Copy Master CRL to CDP UNC locations if Master CRL's ThisUpdate is greater than CDP CRLs' ThisUpdate
  5. Compare the hash values of the CRLs to make sure the copy was successful. If they do not match override the $SMTP variable to send email alert message.
  6. When Master CRL's ThisUpdate is greater than NextCRLPublish and NextUpdate we want to be alerted when the Master CRL is approaching end of life. Use the $threshold variable to define (in hours, .5 = 30 minutes) how far from NextUpdate you want to receive warnings that the CRLs are soon to expire.

Output:

  1. Run script initially as local administrator to register with the system's application eventlog
  2. Send SMTP message if $STMP = True. Set variable section containing SMTP settings for your environment
  3. To run this script with debug output set powershell $DebugPreference = "Continue"
  4. The 'results' function is used to write to the eventlog and send SMTP messages. Event levels are controlled in the variable section. For example a failed CRL copy you want to make sure the eventlog show "Error" ($EventHigh)

Requirements:

  1. Windows Powershell v2 included in the Windows Management Framework http://support.microsoft.com/kb/968929
  2. Powershell Community Extensions for the Get-Hash commandlet http://pscx.codeplex.com
  3. This powershell script uses a third party, open source .Net reference called 'Mono.' More information can be found at http://www.mono-project.com/Main_Page (Note: the Mono assembly Mono.Security.x509.x509CRL adds 4 hours to the .NextUpdate, .ThisUpdate and .IsCurrent function)
  4. Don't forget to set the powershell set-executionpolicy
  5. Run the script as a scheduled task every 30 minutes
    1. Start Task Scheduler
    2. Select Create Basic Task
    3. Select Daily
    4. Set Repeat task every: 30 minutes for the duration of: Indefinitely
    5. Select Start a program
    6. Set Program/script = %SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe
    7. Set Argument = <full_path>crl_copy.ps1
    8. Within the Task’s Properties -> General tab select the following Security Options
      1. Select Change User or Group
      2. Add service account used to perform backups, make sure it has sufficient rights (remember to run the script as local administrator the first time to register with the eventlog)
      3. Select Run whether user is logged on or not
      4. Select Run with highest privileges

ToDos:

If you have any ideas, please share them. A couple of thoughts about future improvements:

  1. Bind to an LDAP directory to retrieve CRL (e.g. ldap://fpkia.gsa.gov/CommonPolicy/CommonPolicy(1).crl)
  2. Use multidimensional arrays to store CDP HTTP and UNC addresses
  3. Improve error handling

Variables:

Make sure to set for your environment

$crl_master_path

Location where the CA writes the Master CRL

$CRL_Name

Name of the CRL

$CDP1_UNC

UNC path to CDP1 make sure the path ends with “\”

$CDP2_UNC

UNC path to CDP2 make sure the path ends with “\”

$CDP1_HTTP

HTTP path to CDP1 make sure the path ends with “/”

$CDP2_HTTP

HTTP path to CDP2 make sure the path ends with “/”

$SMTP

Boolean value to determine if SMTP message is sent via the ‘results’ function

$SmtpServer

Hostname (name, FQDN or IP) of SMTP server

$From

From address of SMTP based message

$To

Recipients of SMTP message

$Title

Subject of SMTP message

$CRL_Evt_Source

Source field of Application eventlog entry

$threshold

# of hours before the reaching the Master CRL’s NextUpdate time (early warning)

$creep

# of hours that the Mono library adds to NextUpdate and ThisUpdate values

$Cluster

Set to ‘true’ if your Certification Authority is clustered

 

clip_image001

################################################
#
# Title:     CRLCopy.ps1
# Date:     4/28/2010
# Author: Paul Fox (MCS)
# Copyright Microsoft Corporation @2010
#
# Description:     This script writes a Certification Authority's Certificate Revocation List to HTTP based CRL Distribution Points via a UNC path.
#               Performs the following steps:
#                 1) Determines if the Active Directory Certificate Services are running on the system. In the case of a cluster make sure to set the $Cluster variable to '$TRUE'
#                 2) Reads the CA's CRL from %windir%\system32\certsrv\certenroll (defined by $crl_master_path + $crl_name variables). I'll refer to this CRL as "Master CRL."
#                 3) Checks the NextUpdate value of the Master CRL to make sure is has not expired. (Note that the Mono library adds hours to the NextUpdate and EffectiveDate values, control this time difference with the $creep variable)
#                 4) Copy Master CRL to CDP UNC locations if Master CRL's ThisUpdate is greater than CDP CRLs' ThisUpdate
#                 5) Compare the hash values of the CRLs to make sure the copy was successful. If they do not match override the $SMTP variable to send email alert message.
#                 6) When Master CRL's ThisUpdate is greater than NextCRLPublish and NextUpdate we want to be alerted when the Master CRL is approaching end of life. Use the $threshold variable to define (in hours) how far from
#                    NextUpdate you want to receive warnings that the CRLs are soon to expire.              
#
# Output: 1) Run script initially as local administrator to register with the system's application eventlog
#         2) Send SMTP message if $STMP = True. Set variable section containing SMTP settings for your environment
#         3) To run this script with debug output set powershell $DebugPreference = "Continue"
#         4) The 'results' function is used to write to the eventlog and send SMTP messages. Event levels are controlled in the variable section. For example a failed CRL copy you want to make sure the eventlog show "Error" ($EventHigh)
#      
# Requirements: 1) Windows Powershell v2 included in the Windows Management Framework http://support.microsoft.com/kb/968929
#                 2)Powershell Community Extensions for the Get-Hash commandlet http://pscx.codeplex.com
#                 3) This powershell script uses a third party, open source .Net reference called 'Mono'    More information can be found at http://www.mono-project.com/Main_Page
#                             Note: the Mono assembly Mono.Security.x509.x509CRL adds 4 hours to the .NextUpdate, .ThisUpdate and .IsCurrent function
#                         4) Don't forget to set the powershell set-executionpolicy
#
# ToDos: Bind to an LDAP directory to retrieve CRL (e.g. ldap://fpkia.gsa.gov/CommonPolicy/CommonPolicy(1).crl)
#        Use multidimensional arrays to store CDP HTTP and UNC addresses
#
# Debug: To run this script with debug output set powershell $DebugPreference = "Continue"
#
################################################

################################################
#
# Function:     Results
# Description:    Writes the $evt_string to the Application eventlog and sends
#                SMTP message to recipients if $SMTP = [bool]$true
#   
#
################################################
function results([string]$evt_string, [int]$level ,[bool]$sendsmtp)
{
write-debug "******** Inside results function ********"
write-debug "SMTP = $sendsmtp"
write-debug "Evtstring = $evt_string"
write-debug "Level: $level"
###############
#if eventlog does not exist create it (must run script as local administrator once to create)
###############
if(![system.diagnostics.eventlog]::sourceExists($CRL_Evt_Source))
    {
        $evtlog = [system.diagnostics.eventlog]::CreateEventSource($CRL_Evt_Source,"Application")
    }

###############
# set eventlog object
###############
$evtlog = new-object system.diagnostics.eventlog("application",".")
$evtlog.source = $CRL_Evt_Source

###############
# write to eventlog
###############
$evtlog.writeEntry($evt_string, $level, $EventID)

if($sendsmtp)
    {
    $SmtpClient = new-object system.net.mail.smtpClient
    $SmtpClient.host = $SmtpServer
    $Body = $evt_string
    $SmtpClient.Send($from,$to,$title,$Body)
    }
}

################################################
#
# Main program
#
################################################

################################################
#
# Add Mono .Net References
# If running on an x64 system make sure the path is correct
#
################################################
Add-Type -Path "C:\Program Files (x86)\Mono-2.6.4\lib\mono\2.0\Mono.Security.dll"

################################################
#
# Variables
#
################################################
$crl_master_path = "c:\windows\system32\certsrv\certenroll\"
$CRL_Name = "master.crl"
$CDP1_UNC = "\\cdp1\cdp1\"
$CDP2_UNC = "\\cdp2\cdp2\"
$CDP1_HTTP = "http://keys1.your.domain/"
$CDP2_HTTP = "http://keys2.your.domain/"

$SMTP = [bool]$false
$SmtpServer = "your.mx.mail.server"
$From = "crlcopy@your.domain"
$To = "CAAdmins@your.domain"
$Title = "CRL Copy Process Results"

$CRL_Evt_Source = "CRL Copy Process"
$EventID = "5000"
$EventHigh = "1"
$EventWarning = "2"
$EventInformation = "4"

$newline = [System.Environment]::NewLine
$time = Get-Date
$threshold = 1
$creep = -4
$Cluster =  [bool]$false

################################################
#
# Is certsrv running? Is it a clustered CA?
# If clustered it is not running don't send an SMTP message
#
################################################
$service = get-service | where-Object {$_.name -eq "certsvc"}

if (!($service.Status -eq "Running"))
    {
    if($Cluster)
        {
       $evt_string = "Active Directory Certificate Services is not running on this node of the cluster. Exiting program."
       write-debug "ADCS is not running. This is a clustered node. Exiting"
       results $evt_string $EventInformation $SMTP
       exit
       }
    else
        {
        $evt_string = "**** IMPORTANT **** IMPORTANT **** IMPORTANT ****" +  $newline + "Certsvc status is: " + $service.status + $newline
        write-debug "ADCS is not running and not a clustered node. Not good."
        results $evt_string $EventHigh $SMTP
        exit
        }
      }
else
     {
     write-debug "Certsvc is running. Continue."
     }

################################################
#
# Pull CRLs from Master and HTTP CDP locations
# Not going to bother with Active Directory since this
# is probably a Windows Enterprise CA (todo)
#
################################################
$CRL_Master = [Mono.Security.X509.X509Crl]::CreateFromFile($crl_master_path + $CRL_Name)
$web_client = New-Object System.Net.WebClient
$CDP1_CRL = [Mono.Security.X509.X509Crl]$web_client.DownloadData($CDP1_HTTP + $CRL_Name)
$CDP2_CRL = [Mono.Security.X509.X509Crl]$web_client.DownloadData($CDP2_HTTP + $CRL_Name)

################################################
#
# Debug section to give you the time/dates of the CRLs
#
################################################
if($debugpreference -eq "continue")
    {
    write-debug $newline
    write-debug "Master CRL Values"
    $debug_out = $CRL_Master.ThisUpdate.AddHours($creep)
    write-debug "Master ThisUpdate $debug_out"
    $debug_out = $CDP1_CRL.ThisUpdate.AddHours($creep)
    write-debug "CDP1_CRL ThisUpdate: $debug_out"
    $debug_out = $CDP2_CRL.ThisUpdate.AddHours($creep)
    write-debug "CDP2_CRL ThisUpdate: $debug_out"
    $debug_out = $CRL_Master.NextUpdate.AddHours($creep)
    write-debug "Master NextUpdate: $debug_out"
    $debug_out = $CDP1_CRL.NextUpdate.AddHours($creep)
    write-debug "CDP1_CRL NextUpdate: $debug_out"
    $debug_out = $CDP2_CRL.NextUpdate.AddHours($creep)
    write-debug "CDP2_CRL NextUpdate: $debug_out"
    write-debug $newline
    }

################################################
#
# Determine the status of the master CRL
# Master and CDP CRLs have the same EffectiveDate (Mono = ThisUpdate)   
#
################################################
if($CRL_Master.NextUpdate.AddHours($creep) -gt $time)
        {
        # This is healthy Master CRL
        write-debug "Master CRL EffectiveDate: "
        write-debug $CRL_Master.ThisUpdate.AddHours($creep)
        write-debug "Time now is: "
        write-debug $time
        write-debug $newline
        }
else
        {
        # Everything has gone stale, not good. Alert.
        write-debug "Master CRL has gone stale"
        $evt_string = "**** IMPORTANT **** IMPORTANT **** IMPORTANT ****" + $newline + "Master CRL: " + $CRL_Name + " has an EffectiveDate of: " + $CRL_Master.ThisUpdate.AddHours($creep) + " and an NextUpdate of: " + $CRL_Master.NextUpdate.AddHours($creep) + $newline + "Certsvc status is: " + $service.status
        results $evt_string $EventHigh $SMTP
        exit
        }
################################################
#   
# Determine what the status of the CDPs
# Does the Master and the CDP CRLs match up?
#
################################################
if (($CRL_Master.ThisUpdate -eq $CDP1_CRL.ThisUpdate) -and ($CRL_Master.ThisUpdate -eq $CDP2_CRL.ThisUpdate))
    {
    write-debug "All CRLs EffectiveDates match"
write-debug $CRL_Master.ThisUpdate
        write-debug $CDP1_CRL.ThisUpdate
        write-debug $CDP2_CRL.ThisUpdate
        write-debug $newline
        }
################################################
#
# New Master CRL, Update CDP CRLs if or or both are old
# would be nice to use the 'CRL Number'
# Compare the hash values of the Master CRL and CDP CRLs
# after the copy command to make sure the copy completed
#
################################################
elseif (($CRL_Master.ThisUpdate -gt $CDP1_CRL.ThisUpdate) -or ($CRL_Master.ThisUpdate -gt $CDP2_CRL.ThisUpdate))
    {
    # There is a new master CRL, copy to CDPs
    write-debug "New master crl. Copy out to CDPs"
    $source = Get-Item $crl_master_path$CRL_Name
    Copy-Item $source $CDP1_UNC$CRL_Name
    Copy-Item $source $CDP2_UNC$CRL_Name
    # Compare the hash values of the master CRL to the CDP CRL
    # If they do not equal alert via SMTP by setting the $SMTP boolian value to '$true'
    $master_hash = get-hash $source
    $cdp1_hash = get-hash $CDP1_UNC$CRL_Name
    $cdp2_hash = get-hash $CDP2_UNC$CRL_Name
    if(($master_hash.HashString -ne $cdp1_hash.HashString) -or ($master_hash.HashString -ne $cdp2_hash.HashString))
        {
        $evt_string = "CRL copy to CDP location failed:" +$newline +"Master CRL Hash: " +$master_hash.HashString +$newline + "CPD1  Hash:" +$cdp1_hash.HashString +$newline + "CDP2 Hash:" +$cdp2_hash.HashString +$newline
        # Make sure the email alert goes out. Override the $SMTP variable
        write-debug $newline
        write-debug "CRLs copied to CDPs hash values do not match Master CRL Hash"
        write-debug "Master CRL Hash value"
        write-debug $master_hash.HashString
        write-debug "CDP1 CRL Hash value"
        write-debug $cdp1_hash.HashString
        write-debug "CDP2 CRL Hash value"
        write-debug $cdp2_hash.HashString
        $SMTP = [bool]$true
        results $evt_string $EventHigh $SMTP
        exit
        }
    else
        {
        $evt_string = "New Master CRL published to CDPs. " + $CRL_Name + " has an EffectiveDate of: " + $CRL_Master.ThisUpdate.AddHours($creep) + " and an NextUpdate of: " + $CRL_Master.NextUpdate.AddHours($creep)
        results $evt_string $EventInformation $SMTP
        }
    }
else
    {
     write-debug "logic bomb, can't determine where the Master CRL is in relationship to the CDP CRLs"
     }
################################################
#
# Master CRL’s ThisUpdate time is in between the NextCRLPublish time and NextUpdate.
# Note Mono does not have a method to read 'NextCRLPublish'
# The CA Operator can define the '$threshold' at which that want to start receiving alerts
#
################################################
if (($CRL_Master.NextUpdate.AddHours($creep) -gt $time) -and ($CRL_Master.ThisUpdate.AddHours($creep) -lt $time))
    {
    write-debug "checking threshold"
    # Is the Master CRL NextUpdate within the defined alert threshold?
    if($CRL_Master.NextUpdate.AddHours(-($threshold - $creep)) -lt $time)
        {
        write-debug "***** WARNING ****** Master CRL NextUpdate has a life less than threshold."
        write-debug $CRL_Master.NextUpdate.AddHours(-($threshold - $creep))
        $evt_string = "***** WARNING ****** Master CRL NextUpdate has a life less than threshold of: " + $threshold + " hour(s)" + $newline + "Master CRLs NextUpdate is: " + $CRL_Master.NextUpdate.AddHours($creep) + $newline +"Certsvc service is: " + $service.Status
        results $evt_string $EventWarning $SMTP
        }
    else
        {
        write-debug "Within the Master CRLs NextCRLPublish and NextUpdate period. Within threshold period."
        write-debug $CRL_Master.NextUpdate.AddHours(-($threshold - $creep))
        # Uncomment the following if you want notification on the CRLs
        #$evt_string = "Within the Master CRLs NextCRLPublish and NextUpdate period. Alerts will be send at " + $threshold + " hour(s) before NextUpdate period is reached."
        #results $evt_string $EventInformation $SMTP
        }
    }
else
    {
     write-debug "logic bomb, can't determine where we are in the threshold"
    }

Comments
  • Nice Script! But I've a problem using this. It always generates me an Error caused by the Mono.Security.X509.X509Crl. "Input data cannot be coded as a valid CRL". I have one revoked ant two signed certs until now. Can you help me out here? This would be very great.

  • did you load the mono libraries? www.mono-project.com/Main_Page

  • Excellent script!

    Before powershell we had to do this with a custom C++ windows service :-)

    The LDAP write portion could be done with the System.DirectoryServices namespace like this:

    (note: I chose BouncyCastle .Net assembly in my script to handle the X509CRL type just for familiarity with BC. The Mono library still works with minor edits)

    #Ldap properties

    $ldapPath = [string]"LDAP://dc01.domain.local:389/CN=Test CA 01,CN=DC01,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,DC=domain,DC=local"

    $bindUserDN = [string]"administrator@domain.local"

    $bindUserPw = [string]"Password1!"

    $crlFilePath = [string]"C:\Windows\System32\certsrv\CertEnroll\<Name of CRL file>.crl"

    ##LDAP Bind

    $directoryEntry = new-object System.DirectoryServices.DirectoryEntry($ldapPath,$bindUserDN,$bindUserPw)

    $directoryEntry.psbase.AuthenticationType=[System.DirectoryServices.AuthenticationTypes]::FastBind

    # load CRL And ARL from file into to byte[]

    [byte[]] $crl = [System.IO.File]::ReadAllBytes( $( resolve-path $crlFilePath) )

    [byte[]] $arl = [System.IO.File]::ReadAllBytes( $( resolve-path $crlFilePath) )

    # Load the Local and remote CRL's and ARL's for comparison:

    [System.Reflection.Assembly]::LoadFrom("C:\Windows\System32\certsrv\CertEnroll\BouncyCastle.Crypto.dll")            

    [Org.BouncyCastle.X509.X509CrlParser]$CrlXP = new-object Org.BouncyCastle.X509.X509CrlParser;

    [Org.BouncyCastle.X509.X509Crl] $Local_CRL = $CrlXP.ReadCrl($crl)

    [Org.BouncyCastle.X509.X509CrlParser]$ArlXP = new-object Org.BouncyCastle.X509.X509CrlParser;

    [Org.BouncyCastle.X509.X509Crl] $Local_ARL = $ArlXP.ReadCrl($arl)

    #set CRL and ARL property values in LDAP

    [byte[]] $LDAPCRL = $directoryEntry.Properties["certificateRevocationList"][0]

    [byte[]] $LDAPARL = $directoryEntry.Properties["authorityRevocationList"][0]

                                                                                                                                                       [Org.BouncyCastle.X509.X509CrlParser]$CrlXP = new-object Org.BouncyCastle.X509.X509CrlParser;

    [Org.BouncyCastle.X509.X509Crl] $Remote_CRL = $CrlXP.ReadCrl($LDAPCRL)

    [Org.BouncyCastle.X509.X509CrlParser]$ArlXP = new-object Org.BouncyCastle.X509.X509CrlParser;

    [Org.BouncyCastle.X509.X509Crl] $Remote_ARL = $ArlXP.ReadCrl($LDAPARL)

    ~Gordon

  • Great script. I haven't yet looked in greater detail into it.

    You discribe in step 3 it [...]

    Checks the NextUpdate value of the Master CRL to make sure is has not expired. (Note that the Mono library adds hours to the NextUpdate and ThisUpdate values, control this time difference with the $creep variable)

    [...]

    I don't believe mono is simlpy adding hours to these values but simply displays UTC (as it should).

    PS C:\Scripts> $CRLNextUpdate = get-CRLNextUpdate C:\Scripts\my.crl

    PS C:\Scripts> $CRLNextUpdate

    Samstag, 5. Februar 2011 15:13:14

    Converting it to UTC gives the same result:

    PS C:\Scripts> $CRLNextUpdate.ToUniversalTime()

    Samstag, 5. Februar 2011 15:13:14

    Converting it to local Time is one hour ahead (Consider I'm in Europe time zone Berlin, Zürich, Prague)

    PS C:\Scripts> $CRLNextUpdate.ToLocalTime()

    Samstag, 5. Februar 2011 16:13:14

    So you may want to change:

    $CRL_Master.ThisUpdate.AddHours($creep)  

    to

    $CRL_Master.ThisUpdate.ToLocalTime()

    Regards

    patrick -at- sczepanski -dot- com

  • Thanks for all the feedback. I work on implementing/improving the script. I'll repost.

  • Thank you very much for sharing the script!

    NextCRLPublish and CRLNumber are available via Mono.Security. They are just not that easy accessible, since both are "hidden" in the extensions. But if you know heir OIDs you can get them in this way (NextCRLPublish being in UTC):

    $CRL_Master_NextCRLPublish = [Mono.Security.ASN1Convert]::ToDateTime($CDP1_CRL.Extensions["1.3.6.1.4.1.311.21.4"].ASN1[1].Value)

    $CRL_Master_CRLNumber = [Mono.Security.ASN1Convert]::ToInt32($CRL_Master.Extensions["2.5.29.20"].ASN1[1].Value)

    Regards

    Jakob.

  • Oooops, typo!

    Should be:

    $CRL_Master_NextCRLPublish = [Mono.Security.ASN1Convert]::ToDateTime($CRL_Master.Extensions["1.3.6.1.4.1.311.21.4"].ASN1[1].Value)

    $CRL_Master_CRLNumber = [Mono.Security.ASN1Convert]::ToInt32($CRL_Master.Extensions["2.5.29.20"].ASN1[1].Value)

    Regards

    Jakob.

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment