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.
Related
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'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.
I'm using Microsoft.Web.Administration (inside a Wix CustomAction) to configure Server Name Indication and bind to an existing server certificate on a IIS 8.5 site.
Turns out, setting SNI takes off the certificate binding. The following code will make things clearer:
using Microsoft.Web.Administration;
var binding = site.Bindings.FirstOrDefault(x => x.IsIPPortHostBinding && x.Host == sitename);
binding.CertificateHash = certificate.GetCertHash();
binding.CertificateStoreName = store.Name;
// this statement is causing the certificate info to get messed up.
binding["sslFlags"] = 1; // or binding.SetAttributeValue("sslFlags", 1);
Results:
With binding["sslFlags"] = 1;
Without binding["sslFlags"] = 1;
Is this a bug or am I missing something? How can get both SNI and Certificate binding to stick?
It seems Microsoft.Web.Administration v7.0 is the culprit here. This is the official one on NuGet gallery and it seems that it is meant for IIS 7 mainly (I mean it'll work for features common in both IIS 7 & 8 but anything that 7 doesn't have will have weird results like above).
Using IIS.Microsoft.Web.Adminstration (which seems to be a community uploaded package for IIS 8.5) works. Got the hint from this answer.
Updated code:
binding.CertificateHash = certificate.GetCertHash();
binding.CertificateStoreName = store.Name;
binding.SslFlags = SslFlags.Sni; // <<< notice it has helpful enums
This works for me with Microsoft.Web.Administration 7.0.0.0:
public static void CreateSiteHttps(string siteName, string physicalPath)
{
using (var serverManager = new ServerManager())
{
var applicationPool = serverManager.ApplicationPools.Add(siteName);
applicationPool["startMode"] = "AlwaysRunning";
var x509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
x509Store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);
var certificate = x509Store.Certificates.Find(X509FindType.FindBySubjectName, "MyCertSubjectName", false)[0];
var hash = certificate.GetCertHash();
var site = serverManager.Sites.Add(siteName, $"*:443:{siteName}", physicalPath, hash);
site.ServerAutoStart = true;
site.Bindings[0]["sslFlags"] = 1;
site.ApplicationDefaults.ApplicationPoolName = applicationPool.Name;
site.ApplicationDefaults.EnabledProtocols = "http,https";
serverManager.CommitChanges();
}
}
The certificate is removed when enabling SNI. You can simply get the certificate before and set it again after enabling SNI:
var cert = mySslBinding.CertificateHash;
mySslBinding.SetAttributeValue("SslFlags", Convert.ToInt32(1));
mySslBinding.CertificateHash = cert;
I have X509 certificates that are stored on the network. I can read the chain from remote windows certificate store. I need to sign some data and include chain to the signature to make it possible to validate it later.
The problem is that I can't find a way to put certificate chain to the CsmSigner. I have read that it takes certificate from constructor parameter and tries to build a chain with X509Chain.Build. It ignores Certificates list values and fails (obviously) because no certificate can be found in the local Windows cert store.
Please find below my test code (that works only if certificates were saved locally to the windows cert store)
protected byte[] SignWithSystem(byte[] data, X509Certificate2 cert, X509Certificate[] chain)
{
ContentInfo contentInfo = new ContentInfo(data);
SignedCms signedCms = new SignedCms(contentInfo, true);
CmsSigner cmsSigner = new CmsSigner(cert);
cmsSigner.DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1"); //sha256
cmsSigner.IncludeOption = X509IncludeOption.WholeChain;
if (chain != null)
{
//adding cert chain to signer
cmsSigner.Certificates.AddRange(chain);
signedCms.Certificates.AddRange(chain);
}
signedCms.ComputeSignature(cmsSigner); //fails here with System.Security.Cryptography.CryptographicException : A certificate chain could not be built to a trusted root authority.
byte[] signedPkcs = signedCms.Encode();
return signedPkcs;
}
Is there any way to make it work without uploading certificates to the local store? Should I use any alternative signer?
I can try to upload certificates to the store but the problems are that
I have to add and remove certificates (permissions have to be granted)
There are several processes that applies signature so cross-process synchronization have to be added.
This is not that I'd like to do.
Example CMS Signing with BouncyCastle for .NET
You could use the BouncyCastle crypto library for .NET, which contains its own X509 certificate and CMS signing machinery. A lot of the examples and documentation on the web are for Java, as BouncyCastle was a Java library first. I've used the answer to this Stackoverflow question as a starting point for the certificate and key loading, and added the CMS signing. You may have to tweak parameters to produce the results you want for your use case.
I've made the signing function look approximately like yours, but note the private key is a separate parameter now.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Org.BouncyCastle.Cms;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.X509.Store;
class Program
{
protected static byte[] SignWithSystem(byte[] data, AsymmetricKeyParameter key, X509Certificate cert, X509Certificate[] chain)
{
var generator = new CmsSignedDataGenerator();
// Add signing key
generator.AddSigner(
key,
cert,
"2.16.840.1.101.3.4.2.1"); // SHA256 digest ID
var storeCerts = new List<X509Certificate>();
storeCerts.Add(cert); // NOTE: Adding end certificate too
storeCerts.AddRange(chain); // I'm assuming the chain collection doesn't contain the end certificate already
// Construct a store from the collection of certificates and add to generator
var storeParams = new X509CollectionStoreParameters(storeCerts);
var certStore = X509StoreFactory.Create("CERTIFICATE/COLLECTION", storeParams);
generator.AddCertificates(certStore);
// Generate the signature
var signedData = generator.Generate(
new CmsProcessableByteArray(data),
false); // encapsulate = false for detached signature
return signedData.GetEncoded();
}
static void Main(string[] args)
{
try
{
// Load end certificate and signing key
AsymmetricKeyParameter key;
var signerCert = ReadCertFromFile(#"C:\Temp\David.p12", "pin", out key);
// Read CA cert
var caCert = ReadCertFromFile(#"C:\Temp\CA.cer");
var certChain = new X509Certificate[] { caCert };
var result = SignWithSystem(
Guid.NewGuid().ToByteArray(), // Any old data for sake of example
key,
signerCert,
certChain);
File.WriteAllBytes(#"C:\Temp\Signature.data", result);
}
catch (Exception ex)
{
Console.WriteLine("Failed : " + ex.ToString());
Console.ReadKey();
}
}
public static X509Certificate ReadCertFromFile(string strCertificatePath)
{
// Create file stream object to read certificate
using (var keyStream = new FileStream(strCertificatePath, FileMode.Open, FileAccess.Read))
{
var parser = new X509CertificateParser();
return parser.ReadCertificate(keyStream);
}
}
// This reads a certificate from a file.
// Thanks to: http://blog.softwarecodehelp.com/2009/06/23/CodeForRetrievePublicKeyFromCertificateAndEncryptUsingCertificatePublicKeyForBothJavaC.aspx
public static X509Certificate ReadCertFromFile(string strCertificatePath, string strCertificatePassword, out AsymmetricKeyParameter key)
{
key = null;
// Create file stream object to read certificate
using (var keyStream = new FileStream(strCertificatePath, FileMode.Open, FileAccess.Read))
{
// Read certificate using BouncyCastle component
var inputKeyStore = new Pkcs12Store();
inputKeyStore.Load(keyStream, strCertificatePassword.ToCharArray());
var keyAlias = inputKeyStore.Aliases.Cast<string>().FirstOrDefault(n => inputKeyStore.IsKeyEntry(n));
// Read Key from Aliases
if (keyAlias == null)
throw new NotImplementedException("Alias");
key = inputKeyStore.GetKey(keyAlias).Key;
//Read certificate into 509 format
return (X509Certificate)inputKeyStore.GetCertificate(keyAlias).Certificate;
}
}
}
.NET CMS (Quick-fix with rest of chain omitted from signature)
I can reproduce your problem with a certificate whose root is not in the trusted certificate store, and confirm that adding the certificate chain to the cmsSigner/signedCms Certificates collection does not avoid the A certificate chain could not be built to a trusted root authority error.
You can sign successfully by setting cmsSigner.IncludeOption = X509IncludeOption.EndCertOnly;
However, if you do this, you will not get the rest of the chain in the signature. This probably isn't what you want.
As an aside, in your example you are using X509Certificate for the array of certificates in the chain, but passing them to an X509Certificate2Collection (note the "2" in there). X509Certificate2 derives from X509Certificate, but if its not actually an X509Certificate2 that you put in one of those collections, you'll get a cast error if something iterates over the collection (you don't get an error when adding a certificate of the wrong type unfortunately, because X509Certificate2Collection also derives from X509CertificateCollection and inherits its add methods).
Adding sample code that creates detached PKCS7 signature using BouncyCastle (thanks to softwariness) without Certificate store.
It uses .net X509Certificate2 instances as input parameter. First certificate in collection have to be linked with private key to sign data.
Also I'd like to note that it is not possible to read private key associated with certificate from remote Windows cert store using .net X509Certificate2.PrivateKey property. By default private key is not loaded with certificate using X509Store(#"\\remotemachine\MY", StoreLocation.LocalMachine) and when X509Certificate2.PrivateKey property is accessed on local machine it fails with error "Keyset does not exist".
public void SignWithBouncyCastle(Collection<X509Certificate2> netCertificates)
{
// first cert have to be linked with private key
var signCert = netCertificates[0];
var Cert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(signCert);
var data = Encoding.ASCII.GetBytes(Cert.SubjectDN.ToString());
var bcCertificates = netCertificates.Select(_ => Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(_)).ToList();
var x509Certs = X509StoreFactory.Create("Certificate/Collection", new X509CollectionStoreParameters(bcCertificates));
var msg = new CmsProcessableByteArray(data);
var gen = new CmsSignedDataGenerator();
var privateKey = Org.BouncyCastle.Security.DotNetUtilities.GetKeyPair(signCert.PrivateKey).Private;
gen.AddSigner(privateKey, Cert, CmsSignedDataGenerator.DigestSha256);
gen.AddCertificates(x509Certs);
var signature = gen.Generate(msg, false).GetEncoded();
Trace.TraceInformation("signed");
CheckSignature(data, signature);
Trace.TraceInformation("checked");
try
{
CheckSignature(new byte[100], signature);
}
catch (CryptographicException cex)
{
Trace.TraceInformation("signature was checked for modified data '{0}'", cex.Message);
}
}
void CheckSignature(byte[] data, byte[] signature)
{
var ci = new ContentInfo(data);
SignedCms signedCms = new SignedCms(ci, true);
signedCms.Decode(signature);
foreach (X509Certificate cert in signedCms.Certificates)
Trace.TraceInformation("certificate found {0}", cert.Subject);
signedCms.CheckSignature(true);
}
To be clear, I am no security or cryptography expert.. but per my knowledge, for receiver to be able to validate the signature, the root certificate in the certificate chain you used for signing, must already be a trusted root for the receiver.
If the receiver does not have the root certificate already in their store, and marked as a trusted root... then doesn't matter how you sign the data.. it will fail validation on receiver end. And this is by design.
See more at Chain of trust
Hence the only real solution to your problem I see is to ensure that the root certificate is provisioned as trusted root on both ends... Typically done by a Certificate Authority.
Enterprise application scenario - Typically in an enterprise some group in IT department (who have access to all machines in the domain - like domain admins) would enable this scenario by ensuring that every computer in the domain has root certificate owned by this group, present on every machine as trusted root, and an application developer in the enterprise typically requests a new certificate for use with their application, which has the chain of trust going back to the root certificate already distributed to all machines in the domain.
Found out contact person for this group in your company, and have them issue a certificate you can use for signature.
Internet application scenario - There are established Certificate Authorities, who own their root certificates, and work with OS vendors to ensure that their root certificates are in trusted store, as the OS vendor ships the OS to it's customers. (One reason why using pirated OS can be harmful. It's not just about viruses / malware..). And that is why when you use a certificate issued by VeriSign to sign the data, the signature can be validated by most other machines in the world.
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