Private/New-Template.ps1

function New-Template {
    <#
        .SYNOPSIS
        Creates a new certificate template by cloning an existing template and configuring its properties.
 
        .DESCRIPTION
        This function creates a new certificate template by cloning an existing base template and
        optionally configuring specific properties like subject name handling and private key settings.
        The function uses the Windows Certificate Authority API (certca.dll) to perform the template
        creation and configuration without requiring additional PowerShell modules.
 
        .PARAMETER BaseTemplateName
        The name of the existing certificate template to use as a base for cloning.
        This template must exist in the Active Directory certificate templates container.
 
        .PARAMETER NewTemplateName
        The internal name for the new certificate template. This must be unique and will be
        used as the template identifier in Active Directory.
 
        .PARAMETER NewTemplateFriendlyName
        The display name for the new certificate template. This is the name that will be
        visible in the Certificate Authority management console and to end users.
 
        .PARAMETER EnrolleeSuppliesSubject
        If specified, configures the template to allow the certificate requestor to supply
        the subject name in the certificate request (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT).
        This creates an ESC1 vulnerability.
 
        .PARAMETER AllowExportableKey
        If specified, configures the template to allow private keys to be marked as exportable
        (CT_FLAG_EXPORTABLE_KEY).
 
        .PARAMETER EnableTemplate
        If specified, automatically enables the new template on all Certificate Authorities
        in the forest after creation.
 
        .INPUTS
        None
        This function does not accept pipeline input.
 
        .OUTPUTS
        PSCustomObject
        Returns a result object indicating success/failure and template details.
 
        .EXAMPLE
        New-Template -BaseTemplateName "User" -NewTemplateName "VulnUser" -NewTemplateFriendlyName "Vulnerable User Template" -EnrolleeSuppliesSubject
 
        .EXAMPLE
        New-Template -BaseTemplateName "Computer" -NewTemplateName "TestComputer" -NewTemplateFriendlyName "Test Computer Template" -AllowExportableKey -EnableTemplate
 
        .EXAMPLE
        $result = New-Template -BaseTemplateName "SmartcardLogon" -NewTemplateName "MySmartcard" -NewTemplateFriendlyName "My Smartcard Template" -EnrolleeSuppliesSubject -AllowExportableKey -EnableTemplate
        if ($result.Success) { Write-Host "Template created successfully" }
 
        .LINK
        https://docs.microsoft.com/en-us/windows/win32/api/certca/
 
        .LINK
        https://github.com/Devolutions/devolutions-labs/blob/master/powershell/scripts/New-CertificateTemplate.ps1
 
        .NOTES
        Requires administrative privileges on the Certificate Authority.
        Uses Windows Certificate Authority API (certca.dll) for template operations.
         
        WARNING: Using -EnrolleeSuppliesSubject creates ESC1 vulnerabilities. Only use in test environments.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$BaseTemplateName,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$NewTemplateName,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$NewTemplateFriendlyName,
        
        [Parameter()]
        [switch]$EnrolleeSuppliesSubject,
        
        [Parameter()]
        [switch]$AllowExportableKey,
        
        [Parameter()]
        [switch]$EnableTemplate
    )


    begin {
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Starting $($MyInvocation.MyCommand) on $env:COMPUTERNAME..."
        
        # Define the Certificate Authority API wrapper
        $certcaDefinition = @"
using System;
using System.Runtime.InteropServices;
 
public class CertCA
{
    // Enumeration flags
    public const uint CA_FLAG_ENUM_ALL_TYPES = 0x00000004;
    public const uint CT_FIND_LOCAL_SYSTEM = 0x00000002;
    public const uint CT_ENUM_MACHINE_TYPES = 0x00000040;
    public const uint CT_ENUM_USER_TYPES = 0x00000080;
    public const uint CT_FIND_BY_OID = 0x00000200;
    public const uint CT_FLAG_NO_CACHE_LOOKUP = 0x00000400;
    public const uint CT_FLAG_SCOPE_IS_LDAP_HANDLE = 0x00000800;
    public const uint CT_ENUM_ADMINISTRATOR_FORCE_MACHINE = 0x00001000;
    public const uint CT_ENUM_NO_CACHE_TO_REGISTRY = 0x00002000;
    public const uint CT_FLAG_ENUM_INCLUDE_INVALID_TYPES = 0x00004000;
 
    // Certificate Type Flag Types
    public const uint CERTTYPE_ENROLLMENT_FLAG = 0x01;
    public const uint CERTTYPE_SUBJECT_NAME_FLAG = 0x02;
    public const uint CERTTYPE_PRIVATE_KEY_FLAG = 0x03;
    public const uint CERTTYPE_GENERAL_FLAG = 0x04;
 
    // Subject Name Flags
    public const uint CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT = 0x00000001;
    public const uint CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000;
    public const uint CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000;
    public const uint CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME = 0x40000000;
    public const uint CT_FLAG_SUBJECT_REQUIRE_EMAIL = 0x20000000;
    public const uint CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000;
    public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_DNS = 0x08000000;
    public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000;
    public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_UPN = 0x02000000;
    public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000;
    public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_SPN = 0x00800000;
    public const uint CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000;
    public const uint CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008;
 
    // Private Key Flags
    public const uint CT_FLAG_ALLOW_PRIVATE_KEY_ARCHIVAL = 0x00000001;
    public const uint CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001;
    public const uint CT_FLAG_EXPORTABLE_KEY = 0x00000010;
    public const uint CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED = 0x00000020;
 
    // Common HRESULT values
    public const int S_OK = 0x00000000;
    public const int CRYPT_E_NOT_FOUND = unchecked((int)0x80092004);
    public const int CRYPT_E_EXISTS = unchecked((int)0x80092005);
 
    [DllImport("certca.dll", CharSet = CharSet.Unicode)]
    public static extern int CAFindCertTypeByName(
        string wszCertType,
        IntPtr hCAInfo,
        uint dwFlags,
        out IntPtr phCertType
    );
 
    [DllImport("certca.dll", CharSet = CharSet.Unicode)]
    public static extern int CACloneCertType(
        IntPtr hCertType,
        string wszCertType,
        string wszFriendlyName,
        IntPtr pvldap,
        uint dwFlags,
        out IntPtr phCertType
    );
 
    [DllImport("certca.dll", CharSet = CharSet.Unicode)]
    public static extern int CASetCertTypeFlagsEx(
        IntPtr hCertType,
        uint dwOption,
        uint dwFlags
    );
 
    [DllImport("certca.dll", CharSet = CharSet.Unicode)]
    public static extern int CAUpdateCertType(
        IntPtr hCertType
    );
 
    [DllImport("certca.dll", CharSet = CharSet.Unicode)]
    public static extern int CACloseCertType(
        IntPtr hCertType
    );
}
"@


        try {
            if (-not ([System.Management.Automation.PSTypeName]'CertCA').Type) {
                Add-Type -TypeDefinition $certcaDefinition
                Write-Verbose "Certificate Authority API wrapper loaded successfully"
            }
        } catch {
            Write-Error "Failed to load Certificate Authority API wrapper: $($_.Exception.Message)"
            return
        }
    }

    process {
        Write-Verbose "Creating new template '$NewTemplateName' based on '$BaseTemplateName'"
        
        # Initialize variables
        $hCAInfo = [IntPtr]::Zero
        $hBaseCertType = [IntPtr]::Zero
        $hNewCertType = [IntPtr]::Zero
        $dwFlags = [CertCA]::CT_FLAG_NO_CACHE_LOOKUP -bor [CertCA]::CT_ENUM_MACHINE_TYPES -bor [CertCA]::CT_ENUM_USER_TYPES
        $changes = @()
        
        try {
            # Step 1: Find the base certificate template
            Write-Verbose "Searching for base template: $BaseTemplateName"
            $hr = [CertCA]::CAFindCertTypeByName($BaseTemplateName, $hCAInfo, $dwFlags, [ref]$hBaseCertType)
            
            if ($hr -ne [CertCA]::S_OK -or $hBaseCertType -eq [IntPtr]::Zero) {
                if ($hr -eq [CertCA]::CRYPT_E_NOT_FOUND) {
                    $errorMsg = "Base certificate template '$BaseTemplateName' was not found"
                } else {
                    $errorMsg = "Failed to find base template '$BaseTemplateName': HRESULT 0x$($hr.ToString('X8'))"
                }
                
                return [PSCustomObject]@{
                    Success = $false
                    BaseTemplate = $BaseTemplateName
                    NewTemplate = $NewTemplateName
                    NewTemplateFriendlyName = $NewTemplateFriendlyName
                    Changes = @()
                    Error = $errorMsg
                }
            }
            
            Write-Verbose "Base template found successfully"
            
            # Step 2: Clone the base template
            Write-Verbose "Cloning template to create '$NewTemplateName'"
            $hr = [CertCA]::CACloneCertType($hBaseCertType, $NewTemplateName, $NewTemplateFriendlyName, [IntPtr]::Zero, 0, [ref]$hNewCertType)
            
            # Close the base template handle immediately after cloning
            if ($hBaseCertType -ne [IntPtr]::Zero) {
                [CertCA]::CACloseCertType($hBaseCertType) | Out-Null
                $hBaseCertType = [IntPtr]::Zero
            }
            
            if ($hr -ne [CertCA]::S_OK -or $hNewCertType -eq [IntPtr]::Zero) {
                if ($hr -eq [CertCA]::CRYPT_E_EXISTS) {
                    $errorMsg = "Certificate template '$NewTemplateName' already exists"
                } else {
                    $errorMsg = "Failed to clone template '$BaseTemplateName' to '$NewTemplateName': HRESULT 0x$($hr.ToString('X8'))"
                }
                
                return [PSCustomObject]@{
                    Success = $false
                    BaseTemplate = $BaseTemplateName
                    NewTemplate = $NewTemplateName
                    NewTemplateFriendlyName = $NewTemplateFriendlyName
                    Changes = @()
                    Error = $errorMsg
                }
            }
            
            Write-Verbose "Template cloned successfully"
            
            # Step 3: Configure subject name flags
            if ($EnrolleeSuppliesSubject) {
                Write-Verbose "Enabling enrollee supplies subject flag"
                $subjectNameFlag = [CertCA]::CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT
                $hr = [CertCA]::CASetCertTypeFlagsEx($hNewCertType, [CertCA]::CERTTYPE_SUBJECT_NAME_FLAG, $subjectNameFlag)
                
                if ($hr -eq [CertCA]::S_OK) {
                    $changes += "Enabled CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT (ESC1 vulnerability)"
                    Write-Verbose "Subject name flag configured successfully"
                } else {
                    Write-Warning "Failed to set subject name flag: HRESULT 0x$($hr.ToString('X8'))"
                }
            }
            
            # Step 4: Configure private key flags
            if ($AllowExportableKey) {
                Write-Verbose "Enabling exportable key flag"
                $privateKeyFlag = [CertCA]::CT_FLAG_EXPORTABLE_KEY
                $hr = [CertCA]::CASetCertTypeFlagsEx($hNewCertType, [CertCA]::CERTTYPE_PRIVATE_KEY_FLAG, $privateKeyFlag)
                
                if ($hr -eq [CertCA]::S_OK) {
                    $changes += "Enabled CT_FLAG_EXPORTABLE_KEY"
                    Write-Verbose "Private key flag configured successfully"
                } else {
                    Write-Warning "Failed to set private key flag: HRESULT 0x$($hr.ToString('X8'))"
                }
            }
            
            # Step 5: Save the template
            Write-Verbose "Saving template changes to Active Directory"
            $hr = [CertCA]::CAUpdateCertType($hNewCertType)
            
            if ($hr -ne [CertCA]::S_OK) {
                $errorMsg = "Failed to save template '$NewTemplateName': HRESULT 0x$($hr.ToString('X8'))"
                
                return [PSCustomObject]@{
                    Success = $false
                    BaseTemplate = $BaseTemplateName
                    NewTemplate = $NewTemplateName
                    NewTemplateFriendlyName = $NewTemplateFriendlyName
                    Changes = $changes
                    Error = $errorMsg
                }
            }
            
            Write-Verbose "Template saved successfully"
            
            # Step 6: Enable template on CAs if requested
            $enableResult = $null
            if ($EnableTemplate) {
                Write-Verbose "Enabling template on Certificate Authorities"
                try {
                    # Find the newly created template
                    $AdcsObjects = Get-AdcsObjects
                    $NewTemplateObj = $AdcsObjects | Where-Object { 
                        $_.ObjectClass -eq 'pKICertificateTemplate' -and 
                        $_.Properties['name'].Value -eq $NewTemplateName 
                    }
                    
                    if ($NewTemplateObj) {
                        $enableResult = Enable-Template -Template $NewTemplateObj -PassThru
                        if ($enableResult.Success) {
                            $changes += "Enabled template on Certificate Authority: $($enableResult.CertificateAuthority)"
                            Write-Verbose "Template enabled on CA successfully"
                        } else {
                            Write-Warning "Failed to enable template on CA: $($enableResult.Error)"
                        }
                    } else {
                        Write-Warning "Could not find newly created template to enable on CAs"
                    }
                } catch {
                    Write-Warning "Failed to enable template on CAs: $($_.Exception.Message)"
                }
            }
            
            # Return success result
            return [PSCustomObject]@{
                Success = $true
                BaseTemplate = $BaseTemplateName
                NewTemplate = $NewTemplateName
                NewTemplateFriendlyName = $NewTemplateFriendlyName
                Changes = $changes
                EnableResult = $enableResult
                Error = $null
            }
            
        } catch {
            $errorMsg = "Unexpected error during template creation: $($_.Exception.Message)"
            Write-Warning $errorMsg
            
            return [PSCustomObject]@{
                Success = $false
                BaseTemplate = $BaseTemplateName
                NewTemplate = $NewTemplateName
                NewTemplateFriendlyName = $NewTemplateFriendlyName
                Changes = $changes
                Error = $errorMsg
            }
            
        } finally {
            # Clean up handles
            if ($hNewCertType -ne [IntPtr]::Zero) {
                [CertCA]::CACloseCertType($hNewCertType) | Out-Null
                Write-Verbose "New template handle closed"
            }
            if ($hBaseCertType -ne [IntPtr]::Zero) {
                [CertCA]::CACloseCertType($hBaseCertType) | Out-Null
                Write-Verbose "Base template handle closed"
            }
        }
    }

    end {
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Finishing $($MyInvocation.MyCommand) on $env:COMPUTERNAME..."
    }
}