WebSocket secure connection self signed certificate - c#

The goal is a web application that exchanges information with an C# application that is installed on the user's pc.
The client application is the websocket server and the browser is the websocket client.
In the end the websocket client in the user's browser is created persistently via Angular and the application is running on the pc and doing some things.
The C# library used is WebSocket-Sharp. The websocket client is normal javascript.
Obviously this connection happens only local so the client connects to localhost.
As the website is secured via HTTPS the websocket has to be secured too. For this purpose the C# application creates a certificate when it starts up (it's just for testing purposes actually).
The connection doesn't works because the certificate is untrusted. All server checks for the client are disabled but the connection won't establish.
This is the part where the server is created
_server = new WebSocketServer($"wss://localhost:4649")
{
SslConfiguration =
{
ServerCertificate = Utils.Certificate.CreateSelfSignedCert(),
ClientCertificateRequired = false,
CheckCertificateRevocation = false,
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
}
};
_server.AddWebSocketService<CommandsBehaviour>("/commands");
_server.AddWebSocketService<NotificationsBehaviour>("/notifications");
_server.Start();
This is how the certificate is created with BouncyCastle
private static AsymmetricKeyParameter CreatePrivateKey(string subjectName = "CN=root")
{
const int keyStrength = 2048;
// Generating Random Numbers
var randomGenerator = new CryptoApiRandomGenerator();
var random = new SecureRandom(randomGenerator);
// The Certificate Generator
var certificateGenerator = new X509V3CertificateGenerator();
// Serial Number
var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), random);
certificateGenerator.SetSerialNumber(serialNumber);
// Issuer and Subject Name
var subjectDn = new X509Name(subjectName);
var issuerDn = subjectDn;
certificateGenerator.SetIssuerDN(issuerDn);
certificateGenerator.SetSubjectDN(subjectDn);
// Valid For
var notBefore = DateTime.UtcNow.Date;
var notAfter = notBefore.AddYears(70);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// Subject Public Key
var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
return subjectKeyPair.Private;
}
public static X509Certificate2 CreateSelfSignedCert(string subjectName = "CN=localhost", string issuerName = "CN=root")
{
const int keyStrength = 2048;
var issuerPrivKey = CreatePrivateKey();
// Generating Random Numbers
var randomGenerator = new CryptoApiRandomGenerator();
var random = new SecureRandom(randomGenerator);
ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivKey, random);
// The Certificate Generator
var certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, new GeneralNames(new GeneralName[] { new GeneralName(GeneralName.DnsName, "localhost"), new GeneralName(GeneralName.DnsName, "127.0.0.1") }));
certificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage((new ArrayList() { new DerObjectIdentifier("1.3.6.1.5.5.7.3.1") })));
// Serial Number
var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
certificateGenerator.SetSerialNumber(serialNumber);
// Signature Algorithm
//const string signatureAlgorithm = "SHA512WITHRSA";
//certificateGenerator.SetSignatureAlgorithm(signatureAlgorithm);
// Issuer and Subject Name
var subjectDn = new X509Name(subjectName);
var issuerDn = new X509Name(issuerName);
certificateGenerator.SetIssuerDN(issuerDn);
certificateGenerator.SetSubjectDN(subjectDn);
// Valid For
var notBefore = DateTime.UtcNow.Date;
var notAfter = notBefore.AddYears(70);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// Subject Public Key
var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
// self sign certificate
var certificate = certificateGenerator.Generate(signatureFactory);
// corresponding private key
var info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);
// merge into X509Certificate2
var x509 = new X509Certificate2(certificate.GetEncoded());
var seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
if (seq.Count != 9)
{
throw new PemException("malformed sequence in RSA private key");
}
var rsa = RsaPrivateKeyStructure.GetInstance(seq); //new RsaPrivateKeyStructure(seq);
var rsaparams = new RsaPrivateCrtKeyParameters(
rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent, rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2, rsa.Coefficient);
x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
return x509;
}
This behaviour is logical although it is strange as the cert check shouldn't be performed locally.
Is there a possibility to bypass this problem? I already thought about installing the issuer certificate to the trusted certs but this is not an optimal solution.

Have you tried any of the answers to this question?
To summarize, it looks like there are a few options you could try:
Start Chrome with the --ignore-certificate-errors argument specified.
Start an HTTP server on the same port that takes the same self-signed certificate, browse to it, and accept the certificate, after which you should be able to use the WebSocket connection.
Set the configuration option on Firefox network.websocket.allowInsecureFromHTTPS to true, then use the ws:// rather than the wss:// address.
If all this is is for testing and you have the possibility to control that sort of thing, then I think one or more of those should work. If you need your standard end user to be able to do this, I think you'll a need a different solution. As you've found, it doesn't matter if you set the server up to not care about the certificate, the client has to ultimately decide if it wants to accept the certificate or it won't accept the connection.

#Kdawg answers are correct.
You have no hope to have client browsers accept an insecure connection with server-side tuning only. All the behavior for accepting the unsigned (or self-signed) certificate is on the client side.
I would like to add, on top of #Kdawg's answer that on Windows networks, the most common practice for private organizations is to:
Assign a Windows Server to act as Certificate Authority
Add the Certificate Authority's public root certificate into Windows hosts (either by GPO) or manually
Sign the custom-made certificate with the Windows CA server
It sounds painful, and it is.
If I were you, I would go for making a standard publicly-signed certificate and would run SSL off until it is done.
Look at Let's Encrypt for free SSL certificates for your domain.

My final solution was to create a valid certificate for a subdomain and then changed the A/AAAA records to localhost. This way the connection is trusted over HTTPS.

Related

OPC UA Client: client certificate "certificate chain validation incomplete" (using SDK from OPC Foundation)

I'm writing an OPC UA Client in C# using OPC Foundation NuGet package.
I can succesfully connect to server, read and write variables and create subscriptions and monitored items.
Since I'd like to use this code in production environments, I need to use secure connection (username / password policy instead of anonymous connection).
This is how I'm doing it:
bool security = true; // Set here security enabled
Session session; // OPC UA Session
// 1. Generate client application
ApplicationInstance application = new() { ApplicationType = ApplicationType.Client };
// 2. Load application configuration
ClientConfiguration clientConfiguration = new() { DefaultSessionTimeout = _timeout };
ApplicationConfiguration configuration = new() { ApplicationName = "OPC Foundation SDK Personal Implementation", ClientConfiguration = clientConfiguration, ApplicationUri = "urn:OPC Foundation SDK Personal Implementation" };
application.ApplicationConfiguration = configuration;
// 3. Get the endpoint by connecting to server's discovery endpoint
EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint($"opc.tcp://{Target}", security);
// 4. Get server certificate
var rawCertificate = endpointDescription.ServerCertificate;
CertificateIdentifier serverCertificate = new(rawCertificate);
// 5. Add server certificate to trusted peers and trusted issuers
configuration.SecurityConfiguration.TrustedPeerCertificates.TrustedCertificates.Add(serverCertificate);
configuration.SecurityConfiguration.TrustedIssuerCertificates.TrustedCertificates.Add(serverCertificate);
// 6. Create application instance certificate
var appCertificate = new X509Certificate2("TestCertificate.der", "");
configuration.SecurityConfiguration.AddAppCertToTrustedStore = true;
configuration.SecurityConfiguration.ApplicationCertificate = new(appCertificate);
// Validate the configuration
configuration.Validate(ApplicationType.Client);
var result = application.CheckApplicationInstanceCertificate(true, CertificateFactory.DefaultKeySize);
// 7. Setup endpoint
EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(configuration);
ConfiguredEndpoint endpoint = new(null, endpointDescription, endpointConfiguration);
// 8. Create session
if (security)
session = Session.Create(configuration, endpoint, false, false, configuration.ApplicationName, (uint)configuration.ClientConfiguration.DefaultSessionTimeout, new UserIdentity("****", "****"), null).Result; // Username and password
else
session = Session.Create(configuration, endpoint, false, false, configuration.ApplicationName, (uint)configuration.ClientConfiguration.DefaultSessionTimeout, new UserIdentity(), null).Result; // Anonymous login to OPC UA server
If security is set to true, I get error "certificate chain validation incomplete". This is referred at my application instance certificate because without security the session is created successfully (so I'm sure the server certificate is being accepted).
The certificate I'm using is self-signed and it is generated with the following code:
public static void CreateApplicationCertificate(string certFilename, string keyFilename)
{
const string CRT_HEADER = "-----BEGIN CERTIFICATE-----\n";
const string CRT_FOOTER = "\n-----END CERTIFICATE-----";
const string KEY_HEADER = "-----BEGIN RSA PRIVATE KEY-----\n";
const string KEY_FOOTER = "\n-----END RSA PRIVATE KEY-----";
using var rsa = RSA.Create();
var certRequest = new CertificateRequest("cn=TestCertificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// This is a test and the certificate is being regenerated every time I run the code, so one day is enough.
var certificate = certRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(1));
// Export the private key
var privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey(), Base64FormattingOptions.InsertLineBreaks);
File.WriteAllText(keyFilename + ".pem", KEY_HEADER + privateKey + KEY_FOOTER);
// Export the certificate
var exportData = certificate.Export(X509ContentType.Cert);
var crt = Convert.ToBase64String(exportData, Base64FormattingOptions.InsertLineBreaks);
File.WriteAllText(certFilename + ".der", CRT_HEADER + crt + CRT_FOOTER);
}
I added this line of code
configuration.SecurityConfiguration.AddAppCertToTrustedStore = true;
but nothing changed.
Any help is really appreciated.

Adding a certificate to Store then retrieve it

Disclaimer: I am 2 days into reading about Certificates/RSA Algorithms and Encrypt/Decrypt.
I am trying to do a small app that communicates with Windows Key Store ( Certificate Store ) and where I should be able to read certificates/add certificates.
I have created a method to add a certificate.
public void AddKey()
{
CngKey cngKey;
CngKeyCreationParameters cng = new CngKeyCreationParameters
{
KeyUsage = CngKeyUsages.AllUsages
};
if (!CngKey.Exists(KEY_NAME))
{
cngKey = CngKey.Create(CngAlgorithm.Rsa, KEY_NAME, cng);
}
else
{
cngKey = CngKey.Open(KEY_NAME);
}
RSACng rsaKey = new RSACng(cngKey)
{
KeySize = 2048
};
byte[] rsaPrvKeyExport = rsaKey.Key.Export(CngKeyBlobFormat.GenericPrivateBlob);
byte[] rsaPubKeyExport = rsaKey.Key.Export(CngKeyBlobFormat.GenericPublicBlob);
CngKey cngPrv = CngKey.Import(rsaPrvKeyExport, CngKeyBlobFormat.GenericPrivateBlob);
CngKey cngPub = CngKey.Import(rsaPubKeyExport, CngKeyBlobFormat.GenericPublicBlob);
//var signed = Sign512(Constants.STRING_TO_ENCODE.ToByteArray(), rsaPrvKeyExport);
string exportPrivateKey = Convert.ToBase64String(rsaKey.ExportPkcs8PrivateKey());
string pemString = $"{Constants.RSA_KEY_HEADER}\n{exportPrivateKey}\n{Constants.RSA_KEY_FOOTER}";
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
var certificate = BuildSelfSignedServerCertificate(rsaKey);
var thumbprint = certificate.Thumbprint;
store.Certificates.Add(certificate);
store.Close();
}
I know not all lines in this code are needed, but I am in the learning process.
So what I do here is create a cngKey
Open the store.
Create a Certificate from my cngKey
Add the certificate to the store
Close the store.
I generate a certificate from a cngKey using this code
private X509Certificate2 BuildSelfSignedServerCertificate(RSA key)
{
X500DistinguishedName distinguishedName = new X500DistinguishedName($"CN={Constants.CERTIFICATE_NAME}");
var request = new CertificateRequest(distinguishedName, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(
new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
//request.CertificateExtensions.Add(sanBuilder.Build());
var certificate = request.CreateSelfSigned(new DateTimeOffset(DateTime.UtcNow.AddDays(-1)), new DateTimeOffset(DateTime.UtcNow.AddDays(3650)));
certificate.FriendlyName = Constants.CERTIFICATE_NAME;
return new X509Certificate2(certificate.Export(X509ContentType.Pfx, "WeNeedASaf3rPassword"), "WeNeedASaf3rPassword", X509KeyStorageFlags.MachineKeySet);
}
This gives no error, but when I open the certificate store I can't find it
I also tried to retrive it programmaticaly
using this bit of code
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates;
but my certificate is not here.
What am I missing, I am sure I am doing something wrong, but being new to this, I have no idea what.
Online I could not find a complete example of what I tried to achieve here.
As #dimitar.bogdanov pointed out in comments, you are not adding the certificate to the store:
store.Certificates.Add(certificate);
here you are adding the certificate only to disconnected collection. Any changes in this collection object will not reflect actual store state. Instead, you have to use X509Store.Add method to update actual store.

c# MessageSecurityException in a web service

I have a web service feature that I have been using for many years. Today they sent me a new certificate, a .cer file, which I inserted instead of the old one but I get this error:
(Translated with Google, sorry)
MessageSecurityException
Failed to complete identity check for outgoing message.
The expected DNS identity of the remote endpoint was 'pddasl-coll.rmmg.rsr.rupar.puglia.it'
but the remote endpoint provided a DNS claim 'pdd-virtasl.rmmg.rsr.rupar.puglia.it'.
If this is a legitimate remote endpoint, you can fix the problem by specifying
explicitly the DNS identity 'pdd-virtasl.rmmg.rsr.rupar.puglia.it'
as the Identity property of EndpointAddress when creating the channel proxy.
I asked the owner of the web service and they told me that I have to make sure to ignore the error, but I don't know how. I tried to insert in the app.config: enableUnsecuredResponse = "true" but it didn't work.
This is the method for connecting to the web service:
public static CVPClient Connect()
{
CVPClient oConsist = null;
string cEndPoint = "https://pddasl-coll.rmmg.rsr.rupar.puglia.it:8181/aslba/CVPService";
ServicePointManager.ServerCertificateValidationCallback = Leo.CertificateHandler;
datiOperatore DataOp = Leo.OperatorData();//unimportant parameters
datiApplicativo DataApp = Leo.AppData();//unimportant parameters
var b = new CustomBinding();
var sec = new AsymmetricSecurityBindingElement(
new X509SecurityTokenParameters(X509KeyIdentifierClauseType.Any, SecurityTokenInclusionMode.Never),
new X509SecurityTokenParameters(X509KeyIdentifierClauseType.Any, SecurityTokenInclusionMode.AlwaysToRecipient));
sec.MessageSecurityVersion = MessageSecurityVersion.WSSecurity10WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10;
sec.SecurityHeaderLayout = SecurityHeaderLayout.Strict;
sec.IncludeTimestamp = true;
sec.SetKeyDerivation(false);
sec.KeyEntropyMode = System.ServiceModel.Security.SecurityKeyEntropyMode.ServerEntropy;
sec.EnableUnsecuredResponse = true;
b.Elements.Add(sec);
b.Elements.Add(new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8));
b.Elements.Add(new HttpsTransportBindingElement());
EndpointAddress ea = new EndpointAddress(cEndPoint);
oConsist = new CVPClient(b, ea);
X509Certificate2 certSigned = Leo.GetSignedCert();//this returns my private certificate, not the one they replaced me
string cPin = "123456";
System.Security.SecureString SecurePIN = new System.Security.SecureString();
foreach (char ch in cPin)
{ SecurePIN.AppendChar(ch); }
var rsa = (RSACryptoServiceProvider)certSigned.PrivateKey;
string ContinerName = rsa.CspKeyContainerInfo.KeyContainerName;
string CspName = rsa.CspKeyContainerInfo.ProviderName;
int CspType = rsa.CspKeyContainerInfo.ProviderType;
CspParameters csp = new CspParameters(CspType, CspName, ContinerName, new System.Security.AccessControl.CryptoKeySecurity(), SecurePIN);
RSACryptoServiceProvider CSP = new RSACryptoServiceProvider(csp);
X509Certificate2 certUnsigned = Leo.GetUnSignedCertificate();//Here I read the new certificate
oConsist.ClientCredentials.ClientCertificate.Certificate = certSigned;
oConsist.ClientCredentials.ServiceCertificate.DefaultCertificate = certUnsigned;
oConsist.Open();
return oConsist;
}
As you can see also here I have inserted sec.EnableUnsecuredResponse = true; The connection is successful but, as I said before, I get the error when I call the web service.
How can I solve the problem?
UPDATE:
Since the error tells me to explicitly assign the DNS identity 'pdd-virtasl.rmmg.rsr.rupar.puglia.it'
as the Identity property of EndpointAddress, I replaced this line:
EndpointAddress ea = new EndpointAddress(cEndPoint);
with this:
DnsEndpointIdentity identity = new DnsEndpointIdentity(cEndPoint);
EndpointAddress ea = new EndpointAddress(new Uri(cEndPoint), identity, new AddressHeaderCollection());
but it doesn't work, I get the same error
The correct solution is this:
DnsEndpointIdentity identity = new DnsEndpointIdentity("pdd-virtasl.rmmg.rsr.rupar.puglia.it");
EndpointAddress ea = new EndpointAddress(new Uri(cEndPoint), identity, new AddressHeaderCollection());
You have to use the web service endpoint but specify the endopint contained in the certificate in the identity.

Exception on attempt to create not self-signed certificate

I want to switch from self-signed certificate per device to pair of certificates, one of which is previously generated, placed in Trusted Root Certificate Authorities store, is same for all devices, and works as root CA for second certificate, which is generated per device, and placed in Personal store.
I would like to not use makecert, since creating signed certificate shows up UI, which I want to avoid. Also, OpenSSL can't be used due to some license-related stuff (although I have working solution with it). So, for now I'm working with small C# tool, based on CertEnroll lib.
This is how I create pfx for first, root CA certificate.
makecert -n "CN=Root CA" -cy authority -r -a sha256 -len 2048 -sv root.pvk root.cer
pvk2pfx -pvk root.pvk -spc root.cer -pfx root.pfx -pi 123 -po 123
To create certificate from C# code, I've referenced questions How to create self-signed certificate programmatically for WCF service? and C# Generate a non self signed client CX509Certificate Request without a CA using the certenroll.dll.
So far, I have following code. Method for certificate generation:
/// <summary>
/// Generates self-signed certificate with specified subject, which will expire after specified timespan.
/// </summary>
public X509Certificate2 CreateCertificate(string subjectName, TimeSpan expirationLength, X509Certificate2 issuer = null)
{
// create DN for subject and issuer
var dn = new CX500DistinguishedName();
dn.Encode("CN=" + subjectName);
var issuerName = new CX500DistinguishedName();
if(issuer != null)
{
issuerName.Encode(issuer.Subject);
}
var privateKey = new CX509PrivateKey
{
ProviderName = "Microsoft Strong Cryptographic Provider",
Length = 2048,
KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE,
KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_DECRYPT_FLAG |
X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG,
MachineContext = true,
ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG
};
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");
var cert = new CX509CertificateRequestCertificate();
cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
cert.Subject = dn;
if (issuer != null)
cert.Issuer = issuerName;
else
cert.Issuer = dn;
cert.NotBefore = DateTime.Now.Date;
cert.NotAfter = cert.NotBefore + expirationLength;
cert.HashAlgorithm = hashobj; // Specify the hashing algorithm
if(issuer != null)
{
var signerCertificate = new CSignerCertificate();
signerCertificate.Initialize(true, X509PrivateKeyVerify.VerifyAllowUI, EncodingType.XCN_CRYPT_STRING_HEX, issuer.GetRawCertDataString());
cert.SignerCertificate = signerCertificate;
}
cert.Encode();
// Do the final enrollment process
var enroll = new CX509Enrollment();
enroll.InitializeFromRequest(cert); // load the certificate
enroll.CertificateFriendlyName = subjectName; // 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, ""); // no password
// output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
var base64encoded = enroll.CreatePFX("", // no password, this is for internal consumption
PFXExportOptions.PFXExportChainWithRoot);
// instantiate the target class with the PKCS#12 data (and the empty password)
return new X509Certificate2(Convert.FromBase64String(base64encoded), "",
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
}
Simple application to find existing certificate and create new one based on it:
static void Main(string[] args)
{
var certificateGenerator = new CertificateGenerator();
X509Certificate2 rootCA;
using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadWrite);
rootCA = store.Certificates.OfType<X509Certificate2>()
.FirstOrDefault(c => c.Subject.StartsWith("CN=Root CA", StringComparison.Ordinal));
store.Close();
}
if (rootCA == null)
throw new Exception("Can't find root CA certificate");
var testCert = certificateGenerator.CreateCertificate("Test", new TimeSpan(3650, 0, 0, 0), rootCA);
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadWrite);
store.Add(testCert);
store.Close();
}
}
The thing is, that it works great, if I try to reference certificate not in Trusted Root Certificate Authorities, but in Personal (even if I have password on certificate). But if I try to create certificate based on CA certificate from Trusted Root Certificate Authorities, I receive exception on signerCertificate.Initialize, saying
Cannot find object or property. 0x80092004 (-2146885628 CRYPT_E_NOT_FOUND)
So, what am I missing?
ISignerCertificate::Initialize requires that the private key be bound via the Requests or My store:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa376832(v=vs.85).aspx:
If a private key is needed, only the personal and request stores are
searched.
If a private key is not needed, the root and intermediate CA
stores are also searched.
Windows expects that you only put the public portion of the CA into the CA (intermediate) or Root/ThirdPartyRoot stores, and that if you're the issuer you'll ALSO have it installed (with the private key now) into CurrentUser\My or LocalMachine\My.

How to create self-signed certificate programmatically for WCF service?

I have a self-hosted WCF server running as a Windows service under the Local System account. I am trying to create a self-signed certificate programmatically in c# for use with a net.tcp endpoint using Message level security.
I am using the following code which is very closely based on the accepted answer in How to create a self-signed certificate using C#? with some small changes trying to solve my problem.
public static X509Certificate2 CreateSelfSignedCertificate(string subjectName, TimeSpan expirationLength)
{
// create DN for subject and issuer
var dn = new CX500DistinguishedName();
dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
CX509PrivateKey privateKey = new CX509PrivateKey();
privateKey.ProviderName = "Microsoft Strong Cryptographic Provider";
privateKey.Length = 1024;
privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE;
privateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_DECRYPT_FLAG | X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG;
privateKey.MachineContext = true;
privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
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, "SHA1");
// Create the self signing request
var cert = new CX509CertificateRequestCertificate();
cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
cert.Subject = dn;
cert.Issuer = dn; // the issuer and the subject are the same
cert.NotBefore = DateTime.Now.Date;
// this cert expires immediately. Change to whatever makes sense for you
cert.NotAfter = cert.NotBefore + expirationLength;
//cert.X509Extensions.Add((CX509Extension)eku); // add the EKU
cert.HashAlgorithm = hashobj; // Specify the hashing algorithm
cert.Encode(); // encode the certificate
// Do the final enrollment process
var enroll = new CX509Enrollment();
enroll.InitializeFromRequest(cert); // load the certificate
enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name
string 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, ""); // no password
// output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
var base64encoded = enroll.CreatePFX("", // no password, this is for internal consumption
PFXExportOptions.PFXExportChainWithRoot);
// instantiate the target class with the PKCS#12 data (and the empty password)
return new System.Security.Cryptography.X509Certificates.X509Certificate2(
System.Convert.FromBase64String(base64encoded), "",
// mark the private key as exportable (this is usually what you want to do)
// mark private key to go into the Machine store instead of the current users store
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet
);
}
And I store it with this code:
X509Store store = new X509Store(storeName, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
store.Add(newCert);
store.Close();
This creates the certificate and puts it in the LocalMachine certificate store. The problem is that when I try to start the WCF service I get the following exception:
It is likely that certificate 'CN=myCertificate' may not have a private key that is capable of key exchange or the process may not have access rights for the private key. Please see inner exception for detail.
Inner exception: Keyset does not exist
The output of the FindPrivateKey sample (http://msdn.microsoft.com/en-us/library/aa717039%28v=vs.100%29.aspx) for my certificate is:
Private key directory:
C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys
Private key file name:
f0d47c7826b8ef5148b6d412f1c40024_4a8a026f-58e4-40f7-b779-3ae9b6aae1a7
I can see this 1.43KB file in explorer. If I look at the properties|Security I see SYSTEM and Administrators both with Full control.
In researching this error I have seen many answers about the private key missing or incorrect permissions. I can't see what the problem is.
The really strange thing is that if I use the mmc Certificate plugin, go to the certificate and choose All Tasks|Manage Private Keys... I see the same security settings. After viewing this even if I just bring up the dialog and hit the Cancel button the certificate now works correctly in WCF. I can simply restart the service and everything runs perfectly.
If I create a certificate using MakeCert it works just fine from the start. I don't know what it does differently.
One other piece of information that may not be relevant is that the certificate not only gets put in the My store where I told it to get put, but it also gets put in the "Intermediate Certification Authorities" store. I don't know why or if it matters.
So...any ideas what I am doing wrong?
UPDATE: Well, this is not just a WCF issue. I essentially get the same problem when I try to use the certificate to bind to an endpoint with http.sys using HttpSetServiceConfiguration. The method returns 1312 - "A specified logon session does not exist. It may already have been terminated". This is actually not the real error. I saw in the Security Event log an Audit Failure that say this:
Cryptographic Parameters:
Provider Name: Microsoft Software Key Storage Provider
Algorithm Name: Not Available.
Key Name: {A23712D0-9A7B-4377-89DB-B1B39E3DA8B5}
Key Type: Machine key.
Cryptographic Operation:
Operation: Open Key.
Return Code: 0x80090011
0x80090011 is Object was not found. So this appears to be the same problem. Again, after I open the Manage Private Keys dialog for the certificate this works perfectly also.
I am still looking for the cause of the problem.
UPDATE #2: I was able to get this working using the accepted answer below. Interestingly, this code now seems to put the certificate in the Machine store without calling the X509Store code. I still call the code because I am not sure and it does not hurt anything. Here is the final code that I am using to create the certificate.
static public X509Certificate2 CreateSelfSignedCertificate(string subjectName, TimeSpan expirationLength)
{
// create DN for subject and issuer
var dn = new CX500DistinguishedName();
dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
CX509PrivateKey privateKey = new CX509PrivateKey();
privateKey.ProviderName = "Microsoft Strong Cryptographic Provider";
privateKey.Length = 2048;
privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE;
privateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_DECRYPT_FLAG | X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_KEY_AGREEMENT_FLAG;
privateKey.MachineContext = true;
privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
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");
// Create the self signing request
var cert = new CX509CertificateRequestCertificate();
cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, "");
cert.Subject = dn;
cert.Issuer = dn; // the issuer and the subject are the same
cert.NotBefore = DateTime.Now.Date;
// this cert expires immediately. Change to whatever makes sense for you
cert.NotAfter = cert.NotBefore + expirationLength;
cert.HashAlgorithm = hashobj; // Specify the hashing algorithm
cert.Encode(); // encode the certificate
// Do the final enrollment process
var enroll = new CX509Enrollment();
enroll.InitializeFromRequest(cert); // load the certificate
enroll.CertificateFriendlyName = subjectName; // Optional: add a friendly name
string 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, ""); // no password
// output a base64 encoded PKCS#12 so we can import it back to the .Net security classes
var base64encoded = enroll.CreatePFX("", // no password, this is for internal consumption
PFXExportOptions.PFXExportChainWithRoot);
// instantiate the target class with the PKCS#12 data (and the empty password)
return new System.Security.Cryptography.X509Certificates.X509Certificate2(
System.Convert.FromBase64String(base64encoded), "",
// mark the private key as exportable (this is usually what you want to do)
// mark private key to go into the Machine store instead of the current users store
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet
);
}
I had the same issue using the equivalent code in PowerShell. It appears that sometime the private key just disappears. I used Process Monitor and you can see the key file being deleted.
The way I solved this was to add X509KeyStorageFlags.PersistKeySet to the X509Certificate2 constructor.
I could not make this work, but I found an alternate solution. (Update December 2014: I have now gotten it to work using the accepted answer.)
I was able to use the PluralSight.Crypto library to achieve what I need. I had to modify the source code slightly to get the private key to store in the LocalMachine store. The changes I made were to the file CryptContext.cs. I changed the CreateSelfSignedCertificate method. Following is a snippet of code including the change that I made. In essence, I set the Flags member of the CryptKeyProviderInformation structure to set it to 0x20 (CRYPT_MACHINE_KEYSET) if the CryptContext object contains this value in its Flags.
byte[] asnName = properties.Name.RawData;
GCHandle asnNameHandle = GCHandle.Alloc(asnName, GCHandleType.Pinned);
int flags = 0; // New code
if ((this.Flags & 0x20) == 0x20) // New code
flags = 0x20; // New code
var kpi = new Win32Native.CryptKeyProviderInformation
{
ContainerName = this.ContainerName,
KeySpec = (int)KeyType.Exchange,
ProviderType = 1, // default RSA Full provider
Flags = flags // New code
};
Then I use the function in my own code like this:
using (Pluralsight.Crypto.CryptContext ctx = new Pluralsight.Crypto.CryptContext()) {
ctx.Flags = 0x8 | 0x20;
ctx.Open();
X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
new Pluralsight.Crypto.SelfSignedCertProperties
{
IsPrivateKeyExportable = true,
KeyBitLength = 4096,
Name = new X500DistinguishedName("CN=" + subjectName),
ValidFrom = DateTime.Today,
ValidTo = DateTime.Today + expirationLength,
});
return cert;
}
Notice that I set the Flags for the CryptContext object to be 0x8 | 0x20 (CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET).
I wish I could figure out what was wrong with my original solution. But I need something to work and in my testing this solution does what I need. I hope it helps someone else along the way.
You can also use the CLR Security library on CodePlex (https://clrsecurity.codeplex.com/). Here is sample code which creates a self signed certificate, and tests it with SSLStream.
var machineName = Environment.MachineName;
var keyCreationParameters = new CngKeyCreationParameters();
keyCreationParameters.KeyUsage = CngKeyUsages.AllUsages;
keyCreationParameters.KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey;
keyCreationParameters.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(4096), CngPropertyOptions.None));
var cngKey = CngKey.Create(CngAlgorithm2.Rsa, "Test", keyCreationParameters);
var x500DistinguishedName = new X500DistinguishedName("CN=" + machineName);
x500DistinguishedName.Oid.Value = "1.3.6.1.5.5.7.3.1";
var certificateCreationParameters = new X509CertificateCreationParameters(x500DistinguishedName);
certificateCreationParameters.SignatureAlgorithm = X509CertificateSignatureAlgorithm.RsaSha512;
certificateCreationParameters.TakeOwnershipOfKey = true;
certificateCreationParameters.CertificateCreationOptions = X509CertificateCreationOptions.None;
certificateCreationParameters.EndTime = new DateTime(9999, 12,31, 23, 59, 59, 999, DateTimeKind.Utc);
var certificate = cngKey.CreateSelfSignedCertificate(certificateCreationParameters);
var certificateStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
certificateStore.Open(OpenFlags.ReadWrite);
certificateStore.Add(certificate);
certificateStore.Close();
var tcpListener = TcpListener.Create(6666);
tcpListener.Start();
var client = new TcpClient("localhost", 6666);
var acceptedClient = tcpListener.AcceptTcpClient();
var acceptedClinetSslStream = new SslStream(
acceptedClient.GetStream(), false);
var serverAuthTask = acceptedClinetSslStream.AuthenticateAsServerAsync(certificate,
false, SslProtocols.Tls, true);
SslStream clientSslStream = new SslStream(
client.GetStream(),
false,
delegate(object o, X509Certificate x509Certificate, X509Chain chain, SslPolicyErrors errors)
{
if (errors == SslPolicyErrors.None)
return true;
Console.WriteLine("Certificate error: {0}", errors);
// Do not allow this client to communicate with unauthenticated servers.
return false;
},
null);
var clientAuthTask = clientSslStream.AuthenticateAsClientAsync(machineName);
Task.WaitAll(serverAuthTask, clientAuthTask);

Categories