Payload/Import-HDTBootCertificate.ps1

<#
    .SYNOPSIS
        Imports the boot image's certificates into WinPE, before wpeinit.
 
    .DESCRIPTION
        WHAT startnet.cmd RUNS FIRST, and only when the image carries
        certificates - Get-HDTStartnetScript writes the line only then.
 
        BEFORE wpeinit, WHICH IS THE ENTIRE POINT. wpeinit is what brings the
        network up. A machine certificate that arrives afterwards has missed the
        authentication it was carried for: a switch port running 802.1X decides
        whether to give this machine an address BEFORE anything on it can ask.
        Everything else startnet.cmd runs goes after wpeinit, for the opposite
        reason.
 
        WHAT IS IMPORTED, AND WHERE:
 
          root -> LocalMachine\Root the certificate authorities to trust.
                                        WinPE boots with Microsoft's roots and
                                        nothing else, so an internal CA is
                                        trusted everywhere except here.
 
          client -> LocalMachine\My the machine's own certificate, with its
                                        private key, persisted to the machine
                                        key set so a service can use it.
 
        A FAILURE IS LOUD AND NOT FATAL. Nothing here stops the boot: a WinPE
        that refused to continue because a certificate would not import is a
        machine with no engine, no log and no command prompt. The message goes
        to the screen - which at this moment is the only thing anybody watching
        can read - and to X:\HDT\certificate-import.log for afterwards.
 
        IT IS AN ADAPTER OVER X509Store AND IT STAYS ONE. Every decision it acts
        on was made by Get-HDTBootstrapConfiguration, which is unit tested with
        no machine attached: which files, which store, and what the .pfx
        password is.
 
    .PARAMETER BootstrapPath
        The document naming the certificates. Written by Update-HDTBootImage.
 
    .PARAMETER ModuleRoot
        Where the engine module was staged in the image.
 
    .PARAMETER LogPath
        Where to write what happened.
 
    .EXAMPLE
        powershell.exe -NoProfile -ExecutionPolicy Bypass -File X:\HDT\Import-HDTBootCertificate.ps1
 
        What startnet.cmd runs, before wpeinit.
#>

# $LogPath is used inside the $say closure, which the analyzer does not follow -
# the same suppression, for the same reason, as Start-HDTDeployment.ps1's.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
    Justification = 'Used inside a closure, which PSReviewUnusedParameter does not follow.')]
[CmdletBinding()]
param(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $BootstrapPath = 'X:\HDT\bootstrap.json',

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $ModuleRoot = 'X:\HDT\Modules',

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string] $LogPath = 'X:\HDT\certificate-import.log'
)

Set-StrictMode -Version Latest

# NOT 'Stop'. This script's contract is that it never stops the boot; a
# terminating default would make every unlucky import fatal, and the machine
# would sit at a prompt nobody asked for.
$ErrorActionPreference = 'Continue'

# Visible at the WinPE console without Write-Host, which the analyzer refuses -
# the same idiom Start-HDTDeployment.ps1 uses, and for the same reason: at this
# moment the console is the only thing anybody watching can read.
$InformationPreference = 'Continue'

$say = {
    param([string] $Message)

    $text = '{0} {1}' -f (Get-Date).ToString('HH:mm:ss'), $Message

    Write-Information -MessageData $text
    try {
        [System.IO.File]::AppendAllText($LogPath, $text + [System.Environment]::NewLine)
    } catch {
        # THE LOG IS THE THING THAT MAY FAIL HERE, not the import. X: is a RAM
        # disk and it is writable, but a boot image built with a tiny scratch
        # space can fill it - and losing the log is not a reason to lose the
        # certificate.
        Write-Information -MessageData (' (the log at {0} could not be written: {1})' -f $LogPath, $_.Exception.Message)
    }
}

try {
    Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'Hephaestus') -Force -ErrorAction Stop

    $bootstrap = Get-HDTBootstrapConfiguration -Path $BootstrapPath -FileSystem (New-HDTFileSystem)
} catch {
    & $say ('the certificates could not be read from {0}: {1}' -f $BootstrapPath, $_.Exception.Message)
    return
}

# -- the certificate authorities ---------------------------------------------

foreach ($current in @($bootstrap.RootCertificate)) {
    try {
        $certificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 `
            -ArgumentList ([string] $current)

        $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store `
            -ArgumentList 'Root', 'LocalMachine'

        $store.Open('ReadWrite')
        try {
            $store.Add($certificate)
        } finally {
            $store.Close()
        }

        & $say ('trusting {0} ({1})' -f $certificate.Subject, $certificate.Thumbprint)
    } catch {
        & $say ('{0} could not be trusted: {1}' -f $current, $_.Exception.Message)
    }
}

# -- the machine's own certificate --------------------------------------------

if (-not [string]::IsNullOrWhiteSpace([string] $bootstrap.ClientCertificate)) {
    try {
        # MachineKeySet, BECAUSE THERE IS NO USER PROFILE HERE, and
        # PersistKeySet because a key that vanished with this process would
        # leave a certificate nothing can use. Exportable is deliberately NOT
        # set: nothing in WinPE needs to export it again.
        $flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor
        [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet

        $certificate = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 `
            -ArgumentList ([string] $bootstrap.ClientCertificate),
        ([string] $bootstrap.GetCertificatePassword()), $flag

        $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store `
            -ArgumentList 'My', 'LocalMachine'

        $store.Open('ReadWrite')
        try {
            $store.Add($certificate)
        } finally {
            $store.Close()
        }

        & $say ('this machine will authenticate as {0} ({1})' -f $certificate.Subject, $certificate.Thumbprint)
    } catch {
        # THE MESSAGE NAMES THE PASSWORD, because it is what is wrong most of
        # the time - a .pfx exported with one password and
        # Set-HDTBootImageCertificatePassword run with another produces exactly
        # this failure, on a machine with nobody at the keyboard.
        & $say ('{0} could not be imported: {1}. If the file is right, the password stored with Set-HDTBootImageCertificatePassword is not the one it was exported with.' -f
            $bootstrap.ClientCertificate, $_.Exception.Message)
    }
}