Закрепление корневого сертификата в C # /. NET

Я хочу реализовать закрепление сертификата / открытого ключа в моем приложении C #. Я уже видел много решений, которые прикрепляют сертификат сервера напрямую, например, в этом вопросе. Однако для большей гибкости я хочу закрепить только корневой сертификат. Сертификат, который сервер получает в настройке, подписан промежуточным ЦС, который сам подписан корнем.

До сих пор я реализовал сервер, который загружает свой собственный сертификат, закрытый ключ, промежуточный сертификат и корневой сертификат из файла 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 отправлять всю цепочку сертификатов, но я не хочу закреплять сертификат сервера iself или промежуточный, поскольку это разрушает гибкость закрепления корневого сертификата.

Следовательно, знает ли кто-нибудь возможность заставить SslStream отправлять всю цепочку для аутентификации или реализовать функциональность с использованием другого подхода? Или надо сертификаты по-другому упаковывать?

Спасибо!

Изменить: я сделал несколько других тестов, чтобы обнаружить проблему. Как было предложено в комментариях, я создал X509Store, содержащий все сертификаты. После этого я построил X509Chain, используя сертификат моего сервера и хранилище. На самом сервере новая цепочка правильно содержит все сертификаты, но не в функции ValidateServerCertificate ..


person PraMiD    schedule 13.03.2019    source источник
comment
X509Chain содержит цепочку центров сертификации, связанных с сертификатом сервера, вы можете увидеть это в RemoteCertificateValidationCallback. Но вы используете свой собственный сертификат pfx, попробуйте сначала перечислить его и импортировать в хранилище сервера. Есть несколько статей с заголовком stackoverflow.com/questions/9141198/ и stackoverflow.com/questions/5036590/   -  person Pavel Anikhouski    schedule 13.03.2019
comment
Я добавил все сертификаты из файла в хранилище сервера. Однако я не совсем понимаю, как цепочка на стороне клиента должна теперь иметь доступ к полной цепочке. Не могли бы вы быть здесь более откровенным?   -  person PraMiD    schedule 13.03.2019
comment
ChainElements свойство объекта chain в вашем ValidateServerCertificate методе должно иметь полную цепочку   -  person Pavel Anikhouski    schedule 13.03.2019
comment
Я знаю, но ChainElements содержит только один элемент на клиенте. Может быть, что-то не так с тем, как я создаю файл PKCS # 12? Вы знаете способ подтвердить это.   -  person PraMiD    schedule 13.03.2019
comment
Я не создавал файлы pfx самостоятельно, просто использую существующие. Согласно этой статье stackoverflow.com/questions/6307886/, вы должны включить корневой ЦС и промежуточные сертификаты в команду openssl   -  person Pavel Anikhouski    schedule 13.03.2019
comment
Спасибо за ссылку. Однако я не могу указать несколько параметров -in, поскольку openssl будет смотреть только на последний из них. В опубликованной команде openssl, которую я использовал для создания файла pfx, я указал дополнительные сертификаты с помощью параметра -certfile. X509Certificate2Collection также может читать все три из них, но они не содержатся в цепочке на стороне клиента. Я добавил дополнительную информацию о проведенных мною тестах в исходный пост, возможно, это будет полезно.   -  person PraMiD    schedule 13.03.2019


Ответы (1)


SslStream никогда не будет отправлять всю цепочку (кроме самовыданных сертификатов). Соглашение состоит в том, чтобы отправлять все, кроме корня, потому что другая сторона либо уже имеет и доверяет корню, либо не имеет (таким образом / или не доверяет корню), и в любом случае это было пустой тратой полосы пропускания.

Но SslStream может отправлять промежуточные звенья только в том случае, если он понимает их.

var cert = new X509Certificate2(certPath, certPass);

Это только извлекает сертификат конечного объекта (тот, у которого есть закрытый ключ), он отбрасывает любые другие сертификаты в PFX. Если вы хотите загрузить все сертификаты, вам нужно использовать X509Certificate2Collection.Import. Но ... это тебе тоже не очень помогает. SslStream принимает только сертификат конечного объекта, он ожидает, что система сможет построить для него функционирующую цепочку.

Чтобы построить функционирующую цепочку, ваш промежуточный и корневой сертификаты должны быть в любом из:

  • Provided as manual input via X509Chain.ChainPolicy.ExtraStore
    • Since the chain in question is built by SslStream you can't really do this here.
  • CurrentUser \ Мой X509Store
  • * LocalMachine \ My X509Store
  • CurrentUser \ CA X509Store
  • ** LocalMachine \ CA X509Store
  • CurrentUser \ Root X509Store
  • ** LocalMachine \ Root X509Store
  • * LocalMachine \ ThirdPartyRoot X509Store
  • Местоположение http (not-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, а выданные сертификаты должны нести расширение доступа к информации о полномочиях, чтобы иметь возможность динамически их обнаруживать.

person bartonjs    schedule 13.03.2019
comment
Спасибо, это почти сводило меня с ума. Я использовал коллекцию для загрузки сертификатов в новое хранилище для сертификатов, а не в определенное системой ... Таким образом, решение представляет собой комбинацию комментария @ pavel-anikhouski и этого ответа. Сначала мы должны загрузить сертификаты в хранилище System-Defined, используя X509Certificate2Collection, а затем использовать сертификат для аутентификации. В результате клиент сможет увидеть / проверить всю цепочку. - person PraMiD; 13.03.2019