[WCF安全系列]消息的保护等级
到目前为止,对于WCF安全传输的三个方面,我们已经对认证进行了详细的介绍,现在我们来关注另外两个话题:消息的一致性和机密性,两者又统称为消息保护(Message Protection)。消息的安全等级指的是对整个消息或者消息的某个部分事实安全保护采用的等级。按照级别的由低到高,WCF支持如下三种不同的安全等级。在WCF的应用编程接口中,消息保护级别通过如下定义的ProtectionLevel枚举表示。
- None: 不采用任何措施来保护消息的一致性和机密性;
- Sign: 通过对整个消息或者消息的某个部分进行数字签名以确保消息的一致性;
- EncryptAndSign: 通过对整个消息或者消息的某个部分同时进行签名和加密确保消息的一致性和机密性。
{
None,
Sign,
EncryptAndSign
}
一、消息保护级别的定义
消息的保护涉及到签名和(或者)加密,而与签名与加密相对的是签名验证和解密。要确保消息保护机制的正常进行,客户端和服务双方需要首先在保护级别上达成一致,双方按照这个约定完成属于各自的工作。从这个意义上讲,消息保护级别属于契约的一部分,所以基于消息安全级别的编程体现在契约的定义中。
我们在定义服务契约的时候,可以通过ServiceContractAttribute特性的ProtectionLevel属性为整个服务契约设置保护级别。也可以通过OperationContractAttribute特性的ProtectionLevel属性为某个具体的操作设置保护级别。ProtectionLevel属性在这两个特性中的定义如下。
{
//其他成员
public ProtectionLevel ProtectionLevel { get; set; }
publicbool HasProtectionLevel { get; }
}
public sealed class OperationContractAttribute : Attribute
{
//其他成员
public ProtectionLevel ProtectionLevel {get; set; }
public bool HasProtectionLevel { get; }
}
{
//其他成员
public ProtectionLevel ProtectionLevel {get; set; }
public bool HasProtectionLevel { get; }
}
{
//其他成员
public ProtectionLevel ProtectionLevel {get; set; }
publicbool HasProtectionLevel{ get; }
}
publicabstractclass MessageContractMemberAttribute : Attribute
{
//其他成员
public ProtectionLevel ProtectionLevel {get; set; }
publicbool HasProtectionLevel{ get; }
}
publicclass MessageHeaderAttribute : MessageContractMemberAttribute
{
//省略成员
}
publicclass MessageBodyMemberAttribute : MessageContractMemberAttribute
{
//省略成员
}
二、消息保护级别的作用范围
通过上面的介绍我们知道了我们可以通过一系列基于契约(服务契约、错误契约和消息契约)的特性来定义消息的保护级别。那么,如果我们在这些特性中设置了不同的保护级别,它们之间具有怎样的优先级?WCF又采用怎样的策略来决定最终的消息保护级别呢?
定义消息保护级别的六个特性分别位于如下图所示的层次结构的四个层次中。低层次可以继承离它最近的高层次的消息保护级别。举个具体的例子,如果通过ServiceContractAttribute特性在服务契约级别将保护级别设置为Sign,该服务契约所有的操作、操作的错误契约,以及操作使用到的消息契约的默认的保护级别都变成Sign。而服务操作可以通过OperationContractAttribute特性将保护级别设置成EncryptAndSign,那么不仅仅是该操作,就连基于该操作的错误契约和消息契约对应的保护级别也动变成EncryptAndSign。
上面我们着重在介绍如何在契约上定义消息的保护级别,接下来我们将关注点放在绑定上面。我们主要关注两个问题:第一、在默认的情况下绑定采用怎样的保护级别?;第二、绑定的保护级别可以自定义吗?
对于第一个问题,为了让读者有一个深刻的印象,我不直接告诉你答案,而是希望读者想我一下通过编程的方式自己去获取这个答案。在这里我们需要用到一个特殊的接口:ISecurityCapabilities。ISecurityCapabilities定义了一些简单的属性成员用以检测绑定具有怎样的安全相关的属性,其中就包括消息的保护级别。如下面的代码片断所示,ISecurityCapabilities具有两个只读属性SupportedRequestProtectionLevel和SupportedResponseProtectionLevel表示对应的绑定对于请求消息和回复消息采用怎样的保护级别。
{
//其他成员
ProtectionLevel SupportedRequestProtectionLevel { get; }
ProtectionLevel SupportedResponseProtectionLevel { get; }
}
那么我们现在就来检测基于某种安全模式下的绑定在默认情况下采用怎样的消息保护级别。为了使我们的程序显得简洁,我写了如下一个针对Binding类型的扩展方法PrintProtectionLevel,用于输出绑定对请求和回复消息采用的保护级别。
{
public static void PrintProtectionLevel(this Binding binding, string securityMode)
{
var bindingParameters = new BindingParameterCollection();
var requestProtectionLevel = binding.GetProperty<ISecurityCapabilities>(bindingParameters).SupportedRequestProtectionLevel;
var responseProtectionLevel = binding.GetProperty<ISecurityCapabilities>(bindingParameters).SupportedResponseProtectionLevel;
Console.WriteLine("{0, -25}{1, -20}{2,-20}", securityMode, requestProtectionLevel, responseProtectionLevel);
}
}
现在我们通过下面的代码检测BasicHttpBinding针对四种不同的安全级别默认采用怎样的消息保护级别。从输出结果我们可以很清楚的看到,除了TransportCredentialOnly之外,BasicHttpBinding都是采用EncryptAndSign保护级别。
var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
binding.PrintProtectionLevel("Transport");
binding = new BasicHttpBinding(BasicHttpSecurityMode.Message);
binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.Certificate;
binding.PrintProtectionLevel("Message");
binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential);
binding.PrintProtectionLevel("Mixed");
binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportCredentialOnly);
binding.PrintProtectionLevel("TransportCredentialOnly");
输出结果:
Transport EncryptAndSign EncryptAndSign
Message EncryptAndSign EncryptAndSign
Mixed EncryptAndSign EncryptAndSign
TransportCredentialOnly None None
如果你将上面的测试程序用于其它的绑定(WSHttpBinding/WS2007DualHttpBinding、WSDualHttpBinding、NetTcpBinding、NetNamedPipeBinding以及Message和Both模式下的NetMsmqBinding)你会发现当安全被开启的情况下,这些绑定默认都是采用最高的消息保护级别EncryptAndSign。
但是我们编写的扩展方法不能用于Transport模式下的NetMsmqBinding。不过在表示NetMsmqBinding基于Transport安全的类型MsmqTransportSecurity中具有一个MsmqProtectionLevel属性返回采用的消息保护级别。从应用在该属性上的DefaultValueAttribute特性的定义中,我们可以直接看出NetMsmqBinding在Transport模式下默认采用的消息保护级别为Sign。
{
//其他成员
[DefaultValue(1)]
public ProtectionLevel MsmqProtectionLevel { get; set; }
}
上面我们讨论了对于我们常用的绑定针对相应的安全模式默认采用的消息保护级别,接下来我们讨论的话题是:这些默认的保护级别可以自定义吗?答案是“部分可以”。具体来说,你只可以修改三个基于局域网的绑定针对Transport安全模式下的消息保护级别。对于NetMsmqBinding,你可以通过MsmqTransportSecurity的MsmqProtectionLevel进行设置。而用于设置NetTcpBinding和NetNamedPipeBinding基于Transport安全的TcpTransportSecurity和NamedPipeTransportSecurity类型中,都具有ProtectionLevel属性用于进行消息保护级别的显式设置。而且从应用在该属性上的DefaultValueAttribute特性中我们可以看出默认值为EncryptAndSign。你可以通过编程或者配置的方式来指定NetTcpBinding、NetNamedPipeBinding和NetMsmqBinding在Transport安全模式下的消息保护级别。
{
//其他成员
[DefaultValue(2)]
public ProtectionLevel ProtectionLevel { get; set; }
}
public sealed class NamedPipeTransportSecurity
{
//其他成员
[DefaultValue(2)]
public ProtectionLevel ProtectionLevel { get; set; }
}
前面我们着重讨论了消息的保护等级如果在契约中定义,定义在不同契约(服务契约、错误契约和消息契约)中的消息保护等级具有怎样的层级关系,以及在默认情况下各种绑定采用怎样的保护等级。接下来,我们进一步来探讨消息保护等级和绑定的关系。
一、契约的保护等级为绑定进行消息保护设置了“最低标准”
二、显式地将保护等级设置成ProtectionLevel.None与没有设置保护等级有区别吗?
三、消息的保护等级与WS-Addressing
一、契约的保护等级为绑定进行消息保护设置了“最低标准”
定义在契约上消息保护级别实际上为WCF实施消息保护设置了一个“最低标准”。由于整个消息保护机制,不论是签名还是加密,都是在信道层实现的。而信道层最终是通过绑定来实现的,绑定的属性决定了信道层处理消息的能力。而绑定安全方面的属性自然就决定了最终的信道层是否有能力对消息实施签名和加密。一方面,以契约形式定义的消息保护级别帮助信道层决定应该对传入的消息采取那个级别的保护机制;另一方面,如果绑定所能提供的消息保护能力不能达到这个最低标准,就会抛出异常。
举个例子,如果我们通过如下的代码将服务契约ICalculator的Add操作的保护级别设置成EncryptAndSign。
public interface ICalculator
{
[OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)]
double Add(double x, double y);
}
但是我们却将终结点使用到的WS2007HttpBinding的安全模式设置成None。那么在对服务进行寄宿的时候,就会跑出如下图所示的InvalidOperationException异常,提示“必须保护请求消息”。
<bindings>
<ws2007HttpBinding>
<binding name="bindingWithNoneSecurityMode">
<security mode="None"/>
</binding>
</ws2007HttpBinding>
</bindings>
<services>
<service name="Artech.WcfServices.Services.CalculatorService">
<endpoint address="http://127.0.0.1/calculatorservice" binding="ws2007HttpBinding" bindingConfiguration="bindingWithNoneSecurityMode"
contract="Artech.WcfServices.Contracts.ICalculator"/>
</service>
</services>
</system.serviceModel>
二、显式地将保护等级设置成ProtectionLevel.None与没有设置保护等级有区别吗?
在这里有一个很多人会忽视的要点。表示消息保护级别的ProtectionLevel类型是一个枚举,所以它肯定有一个默认值。这个默认值就是None,也就是说当你没有显式地指定契约具有采用那么保护级别的时候,默认值就是None。但是这种情况和你显式保护级别设置为None的效果是完全不一致的。因为前者真正采用的保护级别(当绑定安全被开启)实际上是EncryptAndSign,后者才是None。那么WCF如何来区分这两种情况呢?
如果你足够细心,你应该会发现:在上面介绍的定义消息保护级别的特性中,除了具有一个可读可写的ProtectionLevel属性之外,还具有一个只读的HasProtectionLevel属性,该属性表示你是否对消息保护级别进行了“显式”的设置。我们可以通过一个简单的实验来演示HasProtectionLevel的作用。
下面我定义了两个服务契约IServiceContract1和IServiceContract2,其实前者没有对ProtectionLevel进行相应的设置,后者被显式地设置为None。
public interface IServiceContract1
{
[OperationContract]
void DoSomething();
}
[ServiceContract(ProtectionLevel = ProtectionLevel.None)]
public interface IServiceContract2
{
[OperationContract]
void DoSomething();
}
然后我编写了如下的代码,基于上面两个接口类型生成相应的ContractDescription对象,然后将它们的ProtectionLevel和HasProtectionLevel属性输出来。从最终的输出结果我们可以很清楚地看到:两种情况下下ProtectionLevel属性值都是None,但是只有当你显式地设置了ProtectionLevel的情况下,HasProtectionLevel属性才会返回True。WCF就是根据ContractDescription的这两个属性决定最终采用怎样的消息保护级别的。
ContractDescription contract2 = ContractDescription.GetContract(typeof(IServiceContract2));
Console.WriteLine("{0,-10}{1,-20}{2,-20}", "Contract","ProtectionLevel", "HasProtectionLevel");
Console.WriteLine("{0,-10}{1,-20}{2,-20}", "contract1", contract1.ProtectionLevel, contract1.HasProtectionLevel);
Console.WriteLine("{0,-10}{1,-20}{2,-20}", "contract2", contract2.ProtectionLevel, contract2.HasProtectionLevel);
输出结果:
contract1 None False
contract2 None True
三、消息的保护等级与WS-Addressing
关于消息保护级别与绑定的关系,还有一点需要着重强调。虽然我们可以对于同一个服务契约下操作设置不同的保护级别,但是在WSDL中需要基于WS-Addressing中的寻址(Addressing)机制来识别基于操作的保护级别。在使用的绑定不支持WS-Addressing的情况下(比如BasicHttpBinding),它会选择所有操作中等级最高的那个作为所有操作的保护级别。比如说对于如下定义的服务契约ICalculator,在使用BasicHttpBinding的情况下,两个操作采用的保护级别都是EncryptAndSign。
public interface ICalculator
{
[OperationContract(ProtectionLevel = ProtectionLevel.Sign)]
double Add(double x, double y);
[OperationContract(ProtectionLevel = ProtectionLevel.EncryptAndSign)]
double Substract(double x, double y);
}
这实际上会为你的应用带来一个很隐晦的问题,为了将这个问题阐述得更加清楚,我通过一个例子来说明。还是应用我们的计算服务的例子,下面是我们再熟悉不过的服务契约的定义,Add操作的保护级别被设置成Sign。
public interface ICalculator
{
[OperationContract(ProtectionLevel = ProtectionLevel.Sign)]
double Add(double x, double y);
}
但是这个服务契约并被客户端共享,而客户端服务契约中定义了一个额外的操作Substract,该操作的保护级别并未作显式设置。
public interface ICalculator
{
[OperationContract(ProtectionLevel = ProtectionLevel.Sign)]
double Add(double x, double y);
[OperationContract]
double Substract(double x, double y);
}
现在选择BasicHttpBinding作为终结点的绑定,并将安全模式甚至成Message。当你客户端调用Add操作的时候。会抛出如下图所示的MessageSecurityException异常,提示“主签名必须加密”。但是当你将客户端Substract删除或者将Substract操作的消息保护级别也设置成Sign是,这个异常将不会出现。
出现这样的异常的原因在于:对于不支持WS-Addressing的BasicHttpBinding来说,会选择所有操作中等级最高的那个最为所有操作的保护级别。对于客户端来说,由于Substract没有对保护级别进行显式设置,默认采用最高等级的EncryptAndSign。但是服务端的等级确是Sign。
在这种情况下,请求消息会同时被加密和签名。请求消息被服务端接受之后,虽然它对应的等级是Sign,但是依然能够处理该请求。这就是所谓的“消息保护级别的最低标准”原则,定义在契约中的保护级别只是确立了一个消息保护的“底线”。你不能低于这个最低标准,但是可以高于它。但是服务执行正常的运算后,只会按照定义在本地契约中设置的保护级别对回复消息进行签名。客户端接受到这个仅仅被签名的回复消息,会发现等级不够,所以才会提示你“主签名必须加密”。