上一篇中我们介绍了一些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中的。
这一小节我们先简单介绍一些密码学中的概念,有个了解就行。
密码学是对报文进行编 / 解码的机制与技巧。但密码学所能做的还不仅仅是加密报文以防止好事者的读取。我们还可以用它来防止对报文的篡改(报文的完整性验证),甚至还可以用密码学来证明某条报文确实是某个客户端发出的,而不是其他人伪造的,就好像给报文带上了你的个人签名一样。
密码学基于一种名为密码(cipher)的秘密代码。从密码学的角度来理解密码的话(注意和日常生活中所说的密码进行区分),其本质是一套编码方案——一种特殊的报文编码方式和一种稍后使用的相应解码方式的总称。加密之前的原始报文通常被称为明文(plaintext 或 cleartext)。使用了密码之后的编码报文通常被称作密文(ciphertext)。
这里我们举个简单的例子,就是将明文中所有的字母向后移动3个字母后的内容作为密文,解码的时候,将密文的字母向左移动三个字母,就能获得明文了。如下图所示:
密码机专门用密码来快速、精确地对报文进行编解码的机器。
我们的加密内容,如果被其他人截取,如果截取者有加密算法和对应的密码机就能能很容易的解码我们的加密内容,从而获取明文内容。所以后来,又提出了号盘的工作机制(也就是我们现在所说的秘钥),如果没有号盘,有了算法和密码机也不能对密文进行解码。以上面的加密示例为例,秘钥可以理解成我们需要移动字母的个数,这里我们举例中移动的是3个,也可以设置成其他个数,如下图所示:
大体上我们现在一般将加密技术分为对称加密和不对称加密。
这里我们先看对称密钥加密技术,所谓对称密钥加密技术,即编码(这里称之为e,后面大部分时候也会使用e或者E代表编码)时使用的密钥值和解码(这里称之为d,后面大部分时候会使用d或者D代表解码)时一样(e=d),这里的密钥就统称为k。
使用对称加密技术,接收端(一般为解码端)和发送端(一般为编码端)需要共享密钥k,这样才能在两端正确进行通信。如下图所示:
常见的对称加密算法有:DES,Triple-DES,RC2和RC4。
由于对称加密技术,必须在接收端和发送端共享密钥,这里放在HTTP环境来说,就是服务器端和客户端必须共享密钥。一般为了安全,每个客户端和服务端的共享密钥都是特有的(如:A客户端和B客户端都想与服务器建立通信,但是A客户端和B客户端所使用的密钥一般是不同的),这种情况下,如果有很多客户端都想与服务器建立通信,那服务器端需要维护密钥数量就很庞大了。如果我们再将对称加密应用到所有网络节点之间都可以两两通信的情形,那就更复杂了,假如有N个节点,总的就需要要维护N2个密钥。这也是对称加密的一个缺点。
上面介绍了对称加密技术,这里接着说下非对称加密技术,常见的就是公开密钥加密技术的形式。公开密钥加密技术使用两个非对称密钥:编码密钥是开放的,众所周知的(所以称之为公开密钥),但是解密密钥就只有服务器才知道。这种情况下,所有的客户端都能获取同一个密钥对报文进行加密,但是只有服务器能进行解密。这样就解决了对称加密中维护过多密钥的问题。如下图所示:
目前比较流行的公开密钥加密算法就是RSA了,该算法可以满足及时获取了如下几个信息,也不能破解加密。
虽然公开密钥加密技术有这些优点,但是速度却是它的弱项,所以实际操作中,很多时候会混用两种加密技术,比如:在两节点间通过便捷的公开密钥加密技术建立起安全通信,然后再用那条安全的通道产生并发送临时的随机对称密钥,通过更快的对称加密技术对其余的数据进行加密。
在很多情况下,编 / 解码算法都是众所周知的,因此密钥就是唯一保密的东西了。所以我们密钥的好坏影响了我们密文的破解难度。
一些攻击者会尝试遍历每一个可能的密钥进行破解,这种攻击方式被称之枚举攻击(enumeration attack)。如果密钥只有几种可能,很快就会被破解,如果密钥的可能值足够大,那攻击者的破解的可能性就会大大降低。
可用密钥值的数量取决于密钥中的位数,以及可能的密钥中有多少是有效的。一般来说对称密钥加密技术中,通常所有的密钥值都是有效的。而公开密钥加密技术中,可能只有某些密钥值才是有效的(如RSA中,有效密钥 必须以某种方式与质数相关)。1 8 位的密钥只有 256 个可能的密钥值,40 位的密钥可以有 240 个可能的密钥值(大约是一万亿个密钥),128 位的密钥能够产生340 000 000 000 000 000 000 000 000 000 000 000 000 个可能的密钥值。
上面介绍了两种常用的加密技术,除了对报文进行加解密外,我们还可以对报文进行签名,以说明报文是发自谁,同时还可以防止报文被篡改。这种技术就被称为数字签名(digital signing)。
一般数字签名是用非对称公开密钥加密技术产生,因为只有作者本身才知道私钥,所以可以将私钥作为作者的身份证明。示例如下:
从上图看到,发送方(A)将报文摘要用私钥进行签名(私钥和摘要作为输入,使用解码函数D),接收方(B)收到后先使用公钥将前面还原为报文摘要(是要公钥和签名作为输入,使用编码函数E),然后同摘要对比,如果一致说明发送者确实为A,且没有被篡改过。
这里将解码函数D作为签名函数使用,因为在上述介绍公开密钥加密技术的实时也提到了,一般密钥是用于解码函数的输入的。因为我们的加密技术中的编码(E)和解码(D)互为反函数,所以先解码,再编码一样可以还原内容,即: E ( D ( d a t a ) ) = D ( E ( d a t a ) ) E(D(data)) = D(E(data)) E(D(data))=D(E(data))
数字证书,顾名思义,就是一种在网上用的证书而已。该证书一般是由某个受信任的、权威的组织机构颁发的,包含了对某个公司组织一些“证明信息”的担保。
数字证书中还包含一组信息,所有这些信息都是由一个官方的“证书颁发机构”以数字方式签发的。通常包含以下几部分内容:
上面介绍的是数字证书通常包含的部分内容,但是目前数字证书并没有一个统一的标准。所以每个组织机构签发的证书都会有一些细微的差别,不过大多数权威机构签发的证书一般都默认遵守了X.509 v3标准。X.509 v3证书中的字段信息如下表所示:
字 段 | 描 述 |
---|---|
版本 | 这个证书的 X.509 证书版本号。现在使用的通常都是版本3 |
序列号 | 证书颁发机构(CA)生成的唯一整数。CA 生成的每个证书都要有一个唯一的序列号 |
签名算法 ID | 签名所使用的加密算法。例如,“用 RSA 加密的 MD2 摘要” |
证书颁发者 | 发布并签署这个证书的组织名称,以 X.500 格式表示 |
有效期 | 此证书何时有效,由一个起始日期和一个结束日期来表示 |
对象名称 | 证书中描述的实体,比如一个人或一个组织。对象名称是以 X.500 格式表示的 |
对象的公开密钥信息 | 证书对象的公开密钥,公开密钥使用的算法,以及所有附加参数 |
发布者唯一的ID(可选) | 可选的证书发布者唯一标识符,这样就可以重用相同的发布者名称 |
对象唯一的ID(可选) | 可选的证书对象唯一标识符,这样就可以重用相同的对象名称了 |
扩展 | 可选的扩展字段集(在版本 3 及更高的版本中使用)。每个扩展字段都被标识为关键或非关键的。关键扩展非常重要,证书使用者一定要能够理解。如果证书使用者无法识别出关键扩展字段,就必须拒绝这个证书。目前在使用的常用扩展字段包括: 基本约束:对象与证书颁发机构的关系 证书策略:授予证书的策略 密钥的使用:对公开密钥使用的限制 |
证书的颁发机构签名 | 证书颁发机构用指定的签名算法对上述所有字段进行的数字签名 |
通过 HTTPS 建立了一个安全 Web 事务之后,客户端就可以获取所连接服务器的数字证书。如果服务器没有需要的证书,就可以断开连接。一般在服务器证书中包含以下信息:
客户端收到证书后,会对以上内容进行检查。如果签发机构不够权威或者不受客户端信任,客户端可以询问用户是否信任,或者直接断开连接。如果签发机构是受信任的,那么客户端接下来就可以像之前讨论的那样,使用公钥对签发机构的签名进行验证,以确认该证书是真正出自该受信任的机构和证书的完整性。整体流程如下:
前面讲了一些HTTPS中需要用到的一些基础知识。有了前面知识的铺垫,本节我们就可以来介绍一些HTTPS知识了。
目前来说,HTTPS是最流行的一种HTTP安全技术。它是将我们的HTTP和前面介绍的加密技术、数字签名等结合起来,形成的一种安全、灵活的管理机制。
如本篇文章开头所示,HTTPS就是在HTTP和TCP之间,加入了一层安全层,对传输内容进行了加密处理。虽然目前HTTPS是一种可选方案,对Web的请求一样可以使用HTTP。但是可以看到,HTTPS是一种趋势,现在很多大型,流行的Web站点,都已经要求使用HTTPS进行通信了。所以我们去了解一下HTTPS的东西还是很有必要的。
这里我们将HTTP和TCP中间的安全层统称为SSL,SSL是一种二进制协议,不像HTTP是一种文本协议。同时,其承载的端口默认是443,而不是HTTP的80端口,如果我们直接通过80端口传输SSL数据,很有可能会被防火墙拦截。
在普通的 HTTP连接 中,客户端会打开一条到 Web 服务器端口 80 的 TCP 连接,发送一条请求报文,接收一条响应报文,然后关闭连接。
再HTTPS中,由于 SSL 安全层的存在,过程会略微复杂一些。客户端首先打开一条到 Web 服务器端口 443(安全 HTTP 的默认端口)的连接。一旦建立了 TCP 连接,客户端和服务器就会初始化 SSL 层,对加密参数进行沟通,并交换密钥。握手完成之后,SSL 初始化就完成了,客户端就可以将请求报文发送给安全层了。在将这些报文发送给 TCP 之前,要先对其进行加密。
HTTP和HTTPS建立连接的过程对比如下图所示:
SSL在建立连接的时候,有个握手机制,必须完成握手之后才能正常传输后面的报文内容。整个握手过程中主要处理一下几件事情:
SSL是支持双向认证的,即客户端可以验证服务器证书,服务器也可以验证客户端证书。但是客户端证书在实际当中较少用到。这里主要看下服务器证书。
服务器证书是一个显示了组织的名称、地址、服务器DNS域名以及其他信息的X.509 v3派生证书,如下图所示:
有了这些信息,客户端就能确认服务器是否是真正自己所想要对话的服务器,确保对话的可信性和安全性。
SSL协议没有强制要求对证书的有效性做检查,但是大部分客户端都会对证书做一些简单的检查。基本步骤如下:
在5.4中提到了,一般会对站点信息进行检测。如果是使用的虚拟主机,可能会出现一些问题。比如:一个服务器承载多个站点,每个站点有自己的域名信息,其中一个为cajun-shop.securesites.com,但是提供给用户访问的是http://www.cajun-shop.com,可能服务器又只有一个证书,此时这个证书是里面的站点信息是该服务器的信息(主机名为:*.securesites.com),此时我们访问http://www.cajun-shop.com地址的时候,就会出现访问的主机名和证书的主机名不相符的情况。这个可以选择提示用户,让用户进行操作,但是每次都这样的话,就会很麻烦。所以一般这种时候,服务器可能会选择在开始处理安全事务时,将所有用户都重定向到cajun-shop.securesites.com。
SSL是一个二进制协议,而且其中一些加加解密的操作也比较繁琐。所以要自己实现SSL协议也很繁琐,还需要一些密码学的相关知识。不过现在已经有现成开源库可供我们使用,在《HTTP权威指南》提到的是OpenSSL。下面是摘自书中的一个C语言实现的简单客户端代码:
/**********************************************************************
* https_client.c --- very simple HTTPS client with no error checking
* usage: https_client servername
**********************************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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");
}
这个实现很简单,代码里面的注释也分步骤写的很清楚了,这里就不再赘述了。
现在很多客户端都会设置代理,通过代理进行服务器访问。如果我们使用SSL进行安全加密,只要客户端开始用服务器的公开密钥对发往服务器的数据进行加密,代理就再也不能读取 HTTP 首部了!代理不能读取 HTTP 首部,就无法知道应该将请求转向何处了。
为了使 HTTPS 与代理配合工作,一般可以使用 HTTPS 隧道协议,客户端首先要告知代理,它想要连接的安全主机和端口。这是在开始加密之前,以明文形式告知的,所以代理可以理解这条信息。大体流程如下:
关于HTTP隧道的知识前面介绍代理的章节已经讲过,这里就不再详细说明了。