Самозаверяющий сертификат защищенного соединения WebSocket

Цель - это веб-приложение, которое обменивается информацией с приложением C#, которое установлено на компьютере пользователя. Клиентское приложение - это сервер веб-сокетов, а браузер - это клиент веб-сокетов.

В конце концов, клиент websocket в браузере пользователя постоянно создается через Angular, и приложение работает на компьютере и выполняет некоторые действия.

Используемая библиотека C# - WebSocket-Sharp. Клиент websocket - это обычный javascript.

Очевидно, что это соединение происходит только локально, поэтому клиент подключается к localhost. Поскольку веб-сайт защищен через HTTPS, веб-сокет также должен быть защищен. Для этого приложение C# создает сертификат при запуске (на самом деле это только для целей тестирования).

Соединение не работает, потому что сертификат не заслуживает доверия. Все проверки сервера для клиента отключены, но соединение не будет установлено.

Это та часть, где создается сервер

_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();

Вот как сертификат создается с помощью 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;

}

Это поведение логично, хотя и странно, поскольку проверка сертификата не должна выполняться локально. Есть ли возможность обойти эту проблему? Я уже думал об установке сертификата эмитента в доверенные сертификаты, но это не оптимальное решение.

2 ответа

Вы пробовали какие-либо ответы на этот вопрос?

Подводя итог, похоже, что есть несколько вариантов, которые вы можете попробовать:

  • Запустите Chrome с помощью --ignore-certificate-errors аргумент указан.

  • Запустите HTTP-сервер на том же порту, который принимает тот же самозаверяющий сертификат, перейдите к нему и примите сертификат, после чего вы сможете использовать соединение WebSocket.

  • Установите опцию конфигурации в Firefox network.websocket.allowInsecureFromHTTPS в trueзатем используйте ws:// а не wss:// адрес.

Если все это для тестирования, и у вас есть возможность контролировать подобные вещи, то я думаю, что один или несколько из них должны работать. Если вам нужен стандартный конечный пользователь, чтобы сделать это, я думаю, вам нужно другое решение. Как вы обнаружили, не имеет значения, если вы настроите сервер, чтобы не заботиться о сертификате, клиент должен в конечном итоге решить, хочет ли он принять сертификат или он не примет соединение.

@Kdawg ответы верны.

Вы не надеетесь, что клиентские браузеры примут небезопасное соединение только с настройкой на стороне сервера. Все действия по принятию неподписанного (или самоподписанного) сертификата находятся на стороне клиента.

В дополнение к ответу @ Kdawg я хотел бы добавить, что в сетях Windows наиболее распространенной практикой для частных организаций является:

  1. Назначьте Windows Server в качестве центра сертификации

  2. Добавьте общедоступный корневой сертификат центра сертификации на хосты Windows (с помощью объекта групповой политики) или вручную

  3. Подпишите заказной сертификат с сервером Windows CA

Звучит больно, и это так.

На вашем месте я бы сделал стандартный публично подписанный сертификат и запускал бы SSL до тех пор, пока это не будет сделано.

Посмотрите на Let's Encrypt для бесплатных сертификатов SSL для вашего домена.

Моим окончательным решением было создать действительный сертификат для поддомена, а затем изменить записи A/AAAA на localhost. Таким образом, соединение будет доверенным через HTTPS.

Другие вопросы по тегам