Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
266 views
in Technique[技术] by (71.8m points)

c# - Issues compiling in Windows 10

I have identified an issue related to building apps that use C:WindowsSystem32CertEnroll.dll as a reference.

The following code works fine when compiled using VS 2015 on Windows 7 and then ran on a Windows 7 machine.

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);
            }
        }
    }
}

When you try and compile this in Windows 10 and then try and run it on a Windows 7 machine, it throws the following error.

"Unable to cast COM object of type 'System.__ComObject' to interface type 'CERTENROLLLib.CX509PrivateKey'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{728AB362-217D-11DA-B2A4-000E7BBB2B09}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE))."

I have had several people here replicate it and I'd like to get more input before contacting Microsoft on what's going on here.

I guess my question is: Can anybody else confirm this or if it's confirmed they broke backward compatibilty?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Somehow the Interface Implementation on CertEnroll.dll changed between "vanilla" Windows 2008 and Windows 2008 R2. I guess it is the same with some Windows 7 builds. To get it (halfway) working, you have to instantiate the classes with Activator.CreateInstance(Type.GetTypeFromProgID(<TypeName>); This will cause the system to look up the references in HKLM:SOFTWAREClassesInterface to get the right class for you.

Working Example:

(Part of this code was used from 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();
    }
}

Remarks

So why did I write "halfway"? There is a problem with certenroll.dll V. 6, which causes the build to fail on cert.InitializeFromPrivateKey. In certenroll.dll V 6.0, the second parameter must be of type "CX509PrivateKey", whereas on Win10 machines with Certenroll.dll V 10, it's IX509PrivateKey:

error CS1503: Argument 2: cannot convert from 'CERTENROLLLib.IX509PrivateKey' to 'CERTENROLLLib.CX509PrivateKey'

So you would think: Yea, simply "cast" the privateKey in the above example to CX509PrivateKey on Activator.CreateInstance. The Problem here is, it will compile, but on vanilla Win2k8 it will not give you the class (CX509...) but the Interface (IX509...), and thus the cast fails and returns 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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...