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

C#/.NET中的根证书固定,c#,.net,certificate,sslstream,pinning,C#,.net,Certificate,Sslstream,Pinning,我想在我的C#应用程序中实现证书/公钥固定。我已经看到许多解决方案直接将服务器的证书固定为,例如,在中。但是,为了更灵活,我只想锁定根证书。服务器在安装中获得的证书由中间CA签名,中间CA本身由根用户签名 到目前为止,我实现的是一个服务器,它从PKCS#12(.pfx)文件加载自己的证书、私钥、中间证书和根证书。我使用以下命令创建了该文件: openssl pkcs12 -export -inkey privkey.pem -in server_cert.pem -certfile chain.

我想在我的C#应用程序中实现证书/公钥固定。我已经看到许多解决方案直接将服务器的证书固定为,例如,在中。但是,为了更灵活,我只想锁定根证书。服务器在安装中获得的证书由中间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
)
{
    // ??
}
现在的问题是服务器只向客户端发送自己的证书。此外,“链”参数不包含更多信息。 这在某种程度上是合理的,因为X509Certificate2证书(在服务器代码中)只包含服务器证书,而没有关于中间证书或根证书的信息。但是,客户端无法验证整个链,因为(至少)缺少中间证书

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

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

谢谢

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

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

X509Chain
包含与服务器证书关联的证书权限链,您可以在
RemoteCertificateValidationCallback
中看到这一点。但您正在使用自己的pfx证书,请尝试先枚举它,然后导入到服务器存储区。有一些文章,我将文件中的所有证书都添加到了服务器存储区。但是,我不太明白客户端的链现在应该如何访问整个链。这里可以更明确一点吗?
ChainElements
方法中
chain
对象的
property应该具有完整的chain我知道,但是
ChainElements
在客户端上只包含一个元素。我创建PKCS#12文件的方式可能有问题?你知道一种验证的方法吗?我没有自己创建pfx文件,只是使用现有的文件。根据本文,您应该将根CA和中间证书包含到
openssl
命令中。谢谢,这几乎让我发疯。我使用集合将证书加载到证书的新存储中,而不是系统定义的存储。。。因此,解决方案是@pavel anikhouski的评论和这个答案的结合。首先,我们必须将证书加载到系统定义的stor中