WebSocket secure connection self signed certificate
Asked Answered
B

3

8

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.

Bagworm answered 13/7, 2017 at 19:24 Comment(3)
the cert check shouldn't be performed locally Huh?Berenice
@Berenice I mean the cert shouldn't be forced when the connection is made with localhostBagworm
A simple approach for secure localhost connections is to route a subdomain to localhost and then optain a ssl cert for this domain. Works like a charm.Bagworm
B
1

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.

Bagworm answered 6/8, 2020 at 7:31 Comment(0)
M
6

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.

Mantooth answered 20/7, 2017 at 0:58 Comment(7)
Do you have a suggestion for an alternative solution for this problem? I thought about WebRTC but there is no good implementation for C#. Are there other options that could allow a structure like this: img.klemm.one/EZRYx? A solution could be swapping the server and the client so that the browser is the server. But I'm unsure whether this is possible.Bagworm
I'm not an expert in this stuff, but I think you might have dismissed @Fabien's answer too quickly. Because Let's Encrypt is a trusted CA (letsencrypt.org/2015/10/19/lets-encrypt-is-trusted.html), you wouldn't need to add the cert to the trusted certs, because it's already trusted. I think you should then just be able to use https/wss without doing anything extra.Mantooth
I've never dismissed his answer. The core problem is that the connection of the websocket always happens on localhost. It is not possible to get a cert for localhost. Localhost is not a testing thing here it is intended to be the final solution. The web application should be able to communicate with a program running on the client's pc.Bagworm
@chris579 Good point. You mentioned a "just for testing purposes" in your question, so I guess I assumed that you were going to have a different way of doing this in production. One idea: if your client application gets installed, during install time, you could generate the self-signed cert and add it to the trusted certificates programmatically.Mantooth
Already thought about adding such a certificate. Wouldn't it be a bit risky because this certificate would apply to all localhost ports? And how can I programmatically add such a certificate?Bagworm
@chris579 Perhaps add an obviously fake domain in the hosts file and create the certificate for that domain? See #567070 for information on installing the certificate in C#.Mantooth
this should be also possible for localhost certificates, isn't it?Bagworm
H
4

@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:

  1. Assign a Windows Server to act as Certificate Authority

  2. Add the Certificate Authority's public root certificate into Windows hosts (either by GPO) or manually

  3. 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.

Haematocele answered 21/7, 2017 at 18:54 Comment(1)
The problem itself is not the missing SSL cert for the domain it's rather the structure of communication that should be achieved. This is how it should be: img.klemm.one/EZRYx. Turning off SSL is not an option and adding the cert to the trusted certs is not an option too. Are there alternatives to websockets that can be used for this? I thought about using raw tcp but client side javascript is not capable of this.Bagworm
B
1

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.

Bagworm answered 6/8, 2020 at 7:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.