WCF无需证书存储的证书

14

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

虽然我不一定同意所提出的架构,但我们的团队无法更改它,必须使用提供的内容进行工作。

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

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

注意:我曾经问过类似的问题(here),但后来删除了它,并提供更多信息重新提出了这个问题。

3个回答

14

可以实现。我们使用双向证书认证进行类似的操作 - 服务证书和在某些情况下客户端证书是作为自动发现/单点登录机制的一部分从中央机构获取的。

不清楚证书将在什么上下文中使用,但在所有情况下,您需要做的是定义自己的行为和行为元素,派生自在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

非常感谢您的建议。我会告诉您我的进展情况。 - Kane
很抱歉这么晚才问。为什么直接分配客户端证书而不是调用 SetCertificate()?有什么区别吗? - ashes999
@ashes999:该方法没有重载,可以接受实际的 X509Certificate。只有在您的证书已经在本地存储之一中时,它才有用,而这在此明确不是情况。 - Aaronaught

4
我是那个让我们的SO助手Kane提出原始问题的人。我想我最终创建了一个帐户,并发布了我们在Aaronaught所发表答案方面的发现/结果/经验(以上任何荣誉归于他)。
我们尝试添加自定义行为,如上所建议,并将behaviorConfiguration设置为使用端点配置元素。我们无法使代码完全启动,因此最终采用了编程方法。
由于我们设置了包装器类来构建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 将再次正常工作。
再次感谢。

嗯,非常令人困惑,你竟然无法使行为运行。也许你忘记配置带有该行为的端点了吗?不管怎样,看起来你们找到了一个解决方法,所以结局还算圆满! - Aaronaught
是的,我们配置了端点来使用这个行为,但只有在以编程方式添加时才能触发它。这有点奇怪,但我们没有进一步追究,因为我们有了一个方向上的小变化。 - user289344

1
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>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接