上一篇中我們介紹了一些HTTP的認證機制,但是其安全性相對都不甚理想。本篇文章會介紹一種更爲複雜和安全的技術。其具備以下一些特性:服務器認證,客戶端認證,完整性,加密,效率,普適性,管理的可擴展性,適應性,可行性。HTTPS就是目前最流行的一種HTTP安全形式。
使用 HTTPS 時,所有的 HTTP 請求和響應數據在發送到網絡之前,都要進行加密。HTTPS 在 HTTP 下面提供了一個傳輸級的密碼安全層,具體實現可以使用 SSL,也可以使用其後繼者——傳輸層安全(Transport Layer Security,TLS)(在本篇文章中我們直接統稱爲SSL)。整個傳輸的層級結構如下:
可以看到,我們的傳輸加密層位於HTTP和TCP之間。HTTP的報文不直接給到TCP傳輸,在此之前經過SSL加密處理,然後再通過TCP連接傳輸。收到報文同樣先通過SSL解密後,再給到HTTP應用。
從上面的描述可以看到,HTTPS的關鍵就是SSL,SSL的關鍵就是加密和解密,所以接下來我們先大致簡單介紹加解密相關的一些東西,再說SSL是怎麼應用到HTTP中的。
1 數字加密
這一小節我們先簡單介紹一些密碼學中的概念,有個瞭解就行。
密碼學是對報文進行編 / 解碼的機制與技巧。但密碼學所能做的還不僅僅是加密報文以防止好事者的讀取。我們還可以用它來防止對報文的篡改(報文的完整性驗證),甚至還可以用密碼學來證明某條報文確實是某個客戶端發出的,而不是其他人僞造的,就好像給報文帶上了你的個人簽名一樣。
密碼學基於一種名爲密碼(cipher)的祕密代碼。從密碼學的角度來理解密碼的話(注意和日常生活中所說的密碼進行區分),其本質是一套編碼方案——一種特殊的報文編碼方式和一種稍後使用的相應解碼方式的總稱。加密之前的原始報文通常被稱爲明文(plaintext 或 cleartext)。使用了密碼之後的編碼報文通常被稱作密文(ciphertext)。
這裏我們舉個簡單的例子,就是將明文中所有的字母向後移動3個字母后的內容作爲密文,解碼的時候,將密文的字母向左移動三個字母,就能獲得明文了。如下圖所示:
密碼機專門用密碼來快速、精確地對報文進行編解碼的機器。
我們的加密內容,如果被其他人截取,如果截取者有加密算法和對應的密碼機就能能很容易的解碼我們的加密內容,從而獲取明文內容。所以後來,又提出了號盤的工作機制(也就是我們現在所說的祕鑰),如果沒有號盤,有了算法和密碼機也不能對密文進行解碼。以上面的加密示例爲例,祕鑰可以理解成我們需要移動字母的個數,這裏我們舉例中移動的是3個,也可以設置成其他個數,如下圖所示:
2 常用加密技術
2.1 對稱密鑰加密技術
大體上我們現在一般將加密技術分爲對稱加密和不對稱加密。
這裏我們先看對稱密鑰加密技術,所謂對稱密鑰加密技術,即編碼(這裏稱之爲e,後面大部分時候也會使用e或者E代表編碼)時使用的密鑰值和解碼(這裏稱之爲d,後面大部分時候會使用d或者D代表解碼)時一樣(e=d),這裏的密鑰就統稱爲k。
使用對稱加密技術,接收端(一般爲解碼端)和發送端(一般爲編碼端)需要共享密鑰k,這樣才能在兩端正確進行通信。如下圖所示:
常見的對稱加密算法有:DES,Triple-DES,RC2和RC4。
由於對稱加密技術,必須在接收端和發送端共享密鑰,這裏放在HTTP環境來說,就是服務器端和客戶端必須共享密鑰。一般爲了安全,每個客戶端和服務端的共享密鑰都是特有的(如:A客戶端和B客戶端都想與服務器建立通信,但是A客戶端和B客戶端所使用的密鑰一般是不同的),這種情況下,如果有很多客戶端都想與服務器建立通信,那服務器端需要維護密鑰數量就很龐大了。如果我們再將對稱加密應用到所有網絡節點之間都可以兩兩通信的情形,那就更復雜了,假如有N個節點,總的就需要要維護N2個密鑰。這也是對稱加密的一個缺點。
2.2 公開密鑰加密技術
上面介紹了對稱加密技術,這裏接着說下非對稱加密技術,常見的就是公開密鑰加密技術的形式。公開密鑰加密技術使用兩個非對稱密鑰:編碼密鑰是開放的,衆所周知的(所以稱之爲公開密鑰),但是解密密鑰就只有服務器才知道。這種情況下,所有的客戶端都能獲取同一個密鑰對報文進行加密,但是隻有服務器能進行解密。這樣就解決了對稱加密中維護過多密鑰的問題。如下圖所示:
目前比較流行的公開密鑰加密算法就是RSA了,該算法可以滿足及時獲取瞭如下幾個信息,也不能破解加密。
- 公開密鑰(是公有的,所有人都可以獲得);
- 小片攔截下來的密文(可通過對網絡的嗅探獲取);
- 條報文及與之相關的密文(對任意一段文本運行加密器就可以得到)。
雖然公開密鑰加密技術有這些優點,但是速度卻是它的弱項,所以實際操作中,很多時候會混用兩種加密技術,比如:在兩節點間通過便捷的公開密鑰加密技術建立起安全通信,然後再用那條安全的通道產生併發送臨時的隨機對稱密鑰,通過更快的對稱加密技術對其餘的數據進行加密。
2.3 密鑰長度和枚舉攻擊
在很多情況下,編 / 解碼算法都是衆所周知的,因此密鑰就是唯一保密的東西了。所以我們密鑰的好壞影響了我們密文的破解難度。
一些攻擊者會嘗試遍歷每一個可能的密鑰進行破解,這種攻擊方式被稱之枚舉攻擊(enumeration attack)。如果密鑰只有幾種可能,很快就會被破解,如果密鑰的可能值足夠大,那攻擊者的破解的可能性就會大大降低。
可用密鑰值的數量取決於密鑰中的位數,以及可能的密鑰中有多少是有效的。一般來說對稱密鑰加密技術中,通常所有的密鑰值都是有效的。而公開密鑰加密技術中,可能只有某些密鑰值纔是有效的(如RSA中,有效密鑰 必須以某種方式與質數相關)。1 8 位的密鑰只有 256 個可能的密鑰值,40 位的密鑰可以有 240 個可能的密鑰值(大約是一萬億個密鑰),128 位的密鑰能夠產生340 000 000 000 000 000 000 000 000 000 000 000 000 個可能的密鑰值。
3 數字簽名
上面介紹了兩種常用的加密技術,除了對報文進行加解密外,我們還可以對報文進行簽名,以說明報文是發自誰,同時還可以防止報文被篡改。這種技術就被稱爲數字簽名(digital signing)。
一般數字簽名是用非對稱公開密鑰加密技術產生,因爲只有作者本身才知道私鑰,所以可以將私鑰作爲作者的身份證明。示例如下:
從上圖看到,發送方(A)將報文摘要用私鑰進行簽名(私鑰和摘要作爲輸入,使用解碼函數D),接收方(B)收到後先使用公鑰將前面還原爲報文摘要(是要公鑰和簽名作爲輸入,使用編碼函數E),然後同摘要對比,如果一致說明發送者確實爲A,且沒有被篡改過。
這裏將解碼函數D作爲簽名函數使用,因爲在上述介紹公開密鑰加密技術的實時也提到了,一般密鑰是用於解碼函數的輸入的。因爲我們的加密技術中的編碼(E)和解碼(D)互爲反函數,所以先解碼,再編碼一樣可以還原內容,即:
4 數字證書
數字證書,顧名思義,就是一種在網上用的證書而已。該證書一般是由某個受信任的、權威的組織機構頒發的,包含了對某個公司組織一些“證明信息”的擔保。
4.1 數字證書的內容
數字證書中還包含一組信息,所有這些信息都是由一個官方的“證書頒發機構”以數字方式簽發的。通常包含以下幾部分內容:
4.2 X.509 v3證書
上面介紹的是數字證書通常包含的部分內容,但是目前數字證書並沒有一個統一的標準。所以每個組織機構簽發的證書都會有一些細微的差別,不過大多數權威機構簽發的證書一般都默認遵守了X.509 v3標準。X.509 v3證書中的字段信息如下表所示:
字 段 | 描 述 |
---|---|
版本 | 這個證書的 X.509 證書版本號。現在使用的通常都是版本3 |
序列號 | 證書頒發機構(CA)生成的唯一整數。CA 生成的每個證書都要有一個唯一的序列號 |
簽名算法 ID | 簽名所使用的加密算法。例如,“用 RSA 加密的 MD2 摘要” |
證書頒發者 | 發佈並簽署這個證書的組織名稱,以 X.500 格式表示 |
有效期 | 此證書何時有效,由一個起始日期和一個結束日期來表示 |
對象名稱 | 證書中描述的實體,比如一個人或一個組織。對象名稱是以 X.500 格式表示的 |
對象的公開密鑰信息 | 證書對象的公開密鑰,公開密鑰使用的算法,以及所有附加參數 |
發佈者唯一的ID(可選) | 可選的證書發佈者唯一標識符,這樣就可以重用相同的發佈者名稱 |
對象唯一的ID(可選) | 可選的證書對象唯一標識符,這樣就可以重用相同的對象名稱了 |
擴展 | 可選的擴展字段集(在版本 3 及更高的版本中使用)。每個擴展字段都被標識爲關鍵或非關鍵的。關鍵擴展非常重要,證書使用者一定要能夠理解。如果證書使用者無法識別出關鍵擴展字段,就必須拒絕這個證書。目前在使用的常用擴展字段包括: 基本約束:對象與證書頒發機構的關係 證書策略:授予證書的策略 密鑰的使用:對公開密鑰使用的限制 |
證書的頒發機構簽名 | 證書頒發機構用指定的簽名算法對上述所有字段進行的數字簽名 |
4.3 通過數字證書對Web服務器進行驗證
通過 HTTPS 建立了一個安全 Web 事務之後,客戶端就可以獲取所連接服務器的數字證書。如果服務器沒有需要的證書,就可以斷開連接。一般在服務器證書中包含以下信息:
- Web站點的名稱和主機名
- Web站點的公開密鑰
- 簽名頒發機構的名稱
- 來自簽名頒發機構的數字簽名
客戶端收到證書後,會對以上內容進行檢查。如果簽發機構不夠權威或者不受客戶端信任,客戶端可以詢問用戶是否信任,或者直接斷開連接。如果簽發機構是受信任的,那麼客戶端接下來就可以像之前討論的那樣,使用公鑰對簽發機構的簽名進行驗證,以確認該證書是真正出自該受信任的機構和證書的完整性。整體流程如下:
5 HTTPS介紹
前面講了一些HTTPS中需要用到的一些基礎知識。有了前面知識的鋪墊,本節我們就可以來介紹一些HTTPS知識了。
5.1 HTTPS概述
目前來說,HTTPS是最流行的一種HTTP安全技術。它是將我們的HTTP和前面介紹的加密技術、數字簽名等結合起來,形成的一種安全、靈活的管理機制。
如本篇文章開頭所示,HTTPS就是在HTTP和TCP之間,加入了一層安全層,對傳輸內容進行了加密處理。雖然目前HTTPS是一種可選方案,對Web的請求一樣可以使用HTTP。但是可以看到,HTTPS是一種趨勢,現在很多大型,流行的Web站點,都已經要求使用HTTPS進行通信了。所以我們去了解一下HTTPS的東西還是很有必要的。
這裏我們將HTTP和TCP中間的安全層統稱爲SSL,SSL是一種二進制協議,不像HTTP是一種文本協議。同時,其承載的端口默認是443,而不是HTTP的80端口,如果我們直接通過80端口傳輸SSL數據,很有可能會被防火牆攔截。
5.2 建立安全傳輸
在普通的 HTTP連接 中,客戶端會打開一條到 Web 服務器端口 80 的 TCP 連接,發送一條請求報文,接收一條響應報文,然後關閉連接。
再HTTPS中,由於 SSL 安全層的存在,過程會略微複雜一些。客戶端首先打開一條到 Web 服務器端口 443(安全 HTTP 的默認端口)的連接。一旦建立了 TCP 連接,客戶端和服務器就會初始化 SSL 層,對加密參數進行溝通,並交換密鑰。握手完成之後,SSL 初始化就完成了,客戶端就可以將請求報文發送給安全層了。在將這些報文發送給 TCP 之前,要先對其進行加密。
HTTP和HTTPS建立連接的過程對比如下圖所示:
SSL在建立連接的時候,有個握手機制,必須完成握手之後才能正常傳輸後面的報文內容。整個握手過程中主要處理一下幾件事情:
- 交換協議版本號
- 選擇一個兩端都瞭解的密碼
- 對兩端的身份進行驗證
- 生成臨時的密鑰,以便加密通信
簡單的握手流程如圖所示:
5.3 服務器證書
SSL是支持雙向認證的,即客戶端可以驗證服務器證書,服務器也可以驗證客戶端證書。但是客戶端證書在實際當中較少用到。這裏主要看下服務器證書。
服務器證書是一個顯示了組織的名稱、地址、服務器DNS域名以及其他信息的X.509 v3派生證書,如下圖所示:
有了這些信息,客戶端就能確認服務器是否是真正自己所想要對話的服務器,確保對話的可信性和安全性。
5.4 站點證書有效性檢查
SSL協議沒有強制要求對證書的有效性做檢查,但是大部分客戶端都會對證書做一些簡單的檢查。基本步驟如下:
- 日期檢測:收到證書後會對證書的有效期進行檢查,驗證證書是否還在有效期範圍內;
- 簽名頒發者的可信度:每個證書都是由某個機構或組織簽發的。任何人也都可以生成證書,所以我們需要對證書的頒發者進行檢測,驗證頒發者的可信度。客戶端可以維護一個可信頒發者列表,如果當前證書頒發者是可信列表中的,一般認爲其證書是可信的,否則需要做一些其他操作,如:提醒用戶,讓用戶決定該證書是否可信,或者直接斷開連接;
- 簽名檢測:簽名學習證書的時候說過,每個證書都會附上一個證書籤名。客戶端可以利用收到的公鑰對證書相關內容進行編碼後得到校驗碼,與證書的簽名進行對比,以驗證證書的完整性,防止被篡改過;
- 站點身份檢測:上面提到,服務器證書一般包含有站點的域名等信息。這個時候我們可以檢測證書中的站點信息是否和我們當前對話的站點信息一致,如果不匹配,說明有可能是一個假冒服務器,所以就有必要提示用戶,或者斷開連接。
5.5 虛擬主機的問題
在5.4中提到了,一般會對站點信息進行檢測。如果是使用的虛擬主機,可能會出現一些問題。比如:一個服務器承載多個站點,每個站點有自己的域名信息,其中一個爲cajun-shop.securesites.com,但是提供給用戶訪問的是http://www.cajun-shop.com,可能服務器又只有一個證書,此時這個證書是裏面的站點信息是該服務器的信息(主機名爲:*.securesites.com),此時我們訪問http://www.cajun-shop.com地址的時候,就會出現訪問的主機名和證書的主機名不相符的情況。這個可以選擇提示用戶,讓用戶進行操作,但是每次都這樣的話,就會很麻煩。所以一般這種時候,服務器可能會選擇在開始處理安全事務時,將所有用戶都重定向到cajun-shop.securesites.com。
6 HTTPS客戶端
SSL是一個二進制協議,而且其中一些加加解密的操作也比較繁瑣。所以要自己實現SSL協議也很繁瑣,還需要一些密碼學的相關知識。不過現在已經有現成開源庫可供我們使用,在《HTTP權威指南》提到的是OpenSSL。下面是摘自書中的一個C語言實現的簡單客戶端代碼:
/**********************************************************************
* https_client.c --- very simple HTTPS client with no error checking
* usage: https_client servername
**********************************************************************/
#include <stdio.h>
#include <memory.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <openssl/crypto.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
void main(int argc, char **argv)
{
SSL *ssl;
SSL_CTX *ctx;
SSL_METHOD *client_method;
X509 *server_cert;
int sd,err;
char *str,*hostname,outbuf[4096],inbuf[4096],host_header[512];
struct hostent *host_entry;
struct sockaddr_in server_socket_address;
struct in_addr ip;
/*========================================*/
/* (1) initialize SSL library */
/*========================================*/
SSLeay_add_ssl_algorithms( );
client_method = SSLv2_client_method( );
SSL_load_error_strings( );
ctx = SSL_CTX_new(client_method);
printf("(1) SSL context initialized\n\n");
/*=============================================*/
/* (2) convert server hostname into IP address */
/*=============================================*/
hostname = argv[1];
host_entry = gethostbyname(hostname);
bcopy(host_entry->h_addr, &(ip.s_addr), host_entry->h_length);
printf("(2) '%s' has IP address '%s'\n\n", hostname, inet_ntoa(ip));
/*=================================================*/
/* (3) open a TCP connection to port 443 on server */
/*=================================================*/
sd = socket (AF_INET, SOCK_STREAM, 0);
memset(&server_socket_address, '\0', sizeof(server_socket_address));
server_socket_address.sin_family = AF_INET;
server_socket_address.sin_port = htons(443);
memcpy(&(server_socket_address.sin_addr.s_addr),
host_entry->h_addr, host_entry->h_length);
err = connect(sd, (struct sockaddr*) &server_socket_address,
sizeof(server_socket_address));
if (err < 0) { perror("can't connect to server port"); exit(1); }
printf("(3) TCP connection open to host '%s', port %d\n\n",
hostname, server_socket_address.sin_port);
/*========================================================*/
/* (4) initiate the SSL handshake over the TCP connection */
/*========================================================*/
ssl = SSL_new(ctx); /* create SSL stack endpoint */
SSL_set_fd(ssl, sd); /* attach SSL stack to socket */
err = SSL_connect(ssl); /* initiate SSL handshake */
printf("(4) SSL endpoint created & handshake completed\n\n");
/*============================================*/
/* (5) print out the negotiated cipher chosen */
/*============================================*/
printf("(5) SSL connected with cipher: %s\n\n", SSL_get_cipher(ssl));
/*========================================*/
/* (6) print out the server's certificate */
/*========================================*/
server_cert = SSL_get_peer_certificate(ssl);
printf("(6) server's certificate was received:\n\n");
str = X509_NAME_oneline(X509_get_subject_name(server_cert), 0, 0);
printf(" subject: %s\n", str);
str = X509_NAME_oneline(X509_get_issuer_name(server_cert), 0, 0);
printf(" issuer: %s\n\n", str);
/* certificate verification would happen here */
X509_free(server_cert);
/*********************************************************/
/* (7) handshake complete --- send HTTP request over SSL */
/*********************************************************/
sprintf(host_header,"Host: %s:443\r\n",hostname);
strcpy(outbuf,"GET / HTTP/1.0\r\n");
strcat(outbuf,host_header);
strcat(outbuf,"Connection: close\r\n");
strcat(outbuf,"\r\n");
err = SSL_write(ssl, outbuf, strlen(outbuf));
shutdown (sd, 1); /* send EOF to server */
printf("(7) sent HTTP request over encrypted channel:\n\n%s\n",outbuf);
/**************************************************/
/* (8) read back HTTP response from the SSL stack */
/**************************************************/
err = SSL_read(ssl, inbuf, sizeof(inbuf) - 1);
inbuf[err] = '\0';
printf ("(8) got back %d bytes of HTTP response:\n\n%s\n",err,inbuf);
/************************************************/
/* (9) all done, so close connection & clean up */
/************************************************/
SSL_shutdown(ssl);
close (sd);
SSL_free (ssl);
SSL_CTX_free (ctx);
printf("(9) all done, cleaned up and closed connection\n\n");
}
這個實現很簡單,代碼裏面的註釋也分步驟寫的很清楚了,這裏就不再贅述了。
7 通過代理以隧道形式傳輸安全流量
現在很多客戶端都會設置代理,通過代理進行服務器訪問。如果我們使用SSL進行安全加密,只要客戶端開始用服務器的公開密鑰對發往服務器的數據進行加密,代理就再也不能讀取 HTTP 首部了!代理不能讀取 HTTP 首部,就無法知道應該將請求轉向何處了。
爲了使 HTTPS 與代理配合工作,一般可以使用 HTTPS 隧道協議,客戶端首先要告知代理,它想要連接的安全主機和端口。這是在開始加密之前,以明文形式告知的,所以代理可以理解這條信息。大體流程如下:
- HTTP 通過新的名爲 CONNECT 的擴展方法來發送明文形式的端點信息
- 代理收到該條請求後,會打開一條道服務器的連接
- 完成上述工作後,直接在客戶端和服務器之間以隧道形式傳輸SSL流量了
關於HTTP隧道的知識前面介紹代理的章節已經講過,這裏就不再詳細說明了。