没有证书存储的WCF证书

时间:2010-03-09 01:51:33

标签: c# wcf ssl certificate

我的团队正在为第三方胖客户端应用程序开发一些WPF插件。 WPF插件使用WCF来使用由许多TIBCO服务发布的Web服务。胖客户端应用程序维护一个单独的中央数据存储,并使用专有API来访问数据存储。胖客户端和WPF插件将部署到10,000个工作站上。我们的客户希望将胖客户端使用的证书保留在中央数据存储中,这样他们就不必担心重新颁发证书(当前重发周期大约需要3个月),并且还有机会授权使用证书。建议的体系结构在中央数据存储和TIBCO服务之间提供了一种共享密钥/身份验证形式。

虽然我不一定同意所提议的架构,但我们的团队无法改变它并且必须使用所提供的内容。

基本上我们的客户希望我们在我们的WPF插件中构建一个机制,该机制从中央数据存储(基于该数据存储中的角色允许或拒绝)检索证书到内存然后使用证书创建与TIBCO服务的SSL连接。不允许使用本地计算机的证书存储区,并且在每个会话结束时将丢弃内存版本。

所以问题是,是否有人知道是否可以将内存中的证书传递给WCF(.NET 3.5)服务以进行SSL传输级加密?

注意:我问了一个类似的问题(here),但之后又将其删除,并重新询问了更多信息。

3 个答案:

答案 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>