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 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)
        $published = @()

        # 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 validate
        for ($i=0; $i -lt $allAuths.Count; $i++) {

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

                # Determine which challenge to publish based on the plugin type
                $chalType = $script:Plugins.($Order.Plugin[$i]).ChallengeType
                $challenge = $auth.challenges | Where-Object { $_.type -eq $chalType }
                if (-not $challenge) {
                    throw "$($auth.fqdn) authorization contains no challenges that match $($Order.Plugin[$i]) plugin type, $chalType"
                }

                if ($Order.UseSerialValidation) {
                    # Publish and validate each challenge separately
                    try {
                        Publish-Challenge $auth.DNSId $acct $challenge.token $Order.Plugin[$i] $PluginArgs -DnsAlias $Order.DnsAlias[$i]
                        Save-Challenge $Order.Plugin[$i] $PluginArgs

                        # sleep while DNS changes propagate if it was a DNS challenge that was published
                        if ($Order.DnsSleep -gt 0 -and 'dns-01' -eq $chalType) {
                            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 {
                        Unpublish-Challenge $auth.DNSId $acct $challenge.token $Order.Plugin[$i] $PluginArgs -DnsAlias $Order.DnsAlias[$i]
                        Save-Challenge $Order.Plugin[$i] $PluginArgs
                    }
                }
                else {
                    try {
                        # Publish each challenge
                        Publish-Challenge $auth.DNSId $acct $challenge.token $Order.Plugin[$i] $PluginArgs -DnsAlias $Order.DnsAlias[$i]

                        # save the details of what we published for validation and cleanup later
                        $published += @{
                            identifier = $auth.DNSId
                            fqdn = $auth.fqdn
                            authUrl = $auth.location
                            plugin = $Order.Plugin[$i]
                            chalType = $chalType
                            chalToken = $challenge.token
                            chalUrl = $challenge.url
                            DNSAlias = $Order.DnsAlias[$i]
                        }
                    }
                    catch {
                        $PSCmdlet.ThrowTerminatingError($_)
                    }
                }

            } 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
                throw "$($auth.fqdn) authorization status is '$($auth.status)'. Create a new order and try again."
            }
        }

        if (-not $Order.UseSerialValidation) {
            try {
                # if we published any records, now we need to save them, wait for DNS
                # to propagate, and notify the server it can perform the validation
                if ($published.Count -gt 0) {

                    # grab the set of unique plugins that were used to publish challenges
                    $uniquePluginsUsed = $published.plugin | Sort-Object -Unique

                    # call the Save function for each plugin used
                    $uniquePluginsUsed | ForEach-Object {
                        Save-Challenge $_ $PluginArgs
                    }

                    # sleep while DNS changes propagate if there were DNS challenges published
                    $uniqueChalTypes = $script:Plugins[$uniquePluginsUsed].ChallengeType
                    if ($Order.DnsSleep -gt 0 -and 'dns-01' -in $uniqueChalTypes) {
                        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"
                    $published.chalUrl | Send-ChallengeAck -Account $acct

                    # and wait for them to succeed or fail
                    Wait-AuthValidation @($published.authUrl) $Order.ValidationTimeout
                }
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            finally {
                # always cleanup the challenges that were published
                $published | ForEach-Object {
                    Unpublish-Challenge $_.identifier $acct $_.chalToken $_.plugin $PluginArgs -DnsAlias $_.DNSAlias
                }

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

    }





    <#
    .SYNOPSIS
        Respond to authorization challenges for an ACME order and wait for the ACME server to validate them.
 
    .DESCRIPTION
        An ACME order contains an authorization object for each domain in the order. The client must complete at least one of a set of challenges for each authorization in order to prove they own the domain. Once complete, the client asks the server to validate each challenge and waits for the server to do so and update the authorization status.
 
    .PARAMETER Order
        The ACME order to perform the validations against. The order object must be associated with the currently active ACME account.
 
    .EXAMPLE
        Submit-ChallengeValidation
 
        Begin challenge validation on the current order.
 
    .EXAMPLE
        Get-PAOrder | Submit-ChallengeValidation
 
        Begin challenge validation on the current order.
 
    .LINK
        Project: https://github.com/rmbolger/Posh-ACME
 
    .LINK
        Get-PAOrder
 
    .LINK
        New-PAOrder
 
    #>

}