Подтвердить что ты не робот

Сертификаты WCF без магазина сертификатов

Моя команда разрабатывает ряд плагинов WPF для клиентского приложения сторонней толщины. Плагины WPF используют WCF для использования веб-сервисов, опубликованных рядом служб TIBCO. Толстое клиентское приложение поддерживает отдельный центральный хранилище данных и использует проприетарный API для доступа к хранилищу данных. Толстые клиентские и WPF-модули должны быть развернуты на 10 000 рабочих станций. Наш клиент хочет сохранить сертификат, используемый толстым клиентом в центральном хранилище данных, чтобы им не нужно было беспокоиться о повторном выдаче сертификата (текущий цикл повторной эмиссии занимает около 3 месяцев), а также имеет возможность авторизации использование сертификата. Предлагаемая архитектура предлагает форму общей секретности/аутентификации между центральным хранилищем данных и услугами TIBCO.

Пока я не согласен с предлагаемой архитектурой, наша команда не в состоянии изменить ее и должна работать с предоставленными.

В основном наш клиент хочет, чтобы мы встроили в наши плагины WPF механизм, который извлекает сертификат из центрального хранилища данных (который будет разрешен или запрещен на основе ролей в этом хранилище данных) в память, а затем использует сертификат для создания SSL-соединение с услугами TIBCO. Запрещается использовать хранилище сертификатов локального компьютера, и в конце каждого сеанса должна быть сброшена версия памяти.

Итак, вопрос в том, кто-нибудь знает, можно ли передать сертификат в память в службу WCF (.NET 3.5) для шифрования транспортного уровня SSL?

Примечание. Я задал аналогичный вопрос (здесь), но с тех пор удалил его и повторно спросил его с дополнительной информацией.

4b9b3361

Ответ 1

Это возможно. Мы делаем что-то подобное с Mutual Certificate Auth - сертификатом сервиса, и в некоторых случаях сертификат клиента подбирается из центрального органа как часть механизма автоматического обнаружения/единого входа.

Не совсем понятно, в каком контексте будет использоваться сертификат, но во всех случаях вам нужно определить свой собственный элемент поведения и поведения, вытекающий из конкретного поведения/элемента в пространстве имен System.ServiceModel.Description, который принимает сертификат, На данный момент я предполагаю, что это учетная запись клиента. Сначала вы должны написать поведение, которое выглядит примерно так:

public class MyCredentials : ClientCredentials
{
    public override void ApplyClientBehavior(ServiceEndpoint endpoint,
        ClientRuntime behavior)
    {
        // Assuming GetCertificateFromNetwork retrieves from CDS
        ClientCertificate.Certificate = GetCertificateFromNetwork();
    }

    protected override ClientCredentials CloneCore()
    {
        // ...
    }
}

Теперь вам нужно создать элемент, который может идти в конфигурации XML:

public class MyCredentialsExtensionElement : ClientCredentialsElement
{
    protected override object CreateBehavior()
    {
        return new MyCredentials();
    }

    public override Type BehaviorType
    {
        get { return typeof(MyCredentials); }
    }

    // Snip other overrides like Properties
}

После этого вы можете добавить политику в конфигурацию WCF:

<behaviors>
    <endpointBehaviors>
        <behavior name="MyEndpointBehavior">
            <myCredentials/>
        </behavior>
    </endpointBehaviors>
</behaviors>

Изменить: почти забыл упомянуть, вам необходимо зарегистрировать расширение:

<system.serviceModel>
    <extensions>
        <behaviorExtensions>
            <add name="myCredentials"
                 type="MyAssembly.MyCredentialsExtensionElement, MyAssembly,
                       Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        </behaviorExtensions>
    </extensions>
</system.serviceModel>

Надеюсь, что это поможет. Если вам нужна подробная информация о расположении всех этих классов и о том, что происходит за кулисами, попробуйте прочитать Расширение WCF с пользовательскими поведением.

Ответ 2

Я тот парень, у которого Кейн (наш SO-лакей!) задал оригинальный вопрос. Я думал, что, наконец, создаю учетную запись и опубликую наши результаты/результаты/опыт в отношении ответа, опубликованного Aaronaught (так что любой кредит для него выше).

Мы попробовали добавить настраиваемое поведение, как было предложено выше, и установить для него конфигурацию behaviourConfiguration на элементе конфигурации конечной точки. Мы не могли получить код, чтобы стрелять вообще, так что в конечном итоге идет с программным подходом.

Поскольку у нас был класс-оболочка, созданный для создания объекта ClientBase, мы использовали наши существующие функции создания, чтобы добавить поведение после создания всех остальных частей ClientBase.

Мы также столкнулись с несколькими проблемами, а именно, что поведение ClientCredentials уже определено для нашей аутентификации ClientBase с использованием имени пользователя и пароля, а не нашего сертификата + имя пользователя и пароль. Таким образом, мы удалили существующее поведение программно, прежде чем добавлять новое поведение на основе сертификата (с именем пользователя и паролем) в качестве временной меры для тестирования. Все еще не играли в кости, наше поведение строилось, и ApplyClientBehavior увольнялся, но служба все еще падала при вызове Invoke (у нас никогда не было реального исключения из-за кучи использования операторов, которые сложно было реорганизовать).

Затем мы решили вместо удаления существующего поведения ClientCredentials, чтобы мы просто ввели наш сертификат в него, прежде чем разрешить весь процесс партии как обычно. В-третьих, обаяние, все это и работает сейчас.

Я хотел бы поблагодарить Aaronaught (и я проголосую, если бы мог!) за то, что он поставил нас на правильный путь и дал хорошо продуманный и полезный ответ.

Получает небольшой фрагмент кода и работает (используя тестовый файл .CRT).

     protected override ClientBase<TChannel> CreateClientBase(string endpointConfigurationName)
    {
        ClientBase<TChannel> clientBase = new ClientBase<TChannel>(endpointConfigurationName); // Construct yours however you want here

        // ...

        ClientCredentials credentials = clientBase.Endpoint.Behaviors.Find<ClientCredentials>();

        X509Certificate2 certificate = new X509Certificate2();
        byte[] rawCertificateData = File.ReadAllBytes(@"C:\Path\To\YourCert.crt");
        certificate.Import(rawCertificateData);

        credentials.ClientCertificate.Certificate = certificate;

        return clientBase;
    }

В качестве другой стороны, в рамках тестирования мы удалили все наши сертификаты из локального хранилища компьютеров, это фактически вызвало проблему с использованием Fiddler. Fiddler не обнаружил наш клиентский сертификат, потому что он был чисто в памяти, а не в доверенном хранилище. Если мы добавим его обратно в доверенный магазин, то Fiddler снова начнет играть хорошо.

Еще раз спасибо.

Ответ 3

У Aaronaught была правильная идея, но мне пришлось внести несколько изменений, чтобы заставить ее работать. Реализация, которую я использовал, следует. Я добавил к нему немного больше, чтобы получить сертификат из встроенного ресурса.

using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Configuration;
using System.Configuration;
using System.ServiceModel.Description;

namespace System.ServiceModel.Description
{
    /// <summary>
    /// Uses a X509 certificate from disk as credentials for the client.
    /// </summary>
    public class ClientCertificateCredentialsFromFile : ClientCredentials
    {
        public ClientCertificateCredentialsFromFile(CertificateSource certificateSource, string certificateLocation)
        {
            if (!Enum.IsDefined(typeof(CertificateSource), certificateSource)) { throw new ArgumentOutOfRangeException(nameof(certificateSource), $"{nameof(certificateSource)} contained an unexpected value."); }
            if (string.IsNullOrWhiteSpace(certificateLocation)) { throw new ArgumentNullException(nameof(certificateLocation)); }

            _certificateSource = certificateSource;
            _certificateLocation = certificateLocation;

            ClientCertificate.Certificate = certificateSource == CertificateSource.EmbeddedResource ?
                GetCertificateFromEmbeddedResource(certificateLocation)
                : GetCertificateFromDisk(certificateLocation);
        }

        /// <summary>
        /// Retrieves a certificate from an embedded resource.
        /// </summary>
        /// <param name="certificateLocation">The certificate location and assembly information. Example: The.Namespace.certificate.cer, Assembly.Name</param>
        /// <returns>A new instance of the embedded certificate.</returns>
        private static X509Certificate2 GetCertificateFromEmbeddedResource(string certificateLocation)
        {
            X509Certificate2 result = null;

            string[] parts = certificateLocation.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length < 2) { throw new ArgumentException($"{certificateLocation} was expected to have a format of namespace.resource.extension, assemblyName"); }
            string assemblyName = string.Join(",", parts.Skip(1));

            var assembly = Assembly.Load(assemblyName);
            using (var stream = assembly.GetManifestResourceStream(parts[0]))
            {
                var bytes = new byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);
                result = new X509Certificate2(bytes);
            }

            return result;
        }

        /// <summary>
        /// Retrieves a certificate from disk.
        /// </summary>
        /// <param name="certificateLocation">The file path to the certificate.</param>
        /// <returns>A new instance of the certificate from disk</returns>
        private static X509Certificate2 GetCertificateFromDisk(string certificateLocation)
        {
            if (!File.Exists(certificateLocation)) { throw new ArgumentException($"File {certificateLocation} not found."); }
            return new X509Certificate2(certificateLocation);
        }


        /// <summary>
        /// Used to keep track of the source of the certificate. This is needed when this object is cloned.
        /// </summary>
        private readonly CertificateSource _certificateSource;

        /// <summary>
        /// Used to keep track of the location of the certificate. This is needed when this object is cloned.
        /// </summary>
        private readonly string _certificateLocation;

        /// <summary>
        /// Creates a duplicate instance of this object.
        /// </summary>
        /// <remarks>
        /// A new instance of the certificate is created.</remarks>
        /// <returns>A new instance of <see cref="ClientCertificateCredentialsFromFile"/></returns>
        protected override ClientCredentials CloneCore()
        {
            return new ClientCertificateCredentialsFromFile(_certificateSource, _certificateLocation);
        }
    }
}


namespace System.ServiceModel.Configuration
{
    /// <summary>
    /// Configuration element for <see cref="ClientCertificateCredentialsFromFile"/>
    /// </summary>
    /// <remarks>
    /// When configuring the behavior an extension has to be registered first.
    /// <code>
    /// <![CDATA[
    /// <extensions>
    ///     <behaviorExtensions>
    ///         <add name = "clientCertificateCredentialsFromFile"
    ///             type="System.ServiceModel.Configuration.ClientCertificateCredentialsFromFileElement, Assembly.Name" />
    ///     </behaviorExtensions>
    /// </extensions>
    /// ]]>
    /// </code>
    /// Once the behavior is registered it can be used as follows.
    /// <code>
    /// <![CDATA[ 
    /// <behaviors>
    ///     <endpointBehaviors>
    ///         <behavior name = "BehaviorConfigurationName" >
    ///             <clientCertificateCredentialsFromFile fileLocation="C:\certificates\paypal_cert.cer" />
    ///         </behavior>
    ///     </endpointBehaviors>
    /// </behaviors>
    /// <client>
    ///     <endpoint address="https://endpoint.domain.com/path/" behaviorConfiguration="BehaviorConfigurationName" ... />
    /// </client>
    /// ]]>
    /// </code>
    /// </remarks>
    public class ClientCertificateCredentialsFromFileElement : BehaviorExtensionElement
    {
        /// <summary>
        /// Creates a new <see cref="ClientCertificateCredentialsFromFile"/> from this configuration element.
        /// </summary>
        /// <returns>The newly configured <see cref="ClientCertificateCredentialsFromFile"/></returns>
        protected override object CreateBehavior()
        {
            return new ClientCertificateCredentialsFromFile(Source, Location);
        }

        /// <summary>
        /// Returns <code>typeof(<see cref="ClientCertificateCredentialsFromFile"/>);</code>
        /// </summary>
        public override Type BehaviorType
        {
            get
            {
                return typeof(ClientCertificateCredentialsFromFile);
            }
        }

        /// <summary>
        /// An attribute used to configure the file location of the certificate to use for the client credentials.
        /// </summary>
        [ConfigurationProperty("location", IsRequired = true)]
        public string Location
        {
            get
            {
                return this["location"] as string;
            }
            set
            {
                this["location"] = value;
            }
        }

        /// <summary>
        /// An attribute used to configure where the certificate should should be loaded from. 
        /// </summary>
        [ConfigurationProperty("source", IsRequired = true)]
        public CertificateSource Source
        {
            get
            {
                return (CertificateSource)this["source"];
            }
            set
            {
                this["source"] = value;
            }
        }
    }

    /// <summary>
    /// Used to declare the source of a certificate.
    /// </summary>
    public enum CertificateSource
    {
        FileOnDisk,
        EmbeddedResource
    }
}

используя приведенный выше код, мне удалось настроить мой клиент следующим образом

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <extensions>
            <behaviorExtensions>
                <add name="clientCertificateCredentialsFromFile"
                     type="System.ServiceModel.Configuration.ClientCertificateCredentialsFromFileElement, My.Project.PayPal" />
            </behaviorExtensions>
        </extensions>

        <bindings>
            <basicHttpBinding>
                <binding name="PayPalAPISoapBinding">
                    <security mode="Transport">
                        <transport clientCredentialType="Certificate" />
                    </security>
                </binding>
                <binding name="PayPalAPIAASoapBinding">
                    <security mode="Transport">
                        <transport clientCredentialType="Certificate" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <behaviors>
            <endpointBehaviors>
                <behavior name="PayPalAPICredentialBehavior">
                    <clientCertificateCredentialsFromFile source="EmbeddedResource" location="My.Project.PayPal.Test.Integration.paypal_cert.cer, My.Project.PayPal.Test.Integration" />
                </behavior>
                <behavior name="PayPalAPIAACredentialBehavior">
                    <clientCertificateCredentialsFromFile source="EmbeddedResource" location="My.Project.PayPal.Test.Integration.paypal_cert.cer, My.Project.PayPal.Test.Integration" />
                </behavior>
            </endpointBehaviors>
        </behaviors>
        <client>
            <endpoint 
                address="https://api.sandbox.paypal.com/2.0/" 
                behaviorConfiguration="PayPalAPICredentialBehavior"
                binding="basicHttpBinding"
                bindingConfiguration="PayPalAPISoapBinding" 
                contract="My.Project.PayPal.Proxy.PayPalAPIInterface"
                name="PayPalAPI" />
            <endpoint 
                address="https://api-aa.sandbox.paypal.com/2.0/" 
                behaviorConfiguration="PayPalAPIAACredentialBehavior"
                binding="basicHttpBinding"
                bindingConfiguration="PayPalAPIAASoapBinding" 
                contract="My.Project.PayPal.Proxy.PayPalAPIAAInterface"
                name="PayPalAPIAA" />
        </client>
    </system.serviceModel>
</configuration>