Сертификаты WCF без хранилища сертификатов - PullRequest
12 голосов
/ 09 марта 2010

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

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

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

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

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

Ответы [ 3 ]

13 голосов
/ 09 марта 2010

Это возможно. Мы делаем нечто подобное с 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 с помощью пользовательского поведения .

4 голосов
/ 10 марта 2010

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

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

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

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

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

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

Вот небольшой фрагмент кода, который запускается и работает (с использованием тестового файла .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 снова начнет играть хорошо.

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

0 голосов
/ 19 апреля 2016

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

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's 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>
...