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 } } |