Загрузка данных


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