Public/Submit-ChallengeValidation.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
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.MainDomain -notin (Get-PAOrder -List).MainDomain) {
            Write-Error "Order for $($Order.MainDomain) 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 status is invalid for $($Order.MainDomain). 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.MainDomain)."
            return
        }
        elseif ($Order.status -eq 'ready') {
            Write-Warning "The order $($Order.MainDomain) 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 $Order.MainDomain

        # 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 111 | Submit-ChallengeValidation
 
        Begin challenge validation on the specified order.
 
    .LINK
        Project: https://github.com/rmbolger/Posh-ACME
 
    .LINK
        Get-PAOrder
 
    .LINK
        New-PAOrder
 
    #>

}