Private/ConvertFrom-CwCrl.ps1

function ConvertFrom-CwCrl {
    <#
    .SYNOPSIS
        Parses a DER-encoded X.509 CRL (CertificateList) without external tools.
    .DESCRIPTION
        Minimal, dependency-free ASN.1/DER walker over the CRL structure:
 
          CertificateList ::= SEQUENCE {
              tbsCertList TBSCertList,
              signatureAlgorithm AlgorithmIdentifier,
              signatureValue BIT STRING }
 
          TBSCertList ::= SEQUENCE {
              version INTEGER OPTIONAL,
              signature AlgorithmIdentifier,
              issuer Name,
              thisUpdate Time,
              nextUpdate Time OPTIONAL,
              revokedCertificates SEQUENCE OF ... OPTIONAL,
              crlExtensions [0] Extensions OPTIONAL }
 
        Extracts ThisUpdate/NextUpdate, the issuer DN, the Delta CRL Indicator
        (OID 2.5.29.27), the number of revoked entries, and keeps the raw
        TBS/signature bytes so callers can validate the CRL signature against
        the issuing CA certificate.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Bytes')]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Bytes')]
        [byte[]]$Bytes,

        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [string]$Path
    )

    if ($PSCmdlet.ParameterSetName -eq 'Path') {
        $Bytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $Path).ProviderPath)
    }

    # --- tiny DER TLV reader -------------------------------------------------
    # Returns @{ Tag; ContentStart; ContentLength; TotalLength } for the TLV at $Offset.
    $readTlv = {
        param([byte[]]$Data, [int]$Offset)
        if ($Offset + 2 -gt $Data.Length) { throw 'DER: truncated TLV header.' }
        $tag = $Data[$Offset]
        $lenByte = $Data[$Offset + 1]
        $headerLen = 2
        if ($lenByte -lt 0x80) {
            $len = [int]$lenByte
        }
        else {
            $numLenBytes = $lenByte -band 0x7F
            if ($numLenBytes -lt 1 -or $numLenBytes -gt 4) { throw "DER: unsupported length encoding ($numLenBytes bytes)." }
            $len = 0
            for ($i = 0; $i -lt $numLenBytes; $i++) {
                $len = ($len -shl 8) -bor $Data[$Offset + 2 + $i]
            }
            $headerLen = 2 + $numLenBytes
        }
        if ($Offset + $headerLen + $len -gt $Data.Length) { throw 'DER: TLV length exceeds buffer.' }
        @{
            Tag           = $tag
            ContentStart  = $Offset + $headerLen
            ContentLength = $len
            TotalLength   = $headerLen + $len
        }
    }

    $parseTime = {
        param([byte[]]$Data, [hashtable]$Tlv)
        $text = [System.Text.Encoding]::ASCII.GetString($Data, $Tlv.ContentStart, $Tlv.ContentLength).TrimEnd('Z')
        $format = if ($Tlv.Tag -eq 0x17) { 'yyMMddHHmmss' } else { 'yyyyMMddHHmmss' }
        [datetime]::SpecifyKind(
            [datetime]::ParseExact($text, $format, [System.Globalization.CultureInfo]::InvariantCulture),
            [System.DateTimeKind]::Utc)
    }

    # --- outer CertificateList ----------------------------------------------
    $outer = & $readTlv $Bytes 0
    if ($outer.Tag -ne 0x30) { throw 'Not a DER CRL: outer element is not a SEQUENCE.' }

    # tbsCertList (keep the full raw TLV for signature verification)
    $tbs = & $readTlv $Bytes $outer.ContentStart
    if ($tbs.Tag -ne 0x30) { throw 'Not a DER CRL: tbsCertList is not a SEQUENCE.' }
    $tbsBytes = New-Object byte[] $tbs.TotalLength
    [Array]::Copy($Bytes, $outer.ContentStart, $tbsBytes, 0, $tbs.TotalLength)

    $pos = $tbs.ContentStart
    $tbsEnd = $tbs.ContentStart + $tbs.ContentLength

    # version INTEGER (optional)
    $tlv = & $readTlv $Bytes $pos
    if ($tlv.Tag -eq 0x02) {
        $pos += $tlv.TotalLength
        $tlv = & $readTlv $Bytes $pos
    }

    # signature AlgorithmIdentifier (SEQUENCE) - skip
    if ($tlv.Tag -ne 0x30) { throw 'DER CRL: expected signature AlgorithmIdentifier.' }
    $pos += $tlv.TotalLength

    # issuer Name (SEQUENCE) - decode via X500DistinguishedName
    $tlv = & $readTlv $Bytes $pos
    if ($tlv.Tag -ne 0x30) { throw 'DER CRL: expected issuer Name.' }
    $issuerBytes = New-Object byte[] $tlv.TotalLength
    [Array]::Copy($Bytes, $pos, $issuerBytes, 0, $tlv.TotalLength)
    $issuer = try {
        (New-Object System.Security.Cryptography.X509Certificates.X500DistinguishedName(, $issuerBytes)).Name
    }
    catch { $null }
    $pos += $tlv.TotalLength

    # thisUpdate
    $tlv = & $readTlv $Bytes $pos
    if ($tlv.Tag -notin 0x17, 0x18) { throw 'DER CRL: expected thisUpdate time.' }
    $thisUpdate = & $parseTime $Bytes $tlv
    $pos += $tlv.TotalLength

    # nextUpdate (optional)
    $nextUpdate = $null
    $revokedCount = 0
    $isDelta = $false
    if ($pos -lt $tbsEnd) {
        $tlv = & $readTlv $Bytes $pos
        if ($tlv.Tag -in 0x17, 0x18) {
            $nextUpdate = & $parseTime $Bytes $tlv
            $pos += $tlv.TotalLength
        }
    }

    # revokedCertificates (optional SEQUENCE) - count entries only
    if ($pos -lt $tbsEnd) {
        $tlv = & $readTlv $Bytes $pos
        if ($tlv.Tag -eq 0x30) {
            $entryPos = $tlv.ContentStart
            $entryEnd = $tlv.ContentStart + $tlv.ContentLength
            while ($entryPos -lt $entryEnd) {
                $entry = & $readTlv $Bytes $entryPos
                $revokedCount++
                $entryPos += $entry.TotalLength
            }
            $pos += $tlv.TotalLength
        }
    }

    # crlExtensions [0] EXPLICIT (optional) - look for Delta CRL Indicator 2.5.29.27 (55 1D 1B)
    if ($pos -lt $tbsEnd) {
        $tlv = & $readTlv $Bytes $pos
        if ($tlv.Tag -eq 0xA0) {
            $extSeq = & $readTlv $Bytes $tlv.ContentStart
            $extPos = $extSeq.ContentStart
            $extEnd = $extSeq.ContentStart + $extSeq.ContentLength
            while ($extPos -lt $extEnd) {
                $ext = & $readTlv $Bytes $extPos
                $oidTlv = & $readTlv $Bytes $ext.ContentStart
                if ($oidTlv.Tag -eq 0x06 -and $oidTlv.ContentLength -eq 3 -and
                    $Bytes[$oidTlv.ContentStart] -eq 0x55 -and
                    $Bytes[$oidTlv.ContentStart + 1] -eq 0x1D -and
                    $Bytes[$oidTlv.ContentStart + 2] -eq 0x1B) {
                    $isDelta = $true
                }
                $extPos += $ext.TotalLength
            }
        }
    }

    # --- signatureAlgorithm + signatureValue --------------------------------
    $sigAlgPos = $outer.ContentStart + $tbs.TotalLength
    $sigAlgTlv = & $readTlv $Bytes $sigAlgPos
    $sigAlgOid = $null
    if ($sigAlgTlv.Tag -eq 0x30) {
        $oidTlv = & $readTlv $Bytes $sigAlgTlv.ContentStart
        if ($oidTlv.Tag -eq 0x06) {
            $oidHex = -join (($oidTlv.ContentStart)..($oidTlv.ContentStart + $oidTlv.ContentLength - 1) |
                ForEach-Object { $Bytes[$_].ToString('X2') })
            $sigAlgOid = switch ($oidHex) {
                '2A864886F70D01010B' { 'sha256RSA' }
                '2A864886F70D01010C' { 'sha384RSA' }
                '2A864886F70D01010D' { 'sha512RSA' }
                '2A864886F70D010105' { 'sha1RSA' }
                '2A8648CE3D040302'   { 'sha256ECDSA' }
                '2A8648CE3D040303'   { 'sha384ECDSA' }
                '2A8648CE3D040304'   { 'sha512ECDSA' }
                default              { "oid:$oidHex" }
            }
        }
    }

    $signatureBytes = $null
    $sigValPos = $sigAlgPos + $sigAlgTlv.TotalLength
    if ($sigValPos -lt $outer.ContentStart + $outer.ContentLength) {
        $sigValTlv = & $readTlv $Bytes $sigValPos
        if ($sigValTlv.Tag -eq 0x03 -and $sigValTlv.ContentLength -gt 1) {
            # first content byte = number of unused bits (0 for signatures)
            $signatureBytes = New-Object byte[] ($sigValTlv.ContentLength - 1)
            [Array]::Copy($Bytes, $sigValTlv.ContentStart + 1, $signatureBytes, 0, $sigValTlv.ContentLength - 1)
        }
    }

    [pscustomobject]@{
        Issuer             = $issuer
        ThisUpdate         = $thisUpdate
        NextUpdate         = $nextUpdate
        IsDelta            = $isDelta
        RevokedCount       = $revokedCount
        SignatureAlgorithm = $sigAlgOid
        TbsBytes           = $tbsBytes
        SignatureBytes     = $signatureBytes
    }
}