Private/Invoke-ACME.ps1

function Invoke-ACME {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Uri,
        [Parameter(Mandatory)]
        [Security.Cryptography.AsymmetricAlgorithm]$Key,
        [Parameter(Mandatory)]
        [hashtable]$Header,
        [Parameter(Mandatory)]
        [string]$PayloadJson,
        [switch]$NoRetry
    )

    # make sure we have a server configured
    if (!(Get-PAServer)) {
        throw "No ACME server configured. Run Set-PAServer first."
    }

    # Because we're not refreshing the server on module load, we may not have a
    # NextNonce set yet. So check the header, and grab a fresh one if it's empty.
    if ([string]::IsNullOrWhiteSpace($Header.nonce)) {
        $Header.nonce = Get-Nonce
    }

    # Validation on the rest of the header will be taken care of by New-Jws. And
    # the only reason we aren't just simplifying by changing the input param to a
    # completed JWS string is because we want to be able to auto-retry on errors
    # like badNonce which requires modifying the Header and re-signing a new JWS.
    $Jws = New-Jws $Key $Header $PayloadJson

    # since HTTP error codes make Invoke-WebRequest throw an exception,
    # we need to wrap it in a try/catch. But we can still get the response
    # object via the exception.

    try {
        $response = Invoke-WebRequest -Uri $Uri -Body $Jws -Method Post `
            -ContentType 'application/jose+json' -UserAgent $script:USER_AGENT `
            -Headers $script:COMMON_HEADERS -EA Stop @script:UseBasic

        # update the next nonce if it was sent
        if ($response.Headers.ContainsKey($script:HEADER_NONCE)) {
            $script:Dir.nonce = $response.Headers[$script:HEADER_NONCE] | Select-Object -First 1
            Write-Debug "Updating nonce: $($script:Dir.nonce)"
        }

        return $response

    } catch {
        # Since we can't catch explicit exception types between PowerShell editions
        # without errors for non-existent types, we need to string match the type names
        # and re-throw anything we don't care about.
        $exType = $_.Exception.GetType().FullName
        if ('System.Net.WebException' -eq $exType) {

            # This is the exception that gets thrown in PowerShell Desktop edition

            # get the response object: System.Net.HttpWebResponse
            $ex = $_.Exception
            $response = $ex.Response

            # update the next nonce if it was sent
            if ($script:HEADER_NONCE -in $response.Headers) {
                $script:Dir.nonce = $response.GetResponseHeader($script:HEADER_NONCE) | Select-Object -First 1
                Write-Debug "Updating nonce from error response: $($script:Dir.nonce)"
                $freshNonce = $true
            }

            # grab the raw response body
            $sr = New-Object IO.StreamReader($response.GetResponseStream())
            $sr.BaseStream.Position = 0
            $sr.DiscardBufferedData()
            $body = $sr.ReadToEnd()
            Write-Debug "Error Body: $body"

        } elseif ('Microsoft.PowerShell.Commands.HttpResponseException' -eq $exType) {

            # This is the exception that gets thrown in PowerShell Core edition

            # get the response object
            # Linux type: System.Net.Http.CurlHandler+CurlResponseMessage
            # Mac type: ???
            # Win type: System.Net.Http.HttpResponseMessage
            $ex = $_.Exception
            $response = $ex.Response

            # update the next nonce if it was sent
            if ($script:HEADER_NONCE -in $response.Headers.Key) {
                $script:Dir.nonce = ($response.Headers | Where-Object { $_.Key -eq $script:HEADER_NONCE }).Value | Select-Object -First 1
                Write-Debug "Updating nonce from error response: $($script:Dir.nonce)"
                $freshNonce = $true
            }

            # Currently in PowerShell 6, there's no way to get the raw response body from an
            # HttpResponseException because they dispose the response stream.
            # https://github.com/PowerShell/PowerShell/issues/5555
            # https://get-powershellblog.blogspot.com/2017/11/powershell-core-web-cmdlets-in-depth.html
            # However, a "processed" version of the body is available via ErrorDetails.Message
            # which *should* work for us. The processing they're doing should only be removing HTML
            # tags. And since our body should be JSON, there shouldn't be any tags to remove.
            # So we'll just go with it for now until someone reports a problem.
            $body = $_.ErrorDetails.Message
            Write-Debug "Error Body: $body"

        } else { throw }

        # ACME uses RFC7807, Problem Details for HTTP APIs
        # https://tools.ietf.org/html/rfc7807
        # So a JSON parseable error object should be in the response body.
        try { $acmeError = $body | ConvertFrom-Json }
        catch {
            Write-Warning "Response body was not JSON parseable"
            # re-throw the original exception
            throw $ex
        }

        # check for badNonce and retry once
        if (!$NoRetry -and $freshNonce -and $acmeError.type -and $acmeError.type -like '*:badNonce') {
            $Header.nonce = $script:Dir.nonce
            Write-Debug "Retrying with updated nonce"
            return (Invoke-ACME $Uri $Key $Header $PayloadJson -NoRetry)
        }

        # throw the converted AcmeException
        throw [AcmeException]::new($acmeError.detail,$acmeError)
    }

}