16

我发现了一个与构建使用 C:\Windows\System32\CertEnroll.dll 作为参考的应用程序相关的问题。

以下代码在 Windows 7 上使用 VS 2015 编译然后在 Windows 7 机器上运行时运行良好。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CERTENROLLLib;

namespace CertTest
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                CX509PrivateKey key = new CX509PrivateKey();
                key.ContainerName = Guid.NewGuid().ToString();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

当您尝试在 Windows 10 中编译它,然后尝试在 Windows 7 机器上运行它时,它会引发以下错误。

“无法将“System.__ComObject”类型的 COM 对象转换为接口类型“CERTENROLLLib.CX509PrivateKey”。此操作失败,因为对具有 IID 的接口的 COM 组件调用 QueryInterface '{728AB362-217D-11DA-B2A4-000E7BBB2B09} ' 由于以下错误而失败:不支持此类接口(HRESULT 异常:0x80004002 (E_NOINTERFACE))。

我有几个人在这里复制它,我想在联系 Microsoft 了解这里发生的事情之前获得更多输入。

我想我的问题是:其他人能否证实这一点,或者如果证实他们破坏了向后兼容性?

4

3 回答 3

16

不知何故,CertEnroll.dll 上的接口实现在“vanilla”Windows 2008 和 Windows 2008 R2 之间发生了变化。我想某些 Windows 7 版本也是如此。为了让它(中途)工作,你必须用Activator.CreateInstance(Type.GetTypeFromProgID(<TypeName>);实例化类。这将导致系统在 HKLM:\SOFTWARE\Classes\Interface\ 中查找引用以获取适合您的类。

工作示例:

(此代码的一部分来自https://stackoverflow.com/a/13806300/5243037

using System;
using System.Collections.Generic;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using CERTENROLLLib;


/// <summary>
///     Creates a self-signed certificate in the computer certificate store MY.
///     Issuer and Subject are computername and its domain.
/// </summary>
/// <param name="friendlyName">Friendly-name of the certificate</param>
/// <param name="password">Password which will be used by creation. I think it's obsolete...</param>
/// <returns>Created certificate</returns>
/// <remarks>Base from https://stackoverflow.com/a/13806300/5243037 </remarks>
public static X509Certificate2 CreateSelfSignedCertificate(string friendlyName, string password)
{
    // create DN for subject and issuer
    var dnHostName = new CX500DistinguishedName();
    // DN will be in format CN=machinename, DC=domain, DC=local for machinename.domain.local
    dnHostName.Encode(GetMachineDn());
    var dnSubjectName = dnHostName;

    //var privateKey = new CX509PrivateKey();
    var typeName = "X509Enrollment.CX509PrivateKey";
    var type = Type.GetTypeFromProgID(typeName);
    if (type == null)
    {
        throw new Exception(typeName + " is not available on your system: 0x80040154 (REGDB_E_CLASSNOTREG)");
    }
    var privateKey = Activator.CreateInstance(type) as IX509PrivateKey;
    if (privateKey == null)
    {
        throw new Exception("Your certlib does not know an implementation of " + typeName +
                            " (in HKLM:\\SOFTWARE\\Classes\\Interface\\)!");
    }
    privateKey.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider";
    privateKey.ProviderType = X509ProviderType.XCN_PROV_RSA_AES;
    // key-bitness
    privateKey.Length = 2048;
    privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE;
    privateKey.MachineContext = true;
    // Don't allow export of private key
    privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_NONE;

    // use is not limited
    privateKey.Create();

    // Use the stronger SHA512 hashing algorithm
    var hashobj = new CObjectId();
    hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
        ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
        AlgorithmFlags.AlgorithmFlagsNone, "SHA512");

    // add extended key usage if you want - look at MSDN for a list of possible OIDs
    var oid = new CObjectId();
    oid.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // SSL server
    var oidlist = new CObjectIds { oid };
    var eku = new CX509ExtensionEnhancedKeyUsage();
    eku.InitializeEncode(oidlist);

    // add all IPs of current machine as dns-names (SAN), so a user connecting to our wcf 
    // service by IP still claim-trusts this server certificate
    var objExtensionAlternativeNames = new CX509ExtensionAlternativeNames();
    {
        var altNames = new CAlternativeNames();
        var dnsHostname = new CAlternativeName();
        dnsHostname.InitializeFromString(AlternativeNameType.XCN_CERT_ALT_NAME_DNS_NAME, Environment.MachineName);
        altNames.Add(dnsHostname);
        foreach (var ipAddress in Dns.GetHostAddresses(Dns.GetHostName()))
        {
            if ((ipAddress.AddressFamily == AddressFamily.InterNetwork ||
                 ipAddress.AddressFamily == AddressFamily.InterNetworkV6) && !IPAddress.IsLoopback(ipAddress))
            {
                var dns = new CAlternativeName();
                dns.InitializeFromString(AlternativeNameType.XCN_CERT_ALT_NAME_DNS_NAME, ipAddress.ToString());
                altNames.Add(dns);
            }
        }
        objExtensionAlternativeNames.InitializeEncode(altNames);
    }

    // Create the self signing request
    //var cert = new CX509CertificateRequestCertificate();
    typeName = "X509Enrollment.CX509CertificateRequestCertificate";
    type = Type.GetTypeFromProgID(typeName);
    if (type == null)
    {
        throw new Exception(typeName + " is not available on your system: 0x80040154 (REGDB_E_CLASSNOTREG)");
    }
    var cert = Activator.CreateInstance(type) as IX509CertificateRequestCertificate;
    if (cert == null)
    {
        throw new Exception("Your certlib does not know an implementation of " + typeName +
                            " (in HKLM:\\SOFTWARE\\Classes\\Interface\\)!");
    }
    cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
    cert.Subject = dnSubjectName;
    cert.Issuer = dnHostName; // the issuer and the subject are the same
    cert.NotBefore = DateTime.Now.AddDays(-1);
    // this cert expires immediately. Change to whatever makes sense for you
    cert.NotAfter = DateTime.Now.AddYears(1);
    cert.X509Extensions.Add((CX509Extension)eku); // add the EKU
    cert.X509Extensions.Add((CX509Extension)objExtensionAlternativeNames);
    cert.HashAlgorithm = hashobj; // Specify the hashing algorithm
    cert.Encode(); // encode the certificate

    // Do the final enrollment process
    //var enroll = new CX509Enrollment();
    typeName = "X509Enrollment.CX509Enrollment";
    type = Type.GetTypeFromProgID(typeName);
    if (type == null)
    {
        throw new Exception(typeName + " is not available on your system: 0x80040154 (REGDB_E_CLASSNOTREG)");
    }
    var enroll = Activator.CreateInstance(type) as IX509Enrollment;
    if (enroll == null)
    {
        throw new Exception("Your certlib does not know an implementation of " + typeName +
                            " (in HKLM:\\SOFTWARE\\Classes\\Interface\\)!");
    }
    // Use private key to initialize the certrequest...
    enroll.InitializeFromRequest(cert);
    enroll.CertificateFriendlyName = friendlyName; // Optional: add a friendly name
    var csr = enroll.CreateRequest(); // Output the request in base64 and install it back as the response
    enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate, csr,
        EncodingType.XCN_CRYPT_STRING_BASE64, password);

    // This will fail on Win2k8, some strange "Parameter is empty" error... Thus we search the
    // certificate by serial number with the managed X509Store-class
    // // output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
    //var base64Encoded = enroll.CreatePFX(password, PFXExportOptions.PFXExportChainNoRoot, EncodingType.XCN_CRYPT_STRING_BASE64);
    //return new X509Certificate2(Convert.FromBase64String(base64Encoded), password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
    var certFs = LoadCertFromStore(cert.SerialNumber);
    if (!certFs.HasPrivateKey) throw new InvalidOperationException("Created certificate has no private key!");

    return certFs;
}


/// <summary>
///     Converts Domain.local into CN=Domain, CN=local
/// </summary>
private static string GetDomainDn()
{
    var fqdnDomain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
    if (string.IsNullOrWhiteSpace(fqdnDomain)) return null;
    var context = new DirectoryContext(DirectoryContextType.Domain, fqdnDomain);
    var d = Domain.GetDomain(context);
    var de = d.GetDirectoryEntry();
    return de.Properties["DistinguishedName"].Value.ToString();
}

/// <summary>
///     Gets machine and domain name in X.500-format: CN=PC,DN=MATESO,DN=local
/// </summary>
private static string GetMachineDn()
{
    var machine = "CN=" + Environment.MachineName;
    var dom = GetDomainDn();
    return machine + (string.IsNullOrWhiteSpace(dom) ? "" : ", " + dom);
}

/// <summary>
///     Load a certificate by serial number from computer.my-store
/// </summary>
/// <param name="serialNumber">Base64-encoded certificate serial number</param>
private static X509Certificate2 LoadCertFromStore(string serialNumber)
{
    var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
    store.Open(OpenFlags.OpenExistingOnly | OpenFlags.MaxAllowed);
    try
    {
        // serialnumber from certenroll.dll v6 is a base64 encoded byte array, which is reversed.
        // serialnumber from certenroll.dll v10 is a base64 encoded byte array, which is NOT reversed.
        var serialBytes = Convert.FromBase64String(serialNumber);
        var serial = BitConverter.ToString(serialBytes.ToArray()).Replace("-", "");
        var serialReversed = BitConverter.ToString(serialBytes.Reverse().ToArray()).Replace("-", "");

        var serverCerts = store.Certificates.Find(X509FindType.FindBySerialNumber, serial, false);
        if (serverCerts.Count == 0)
        {
            serverCerts = store.Certificates.Find(X509FindType.FindBySerialNumber, serialReversed, false);
        }
        if (serverCerts.Count == 0)
        {
            throw new KeyNotFoundException("No certificate with serial number <" + serial + "> or reversed serial <" + serialReversed + "> found!");
        }
        if (serverCerts.Count > 1)
        {
            throw new Exception("Found multiple certificates with serial <" + serial + "> or reversed serial <" + serialReversed + ">!");
        }

        return serverCerts[0];
    }
    finally
    {
        store.Close();
    }
}

评论

那我为什么要写“中途”呢?certenroll.dll V. 6 存在问题,导致构建在 cert.InitializeFromPrivateKey 上失败。在 certenroll.dll V 6.0 中,第二个参数必须是“CX509PrivateKey”类型,而在带有 Certenroll.dll V 10 的 Win10 机器上,它是 IX509PrivateKey:

错误 CS1503:参数 2:无法从“CERTENROLLLib.IX509PrivateKey”转换为“CERTENROLLLib.CX509PrivateKey”

所以你会想:是的,只需将上面示例中的 privateKey“转换”为 Activator.CreateInstance 上的 CX509PrivateKey。这里的问题是,它会编译,但在 vanilla Win2k8 上它不会给你类(CX509...)而是接口(IX509...),因此转换失败并返回 null。

We've solved this issue by compiling the certenrollment function in a seperate project on a machine with certenroll.dll V 10. It compiles fine and works in Win2k8, too. It's never-the-less a bit annoying to have it in a seperate project, since the build will fail on our buildserver with certenroll.dll V 6.

于 2016-02-12T14:30:38.193 回答
9

These are the steps from Microsoft to resolve this issue

If you use Windows 10 solely as your build environment then the executable would run on downlevel OSes, however if you really just want to have a project that you can compile anywhere and run anywhere then the only solution is to create your own interop DLL that you include in the project folder. You would have to generate it on Windows 7 first and reference that DLL.

Tlbimp.exe CertEnroll_Interop c:\Windows\System32\CertEnroll.dll

This generates a CertEnroll_Interop.dll file that you can copy to your project folder and then browse to in your project. Of course you would need the using “using CertEnroll_Interop;” statement.

You can build the project on Windows 10 and have it run on Windows 7 and Windows 8.1 and any other combination.

于 2016-09-08T17:38:42.257 回答
2

I had the same problem, my development machine is running on windows 10 and the build server windows 8.1.

But since c# has the ability of reflection and dynamic types, i now first analyze which types the InitializeFromPrivateKey method takes as parameters(I separated it from the actually certificate code by creating a method).

    private static bool IsCompiledOnWin10AndAbove()
    {
        var typeOfMethod = typeof(IX509CertificateRequestPkcs10);
        var methodType = typeOfMethod.GetMethod("InitializeFromPrivateKey", new Type[] { typeof(X509CertificateEnrollmentContext), typeof(CX509PrivateKey), typeof(string) });
        var methodeParameters = methodType.GetParameters();
        return methodeParameters[1].ParameterType != typeof(CX509PrivateKey);
    }

And then use a dynamic type dependent on which type the second parameter is.

        dynamic privateKeyCorrectType;
        if (IsCompiledOnWin10AndAbove()) // win 10 and above compiled
        {
            privateKeyCorrectType= privateKey;
        }
        else // below win 10 compiled
        {
            privateKeyCorrectType= (CX509PrivateKey)privateKey;
        }
        cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKeyCorrectType, "");
于 2018-01-12T16:03:37.660 回答