Private/CAInstall.ps1

<#
.SYNOPSIS
    CA Manager - install / configure the CA (dashboard menu option 0). Requires Modules\CACore.ps1
    and, to actually install, the AD CS role binaries + the ADCSDeployment module.
 
.DESCRIPTION
    New-CACapolicyInf is a PURE generator - builds a capolicy.inf from the CA_* answer fields.
    Get-CAInstallPlan returns the Install-AdcsCertificationAuthority parameter set (enterprise root).
    Invoke-CAInstall writes the capolicy.inf, adds the role, and runs the install - through
    Invoke-CAStep, so dry-run just prints everything.
 
    SCOPE: the enterprise ROOT path is built (that's the fresh-client case - single-tier, shared-CA
    model). The subordinate path (request file -> submit to an online root -> install chain) throws
    a clear "not built yet" until there's a real two-tier client to build it against.
#>


# ---------------------------------------------------------------------------
function New-CACapolicyInf {
    <#
    .SYNOPSIS
        Builds capolicy.inf text from the CA_* answers. Pure - returns the string, writes nothing.
    #>

    param(
        [int]$RenewalKeyLength   = 4096,
        [int]$RenewalValidityYears = 10,
        [int]$CrlPeriodDays      = 7,
        [int]$CrlDeltaPeriodDays = 1,
        [bool]$LoadDefaultTemplates = $false,   # $false for a VPN-purpose CA - only our 4 templates
        [string]$CpsUrl,                        # optional certification-practice-statement URL
        [string]$CpsNotice = "All use of this Certification Authority is restricted to authorized purposes."
    )

    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('[Version]')
    $lines.Add('Signature="$Windows NT$"')
    $lines.Add('')
    $lines.Add('[Certsrv_Server]')
    $lines.Add("RenewalKeyLength=$RenewalKeyLength")
    $lines.Add('RenewalValidityPeriod=Years')
    $lines.Add("RenewalValidityPeriodUnits=$RenewalValidityYears")
    $lines.Add('CRLPeriod=Days')
    $lines.Add("CRLPeriodUnits=$CrlPeriodDays")
    $lines.Add('CRLDeltaPeriod=Days')
    $lines.Add("CRLDeltaPeriodUnits=$CrlDeltaPeriodDays")
    $lines.Add("LoadDefaultTemplates=$([int][bool]$LoadDefaultTemplates)")
    $lines.Add('AlternateSignatureAlgorithm=0')
    if ($CpsUrl) {
        $lines.Add('')
        $lines.Add('[PolicyStatementExtension]')
        $lines.Add('Policies=InternalPolicy')
        $lines.Add('[InternalPolicy]')
        $lines.Add('OID=2.5.29.32.0')
        $lines.Add("Notice=`"$CpsNotice`"")
        $lines.Add("URL=$CpsUrl")
    }
    return ($lines -join "`r`n")
}

# ---------------------------------------------------------------------------
function Get-CAInstallPlan {
    <#
    .SYNOPSIS
        The Install-AdcsCertificationAuthority parameter set for an enterprise ROOT CA. Pure -
        returns a hashtable. Throws for anything but the enterprise-root case (see SCOPE above).
    #>

    param(
        [Parameter(Mandatory)][string]$CACommonName,
        [ValidateSet('EnterpriseRootCA', 'EnterpriseSubordinateCA')][string]$CAType = 'EnterpriseRootCA',
        [int]$KeyLength = 4096,
        [string]$HashAlgorithm = 'SHA256',
        [int]$ValidityYears = 10,
        [string]$CryptoProvider = 'RSA#Microsoft Software Key Storage Provider'
    )
    if ($CAType -ne 'EnterpriseRootCA') {
        throw "Get-CAInstallPlan: only EnterpriseRootCA is built (Phase 4). The subordinate path (CSR -> parent -> install chain) is pending a real two-tier client."
    }
    return @{
        CAType                    = 'EnterpriseRootCA'
        CACommonName              = $CACommonName
        KeyLength                 = $KeyLength
        HashAlgorithmName         = $HashAlgorithm
        CryptoProviderName        = $CryptoProvider
        ValidityPeriod            = 'Years'
        ValidityPeriodUnits       = $ValidityYears
        Force                     = $true
    }
}

# ---------------------------------------------------------------------------
function Show-CAInstallPlan {
    param($CAAnswers)
    $cn = if ($CAAnswers.CA_CommonName) { $CAAnswers.CA_CommonName } else { '<CA_CommonName - not set>' }
    $yrs = if ($CAAnswers.CA_ValidityYears -as [int]) { [int]$CAAnswers.CA_ValidityYears } else { 10 }
    $sub = ($CAAnswers.CA_IsSubordinate -match '^(?i)y')
    Write-Host ""
    if ($sub) {
        Write-Host " CA_IsSubordinate = Yes - the subordinate install path is not built yet." -ForegroundColor Yellow
        Write-Host " (Fresh single-tier clients use the enterprise-root path below.)" -ForegroundColor Yellow
        return
    }
    Write-Host " Would install: Enterprise Root CA '$cn'" -ForegroundColor White
    Write-Host (" key {0}-bit RSA, SHA256, valid {1} years" -f 4096, $yrs) -ForegroundColor Gray
    Write-Host " capolicy.inf ->" -ForegroundColor Gray
    $inf = New-CACapolicyInf -RenewalValidityYears $yrs `
        -CrlPeriodDays $(if ($CAAnswers.CA_CrlPeriodDays -as [int]) { [int]$CAAnswers.CA_CrlPeriodDays } else { 7 })
    foreach ($l in ($inf -split "`r?`n")) { Write-Host " $l" -ForegroundColor DarkGray }
}

# ---------------------------------------------------------------------------
function Invoke-CAInstall {
    <#
    .SYNOPSIS
        Writes %windir%\capolicy.inf, adds the AD CS role, and installs an enterprise root CA. Every
        mutating step is an Invoke-CAStep (dry-run aware). Enterprise-root only.
    #>

    param(
        [Parameter(Mandatory)][string]$CACommonName,
        [int]$ValidityYears = 10,
        [int]$KeyLength = 4096,
        [int]$CrlPeriodDays = 7,
        [int]$CrlDeltaPeriodDays = 1,
        [string]$CpsUrl
    )

    # Stale AD objects from a decommissioned / snapshot-reverted CA of the same name block
    # Install-AdcsCertificationAuthority ("A certification authority with the same name was found in
    # the Active Directory."). Surface it up front with the fix rather than crashing mid-install.
    if (-not (Get-CADryRun) -and (Get-Command Get-ADObject -ErrorAction SilentlyContinue)) {
        try {
            $cnc = (Get-ADRootDSE -ErrorAction Stop).configurationNamingContext
            $es  = Get-ADObject -SearchBase "CN=Enrollment Services,CN=Public Key Services,CN=Services,$cnc" `
                     -Filter "Name -eq '$CACommonName'" -ErrorAction SilentlyContinue
            if ($es) {
                Write-Host ""
                Write-Host " AD already has a CA registered as '$CACommonName' (CN=Enrollment Services)." -ForegroundColor Yellow
                Write-Host " If that's a leftover from a decommissioned / reverted CA, clear it first:" -ForegroundColor Yellow
                Write-Host " certutil -f -dsdel `"$CACommonName`"" -ForegroundColor Gray
                Write-Host " Get-ADObject -SearchBase `"CN=Public Key Services,CN=Services,`$((Get-ADRootDSE).configurationNamingContext)`" ``" -ForegroundColor Gray
                Write-Host " -Filter `"Name -eq '$CACommonName'`" -SearchScope Subtree | Remove-ADObject -Recursive -Confirm:`$false" -ForegroundColor Gray
                if ((Read-Host " Type CONTINUE to run the install anyway (it will likely fail), anything else to stop") -cne 'CONTINUE') {
                    return
                }
            }
        } catch { }
    }

    $infText = New-CACapolicyInf -RenewalValidityYears $ValidityYears -CrlPeriodDays $CrlPeriodDays -CrlDeltaPeriodDays $CrlDeltaPeriodDays -CpsUrl $CpsUrl
    $infPath = Join-Path $env:windir 'capolicy.inf'

    Invoke-CAStep -Description "Write $infPath" `
        -Commands @("Set-Content -Path '$infPath' (capolicy.inf, $(($infText -split "`r?`n").Count) lines)") `
        -Action { Set-Content -Path $infPath -Value $infText -Encoding ASCII -Force } | Out-Null

    Invoke-CAStep -Description "Add the AD CS Certification Authority role feature (+ RSAT-ADCS-Mgmt)" `
        -Commands @("Install-WindowsFeature ADCS-Cert-Authority, RSAT-ADCS-Mgmt -IncludeManagementTools") `
        -Action {
            $r = Install-WindowsFeature -Name ADCS-Cert-Authority, RSAT-ADCS-Mgmt -IncludeManagementTools
            if ($r.RestartNeeded -and "$($r.RestartNeeded)" -ne 'No') {
                Write-Host " NOTE: the feature install reports a RESTART is needed." -ForegroundColor Yellow
            }
            $r | Out-String
        } | Out-Null

    # Install-AdcsCertificationAuthority is in the ADCSDeployment module the feature above lays down.
    # A fresh Server usually needs a REBOOT before that module registers; and even after, PowerShell
    # won't see it mid-session if its dir wasn't on PSModulePath at launch. Try to load it (by name,
    # then by literal path); if neither works, bail with a clear next-step note instead of crashing.
    if (-not (Get-CADryRun)) {
        $adcsLoaded = $false
        foreach ($m in @('ADCSDeployment', (Join-Path $env:windir 'System32\WindowsPowerShell\v1.0\Modules\ADCSDeployment'))) {
            try { Import-Module $m -ErrorAction Stop; $adcsLoaded = $true; break } catch { }
        }
        if (-not $adcsLoaded) {
            Write-Host ""
            Write-Host " The AD CS role feature is installed, but its ADCSDeployment PowerShell module" -ForegroundColor Yellow
            Write-Host " can't be loaded yet. On a fresh server this needs a REBOOT. After rebooting:" -ForegroundColor Yellow
            Write-Host " - relaunch CA-Manager and run menu 2 again (capolicy.inf + the feature are" -ForegroundColor Yellow
            Write-Host " already in place, so it goes straight to Install-AdcsCertificationAuthority)." -ForegroundColor Yellow
            Write-Host " If it still won't load after a reboot: Install-WindowsFeature RSAT-ADCS-Mgmt" -ForegroundColor Gray
            return
        }
    }

    $plan = Get-CAInstallPlan -CACommonName $CACommonName -CAType EnterpriseRootCA -KeyLength $KeyLength -ValidityYears $ValidityYears
    # Preview string: render switch/bool params bare (-Force, not -Force 'True'); stable, readable order.
    $order = @('CAType', 'CACommonName', 'KeyLength', 'HashAlgorithmName', 'CryptoProviderName', 'ValidityPeriod', 'ValidityPeriodUnits', 'Force')
    $planStr = (@($order | Where-Object { $plan.ContainsKey($_) } | ForEach-Object {
        $v = $plan[$_]
        if ($v -is [bool]) { if ($v) { "-$_" } } else { "-$_ '$v'" }
    }) -join ' ')
    Invoke-CAStep -Description "Install enterprise root CA '$CACommonName'" `
        -Commands @("Import-Module ADCSDeployment", "Install-AdcsCertificationAuthority $planStr") `
        -Action {
            $litPath = Join-Path $env:windir 'System32\WindowsPowerShell\v1.0\Modules\ADCSDeployment'
            try { Import-Module ADCSDeployment -ErrorAction Stop }
            catch { Import-Module $litPath -ErrorAction Stop }   # broken PSModulePath fallback
            Install-AdcsCertificationAuthority @plan
        } | Out-Null
}