Private/PSPkinit/New-PkinitCertificateRequest.ps1

function New-PkinitCertificateRequest {
    <#
        .SYNOPSIS
        Requests a certificate from an AD CS CA, with a caller-supplied Subject
        Alternative Name, using the native CertEnroll/CertificateAuthority COM
        object model (no certreq.exe, no INF files, no text-scraping - and no
        .NET-version gate, unlike the modern CertificateRequest/
        SubjectAlternativeNameBuilder classes, so this works on Windows
        PowerShell 5.1 as well as PowerShell 7+).
 
        .DESCRIPTION
        Generates a private key and PKCS#10 CSR, submits it to the specified CA
        with the given certificate template and SAN, and installs the resulting
        certificate against the same private key. The returned certificate can
        be passed straight to Invoke-PkinitAuthentication, and the SAN value
        returned alongside it is exactly what to pass as -ClientName so you
        authenticate as the identity that SAN represents.
 
        For the CA to actually honor the supplied SAN, the certificate template
        must allow the enrollee to supply it - the Subject Name tab set to
        "Supply in the request" rather than "Build from this Active Directory
        information" (msPKI-Certificate-Name-Flag bit
        CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT, 0x00000001). This cmdlet embeds the
        SAN directly as an extension on the CSR, which is what that template
        setting honors - no CA-wide registry flag needed for it.
 
        As a fallback for templates that build the subject from AD instead,
        the SAN is also sent as a request attribute string (SAN:upn=...), but
        that mechanism additionally requires the CA to have the
        EDITF_ATTRIBUTESUBJECTALTNAME2 edit flag set:
 
            certutil -config <CAConfig> -setreg policy\EditFlags +EDITF_ATTRIBUTESUBJECTALTNAME2
            Restart-Service CertSvc
 
        .PARAMETER Subject
        The certificate's subject distinguished name, e.g. 'CN=Test User'.
 
        .PARAMETER San
        The Subject Alternative Name value to request, e.g. 'user@adcs.goat'.
 
        .PARAMETER SanType
        The SAN's type. Defaults to 'UserPrincipalName' (an otherName UPN SAN,
        OID 1.3.6.1.4.1.311.20.2.3 - what Windows/PKINIT strong certificate
        mapping actually looks at). 'Dns' and 'Email' are also supported.
 
        .PARAMETER Sid
        Optional. Embeds the SID security extension (OID 1.3.6.1.4.1.311.25.2 -
        the same extension a Windows CA normally adds automatically) directly
        on the CSR, set to this SID value. Useful when a template/CA has this
        extension disabled (msPKI-Enrollment-Flag NO_SECURITY_EXTENSION) and
        therefore won't add it itself - without it, a KDC in Full Enforcement
        mode will reject the resulting certificate for PKINIT.
 
        SECURITY: only ever supply the SID of an identity you are authorized
        to test as. A CA/template that honors an arbitrary client-supplied SID
        here - especially combined with an arbitrary client-supplied SAN - is
        the same primitive used by real certificate SID-spoofing/impersonation
        techniques (see e.g. GhostPack/Certify's --sid option, which is
        compiled out of release builds by default for this reason). This is
        NOT a way to authenticate as an identity you don't already control.
 
        .PARAMETER Template
        The certificate template name to request against (the CA-side
        template's common/short name, not its display name).
 
        .PARAMETER CertificateAuthority
        The target CA's config string, in "<CAHostname>\<CAName>" form (the
        same format certreq.exe's -config parameter uses).
 
        .PARAMETER KeyLength
        RSA key length in bits. Defaults to 2048.
 
        .PARAMETER ProviderName
        The CSP/KSP to generate the private key with. Defaults to 'Microsoft
        Software Key Storage Provider'.
 
        .PARAMETER MachineContext
        Requests/installs the certificate in the local machine context
        (Cert:\LocalMachine\My) instead of the current user's
        (Cert:\CurrentUser\My, the default).
 
        .OUTPUTS
        PSCustomObject with Certificate (X509Certificate2, with private key),
        Thumbprint, Subject, San, and SanType.
 
        .EXAMPLE
        $reqParams = @{
            Subject = 'CN=Test User'
            San = 'user@adcs.goat'
            Template = 'User'
            CertificateAuthority = 'ADCSGOAT-CA\LabRootCA1'
        }
        New-PkinitCertificateRequest @reqParams
 
        Requests and installs a certificate to Cert:\CurrentUser\My, returning
        the result without immediately authenticating.
 
        .EXAMPLE
        $reqParams = @{
            Subject = 'CN=Test User'
            San = 'user@adcs.goat'
            Template = 'UserWithSIDExtension'
            CertificateAuthority = 'ADCSGOAT-CA\LabRootCA1'
            Sid = 'S-1-5-21-1-2-3-1105'
            MachineContext = $true
        }
        New-PkinitCertificateRequest @reqParams
 
        Embeds an explicit SID security extension on the CSR and installs the
        issued certificate to Cert:\LocalMachine\My instead of the current
        user's store.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Subject,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $San,

        [Parameter()]
        [ValidateSet('UserPrincipalName', 'Dns', 'Email')]
        [string] $SanType = 'UserPrincipalName',

        [Parameter()]
        [string] $Sid,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Template,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $CertificateAuthority,

        [Parameter()]
        [int] $KeyLength = 2048,

        [Parameter()]
        [string] $ProviderName = 'Microsoft Software Key Storage Provider',

        [Parameter()]
        [switch] $MachineContext
    )

    try {
        $csrResult = New-PkinitCertificateSigningRequest -Subject $Subject -San $San -SanType $SanType -Sid $Sid `
            -KeyLength $KeyLength -ProviderName $ProviderName -MachineContext:$MachineContext

        $issuedCertBase64 = Submit-PkinitCertificateSigningRequest -Base64Csr $csrResult.Base64Csr -Template $Template `
            -San $San -SanType $SanType -CertificateAuthorityConfig $CertificateAuthority

        Install-PkinitCertificateResponse -Enrollment $csrResult.Enrollment -Base64Certificate $issuedCertBase64

        $issuedCertBytes = [Convert]::FromBase64String($issuedCertBase64)
        $issuedCertPreview = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($issuedCertBytes)

        $installedCert = Get-PkinitCertificateFromStore -Thumbprint $issuedCertPreview.Thumbprint -MachineContext:$MachineContext
        if (-not $installedCert) {
            throw "Certificate was issued (thumbprint $($issuedCertPreview.Thumbprint)) but was not found in the certificate store after install."
        }

        return [PSCustomObject]@{
            PSTypeName  = 'PSPkinit.CertificateRequestResult'
            Certificate = $installedCert
            Thumbprint  = $installedCert.Thumbprint
            Subject     = $Subject
            San         = $San
            SanType     = $SanType
        }
    } catch {
        $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new(
                $_.Exception, 'PkinitCertificateRequestFailed', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null))
    }
}