【问题标题】:Root Certificate Pinning in C#/.NETC#/.NET 中的根证书固定
【发布时间】:2019-08-04 00:04:11
【问题描述】:

我想在我的 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 发送整个证书链的可能性,但我不想自己固定服务器证书或中间证书,因为这破坏了根证书固定的灵活性。

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

谢谢!

编辑: 我做了一些其他的测试来检测这个问题。正如 cmets 中所建议的,我创建了一个包含所有证书的 X509Store。之后,我使用我的服务器证书和存储构建了一个X509Chain。在服务器本身上,新链正确包含所有证书,但不在 ValidateServerCertificate 函数中..

【问题讨论】:

  • X509Chain 包含与服务器证书关联的证书颁发机构链,您可以在RemoteCertificateValidationCallback 中看到。但是您使用的是自己的 pfx 证书,请尝试先枚举它并导入服务器存储。还有一些文章stackoverflow.com/questions/9141198/…stackoverflow.com/questions/5036590/…
  • 我将文件中的所有证书添加到服务器存储中。但是,我不太明白客户端的链现在应该如何访问整个链。你能在这里更明确一点吗?
  • ChainElements chain 对象在您的 ValidateServerCertificate 方法中的属性应该具有完整的链
  • 我知道,但ChainElements 在客户端上只包含一个元素。也许我创建 PKCS#12 文件的方式有问题?你知道验证的方法吗?
  • 我没有自己创建 pfx 文件,只是使用现有的。根据这篇文章stackoverflow.com/questions/6307886/…,您应该在openssl 命令中包含根 CA 和中间证书

标签: c# .net certificate sslstream pinning


【解决方案1】:

SslStream 永远不会发送整个链(自我颁发的证书除外)。约定是发送除根以外的所有内容,因为另一方要么已经拥有并信任根,要么没有(因此/或不信任根),无论哪种方式都是浪费带宽。

但是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 (not-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 分发端点上可用,并且颁发的证书应该带有授权信息访问扩展,以便能够动态定位它们。

【讨论】:

  • 谢谢,这差点把我逼疯了。我使用一个集合将证书加载到证书的新存储中,而不是系统定义的存储中......因此,解决方案是@pavel-anikhouski 的评论和这个答案的组合。首先,我们必须使用X509Certificate2Collection 将证书加载到System-Defined 存储中,然后使用证书进行身份验证。因此,客户端将能够查看/验证整个链。
猜你喜欢
  • 1970-01-01
  • 2018-10-28
  • 2018-03-28
  • 1970-01-01
  • 1970-01-01
  • 2012-01-31
  • 2017-02-14
  • 2019-10-08
  • 1970-01-01
相关资源
最近更新 更多