[WCF安全係列]實例演示:TLS/SSL在WCF中的應用[SSL over TCP]
在接下來的係列文章中我們正是討論關於身份認證的主題。在前麵我們已經談到了,WCF中的認證屬於“雙向認證”,既包括服務對客戶端的認證(以下簡稱客戶端認證),也包括客戶端對服務的認證(以下簡稱服務認證)。客戶端認證和服務認證從本質上並沒有什麼不同,無非都是被認證一方提供相應的用戶憑證供對方對自己的身份進行驗證。我們先來討論服務認證,客戶端認證放在後續的文章中。
在《從兩種安全模式談起》中,我們對TLS/SSL進行了簡單的介紹。我們知道,客戶端和服務在為建立安全上下文而進行的協商過程中會驗證服務端的X.509證書如否值得信任。對於服務證書的驗證實際上可以看成是一種服務認證,或者說TLS/SSL對證書的驗證可以看成是WCF服務認證的一個環節。
目錄
TLS/SSL與X.509證書
創建基於TLS/SSL的WCF服務
創建X.509證書
服務寄宿
服務調用
改變證書認證模式
TLS/SSL是實現Transport安全模式的一種主要的方式,但不是唯一方式。對於所有基於HTTP的綁定(主要指BasicHttpBinding、WSHttpBinding和WS2007HttpBinding,而WSDualHttpBinding不支持Transport安全模式),如果選擇了Transport或者Mixed安全模式,不論采用怎樣的認證方式,底層的實現總是基於TLS/SSL(HTTPS)。
而對於NetTcpBinding來說,如果采用Transport安全模式,並且采用非Windows認證(客戶端憑證類型選擇None或者Certificate),最終的傳輸安全的實現也是基於TLS/SSL(SSL Over TCP)。如果選擇Mixed安全模式,不論選擇怎樣的客戶端憑證類型,WCF最終都會采用TLS/SSL來提供對傳輸安全的實現。也正是因為如此,在這兩種情況下,你總是需要選擇一個X.509證書作為服務的憑證。舉個例子,對於如下的配置,終結點采用NetTcpBinding綁定,並且選擇Transport安全模式,但是卻采用匿名的認證方式(客戶端憑證類型為None)。
1: <system.serviceModel>
2: <bindings>
3: <netTcpBinding>
4: <binding name="transportTcpBinding">
5: <security mode="Transport">
6: <transport clientCredentialType="None"/>
7: </security>
8: </binding>
9: </netTcpBinding>
10: </bindings>
11: <services>
12: <service name="Artech.WcfServices.Services.CalculatorService">
13: <endpoint address="net.tcp://127.0.0.1/calculatorservice"
14: binding="netTcpBinding" bindingConfiguration="transportTcpBinding"
15: contract="Artech.WcfServices.Contracts.ICalculator" />
16: </service>
17: </services>
18: </system.serviceModel>
在對服務進行寄宿時,會拋出如下圖所示的InvalidOperationException異常,提示“未提供服務證書。請在 ServiceCredentials 中指定服務證書”。
作為服務憑證的證書通過服務行為ServiceCredentials來指定,對於WCF的安全體係來說,ServiceCredentials是個非常重要的對象,在本章後續文章中我們將反複地使用到它。對於服務憑證的指定,需要使用到ServiceCredentials的隻讀屬性ServiceCertificate,該屬性對應的類型為X509CertificateRecipientServiceCredential。X509CertificateRecipientServiceCredential對象實際上是對一個X509Certificate2對象的封裝,它定義了若幹SetCertificate方法重載用以指定一個X.509證書作為服務的憑證。ServiceCredentials和X509CertificateRecipientServiceCredential的相關定義反映在如下所示的代碼片斷中。
1: public class ServiceCredentials : SecurityCredentialsManager, IServiceBehavior
2: {
3: //其他成員
4: public X509CertificateRecipientServiceCredential ServiceCertificate { get; }
5: }
6: public sealed class X509CertificateRecipientServiceCredential
7: {
8: //其他成員
9: public void SetCertificate(string subjectName);
10: public void SetCertificate(string subjectName, StoreLocation storeLocation, StoreName storeName);
11: public void SetCertificate(StoreLocation storeLocation, StoreName storeName, X509FindType findType, object findValue);
12:
13: public X509Certificate2 Certificate { get; set; }
14: }
如果采用自我寄宿的方式,我們可以通過編程的方式來為寄宿的服務設置一個代表服務憑證的X.509證書。在下麵給出的代碼片斷中,我們為服務設置一個主體名稱為Jinnan-PC(我的機器名)的X.509證書,該證書是一個基於個人存儲(Personal Store,通過StoreName.My表示)的本機(StoreLocation.LocalMachine)證書。
1: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
2: {
3: ServiceCredentials serviceCredentials = host.Description.Behaviors.Find<ServiceCredentials>();
4: if (null == serviceCredentials)
5: {
6: serviceCredentials = new ServiceCredentials();
7: host.Description.Behaviors.Add(serviceCredentials);
8: }
9: serviceCredentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindBySubjectName, "Jinnan-PC");
10: host.Open();
11: ...
12: }
當然,我們依舊推薦采用配置的方式進行服務憑證的設置。對於上麵一段設置服務證書的代碼,我們可以通過下麵的一段配置來代替。
1: <system.serviceModel>
2: ...
3: <services>
4: <service name="Artech.WcfServices.Services.CalculatorService" behaviorConfiguration="serviceCertificateBehavior">
5: <endpoint address="net.tcp://127.0.0.1/calculatorservice"
6: binding="netTcpBinding" bindingConfiguration="transportTcpBinding" contract="Artech.WcfServices.Contracts.ICalculator" />
7: </service>
8: </services>
9: <behaviors>
10: <serviceBehaviors>
11: <behavior name="serviceCertificateBehavior">
12: <serviceCredentials>
13: <serviceCertificate storeLocation="LocalMachine" storeName="My"
14: x509FindType="FindBySubjectName" findValue="Jinnan-PC" />
15: </serviceCredentials>
16: </behavior>
17: </serviceBehaviors>
18: </behaviors>
19: </system.serviceModel>
對於采用基於TLS/SSL的Transport安全模式,對服務證書的驗證方式會因為綁定類型的不同而具有小小的差異。
接下來我們會通過一個簡單的例子來演示如何在WCF服務中使用基於TLS/SSL的Transport安全。該實例會涉及兩種不同的綁定類型(WS2007HttpBinding和NetTcpBinding)和寄宿方式(自我寄宿和IIS寄宿)。
我們還是采用慣用的計算服務的例子,演示實例的解決方式具有右圖所示的結構。Contract和Services為兩個類庫項目,分別用於定義服務契約和實現契約的服務類型。而Hosting和Client為兩個控製台應用,前者用於進行服務寄宿(自我寄宿),後者用於模擬客戶端程序。下麵的代碼片斷代碼了分別定義在Contracts和Services項目中的服務契約接口ICalculator和具體的服務類型CalculatorService。
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Contracts
3: {
4: [ServiceContract(Namespace = "https://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract]
8: double Add(double x, double y);
9: }
10: }
11:
12: using Artech.WcfServices.Contracts;
13: namespace Artech.WcfServices.Services
14: {
15: public class CalculatorService : ICalculator
16: {
17: public double Add(double x, double y)
18: {
19: return x + y;
20: }
21: }
22: }
創建X.509證書
由於TLS/SSL需要通過協商的方式生成一個用於消息簽名和加密的會話密鑰,而會話密鑰的交換依賴一個X.509證書以確保安全。所以我們首要的任務是需要得到一個X.509證書,這樣一個證書可以直接借助於MakeCert工具,通過命令行的方式創建一個主體名稱為Jinnan-PC(我個人的機器名,你需要替換成你本機的名稱)的證書。為了方便,我們在測試的時候傾向於創建自簽名證書,即證書授予者和頒發者身份合二為一。不過為了演示證書正常的信任鏈,我們不采用這種方式。所以我們需要通過運行如下的命令行先創建一個CA證書。該CA證書本身是自簽名的(對應於-r命令行開關)
1: Makecert -n "CN=RootCA" -r -sv C:\RootCA.pvk C:\RootCA.cer
上麵的命令行在執行的過程中,會彈出兩個用於輸入密碼的對話框。你需要輸入相應的密碼用以包括生成的兩個文件,一個是包含私鑰的文件RootCA.pvk,另一個是證書文件RootCA.cer,它們都保存在C盤根目錄下。
然後通過如下的命令行創建一個主題名稱為Jinnan-PC(我的機器名,你需要換成你的機器名或者本機影射的Host Name)的證書,並以上麵創建證書對應的CA(RootCA)作為該證書的頒發者(-ic C:\RootCA.cer -iv C:\RootCA.pvk)。該證書最終自動保存到本機(-sr LocalMachine)的個人存儲區(-ss My)。而-pe表示證書的私鑰可以被導出。-sky表示密鑰的類型或者作用,具有兩個選項signature和exchange,前者用於數字簽名,後者用於加密和密鑰交換,這裏選用exchange。
1: Makecert -n "CN=Jinnan-PC" -ic C:\RootCA.cer -iv C:\RootCA.pvk -sr LocalMachine -ss My -pe -sky exchange
服務寄宿
我們先使用NetTcpBinding作為綁定,在Hosting項目中定義如下的配置。從配置中我們可以看到:寄宿的CalculatorService服務唯一的終結點使用了Transport模式的NetTcpBinding綁定。該綁定的客戶端憑證類型為None,意味著接受匿名客戶端。通過命令行生成和存儲的X.509證書通過服務行為的方式被設置成寄宿服務的憑證。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <netTcpBinding>
6: <binding name="transportTcpBinding">
7: <security mode="Transport">
8: <transport clientCredentialType="None"/>
9: </security>
10: </binding>
11: </netTcpBinding>
12: </bindings>
13: <services>
14: <service name="Artech.WcfServices.Services.CalculatorService" behaviorConfiguration="serviceCertificateBehavior">
15: <endpoint address="net.tcp://Jinnan-PC/calculatorservice"
16: binding="netTcpBinding" bindingConfiguration="transportTcpBinding"
17: contract="Artech.WcfServices.Contracts.ICalculator" />
18: </service>
19: </services>
20: <behaviors>
21: <serviceBehaviors>
22: <behavior name="serviceCertificateBehavior">
23: <serviceCredentials>
24: <serviceCertificate storeLocation="LocalMachine" storeName="My"
25: x509FindType="FindBySubjectName" findValue="Jinnan-PC" />
26: </serviceCredentials>
27: </behavior>
28: </serviceBehaviors>
29: </behaviors>
30: </system.serviceModel>
31: </configuration>
通過上麵的配置,我們創建的X.509證書通過ServiceCredentials服務行為被指定為服務的憑證。此外還有一點值得注意的是:終結點地址采用了沒有使用localhost和127.0.0.1,而是直接使用了機器名(Jinnan-PC),至於為什麼需要這麼做,在後續的內容中你會找到答案。而對於寄宿服務的程序,我們力求簡潔,在Main方法中僅僅包括如下的代碼。
1: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
2: {
3: host.Open();
4: Console.Read();
5: }
服務調用
然後我們在Client項目中定義如下的客戶端配置,用於進行服務調用的終結點的NetTcpBinding具有與服務端相同的配置。客戶端通過如下一段簡單的代碼進行服務的調用。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <netTcpBinding>
6: <binding name="transportTcpBinding">
7: <security mode="Transport">
8: <transport clientCredentialType="None"/>
9: </security>
10: </binding>
11: </netTcpBinding>
12: </bindings>
13: <client>
14: <endpoint name="calculatorService" address="net.tcp://jinnan-PC/calculatorservice"
15: binding="netTcpBinding" bindingConfiguration="transportTcpBinding"
16: contract="Artech.WcfServices.Contracts.ICalculator" />
17: </client>
18: </system.serviceModel>
19: </configuration>
服務調用程序:
1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService"))
2: {
3: ICalculator calculator = channelFactory.CreateChannel();
4: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2)); ;
5: }
6: Console.Read();
完成所有編程和配置工作之後,我們先後啟動Hosting和Client這兩個控製台程序,你會發現服務並不能正常地調用,而是拋出如下圖所示的SecurityNegotiationException異常,提示服務證書不受信任。
改變證書認證模式
之所以會拋出這樣的異常,原因在於:。該認證模式要求服務證書的頒發機構鏈必須在客戶端的“受信任根證書頒發機構(Trusted Root Certification Authorities)”。為了解決這個問題,我們具有如下兩種“解決方案”:
- 將服務證書的頒發機構納入到受信任根證書頒發機構中。你可以通過MMC的證書管理單元的導出/入功能將頒發機構的證書(C:\RootCA.cer)導入到受信任根證書頒發機構存儲區中。但是不幸的是,由於CA證書是通過MakeCert.exe創建的,即使導入到受信任根證書頒發機構存儲區,它也不能作為受信任的CA;
- 通過System.ServiceModel.Description.ClientCredentials這個終結點行為改變默認的認證模式。
ClientCredentials和之前提到的ServiceCredentials是兩個相對的“行為”類型,前者是使用在客戶端的終結點行為,後者則是使用在服務端的服務行為。在本章後續的內容中,我們還將不斷的使用到它們。現在我們先看討論一下如何通過ClientCredentials來改變客戶端對服務證書的認證模式。
首選你可以通過通過ChannelFactory的Credentials屬性得到ClientCredentials對象。ClientCredential具有一個類型為X509CertificateRecipientClientCredential的ServiceCertificate隻讀屬性表示服證書。證書的認證行為定義在X509CertificateRecipientClientCredential的Authentication隻讀屬性中,該屬性的類型為X509ServiceCertificateAuthentication。我們通過X509ServiceCertificateAuthentication的CertificateValidationMode屬性設置相應的證書認證模式。關於服務證書認證模式涉及到的應用編程接口反映在如下所示的代碼片斷中。
1: public abstract class ChannelFactory
2: {
3: //其他成員
4: public ClientCredentials Credentials { get; }
5: }
6: public class ClientCredentials : SecurityCredentialsManager, IEndpointBehavior
7: {
8: //其他成員
9: public X509CertificateRecipientClientCredential ServiceCertificate { get; }
10: }
11: public sealed class X509CertificateRecipientClientCredential
12: {
13: //其他成員
14: public X509ServiceCertificateAuthentication Authentication { get; }
15: }
16: public class X509ServiceCertificateAuthentication
17: {
18: //其他成員
19: public X509CertificateValidator CustomCertificateValidator { get; set; }
20: public X509CertificateValidationMode CertificateValidationMode { get; set; }
21: }
證書認證模式通過枚舉X509CertificateValidationMode表示,它具有如下五個選項:None、PeerTrust、ChainTrust、PeerOrChainTrust和Custom。選擇None意味著無需認證,而ChainTrust則要求證書的頒發機構必須是“受信任根證書頒發機構”存儲區,而PerTrust要求證書本身(不是CA證書)存在於“受信任的個人(Trusted People)”存儲區。如果這些認證模式不能滿足你的需求,你還可以選擇Custom。在這種情況下,你需要通過繼承抽象類X509CertificateValidator自定義驗證規則,並將驗證邏輯定義在抽象方法Validate中。最終將自定義的X509CertificateValidator賦給X509ServiceCertificateAuthentication的CustomCertificateValidator屬性。X509CertificateValidationMode和X509CertificateValidator的定義如下。
1: public enum X509CertificateValidationMode
2: {
3: None,
4: PeerTrust,
5: ChainTrust,
6: PeerOrChainTrust,
7: Custom
8: }
9: public abstract class X509CertificateValidator
10: {
11: //其他成員
12: public abstract void Validate(X509Certificate2 certificate);
13: }
對於本例來說,我們創建的證書既不再受信任根證書頒發機構存儲區,也不在受信任的個人存儲區。如果我們不願意自定義X509CertificateValidator,可以通過如下的代碼選擇None模式以避免異常的發生。
1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorService"))
2: {
3: channelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;
4: ICalculator calculator = channelFactory.CreateChannel();
5: Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2)); ;
6: }
7: Console.Read();
我們也可以通過配置的方式來對ClientCredentials這個終結點行為進行相應的設置,通過上麵這段程序對服務證書驗證模式的設置與下麵的這段配置在功能上是等效的。
1: <system.serviceModel>
2: ...
3: <client>
4: <endpoint name="calculatorService" behaviorConfiguration="IgoreSvcCertValidation"
5: address="net.tcp://jinnan-PC/calculatorservice" binding="netTcpBinding" bindingConfiguration="transportTcpBinding"
6: contract="Artech.WcfServices.Contracts.ICalculator" />
7: </client>
8: <behaviors>
9: <endpointBehaviors>
10: <behavior name="IgoreSvcCertValidation">
11: <clientCredentials>
12: <serviceCertificate>
13: <authentication certificateValidationMode="None"/>
14: </serviceCertificate>
15: </clientCredentials>
16: </behavior>
17: </endpointBehaviors>
18: </behaviors>
19: </system.serviceModel>
通過終結點行為ClientCredentials改變服務證書認證不僅僅可以適用於非HTTPS下的Transport安全模式,同時適用於Message安全模式。但是當我們采用HTTPS的時候,我們需要采用另外一種改變證書認證模式的方式,詳情請關注下篇。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 16:34:43