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
                }
            }
        }

    }
}