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.
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.
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.
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
- Understanding certificates (Windows Server 2003 Enterprise Edition Web site)
- PKCS #7 Concepts (Platform SDK)
- Certificate Enrollment Control (Platform SDK)
- The Cryptography API, or How to Keep a Secret (MSDN technical article)
- Requesting a Key Archival Certificate (Platform SDK)
- Key Archival and Management in Windows Server 2003 (TechNet article)
- Implementing and Administering Certificate Templates in Windows Server 2003 (TechNet article)