How to create a certificate request on behalf of another user in C# on Windows 2003

If you're writing an application to create a certificate request, Microsoft provides the certificate enrollment controls.  On Windows XP and Windows 2003, XEnroll is the interface that's available for generating certificat requests.  Windows Vista made a clean break away from XEnroll and instead offers the more robust CertEnroll interface. 

If you need to generate a certificate requests on behalf of another user (such as a service that needs to provide certificate management), CertEnroll provides the properties and methods to do that.  An attribute called RequesterName has to be added to the request.  The value of this attribute is a domain\user value.  Also, an Enrollment Agent certificate must sign the request. 

Unfortunately, although Windows 2003 offers the ICEnroll4 interface, generating a request on behalf of another user isn't possible from .NET.  Although ICEnroll4 does provide a ICEnroll4::SignerCertificate property for setting up an enrollment agent certificate, it doesn't actually do anything. 

A C++ application can use the IEnroll4 interface and call IEnroll4::SetSignerCertificate, but that's not an automated class and can't be called from a .NET.

There is a way to get around this limitation with a .NET solution.  The key is to use ICEnroll4 and the SignedCms .NET class.  First ICEnroll4 can be used to generate the certificate request with the RequesterName attribute.  The second step is to use SignedCms to strip out the inner content (an inner CMC request with the RequesterName attriubte) and then resign the request with the enrollment agent certificate.  This in effect, creates a valid CMC certificate request on behalf of another user.

Below is example code that does this: 

class Program
{
    private const string ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0";
    private const int PROV_RSA_FULL = 1;
    private const int XECR_CMC = 3;
    private const string TemplateName = "user";
    private const int AT_KEYEXCHANGE = 0x1;
    private const int CRYPT_EXPORTABLE = 1;

    static void Main(string[] args)
    {
        X509Store Store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        ICEnroll4 certEnroll = new CEnroll2Class();

        Store.Open(OpenFlags.ReadOnly);
        X509Certificate2 EnrollCert = null;

        foreach (X509Certificate2 ECert in Store.Certificates)
        {
            foreach (X509Extension Ext in ECert.Extensions)
            {
                // Find Enhanced Key Usage Extension
                if (Ext.Oid.Value == "2.5.29.37")
                {
                    X509EnhancedKeyUsageExtension EnhExt = (X509EnhancedKeyUsageExtension)Ext;

                    for (int n = 0; n < EnhExt.EnhancedKeyUsages.Count; n++)
                    {
                        // We found an enrollment agent certificate
                        if (EnhExt.EnhancedKeyUsages[n].Value == "1.3.6.1.4.1.311.20.2.1")
                        {
                            EnrollCert = ECert;
                        }
                    }
                }
            }
        }

        certEnroll.ProviderName = ProviderName;
        certEnroll.addCertTypeToRequest(TemplateName);
        certEnroll.ProviderType = PROV_RSA_FULL;
        certEnroll.KeySpec = AT_KEYEXCHANGE;
        certEnroll.GenKeyFlags = CRYPT_EXPORTABLE;
        certEnroll.EnableSMIMECapabilities = 1;

        // Add RequesterName attribute
        certEnroll.addNameValuePairToSignature("RequesterName", "MyDomain\\TestUser");

        // Generate the request
        string certRequest = certEnroll.createRequest(XECR_CMC, "", "");

        // strip out header and footer
        certRequest = certRequest.Remove(0, "-----BEGIN NEW CERTIFICATE REQUEST-----\r\n".Length);
        certRequest = certRequest.Remove(certRequest.IndexOf("-----END NEW CERTIFICATE REQUEST-----"), "-----END NEW CERTIFICATE REQUEST-----".Length);

        if (EnrollCert != null)
        {
            // Get inner context of the request
            SignedCms decoder = new SignedCms();
            decoder.Decode(Convert.FromBase64String(certRequest));
            byte[] content = decoder.ContentInfo.Content;

            // Sign the inner content with an enrollment agent certificate
            Oid CMCDataOid = new Oid("1.3.6.1.5.5.7.12.2");
            ContentInfo ci = new ContentInfo(CMCDataOid, content);

            CmsSigner Signer = new CmsSigner();
            Signer.Certificate = EnrollCert;
            SignedCms SCms = new SignedCms(ci, false);

            SCms.ComputeSignature(Signer);
            string EncodedRequest = Convert.ToBase64String(SCms.Encode());
            Console.WriteLine(EncodedRequest);
        }
    }
}