Public/Revoke-PACertificate.ps1

function Revoke-PACertificate {
    [CmdletBinding(
        DefaultParameterSetName='MainDomain',
        SupportsShouldProcess,
        ConfirmImpact='High'
    )]
    param(
        [Parameter(ParameterSetName='MainDomain',Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string]$MainDomain,
        [Parameter(ParameterSetName='MainDomain',ValueFromPipelineByPropertyName)]
        [ValidateScript({Test-ValidFriendlyName $_ -ThrowOnFail})]
        [string]$Name,
        [Parameter(ParameterSetName='CertFile',Mandatory,ValueFromPipelineByPropertyName)]
        [string]$CertFile,
        [Parameter(ParameterSetName='CertFile',ValueFromPipelineByPropertyName)]
        [string]$KeyFile,
        [RevocationReasons]$Reason,
        [switch]$Force
    )

    Begin {
        # make sure we have a server configured
        if (-not (Get-PAServer)) {
            try { throw "No ACME server configured. Run Set-PAServer first." }
            catch { $PSCmdlet.ThrowTerminatingError($_) }
        }

        try {
            # grab a reference to the current account if it exists
            $acct = Get-PAAccount
        } catch {}

        if ($Force){
            $ConfirmPreference = 'None'
        }

        $pemHeader = '-----BEGIN CERTIFICATE-----'
        $pemFooter = '-----END CERTIFICATE-----'
    }

    Process {

        if ('MainDomain' -eq $PSCmdlet.ParameterSetName) {

            if (-not $acct) {
                try { throw "No ACME account configured. Run Set-PAAccount or New-PAAccount first." }
                catch { $PSCmdlet.ThrowTerminatingError($_) }
            }

            if (-not $Name -and -not $MainDomain) {
                try { throw "Please specify either MainDomain or Name parameters." }
                catch { $PSCmdlet.WriteError($_); return }
            }

            # check for a unique matching order
            if ($Name) {
                if (-not ($order = Get-PAOrder -Name $Name)) {
                    try { throw "No order found matching Name '$Name'." }
                    catch { $PSCmdlet.WriteError($_); return }
                }
            } else {
                $matchingOrders = @(Get-PAOrder -List | Where-Object { $_.MainDomain -eq $MainDomain })
                if ($matchingOrders.Count -eq 1) {
                    $order = $matchingOrders
                } elseif ($matchingOrders.Count -ge 2) {
                    # error because we can't be sure which object to affect
                    try { throw "Multiple orders found for MainDomain '$MainDomain'. Please specify Name as well." }
                    catch { $PSCmdlet.WriteError($_); return }
                } else {
                    try { throw "No order found matching MainDomain '$MainDomain'." }
                    catch { $PSCmdlet.WriteError($_); return }
                }
            }

            # check for an existing certificate
            if (-not ($paCert = $order | Get-PACertificate)) {
                try { throw "No existing certificate found for $MainDomain." }
                catch { $PSCmdlet.WriteError($_); return }
            }

            # set the cert file path
            $CertFile = $paCert.CertFile
        }

        # do some minimal sanity checking on the cert file contents
        try {
            $certStr = (Get-Content $CertFile -EA Stop) -join ''
            if (-not ($certStr.StartsWith($pemHeader) -and $certStr.EndsWith($pemFooter))) {
                throw "Malformed certificate file: $CertFile"
            }
        }
        catch { $PSCmdlet.WriteError($_); return }

        # remove the header/footer and convert to Base64Url as ACME expects
        $certStr = $certStr.Replace($pemHeader,'').Replace($pemFooter,'').Trim() |
            ConvertTo-Base64Url -FromBase64

        # Now we need to decide how we're going to sign to request. It can either
        # be signed with the private key that matches the cert or an ACME account
        # key. The ACME account must either be the one that orderd the cert
        # or one that has currently valid authorizations for all identifiers in
        # the cert.
        # https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1
        #
        # BuyPass currently only implements Account key based revocation and throws
        # an Internal Server Error if you try to revoke via the cert key.

        # check the private key
        if ($KeyFile) {
            if (Test-Path $KeyFile -PathType Leaf) {
                try {
                    $certKey = Import-Pem -InputFile $KeyFile | ConvertFrom-BCKey
                }
                catch {
                    Write-Warning "Unable to import private key file $($KeyFile): $($_.Exception.Message). Will attempt revocation with account key."
                }
            }
            else {
                Write-Warning "Private key $KeyFile was not found. Will attempt revocation with account key."
            }
        }
        else { Write-Debug "Will attempt revocation with account key." }

        # start building the splat for Invoke-ACME
        $acmeParams = @{
            ErrorAction = 'Stop'
        }

        if ($certKey) {
            Write-Debug "Attempting to use cert key"

            # determine the alg from the key
            $alg = 'RS256'
            if ($certKey -is [Security.Cryptography.ECDsa]) {
                if     ($certKey.KeySize -eq 256) { $alg = 'ES256' }
                elseif ($certKey.KeySize -eq 384) { $alg = 'ES384' }
                elseif ($certKey.KeySize -eq 521) { $alg = 'ES512' }
            }

            # build the protected header
            $acmeParams.Header = @{
                alg   = $alg
                jwk   = ($certKey | ConvertTo-Jwk -PublicOnly)
                nonce = $script:Dir.nonce
                url   = $script:Dir.revokeCert
            }

            # set the key
            $acmeParams.Key = $certKey

        } else {
            Write-Debug "Attempting to use account key"

            # build the protected header
            $acmeParams.Header = @{
                alg   = $acct.alg
                kid   = $acct.location
                nonce = $script:Dir.nonce
                url   = $script:Dir.revokeCert
            }

            # set the account
            $acmeParams.Account = $acct
        }

        # build the payload
        $payload = @{ certificate = $certStr }
        if ($Reason) {
            $payload.reason = $Reason
        }
        $acmeParams.PayloadJson = $payload | ConvertTo-Json -Depth 5 -Compress

        # send the request
        if ($PSCmdlet.ShouldProcess($CertFile)){
            Write-Verbose "Sending revocation request."
            try {
                Invoke-ACME @acmeParams | Out-Null
            } catch { $PSCmdlet.ThrowTerminatingError($_) }
        }

    }
}

# Define an enum to represent the revocations reasons defined in RFC 5280
# that ACME supports.
# https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
# https://datatracker.ietf.org/doc/html/rfc5280#section-5.3.1
if (-not ([System.Management.Automation.PSTypeName]'RevocationReasons').Type)
{
    enum RevocationReasons {
        keyCompromise        = 1
        cACompromise         = 2
        affiliationChanged   = 3
        superseded           = 4
        cessationOfOperation = 5
        certificateHold      = 6
        removeFromCRL        = 8
        privilegeWithdrawn   = 9
        aACompromise         = 10
    }
}