Posts on this page:

Basic CRL parser for PowerShell

As it is known that PowerShell natively don't support CRLs, but there are other ways to deal with them. For example use Quest Software AD cmdlets (read more: CRL's and PowerShell). Just for fun (and other useful stuff) I wrote manual parser for CRL files. CRL ASN.1 example can be found in RFC 5280 Appendix C.4. By using this info and, any ASN.1 editor and CRL_INFO Structure it is possible to write your own parser. Though this is not a best choice and I publish the code for demonstration purposes only:

function Parse-CRL ([byte[]]$crl, [switch]$Debug) {
Add-Type @'
using System;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace System
{
    namespace Security
    {
        namespace Cryptography
        {
            namespace X509Certificates
            {
                public class X509CRLRevokedCertificate
                {
                    public string SerialNumber;
                    public DateTime RevocationDate;
                }
                public class X509CRLv2
                {
                    public string Version;
                    public X500DistinguishedName IssuerDN;
                    public string Issuer;
                    public DateTime EffectiveDate;
                    public DateTime NextUpdate;
                    public Oid SignatureAlgorithm;
                    public Oid SignatureHashAlgorithm;
                    public X509CRLRevokedCertificate[] RevocationList;
                    public X509Extension[] Extensions;
                    public byte[] Signature;
                    public byte[] RawData;
                }
            }
        }
    }
}
'@
    function Get-ASNLength ($crl, $offset) {
        $return = "" | Select FullLength, Padding, LengthBytes, PayLoadLength
        if ($crl[$offset + 1] -lt 128) {
            $return.lengthbytes = 1
            $return.Padding = 0
            $return.PayLoadLength = $crl[$offset + 1]
            $return.FullLength = $return.Padding + $return.lengthbytes + $return.PayLoadLength + 1
        } else {
            $return.lengthbytes = $crl[$offset + 1] - 128
            $return.Padding = 1
            $lengthstring = -join ($crl[($offset + 2)..($offset + 1 + $return.lengthbytes)] | %{"{0:x2}" -f $_})
            $return.PayLoadLength = Invoke-Expression 0x$($lengthstring)
            $return.FullLength = $return.Padding + $return.lengthbytes + $return.PayLoadLength + 1
        }
        $return
    }
    function Get-CRLInfo ($crl, $offset, $RLPLength) {
        $remaining = $RLPLength.PayloadLength
        $offset = $offset + $RLPLength.Padding + $RLPLength.lengthbytes + 1
        do {
            $RLEntry = New-Object Security.Cryptography.X509Certificates.X509CRLRevokedCertificate
            if ($crl[$offset] -ne 48) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)" -ForegroundColor Red; break}
            $CASNLength = Get-ASNLength $crl $offset
            $offset = $offset + $CASNLength.Padding + $CASNLength.lengthbytes + 1
            if ($crl[$offset] -ne 2) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x02 (2)" -ForegroundColor Red; break}
            $SNASN = Get-ASNLength $crl $offset
            $SN = -join ($crl[($offset + $SNASN.Padding + $SNASN.lengthbytes + 1)..($offset + $SNASN.FullLength -1)] | %{"{0:x2}" -f $_})
            Write-Debug "Revoked certificate serial number is: $SN"
            $RLEntry.SerialNumber = $SN
            $offset = $offset + $SNASN.FullLength
            if ($crl[$offset] -ne 0x17) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x17 (23)"; break}
            if ($crl[$offset + 1] -ne 0x0d) {Write-Host "ASN1 bad DateTime length value met at offset $($offset + 1). Expected 0x0d (13)"; break}
            if ($crl[$offset + 14] -ne 0x5a) {Write-Host "ASN1 bad DateTime time zone value met at offset $($offset + 14). Expected 0x0d (90)"; break}
            $RawData = $crl[$offset..($offset + 14)]
            $string = [Text.Encoding]::ASCII.GetString($RawData,2,12)
            $DateTime = [datetime]::ParseExact($string,"yyMMddHHmmss",$null)
            Write-Debug "Revocation date is: $($DateTime.ToLocalTime().ToString())"
            $RLEntry.RevocationDate = $DateTime
            $global:offset = $offset += 15
            $remaining -= $CASNLength.FullLength
            $CRLObject.RevocationList += ,$RLEntry
        } while ($remaining -gt $CASNLength.Padding + $CASNLength.Lengthbytes + 2)
    }
    function Get-CRLExtension ($crl, $offset, $CRLELength) {
        $remaining = $CRLELength.PayloadLength
        $offset = $offset + $CRLELength.Padding + $CRLELength.lengthbytes + 1
        if ($crl[$offset] -ne 48) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)" -ForegroundColor Red; break}
        $ExtensionsLength = Get-ASNLength $crl $offset
        $offset = $offset + $ExtensionsLength.Padding + $ExtensionsLength.lengthbytes + 1
        Write-Debug "CRL extensions offset is: $offset"
        do {
            $CASNOffset = $offset
            if ($crl[$offset] -ne 48) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)" -ForegroundColor Red; break}
            $CurrentExtension = Get-ASNLength $crl $offset
            Write-Debug "New extension offset: $offset"
            $offset = $offset + $CurrentExtension.Padding + $CurrentExtension.lengthbytes + 1
            if ($crl[$offset] -ne 6) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x06 (6)" -ForegroundColor Red; break}
            $Oidlength = Get-ASNLength $crl $offset
            $ConstructedOID = 0x30, $OIDLength.FullLength, 0x06, $OIDLength.PayLoadLength
            [byte[]]$ConstructedOID += $crl[($offset + $OIDLength.Padding + $OIDLength.lengthbytes + 1)..($offset + $OIDLength.FullLength - 1)]
            $ExtensionOID = New-Object Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension $ConstructedOID, $false
            Write-Debug "Extension OID is: $($ExtensionOID.EnhancedKeyUsages[0].FriendlyName)"
            $offset = $offset + $OIDLength.FullLength
            if ($crl[$offset] -ne 4) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x04 (4)" -ForegroundColor Red; break}
            $BSTR = Get-ASNLength $crl $offset
            $offset = $offset + $BSTR.Padding + $BSTR.lengthbytes + 1
            $ExtValueLength = Get-ASNLength $crl $offset
            $RawData = $crl[$offset..($offset + $ExtValueLength.FullLength - 1)]
            $Extension = New-Object Security.Cryptography.X509Certificates.X509Extension $ExtensionOID.EnhancedKeyUsages[0].Value, $RawData, $false
            Write-Debug "Extension value: $($Extension.Format($false))"
            $CRLObject.Extensions += $Extension
            $offset = $casnoffset + $CurrentExtension.FullLength
            $remaining = $Remaining - $CurrentExtension.FullLength
            $global:offset = $offset
        } while ($remaining -gt ($ExtensionsLength.Padding + $ExtensionLength.LengthBytes + 2))
    }
    if ($PSBoundParameters.Debug) {$DebugPreference = "continue"}
    $CRLObject = New-Object Security.Cryptography.X509Certificates.x509CRLv2
    # determine actual file size
    $length = $crl.count
    # set initial offset
    $offset = 0
    Write-Debug "Initial offset is: $offset"
    if ($crl[$offset] -ne 48) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)" -ForegroundColor Red; break}
    $FullFile = Get-ASNLength $crl $offset
    if ($crl.count -eq $FullFile.FullLength) {Write-Debug "Valid data length: $($FullFile.FullLength)"}
    else {Write-Host "Invalid data length. Actual length is $($crl.Count), expected: $($FullFile.FullLength)"; break}
    $CRLObject.RawData = $crl
    $offset = $offset + $FullFile.Padding + $FullFile.lengthbytes + 1
    Write-Debug "CRL payload offset is: $offset"
    if ($crl[$offset] -ne 48) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)"; break}
    $PayloadLength = Get-ASNLength $crl $offset
    Write-Debug "Payload calculated length: $($ASNLength.PayloadLength)"
    $offset = $offset +  $PayloadLength.Padding + $PayloadLength.lengthbytes + 1
    Write-Debug "current offset: $offset"
    Write-Debug "Determine CRL version..."
    if ($crl[$offset] -eq 2) {
        $CRLVersionLength = Get-ASNLength $crl ($offset + 1)
        $CRLVersion = $crl[($offset + $CRLVersionLength.lengthbytes + 1)] + 1
        Write-Debug "CRL Version is: $CRLVersion"
        $offset = $offset + 3
    } elseif ($crl[$offset] -eq 48) {
        $CRLVersion = 1
        Write-Debug "CRL Version is: $CRLVersion"
    } else {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)"; break}
    $CRLObject.Version = $CRLVersion
    Write-Debug "current offset: $offset"
    Write-Debug "Determine signature algorithm..."
    $SignatureLength = Get-ASNLength $crl $offset
    $RawData = $crl[$offset..($offset + 1 + $SignatureLength.PayLoadLength)]
    $diff = $Rawdata[1] - $RawData[3] - 2
    Write-Debug "Algorithm Parameters length: $diff bytes"
    $RawData[1] = $RawData[1] - $diff
    [byte[]]$RawData1 = $RawData[0..($RawData[1] + 1)]
    $SignatureOID = New-Object Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension $RawData1, $false
    Write-Debug "CRL signature algorithm is: $($SignatureOID.EnhancedKeyUsages[0].FriendlyName)"
    $CRLObject.SignatureAlgorithm = $SignatureOID.EnhancedKeyUsages[0]
    Write-Debug "Algorithm Parameters: $($RawData[($RawData.Count - $diff)..($RawData.count - 1)] | %{'{0:x2}' -f $_})"
    $offset = $offset + $crl[$offset + 1] + $diff
    if ($crl[($offset)] -ne 48) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x30 (48)"}
    Write-Debug "current offset: $offset"
    Write-Debug "Retrieving CRL Issuer name..."
    $IssuerLength = Get-ASNLength $crl $offset
    Write-Debug "CRL Issuer length: $($IssuerLength.PayLoadLength)"
    [Byte[]]$RawData = $crl[$offset..($offset + $IssuerLength.FullLength - 1)]
    $IssuerDN = New-Object Security.Cryptography.X509Certificates.X500DistinguishedName @(,$RawData)
    Write-Debug "CRL Issuer is: $($IssuerDN.Name)"
    $CRLObject.IssuerDN = $IssuerDN
    $CRLObject.Issuer = $IssuerDN.Name
    $offset = $offset + $IssuerLength.FullLength
    Write-Debug "EffectiveDate offset is: $offset"
    Write-Debug "Retrieving EffectiveDate value..."
    if ($crl[$offset] -ne 0x17) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x17 (23)"; break}
    if ($crl[$offset + 1] -ne 0x0d) {Write-Host "ASN1 bad DateTime length value met at offset $($offset + 1). Expected 0x0d (13)"; break}
    if ($crl[$offset + 14] -ne 0x5a) {Write-Host "ASN1 bad DateTime time zone value met at offset $($offset + 14). Expected 0x0d (90)"; break}
    $RawData = $crl[$offset..($offset + 14)]
    $string = [Text.Encoding]::ASCII.GetString($RawData,2,12)
    $DateTime = [datetime]::ParseExact($string, "yyMMddHHmmss", $null)
    Write-Debug "EffectiveDate value is: $($DateTime.ToLocalTime().ToString())"
    $CRLObject.EffectiveDate = $DateTime.ToLocalTime()
    $offset += 15
    Write-Debug "NextPublish offset is: $offset"
    Write-Debug "Retrieving NextPublish value..."
    if ($crl[$offset] -ne 0x17) {Write-Host "ASN1 bad tag value met at offset $offset. Expected 0x17 (23)"; break}
    if ($crl[$offset + 1] -ne 0x0d) {Write-Host "ASN1 bad DateTime length value met at offset $($offset + 1). Expected 0x0d (13)"; break}
    if ($crl[$offset + 14] -ne 0x5a) {Write-Host "ASN1 bad DateTime time zone value met at offset $($offset + 14). Expected 0x0d (90)"; break}
    $RawData = $crl[$offset..($offset + 14)]
    $string = [Text.Encoding]::ASCII.GetString($RawData,2,12)
    $DateTime = [datetime]::ParseExact($string, "yyMMddHHmmss", $null)
    Write-Debug "NextUpdate value is: $($DateTime.ToLocalTime().ToString())"
    $CRLObject.NextUpdate = $DateTime.ToLocalTime()
    $offset += 15
    Write-Debug "We are reached RevokedList sequence!"
    Write-Debug "current offset: $offset"
    if ($crl[$offset] -eq 48) {
        $RLPLength = Get-ASNLength $crl $offset
        Write-Debug "RevokedList payload length is: $($RLPLength.PayLoadLength)"
        Get-CRLInfo $crl $offset $RLPLength
        $offset = $global:offset
    }
    Write-Debug "CRL extensions offset is: $offset"
    Write-Debug "Retrieving CRL extensions value..."
    if($crl[$offset] -eq 160) {
        $CRLELength = Get-ASNLength $crl $offset
        Get-CRLExtension $crl $offset $CRLELength
        $offset = $global:offset
        Write-Debug "Current offset is: $offset"
    }
    $CRLObject
}

the function parses CRL file as follows: there is well-known structure sequence with encoded data inside. For example:

   0  352: SEQUENCE {
   4  202:   SEQUENCE {
   7    1:     INTEGER 1
  10   13:     SEQUENCE {
  12    9:       OBJECT IDENTIFIER
         :         sha1withRSAEncryption (1 2 840 113549 1 1 5)
  23    0:       NULL
         :       }

First sequence represents whole CRL file. Second sequence represents CRL payload (data without signature). Inside this sequence are defined all CRL fields. First field is CRL version (data type — integer). CRL version value is determined as: integer + 1. Therefore in a given example we have CRL version = 2 (1 + 1). As described in CRL_Info Structure the next field is SignatureAlgorithm and so on. I wrote little helper function called Get-ASNLength that extracts current (depending on current offset) ASN.1 object (SEQUENCE, OBJECT IDENTIFIER, BIT STRING, DATE, etc) length. By excluding this length we can get pure encoded data and decode it. After that we move over this length to the next object or structure and repeat this task. Aftter SignatureAlgorithm we have Issuer entry. As it is possible, I used standard .NET classes to represent certain object. In that case I don't need to parse Issuer object, but put it to .NET X500DistinguishedName object.

As I have tested, it works great, but pretty slow. PowerShell is not the best tool for parsing such data. If you have a large CRL (50kb and above) this may take a lot of time (more thatn 1 minute). Therefore this code is just an example of CRL parser.

Add/Remove Certificate Enrollment Policy service using PowerShell

Hello again! Continuing Certificate Enrollment Service (CES) and Certificate Enrollment Policy (CEP) service subject I would like to post another PowerShell script that will install and remove CEP service. Like CES, CEP CryptoAPI COM interface is not documented yet. However you can manually find this interface in your registry:

these keys are located in HKEY_CLASSES_ROOT hive. Actually there are a lot of interesting things that are not documented on MSDN. I don't know why, so don't ask me why, I'm not Microsoft guy. The code is quite similar as posted in the previous post, so I don't think that I need to additionally explain something else. Yes, I know, I'm bad PowerShell MVP, because my posts are quite complex and not all PowerShell users can understand it. Instead I provide (ate least I try) finished solutions for end-users. They just run my scripts and get the fun (or PROFIT!!!). Let's go:


Read more →

Add multiple Certificate Enrollment Service instances

Sometimes I don't understand Microsoft. They a lot of useful things, but thing implementation is quite poor. For example in Windows Server 2008 R2 we have an option to install certificate enrollment service (hereinafter CES) that will allow to securely enroll certificates outside of domain network perimeter. Also CES allows to enroll certificates from non-domain clients. Here is excellent whitepaper about the subject: Certificate Enrollment Web Services in Windows Server 2008 R2. You can setup only one CES instance via Server Manager snap-in. What if we have multiple CA servers and we need to configure CES to work with them? For example, one CA is configured to issue user certificates only and another CA is configured to issue computer certificates only. Also we need to issue these certificates to external clients. In that case we need to setup at least two Windows Server 2008 R2 servers, assign them public IP address and install required CES instance on each CES server. This is pretty ugly. Hopefully there is a trick to install additional CES instances on the same server via CryptoAPI COM interface: CERTOCM.CertificateEnrollmentServerSetup. Currently this interface is not documented on MSDN, therefore I cannot provide interface explanation links. However I wrote PowerShell script that will add additional CES instance and remove specified or all CES instances from local computer. I have commented some code parts for understanding, but the code generally is self-explanatory.


Read more →

How to encode Object Identifier to an ASN.1 DER encoded string

Looking to my previous posts I've noticed that I haven't described the methods how certificate extensions are encoded. Cryptography in overall relies on encoded data. For example, digital certificate is a byte array that contains encoded certificate fields. All certificate content is encoded using Abstract Syntax Notation 1 Distinguished Encoding Rules (simply ASN.1 DER). If certificate is stored in Base64 string format, system just converts Base64 content to a byte array. There are several encoding rules for each data type. For example, object identifiers (OIDs) has their own encoding rules, DateTime — their own encoding rules and so on.

In this post I would like to demonstrate encoding rules for Object Identifier data type. Object Identifiers are used to encode Enhanced Key Usage, Application Policies, Certificate Policies and other certificate extensions. The following format is used:

  • Tag value
  • String length
  • Data type
  • Actual data string length
  • Encoded data string

Read more →

New-OpsMgrRequest and Install-OpsMgrCertificate (revisited)

In my previous posts: New-OpsMgrRequest and Install-OpsMgrCertificate I posted two nice scripts. However there is a little bug that operating system version is not recognized correctly. Also these scripts have limited Windows versions support — only Windows Vista and higher. Now I have updated both scripts by fixing several bugs and added Windows XP/Windows Server 2003 (including R2) support. The following scripts demonstrates as well as CertEnroll and XEnroll CryptoAPI interfaces and how you can deal with them in Windows PowerShell. Here is an updated code:


Read more →