Private/Export-PACertFiles.ps1

function Export-PACertFiles {
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [PSTypeName('PoshACME.PAOrder')]$Order,
        [switch]$PfxOnly
    )

    # Make sure we have an account configured
    if (-not ($acct = Get-PAAccount)) {
        throw "No ACME account configured. Run Set-PAAccount or New-PAAccount first."
    }

    # Make sure we have an order
    if (-not $Order -and -not ($Order = Get-PAOrder)) {
        throw "No ACME order specified and no current order selected. Run Set-PAOrder or specify an existing order object."
    }

    # build output paths
    $certFile      = Join-Path $Order.Folder 'cert.cer'
    $keyFile       = Join-Path $Order.Folder 'cert.key'
    $chainFile     = Join-Path $Order.Folder 'chain.cer'
    $fullchainFile = Join-Path $Order.Folder 'fullchain.cer'
    $pfxFile       = Join-Path $Order.Folder 'cert.pfx'
    $pfxFullFile   = Join-Path $Order.Folder 'fullchain.pfx'

    if (-not $PfxOnly) {

        # Download the cert+chain if the order has not expired.
        if ((Get-DateTimeOffsetNow) -lt [DateTimeOffset]::Parse($Order.expires)) {

            Write-Verbose "Downloading signed certificate"

            # build the header for the Post-As-Get request
            $header = @{
                alg   = $acct.alg
                kid   = $acct.location
                nonce = $script:Dir.nonce
                url   = $Order.certificate
            }

            # download the cert+chain which is what ACME delivers by default
            # https://tools.ietf.org/html/rfc8555#section-7.4.2
            try {
                $response = Invoke-ACME $header ([String]::Empty) $acct -EA Stop
            } catch { throw }

            $pems = @(Split-PemChain -ChainBytes $response.Content)

            # Do some basic validation to make sure we got what we were expecting.
            $cert = Import-Pem -InputString ($pems[0] -join "`n")
            $altNames = $cert.GetSubjectAlternativeNames() | ForEach-Object {
                if ($_[0] -eq [Org.BouncyCastle.Asn1.X509.GeneralName]::DnsName) {
                    # second index is the actual DNS name
                    $_[1]
                }
                elseif ($_[0] -eq [Org.BouncyCastle.Asn1.X509.GeneralName]::IPAddress) {
                    # second index is a IP hex string like "#01010101" that we need to parse
                    ([ipaddress]([byte[]] -split ($_[1].Substring(1) -replace '..', '0x$& '))).ToString()
                }
            }
            Write-Debug "SANs in downloaded cert: $(($altNames -join ', '))"
            $orderNames = @($Order.MainDomain) + @($Order.SANs)
            $orderNames | ForEach-Object {
                if ($_ -notin $altNames) {
                    Write-Error "$_ was requested but is not present in the list of Subject Alternative Names in the signed certificate."
                }
            }
            $altNames | ForEach-Object {
                if ($_ -notin $orderNames) {
                    Write-Error "An extra name, $_, is present in the list of Subject Alternative Names in the signed certificate, but was not requested as part of the order."
                }
            }

            # write the lone cert
            Export-Pem $pems[0] $certFile

            # write the primary chain as chain0.cer
            $chain0File = Join-Path $Order.Folder 'chain0.cer'
            Export-Pem ($pems[1..($pems.Count-1)] | ForEach-Object {$_}) $chain0File

            # check for alternate chain header links
            $links = @(Get-AlternateLinks $response.Headers)

            # download the alternate chains
            for ($i = 0; $i -lt $links.Count; $i++) {
                Write-Debug "Alt Chain $($i+1): $($links[$i])"
                $header.url = $links[$i]
                $header.nonce = $script:Dir.nonce

                try {
                    $response = Invoke-ACME $header ([String]::Empty) $acct -EA Stop
                } catch {throw}
                $pems = @(Split-PemChain -ChainBytes $response.Content)

                # write additional chain files as chain1.cer,chain2.cer,etc.
                $altChainFile = Join-Path $Order.Folder "chain$($i+1).cer"
                Export-Pem ($pems[1..($pems.Count-1)] | ForEach-Object {$_}) $altChainFile
            }
        }
        else {
            Write-Warning "Order has expired. Unable to re-download cert/chain files. Using cached copies."
            $chain0File = Join-Path $Order.Folder 'chain0.cer'

            # if the chain0 file doesn't exist, it means this config was likely recently
            # upgraded from 3.x. So we'll just fake it by copying the current chain.cer to chain0.cert
            if (-not (Test-Path $chain0File -PathType Leaf)) {
                Copy-Item $chainFile $chain0File
            }
        }

        # try to find the chain file matching the preferred issuer if specified
        if (-not ([String]::IsNullOrWhiteSpace($order.PreferredChain))) {
            $chainIssuers = Get-ChainIssuers $Order.Folder
            Write-Debug ($chainIssuers | ConvertTo-Json)
            $selectedChainFile = $chainIssuers |
                Where-Object { $_.issuer -eq $order.PreferredChain } |
                Sort-Object index |
                Select-Object -First 1 -Expand filePath
            Write-Debug "Preferred chain, $($order.PreferredChain), matched: $selectedChainFile"

            if (-not $selectedChainFile) {
                Write-Warning "The preferred chain issuer, $($order.PreferredChain), was not found. Using the default chain."
                $selectedChainFile = $chain0File
            } else {
                Write-Verbose "Using preferred chain issuer, $($order.PreferredChain)."
            }
        } else {
            $selectedChainFile = $chain0File
        }

        # build the appropriate chain and fullchain files
        if (-not (Test-Path $certFile -PathType Leaf)) {
            throw "Cert file not found: $certFile"
        }
        if (-not (Test-Path $selectedChainFile -PathType Leaf)) {
            throw "Chain file not found: $selectedChainFile"
        }
        Copy-Item $selectedChainFile $chainFile
        $fullchainLines = (Get-Content $certFile) + (Get-Content $selectedChainFile)
        Export-Pem $fullchainLines $fullchainFile
    }

    # When using an pre-generated CSR file, there may be no private key.
    # So make sure we have a one before we try to generate PFX files.
    if (Test-Path $keyFile -PathType Leaf) {

        $pfxParams = @{
            CertFile     = $certFile;
            KeyFile      = $keyFile;
            OutputFile   = $pfxFile;
            FriendlyName = $Order.FriendlyName;
            PfxPass      = $Order.PfxPass;
        }
        if ($Order.UseModernPfxEncryption) {
            $pfxParams.UseModernPfxEncryption = $true
        }
        Export-CertPfx @pfxParams
        $pfxParams.OutputFile = $pfxFullFile
        Export-CertPfx @pfxParams -ChainFile $chainFile

    } else {
        Write-Verbose "No private key available. Skipping Pfx creation."
    }
}