I have identified an issue related to building apps that use C:\Windows\System32\CertEnroll.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?
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:\SOFTWARE\Classes\Interface\ 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=YourFirm,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.
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.
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, "");
Related
I'm writing a .NET 6 application for Windows that is intended to extract the private key from a PFX file containing an RSA cert/key bundle.
public static Boolean ToCertAndKey(String pfxFilePath, String? unlockPassword, String certFilePath, String keyFilePath, String? keyPassword, out String error) {
try {
error = String.Empty;
using var bundle = new X509Certificate2(pfxFilePath, unlockPassword);
RSA key = bundle.GetRSAPrivateKey();
Byte[] publicKeyBytes = key.ExportSubjectPublicKeyInfo();
Byte[] privateKeyBytes;
//We fail here.
if (String.IsNullOrEmpty(keyPassword)) {
privateKeyBytes = key.ExportPkcs8PrivateKey();
} else {
privateKeyBytes = key.ExportEncryptedPkcs8PrivateKey(keyPassword,
new PbeParameters(
PbeEncryptionAlgorithm.Aes256Cbc,
HashAlgorithmName.SHA256,
iterationCount: 1));
}
String encodedCert = new(PemEncoding.Write("PUBLIC KEY", publicKeyBytes));
File.WriteAllText(certFilePath, encodedCert);
String encodedKey = new(PemEncoding.Write("PRIVATE KEY", privateKeyBytes));
File.WriteAllText(keyFilePath, encodedKey);
return true;
} catch (Exception ex) {
error = $"An exception occurred: '{ex.Message}'\r\n\r\nStack Trace:\r\n{ex.StackTrace}";
return false;
}
}
It fails at both ExportPkcs8PrivateKey (When I don't specify a password to encrypt the key) and ExportEncryptedPkcs8PrivateKey (when I do) with the same exception text:
WindowsCryptographicException: The requested operation is not supported
I came across this answer however, I'm still receiving the same exception at RSA.ExportEncryptedPkcs8PrivateKey.
There doesn't appear to be anything wrong with the PFX files I've been testing with; I'm able to import them into my certstore via the UI or PowerShell with no issues.
Hoping someone else has run into this issue.
You need to mark the keys as exportable.
Change
using var bundle = new X509Certificate2(pfxFilePath, unlockPassword);
to
using var bundle = new X509Certificate2(pfxFilePath, unlockPassword, X509KeyStorageFlags.Exportable);
I have an InstallShield installation that deploys a website to IIS on a Windows machine. It has always deployed the website with an HTTP binding. I am adding support for deploying the website using HTTPS and I am having some difficulty getting it to work in all cases. I am allowing the user to either select an already installed certificate from the "Personal" or "Web Hosting" stores for the local machine, or select a PFX file from the file system and provide the password. Either way, I obtain the thumbprint of the certificate. As long as the user chooses an SSL certificate that is already in the store, it works as expected. However, if the user chooses to select a PFX file & enter the password, I am able to successfully import the certificate into the "Web Hosting" store, but when I try to add the https binding and associate the certificate to it, I receive the following exception when committing the changes:
"A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)"
The https binding is added successfully, and even if I try to associate the certificate using IIS I get the same message, which tells me it may be related to the Import process more than the Binding process.
I have implemented a handful of methods in a managed library in C# and include the library in the installer project, and created CustomActions in the InstallShield project to invoke these methods. All CustomActions run in Deferred mode in System context, so it has admin rights. I have tons of logging output for troubleshooting purposes, and can see that everything happens without issues, right up to the point that the CommitChanges() method is called when adding the https binding to the website.
Here is my code. I have tried about 10 variations of this as I have found different potential solutions online, but none has been 100% successful. Current state is what I have been most successful with so far (the FindCertificate() method returns a X509Certificate2 object and the name of the store where it was found, and works properly):
/// <summary>
/// Imports a SSL certificate from a file with a password to the specified store on the Local Machine
/// </summary>
/// <param name="customActionData">Semi-colon delimited list containing the certificate path/filename, password, and store name</param>
/// <returns>
/// 1=Success; 0=Invalid arguments; -1=Cert file not found; -100=Exception occurred
/// </returns>
public static Int32 ImportCertificateFromFile(string customActionData)
{
var args = customActionData.Split(new char[] { ';' });
if (args.Length != 3)
return 0;
var certFile = args[0].Replace("\\", "\\\\");
var password = args[1];
var storeName = args[2];
if (string.IsNullOrEmpty(certFile) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(storeName))
return 0;
if (System.IO.File.Exists(certFile))
{
try
{
using (var store = new X509Store(storeName, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadWrite);
var certificate = new X509Certificate2(certFile, password);
store.Add(certificate);
return 1;
}
}
catch (Exception ex)
{
return -100;
}
}
return -1;
}
/// <summary>
/// Adds a binding to a website
/// </summary>
/// <param name="customActionData">Semi-colon delimited list containing the website name, IP address, port, host header, protocol, and certificate thumbprint for the binding to add</param>
/// <returns>
/// 1=Success; 0=Invalid arguments; -1=Website not found; -100=Exception occurred
/// </returns>
public static Int32 AddWebsiteBinding(string customActionData)
{
var args = customActionData.Split(new char[] { ';' });
if (args.Length != 6)
return 0;
var siteName = args[0];
var ipAddress = args[1];
var port = args[2];
var hostHeader = args[3];
var protocol = args[4].ToLower();
var thumbprint = args[5];
var ret = 1;
var serverManager = new ServerManager();
var webSite = serverManager.Sites.FirstOrDefault(s => s.Name == siteName);
if (!(webSite is null))
{
try
{
var binding = webSite.Bindings.CreateElement("binding");
binding.Protocol = protocol;
binding.BindingInformation = $"{ipAddress}:{port}:{hostHeader}";
// add SSL certificate if thumbprint was provided
if (!string.IsNullOrEmpty(thumbprint))
{
var (cert, storeName) = FindCertificate(thumbprint);
if (!(cert is null))
{
binding.CertificateHash = cert.GetCertHash();
binding.CertificateStoreName = storeName;
}
}
webSite.Bindings.Add(binding);
serverManager.CommitChanges();
}
catch (Exception ex)
{
ret = -100;
}
}
else
ret = -1;
return ret;
}
The certificates I am using for testing purposes are self-signed certificates generated using IIS on the machine where the testing is being done, and then the certificates wer exported to .pfx files and given a password. I have tried creating certificates in "Personal" and "Web Hosting" stores, but I don't see any difference once exported to a .pfx file and imported into the "Web Hosting" store.
I am not sure if this will solve your issue, but I have encountered the same error in some code we have for setting up our local dev environments' websites with certs through an app to get us started quickly. It appeared to be because our certificates were not set up correctly when creating the websites. In my case I had to delete the cert first through certlm to make sure the new one was imported in code properly.
Then, in our code to import certificates, we set the key storage flags in the following way (assuming your certificate was created with the option to allow it to be exported):
// Certificate needs to be exportable with the private key to import into IIS
var keyStorageFlags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet;
// Store location and key storage flag need to match to import into IIS - this is for our developer environments, probably not for your installshield scenario
if (storeLocation == StoreLocation.LocalMachine)
{
keyStorageFlags |= X509KeyStorageFlags.MachineKeySet;
}
It seems that it would be investigating these X509KeyStorageFlags enum options for your scenarios...this worked for our pfx files, but yours may not have been created with the same settings.
I need to send data to a webservice everyday so I made a scheduled task that runs my code. The problem is the webservice requires a certificate with a PIN code. I have attached the certificate but I can't find a way to set the PIN to it, therefor it shows a popup everytime to enter it manually.
Here is my code for the certificate:
private void SendData(string data)
{
using (SerWSService webService = new SerWSService())
{
string certificateSN = "serial number for the certificate";
webService.ClientCertificates.Add(FindCertificate(certificateSN));
webService.SendData(data);
}
}
private X509Certificate2 FindCertificate(string certserial)
{
X509Certificate2 WPE_UserCert = null;
X509Store wstore = default(X509Store);
wstore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
wstore.Open(OpenFlags.ReadOnly);
var wcerts = wstore.Certificates;
foreach (var wcert in wcerts)
{
if (wcert.SerialNumber.ToUpper() == certserial.Replace(" ", "").ToUpper())
{
WPE_UserCert = wcert;
break;
}
}
wstore.Close();
if (WPE_UserCert != null)
{
//TO DO: add PIN code to certificate
}
return WPE_UserCert;
}
Is there any way I can set the PIN to the certificate?
No, because certificates don't have PINs; (private) keys do.
If you are finding a certificate with a private key and you pass that certificate to a class that expects the unified pair (e.g. SslStream, HttpClient) then there's no real/good solution.
If you are using the private key yourself, you have some leeway:
using (RSA rsa = cert.GetRSAPrivateKey())
{
RSACng rsaCng = rsa as RSACng;
RSACryptoServiceProvider rsaCsp = rsa as RSACryptoServiceProvider;
if (rsaCng != null)
{
// Set the PIN, an explicit null terminator is required to this Unicode/UCS-2 string.
byte[] propertyBytes;
if (pin[pin.Length - 1] == '\0')
{
propertyBytes = Encoding.Unicode.GetBytes(pin);
}
else
{
propertyBytes = new byte[Encoding.Unicode.GetByteCount(pin) + 2];
Encoding.Unicode.GetBytes(pin, 0, pin.Length, propertyBytes, 0);
}
const string NCRYPT_PIN_PROPERTY = "SmartCardPin";
CngProperty pinProperty = new CngProperty(
NCRYPT_PIN_PROPERTY,
propertyBytes,
CngPropertyOptions.None);
rsaCng.Key.SetProperty(pinProperty);
}
else if (rsaCsp != null)
{
// This is possible, but painful.
// Copy out the CspKeyContainerInfo data into a new CspParameters,
// build the KeyPassword value on CspParameters,
// Dispose() the existing instance,
// and open a new one to replace it.
//
// But if you really called GetRSAPrivateKey() and it has returned an RSACng for
// your device once, it pretty much will forever (until the next
// new RSA type for Windows is invented... at which point it still
// won't return an RSACryptoServiceProvider).
}
else
{
// No other built-in RSA types support setting a PIN programmatically.
}
// Use the key here.
// If you needed to return it, don't put it in a using :)
}
In the case of a CSP private key use CryptAcquireCertificatePrivateKey to acquire CryptoProv handle and then use CryptSetProvParam(h,PP_SIGNATURE_PIN,pin,0) to set the PIN.
I'm trying to automate the process of generating a site in IIS 6 via C# code. I'm using DirectoryServices and I'm nearly there.. I have it creating the site, setting up all the bindings etc. just fine. I have not figured out how to install our wildcard ssl cert. Here's the details:
We have a SSL certificate that matches '*.example.com'.
Every site we host has a server binding that matches. e.g. 'test.example.com'.
I think I know how to add the SecureBinding property:
DirectoryEntrySite.Properties["SecureBindings"][0] = "xx.xx.xx.xx:443:test.example.com";
But I have had no success finding information on how to automatically install the certificate to the site. In the IIS 6 Manager, you would do this by right clicking a site -> properties -> Directory Security -> Server Certificate -> Next -> Assign an existing certificate -> (select certificate) -> Next...
Can anyone help?
See this: http://forums.iis.net/t/1163325.aspx
using Microsoft.Web.Administration;
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);
X509Certificate2 certificate = new X509Certificate2(pfxFilePath);
store.Add(certificate);
using (ServerManager serverManager = new ServerManager())
{
Site site = serverManager.Sites["Default Web Site"];
if (site != null)
{
site.Bindings.Add("*:443:", certificate.GetCertHash(), store.Name);
}
store.Close();
}
OK, this question is already answered, but the awarded answer is not for IIS6, but rather IIS7 and greater. The namespace Microsoft.Web.Administration is not available for IIS6. We hobbled together a series of technologies, all in .NET 4.0 to make this work.
Steps...
Add reference to COM component IIS CertObj 1.0 Type Library
On the added reference CERTOBJLib, in the properties sheet, set
'Embed Interop Types' to false
Create a class with the following method...
using System.Linq;
using System.Management;
namespace CertStuff
{
public class CertificateInstaller
{
public void RegisterCertificateWithIIS6(string webSiteName, string certificateFilePath, string certificatePassword)
{
// USE WMI TO DERIVE THE INSTANCE NAME
ManagementScope managementScope = new ManagementScope(#"\\.\root\MicrosoftIISv2");
managementScope.Connect();
ObjectQuery queryObject = new ObjectQuery("SELECT Name FROM IISWebServerSetting WHERE ServerComment = '" + webSiteName + "'");
ManagementObjectSearcher searchObject = new ManagementObjectSearcher(managementScope, queryObject);
var instanceNameCollection = searchObject.Get();
var instanceName = (from i in instanceNameCollection.Cast<ManagementObject>() select i).FirstOrDefault();
// USE IIS CERT OBJ TO IMPORT CERT - THIS IS A COM OBJECT
var IISCertObj = new CERTOBJLib.IISCertObjClass();
IISCertObj.InstanceName = instanceName["Name"].ToString();
IISCertObj.Import(certificateFilePath, certificatePassword, false, true); // OVERWRITE EXISTING
}
}
}
to remove the cert reference, use the following method...
public void UnRegisterCertificateWithIIS6(string webSiteName)
{
// USE WMI TO DERIVE THE INSTANCE NAME
ManagementScope managementScope = new ManagementScope(#"\\.\root\MicrosoftIISv2");
managementScope.Connect();
ObjectQuery queryObject = new ObjectQuery("SELECT Name FROM IISWebServerSetting WHERE ServerComment = '" + webSiteName + "'");
ManagementObjectSearcher searchObject = new ManagementObjectSearcher(managementScope, queryObject);
foreach (var instanceName in searchObject.Get())
{
var IISCertObj = new CERTOBJLib.IISCertObjClass();
IISCertObj.InstanceName = instanceName["Name"].ToString();
// THE REMOVE CERT CALL COMPLETES SUCCESSFULLY, BUT FOR WHATEVER REASON, IT ERRORS OUT.
// SWALLOW THE ERROR.
try
{
IISCertObj.RemoveCert(false, true);
}
catch (Exception ex)
{
}
}
}
NOTE: if you receive error "Interop type 'CERTOBJLib.IISCertObjClass' cannot be embedded. Use the applicable interface instead.", this means step 2 was skipped. Ensure the reference object is NOT embedded.
To do this with .Net 4.7 and IIS 10, you can pass the following flags:
X509Certificate2 certificate = new X509Certificate2(path, "password", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable| X509KeyStorageFlags.MachineKeySet);
If you are storing the cert in the CurrentUser store rather than in the LocalMachine store, do the following:
X509Certificate2 certificate = new X509Certificate2(path, "password", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable| X509KeyStorageFlags.UserKeySet);
The Key set flags indicate the following:
//
// Summary:
// Private keys are stored in the current user store rather than the local computer
// store. This occurs even if the certificate specifies that the keys should go
// in the local computer store.
UserKeySet = 1,
//
// Summary:
// Private keys are stored in the local computer store rather than the current user
// store.
MachineKeySet = 2,
The private key needs to be in the same place as the rest of the certificate for it to work with IIS.
I'm using WCF to send a message via MSMQ (net.msmq protocol). All is going well the BizTalk server receives the message and processes it. However, when I looked into the SVCLOG, I see the message is encrypted when I specifically set MsmqProtectionLevel to Sign.
Has anyone else seen this behaviour? Is it possible to stop the encryption? Some of my messages are over 1MB and encryption makes things real slow.
Thanks in advance!
ChannelFactory<OnRampEntry> Factory
{
get
{
if (factory == null)
{
lock (this)
{
if (factory == null)
{
var uri = ResolveQueueName(new Uri(Url));
var identity = EndpointIdentity.CreateDnsIdentity(BizTalkIdentity);
var binding = new NetMsmqBinding(NetMsmqSecurityMode.Both)
{
DeadLetterQueue = DeadLetterQueue.System,
ExactlyOnce = true
};
binding.Security.Message.ClientCredentialType = MessageCredentialType.Certificate;
binding.Security.Transport.MsmqProtectionLevel = System.Net.Security.ProtectionLevel.Sign;
binding.Security.Transport.MsmqAuthenticationMode = MsmqAuthenticationMode.WindowsDomain;
binding.Security.Transport.MsmqSecureHashAlgorithm = MsmqSecureHashAlgorithm.Sha1;
factory = new ChannelFactory<OnRampEntry>(binding, new EndpointAddress(uri, identity, (AddressHeaderCollection) null));
factory.Endpoint.Behaviors.Add(new LogonCertificateBehavior());
factory.Credentials.ServiceCertificate.SetDefaultCertificate(StoreLocation.LocalMachine, StoreName.TrustedPeople, X509FindType.FindBySubjectName, BizTalkIdentity);
factory.Open();
}
}
}
return factory;
}
}
/// <summary>
/// MSMQ does not allow a DNS alias to be used in a queue name, e.g. "net.msmq://alias/private$/queue".
/// <b>ResolveQueueName</b> will tranlsate an alias to its actual machine name.
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
Uri ResolveQueueName(Uri uri)
{
var hostName = uri.DnsSafeHost;
try
{
var hostEntry = Dns.GetHostEntry(hostName);
var resolved = new Uri(uri.ToString().Replace(hostName, hostEntry.HostName));
if (log.IsDebugEnabled)
log.Debug(string.Format("Resolved '{0}' to '{1}'.", uri, resolved));
return resolved;
}
catch (SocketException e)
{
if (e.SocketErrorCode == SocketError.HostNotFound)
return uri;
throw e;
}
}
The reason why the message is encrypted is the use of the NetMsmqSecurityMode.Both - both transport and message security.
var binding = new NetMsmqBinding(NetMsmqSecurityMode.Both)
At the transport level, the configuration above uses
binding.Security.Transport.MsmqProtectionLevel = System.Net.Security.ProtectionLevel.Sign;
Looking in WCF logs it will not be possible to see what is set at the transport level, as message level encryption is in place.
Unfortunately this does not answer the question of how to sign the message (with a X.509 certificate) without using the certificate to encrypt the body of the message.