private/functions/Submit-AxisCsrToWindowsCA.ps1

function Submit-AxisCsrToWindowsCA {
    <#
    .SYNOPSIS
    Submits a PKCS#10 CSR to a Microsoft AD CS certification authority and returns the issued certificate.

    .DESCRIPTION
    Issuer backend for authorities of Type 'WindowsCA'. Uses the ICertRequest2 COM interface
    (CertificateAuthority.Request, certcli.dll) exactly like certreq -submit does:
      Submit(CR_IN_BASE64HEADER|CR_IN_PKCS10, <csr>, "CertificateTemplate:<Template>", <CAConfig>)
    Disposition 3 (Issued) -> certificate is fetched with GetCertificate; 5 (Under submission / pending
    CA manager approval) -> RetrievePending is polled every PollSeconds until TimeoutSec (unless -NoWait,
    in which case the Pending status is returned); 2 (Denied), 0 (Incomplete) and 1 (Error) throw with the
    text of GetDispositionMessage.
    The chain is taken from GetCertificate(CR_OUT_BASE64HEADER|CR_OUT_CHAIN) (a PKCS#7 bundle), falling
    back to GetCACertificate(0, config, 0) (the CA signing certificate) when the PKCS#7 cannot be decoded.
    Constants come from CertCli.h (Windows SDK):
      CR_IN_BASE64HEADER=0, CR_IN_BASE64=1, CR_IN_PKCS10=0x100, CR_OUT_BASE64HEADER=0, CR_OUT_CHAIN=0x100,
      CR_DISP_INCOMPLETE=0, CR_DISP_ERROR=1, CR_DISP_DENIED=2, CR_DISP_ISSUED=3,
      CR_DISP_ISSUED_OUT_OF_BAND=4, CR_DISP_UNDER_SUBMISSION=5, CR_DISP_REVOKED=6
      https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/um/CertCli.h
      https://learn.microsoft.com/en-us/windows/win32/api/certcli/nf-certcli-icertrequest-submit
      https://learn.microsoft.com/en-us/windows/win32/api/certcli/nf-certcli-icertrequest-getcertificate
      https://learn.microsoft.com/en-us/windows/win32/api/certcli/nf-certcli-icertrequest-retrievepending
    Windows only (COM). The caller needs Enroll permission on the template and Request permission on the CA.

    .PARAMETER Authority
    Authority hashtable from $Config.CertificateAuthorities:
    @{ Type='WindowsCA'; CAConfig='host\CA Name'; Template='<template name>'; PollSeconds=5; TimeoutSec=300 }

    .PARAMETER Csr
    The certificate signing request as a PEM string (-----BEGIN CERTIFICATE REQUEST-----).

    .PARAMETER Device
    Optional device name, used only for verbose/log output.

    .PARAMETER TimeoutSec
    Overrides Authority.TimeoutSec (seconds to wait for a pending request). Default 300.

    .PARAMETER NoWait
    Do not poll a pending request; return Status='Pending' with the RequestId instead of throwing.

    .OUTPUTS
    [pscustomobject]@{ Certificate=<PEM leaf>; Chain=@(<PEM CA certs, leaf-nearest first>); Serial=<hex upper>; RequestId=<string>; Status='Issued'|'Pending' }
    #>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory)]
        [Hashtable]$Authority,

        [Parameter(Mandatory)]
        [String]$Csr,

        [Parameter()]
        [String]$Device = "",

        [Parameter()]
        [Int]$TimeoutSec,

        [Parameter()]
        [Switch]$NoWait
    )

    #Constants from CertCli.h (see .DESCRIPTION for the source)
    $CR_IN_BASE64HEADER = 0
    $CR_IN_BASE64 = 1
    $CR_IN_PKCS10 = 0x100
    $CR_OUT_BASE64HEADER = 0
    $CR_OUT_CHAIN = 0x100
    $CR_DISP_INCOMPLETE = 0
    $CR_DISP_ERROR = 1
    $CR_DISP_DENIED = 2
    $CR_DISP_ISSUED = 3
    $CR_DISP_ISSUED_OUT_OF_BAND = 4
    $CR_DISP_UNDER_SUBMISSION = 5
    $CR_DISP_REVOKED = 6

    $CAConfig = [string]$Authority.CAConfig
    $Template = [string]$Authority.Template
    if(!$CAConfig) { Throw "Authority is missing CAConfig (expected 'host\CA Name')" }
    if(!$Template) { Throw "Authority is missing Template (AD CS certificate template name)" }

    $PollSeconds = 5
    if($Authority.PollSeconds) { $PollSeconds = [int]$Authority.PollSeconds }
    if($PollSeconds -lt 1) { $PollSeconds = 1 }
    if(!$PSBoundParameters.ContainsKey('TimeoutSec')) {
        $TimeoutSec = 300
        if($Authority.TimeoutSec) { $TimeoutSec = [int]$Authority.TimeoutSec }
    }

    $LogId = "Certificate"
    if($Device) { $LogId = $Device }

    #certreq accepts both the armored and the bare base64 form; mirror that.
    $Flags = $CR_IN_PKCS10
    if($Csr -match '-----BEGIN') {
        $Flags = $Flags -bor $CR_IN_BASE64HEADER
    }
    else {
        $Flags = $Flags -bor $CR_IN_BASE64
    }
    $Attributes = "CertificateTemplate:$Template"

    $Request = New-AxisComObject -ProgId "CertificateAuthority.Request"

    Write-Verbose "Submitting CSR to $CAConfig with template $Template"
    Try {
        $Disposition = $Request.Submit($Flags, $Csr, $Attributes, $CAConfig)
    }
    Catch {
        Throw "Failed to submit certificate request to $CAConfig : $_"
    }

    $RequestId = $null
    Try { $RequestId = [string]$Request.GetRequestId() } Catch { }
    Write-Verbose "CA $CAConfig returned disposition $Disposition for request $RequestId"
    Write-Log -ID $LogId -Message "Submitted CSR to $CAConfig (template $Template): request $RequestId disposition $Disposition"

    #Pending: poll RetrievePending until issued, denied or timed out.
    if($Disposition -eq $CR_DISP_UNDER_SUBMISSION -or $Disposition -eq $CR_DISP_INCOMPLETE) {
        if($NoWait) {
            return [pscustomobject]@{
                Certificate = $null
                Chain = @()
                Serial = $null
                RequestId = $RequestId
                Status = "Pending"
            }
        }
        if(!$RequestId) {
            Throw "Certificate request to $CAConfig is pending but no request ID was returned: $($Request.GetDispositionMessage())"
        }
        $Deadline = (Get-Date).AddSeconds($TimeoutSec)
        while($Disposition -eq $CR_DISP_UNDER_SUBMISSION -or $Disposition -eq $CR_DISP_INCOMPLETE) {
            if((Get-Date) -ge $Deadline) {
                Throw "Certificate request $RequestId is pending approval on $CAConfig (timed out after $TimeoutSec seconds). Approve it in the CA console and run the request again."
            }
            Write-Verbose "Request $RequestId pending on $CAConfig, polling again in $PollSeconds s"
            Start-Sleep -Seconds $PollSeconds
            Try {
                $Disposition = $Request.RetrievePending([int]$RequestId, $CAConfig)
            }
            Catch {
                Throw "Failed to poll pending request $RequestId on $CAConfig : $_"
            }
        }
    }

    if($Disposition -ne $CR_DISP_ISSUED) {
        $Message = ""
        Try { $Message = [string]$Request.GetDispositionMessage() } Catch { }
        switch($Disposition) {
            $CR_DISP_DENIED { Throw "Certificate request $RequestId was denied by $CAConfig : $Message" }
            $CR_DISP_ERROR { Throw "Certificate request $RequestId failed on $CAConfig : $Message" }
            $CR_DISP_ISSUED_OUT_OF_BAND { Throw "Certificate request $RequestId was issued out of band by $CAConfig and cannot be retrieved: $Message" }
            $CR_DISP_REVOKED { Throw "Certificate request $RequestId is already revoked on $CAConfig : $Message" }
            default { Throw "Certificate request $RequestId returned unexpected disposition $Disposition from $CAConfig : $Message" }
        }
    }

    #Issued: leaf certificate (PEM with header) and chain (PKCS#7 PEM with header).
    Try {
        $LeafPem = [string]$Request.GetCertificate($CR_OUT_BASE64HEADER)
    }
    Catch {
        Throw "Request $RequestId was issued but the certificate could not be retrieved from $CAConfig : $_"
    }
    $Leaf = @(ConvertFrom-PemString -Pem $LeafPem -AsCertificate)[0]
    if(!$Leaf) { Throw "Request $RequestId : CA $CAConfig returned an unreadable certificate" }
    $Serial = ($Leaf.SerialNumber -replace '[^0-9A-Fa-f]','').ToUpper()
    #Normalise the leaf to one clean PEM block
    $LeafPem = ConvertTo-PemString -Certificate $Leaf

    $Chain = @()
    $ChainCerts = @()
    Try {
        $P7Pem = [string]$Request.GetCertificate($CR_OUT_BASE64HEADER -bor $CR_OUT_CHAIN)
        $P7Bytes = @(ConvertFrom-PemString -Pem $P7Pem)[0].Bytes
        $Collection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
        #X509Certificate2Collection.Import decodes PKCS#7 (and DER) on .NET Framework and .NET 8
        $Collection.Import([byte[]]$P7Bytes)
        $ChainCerts = @($Collection | Where-Object { $_.Thumbprint -ne $Leaf.Thumbprint })
    }
    Catch {
        Write-Verbose "PKCS#7 chain not available from $CAConfig ($_), falling back to GetCACertificate"
        Try {
            $CaPem = [string]$Request.GetCACertificate(0, $CAConfig, $CR_OUT_BASE64HEADER)
            $ChainCerts = @(ConvertFrom-PemString -Pem $CaPem -AsCertificate)
        }
        Catch {
            Write-Warning "Could not retrieve the CA certificate chain from $CAConfig : $_"
        }
    }

    #Order leaf-nearest first by following Issuer -> Subject
    $Remaining = [System.Collections.ArrayList]@($ChainCerts)
    $Current = $Leaf
    while($Remaining.Count -gt 0) {
        $Next = $Remaining | Where-Object { $_.Subject -eq $Current.Issuer -and $_.Thumbprint -ne $Current.Thumbprint } | Select-Object -First 1
        if(!$Next) { break }
        $Chain += ConvertTo-PemString -Certificate $Next
        [void]$Remaining.Remove($Next)
        if($Next.Subject -eq $Next.Issuer) { break }
        $Current = $Next
    }
    #Anything that did not link (cross certs etc.) is appended so nothing is lost
    foreach($Extra in $Remaining) { $Chain += ConvertTo-PemString -Certificate $Extra }

    Write-Log -ID $LogId -Message "Certificate issued by $CAConfig : request $RequestId serial $Serial"

    return [pscustomobject]@{
        Certificate = $LeafPem
        Chain = @($Chain)
        Serial = $Serial
        RequestId = $RequestId
        Status = "Issued"
    }
}