Загрузка данных
<#
.SYNOPSIS
Поиск сертификатов, развернутых через групповые политики (GPO),
с weak RSA-ключами и выводом Name Constraints.
.DESCRIPTION
Скрипт проверяет GPO-хранилища сертификатов в реестре:
HKLM:\SOFTWARE\Policies\Microsoft\SystemCertificates
HKCU:\SOFTWARE\Policies\Microsoft\SystemCertificates
а также (опционально) Enterprise-хранилище:
HKLM:\SOFTWARE\Microsoft\EnterpriseCertificates
Отбирает сертификаты, удовлетворяющие ЛЮБОМУ из условий:
1) RSA KeySize <= 2048 И NotAfter >= 2030-01-01
2) RSA KeySize <= 1024 (любая дата истечения)
Для каждого найденного сертификата дополнительно извлекает
расширение Name Constraints (OID 2.5.29.30) и выводит
разрешённые/запрещённые DNS-имена, IP-адреса, email-адреса и URI.
.NOTES
Для разбора Name Constraints требуется PowerShell 7+ (класс System.Formats.Asn1.AsnReader).
Для чтения HKLM-веток требуются права администратора.
#>
# ============================================================
# Параметры
# ============================================================
$maxKeySizeLongLived = 2048
$maxKeySizeAnyDate = 1024
$thresholdDate = Get-Date "2030-01-01 00:00:00"
# Пути к GPO-хранилищам в реестре
$gpoRegistryPaths = @(
@{ Path = "HKLM:\SOFTWARE\Policies\Microsoft\SystemCertificates"; Scope = "LocalMachine (GPO)"; CertProvider = "Cert:\LocalMachine" },
@{ Path = "HKCU:\SOFTWARE\Policies\Microsoft\SystemCertificates"; Scope = "CurrentUser (GPO)"; CertProvider = "Cert:\CurrentUser" }
# Раскомментируйте при необходимости:
# @{ Path = "HKLM:\SOFTWARE\Microsoft\EnterpriseCertificates"; Scope = "LocalMachine (Enterprise)"; CertProvider = "Cert:\LocalMachine" }
)
# ============================================================
# Функция: проверка условий weak RSA
# ============================================================
function Test-WeakRsaCert {
param(
[Parameter(Mandatory)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Cert
)
# Только RSA
if ($Cert.PublicKey.Oid.Value -ne "1.2.840.113549.1.1.1") { return $false }
$keySize = $Cert.PublicKey.Key.KeySize
$notAfter = $Cert.NotAfter
# Условие 1: <= 2048 И истекает в 2030 или позже
$cond1 = ($keySize -le $maxKeySizeLongLived) -and ($notAfter -ge $thresholdDate)
# Условие 2: <= 1024 (любая дата)
$cond2 = ($keySize -le $maxKeySizeAnyDate)
return ($cond1 -or $cond2)
}
# ============================================================
# Функция: извлечение Name Constraints (OID 2.5.29.30)
# ============================================================
function Get-NameConstraints {
param(
[Parameter(Mandatory)]
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
)
$result = [PSCustomObject]@{
PermittedDNS = @()
ExcludedDNS = @()
PermittedEmail = @()
ExcludedEmail = @()
PermittedIP = @()
ExcludedIP = @()
PermittedURI = @()
ExcludedURI = @()
}
if (-not ('System.Formats.Asn1.AsnReader' -as [type])) {
Write-Warning "Для разбора Name Constraints требуется PowerShell 7+ (System.Formats.Asn1.AsnReader)."
return $result
}
$extension = $Certificate.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.30' }
if (-not $extension) { return $result }
try {
$reader = [System.Formats.Asn1.AsnReader]::new(
[ReadOnlyMemory[byte]]$extension.RawData,
[System.Formats.Asn1.AsnEncodingRules]::DER
)
# NameConstraints ::= SEQUENCE {
# permittedSubtrees [0] GeneralSubtrees OPTIONAL,
# excludedSubtrees [1] GeneralSubtrees OPTIONAL }
$ncSeq = $reader.ReadSequence()
# --- permittedSubtrees [0] ---
if ($ncSeq.HasData -and $ncSeq.PeekTag().TagValue -eq 0) {
$permitted = $ncSeq.ReadSequence([System.Formats.Asn1.Asn1Tag]::ContextSpecific(0))
while ($permitted.HasData) {
$subtree = $permitted.ReadSequence()
$baseTag = $subtree.PeekTag()
switch ($baseTag.TagValue) {
1 { # rfc822Name
$value = $subtree.ReadCharacterString([System.Formats.Asn1.UniversalTagNumber]::IA5String)
$result.PermittedEmail += $value
}
2 { # dNSName
$value = $subtree.ReadCharacterString([System.Formats.Asn1.UniversalTagNumber]::IA5String)
$result.PermittedDNS += $value
}
6 { # uniformResourceIdentifier
$value = $subtree.ReadCharacterString([System.Formats.Asn1.UniversalTagNumber]::IA5String)
$result.PermittedURI += $value
}
7 { # iPAddress
$bytes = $subtree.ReadOctetString()
if ($bytes.Length -eq 4 -or $bytes.Length -eq 16) {
$result.PermittedIP += ([System.Net.IPAddress]::new($bytes)).ToString()
} else {
$result.PermittedIP += [System.BitConverter]::ToString($bytes)
}
}
}
# Пропускаем minimum/maximum, если есть
while ($subtree.HasData) { $subtree.ReadEncodedValue() | Out-Null }
}
}
# --- excludedSubtrees [1] ---
if ($ncSeq.HasData -and $ncSeq.PeekTag().TagValue -eq 1) {
$excluded = $ncSeq.ReadSequence([System.Formats.Asn1.Asn1Tag]::ContextSpecific(1))
while ($excluded.HasData) {
$subtree = $excluded.ReadSequence()
$baseTag = $subtree.PeekTag()
switch ($baseTag.TagValue) {
1 { # rfc822Name
$value = $subtree.ReadCharacterString([System.Formats.Asn1.UniversalTagNumber]::IA5String)
$result.ExcludedEmail += $value
}
2 { # dNSName
$value = $subtree.ReadCharacterString([System.Formats.Asn1.UniversalTagNumber]::IA5String)
$result.ExcludedDNS += $value
}
6 { # uniformResourceIdentifier
$value = $subtree.ReadCharacterString([System.Formats.Asn1.UniversalTagNumber]::IA5String)
$result.ExcludedURI += $value
}
7 { # iPAddress
$bytes = $subtree.ReadOctetString()
if ($bytes.Length -eq 4 -or $bytes.Length -eq 16) {
$result.ExcludedIP += ([System.Net.IPAddress]::new($bytes)).ToString()
} else {
$result.ExcludedIP += [System.BitConverter]::ToString($bytes)
}
}
}
while ($subtree.HasData) { $subtree.ReadEncodedValue() | Out-Null }
}
}
} catch {
Write-Warning "Не удалось разобрать Name Constraints для $($Certificate.Thumbprint): $_"
}
return $result
}
# ============================================================
# Основной сбор сертификатов
# ============================================================
Write-Host "`nПоиск сертификатов GPO с weak RSA-ключами..." -ForegroundColor Cyan
$foundCerts = foreach ($gpo in $gpoRegistryPaths) {
if (-not (Test-Path $gpo.Path)) {
Write-Verbose "Путь не существует: $($gpo.Path)"
continue
}
# Перебираем подразделы (Root, CA, Trust, My и т.д.)
Get-ChildItem -Path $gpo.Path -ErrorAction SilentlyContinue | ForEach-Object {
$storeName = $_.PSChildName
$certKeyPath = Join-Path $_.PSPath "Certificates"
if (-not (Test-Path $certKeyPath)) { return }
Get-ChildItem -Path $certKeyPath -ErrorAction SilentlyContinue | ForEach-Object {
$thumbprint = $_.PSChildName
# Ищем сертификат в логическом хранилище по отпечатку
$cert = Get-ChildItem -Path "$($gpo.CertProvider)\$storeName" -ErrorAction SilentlyContinue |
Where-Object { $_.Thumbprint -eq $thumbprint } |
Select-Object -First 1
if (-not $cert) { return }
if (-not (Test-WeakRsaCert -Cert $cert)) { return }
$keySize = $cert.PublicKey.Key.KeySize
$matched = if ($keySize -le $maxKeySizeAnyDate) {
"KeySize<=1024 (any date)"
} else {
"KeySize<=2048 & NotAfter>=2030"
}
# --- Name Constraints ---
$nc = Get-NameConstraints -Certificate $cert
[PSCustomObject]@{
Source = $gpo.Scope
StoreName = $storeName
Subject = $cert.Subject
Thumbprint = $thumbprint
KeySize = $keySize
NotAfter = $cert.NotAfter
HasPrivateKey = $cert.HasPrivateKey
MatchedRule = $matched
PermittedDNS = ($nc.PermittedDNS -join '; ')
ExcludedDNS = ($nc.ExcludedDNS -join '; ')
PermittedEmail = ($nc.PermittedEmail -join '; ')
ExcludedEmail = ($nc.ExcludedEmail -join '; ')
PermittedIP = ($nc.PermittedIP -join '; ')
ExcludedIP = ($nc.ExcludedIP -join '; ')
PermittedURI = ($nc.PermittedURI -join '; ')
ExcludedURI = ($nc.ExcludedURI -join '; ')
}
}
}
}
# ============================================================
# Вывод результатов
# ============================================================
if ($foundCerts) {
Write-Host "`nНайдены сертификаты GPO, удовлетворяющие условиям:" -ForegroundColor Yellow
Write-Host " 1) RSA KeySize <= 2048 и NotAfter >= 2030-01-01"
Write-Host " 2) RSA KeySize <= 1024 (любая дата истечения)"
Write-Host ""
$foundCerts | Format-Table -AutoSize -Wrap
$csvPath = ".\GPO_WeakRSA_NameConstraints_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$foundCerts | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Host "`nРезультаты сохранены в CSV: $csvPath" -ForegroundColor Green
Write-Host "`nСводка по правилам:" -ForegroundColor Cyan
$foundCerts | Group-Object MatchedRule | Format-Table Name, Count -AutoSize
Write-Host "Сводка по областям:" -ForegroundColor Cyan
$foundCerts | Group-Object Source, StoreName | Format-Table Name, Count -AutoSize
} else {
Write-Host "`nСертификаты GPO, удовлетворяющие условиям, не найдены." -ForegroundColor Green
}