C#/。NET中的根证书固定

时间:2019-03-13 10:33:16

标签: c# .net certificate sslstream pinning

我想在我的C#应用​​程序中实现证书/公钥固定。我已经看到很多解决方案可以将服务器证书直接固定为例如在this question中。但是,为了更加灵活,我只想固定根证书。服务器在设置中获取的证书由中间的CA签名,中间的CA本身由根签名。

到目前为止,我实现的是一台服务器,该服务器从PKCS#12(.pfx)文件中加载自己的证书,私钥,中间证书和根证书。我使用以下命令创建了文件:

openssl pkcs12 -export -inkey privkey.pem -in server_cert.pem -certfile chain.pem -out outfile.pfx

chain.pem 文件包含根证书和中间证书。

服务器加载此证书,并希望针对客户端进行身份验证:

// certPath is the path to the .pfx file created before
var cert = new X509Certificate2(certPath, certPass)
var clientSocket = Socket.Accept();
var sslStream = new SslStream(
    new NetworkStream(clientSocket),
    false
);

try {
    sslStream.AuthenticateAsServer(cert, false, SslProtocols.Tls12, false);
} catch(Exception) {
     // Error during authentication
}

现在,客户端要对服务器进行身份验证:

public void Connect() {
    var con = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    con.Connect(new IPEndPoint(this.address, this.port));
    var sslStream = new SslStream(
        new NetworkStream(con),
        false,
        new RemoteCertificateValidationCallback(ValidateServerCertificate),
        null
    );
    sslStream.AuthenticateAsClient("serverCN");
}

public static bool ValidateServerCertificate(
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors
)
{
    // ??
}

现在的问题是服务器仅将自己的证书发送给客户端。此外, chain 参数不包含更多信息。 这在某种程度上是合理的,因为 X509Certificate2证书(在服务器代码中)仅包含服务器证书,而没有有关中间或根证书的信息。但是,由于(至少)缺少中间证书,客户端无法验证整个链。

到目前为止,我没有发现使.NET发送整个证书链的任何可能性,但是我不想固定服务器证书iself或中间证书,因为这破坏了根证书固定的灵活性。

因此,有谁知道让SslStream发送整个链进行身份验证或使用其他方法实现功能的可能性吗?还是我必须以其他方式打包证书?

谢谢!

编辑: 我进行了一些其他测试以发现问题。如评论中所建议,我创建了一个包含所有证书的X509Store。之后,我使用服务器的证书和商店构建了X509Chain。在服务器本身上,新链正确地包含了所有证书,但没有包含在ValidateServerCertificate函数中。.

1 个答案:

答案 0 :(得分:1)

SslStream将永远不会发送整个链(自颁发的证书除外)。约定是发送除根以外的所有内容,因为另一端已经拥有并信任根或不具有根(因此/或不信任根),并且任一种方式都浪费带宽。 / p>

但是SslStream仅在了解中间物时才能发送中间物。

var cert = new X509Certificate2(certPath, certPass);

这仅提取最终实体证书(带有私钥的证书),并丢弃PFX中的所有其他证书。如果要加载所有证书,则需要使用X509Certificate2Collection.Import。但是...那也没有帮助。 SslStream仅接受最终实体证书,它希望系统能够为其建立功能链。

为了构建功能链,您的中间证书和根证书必须位于以下任一位置:

  • 通过X509Chain.ChainPolicy.ExtraStore提供为手动输入
    • 由于所讨论的链条是由SslStream构建的,因此您不能在这里真正做到这一点。
  • CurrentUser \ My X509Store
  • * LocalMachine \ My X509Store
  • CurrentUser \ CA X509Store
  • ** LocalMachine \ CA X509Store
  • CurrentUser \ Root X509Store
  • ** LocalMachine \ Root X509Store
  • * LocalMachine \ ThirdPartyRoot X509Store
  • 在证书的授权访问标识符扩展中标识的http(非s)位置。

标记为*的存储在Linux的.NET Core中不存在。标记为**的存储在Linux上确实存在,但是不能由.NET应用程序修改。

这还不够,因为(至少对于Linux上的SslStream以及.NET Core上的macOS而言),它仍然仅在构建了可信链时才发送中间件。因此,服务器实际上需要信任根证书才能发送中间体。 (或者客户端需要信任客户端证书的根)


另一方面,适用相同的规则。区别在于在回调中,您可以选择重建链以添加额外的证书。

private static bool IsExpectedRootPin(X509Chain chain)
{
    X509Certificate2 lastCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
    return lastCert.RawBytes.SequenceEquals(s_pinnedRootBytes);
}

private static bool ValidateServerCertificate(
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors
)
{
    if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
    {
        // No cert, or name mismatch (or any future errors)
        return false;
    }

    if (IsExpectedRootPin(chain))
    {
        return true;
    }

    chain.ChainPolicy.ExtraStore.Add(s_intermediateCert);
    chain.ChainPolicy.ExtraStore.Add(s_pinnedRoot);
    chain.ChainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority;

    if (chain.Build(chain.ChainElements[0].Certificate))
    {
        return IsExpectedRootPin(chain);
    }

    return false;
}

当然,此方法的问题在于,您还需要了解并在远程端提供中间件。真正的解决方案是,中间体应该在HTTP分发端点上可用,并且颁发的证书应带有Authority Information Access扩展名,以便能够动态定位它们。