Public/Steps/Invoke-HDTInstallCertificateStep.ps1

function Invoke-HDTInstallCertificateStep {
    <#
        .SYNOPSIS
            Carries the boot image's certificates into the applied OS.
 
        .DESCRIPTION
            Installing the certificates, as a step:
 
              - name: Install certificates
                type: InstallCertificate
                target: "%HDTOSVolume%" # optional
                bootstrap: X:\HDT\bootstrap.json # optional
                condition: "%HDTHasCertificate% == True"
 
            THE REBOOT IS WHAT THIS EXISTS FOR. WinPE imported the certificates
            into a store on a RAM disk, and that disk is gone the moment the
            machine restarts. The applied OS then comes up on the same 802.1X
            port with no identity of its own: no lease, no share, and a resume
            leg that never reaches the engine - after every step before it
            reported Completed.
 
            IT DOES NOT IMPORT ANYTHING ITSELF, and cannot. A private key is
            protected by the machine's own key storage, which does not exist
            until something boots, so there is no supported way to write a
            usable machine certificate into an image that has never run. What
            this step does is STAGE the files and arrange for the machine to
            import them on its first boot.
 
            SetupComplete.cmd IS THE SLOT, and Windows chose it: it runs after
            Setup and BEFORE THE FIRST LOGON, which puts the import ahead of
            autologon and therefore ahead of Start-HDTResume needing the
            network. An unattend RunSynchronous would run earlier and fail
            Setup if anything went wrong; a FirstLogonCommand would run after
            the resume leg had already tried.
 
            IT REFUSES TO OVERWRITE A SetupComplete.cmd IT DID NOT WRITE.
            Windows runs one, and a shop's own post-Setup script silently
            replaced is a thing nobody notices until whatever it did stops
            happening.
 
            NOTHING TO DO IS NOT A FAILURE. An image carrying no certificates -
            and a run with no bootstrap document, which is every full-OS leg -
            completes having written nothing. The template puts a condition on
            this step as well; this is the second guard, for the sequence
            somebody wrote by hand.
 
            THE PASSWORD NEVER LANDS IN THE .cmd OR THE .ps1. Both files sit on
            the deployed machine's disk for the life of it and every user can
            read them. It rides in certificate.json beside them, obfuscated the
            same way the share credential is - which is not security, and is
            still not plain text in a batch file.
 
        .PARAMETER Step
            A flattened step from Import-HDTSequenceDocument.
 
        .PARAMETER Context
            The execution context. Its service catalog must carry a FileSystem.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject - a step result. Data
            carries certificateCount and the staged paths, never the password.
 
        .EXAMPLE
            Invoke-HDTInstallCertificateStep -Step $step -Context $context
 
        .LINK
            Set-HDTBootImageClientCertificate
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $Step,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $Context
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $fail = {
        param([string] $Message, [string] $ErrorId)

        $data = [ordered] @{}
        if (-not [string]::IsNullOrWhiteSpace($ErrorId)) { $data['errorId'] = $ErrorId }

        Write-HDTLog -Context $Context.Log -Message $Message -Severity Error -Event step.fail `
            -Component 'InstallCertificate' -Data $data

        return (New-HDTStepResult -Status Failed -Message $Message -Data $data)
    }

    try {
        $target = Get-HDTStepProperty -Step $Step -Name 'target' -Context $Context -Expand -As String
        $bootstrapPath = Get-HDTStepProperty -Step $Step -Name 'bootstrap' -Default 'X:\HDT\bootstrap.json' `
            -Context $Context -Expand -As String
    } catch {
        return (& $fail ([string] $_.Exception.Message) 'HDTConfigurationError')
    }

    try {
        $fileSystem = $Context.Service.GetRequired('FileSystem', 'InstallCertificate')
    } catch {
        return (& $fail ([string] $_.Exception.Message) '')
    }

    # -- what the image carries ----------------------------------------------
    #
    # NO DOCUMENT IS AN ANSWER. The full-OS leg and a sequence run by hand both
    # reach this with no X:\HDT\bootstrap.json, and neither is an error.

    if (-not $fileSystem.TestPath($bootstrapPath)) {
        return (New-HDTStepResult -Status Completed `
                -Message ("no boot image document at '{0}', so this image carries no certificates to install." -f $bootstrapPath) `
                -Data ([ordered] @{ certificateCount = 0 }))
    }

    try {
        $bootstrap = Get-HDTBootstrapConfiguration -Path $bootstrapPath -FileSystem $fileSystem
    } catch {
        return (& $fail ("the boot image document '{0}' could not be read: {1}" -f
                $bootstrapPath, [string] $_.Exception.Message) '')
    }

    $rootCertificate = [string[]] @($bootstrap.RootCertificate)
    $clientCertificate = [string] $bootstrap.ClientCertificate

    if (@($rootCertificate).Count -eq 0 -and [string]::IsNullOrWhiteSpace($clientCertificate)) {
        return (New-HDTStepResult -Status Completed `
                -Message 'this boot image carries no certificates, so there is nothing to carry into the OS.' `
                -Data ([ordered] @{ certificateCount = 0 }))
    }

    # -- where they go --------------------------------------------------------

    $letter = $target
    if ([string]::IsNullOrWhiteSpace($letter)) {
        $letter = [string] $Context.Variable['HDTOSVolume']
    }

    if ([string]::IsNullOrWhiteSpace($letter)) {
        return (& $fail ("step '{0}' stages the certificates on the applied volume and HDTOSVolume is not set. The partition step publishes it; set target: on this step if the sequence knows better." -f
                $Step.Name) 'HDTConfigurationError')
    }

    $letter = $letter.Trim().TrimEnd(':')
    $volume = $letter.Substring(0, 1).ToUpperInvariant()

    # WINDOWS' OWN TWO FOLDERS. Setup\Scripts is where SetupComplete.cmd is
    # looked for and nowhere else; Setup\Files is copied to the machine and is
    # where everything it needs beside it belongs.
    $scriptRoot = '{0}:\Windows\Setup\Scripts' -f $volume
    $fileRoot = '{0}:\Windows\Setup\Files\HDT' -f $volume
    $certificateRoot = '{0}\Certs' -f $fileRoot
    $setupComplete = '{0}\SetupComplete.cmd' -f $scriptRoot

    # -- somebody else's script -----------------------------------------------

    if ($fileSystem.TestPath($setupComplete)) {
        $existing = [string] $fileSystem.ReadAllText($setupComplete)

        if ($existing -notlike '*Import-HDTOsCertificate.ps1*') {
            return (& $fail ("'{0}' is already there and was not written by HDT. Windows runs one SetupComplete.cmd, and replacing this one would silently drop whatever it does. Move that script's work into a step, or into the HDT one after it is written." -f
                    $setupComplete) 'HDTConfigurationError')
        }
    }

    # -- the files ------------------------------------------------------------

    $importScript = Join-Path -Path (Join-Path -Path $script:HDTModuleRoot -ChildPath 'Payload') `
        -ChildPath 'Import-HDTOsCertificate.ps1'

    $moduleFileSystem = New-HDTFileSystem

    if (-not $moduleFileSystem.TestPath($importScript)) {
        return (& $fail ("this module ships the first-boot import script and it is not at '{0}'. The install is incomplete." -f
                $importScript) 'HDTDependencyError')
    }

    try {
        $fileSystem.CreateDirectory($certificateRoot)
        $fileSystem.CreateDirectory($scriptRoot)

        $staged = New-Object -TypeName System.Collections.ArrayList
        $stagedClient = ''

        foreach ($current in @($rootCertificate)) {
            $leaf = [System.IO.Path]::GetFileName([string] $current)
            $fileSystem.CopyItem([string] $current, ('{0}\{1}' -f $certificateRoot, $leaf))
            [void] $staged.Add(('{0}\{1}' -f $certificateRoot, $leaf))
        }

        if (-not [string]::IsNullOrWhiteSpace($clientCertificate)) {
            $leaf = [System.IO.Path]::GetFileName($clientCertificate)
            $stagedClient = '{0}\{1}' -f $certificateRoot, $leaf
            $fileSystem.CopyItem($clientCertificate, $stagedClient)
        }

        # THE MANIFEST, WHICH IS WHAT THE FIRST BOOT READS. The password is
        # carried obfuscated - see the header - so neither the .cmd nor the .ps1
        # has to hold it.
        $manifest = [ordered] @{
            schemaVersion = 1
            root          = [string[]] @($staged)
            client        = $stagedClient
            protected     = ''
            warning       = 'This password is obfuscated, not encrypted: the key is a constant in the Hephaestus module. Delete this folder once the machine is built if the certificate matters.'
        }

        if (-not [string]::IsNullOrWhiteSpace($stagedClient)) {
            $manifest['protected'] = Protect-HDTShareSecret -Secret ([string] $bootstrap.GetCertificatePassword())
        }

        $fileSystem.WriteAllText(('{0}\certificate.json' -f $fileRoot),
            (ConvertTo-Json -InputObject $manifest -Depth 4))

        $fileSystem.WriteAllText(('{0}\Import-HDTOsCertificate.ps1' -f $fileRoot),
            [string] $moduleFileSystem.ReadAllText($importScript))

        # CRLF AND NO BOM, because cmd.exe reading a byte order mark as a command
        # is a class of failure with no useful message at all. IFileSystem writes
        # BOM-free on both engines.
        $batch = @(
            '@echo off'
            'rem Written by the HDT InstallCertificate step. Windows runs this once,'
            'rem after Setup and before the first logon.'
            ('powershell.exe -NoProfile -ExecutionPolicy Bypass -File {0}\Import-HDTOsCertificate.ps1' -f $fileRoot)
        ) -join "`r`n"

        $fileSystem.WriteAllText($setupComplete, $batch + "`r`n")
    } catch {
        return (& $fail ("the certificates could not be staged onto {0}: {1}" -f $volume, [string] $_.Exception.Message) '')
    }

    $count = @($staged).Count
    if (-not [string]::IsNullOrWhiteSpace($stagedClient)) { $count++ }

    $data = [ordered] @{
        certificateCount = $count
        setupComplete    = $setupComplete
        certificateRoot  = $certificateRoot
    }

    $message = "{0} certificate(s) staged on {1}: and armed for the first boot, before the first logon." -f $count, $volume

    Write-HDTLog -Context $Context.Log -Message $message -Severity Info -Event message `
        -Component 'InstallCertificate' -Data $data

    return (New-HDTStepResult -Status Completed -Message $message -Data $data)
}