C#/.NET 中的根证书固定
Root Certificate Pinning in C#/.NET
我想在我的 C# 应用程序中实现 certificate/public 键固定。我已经看到很多直接固定服务器证书的解决方案,例如在 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 cert(在服务器代码中)仅包含服务器证书,不包含有关中间证书或根证书的信息。但是,客户端无法验证整个链,因为(至少)缺少中间证书。
到目前为止,我没有发现任何让 .NET 发送整个证书链的可能性,但我不想自己固定服务器证书或中间证书,因为这会破坏根证书固定的灵活性。
因此,有谁知道可以让 SslStream 发送整个链进行身份验证或使用其他方法实现功能?还是我必须以不同方式打包证书?
谢谢!
编辑:
我做了一些其他测试来检测问题。正如评论中所建议的,我创建了一个包含所有证书的 X509Store
。之后,我使用服务器的证书和商店构建了 X509Chain
。在服务器本身上,新链正确包含所有证书,但不在 ValidateServerCertificate
函数中..
SslStream 永远不会发送整个链(自颁发的证书除外)。惯例是发送除 root 之外的所有内容,因为另一方要么已经拥有并信任 root,要么没有(thus/or 不信任 root),无论哪种方式都是对带宽的浪费.
但是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 分发端点上可用,并且颁发的证书应该带有授权信息访问扩展,以便能够动态定位它们。
我想在我的 C# 应用程序中实现 certificate/public 键固定。我已经看到很多直接固定服务器证书的解决方案,例如在 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 cert(在服务器代码中)仅包含服务器证书,不包含有关中间证书或根证书的信息。但是,客户端无法验证整个链,因为(至少)缺少中间证书。
到目前为止,我没有发现任何让 .NET 发送整个证书链的可能性,但我不想自己固定服务器证书或中间证书,因为这会破坏根证书固定的灵活性。
因此,有谁知道可以让 SslStream 发送整个链进行身份验证或使用其他方法实现功能?还是我必须以不同方式打包证书?
谢谢!
编辑:
我做了一些其他测试来检测问题。正如评论中所建议的,我创建了一个包含所有证书的 X509Store
。之后,我使用服务器的证书和商店构建了 X509Chain
。在服务器本身上,新链正确包含所有证书,但不在 ValidateServerCertificate
函数中..
SslStream 永远不会发送整个链(自颁发的证书除外)。惯例是发送除 root 之外的所有内容,因为另一方要么已经拥有并信任 root,要么没有(thus/or 不信任 root),无论哪种方式都是对带宽的浪费.
但是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 分发端点上可用,并且颁发的证书应该带有授权信息访问扩展,以便能够动态定位它们。