我的团队正在为第三方胖客户端应用程序开发一些WPF插件。 WPF插件使用WCF来使用由许多TIBCO服务发布的Web服务。胖客户端应用程序维护一个单独的中央数据存储,并使用专有API来访问数据存储。胖客户端和WPF插件将部署到10,000个工作站上。我们的客户希望将胖客户端使用的证书保留在中央数据存储中,这样他们就不必担心重新颁发证书(当前重发周期大约需要3个月),并且还有机会授权使用证书。建议的体系结构在中央数据存储和TIBCO服务之间提供了一种共享密钥/身份验证形式。
虽然我不一定同意所提议的架构,但我们的团队无法改变它并且必须使用所提供的内容。
基本上我们的客户希望我们在我们的WPF插件中构建一个机制,该机制从中央数据存储(基于该数据存储中的角色允许或拒绝)检索证书到内存然后使用证书创建与TIBCO服务的SSL连接。不允许使用本地计算机的证书存储区,并且在每个会话结束时将丢弃内存版本。
所以问题是,是否有人知道是否可以将内存中的证书传递给WCF(.NET 3.5)服务以进行SSL传输级加密?
注意:我问了一个类似的问题(here),但之后又将其删除,并重新询问了更多信息。
答案 0 :(得分:13)
有可能。我们使用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>
希望有所帮助。如果您需要有关所有这些课程安排以及幕后情况的更多详细信息,请尝试阅读Extending WCF with Custom Behaviors。
答案 1 :(得分:4)
我是那个让Kane(我们的SO lackey!)问原问题的人。我想我最终会创建一个帐户并发布我们关于Aaronaught发布的答案的调查结果/结果/经验(所以对他的任何信任)。
我们尝试按照上面的建议添加自定义行为,并在端点配置元素上设置behaviourConfiguration以使用它。我们无法完全触发代码,因此最终采用了程序化方法。
由于我们设置了一个构建ClientBase对象的包装类,我们使用现有的创建函数在构建ClientBase的所有其他部分之后添加行为。
我们遇到了一些问题,即已经为我们的ClientBase定义了ClientCredentials行为,使用用户名和密码而不是我们的证书+用户名和密码进行身份验证。因此,在添加基于证书的新行为(注入了用户名和密码)作为临时测试措施之前,我们以编程方式删除了现有行为。仍然没有骰子,我们的行为正在被构建并且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又开始玩得很好了。
再次感谢。
答案 2 :(得分:0)
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'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>