Сертификаты WCF без магазина сертификатов
Моя команда разрабатывает ряд плагинов WPF для клиентского приложения сторонней толщины. Плагины WPF используют WCF для использования веб-сервисов, опубликованных рядом служб TIBCO. Толстое клиентское приложение поддерживает отдельный центральный хранилище данных и использует проприетарный API для доступа к хранилищу данных. Толстые клиентские и WPF-модули должны быть развернуты на 10 000 рабочих станций. Наш клиент хочет сохранить сертификат, используемый толстым клиентом в центральном хранилище данных, чтобы им не нужно было беспокоиться о повторном выдаче сертификата (текущий цикл повторной эмиссии занимает около 3 месяцев), а также имеет возможность авторизации использование сертификата. Предлагаемая архитектура предлагает форму общей секретности/аутентификации между центральным хранилищем данных и услугами TIBCO.
Пока я не согласен с предлагаемой архитектурой, наша команда не в состоянии изменить ее и должна работать с предоставленными.
В основном наш клиент хочет, чтобы мы встроили в наши плагины WPF механизм, который извлекает сертификат из центрального хранилища данных (который будет разрешен или запрещен на основе ролей в этом хранилище данных) в память, а затем использует сертификат для создания SSL-соединение с услугами TIBCO. Запрещается использовать хранилище сертификатов локального компьютера, и в конце каждого сеанса должна быть сброшена версия памяти.
Итак, вопрос в том, кто-нибудь знает, можно ли передать сертификат в память в службу WCF (.NET 3.5) для шифрования транспортного уровня SSL?
Примечание. Я задал аналогичный вопрос (здесь), но с тех пор удалил его и повторно спросил его с дополнительной информацией.
Ответы
Ответ 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>