Other posts in the series:


Hello S-1-1-0! In previous post we covered basic information about certificate requests, what they are and which information they store. Today we will get basic practice in working with CertEnroll interfaces in Windows PowerShell.

COM interface naming convention

COM objects are normally instantiated by using their ProgId which in most cases consist of two parts:

  • Usage area (for example, X509Enrollment);
  • Class name.

While usage area is used as is, class names differs to interface name. In MSDN documentation CertEnroll interfaces are prefixed with IX509 that denotes the area type — X.509 objects. For example, IX509PrivateKey. In order to create an instance of this interface in PowerShell, you will have to replace first "I" letter ("I" means Interface) with "C" letter and prepend entire name with X509Enrollment prefix. For example:

New-Object -ComObject X509Enrollment.CX509PrivateKey

For IObjectId you'll do the same conversation: replace first I with C and prepend with X509Enrollment:

New-Object -ComObject X509Enrollment.CObjectID

If you are still unsure about the COM class name for particular interface, you can check the registry, as all COM interfaces are registered there. Here is a little cheat sheet for CertEnroll COM class names:

[↓] [vPodans] dir hklm:\SOFTWARE\Classes\x509* | %{$_.PSChildName}
X509Enrollment.CAlternativeName
X509Enrollment.CAlternativeName.1
X509Enrollment.CAlternativeNames
X509Enrollment.CAlternativeNames.1
X509Enrollment.CBinaryConverter
X509Enrollment.CBinaryConverter.1
X509Enrollment.CCertificatePolicies
X509Enrollment.CCertificatePolicies.1
X509Enrollment.CCertificatePolicy
X509Enrollment.CCertificatePolicy.1
<...>

Many COM class names contains two entries. One with ".1" suffix and without. I will not talk about these differences, just tell you that you can use either name.

Technical details

In the first example we will generate an offline certificate request. Offline request is the request that is saved to a file and is submitted to CA server by using out of band means. Once the certificate is issued, it is installed on the original computer.

it is common misconception about how certificate request and installation works. Many users think that CA issues certificate with private key (and private key is generated on the server). This is completely wrong assumption and the section below describes why.

When you create the request, a copy of the public information is saved to a file and private information (private key) remains on the computer and never leaves it. Therefore you can install the issued certificate only on the machine where the request is generated. This is because the system needs to associate the private key with a certificate. Once certificate is installed, the system tries to find an incomplete request that would match installed certificate. If the matching request is found, the private key is associated with the certificate and they are moved to Personal certificate store.

Getting started

Now we are ready to start with request generation. As explained above, we need to specify some mandatory information:

  • Certificate subject (certificate holder);
  • Key information;
  • Certificate extensions.

Certificate subject is constructed by using IX500DistinguishedName interface. Encode method is used to encode distinguished name to an ASN.1-encoded format as follows:

$SubjectDN = New-Object -ComObject X509Enrollment.CX500DistinguishedName
$SubjectDN.Encode("CN=www.contoso.com", 0x0)

That is enough. Though you may want to add additional subject names (multiple names are used in SSL certificates, for example). Since Subject field do not support multiple names, X.509 Version 3 certificate introduces special extension called Subject Alternative Name (SAN). Let's create few alternative names.

if you add alternative names in SAN extension you must repeat the Subject field value in SAN extension. This is because most applications do not process Subject field if SAN extension is presented.

SAN extension is implemented in IX509ExtensionAlternativeNames interface. As you see, the object is instantiated by using IAlternativeNames interface. So, create objects that instantiate mentioned interfaces:

$SAN = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames
$IANs = New-Object -ComObject X509Enrollment.CAlternativeNames

Now we need to create objects which implements IAlternativeName interface. We need to create an object per each alternative name and add them to IAlternativeNames object. In this example we will create 2 alternative names of DNS Name type. To simplify the process we will use Foreach-Object loop:

"www.contoso.com", "owa.contoso.com" | ForEach-Object {
    # instantiate a IAlternativeName object
    $IAN = New-Object -ComObject X509Enrollment.CAlternativeName
    # initialize the object by using current element in the pipeline
    $IAN.InitializeFromString(0x3,$_)
    # add created object to an object collection of IAlternativeNames
    $IANs.Add($IAN)
}
# finally, initialize SAN extension from a collection of alternative names:
$SAN.InitializeEncode($IANs)

Let's go to key information. To generate key pair we will use IX509PrivateKey interface. The IX509PrivateKey interface contains the following useful properties:

  • ProviderName — specifies the provider name. For example 'Microsoft RSA SChannel Cryptographic Provider';
  • MachineContext — specifies whether the certificate is intended for user or computer account.
  • Length — specifies the key length in bits;
  • KeyUsage — specifies the intended usage for the key.
  • KeySpec — specifies the main purpose for the key. If the key is intended only for data signing, the property must be set to AT_SIGNATURE (0x2), otherwise it should be set to AT_KEYEXCHANGE (0x1). You cannot combine both flags.
  • ExportPolicy — defines export policy. This property is necessary only when you need to make private key exportable or if you want to perform key archival. Normally this property should not be used.
$PrivateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey -Property @{
    ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
    MachineContext = $true
    Length = 2048
    KeySpec = 1
    KeyUsage = [int][Security.Cryptography.X509Certificates.X509KeyUsageFlags]::KeyEncipherment
}

I'm using advanced technique to create an object and fill it's properties. To create the key, we need to call Create() method with no parameters:

$PrivateKey.Create()

Now we will define at least two extensions, which publically describes key usage and certificate usage (how the certificate will be used). We already used KeyUsage parameter to generate a key pair, but this information is not included in the request, therefore we need to duplicate this information in a regular X509 extension form:

$KeyUsage = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage
$KeyUsage.InitializeEncode([int][Security.Cryptography.X509Certificates.X509KeyUsageFlags]"DigitalSignature,KeyEncipherment")
$KeyUsage.Critical = $true

KeyUsages extension always should be marked as critical.

and, finally, certificate usage, which is known as Enhanced Key Usage extension. The extension is implemented in IX509ExtensionEnhancedKeyUsage interface. As in IX509ExtensionAlternativeNames interface, the IX509ExtensionEnhancedKeyUsage object is instantiated by using the collection of IObjectId objects where each element represents a single usage. The collection is implemented in IObjectIds interface. And, again, we will use Foreach-Object cmdlet to loop the process:

# create appropriate interface objects
$EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage
$OIDs = New-Object -ComObject X509Enrollment.CObjectIDs
"Server Authentication", "Client Authentication" | ForEach-Object {
    # transform current element to an Oid object. This is necessary to retrieve OID value.
    # this step is not required when you pass OID values directly.
    $netOid = New-Object Security.Cryptography.Oid $_
    # instantiate a IObjectID object for current element.
    $OID = New-Object -ComObject X509Enrollment.CObjectID
    # initialize the object with current enhanced key usage
    $OID.InitializeFromValue($netOid.Value)
    # add the object to an object collection
    $OIDs.Add($OID)
}
# when all EKUs are processed, initialized the IX509ExtensionEnhancedKeyUsage with the IObjectIDs collection
$EKU.InitializeEncode($OIDs)

Ok, we created enough information about our request and we are ready to generate the target request. As already said, we need a PKCS#10 certificate request. This request type is implemented in IX509CertificateRequestPkcs10 interface:

$PKCS10 = New-Object -ComObject X509Enrollment.CX509CertificateRequestPkcs10

On the interface description page we see few initialization methods. Which one we need to use? It depends on how the request is created. Since we (at this point) do not have certificate template information (in our case, the CA may not implement them at all), therefore we will use InitializeFromPrivateKey method. The following syntax is used to initialize request object:

# 0x2 argument for Context parameter indicates that the request is intended for computer (or machine context).
# strTemplateName parameter is optional and we pass just empty string.
$PKCS10.InitializeFromPrivateKey(0x2,$PrivateKey,"")

the request is not signed yet, so we will add subject information and certificate extension information:

$PKCS10.Subject = $SubjectDN
$PKCS10.X509Extensions.Add($SAN)
$PKCS10.X509Extensions.Add($EKU)
$PKCS10.X509Extensions.Add($KeyUsage)

the process is quite self-explanatory. And what we have:

PS C:\> $PKCS10


Type                        : 1
EnrollmentContext           : 2
Silent                      : False
ParentWindow                :
UIContextMessage            :
SuppressDefaults            : False
ClientId                    : 5
CspInformations             : System.__ComObject
HashAlgorithm               :
AlternateSignatureAlgorithm : False
TemplateObjectId            :
PublicKey                   : System.__ComObject
PrivateKey                  : System.__ComObject
NullSigned                  : False
ReuseKey                    : False
Subject                     : System.__ComObject
CspStatuses                 : System.__ComObject
SmimeCapabilities           : False
SignatureInformation        : System.__ComObject
KeyContainerNamePrefix      : lp
CryptAttributes             : System.__ComObject
X509Extensions              : System.__ComObject
CriticalExtensions          : System.__ComObject
SuppressOids                : System.__ComObject
PolicyServer                :
Template                    :



PS C:\>

You can manually explore PKCS#10 object information. Once we added all required information, we are ready to create signed request through enrollment interface IX509Enrollment. This interface is a top level object that implements enrollment methods and MS-WCCE enrollment protocol.

why IX509CertificateRequestPkcs10 is not enough for us? This is because the IX509CertificateRequestPkcs10 interface prepares PKCS#10 object and fills all required properties. Moreover, this interface do not support certificate properties (for example, Friendly Name property). While it is possible to sign the PKCS#10 object without IX509Enrollment interface, it is recommended to go through IX509Enrollment interface where you can define additional properties for certificate store.

The following commands will instantiate and initialize IX509Enrollment object:

# instantiate IX509Enrollment object
$Request = New-Object -ComObject X509Enrollment.CX509Enrollment
# provide certificate friendly name:
$Request.CertificateFriendlyName = "My cool SSL cert"
# initialize the object from PKCS#10 object:
$Request.InitializeFromRequest($PKCS10)

The request is now ready to be signed. To sign the request we have to call CreateRequest method. The method has the only parameter Encoding which specifies the output encoding. Normally for all new requests we will use XCN_CRYPT_STRING_BASE64REQUESTHEADER = 0x3 argument. The method outputs the request contents which we will save to a file by using Set-Content cmdlet:

$Base64 = $Request.CreateRequest(0x3)
Set-Content $path -Value $Base64 -Encoding Ascii

the default behavior for Set-Content (as well as other cmdlets and redirection operators that writes to a file) is to save a file in a Unicode encoding. Unfortunately Windows CA do not support certificate request files in a Unicode encoding. Therefore you must always specify ASCII encoding when writing request contents to a file.

Final

Once the request is generated, a copy of request object is stored in Certificate Enrollment Requests container in certificate store. You can submit generated request and get the issued certificate. In the next post (I think it will be in next Tuesday) I'll talk about certificate installation and advanced less-known tricks ;)


Share this article:

Comments:

Pave

I was looking for how to create a certificate request and it is helps me, thank you!

Rob vab Halteren

Great post, but if using an enterprise PKI I think it needs to use a template instead of specifying keyusage Got an example for that as well ?

Rob van Halteren

Just found part 4....

nainw

You can manually explore PKCS#10 object information. Once we added all required information

Alex

Vadims-

Question about creating the PrivateKey object, why include the KeyUsage property in the HashTable when setting the KeySpec propery to 1 (XCN_AT_KEYEXCHANGE)?

I noticed that no matter what values I provided for KeyUsage, the value of said property in the instantiated object was always coming back as 5. I could change it after creation, but not as part of the creation. Then I read the documentation about KeySpec HERE and it provided the answer as to why this was happening:

"If you specify XCN_AT_KEYEXCHANGE, the KeyUsage property is set to XCN_NCRYPT_ALLOW_DECRYPT_FLAG | XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG."

Looking at the allowable values in the X509PrivateKeyUsageFlags enumeration listed HERE those two values are 0x1 and 0x4 respectively which sum to 0x5. Maybe this behavior was different back in 2012 when the article was originally written?

Alex


Post your comment:

Please, solve this little equation and enter result below. Captcha