Давным давно я мечтал об удобном (ну или не очень, но чтобы стандартном) способе работы с CRL'ами в PowerShell. Ленивые разрабы .NET'а как не чесались, так и до сих пор не чешутся по этому поводу, хотя, задача весьма востребованная. Я несколько раз пытался подойти к решению этого вопроса самостоятельно. Но незнание чего-то не позволяло этого достичь. Но не так давно в качестве практического упражнения в ASN.1 я написал вот такой страшный парсер: Basic CRL parser for PowerShell. Это даже не концепт, а просто отработка навыков работы с ASN.1. Но в виду его неуниверсальности, розовомедленности его даже стыдно запускать и показывать как он работает. Неделю назад я снова вспомнил об этой теме и решил подойти к вопросу более основательно и написал код с использованием p/invoke неуправляемого кода и с блек-джеком и шлюхами, который используется стандартыми Windows инструментами для работы с CRL объектами. Я не буду рассказывать об истории его создания и как он там внутри работает, потому что это лишено всякого смысла. Взамен я предлагаю рабочий код, который вы можете использовать в собственных целях:
#####################################################################
# Get-CRL.ps1
# Version 1.0
#
# Retrieves CRL object from a file or a DER-encoded byte array.
#
# Vadims Podans (c) 2011
# http://www.sysadmins.lv/
#####################################################################
#requires -Version 2.0
function Get-CRL {
<#
.Synopsis
Retrieves CRL object from a file or a DER-encoded byte array.
.Description
Retrieves CRL object from a file or a DER-encoded byte array.
.Parameter Path
Specifies the path to a file.
.Parameter RawCRL
Specifies a pointer to a DER-encoded CRL byte array.
.Example
Get-CRL C:\Custom.crl
Returns X509CRL2 object from a specified file
.Example
$Raw = [IO.FILE]::ReadAllBytes("C:\Custom.crl")
Get-CRL -RawCRL $Raw
Returns X509CRL2 object from a DER-encoded byte array.
.Outputs
System.Security.Cryptography.X509Certificates.X509CRL2
.NOTES
Author: Vadims Podans
Blog : http://en-us.sysadmins.lv
#>
[OutputType('System.Security.Cryptography.X509Certificates.X509CRL2')]
[CmdletBinding(DefaultParameterSetName='FileName')]
param(
[Parameter(ParameterSetName = "FileName", Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Path,
[Parameter(ParameterSetName = "RawData", Mandatory = $true, Position = 0)]
[Byte[]]$RawCRL
)
#region content parser
switch ($PsCmdlet.ParameterSetName) {
"FileName" {
if ($(Get-Item $Path -ErrorAction Stop).PSProvider.Name -ne "FileSystem") {
throw {"File either does not exist or not a file object"}
}
if ($(Get-Item $Path -ErrorAction Stop).Extension -ne ".crl") {
throw {"File is not valid CRL file"}
}
$Content = Get-Content $Path
if ($Content[0] -eq "-----BEGIN X509 CRL-----") {
[Byte[]]$cBytes = [Convert]::FromBase64String($(-join $Content[1..($Content.Count - 2)]))
} elseif ($Content[0][0] -eq "M") {
[Byte[]]$cBytes = [Convert]::FromBase64String($(-join $Content))
} else {
[Byte[]]$cBytes = [IO.File]::ReadAllBytes($Path)
}
}
"RawData" {[Byte[]]$cBytes = $RawCRL}
}
#endregion
$signature = @"
[DllImport("CRYPT32.DLL", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int CertCreateCRLContext(
int dwCertEncodingType,
byte[] pbCrlEncoded,
int cbCrlEncoded
);
[DllImport("CRYPT32.DLL", SetLastError = true)]
public static extern Boolean CertFreeCRLContext(
IntPtr pCrlContext
);
[DllImport("CRYPT32.DLL", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int CertNameToStr(
int dwCertEncodingType,
ref CRYPTOAPI_BLOB pName,
int dwStrType,
System.Text.StringBuilder psz,
int csz
);
[DllImport("CRYPT32.DLL", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CertFindExtension(
[MarshalAs(UnmanagedType.LPStr)]String pszObjId,
int cExtensions,
IntPtr rgExtensions
);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRL_CONTEXT
{
public int dwCertEncodingType;
public byte[] pbCrlEncoded;
public int cbCrlEncoded;
public IntPtr pCrlInfo;
public IntPtr hCertStore;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRL_INFO
{
public int dwVersion;
public CRYPT_ALGORITHM_IDENTIFIER SignatureAlgorithm;
public CRYPTOAPI_BLOB Issuer;
public Int64 ThisUpdate;
public Int64 NextUpdate;
public int cCRLEntry;
public IntPtr rgCRLEntry;
public int cExtension;
public IntPtr rgExtension;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPT_ALGORITHM_IDENTIFIER
{
[MarshalAs(UnmanagedType.LPStr)]public String pszObjId;
public CRYPTOAPI_BLOB Parameters;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRYPTOAPI_BLOB
{
public int cbData;
public IntPtr pbData;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CRL_ENTRY
{
public CRYPTOAPI_BLOB SerialNumber;
public Int64 RevocationDate;
public int cExtension;
public IntPtr rgExtension;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct CERT_EXTENSION
{
[MarshalAs(UnmanagedType.LPStr)]public String pszObjId;
public Boolean fCritical;
public CRYPTOAPI_BLOB Value;
}
"@
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 X509CRL2
{
public int Version;
public string Type;
public X500DistinguishedName IssuerDN;
public string Issuer;
public DateTime ThisUpdate;
public DateTime NextUpdate;
public Oid SignatureAlgorithm;
public X509ExtensionCollection Extensions;
public X509CRLEntry[] RevokedCertificates;
public byte[] RawData;
}
public class X509CRLEntry
{
public string SerialNumber;
public DateTime RevocationDate;
public int ReasonCode;
public string ReasonMessage;
}
}
}
}
}
"@
try {Add-Type -MemberDefinition $signature -Namespace PKI -Name CRL}
catch {throw "Unable to load required types"}
#region Variables
[IntPtr]$pvContext = [IntPtr]::Zero
[IntPtr]$rgCRLEntry = [IntPtr]::Zero
[IntPtr]$pByte = [IntPtr]::Zero
[byte]$bByte = 0
[IntPtr]$rgExtension = [IntPtr]::Zero
$ptr = [IntPtr]::Zero
$Reasons = @{1="Key compromise";2="CA Compromise";3="Change of Affiliation";4="Superseded";5="Cease Of Operation";
6="Hold Certificiate";7="Privilege Withdrawn";10="aA Compromise"}
#endregion
# retrive CRL context and CRL_CONTEXT structure
$pvContext = [PKI.CRL]::CertCreateCRLContext(65537,$cBytes,$cBytes.Count)
if ($pvContext.Equals([IntPtr]::Zero)) {throw "Unable to retrieve context"}
$CRL = New-Object System.Security.Cryptography.X509Certificates.X509CRL2
# void first marshaling operation, because it throws unexpected exception
try {$CRLContext = [Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]$pvContext,[PKI.CRL+CRL_CONTEXT])} catch {}
$CRLContext = [Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]$pvContext,[PKI.CRL+CRL_CONTEXT])
$CRLInfo = [Runtime.InteropServices.Marshal]::PtrToStructure($CRLContext.pCrlInfo,[PKI.CRL+CRL_INFO])
$CRL.Version = $CRLInfo.dwVersion + 1
$CRL.Type = "Base CRL"
$CRL.RawData = $cBytes
$CRL.SignatureAlgorithm = New-Object Security.Cryptography.Oid $CRLInfo.SignatureAlgorithm.pszObjId
$CRL.ThisUpdate = [datetime]::FromFileTime($CRLInfo.ThisUpdate)
$CRL.NextUpdate = [datetime]::FromFileTime($CRLInfo.NextUpdate)
$csz = [PKI.CRL]::CertNameToStr(65537,[ref]$CRLInfo.Issuer,3,$null,0)
$psz = New-Object text.StringBuilder $csz
$csz = [PKI.CRL]::CertNameToStr(65537,[ref]$CRLInfo.Issuer,3,$psz,$csz)
$CRL.IssuerDN = New-Object Security.Cryptography.X509Certificates.X500DistinguishedName $psz
$CRL.Issuer = $CRL.IssuerDN.Format(0)
$rgCRLEntry = $CRLInfo.rgCRLEntry
if ($CRLInfo.cCRLEntry -ge 1) {
for ($n = 0; $n -lt $CRLInfo.cCRLEntry; $n++) {
$Entry = New-Object System.Security.Cryptography.X509Certificates.X509CRLEntry
$SerialNumber = ""
$CRLEntry = [Runtime.InteropServices.Marshal]::PtrToStructure($rgCRLEntry,[PKI.CRL+CRL_ENTRY])
$pByte = $CRLEntry.SerialNumber.pbData
$SerialNumber = ""
for ($m = 0; $m -lt $CRLEntry.SerialNumber.cbData; $m++) {
$bByte = [Runtime.InteropServices.Marshal]::ReadByte($pByte)
$SerialNumber = "{0:x2}" -f $bByte + $SerialNumber
$pByte = [int]$pByte + [Runtime.InteropServices.Marshal]::SizeOf([byte])
}
$Entry.SerialNumber = $SerialNumber
$Entry.RevocationDate = [datetime]::FromFileTime($CRLEntry.RevocationDate)
$CRLReasonCode = ""
[IntPtr]$rcExtension = [PKI.CRL]::CertFindExtension("2.5.29.21",$CRLEntry.cExtension,$CRLEntry.rgExtension)
if (!$rcExtension.Equals([IntPtr]::Zero)) {
$CRLExtension = [Runtime.InteropServices.Marshal]::PtrToStructure($rcExtension,[PKI.CRL+CERT_EXTENSION])
$pByte = $CRLExtension.Value.pbData
$bBytes = $null
for ($m = 0; $m -lt $CRLExtension.Value.cbData; $m++) {
$bByte = [Runtime.InteropServices.Marshal]::ReadByte($pByte)
[Byte[]]$bBytes += $bByte
$pByte = [int]$pByte + [Runtime.InteropServices.Marshal]::SizeOf([byte])
}
$Entry.ReasonCode = $bBytes[2]
$Entry.ReasonMessage = $Reasons[$Entry.ReasonCode]
}
$CRL.RevokedCertificates += $Entry
$rgCRLEntry = [int]$rgCRLEntry + [Runtime.InteropServices.Marshal]::SizeOf([PKI.CRL+CRL_ENTRY])
}
}
$rgExtension = $CRLInfo.rgExtension
if ($CRLInfo.cExtension -ge 1) {
$Exts = New-Object Security.Cryptography.X509Certificates.X509ExtensionCollection
for ($n = 0; $n -lt $CRLInfo.cExtension; $n++) {
$ExtEntry = [Runtime.InteropServices.Marshal]::PtrToStructure($rgExtension,[PKI.CRL+CERT_EXTENSION])
[IntPtr]$rgExtension = [PKI.CRL]::CertFindExtension($ExtEntry.pszObjId,$CRLInfo.cExtension,$CRLInfo.rgExtension)
$pByte = $ExtEntry.Value.pbData
$bBytes = $null
for ($m = 0; $m -lt $ExtEntry.Value.cbData; $m++) {
[byte[]]$bBytes += [Runtime.InteropServices.Marshal]::ReadByte($pByte)
$pByte = [int]$pByte + [Runtime.InteropServices.Marshal]::SizeOf([byte])
}
$ext = New-Object Security.Cryptography.X509Certificates.X509Extension $ExtEntry.pszObjId, @([Byte[]]$bBytes), $ExtEntry.fCritical
[void]$Exts.Add($ext)
$rgExtension = [int]$rgExtension + [Runtime.InteropServices.Marshal]::SizeOf([PKI.CRL+CERT_EXTENSION])
}
if ($exts | ?{$_.Oid.Value -eq "2.5.29.27"}) {$CRL.Type = "Delta CRL"}
$CRL.Extensions = $Exts
}
$CRL
[void][PKI.CRL]::CertFreeCRLContext($pvContext)
}
И, собственно, его вывод:
[↓] [vPodans] Get-CRL .\Desktop\pica-1.crl
Version : 2
Type : Base CRL
IssuerDN : System.Security.Cryptography.X509Certificates.X500DistinguishedName
Issuer : CN=Sysadmins LV Internal Class 1 SubCA-1, OU=Information Systems, O=Sysadmins LV, C=LV
ThisUpdate : 22.02.2011 19:22:27
NextUpdate : 26.02.2011 19:42:27
SignatureAlgorithm : System.Security.Cryptography.Oid
Extensions : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography
.Oid, System.Security.Cryptography.Oid...}
RevokedCertificates : {System.Security.Cryptography.X509Certificates.X509CRLEntry, System.Security.Cryptography.X509Cer
tificates.X509CRLEntry, System.Security.Cryptography.X509Certificates.X509CRLEntry, System.Securi
ty.Cryptography.X509Certificates.X509CRLEntry...}
RawData : {48, 130, 3, 39...}
[↓] [vPodans]
И некоторые внутренности:
[↓] [vPodans] $crl = Get-CRL .\Desktop\pica-1.crl
[↓] [vPodans] $crl.Extensions[0].format(0)
KeyID=1b fa 5e 73 2d 67 13 5c ce d3 0e e6 e8 7a a9 60 8c 0b 63 fc
[↓] [vPodans] $crl.Extensions[4].format(1)
[1]Freshest CRL
Distribution Point Name:
Full Name:
URL=http://www.sysadmins.lv/pki/pica-1+.crl
[↓] [vPodans] $crl.RevokedCertificates
SerialNumber RevocationDate ReasonCode ReasonMessage
------------ -------------- ---------- -------------
3bfe8e77000000000078 27.12.2010 19:32:00 0
163c8142000000000072 27.11.2010 23:27:00 5 Cease Of Operation
14d70748000000000071 27.11.2010 23:27:00 5 Cease Of Operation
411726e0000000000054 04.08.2010 21:26:00 0
1cee2e2000000000002b 01.05.2010 15:32:00 0
2ee0af5a000000000021 24.04.2010 22:25:00 0
[↓] [vPodans] $crl.GetType().FullName
System.Security.Cryptography.X509Certificates.X509CRL2
[↓] [vPodans]