支持 Https 的 Localhost HttpListener - 一段时间后停止工作

Localhost HttpListener with Https support - stops working after a period of time

我已经使用自签名证书设置了一个测试 C# Https 侦听器(遵循 中的建议)。

该服务(在本地系统下作为 windows 服务运行)和 最初(启动后)运行良好。 在一段时间(~1.5 小时)后,对 https 端点的调用停止工作,Edge/IE 抱怨:

无法安全连接到此页面 这可能是因为该网站使用了过时或不安全的 TLS 安全设置。如果这种情况持续发生,请尝试联系网站所有者。

Chrome投诉如下:

无法访问此站点 连接被重置。 ERR_CONNECTION_RESET

发生这种情况时,检查证书存储会显示证书(在 Root 和 My 存储中)仍然存在。

正在检查

netsh http show sslcert

也说明证书到端口的注册还在。

重新启动应用程序(重新创建、重新安装证书并将其重新绑定到 C# http(s) 侦听器侦听的端口)会有所帮助,但直到下一次可能会在一段时间内发生的小问题 (~ 1.5..2 小时?).

我知道侦听器线程仍然存在,因为不安全端口上的请求仍然有效。

在此期间发生了一些事情,我不知道是什么...

代码:

一切都从生成自签名证书开始:

        // create DN for subject and issuer
        var dn = new CX500DistinguishedName();
        dn.Encode("CN=localhost");

        // create a new private key for the certificate
        var privateKey = new CX509PrivateKey
        {
            ProviderName = "Microsoft Base Cryptographic Provider v1.0",
            MachineContext = false,
            Length = 2048,
            KeySpec = X509KeySpec.XCN_AT_SIGNATURE,
            KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_SIGNING_FLAG,
            FriendlyName = "Application Testing Key",
            ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG
        };
        privateKey.Create();

        var hashobj = new CObjectId();
        hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
            ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
            AlgorithmFlags.AlgorithmFlagsNone, "SHA256");

        // Create the self signing request
        var certificateRequest = new CX509CertificateRequestCertificate();
        certificateRequest.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser, privateKey, String.Empty);
        certificateRequest.Subject = dn;
        certificateRequest.Issuer = dn; // the issuer and the subject are the same
        certificateRequest.NotBefore = DateTime.UtcNow.AddDays(-1);
        certificateRequest.NotAfter = DateTime.UtcNow.AddYears(10);
        certificateRequest.HashAlgorithm = hashobj;

        // Set up the Subject Alternative Names extension.
        var nameslist = new CAlternativeNames();
        var alternativeName = new CAlternativeName();
        alternativeName.InitializeFromString(AlternativeNameType.XCN_CERT_ALT_NAME_DNS_NAME, "localhost");
        nameslist.Add(alternativeName);
        var subjectAlternativeNamesExtension = new CX509ExtensionAlternativeNames();
        subjectAlternativeNamesExtension.InitializeEncode(nameslist);
        certificateRequest.X509Extensions.Add((CX509Extension)subjectAlternativeNamesExtension);

        var skiExtension = new CX509ExtensionSubjectKeyIdentifier();
        skiExtension.InitializeEncode(EncodingType.XCN_CRYPT_STRING_BASE64, Convert.ToBase64String(StringToByteArray(certSKI)));
        certificateRequest.X509Extensions.Add((CX509Extension)skiExtension);

        certificateRequest.Encode();

        // Do the final enrollment process
        var enroll = new CX509Enrollment();
        enroll.InitializeFromRequest(certificateRequest); // load the certificate
        enroll.CertificateFriendlyName = "Application Testing Cert";
        var csr = enroll.CreateRequest(); // Output the request in base64
        var pwd = Guid.NewGuid().ToString();
        enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate, csr, EncodingType.XCN_CRYPT_STRING_BASE64, pwd); // and install it back as the response
        var base64encoded = enroll.CreatePFX(pwd, PFXExportOptions.PFXExportChainWithRoot);

        // instantiate the target class with the PKCS#12 data 
        return new X509Certificate2(Convert.FromBase64String(base64encoded), pwd);

然后将新生成的证书添加到本地根目录和私有存储:

InstallCertificateToCertStore(cert, new X509Store(StoreName.Root, StoreLocation.LocalMachine));
InstallCertificateToCertStore(cert, new X509Store(StoreName.My, StoreLocation.LocalMachine));

然后将新创建和安装的证书注册到 Http Listener 侦听的端口:

netsh.exe http add sslcert ipport=0.0.0.0:{port} certhash={certThumbprint} appid={appid_guid} 

我遇到的问题如下:

当您在 .NET 中创建自签名证书时,您构建了一个 X509Certificate(2) 的实例。

如果您不指定

X509KeyStorageFlags.PersistKeySet

flag,那么在对您的证书的所有引用都超出我们的范围后,与您的证书对应的私钥最终将被垃圾收集删除(因此问题不是立即重现,而是在一段时间后重现)。

要保留证书的私钥,您需要如上所述指定标志。

这是微软在这里提到的: https://support.microsoft.com/en-us/help/950090/installing-a-pfx-file-using-x509certificate-from-a-standard--net-appli

自签名证书创建的最终工作代码如下:

        // create DN for subject and issuer
        var dn = new CX500DistinguishedName();
        dn.Encode("CN=localhost");

        // create a new private key for the certificate
        var privateKey = new CX509PrivateKey
        {
            ProviderName = "Microsoft Base Cryptographic Provider v1.0",
            MachineContext = true,
            Length = 2048,
            KeySpec = X509KeySpec.XCN_AT_SIGNATURE,
            KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES,
            FriendlyName = "App Testing Key",
            ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG
        };
        privateKey.Create();

        var hashobj = new CObjectId();
        hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
            ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
            AlgorithmFlags.AlgorithmFlagsNone, "SHA256");

        // Create the self signing request
        // also see: https://security.stackexchange.com/a/103362
        var certificateRequest = new CX509CertificateRequestCertificate();
        certificateRequest.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, String.Empty);
        certificateRequest.Subject = dn;
        certificateRequest.Issuer = dn; // the issuer and the subject are the same
        certificateRequest.NotBefore = DateTime.UtcNow.AddDays(-1);
        certificateRequest.NotAfter = DateTime.UtcNow.AddYears(10);
        certificateRequest.HashAlgorithm = hashobj;

        // Set up the Subject Alternative Names extension.
        var nameslist = new CAlternativeNames();
        var alternativeName = new CAlternativeName();
        alternativeName.InitializeFromString(AlternativeNameType.XCN_CERT_ALT_NAME_DNS_NAME, "localhost");
        nameslist.Add(alternativeName);
        var subjectAlternativeNamesExtension = new CX509ExtensionAlternativeNames();
        subjectAlternativeNamesExtension.InitializeEncode(nameslist);
        certificateRequest.X509Extensions.Add((CX509Extension)subjectAlternativeNamesExtension);

        var skiExtension = new CX509ExtensionSubjectKeyIdentifier();
        skiExtension.InitializeEncode(EncodingType.XCN_CRYPT_STRING_BASE64, Convert.ToBase64String(StringToByteArray(certSKI)));
        certificateRequest.X509Extensions.Add((CX509Extension)skiExtension);

        certificateRequest.Encode();

        // Do the final enrollment process
        var enroll = new CX509Enrollment();
        enroll.InitializeFromRequest(certificateRequest); // load the certificate
        enroll.CertificateFriendlyName = "App Testing Cert";
        var csr = enroll.CreateRequest(); // Output the request in base64
        var pwd = Guid.NewGuid().ToString();
        enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate, csr, EncodingType.XCN_CRYPT_STRING_BASE64, pwd); // and install it back as the response
        var base64encoded = enroll.CreatePFX(pwd, PFXExportOptions.PFXExportChainWithRoot);

        // instantiate the target class with the PKCS#12 data 
        return new X509Certificate2(Convert.FromBase64String(base64encoded), pwd, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);