Last active
June 13, 2025 13:55
-
-
Save Bill-Stewart/da5256c544d38092174c4fe1262bc1d0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# New-CertificateSigningRequest.ps1 | |
# Written by Bill Stewart (bstewart AT iname.com) | |
# MIT license: Copyright 2025 Bill Stewart | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in | |
# all copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
# Windows provides the 'certreq -New' command to generate a certificate signing | |
# request (CSR) file, but the "policy" (.inf) file format it requires is arcane | |
# and cumbersome to format correctly. One of the the primary purposes of this | |
# script is to provode an easier-to-use INI file format that makes it simpler | |
# to specify the subject and subject alternative names (SANs) for the CSR | |
# without worrying about the formatting requirements of the .inf file. | |
# | |
# One of the behavioral "quirks" of the 'certreq -New' command is how it | |
# generates and stores the private key associated with the CSR: It creates a | |
# certificate in the 'Certificate Enrollment Requests' (REQUEST) certificate | |
# store and stores the private key with that newly created certificate. Usually | |
# we want the private key as a standalone file rather than stored in the local | |
# computer's certificate store, so this script uses the 'certutil -dump' | |
# command to determine the Subject Key Identifier (SKI) from the CSR. (The SKI | |
# uniquely identificates the CSR and the certificate thet stores the private | |
# key.) Then it identifies the matching certificate in the 'Certificate | |
# Enrollment Requests' store and exports the private key. | |
# | |
# If you specify -RemoveRequestCertificate, the script will remove the | |
# certificate containing the private key from the certificate store if it | |
# successfully exported the private key. | |
# | |
# One nuisance is that the script must be run from an elevated PowerShell | |
# session, but this seems to be a requirement for the 'certreq -New' command | |
# when specifying a [RequestAttributes] section and a certificate template in | |
# the .inf file. (The .inf specifies MachineKeySet=true so elevation would be | |
# required in any case.) | |
# | |
# It's recommended not to run this script from a remote PowerShell session | |
# because the 'certreq -New' command can produce GUI dialogs under certain | |
# circumstances. | |
# | |
# Version history: | |
# 2025-06-13 | |
# * No code changes; added MIT license. | |
# | |
# 2025-04-29 | |
# * I had tested only on Windows 11; the 'certutil -dump' output is different | |
# in Windows 10; updated Get-CertificateFileSubjectKeyIdentifier function | |
# to accommodate the differences | |
# | |
# 2025-04-28 | |
# * Initial version | |
#requires -version 5 | |
#requires -RunAsAdministrator | |
<# | |
.SYNOPSIS | |
Creates a certificate signing request (CSR) file and a private key file based on information provided in an INI file. | |
.DESCRIPTION | |
Creates a certificate signing request (CSR) file and a private key file based on information provided in an INI file. The CSR is suitable for use by web servers because it includes the DNS host name from the subject name's Common Name (CN) as a Subject Alternative Name (SAN). The INI file content is used to generate a certificate request policy (.inf) file for the 'certreq -New' command. See the NOTES section (i.e., 'help New-CertificateSigningRequest -Full') for more information. | |
.PARAMETER Path | |
Specifies the path to a text-based INI file that specifies the host name and optional subject alternative names (SANs) for the certificate signing request. | |
.PARAMETER TemplateName | |
This parameter is required is required if submitting a CSR to a Windows domain Certicication Authority (CA) and specifies the name of the certificate template to use. | |
.PARAMETER RemoveRequestCertificate | |
Removes the certificate containing the private key from the certificate store if the private key was successfully exported. There will be a removal prompt unless action confirmation is disabled (e.g., '-Confirm:0'). | |
.NOTES | |
The INI file for the Path parameter uses the following format: | |
---------------------------------------- | |
[Certificate] | |
Subject=CN=<full DNS hostname>[, O=<organization>[, L=<city>[, ST=<state>[, C=<country>]]]] | |
[SubjectAlternativeNames] | |
1=<alternative DNS hostname> | |
2=<alternative DNS hostname> | |
---------------------------------------- | |
Where: | |
* Subject specifies the common name (CN) that contains the DNS host name and other attributes that compose the certificate's subject; consult the CA's owner to determine which attributes of the subject (besides CN) are mandatory | |
* The [SubjectAlternativeNames] section is optional, but each alternative DNS hostname must start with a unique number | |
* The DNS host name is automatically added as a Subject Alternative Name (SAN), so the DNS host name from the [Certificate] section doesn't need to be repeated in the [SubjectAlternativeNames] section | |
Output files: | |
* <filename>.inf - Policy file used by 'certreq -New' command | |
* <filename>.req - Certificate signing request (CSR) file | |
* <filename>.key - Certificate's base64-encoded private key | |
If any of these output files already exist, there will be a prompt before overwriting. You can bypass the prompt(s) by specifying '-Confirm:0'. | |
The certificate request policy (.inf) file is retained for reference and troubleshooting. | |
If the CSR (.req) and private key (.key) files are generated successfully, the CSR (.req) file can be submitted to a Certification Authority (CA), and the CA will generate a certificate based on the CSR. | |
To create a PFX file containing the certificate and private key, follow these steps: | |
1. Copy the certificate file obtained from the CA to the same directory as the private key file. The certificate file must use the same filename (with a different extension) as the private key file (which must have the .key extension). | |
2. Run the following command: certutil -MergePFX <filename>.cer <filename>.pfx | |
.EXAMPLE | |
New-CertificateSigningRequest fabrikam-2025.ini -Template WebServer | |
fabrikam-2025.ini contains the following content: | |
---------------------------------------- | |
[Certificate] | |
Subject=CN=fabrikam.local | |
[SubjectAlternativeNames] | |
1=www.fabrikam.local | |
---------------------------------------- | |
This command will output the following files: | |
* fabrikam-2025.inf - Policy (.inf) file used by 'certreq -New' command | |
* fabrikam-2025.req - CSR file output by 'certreq -New' command | |
* fabrikam-2025.key - Certificate's base64-encoded private key | |
After obtaining the certificate file from the CA, copy it to the same directory as the private key file (fabrikam-2025.key) using the same filename but different extension from the private key file (e.g., fabrikam-2025.cer). You can then create a PFX file containing both the certificate and private key using the following command: | |
certutil -mergePFX fabrikam-2025.cer fabrikam-2025.pfx | |
The certutil command will prompt for a PFX file password and create the PFX file. | |
#> | |
[CmdletBinding(SupportsShouldProcess,ConfirmImpact = "High")] | |
param( | |
[Parameter(Position = 0,Mandatory)] | |
[ValidateNotNullOrEmpty()] | |
[String] | |
$Path, | |
[Parameter(Position = 1)] | |
[ValidateNotNullOrEmpty()] | |
[String] | |
$TemplateName, | |
[Switch] | |
$RemoveRequestCertificate | |
) | |
# Adds the P/Invoke definition for GetPrivateProfileString Win32 API. Call | |
# the GetPrivateProfileString by writing: | |
# [E4A889C175DD4F0A9B9550FA12A3E6D7.Kernel32]::GetPrivateProfileString | |
# The randomized type name ensures no namespace conflicts. | |
Add-Type -TypeDefinition @' | |
namespace E4A889C175DD4F0A9B9550FA12A3E6D7 { | |
using System.Runtime.InteropServices; | |
public static class Kernel32 { | |
// [E4A889C175DD4F0A9B9550FA12A3E6D7.Kernel32]::GetPrivateProfileString() | |
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |
public static extern uint GetPrivateProfileString(string lpAppName, | |
string lpKeyName, string lpDefault, [Out] byte[] lpBuffer, uint nSize, | |
string lpFileName); | |
} | |
} | |
'@ | |
# Common Win32 error codes | |
$ERROR_FILE_NOT_FOUND = 2 | |
$ERROR_ALREADY_EXISTS = 183 | |
# This here-string specifies the certificate request policy (.inf) content for | |
# the 'certreq -New' command. In the here-string, '{{' is a single '{' and '}}' | |
# is a single '}'.'{0}' will be replaced with the certificate subject and '{1}' | |
# will be replaced with a new line and a [RequestAttributes] section containing | |
# the CA template name (if specified). | |
$CertificateRequestPolicy = @' | |
[Version] | |
Signature="$Windows NT$" | |
[Strings] | |
OID_ENHANCED_KEY_USAGE="2.5.29.37" | |
OID_SUBJECT_ALT_NAME="2.5.29.17" | |
OID_PKIX_KP_SERVER_AUTH="1.3.6.1.5.5.7.3.1" | |
OID_PKIX_KP_CLIENT_AUTH="1.3.6.1.5.5.7.3.2" | |
[NewRequest] | |
Subject={0} | |
Exportable=true | |
HashAlgorithm="SHA256" | |
KeyLength=2048 | |
KeySpec="AT_KEYEXCHANGE" | |
KeyUsage="CERT_KEY_ENCIPHERMENT_KEY_USAGE | CERT_DIGITAL_SIGNATURE_KEY_USAGE" | |
MachineKeySet=true | |
ProviderName="Microsoft RSA SChannel Cryptographic Provider" | |
ProviderType=12 | |
RequestType="PKCS10" | |
SMIME=false | |
X500NameFlags="CERT_NAME_STR_REVERSE_FLAG"{1} | |
[Extensions] | |
%OID_ENHANCED_KEY_USAGE%="{{text}}%OID_PKIX_KP_SERVER_AUTH%,%OID_PKIX_KP_CLIENT_AUTH%" | |
%OID_SUBJECT_ALT_NAME%="{{text}} | |
'@ | |
# Outputs a message based on an error code. | |
function Get-Message { | |
param( | |
[Parameter(Mandatory)] | |
[Int] | |
$errorCode | |
) | |
$message = ([ComponentModel.Win32Exception] $errorCode).Message | |
if ( $errorCode -ne 0 ) { | |
"{0} [{1:X8}]" -f $message,$errorCode | |
} | |
else { | |
"{0}" -f $message | |
} | |
} | |
# Outputs content to the specified file as UTF8. The file path must be an | |
# absolute path. This function does not append a newline at the end of the | |
# content. | |
function Out-FileUTF8 { | |
param( | |
[Parameter(Position = 0,Mandatory)] | |
[String] | |
$path, | |
[Parameter(Position = 1,Mandatory,ValueFromPipeline)] | |
$content | |
) | |
[IO.File]::WriteAllText($path,$content) | |
} | |
# Outputs information from a .ini file using the GetPrivateProfileString Win32 | |
# API. Omit the section parameter to get a list of section names; omit the key | |
# parameter to get all key names from a section. | |
function Get-IniValue { | |
[CmdletBinding()] | |
param( | |
[parameter(Mandatory)] | |
[String] | |
$path, | |
[String] | |
$section, | |
[String] | |
$key | |
) | |
$fullPath = Resolve-Path $path | Select-Object -ExpandProperty ProviderPath | |
if ( $null -eq $fullPath ) { | |
return | |
} | |
$sectionSpecified = $PSBoundParameters.ContainsKey("section") | |
$keySpecified = $PSBoundParameters.ContainsKey("key") | |
# Conditional syntax: (<f>,<t>)[<bool>] | |
# Outputs <f> if <bool> is $false or <t> otherwise | |
$charsDiff = (1,2)[(-not $sectionSpecified) -or (-not $keySpecified)] | |
$charSize = [Text.Encoding]::Unicode.GetByteCount([Char] 0) | |
$nSize = 0 | |
do { | |
$nSize += 2KB | |
$buffer = New-Object Byte[] ($nSize * $charSize) | |
$charsCopied = [E4A889C175DD4F0A9B9550FA12A3E6D7.Kernel32]::GetPrivateProfileString( | |
([NullString]::Value,$section)[$sectionSpecified], # lpAppName | |
([NullString]::Value,$key)[$keySpecified], # lpKeyName | |
[NullString]::Value, # lpDefault | |
$buffer, # lpbuffer | |
$nSize, # nSize | |
$fullPath) # lpFileName | |
$lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() | |
if ( ($lastError -ne 0) -and ($lastError -ne 2) ) { | |
Write-Error (New-Object Management.Automation.ErrorRecord( | |
([ComponentModel.Win32Exception] $lastError), | |
$MyInvocation.MyCommand.Name, | |
([Management.Automation.ErrorCategory]::OpenError),$fullPath)) | |
return | |
} | |
} | |
until ( ($charsCopied -eq 0) -or ($charsCopied -ne $nSize - $charsDiff) ) | |
if ( $charsCopied -eq 0 ) { | |
return | |
} | |
[Text.Encoding]::Unicode.GetString($buffer,0, | |
(($charsCopied - --$charsDiff) * $charSize)) -split ([Char] 0) | |
} | |
# Uses the Pathname COM object (IADsPathname interface) to retrieve the Common | |
# Name (CN) portion of a distinguished name (DN). The advantage to using the | |
# Pathname object over, say, regex parsing of the DN, is that it correctly | |
# handles escape characters and other "gotchas." (The Pathname COM object | |
# unfortunately lacks a type library, so calling its methods and setting its | |
# properties are a bit "ugly". The '$null =' assignments are to prevent nulls | |
# from emitting to the output stream.) | |
function Get-CommonName { | |
param( | |
[String] | |
$distinguishedName | |
) | |
$pathname = New-Object -ComObject "Pathname" | |
if ( $null -eq $pathname ) { | |
return | |
} | |
# Values used by the Pathname object | |
$ADS_SETTYPE_DN = 4 | |
$ADS_DISPLAY_VALUE_ONLY = 2 | |
$ADS_ESCAPEDMODE_OFF_EX = 4 | |
# Initialize the object with the specified distinguished name (DN) | |
$null = [__ComObject].InvokeMember("Set", | |
[Reflection.BindingFlags]::InvokeMethod,$null,$pathname, | |
($distinguishedName,$ADS_SETTYPE_DN)) | |
# Get count of name elements in the DN | |
$numElements = [__ComObject].InvokeMember("GetNumElements", | |
[Reflection.BindingFlags]::InvokeMethod,$null,$pathname,$null) | |
if ( ($null -eq $numElements) -or ($numElements -eq 0) ) { | |
return | |
} | |
for ( $i = 0; $i -lt $numElements; $i++ ) { | |
# Get the 'i'th name element | |
$nameElement = [__ComObject].InvokeMember("GetElement", | |
[Reflection.BindingFlags]::InvokeMethod,$null,$pathname,$i) | |
# Does name element start with 'CN' followed by 0 or more whitespace | |
# characters and '='? | |
if ( $nameElement -match '^CN\s*=' ) { | |
# Suppress output of the name attribute prefix (e.g., 'CN=') | |
$null = [__ComObject].InvokeMember("SetDisplayType", | |
[Reflection.BindingFlags]::InvokeMethod,$null,$pathname, | |
$ADS_DISPLAY_VALUE_ONLY) | |
# Suppress all escape characters in the output | |
$null = [__ComObject].InvokeMember("EscapedMode", | |
[Reflection.BindingFlags]::SetProperty,$null,$pathname, | |
$ADS_ESCAPEDMODE_OFF_EX) | |
# Output the name element without escaping or prefix | |
[__ComObject].InvokeMember("GetElement", | |
[Reflection.BindingFlags]::InvokeMethod,$null,$pathname,$i) | |
return | |
} | |
} | |
} | |
# Outputs the certificate request policy (.inf) file content for the certreq | |
# command based on the specified .ini file content. | |
function Export-CertificateRequestPolicy { | |
param( | |
[Parameter(Mandatory)] | |
$path | |
) | |
$subject = Get-IniValue $path "Certificate" "Subject" | |
if ( -not $subject ) { | |
Write-Error "File '$path' is missing the '[Certificate]' section and/or 'Subject' value." -Category ObjectNotFound | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
$dnsHostName = Get-CommonName $subject | |
if ( -not $dnsHostName ) { | |
Write-Error "Unable to determine DNS host name from file '$path'." -Category ObjectNotFound | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
if ( $TemplateName ) { | |
$template = $CertificateRequestPolicy -f ('"{0}"' -f $subject), | |
('{0}[RequestAttributes]{0}CertificateTemplate="{1}"' -f | |
[Environment]::NewLine,$TemplateName) | |
} | |
else { | |
$template = $CertificateRequestPolicy -f ('"{0}"' -f $subject),'' | |
} | |
# Collect list of SANs; skip the DNS host name and duplicates | |
$subjectAltNames = New-Object Collections.Generic.List[String] | |
$iniKeys = Get-IniValue $path "SubjectAlternativeNames" | |
foreach ( $iniKey in $iniKeys ) { | |
$altName = Get-IniValue $path "SubjectAlternativeNames" $iniKey | |
if ( $altName -and ($subjectAltNames -notcontains $altName) -and | |
($altName -ne $dnsHostName) ) { | |
$SubjectAltNames.Add($altName) | |
} | |
} | |
# Add SAN for DNS host name specified in the subject | |
$template += "DNS=$dnsHostName" | |
# Add any additional SANs, delimited by '&' | |
foreach ( $subjectAltName in $subjectAltNames ) { | |
if ( $subjectAltName -ne $dnsHostName ) { | |
$template += "&DNS=$subjectAltName" | |
} | |
} | |
# Output with closing '"' and final newline | |
'{0}"{1}' -f $template,[Environment]::NewLine | |
} | |
# Runs the 'certreq -New' command. The function's return value is the | |
# executable's exit code. | |
function Invoke-Certreq { | |
param( | |
[Parameter(Mandatory)] | |
[String] | |
$policyFilePath, | |
[Parameter(Mandatory)] | |
[String] | |
$requestFilePath | |
) | |
$certreq = Join-Path ` | |
([Environment]::GetFolderPath([Environment+SpecialFolder]::System)) ` | |
"certreq.exe" | |
Write-Host "Running 'certreq -New' to create CSR from policy file..." | |
$null = & $certreq -New $policyFilePath $requestFilePath | |
Write-Host (Get-Message $LASTEXITCODE) | |
$LASTEXITCODE | |
} | |
# Uses 'certutil -dump' to extract the 'Subject Key Identifier' (SKI, OID | |
# 2.5.29.14) value from an X509 certificate file as a hexadecimal string. This | |
# function assumes that the 'certutil -dump' command output contains the hex | |
# value of the SKI two lines following the OID '2.5.29.14'. If Microsoft | |
# changes the 'certutil -dump' output such that this is no longer the case, | |
# then of course this function will have to be modified to accommodate the new | |
# output. | |
function Get-CertificateFileSubjectKeyIdentifier { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory)] | |
[String] | |
$path | |
) | |
$path = (Resolve-Path -LiteralPath $path).ProviderPath | |
if ( $null -eq $path ) { | |
return | |
} | |
$certutil = Join-Path ` | |
([Environment]::GetFolderPath([Environment+SpecialFolder]::System)) ` | |
"certutil.exe" | |
Write-Host "Running 'certutil -dump' to retrieve Subject Key Identifier from CSR file..." | |
$value = & $certutil -dump $path | | |
Select-String '2\.5\.29\.14' -Context 2 | | |
Select-Object -First 1 | ForEach-Object { | |
$_.Context.PostContext | | |
Select-String '^\s*(?:KeyID=)?([0-9a-f ]+)' | | |
Select-Object -Last 1 | | |
ForEach-Object { $_.Matches[0].Groups[1].Value } | |
} | |
Write-Host (Get-Message $LASTEXITCODE) | |
if ( $null -ne $value ) { | |
$value -replace '\s','' | |
} | |
} | |
# This function enumerates the certificates in the 'Certificate Enrollment | |
# Requests' store and outputs the X509Certificate2 object that matches the | |
# specified Subject Key Identifier (SKI). | |
function Get-CertificateBySubjectKeyIdentifier { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory)] | |
[String] | |
$subjectKeyIdentifier | |
) | |
foreach ( $cert in Get-ChildItem "Cert:\LocalMachine\REQUEST" ) { | |
$extensions = $cert.Extensions | | |
Where-Object { $_.Oid.Value -eq "2.5.29.14" } | |
foreach ( $extension in $extensions ) { | |
if ( $extension.SubjectKeyIdentifier -eq $subjectKeyIdentifier ) { | |
return $cert | |
} | |
} | |
} | |
} | |
# Outputs the private key from the specified certificate in PEM format. | |
function Export-PrivateKey { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory)] | |
[Security.Cryptography.X509Certificates.X509Certificate2] | |
$certificate | |
) | |
if ( -not $certificate.HasPrivateKey ) { | |
$certPath = 'Cert:\{0}' -f (Resolve-Path $certificate.PSPath | | |
Select-Object -ExpandProperty ProviderPath) | |
Write-Error ("Certificate '{0}' does not have a private key." -f | |
$certPath) -Category ObjectNotFound | |
return | |
} | |
$rsaCng = [Security.Cryptography.X509Certificates.RSACertificateExtensions]:: | |
GetRSAPrivateKey($certificate) | |
if ( $null -eq $rsaCng ) { | |
return | |
} | |
$keyBytes = $rsaCng.Key.Export([Security.Cryptography.CngKeyBlobFormat]:: | |
Pkcs8PrivateBlob) | |
if ( $null -eq $keyBytes ) { | |
return | |
} | |
'-----BEGIN PRIVATE KEY-----{0}{1}{0}-----END PRIVATE KEY-----{0}' -f | |
[Environment]::NewLine,[Convert]::ToBase64String($KeyBytes, | |
[Base64FormattingOptions]::InsertLineBreaks) | |
} | |
#----------------------------------------------------------------------------- | |
# MAIN SCRIPT BODY | |
#----------------------------------------------------------------------------- | |
# Resolve absolute INI file path. | |
$IniFilePath = Resolve-Path $Path | Select-Object -ExpandProperty ProviderPath | |
if ( -not $IniFilePath ) { | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
# Specify absolute policy (.inf) file path. | |
$PolicyFilePath = "{0}.inf" -f (Join-Path ` | |
([IO.Path]::GetDirectoryName($IniFilePath)) ` | |
([IO.Path]::GetFileNameWithoutExtension($IniFilePath))) | |
if ( Test-Path -LiteralPath $PolicyFilePath ) { | |
if ( -not $PSCmdlet.ShouldProcess($PolicyFilePath,"Overwrite file") ) { | |
exit $ERROR_ALREADY_EXISTS | |
} | |
Remove-Item -LiteralPath $PolicyFilePath -ErrorAction Stop | |
} | |
# File should not exist. | |
if ( Test-Path -LiteralPath $PolicyFilePath ) { | |
exit $ERROR_ALREADY_EXISTS | |
} | |
# Export the policy (.inf) file. | |
Export-CertificateRequestPolicy $IniFilePath | Out-FileUTF8 $PolicyFilePath | |
# File should exist. | |
if ( -not (Test-Path -LiteralPath $PolicyFilePath) ) { | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
Write-Host "Created certificate request policy file '$PolicyFilePath'." | |
# Specify absolute CSR (.req) file path. | |
$RequestFilePath = "{0}.req" -f (Join-Path ` | |
([IO.Path]::GetDirectoryName($IniFilePath)) ` | |
([IO.Path]::GetFileNameWithoutExtension($IniFilePath))) | |
if ( Test-Path -LiteralPath $RequestFilePath ) { | |
if ( -not $PSCmdlet.ShouldProcess($RequestFilePath,"Overwrite file") ) { | |
exit $ERROR_ALREADY_EXISTS | |
} | |
Remove-Item -LiteralPath $RequestFilePath -ErrorAction Stop | |
} | |
# File should not exist. | |
if ( Test-Path -LiteralPath $RequestFilePath ) { | |
exit $ERROR_ALREADY_EXISTS | |
} | |
# Run 'certreq -New' command to creates CSR (.req) file. | |
$ExitCode = Invoke-Certreq $PolicyFilePath $RequestFilePath | |
if ( $ExitCode -ne 0 ) { | |
exit $ExitCode | |
} | |
# File should exist. | |
if ( -not (Test-Path -LiteralPath $RequestFilePath) ) { | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
Write-Host "Created certificate request file '$RequestFilePath'." | |
# Retrieve Subject Key Identifier (SKI) from CSR (.req) file using 'certutil | |
# -dump' command. | |
$SubjectKeyId = Get-CertificateFileSubjectKeyIdentifier $RequestFilePath | |
if ( -not $SubjectKeyId ) { | |
Write-Error "Unable to determine Subject Key Identifier from '$RequestFilePath'." -Category ObjectNotFound | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
Write-Host "Certificate request Subject Key Identifier: $SubjectKeyId" | |
# When 'certreq -New' generates a CSR, it generates the private key as a | |
# certificate in the REQUEST (GUI, 'Certificate Enrollment Requests') | |
# certificate store. We need to find this certificate so we can export the | |
# private key. | |
$RequestedCert = Get-CertificateBySubjectKeyIdentifier $SubjectKeyId | |
if ( $null -eq $RequestedCert ) { | |
Write-Error "Unable to find certificate with Subject Key Identifier '$SubjectKeyId'." -Category ObjectNotFound | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
$RequestedCertPath = 'Cert:\{0}' -f (Resolve-Path $RequestedCert.PSPath | | |
Select-Object -ExpandProperty ProviderPath) | |
Write-Host "Found matching certificate '$RequestedCertPath'." | |
# Get the private key from the requested cert. | |
$PrivateKey = Export-PrivateKey $RequestedCert | |
if ( -not $PrivateKey ) { | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
Write-Host "Retrieved private key from '$RequestedCertPath'." | |
# Specify absolute private key (.key) file path. | |
$PrivateKeyPath = "{0}.key" -f (Join-Path ` | |
([IO.Path]::GetDirectoryName($IniFilePath)) ` | |
([IO.Path]::GetFileNameWithoutExtension($IniFilePath))) | |
if ( Test-Path -LiteralPath $PrivateKeyPath ) { | |
if ( -not $PSCmdlet.ShouldProcess($PrivateKeyPath,"Overwrite file") ) { | |
exit $ERROR_ALREADY_EXISTS | |
} | |
Remove-Item -LiteralPath $PrivateKeyPath -ErrorAction Stop | |
} | |
# File should not exist. | |
if ( Test-Path -LiteralPath $PrivateKeyPath ) { | |
exit $ERROR_ALREADY_EXISTS | |
} | |
$PrivateKey | Out-FileUTF8 $PrivateKeyPath | |
if ( -not (Test-Path -LiteralPath $PrivateKeyPath) ) { | |
exit $ERROR_FILE_NOT_FOUND | |
} | |
Write-Host "Exported private key to file '$PrivateKeyPath'." | |
if ( $RemoveRequestCertificate ) { | |
Remove-Item $RequestedCertPath -Confirm:$ConfirmPreference | |
if ( -not (Get-Item $RequestedCertPath -ErrorAction SilentlyContinue) ) { | |
Write-Host "Removed certificate '$RequestedCertPath'." | |
} | |
} |
Thanks for the feedback! You're free to use the code however you want. The MIT License is now at the top of the script.
Thank you so much for the quick response!
I also sent you an email yesterday. You can ignore it.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice code, thank you!
It currently does not appear to have a license associated with it. I would like to use this code but only if it has an open source license. Would you be able to let me know what license this code is under?
Thanks!