1. 程式人生 > >iOS: HTTPS 與自簽名證書

iOS: HTTPS 與自簽名證書

不是每個公司都會以數百美金一年的代價向CA購買SSL證書。在企業應用中,付費的SSL證書經常被自簽名證書所替代。當然,對於自簽名證書iOS是沒有能力驗證的。Safari遇到這種無法驗證的自簽名證書的唯一處理方法,就是將問題扔給使用者,讓使用者決定是否應該相信此類證書。它提供了兩個按鈕,一個“繼續”按鈕和一個“取消”按鈕。當你點選“取消”按鈕,則你將無法訪問所請求的資源。 當你點選“繼續”按鈕,則Safari會認為使用者授權它放棄對該伺服器的驗證,所產生的風險由使用者自己承擔。 當然,HTTPS傳輸仍然是加密的。

一、配置HTTPS伺服器

我們將使用Tomcat來配置HTTPS伺服器。關於Tomcat在mac下的安裝,請參考《 安裝Tomcat到Mac OSX 》一文。如果你安裝了Eclipse,則很可能Eclipse IDE中已經配置過Tomcat。帶開Eclipse的“偏好設定”,在Server->RuntimeEnvironments中可以看到已安裝好的Tomcat伺服器:


點選“Edit…”按鈕,你可以找到Tomcat安裝路徑:


開啟“終端”,進入Tomcat安裝目錄:

cd /Library/Tomcat/apache-tomcat-7.0.14

執行以下命令:

keytool -genkey -v -alias tomcat-keyalg RSA -storetype JKS -keystore tomcat.keystore -dname"CN=www.handtimes.com,OU=ipcc,O=雲電同方,L=昆明,ST=雲南,C=中國" -storepass 123456 -keypass 123456

這將在Tomcat安裝路徑下生成伺服器證書及金鑰庫(庫名:tomcat,檔名:tomcat.keystore),證書是自簽名的,證書機構採用域名。金鑰庫密碼和私鑰密碼都是123456。預設過期時間為3個月(90天)。

注意:-storetype JKS指定了金鑰庫的型別為java key store。這很重要,如果你指定為其他型別如PKCS12,則Tomcat會報"Invalidkeystore format"錯誤。

開啟tomcat目錄下的server.xml,你可以直接從Finder中開啟它,或者在Eclipse的Servers專案中編輯這個檔案 。

查詢 <Connector port="8443",將此段程式碼的註釋取消:

<Connectorport="8443" protocol="HTTP/1.1" SSLEnabled="true"

maxThreads="150" scheme="https

"secure="true"

clientAuth="false" sslProtocol="TLS" />

在其中加入兩個屬性:

keystoreFile="/Library/Tomcat/apache-tomcat-7.0.14/tomcat.keystore"

keystorePass="123456"

注意,本文中的keytool工具使用的是sun/oracle JDK的版本。如果你的機器上安裝的是GNU jvm,則keytool應該是GNU的版本,則上述server.xml程式碼中還應該加入以下屬性:

keystoreType="gkr"algorithm="JessieX509"

在Eclipse中編輯完server.xml,然後使用“Run As->Run Configurations…->Run”使修改生效,Eclipse會自動重啟Tomcat,但控制檯輸出如下內容時,表明https服務已經啟動:

資訊: StartingProtocolHandler ["http-bio-8443"]

2012-7-9 11:29:01org.apache.coyote.AbstractProtocolHandler start

此時,使用HTTPS埠8443隨便訪問一個頁面,例如: https://localhost:8443/AnyMail/table_css_test.html

此時safari會彈出一個視窗提示使用者,接收伺服器的證書:


點選開啟連結

只有點選“繼續”,使用者才可以訪問該頁面。

如果你使用FireFox,則需要將此頁面新增到例外。

二、iOS 訪問HTTPS

新建Single View Application。在ViewController.xib上拖入一個按鈕,一個UILabel和一個UIWebView,並連線到原始碼。


開啟ViewController.h,宣告如下成員:

NSURLRequest* _request;

NSURLConnectionconnection;

NSString* filePath;

NSOutputStreamfileStream;

NSURL* url,*baseUrl;

NSStringEncoding enc;

NSURLAuthenticationChallenge *_challenge;

_request和connection物件不用多說,我們準備使用URLRequest和URLConnection來請求HTTPS。

程式中將向HTTPS伺服器請求一個html檔案,這個檔案會以臨時檔案的形式儲存到filePath的路徑。fileStream則是檔案輸出流。

url和baseUrl分別指定這個html頁面的url地址和base url地址。

enc是伺服器頁面編碼,本例中將使用GBK編碼。

由於HTTPS伺服器採用了自簽名的證書,iOS無法驗證此類證書, 所以客戶端會向用戶索要一個憑據(即Credential,使用者對此證書採取什麼樣的信任策略)。在Safari中,是通過前圖所示的那個“伺服器證書”視窗來進行的。而在我們的程式中,這個視窗會用一個AlertView替代,伺服器詢問時的相關內容會封裝在一個NSURLAuthenticationChallenge物件中(包括伺服器證書),我們以_challenge成員retain它。

使用NSURLConnection請求一個網頁資源很簡單,這個過程在按鈕的觸控事件中觸發:

- (IBAction)goAction:(id)sender {

_challenge=nil;

filePath = [[[AppDelegatesharedAppDelegate] pathForTemporaryFileWithPrefix:@"Get"]retain];

NSLog(@"filePath=%@",filePath);

fileStream = [[NSOutputStreamalloc]initToFileAtPath:filePathappend:NO];

assert(fileStream != nil);

[fileStreamopen];

_request = [NSURLRequestrequestWithURL:url];

assert(_request != nil);

connection = [NSURLConnectionconnectionWithRequest:_requestdelegate:self];

}

在方法中我先打開了NSOutputStream 物件,以便將網頁寫入到臨時檔案中。這裡本來想實現一種快取機制,不過由於時間原因,我沒有再深究下去,導致多次請求後在tmp資料夾中留下了一堆的臨時檔案。臨時檔案的檔名是以Get+UUID命名的,我把命名方法寫在了AppDelegate裡,希望你能找到並解決這個我遺留下來的問題:


點選開啟連結

重要的是[NSURLConnectionconnectionWithRequest:delegate:]方法的呼叫,我們獲取了一個NSURLConnection物件並將它的delegate設定為self。

self在實現NSULConnectionDelegate方法時,特別實現了connection:canAuthenticateAgainstProtectionSpace:方法,以及connection:didReceiveAuthenticationChallenge:方法:

- (BOOL)connection:(NSURLConnection *)conncanAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace

{

NSLog(@"authenticatemethod:%@",protectionSpace.authenticationMethod);

return [protectionSpace.authenticationMethodisEqualToString:

NSURLAuthenticationMethodServerTrust];

}

- (void)connection:(NSURLConnection *)conndidReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge

{

_challenge=[challenge retain];

UIAlertView* alertView = [[[UIAlertViewalloc] initWithTitle:@"伺服器證書"

message:@"這個網站有一個伺服器證書,點選接受,繼續訪問該網站,如果你不確定,請點選取消"

delegate:self

cancelButtonTitle:@"接受"

otherButtonTitles:@"取消", nil] autorelease];

[alertView show];

}

canAuthenticateAgainstProtectionSpace:方法在連線到一些有安全限制的網站時呼叫,例如:伺服器信任、客戶端證書、HTTP表單驗證等。但URLConnection不知道也沒有強制程式設計師必需處理哪些安全問題,因此它把一個NSURLProtectionSpace物件作為引數傳遞,如果程式設計師想響應某一類安全問題,那麼在這個方法最後就返回YES。你要明白程式設計師可以處理哪些安全問題,你可以檢視NSURLProtectionSpace的authenticationMethod屬性。這是一個NSString屬性,可能取值包括以下常量:

NSString *NSURLAuthenticationMethodDefault;

NSURLAuthenticationMethodHTTPBasic;

NSURLAuthenticationMethodHTTPDigest;

NSURLAuthenticationMethodHTMLForm;

NSURLAuthenticationMethodNegotiate;

NSURLAuthenticationMethodNTLM;

NSURLAuthenticationMethodClientCertificate;

NSURLAuthenticationMethodServerTrust;

當然在這裡,我們只處理“伺服器信任”的安全問題。

didReceiveAuthenticationChallenge方法則緊接第一個方法之後呼叫。如果第一個方法中返回true,那麼URLConnection接下來就呼叫delegate的第二個方法(NO則跳過第二個方法)。

在這裡,我們彈出了一個UIAlertView,提示使用者進行處理。

接下來實現UIAlertView的delegate方法:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex

{

// Accept=0,Cancel=1;

if(buttonIndex==0){

NSURLCredential *   credential;

NSURLProtectionSpaceprotectionSpace;

SecTrustReftrust;

NSString *              host;

SecCertificateRefserverCert;

assert(_challenge !=nil);

protectionSpace = [_challengeprotectionSpace];

assert(protectionSpace != nil);

trust = [protectionSpaceserverTrust];

assert(trust != NULL);

credential = [NSURLCredentialcredentialForTrust:trust];

assert(credential != nil);

host = [[_challengeprotectionSpace] host];

if (SecTrustGetCertificateCount(trust) > 0) {

serverCert = SecTrustGetCertificateAtIndex(trust, 0);

} else {

serverCert = NULL;

}

[[_challengesender] useCredential:credential forAuthenticationChallenge:_challenge];

}

}

這個方法中,如果使用者選擇“接受”,則我們從NSURLAuthenticationChallenge物件中獲取伺服器證書,並將該證書應用於URLConnection,接下來會繼續呼叫URLConnection的其他delegate方法。如果使用者選擇“取消”,則會導致伺服器返回一個錯誤,這會呼叫connection:didFailedWithError:方法。

其它方法請自行參考原始碼:資源下載