Public/Submit-ChallengeValidation.ps1

function Submit-ChallengeValidation {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [PSTypeName('PoshACME.PAOrder')]$Order
    )

    # Here's the overview of this function's purpose:
    # - publish challenges for any pending authorizations in the Order
    # - notify ACME server to validate those challenges
    # - wait until the validations are complete (good or bad)
    # - unpublish the non-persistent challenges that were published
    # - return the updated order if successful, otherwise throw

    Begin {
        try {
            # make sure an account exists
            if (-not ($acct = Get-PAAccount)) {
                throw "No ACME account configured. Run Set-PAAccount or New-PAAccount first."
            }
            # make sure it's valid
            if ($acct.status -ne 'valid') {
                throw "Account status is $($acct.status)."
            }
        }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }

    Process {

        # make sure any order passed in is actually associated with the account
        # or if no order was specified, that there's a current order.
        if (-not $Order) {
            if (-not ($Order = Get-PAOrder)) {
                try { throw "No Order parameter specified and no current order selected. Try running Set-PAOrder first." }
                catch { $PSCmdlet.ThrowTerminatingError($_) }
            }
        } elseif ($Order.Name -notin (Get-PAOrder -List).Name) {
            Write-Error "Order '$($Order.Name)' was not found in the current account's order list."
            return
        }

        # make sure the order has a valid state for this function
        if ($Order.status -eq 'invalid') {
            Write-Error "Order '$($Order.Name)' status is invalid. Unable to continue."
            return
        }
        elseif ($Order.status -in 'valid','processing') {
            Write-Warning "The server has already issued or is processing a certificate for order '$($Order.Name)'."
            return
        }
        elseif ($Order.status -eq 'ready') {
            Write-Warning "Order '$($Order.Name)' has already completed challenge validation and is awaiting finalization."
            return
        }


        # The only order status left is 'pending'. This means that at least one
        # authorization hasn't been validated yet according to
        # https://tools.ietf.org/html/rfc8555#section-7.1.6
        # So we're going to check all of the authorization statuses and publish
        # records for any that are still pending.

        $allAuths = @($Order | Get-PAAuthorization)

        # fill out the order's Plugin attribute so there's a value for each authorization
        if (-not $Order.Plugin) {
            Write-Warning "No plugin found associated with order. Defaulting to Manual."
            $Order.Plugin = @('Manual') * $allAuths.Count
        } elseif ($Order.Plugin.Count -lt $allAuths.Count) {
            $lastPlugin = $Order.Plugin[-1]
            Write-Warning "Fewer Plugin values than names in the order. Using $lastPlugin for the rest."
            $Order.Plugin += @($lastPlugin) * ($allAuths.Count-$Order.Plugin.Count)
        }
        Write-Debug "Plugin: $($Order.Plugin -join ',')"

        # fill out the order's DnsAlias attribute so there's a value for each authorization
        if (-not $Order.DnsAlias) {
            # no alias means they should all just be empty
            $Order.DnsAlias = @('') * $allAuths.Count
        } elseif ($Order.DnsAlias.Count -lt $allAuths.Count) {
            $lastAlias = $Order.DnsAlias[-1]
            Write-Warning "Fewer DnsAlias values than names in the order. Using $lastAlias for the rest."
            $Order.DnsAlias += @($lastAlias) * ($allAuths.Count-$Order.DnsAlias.Count)
        }
        Write-Debug "DnsAlias: $($Order.DnsAlias -join ',')"

        # import existing args
        $PluginArgs = Get-PAPluginArgs -Name $Order.Name

        # loop through the authorizations looking for challenges to publish and validate
        $published = @()
        $toValidate = @()
        $sleepForPersist = $false
        for ($i=0; $i -lt $allAuths.Count; $i++) {

            $pubParams = $null
            $auth = $allAuths[$i]
            if ($auth.status -eq 'pending') {

                # Determine which challenge to publish based on the plugin type and check
                # for any alternate DNS challenge preference specified in the order
                $chalType = $script:Plugins.($Order.Plugin[$i]).ChallengeType
                if ($chalType -eq 'dns-01' -and $Order.DnsVariant -and $Order.DnsVariant -ne 'dns-01') {
                    $chalType = $Order.DnsVariant
                    Write-Verbose "Using alternate DNS challenge type '$chalType' for $($auth.fqdn) based on order preference."
                }
                $challenge = $auth.challenges | Where-Object { $_.type -eq $chalType }
                if (-not $challenge) {
                    try {
                        throw "$($auth.fqdn) authorization contains no challenges that match $chalType"
                    } catch { $PSCmdlet.ThrowTerminatingError($_) }
                }

                if ($chalType -eq 'dns-persist-01') {
                    # publish the persist record if requested
                    if ($PluginArgs.PublishPersist) {
                        $issuer = Get-IssuerFromChallenge $challenge
                        if (-not $issuer) {
                            try {
                                throw "Unable to determine issuer domain name from dns-persist-01 challenge."
                            } catch { $PSCmdlet.ThrowTerminatingError($_) }
                        }
                        $pubParams = @{
                            Domain           = $auth.fqdn # this includes wildcard prefixes which will get automatically handled
                            Account          = $acct
                            IssuerDomainName = $issuer
                            Plugin           = $Order.Plugin[$i]
                            PluginArgs       = $PluginArgs
                        }
                        try {
                            Publish-DnsPersistChallenge @pubParams
                            $sleepForPersist = $true
                        } catch { throw }
                    }
                } else {
                    # publish standard challenge
                    $pubParams = @{
                        Domain = $auth.DNSId
                        Account = $acct
                        Token = $challenge.token
                        Plugin = $Order.Plugin[$i]
                        PluginArgs = $PluginArgs
                        DnsAlias = $Order.DnsAlias[$i]
                    }
                    if ($Order.DnsVariant) { $pubParams.DnsVariant = $Order.DnsVariant }
                    try {
                        Publish-Challenge @pubParams
                        # save the params to unpublish later
                        $published += $pubParams
                    } catch { throw }
                }

                if ($Order.UseSerialValidation) {
                    # save and validate the challenge before moving on to the next
                    try {
                        if ($chalType -ne 'dns-persist-01') {
                            Save-Challenge -Plugin $pubParams.Plugin -PluginArgs $PluginArgs
                        }

                        # sleep while DNS changes propagate if a DNS challenge was published
                        if ($chalType -like 'dns-*' -and $Order.DnsSleep -gt 0 -and $pubParams) {
                            Write-Verbose "Sleeping for $($Order.DnsSleep) seconds while DNS change(s) propagate"
                            Start-SleepProgress $Order.DnsSleep -Activity "Waiting for DNS to propagate"
                        }

                        # ask the server to validate the challenge
                        Write-Verbose "Requesting challenge validation"
                        $challenge.url | Send-ChallengeAck -Account $acct

                        # and wait for it to succeed or fail
                        Wait-AuthValidation $auth.location $Order.ValidationTimeout
                    }
                    catch {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                    finally {
                        # always try to cleanup non-persistent challenges
                        if ($chalType -ne 'dns-persist-01') {
                            Unpublish-Challenge @pubParams
                            Save-Challenge -Plugin $pubParams.Plugin -PluginArgs $PluginArgs
                        }
                    }
                } else {
                    # save the details we'll need later for batch validation
                    $toValidate += @{
                        chalType = $chalType
                        chalUrl = $challenge.url
                        authUrl = $auth.location
                    }
                }

            } elseif ($auth.status -eq 'valid') {
                # skip ones that are already valid
                Write-Verbose "$($auth.fqdn) authorization is already valid"
                continue
            } else {
                #status invalid, revoked, deactivated, or expired
                try {
                    throw "$($auth.fqdn) authorization status is '$($auth.status)'. Create a new order and try again."
                } catch { $PSCmdlet.ThrowTerminatingError($_) }
            }
        }

        if ($Order.UseSerialValidation) {
            # nothing left to do
            return
        }

        try {
            # save the non-persistent challenges we published
            if ($published.Count -gt 0) {
                $published.plugin | Sort-Object -Unique | ForEach-Object {
                    Save-Challenge -Plugin $_ -PluginArgs $PluginArgs
                }
            }

            # sleep while DNS changes propagate if there were DNS challenges published
            if ($Order.DnsSleep -gt 0 -and $toValidate.chalType -match '^dns-' -and
                ($published.Count -gt 0 -or $sleepForPersist)
            ) {
                Write-Verbose "Sleeping for $($Order.DnsSleep) seconds while DNS change(s) propagate"
                Start-SleepProgress $Order.DnsSleep -Activity "Waiting for DNS to propagate"
            }

            # ask the server to validate the challenges
            Write-Verbose "Requesting challenge validations"
            $toValidate.chalUrl | Send-ChallengeAck -Account $acct

            # and wait for them to succeed or fail
            Wait-AuthValidation @($toValidate.authUrl) $Order.ValidationTimeout
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
        finally {
            # always try to cleanup non-persistent challenges
            $published | ForEach-Object {
                Unpublish-Challenge @_
            }

            # save the cleanup changes
            $published.plugin | Sort-Object -Unique | ForEach-Object {
                Save-Challenge -Plugin $_ -PluginArgs $PluginArgs
            }
        }

    }
}