我想在我的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
函数中。.
答案 0 :(得分:1)
SslStream将永远不会发送整个链(自颁发的证书除外)。约定是发送除根以外的所有内容,因为另一端已经拥有并信任根或不具有根(因此/或不信任根),并且任一种方式都浪费带宽。 / p>
但是SslStream仅在了解中间物时才能发送中间物。
var cert = new X509Certificate2(certPath, certPass);
这仅提取最终实体证书(带有私钥的证书),并丢弃PFX中的所有其他证书。如果要加载所有证书,则需要使用X509Certificate2Collection.Import
。但是...那也没有帮助。 SslStream仅接受最终实体证书,它希望系统能够为其建立功能链。
为了构建功能链,您的中间证书和根证书必须位于以下任一位置:
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扩展名,以便能够动态定位它们。