Bagikan melalui


Creating Certificate Requests Using the Certificate Enrollment Control and CryptoAPI

 

David J. Hoyle
Microsoft Corporation

April 2004

Applies to:
   C++ Language
   Cryptography API (CryptoAPI)
   Microsoft® Visual Basic® , Scripting Edition (VBScript)
   Microsoft® Windows® 2000
   Microsoft® Windows® XP
   Microsoft® Windows Server™ 2003 family

Summary: Use the Certificate Enrollment control (IEnroll and ICEnroll) in xenroll.dll and CryptoAPI (CAPI) to create various examples of certificate requests. These requests may be used to enroll with a Microsoft Certificate Server in Windows 2000 or Windows Server 2003, or even used to enroll with a third-party certificate authority service provider. (25 printed pages)

Download the associated CertificateEnrollmentSamples.exe.

Contents

Introduction
What Is the Certificate Enrollment Control?
Enrollment Process Using the Certificate Enrollment Control
Creating a PKCS #10 Request Using the Certificate Enrollment Control
PKCS #10 Request Format
Creating a CMC Request Using the Certificate Enrollment Control
Creating a CMC Request for Private Key Archival
Creating a Renewal Request
Creating an Enrollment Agent Signed CMC Request (Single Signer)
Creating a Enrollment Agent Signed CMC Request (Multiple Signers)
Adding Subject Alternative Name Extension to Requests
Adding DNS Name to Subject Alternative Name
Creating a Null Signed PKCS#10 Request Using CryptoAPI
Submitting a Request to a Microsoft Enterprise CA Using ICertRequest2
Conclusion
References

Introduction

In order for a certificate to be issued by a Certificate Authority (CA), a key pair is typically first generated at the client computer comprising a public and private key. The public half of the key pair is then sent to a CA together with other information about the subject (e.g. Subject name, etc) in a certificate request that allows the public key and the subject to be associated together. The CA then generates and signs the certificate containing the subject name and public key together with other information as required, often based on server policies configured by the administrator.

There are several different mechanisms that can generate certificate requests within the Windows environment. These mechanisms include auto-enrollment, the Certificates Microsoft Management Console (MMC), Certificate Server web interface, command-line utilities such as CertReq.exe and many others. In certain circumstances however the existing mechanisms for generating certificate requests may not meet the requirements of the environment and it is necessary to generate custom enrollment requests using either command-line scripts or code.

This article delves into the mechanism to create various certificate request types using the Certificate Enrollment Control and CryptoAPI that are native to the Windows operating system.

The sample code is written primarily in C++ with a single VBScript example provided as well for a comparative reference (CMC_VBS.vbs).

In order to build the samples, the preprocessor definition _WIN32_DCOM is required. Many of the samples require the inclusion of the Crypt32.lib and Certidl.lib libraries as inputs to the linker when using Visual Studio .NET 2003 as the development environment.

What Is the Certificate Enrollment Control?

The Certificate Enrollment Control is comprised of two different objects/interfaces depending upon the language chosen,

  • The ICEnroll interface (CEnroll object) represents the Certificate Enrollment Control. It is primarily used when programming in Visual Basic or another Automation language.
  • The IEnroll interface represents the Certificate Enrollment Control. It is primarily of interest if you are not using Automation for instance when programming in C++. The IEnroll interface provides a slightly richer interface for the creation of requests than the CEnroll interface.

The Certificate Enrollment Control allows the creation of different certificate requests in various certificate request message formats including PKCS #10, PKCS #7 and the CMC format (defined in RFC 2797). The certificate enrollment control provides support for performing private key archival together with signing requests using an Enrollment Agent certificate on behalf of another user.

Example code is provided for the creation of PKCS #10 and CMC requests in various formats. All the code needs to be compiled with the _WIN32_DCOM pre-processor flag. The code can be compiled either as Unicode or ASCII.

Enrollment Process Using the Certificate Enrollment Control

The following diagram shows a typical process flow from a client generating a request to submitting the request to a Windows Certificate Server, receiving a reply and then saving the reply into the user's CryptoAPI MY store.

ms867026.certenrollment_01(en-us,MSDN.10).gif

Figure 1. Enrollment process flow

Creating a PKCS #10 Request Using the Certificate Enrollment Control

The PKCS#10 request is the standard format most widely supported by systems for certificate requests. In order to create a request a CertEnroll object is first created using CoCreateInstance.

Calling the method/property put_ProviderNameWStr specifies the name of the required Cryptographic Support Provider (CSP), if the default CSP is not appropriate. The default CSP is "Microsoft Base Cryptographic Provider".

The type of key generated (either Signature or Exchange) is then set using the method put_KeySpec. The default key type is AT_SIGNATURE.

The AddCertTypeToRequestWStr method can be called to specify the template name for the certificate request. The template name is required in requests sent to a Microsoft Enterprise CA.

The put_GenKeyFlags method/property is used to specify the type of key generated by the request. The top 16 bits of the passed DWORD represent the key length (in the example 1024 bits). The bottom 16 bits are flags that control the way the key is generated (for Create Salt, Exportable, No Salt, DH or DSS Key Generation, Strong key protection, and Archivable flags, see help on CryptGenKey for more info).

Finally, the method createFileRequestWStr (or CreateFileRequest in VBScript) is called to create a request and store it into a file encoded in base 64. The first parameter is the type of request to generate (PKCS #10, CMC, or PKCS #7). The second parameter is the subject name for the certificate, and the third is a list of Object Identifiers (OIDs) that specify the purpose of the certificate (Extended Key Usage) ("1.3.6.1.5.5.7.3.2" is the Client Authentication OID—commonly used OIDs are defined in the include file wincrypt.h). When creating a request to a Microsoft Enterprise CA, the subject name and key usage normally do not need to be specified as these will typically be derived from the user object in Active Directory and populated automatically by the CA.

Creation of a PKCS #10 request can be achieved using either C++ or VBScript as shown below.

C++

IEnroll4*      CertEnroll = NULL;
CRYPT_DATA_BLOB      MyBlob = { 0, NULL };
hr=CoCreateInstance( 
   CLSID_CEnroll,
   NULL,
   CLSCTX_INPROC_SERVER,
   IID_IEnroll4,
   (void **)&CertEnroll );
hr=CertEnroll->put_ProviderNameWStr(
   L"Microsoft Enhanced Cryptographic Provider v1.0" );
hr=CertEnroll->put_KeySpec( AT_KEYEXCHANGE );
hr=CertEnroll->AddCertTypeToRequestWStr( L"ClientAuth" );
hr=CertEnroll->put_GenKeyFlags( 1024 << 16 );
hr=CertEnroll->createFileRequestWStr( 
   XECR_PKCS10_V2_0,
   L"",
   L"",
   L"request.req" );

VBScript

Const AT_KEYEXCHANGE = 1
Const XECR_PKCS10_V2_0 = 1
Dim CertEnroll
Set CertEnroll = CreateObject( "CEnroll.CEnroll" )
CertEnroll.ProviderName = _
   "Microsoft Enhanced Cryptographic Provider v1.0"
CertEnroll.KeySpec = AT_KEYEXCHANGE
CertEnroll.GenKeyFlags = 1024 * (256*256)
CertEnroll.addCertTypeToRequest "ClientAuth"
CertEnroll.CreateFileRequest _
   XECR_PKCS10_V2_0, _
   "", _
   "", _
   "request.req"

Instead of calling the method createFileRequestWStr to create a file base request, the method createRequestWStr (or CreateRequest in VBScript) can be called instead, which creates a request and returns it as a string.

The example code pkcs10.cpp shows a more complete example for creating an EFS request.

PKCS #10 Request Format

The format of the PKCS #10 Request is defined in RFC 2986 and is comprised of a version label, the Subject Name, Public Key, and optional attributes. The format is shown below.

ms867026.certenrollment_03(en-us,MSDN.10).gif

When we look at the generated PKCS#10 request file using the certutil utility, the Version (1), Subject ("John Smith"), Algorithm, Public Key, and Signature can all be seen. The Certificate Enrollment Control also adds several attributes to the request, including client information, certificate extensions, and CSP information, as shown below.

C:\>certutil -dump request.req
402.203.0: 0x80070057 (WIN32: 87): ..CertCli Version
PKCS10 Certificate Request:
Version: 1
Subject:
    CN=John Smith

Public Key Algorithm:
    Algorithm ObjectId: 1.2.840.113549.1.1.1 RSA
    Algorithm Parameters:
    05 00
Public Key Length: 1024 bits
Public Key: UnusedBits = 0
    0000  30 81 89 02 81 81 00 bc  d6 cc 13 34 21 1e c9 dd
    0010  48 84 92 5b bf 7b 4e 1b  87 f8 3a 8e 9e 23 6c ce
    0020  5f 01 c5 3b 4a 01 5f b2  bb 67 3a 67 5f d7 76 15
    0030  78 f4 d8 f1 ba 3a b3 ab  56 69 bd e3 0d 39 22 f7
    0040  a4 18 96 61 c2 ee 12 b4  63 ba ee 04 cf ad fe d4
    0050  08 5e 95 51 44 3d 76 38  5c 00 77 c6 0e 7d 7b dd
    0060  96 58 70 8f 82 51 95 9b  75 be 45 a0 ea d3 a8 0a
    0070  52 5c 97 8e a4 c4 48 1a  4f 0f bd f9 20 a2 70 de
    0080  2f a9 22 6e a7 58 a5 02  03 01 00 01
Request Attributes: 4
  4 attributes:

  Attribute[0]: 1.3.6.1.4.1.311.13.2.3 (OS Version)
    Value[0][0]:
        5.1.2600.2

  Attribute[1]: 1.3.6.1.4.1.311.21.20 (Client Information)
    Value[1][0]:
    Unknown Attribute type
    Client Id: = 1
    XECI_XENROLL -- 1
    User:
    Machine: dhoyle04.europe.corp.microsoft.com
    Process: cscript

  Attribute[2]: 1.2.840.113549.1.9.14 (Certificate Extensions)
    Value[2][0]:
    Unknown Attribute type
Certificate Extensions: 5
    2.5.29.15: Flags = 1(Critical), Length = 4
    Key Usage
        Digital Signature, Non-Repudiation, Key Encipherment, Data Encipherment
(f0)

    1.2.840.113549.1.9.15: Flags = 0, Length = 37
    SMIME Capabilities
        [1]SMIME Capability
             Object ID=1.2.840.113549.3.2
             Parameters=02 02 00 80
        [2]SMIME Capability
             Object ID=1.2.840.113549.3.4
             Parameters=02 02 00 80
        [3]SMIME Capability
             Object ID=1.3.14.3.2.7
        [4]SMIME Capability
             Object ID=1.2.840.113549.3.7

    2.5.29.14: Flags = 0, Length = 16
    Subject Key Identifier
        7c 4e b0 7b ca b7 c1 66 a8 b5 c2 15 83 84 f2 7d a1 eb 43 ac

    2.5.29.37: Flags = 0, Length = c
    Enhanced Key Usage
        Client Authentication (1.3.6.1.5.5.7.3.2)

    1.3.6.1.4.1.311.20.2: Flags = 0, Length = 16
    Certificate Template Name
        ClientAuth


  Attribute[3]: 1.3.6.1.4.1.311.13.2.2 (Enrollment CSP)
    Value[3][0]:
    Unknown Attribute type
    CSP Provider Info
    KeySpec = 1
    Provider = Microsoft Enhanced Cryptographic Provider v1.0
    Signature: UnusedBits=0
    0000  9f f8 46 13 93 4c a4 79  bb 10 82 53 70 12 b9 8f
    0010  48 05 8b 76 07 c8 8c d1  db 78 71 e3 44 c3 a3 2b
    0020  c5 43 01 6d 15 1b c2 d3  aa 29 3f f5 3c 43 8a fa
    0030  e1 2d 6a 71 da 26 ff 97  a7 58 59 73 d8 db 8d 53
    0040  e7 25 3a bf 21 16 d5 1b  1c bc f7 1e 83 de 3e 92
    0050  0a f0 70 d0 b5 9a 11 79  44 7f d6 aa 4d 70 4d cd
    0060  25 83 9f 3a 3c 59 30 03  d0 05 24 1b 19 74 5e 24
    0070  76 7e 76 8f cb 39 14 48  66 19 84 45 d8 08 b0 0d
    0080  00 00 00 00 00 00 00 00
Signature Algorithm:
    Algorithm ObjectId: 1.2.840.113549.1.1.5 sha1RSA
    Algorithm Parameters:
    05 00
Signature: UnusedBits=0
    0000  31 84 ff 5d e4 0f 32 69  27 ca e4 fb 6a 34 f9 9c
    0010  53 6e ac d0 80 98 19 ba  d6 55 8f 9f 7b dd 2c 0e
    0020  32 a6 cc 18 0e 34 2f a3  dc 11 49 e3 54 69 08 ad
    0030  fa 15 8e 52 7b 16 b4 ad  98 bc 4f 0d 00 7a 20 29
    0040  a8 ac e2 c6 48 d6 c7 e7  dd 77 9a 0b 37 f9 ef 77
    0050  09 b1 28 01 f6 a1 40 12  2e a8 98 9d 16 b9 99 ff
    0060  8b b3 59 0d ac 50 ca 8a  1f d5 8c 38 ac 92 a8 71
    0070  28 f0 34 07 dc fb d2 68  4e ee d7 fc 5a 34 9b 11
Signature matches Public Key
Key Id Hash(sha1): 7c 4e b0 7b ca b7 c1 66 a8 b5 c2 15 83 84 f2 7d a1 eb 43 ac
CertUtil: -dump command completed successfully.

Creating a CMC Request Using the Certificate Enrollment Control

A CMC request is generated in the same way as a PKCS #10 request but specifying instead the XECR_CMC format when creating the request. The request format flag passed to the createFileRequestWStr method should be changed to XECR_CMC as follows:

C++

hr=CertEnroll->createFileRequestWStr( 
   XECR_CMC,
   L"",
   L"",
   L"request.req" );

VBScript

Const XECR_CMC = 3
CertEnroll.CreateFileRequest _
   XECR_CMC, _
   "", _
   "", _
   "request.req"

The example code CMC.cpp shows a more complete example for creating an EFS request.

Creating a CMC Request for Private Key Archival

The use of the CMC request format allows for additional information to be included in the certificate request as unauthenticated attributes. One use for this is to allow the archival of the private key (sometimes called key escrow). The private key is encrypted using the CA exchange (encryption) key, is added to the request and this sent to the CA for archival. The enrollment process is now modified as shown below.

ms867026.certenrollment_02(en-us,MSDN.10).gif

Figure 2: Enrollment using key archival

In order to retrieve the CA encryption certificate, the client would normally be online and hence would be able to request the certificate directly from the CA itself. The client validates that the CA's exchange certificate has been signed by the same key as the CA signing certificate and performs a revocation status check on that certificate. This ensures that only the intended CA may decrypt the certificate request containing the private key. The template that is used for a particular request will also need to be configured to perform Key Archival – an existing template may need to be duplicated and edited to allow this (for more information refer to the following TechNet article, Key Archival and Management in Windows Server 2003).

The GetCACertificate method call retrieves the exchange certificate (in one of several formats) for the specified CA. The certificate content is then created from this certificate by calling the CryptoAPI function CertCreateCertificateContext. This certificate context is then passed to the CertEnroll object using method SetPrivateKeyArchiveCertificate.

The code to generate this request is as shown below:

C++

ICertRequest2*      CertRequest = NULL;
BSTR CACert = NULL;
BSTR CAName = NULL;
PCCERT_CONTEXT    CAKeyContext = NULL;
hr=CoCreateInstance(
   CLSID_CCertRequest,
   NULL,
   CLSCTX_INPROC_SERVER,
   IID_ICertRequest2,
   (void **)&CertRequest );
CAName = SysAllocString( L"COMPUTERNAME\\CA Name" );
hr=CertRequest->GetCACertificate( 
   TRUE, CAName, CR_OUT_BINARY, &CACert );
CAKeyContext = CertCreateCertificateContext( 
   X509_ASN_ENCODING,
(   BYTE*) CACert,
   SysStringByteLen( CACert ) );
...
hr=CertEnroll->SetPrivateKeyArchiveCertificate( CAKeyContext );
hr=CertEnroll->createFileRequestWStr(...);

VBScript

Dim CertRequest, CACert
Set CertRequest = CreateObject( "CertificateAuthority.Request" )
CACert = CertRequest.GetCACertificate( _
   1, _
   "COMPUTERNAME\CA Name", _
   CR_OUT_BASE64 )
CertEnroll.PrivateKeyArchiveCertificate = CACert

The example code CMCKeyArchival.cpp shows a more complete example for creating an EFS request.

Creating a Renewal Request

It is sometimes necessary to renew an existing certificate as the certificate may be near expiry. In order to renew a certificate, the renewal request is signed by a valid existing certificate. The renewal may reuse the key pair from an existing certificate or may create a new key pair.

The CertOpenStore function is first used to open the certificate store. The CertFindCertificateInStore function is then be used to search for an existing certificate that is to be renewed. Typically the store would be searched looking for certificates that are nearly expired.

After a certificate has been found for renewal, the certificate template information needs to be obtained to pass in the renewal request. The CertFindExtension function is called to get a pointer to the V1 or V2 template extension. The template OID or name is decoded using the CryptDecodeObjectEx function and is then passed when calling the AddCertTypeToRequestWStr method. The put_RenewalCertificate method call is the then used to set the renewal certificate for the request.

The code to generate this request is as shown below, in C++.

HCERTSTORE      hCertStore = NULL;
PCCERT_CONTEXT      pEnrollmentCert = NULL;
LPSTR            pszOID = "1.3.6.1.4.1.311.10.3.4";
CERT_ENHKEY_USAGE   stCertUsage = {1, &pszOID};
LPWSTR            TemplateName = NULL;
LPWSTR            AllocatedTemplateName = NULL;
CERT_TEMPLATE_EXT*   TemplateExt = NULL;
DWORD            TemplateExtSiz = 0;
CERT_NAME_VALUE*   TemplateExtName = NULL;
DWORD            TemplateExtNameSiz = 0;
hCertStore = CertOpenStore(
   CERT_STORE_PROV_SYSTEM,
   0,
   NULL,
   CERT_SYSTEM_STORE_CURRENT_USER,
   L"MY" );
pEnrollmentCert = CertFindCertificateInStore(
   hCertStore,
   X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
   0,
   CERT_FIND_ENHKEY_USAGE,
   &stCertUsage,
   NULL );
PCERT_EXTENSION ext = CertFindExtension(
   szOID_CERTIFICATE_TEMPLATE,
   pEnrollmentCert->pCertInfo->cExtension,
   pEnrollmentCert->pCertInfo->rgExtension );
if ( !ext )
{
ext = CertFindExtension(
   szOID_ENROLL_CERTTYPE_EXTENSION,
   pEnrollmentCert->pCertInfo->cExtension,
   pEnrollmentCert->pCertInfo->rgExtension );
CryptDecodeObjectEx( 
   (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING),
   X509_UNICODE_NAME_VALUE,
   ext->Value.pbData,
   ext->Value.cbData,
   CRYPT_DECODE_ALLOC_FLAG,
   NULL,
   &TemplateExtName,
   &TemplateExtNameSiz );
TemplateName = (LPWSTR) TemplateExtName->Value.pbData;
}
else
{
CryptDecodeObjectEx( 
   (X509_ASN_ENCODING | PKCS_7_ASN_ENCODING),
   X509_CERTIFICATE_TEMPLATE,
   ext->Value.pbData,
   ext->Value.cbData,
   CRYPT_DECODE_ALLOC_FLAG,
   NULL,
   &TemplateExt,
   &TemplateExtSiz );
int   tmplen = MultiByteToWideChar( 
   CP_THREAD_ACP, 0, TemplateExt->pszObjId, -1, NULL, 0 );
AllocatedTemplateName = (LPWSTR) LocalAlloc( LPTR, 
   sizeof(WCHAR) * tmplen );
MultiByteToWideChar( CP_THREAD_ACP, 0, 
   TemplateExt->pszObjId, -1, AllocatedTemplateName, tmplen );
TemplateName = AllocatedTemplateName;
}
hr=CertEnroll->AddCertTypeToRequestWStr( TemplateName );
hr=CertEnroll->put_RenewalCertificate( pEnrollmentCert );

If the renewal is to re-use an existing key pair then it will be necessary to call the method put_UseExistingKeySet to make sure the existing key set is re-used. The provider name and key set name will also need to be set. These can be obtained from the renewal certificate context by calling CertGetCertificateContextProperty and then calling methods put_RenewalCertificate, put_ProviderNameWStr and put_KeySpec.

The code for doing this is as shown below in C++.

CRYPT_KEY_PROV_INFO*   ProvInfo = NULL;
DWORD            ProvInfoSiz = 0;
CertGetCertificateContextProperty( 
   pEnrollmentCert,
   CERT_KEY_PROV_INFO_PROP_ID,
   NULL,
   &ProvInfoSiz );
ProvInfo = (CRYPT_KEY_PROV_INFO*) LocalAlloc( LPTR, ProvInfoSiz );
CertGetCertificateContextProperty( 
   pEnrollmentCert,
   CERT_KEY_PROV_INFO_PROP_ID,
   ProvInfo,
   &ProvInfoSiz );
hr=CertEnroll->put_UseExistingKeySet( TRUE );
hr=CertEnroll->put_ProviderNameWStr(ProvInfo->pwszProvName);
hr=CertEnroll->put_ContainerNameWStr(ProvInfo->pwszContainerName);
hr=CertEnroll->put_KeySpec( ProvInfo->dwKeySpec );

The example code CertificateRenewal.cpp shows a more complete example for renewing an EFS certificate.

Creating an Enrollment Agent Signed CMC Request (Single Signer)

The Certificate Enrollment Control allows a request to be signed using a single certificate. The certificate used to sign the request will normally be an Enrollment Agent certificate that allows a user to enroll for a certificate on behalf of another user. In order to enroll on behalf of another user, the request must also contain an extra attribute, called RequesterName together with the user SAM name (acmecorp\JSmith). Typically, enroll on behalf scenarios are used for corporate security officers to enroll smartcards through an in-person proofing process where the certificate requestor is not the user themselves.

The following C++ code opens a certificate store using the CertOpenStore function, finds an enrollment certificate in the store using the CertFindCertificateInStore function. The certificate is then checked for validity using functions CertGetCertificateChain and CertVerifyCertificateChainPolicy. If the certificate is valid it is then passed when calling method SetSignerCertificate to set the certificate used to sign the request. The AddNameValuePairToSignatureWStr method is used to set the RequesterName attribute to the user SAM name (acmecorp\JSmith). (No example for VBScript is shown, as it is not possible to sign a request using the ICEnroll interface)

PCCERT_CONTEXT      pEnrollmentCert = NULL;
LPSTR            pszOIDs[1]={szOID_ENROLLMENT_AGENT};
CERT_ENHKEY_USAGE   stCertUsage = {1,pszOIDs};
CERT_CHAIN_PARA      ChainPara;
PCCERT_CHAIN_CONTEXT   ChainContext = NULL;
HCERTSTORE hCertStore = CertOpenStore(
   CERT_STORE_PROV_SYSTEM,
   0,
   NULL,
   CERT_SYSTEM_STORE_CURRENT_USER,
   L"MY" );
if (( pEnrollmentCert = CertFindCertificateInStore(
   hCertStore,
   X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
   0,
   CERT_FIND_ENHKEY_USAGE,
   &stCertUsage,
   NULL)) == NULL )
{
   goto error;
}
ZeroMemory( &ChainPara, sizeof(ChainPara));
ChainPara.cbSize = sizeof(ChainPara);
ChainPara.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND;
ChainPara.RequestedUsage.Usage = stCertUsage;
CertGetCertificateChain(
   NULL,
   hChainEngine,
   pEnrollmentCert,
   NULL,
   NULL,
   hAdditionalStore,
   &ChainPara,
   pChainPara,
   CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT,
   dwFlags,
   NULL,
   &ChainContext );
if ( ChainContext->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR )
{
   goto error;
}
ZeroMemory(&ChainPolicy, sizeof(ChainPolicy));
ChainPolicy.cbSize = sizeof(ChainPolicy);
ZeroMemory(&PolicyStatus, sizeof(PolicyStatus));
PolicyStatus.cbSize = sizeof(PolicyStatus);
ChainPolicy.dwFlags = CERT_CHAIN_POLICY_IGNORE_NOT_TIME_NESTED_FLAG;
PolicyStatus.lChainIndex = -1;
PolicyStatus.lElementIndex = -1;
if ( !CertVerifyCertificateChainPolicy(
   CERT_CHAIN_POLICY_BASE,
   ChainContext,
   &ChainPolicy,
   &PolicyStatus))
{
   goto error;
}
hr=CertEnroll->SetSignerCertificate( pEnrollmentCert );
hr = CertEnroll->AddNameValuePairToSignatureWStr(
   (LPWSTR) L"RequesterName",
   L"acmecorp\\JSmith" );

The example code CMCOnBehalf.cpp shows a more complete example for creating an EFS request on behalf of another user.

Creating a Enrollment Agent Signed CMC Request (Multiple Signers)

The previous example shows how to sign a request using a single certificate. If on the other hand, a request needs to be signed by multiple certificates or an existing request is required to be resigned then another approach must be taken. In this case, CryptoAPI must be used instead of the certificate enrollment control.

This example demonstrates the use of the CryptoAPI routines CryptMsgOpenToDecode, CryptMsgUpdate, CryptMsgControl, and CryptMsgGetParam to sign an existing CMC request.

The request file is first read into memory from a file (using function ReadRequestfile not shown here). The request is assumed to be in binary form, rather than Base 64 (Base 64 files can be converted to binary using the certutil –decode command or programmatically using CryptStringToBinary if required).

The CryptMsgOpenToDecode function opens a handle to a cryptographic message. The original request is decoded by calling CryptMsgUpdate. A signature and certificate are then added to the message using the function CryptMsgControl, and then finally the request is re-encoded using the CryptMsgGetParam function. The CryptoAPI functions used to open the certificate store, acquire the signing certificate, validate the certificate and then acquire the private key are omitted from below (but are included in the full example),

CRYPT_DATA_BLOB RequestData;
HCRYPTMSG hMsg = NULL;
CMSG_SIGNER_ENCODE_INFO SignerInfo;
CRYPT_DATA_BLOB CertBlob;
CRYPT_DATA_BLOB UpdatedRequestBlob;

RequestData = ReadRequestFile( "request.req" );
hMsg = CryptMsgOpenToDecode(
   X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
   0, 0, 0, NULL, NULL);
fResult = CryptMsgUpdate(
   hMsg, RequestData.pbData, RequestData.cbData, TRUE);
...
fResult = CryptMsgControl(hMsg, 0, CMSG_CTRL_ADD_SIGNER, &SignerInfo);
CertBlob.cbData = pCertContext->cbCertEncoded;
CertBlob.pbData = pCertContext->pbCertEncoded;
fResult = CryptMsgControl(hMsg, 0, CMSG_CTRL_ADD_CERT, &CertBlob);
fResult = CryptMsgGetParam(
   hMsg, CMSG_ENCODED_MESSAGE,
   0, NULL, &UpdatedRequestBlob.cbData );
UpdatedRequestBlob.pbData = (BYTE*) LocalAlloc(
   LPTR, UpdatedRequestBlob.cbData);
fResult = CryptMsgGetParam( hMsg, CMSG_ENCODED_MESSAGE, 0,
   UpdatedRequestBlob.pbData,
   &UpdatedRequestBlob.cbData);
WriteRequestFile( "requestout.req", &UpdatedRequestBlob );

The example code SignCMCRequest.cpp shows a complete example command-line application that can be used to re-sign existing binary CMC requests.

Adding Subject Alternative Name Extension to Requests

In certain situations in order to support non-Microsoft CAs it may be necessary to add extra extensions to a request. This example shows how to add the Subject Alternative Name extension to a PKCS #10 request. The Subject Alternative Name extension is used to store extra identifiers for a subject including the User Principal Name (UPN) of the user that is used by Windows for smart card logon and the user e-mail address (RFC 822 name). Microsoft Certificate Services automatically populates this field for issued certificates but other CAs may not do this and hence may need this value passed to the CA in the enrollment request.

The following C++ code will create a Subject Alternative Extension for a request.

The structure CERT_ALT_NAME_INFO is used to store the two names that are to be added to the enrollment request (the e-mail/rfc822 and the UPN).

The e-mail name can be simply added to the array of Alt Names, as shown below.

CERT_ALT_NAME_ENTRY   AltNames[2];
CERT_ALT_NAME_INFO   AltNameInfo = { 2, AltNames };
CRYPT_DATA_BLOB      ExtBlob;
AltNames[0].dwAltNameChoice = CERT_ALT_NAME_RFC822_NAME;
AltNames[0].pwszRfc822Name = (LPWSTR) L"jsmith@acmecorp.net";

The User Principal Name, though, requires more effort to be added to the request. The Name is first converted into an ASN.1 binary blob by calling CryptEncodeObjectEx, as shown below.

CERT_NAME_VALUE    UPNName;
CERT_OTHER_NAME    ASNupnName;

UPNName.dwValueType = CERT_RDN_UTF8_STRING; 
UPNName.Value.pbData = (BYTE *) L"jsmith@acmecorp.net"; 
UPNName.Value.cbData = 0;

rOK = CryptEncodeObjectEx( X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, X509_UNICODE_NAME_VALUE,
   &UPNName, 
   CRYPT_ENCODE_ALLOC_FLAG,
   NULL,
   &ASNupnName.Value.pbData,
   &ASNupnName.Value.cbData );
AltNames[1].dwAltNameChoice = CERT_ALT_NAME_OTHER_NAME;
AltNames[1].pOtherName = &ASNupnName;
ASNupnName.pszObjId = szOID_NT_PRINCIPAL_NAME;

Finally, an ASN.1 binary blob needs to be created of the Subject Alternative Name, again by calling CryptEncodeObjectEx, but with the structure type defined as X509_ALTERNATE_NAME, as shown below. This is then added to the request by calling the method addExtensionToRequestWStr. Other extensions can be added in a similar manner to requests.

rOK = CryptEncodeObjectEx( 
   X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
   X509_ALTERNATE_NAME,
   &AltNameInfo,
   CRYPT_ENCODE_ALLOC_FLAG,
   NULL,
   &ExtBlob.pbData,
   &ExtBlob.cbData );
hr = CertEnroll->addExtensionToRequestWStr(TRUE, 
   CComBSTR(szOID_SUBJECT_ALT_NAME2), &ExtBlob);

If the request is dumped, the following extension can be seen in the request,

2.5.29.17: Flags = 1(Critical), Length = 3c
    Subject Alternative Name
        RFC822 Name=jsmith@acmecorp.net
        Other Name:
             Principal Name=jsmith@acmecorp.net

The example code PKCS10AltSubName.cpp shows a complete example of the above code for adding a Subject Alternative Name extension.

Adding DNS Name to Subject Alternative Name

This example shows how to add a DNS Name into the Subject Alternative Name extension of a PKCS #10 request. Domain Controller Certificates require the DNS name is to be included in the Subject Alternative Name.

CERT_ALT_NAME_ENTRY   AltNames[1];
CERT_ALT_NAME_INFO   AltNameInfo = { 1, AltNames };
CRYPT_DATA_BLOB      ExtBlob;
AltNames[0].dwAltNameChoice = CERT_ALT_NAME_DNS_NAME;
AltNames[0].pwszDNSName = (LPWSTR) L"rootdc.acmecorp.net";

Next an ASN blob needs to be created for the Subject Alternative Name, by calling CryptEncodeObjectEx, but with the structure type defined as X509_ALTERNATE_NAME, as shown below. This is then added to the request by calling the method addExtensionToRequestWStr. Other extensions can be added in a similar manner to requests.

rOK = CryptEncodeObjectEx( 
   X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
   X509_ALTERNATE_NAME,
   &AltNameInfo,
   CRYPT_ENCODE_ALLOC_FLAG,
   NULL,
   &ExtBlob.pbData,
   &ExtBlob.cbData );
hr = CertEnroll->addExtensionToRequestWStr(TRUE, 
   CComBSTR(szOID_SUBJECT_ALT_NAME2), &ExtBlob);

If the request is dumped, the following extension can be seen in the request,

2.5.29.17: Flags = 1(Critical), Length = 17
Subject Alternative Name
    DNS Name=rootdc.acmecorp.net

The example code PKCS10DC.cpp shows a complete example for creating a PKCS #10 request for a Windows Server 2003 Domain Controller.

Creating a Null Signed PKCS#10 Request Using CryptoAPI

In certain cases you may wish to create a PKCS #10 enrollment request using CryptoAPI directly rather than using the Certificate Enrollment Control. This might be required when you already have an existing RSA key pair for which you need to generate a certificate request. In the following example the PKCS #10 request is signed with a null signature (SHA-1 hash) rather than using an RSA signature. The Microsoft CA will reject requests that are null signed but some third party CAs require that requests be null signed.

The following example demonstrates how this can be done.

First convert the user's NULL-terminated X.500 subject name string to an encoded certificate name using the CertStrToName function and then add the encoded name into the certificate request structure.

DWORD            cbNameEncoded;
BYTE*            pbNameEncoded = NULL; 
CERT_REQUEST_INFO   CertReqInfo;
CertStrToName(    
   MY_ENCODING_TYPE,
   _T("CN=John Smith,CN=users,DC=acmecorp,DC=net"),
   CERT_OID_NAME_STR | CERT_NAME_STR_REVERSE_FLAG,
   NULL,
   NULL,
   &cbNameEncoded,
   NULL );
pbNameEncoded = (BYTE*) LocalAlloc( LPTR, cbNameEncoded );
CertStrToName( 
   MY_ENCODING_TYPE,
   _T("CN=John Smith,CN=users,DC=acmecorp,DC=net"),
   CERT_OID_NAME_STR | CERT_NAME_STR_REVERSE_FLAG,
   NULL,
   pbNameEncoded,
   &cbNameEncoded,
   NULL );
CertReqInfo.Subject.cbData = cbNameEncoded;
CertReqInfo.Subject.pbData = pbNameEncoded;
CertReqInfo.cAttribute = 0;
CertReqInfo.rgAttribute = NULL;
CertReqInfo.dwVersion = CERT_REQUEST_V1;

Next, open a handle to an existing key container in a CSP by calling CryptAcquireContext. In this case the key container is called "TestContainer". The public key is then exported by calling CryptExportPublicKeyInfo and added to the certificate request structure.

CryptAcquireContext( &hCryptProv, _T("TestContainer"), 
   _T("Microsoft Enhanced Cryptographic Provider v1.0"),
   PROV_RSA_FULL, 0 );
CryptExportPublicKeyInfo( hCryptProv, KeySpec, 
   MY_ENCODING_TYPE, NULL, &cbPublicKeyInfo );
pbPublicKeyInfo = (CERT_PUBLIC_KEY_INFO*) LocalAlloc( 
   LPTR, cbPublicKeyInfo );
CryptExportPublicKeyInfo( hCryptProv, KeySpec, 
   MY_ENCODING_TYPE, pbPublicKeyInfo, &cbPublicKeyInfo );
CertReqInfo.SubjectPublicKeyInfo = *pbPublicKeyInfo;

Finally, by calling CryptSignAndEncodeCertificate, the request is created. The example below creates a null signature for the request (szOID_OIWSEC_sha1). If an RSA signature is required, then szOID_RSA_SHA1RSA OID should be used instead,

ZeroMemory(&SigAlg, sizeof(SigAlg));
SigAlg.pszObjId = szOID_OIWSEC_sha1;
CryptSignAndEncodeCertificate( 
   hCryptProv, AT_KEYEXCHANGE, MY_ENCODING_TYPE,
   X509_CERT_REQUEST_TO_BE_SIGNED, &CertReqInfo, 
   &SigAlg, NULL,
   NULL,
   NULL,
   &MyCertBlob.cbData );
MyCertBlob.pbData = (BYTE*) LocalAlloc( LPTR, MyCertBlob.cbData );
CryptSignAndEncodeCertificate( 
   hCryptProv, AT_KEYEXCHANGE, MY_ENCODING_TYPE,
   X509_CERT_REQUEST_TO_BE_SIGNED, &CertReqInfo, 
   &SigAlg, NULL,
   NULL,
   MyCertBlob.pbData,
   &MyCertBlob.cbData );
WriteRequestFile( _T("request.req"), &MyCertBlob );

The example code PKCS10NULLSig.cpp shows a complete example of the above code but also allows requests to choose either the Signing or Exchange key, to add a template to the request and also to sign the request with the private key. After generating a null-signed PKCS #10 request, it is possible to generate a signed CMC request using the certreq.exe –policy command and submit this to a Microsoft Enterprise CA.

Submitting a Request to a Microsoft Enterprise CA Using ICertRequest2

After generating a request using one of the approaches above, it can then be sent directly to a Microsoft Enterprise CA that can then process the request and can (depending upon configuration) immediately issue a certificate. The ICertRequest2 Interface is used to send and receive requests and this section demonstrates sending requests and receiving replies.

The certificate request string (created by methods createRequestWStr or createRequest) is submitted to a CA by calling the ICertRequest2::Submit method. If the CA issues the certificate immediately the method will return a disposition status of CR_DISP_ISSUED. Other return values indicate either failure conditions or that the certificate request is waiting for approval (CR_DISP_UNDER_SUBMISSION).

The method ICertRequest2::GetFullResponseProperty is then called to read the full response from the CA (calling the GetCertificate method will get the issued certificate but not other required information). The returned data blob is then passed to the IEnroll4::acceptResponseBlob method. This method checks that the issued certificate contains an Encrypted Key Hash (if the request used key archival), checks that there is a private key for the requested certificate and if so it writes the issued certificate to the store.

C++

ICertRequest2* CertRequest = NULL;
hr=CoCreateInstance(
   CLSID_CCertRequest,
   NULL,
   CLSCTX_INPROC_SERVER,
   IID_ICertRequest2,
   (void **)&CertRequest );
BSTR CAName = SysAllocString( L"COMPUTERNAME\\CA Name" );
BSTR RequestStr = SysAllocString( CertificateRequestString );
BSTR AttributesStr = SysAllocString( L"" );
CRYPT_DATA_BLOB MyCertBlob;
LONG Disp;
VARIANT varFullResp;
VariantInit(&varFullResp);
hr = CertRequest->Submit( CR_IN_ENCODEANY | CR_IN_FORMATANY, RequestStr, AttributesStr, CAName, &Disp );
if ( Disp == CR_DISP_ISSUED )
{
    hr = CertRequest->GetFullResponseProperty( 
   FR_PROP_FULLRESPONSE, 
   0,
   PROPTYPE_BINARY,
   CR_OUT_BINARY,
   &varFullResp );
    MyCertBlob.cbData = SysStringByteLen( varFullResp.bstrVal );
    MyCertBlob.pbData = (BYTE*) varFullResp.bstrVal;
    hr = CertEnroll->acceptResponseBlob( &MyCertBlob );
}

VBScript

Dim RequestStr, CertRequest, Disposition, ID
RequestStr = CertEnroll.createRequest( XECR_CMC, "CN=John Smith",  "1.3.6.1.5.5.7.3.2" )
Set CertRequest = CreateObject( "CertificateAuthority.Request" )
Disposition = CertRequest.Submit( _
   CR_IN_ENCODEANY Or CR_IN_FORMATANY, _
   RequestStr, _
   "", _
   "COMPUTERNAME\CA Name" )
ID = CertRequest.GetRequestId
WScript.echo "Request Id = " + cstr(ID)
If Disposition = CR_DISP_ISSUED Then
    Dim Cert
    Cert = CertRequest.GetFullResponseProperty( _
   FR_PROP_FULLRESPONSE, _
   0, _
   PROPTYPE_BINARY, _
   CR_OUT_BASE64 )
    CertEnroll.acceptResponse Cert
Else
    WScript.echo "Disposition = " + cstr( Disposition )
    If CertRequest.GetLastStatus <> 0 Then
    WScript.echo "Error : " + cstr( CertRequest.GetErrorMessageText(  _
   CertRequest.GetLastStatus, CR_GEMT_HRESULT_STRING ))
    End If
End If

The example code PKCS10Request.cpp shows a complete example of the above code.

Conclusion

Hopefully you will find in the code samples presented here some useful information when the built-in enrollment functionality is not sufficient to meet requirements. The code may also help you in exploring the CryptoAPI, which provides a very powerful but generally little understood API for cryptography.

About the Author

David Hoyle is a Consultant working for Microsoft Consulting Services in the UK. David works in the Security team within Business Critical Services and specializes primarily in PKI. David has been in the IT industry for over for 14 years working in both software development and infrastructure roles across many different sectors.

References