Я хочу реализовать привязку сертификата/открытого ключа в своем приложении С#. Я уже видел много решений, которые напрямую закрепляют сертификат сервера, например. в этот вопрос. Однако, чтобы быть более гибким, я хочу закрепить только корневой сертификат. Сертификат, который сервер получает при настройке, подписан промежуточным ЦС, который сам подписан корнем.
До сих пор я реализовал сервер, который загружает свой собственный сертификат, закрытый ключ, промежуточный сертификат и корневой сертификат из файла PKCS#12 (.pfx). Я создал файл с помощью следующей команды:
openssl pkcs12 -export -inkey privkey.pem -in server_cert.pem -certfile chain.pem -out outfile.pfx
Файл цепь.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..
Я добавил все сертификаты в файл в хранилище сервера. Однако я не совсем понимаю, как теперь цепочка на стороне клиента должна иметь доступ к полной цепочке. Не могли бы вы быть более явным здесь?
ChainElements свойство chain объекта в вашем ValidateServerCertificate методе должно иметь полную цепочку
Я знаю, но ChainElements содержит только один элемент на клиенте. Может быть, что-то не так с тем, как я создаю файл PKCS#12? Вы знаете способ проверить это.
Я не создавал файлы pfx самостоятельно, просто использую существующие. Согласно этой статье stackoverflow.com/questions/6307886/…, вы должны включить корневой ЦС и промежуточные сертификаты в команду openssl.
Спасибо за ссылку. Однако я не могу указать несколько параметров -in, так как openssl будет смотреть только на последний. В опубликованной команде openssl, которую я использовал для создания pfx-файла, я указал дополнительные сертификаты с помощью параметра -certfile. X509Certificate2Collection также может прочитать все три из них, но они не содержатся в цепочке на стороне клиента. Я добавил дополнительную информацию о тестах, которые я сделал в исходный пост, возможно, это будет полезно.





SslStream никогда не будет отправлять всю цепочку (за исключением самовыданных сертификатов). Соглашение состоит в том, чтобы отправлять все, кроме корня, потому что другая сторона либо уже имеет корень и доверяет ему, либо не имеет (таким образом/или не доверяет корню), и в любом случае это была пустая трата полосы пропускания.
Но SslStream может отправлять промежуточные сообщения только тогда, когда он их понимает.
var cert = new X509Certificate2(certPath, certPass);
При этом извлекается только сертификат конечного объекта (с закрытым ключом), любые другие сертификаты в PFX отбрасываются. Если вы хотите загрузить все необходимые сертификаты, используйте X509Certificate2Collection.Import. Но... это тебе тоже не поможет. SslStream принимает только сертификат конечного объекта, он ожидает, что система сможет построить для него функционирующую цепочку.
Чтобы построить функционирующую цепочку, ваши промежуточные и корневые сертификаты должны принадлежать одному из следующих типов:
http (не-s), указанное в расширении идентификатора доступа к центру сертификации в сертификате.Магазины, отмеченные *, не существуют в .NET Core в Linux. Магазины, отмеченные **, существуют в Linux, но не могут быть изменены приложением .NET.
Этого также недостаточно для довольно, потому что (по крайней мере, для SslStream в Linux и, возможно, macOS в .NET Core) он по-прежнему отправляет промежуточные звенья только в том случае, если он построил цепочку, которой доверяет. Таким образом, сервер должен фактически доверять корневому сертификату, чтобы отправлять промежуточные звенья. (Или клиент должен доверять корню для клиентского сертификата)
С другой стороны действуют те же правила. Разница в том, что в обратном вызове вы можете перестроить цепочку, чтобы добавить дополнительные сертификаты.
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, а затем использовать сертификат для аутентификации. В результате клиент сможет увидеть/проверить всю цепочку.
X509Chainсодержит цепочку центров сертификации, связанных с сертификатом сервера, вы можете видеть это вRemoteCertificateValidationCallback. Но вы используете свой собственный сертификат pfx, попробуйте сначала перечислить его и импортировать в хранилище сервера. Есть статьи stackoverflow.com/questions/9141198/… и stackoverflow.com/questions/5036590/…