http add sslcert fails when done programmatically - c#

I have developed a self-hosted api.
The api traffic needs to run over SSL.
Using a combination of netsh commands I have managed to successfully add the certificate and then bind a route to my service. Happy Days.
But, I have to write an installer to do this programmatically.
The problem is that when I add the certificate using my c# code, I can see it the certificate MMC but when I try to bind to it I get an error of:
SSL Certificate add failed, Error: 1312
A specified log-on session does not exist. It may already have been terminated.
As I say, when I do it manually with these steps I don't get the problem...
List item
Double click on the .pfx file.
MMC opens.
I select "Local Machine"
On the next screen I confirm the .pfx file location and name.
I enter the password for the certificate and select "Include all extended properties"
On the next screen I let it default to "Automatically select the certificate store based on the type of certificate"
I then get a confirmation screen.
When I click "Finish" I get a message "The import was successful"
I can then see it in MMC under Personal > Certificates
And it lets me add the route using netsh from a command prompt - Happy Days.
When I try an do it programmatically with the following code:
public static bool ConfigureSSLCertificate(string file, string password, string method)
{
try
{
X509Certificate2 cert = new X509Certificate2(file, password);
var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
if (!store.Certificates.Contains(cert))
{
if (method == "add")
{
store.Add(cert);
}
}
if (method == "remove")
{
store.Remove(cert);
}
return true;
}
catch { return false; }
}
The certificate appears in my MMC in exactly the same place but when I try and add the route with the exact same netsh command as before I get the error mentioned above:
netsh>http add sslcert ipport=0.0.0.0:8088 certhash=fb93ce2c4d8bd88c82e63e3372a050ba84f15e94 appid={bb14356a-a14f-4589-82ce-b80d38b8741e}
For some reason, when I add the certificate manually using the MMC and when I run my code something is different. Something that is stopping the route being added.

The solution is actually simple - I too have struggled with this, and have now found the solution. How can a manually added certificate differ from the programatically added one? Well, the short answer is to change your certificate load line to this:
X509Certificate2 cert = new X509Certificate2(file, password, X509KeyStorageFlags.MachineKeySet);
The key being that last parameter, which tells the certificate to save the private key stored in the machine location, and not the user location. Then the netsh command can find the private key, and can work.
The solution was found in the explanatory text by Paul Stovell and some digging to see how I could set that flag when loading the certificate into the store.
Now, why I can't programmatically do the netsh function is another matter...

I think I have fixed it.
there was a problem with the .pfx I had been trying to install. I have no idea why. what fixed it for me was exporting a working certificate from my personal store with all options set to true and then run it in the following:
public static bool ServiceInstall(string serviceName, string serviceDescription, string executablePath)
{
try
{
ServiceProcessInstaller ProcesServiceInstaller = new ServiceProcessInstaller();
ProcesServiceInstaller.Account = ServiceAccount.LocalSystem;
ServiceInstaller ServiceInstallerObj = new ServiceInstaller();
InstallContext Context = new System.Configuration.Install.InstallContext();
String path = String.Format("/assemblypath={0}", executablePath);
String[] cmdline = { path };
Context = new System.Configuration.Install.InstallContext("", cmdline);
ServiceInstallerObj.Context = Context;
ServiceInstallerObj.DisplayName = serviceName;
ServiceInstallerObj.Description = serviceDescription;
ServiceInstallerObj.ServiceName = serviceName;
ServiceInstallerObj.StartType = ServiceStartMode.Automatic;
ServiceInstallerObj.Parent = ProcesServiceInstaller;
System.Collections.Specialized.ListDictionary state = new System.Collections.Specialized.ListDictionary();
ServiceInstallerObj.Install(state);
return true;
}
catch
{
return false;
}
}
and then use that pfx file
I have no idea why the old pfx worked from the command line but didn't work from code.
HTH

Related

Unable to associate an SSL certificate with a binding in IIS programmatically after importing it during an InstallShield installation

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.

C# - Verify client certificate signed by proper root

I know there are a lot of questions on this argument but I'm stuck into this for several days now so here I am.
I've got a root certificate and a client certificate. I need to replicate in a C# web API project what the command openssl verify -CAfile ca.pem client.pem does.
This is what i know for now (hope it's actually true):
Verify() method actually validate that the certificate is signed by an authority. It's like a format control. It doesn't matter which autority signed the certificate.
X509 Chain is the way to go. Add your ca certificate inside an extra store because i'm not going to install the certificate into Windows. Then build passing the client certificate. Let's the magic happens! Unfortunately I've got some problems with the configuration.
Let me be more clear with an example
private bool VerifyCertificate(X509Certificate2 client)
{
X509Chain chain = new X509Chain();
var stringCert = WebConfigurationManager.AppSettings["CACertificate"];
var byteCert = Encoding.ASCII.GetBytes(stringCert);
var authority = new X509Certificate2(byteCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreWrongUsage;
chain.ChainPolicy.ExtraStore.Add(authority);
// Do the preliminary validation.
if (!chain.Build(client))
return false;
return true;
}
With this example the program returns false. The build is not passed. I'm sure the problem is with the ChainPolicy properties so i tried a different configuration
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
But this one is not going to verify anything, in fact using my ca cert the method returns true and using another ca certificate (which i didn't use to signed my client certificate) the method also returns true.
I Searched for an OpenSSL wrapper for C# and i found it but unfortunately is based on old libraries and the repo isn't manteined anymore. Also, i would achieve my goal using just .net framework if possible.
So guys, fast recap. I want to check that only the certificate that is firmed by my ca certificate can pass the validation, all the others must be stopped.
Thanks in advance for any help
ExtraStore isn't limiting, it provides extra certificates to help complete the chain. It provides no trust data.
In order to determine if the certificate is issued by the CA that you want you need to do something like:
private static readonly X509Certificate2 s_trustedRoot = ObtainTheRoot();
private static readonly byte[] s_normalizedRoot = s_trustedRoot.RawData;
private bool VerifyCertificate(X509Certificate2 candidate)
{
X509Chain chain = new X509Chain();
// set all the things you need to set to make it build
if (!chain.Build(candidate))
return false;
// Check that the root certificate was the expected one.
X509ChainElementCollection elements = chain.ChainElements;
return elements[elements.Count - 1].Certificate.RawData.SequenceEqual(s_normalizedRoot);
}
I promoted the cert and normalized byte form of it to statics on the assumption that they don't change once the process starts. If the cert can change dynamically then you should adjust accordingly.
Find the problem.
In my case this was the right response.
private bool VerifyCertificate(X509Certificate2 client)
{
X509Chain chain = new X509Chain();
var stringCert = WebConfigurationManager.AppSettings["CACertificate"];
var byteCert = Encoding.ASCII.GetBytes(stringCert);
var authority = new X509Certificate2(byteCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
chain.ChainPolicy.ExtraStore.Add(authority);
// Do the preliminary validation.
if (!chain.Build(client))
return false;
// This piece makes sure it actually matches your known root
var valid = chain.ChainElements
.Cast<X509ChainElement>()
.Any(x => x.Certificate.Thumbprint == authority.Thumbprint);
if (!valid)
return false;
return true;
}
Now, debugging the application i have some considerations:
What the .Build() method is supposed to do?
I mean, using the certificate that is signed by the ca or using the self-signed certificate the method Always returns true BUT if i use the trusted one it will add the certificate inside the chain.ChainElementsotherwise nothing is added.
I need to understand this thing but all the test I've done said the method works

Service account cannot load the X509Certificate from windows certificate store

All, I run into an issue where the service account my ASP.NET web forms application (.Net Framework 4.6.1) runs under cannot load the X509Certificate(.pfx) from the personal store on the windows 2012 R2 server .Here is how I imported the certificate to the certificate store
I Logged into the server using the service account(domain\username) ,used mmc snap in to import the certificate to Current User Personal Certificate Store (please see screenshot at the end)
This is the code I am using to load the certificate in C#.But the certificate
is null
public X509Certificate2 Load()
{
X509Certificate2 x509Certificate = null;
var store = new X509Store(StoreName.My,StoreLocation.CurrentUser);
string thumbPrint = StripTheSpacesAndMakeItUpper(ConfigurationManager.AppSettings["pfxthumbPrint"]);
store.Open(OpenFlags.ReadOnly);
var certCollection = store.Certificates;
foreach (var x509 in certCollection)
{
if (x509.Thumbprint.Equals(thumbPrint))
{
x509Certificate = x509;
break;
}
}
store.Close();
return x509Certificate;
}
private string StripTheSpacesAndMakeItUpper(string thumbPrint)
{
if(!string.IsNullOrWhiteSpace(thumbPrint))
{
return Regex.Replace(thumbPrint, #"\s|\W", "").ToUpper();
}
return thumbPrint;
}
Any suggestions on why the method Load returns null ?
]3
I don't know how you set the value of ConfigurationManager.AppSettings["pfxthumbPrint"]. I guess you double clicked the certificate in your CurrentUser store and copied the thumbprint from the Details tab, right? If that is the case, you copied also one invisible character from the beginning of the thumbprint.
The saddest thing is that this character (I don't know what it is) is not visible in your app.config/web.config. The only way to get rid of if is to delete the first quote character with the first character of the thumbprint and type them in again manually. Or delete entire thumbprint and the quotes and type them again if you wish.
Instead of
if (x509.Thumbprint.Equals(x509CertificateFriendlyName))
Shouldn't it be
if (x509.Thumbprint.Equals(thumbPrint))
...?
Also, you appear to have x509Certificate declared as a local variable and then you discard it. Did you intend to assign the value to an instance variable perhaps? I don't even see a return statement.
Also, you're not disposing your store, although that probably isn't the cause of your issue.
Here's a different version that addresses these issues, and will also eliminate any invisible characters in the configuration entry (see pepo's answer for why).
public X509Certificate2 Load()
{
var thumbPrintFromConfig = ConfigurationManager.AppSettings["pfxthumbPrint"]
var thumbPrint = Regex.Replace(thumbPrintFromConfig.ToUpper(),"[^A-F0-9]",""); //Keep only hex digits
return Load(thumbPrint);
}
private X509Certificate2 Load(string thumbPrint)
{
using (var store = new X509Store(StoreName.My,StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
var cert = store
.Certificates
.OfType<X509Certificate2>()
.Where(x => x.Thumbprint == thumbPrint)
.Single();
store.Close();
return cert;
}
}

Add a generated certificate to the store and update an IIS site binding

I'm running into the following and after feeling like I've exhausted various avenues of research on Google and Stack Overflow I decided to just ask my own question about it.
I'm trying to generate a personal certificate (using BouncyCastle) based on a CA certificate that I already have and own. After generating the certificate, placing it in the 'My' store, I then attempt to update my IIS website's SSL binding to use this new certificate.
What I'm noticing is that the updates to the IIS website (using ServerManager) are not throwing exception, yet when I go to the IIS Manager console I notice the website's binding has no SSL certificate selected. When I attempt to select the certificate that I created (shows up fine as a viable option) I get the following error message:
A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)
As a test I exported my generated certificate (with the private key) and reinstalled it via the wizard and then once again tried setting up the binding (via IIS Manager) which worked.
Because of this behavior I assumed it was an issue with how I was generating or adding the certificate to the store. I was hoping someone may have some idea of what the issue I'm having may be. The following are the relevant functions (I believe) used in creating the certificate, adding it to the store, and updating the website's binding programmatically:
Main function the generates that get the CA certificate private key, generates the personal self-signed certificate, and updates the sites binding:
public static bool GenerateServerCertificate(
X509Certificate2 CACert,
bool addToStore,
DateTime validUntil)
{
try
{
if (CACert.PrivateKey == null)
{
throw new CryptoException("Authority certificate has no private key");
}
var key = DotNetUtilities.GetKeyPair(CACert.PrivateKey).Private;
byte[] certHash = GenerateCertificateBasedOnCAPrivateKey(
addToStore,
key,
validUntil);
using (ServerManager manager = new ServerManager())
{
Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();
if (site == null)
{
return false;
}
foreach (Binding binding in site.Bindings)
{
if (binding.Protocol == "https")
{
binding.CertificateHash = certHash;
binding.CertificateStoreName = "MY";
}
}
manager.CommitChanges();
}
}
catch(Exception ex)
{
LOG.Error("Error generating certitifcate", ex);
return false;
}
return true;
}
Generating the certificate based on the CA private key:
public static byte[] GenerateCertificateBasedOnCAPrivateKey(
bool addToStore,
AsymmetricKeyParameter issuerPrivKey,
DateTime validUntil,
int keyStrength = 2048)
{
string subjectName = $"CN={CertSubjectName}";
// Generating Random Numbers
CryptoApiRandomGenerator randomGenerator = new CryptoApiRandomGenerator();
SecureRandom random = new SecureRandom(randomGenerator);
ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivKey, random);
// The Certificate Generator
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage,
true,
new ExtendedKeyUsage((new List<DerObjectIdentifier> { new DerObjectIdentifier("1.3.6.1.5.5.7.3.1") })));
// Serial Number
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
certificateGenerator.SetSerialNumber(serialNumber);
// Issuer and Subject Name
X509Name subjectDN = new X509Name(subjectName);
X509Name issuerDN = new X509Name(CACertificateName);
certificateGenerator.SetIssuerDN(issuerDN);
certificateGenerator.SetSubjectDN(subjectDN);
// Valid For
DateTime notBefore = DateTime.UtcNow.Date;
DateTime notAfter = validUntil > notBefore ? validUntil : notBefore.AddYears(1);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// Subject Public Key
AsymmetricCipherKeyPair subjectKeyPair;
var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
// Generating the Certificate
Org.BouncyCastle.X509.X509Certificate certificate = certificateGenerator.Generate(signatureFactory);
// correcponding private key
PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);
// merge into X509Certificate2
X509Certificate2 x509 = new X509Certificate2(certificate.GetEncoded());
Asn1Sequence seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
if (seq.Count != 9)
{
throw new PemException("Malformed sequence in RSA private key");
}
RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(seq);
RsaPrivateCrtKeyParameters rsaparams = new RsaPrivateCrtKeyParameters(
rsa.Modulus,
rsa.PublicExponent,
rsa.PrivateExponent,
rsa.Prime1,
rsa.Prime2,
rsa.Exponent1,
rsa.Exponent2,
rsa.Coefficient);
x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
if (addToStore)
{
// Add certificate to the Personal store
AddCertToStore(x509, StoreName.My, StoreLocation.LocalMachine, "Certificate Friendly Name");
}
return x509.GetCertHash();
}
Adding the certificate to the store:
private static void AddCertToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, string friendlyName)
{
X509Store store = new X509Store(storeName, storeLocation);
try
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
if (!string.IsNullOrWhiteSpace(friendlyName)) {
var certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, cert.Subject, true);
if (certs.Count > 0)
{
certs[0].FriendlyName = friendlyName;
}
}
}
finally
{
store.Close();
}
}
Just a final note, I have tried a few things from what I've seen on various sites in regards to that error (doesn't seem very clear what the issue is):
This works on a different box (my personal development machine) but I hit these snags on a server machine (running Windows Server 2012 R2)
The IIS Help dialog informs me the machine is running IIS 8.5
Verified the validity generated certificate and the CA certificate with CertUtil.exe
Verified the generated certificate and the CA certificate had a private key that could be found
Verified administrators (and eventually even my logged in account) had access to where the private key file for both the CA certificate and the generated certificate.
Any ideas what my issue could be?
Update:
I was able to get some results by doing the following:
Export my certificate to a file programmatically by doing File.WriteAllBytes(filePath, cert.Export(X509ContentType.Pkcs12, password));
Then I import this certificate file to the store by doing:
var cert = new X509Certificate2(certFilePath, certPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// My original AddCertToStore function
AddCertToStore(cert, StoreName.My, StoreLocation.LocalMachine, "Friendly Name");
Finally, I set the binding as I was doing earlier:
using (ServerManager manager = new ServerManager())
{
Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();
if (site == null)
{
return false;
}
foreach (Binding binding in site.Bindings)
{
if (binding.Protocol == "https")
{
binding.CertificateHash = certHash;
binding.CertificateStoreName = "MY";
}
}
manager.CommitChanges();
}
Doing it this way works, but I don't see why I would have export the certificate to a file, THEN load it into a X509Certificate2 object, add to the store, and finally set up the binding.
The ToRSA method most likely creates an ephemeral RSA key, so when the references are all gone the key gets deleted. Exporting the ephemeral structure into a PFX then re-importing it with PersistKeySet is one way to turn it into a persisted key. Others exist, but that one is one of the less convoluted ones.
You don't actually have to write it to a file, though.
byte[] pkcs12Blob = cert.Export(X509ContentType.Pkcs12, password);
ver certWithPersistedKey = new X509Certificate2(pkcs12Blob, password, allTheFlagsYouAlreadySet);
There are also other subtleties going on, like setting the PrivateKey property has different behaviors for a cert instance that was loaded from a store and one which was loaded from bytes... the PFX/PKCS#12 export/import works around all of those.
For us, it was related to an invalid certificate. We went to IIS >> Server Certificates and exported the certificate from there.
The certificate was correctly bound to IIS site after that.

Automate SSL certificate install to IIS 6 sites using C#

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.

Categories

Resources