[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,但是依然能夠處理該請求。這就是所謂的“消息保護級別的最低標準”原則,定義在契約中的保護級別只是確立了一個消息保護的“底線”。你不能低于這個最低標準,但是可以高于它。但是服務執行正常的運算后,只會按照定義在本地契約中設置的保護級別對回復消息進行簽名。客戶端接受到這個僅僅被簽名的回復消息,會發現等級不夠,所以才會提示你“主簽名必須加密”。