本文将尝试从“如何实现”的角度,通过从零开始设计一个安全的通信协议的方式,帮助读者加快对信息安全的理解。
HTTP 面临的三个安全问题分别是:eavesdropping(窃听),tampering(篡改),message forgery(信息伪造)。
浏览器和服务器进行信息传递时,会通过第三方转发信息。
此时,信息安全面临三个问题:
为了防止第三方伪造信息,我们很容易的想到通过 签名 的方式。
讲解签名之前,我们需要先对非对称加密方式以及消息摘要:(Digital Digest)有所了解。
对称加密是通过同一份密钥加密和解密数据,而非对称加密则有两份密钥,分别是公钥和私钥,用公钥加密的数据,要用私钥才能解密,用私钥加密的数据,要用公钥才能解密。
常用的非对称加密方式有:RSA、ECC。
具体的原理,这里不进行展开。我们只需要了解用于签名的非对称加密需要满足的两个特性即可:
非对称加密通常对等待加密的信息长度有要求,所以,我们一般只对消息摘要加密。
消息摘要:(Digital Digest)又称为指纹(Finger Print)。可以通过单向哈希(one-way hash)函数,为不定长度的信息生成一个固定长度的摘要。
有了以上知识,我们来看一下数字签名是如何进行的?
签名:
通对某一份数据进行单向哈希,缩短等待加密信息的长度=》单项哈希
通过私钥对信息摘要进行加密运算并生成签名,表示我认可了这份数据(只有我拥有私钥,第三方难以伪造)=》签名
验签:
通过公钥解析签名
对数据进行单向哈希
判等
我们可以模拟一下会话的握手阶段的通信流程:
浏览器:hello
服务器:服务器公钥 + encrypt(服务器公钥,服务器私钥)
浏览器:encrypt(hash(信息),服务器公钥) + 信息
服务器:encrypt(hash(信息),服务器私钥) + 信息
上面的通信方式是不是安全多了?
且慢,如果服务器返回公钥时,被第三方拦截,然后替换为第三方的公钥,我们该如何怎么办?
考虑到把所有网站的公钥提前存储到浏览器中并不现实(数量巨大,并且新增公钥、撤销公钥都极为不便)。我们可以提前浏览器内置一份或多份公钥(根证书),然后再把流程升级一下:
会话的握手阶段的通信流程:
浏览器:hello
服务器:服务器公钥 + encrypt(服务器公钥,第三方私钥)
浏览器:encrypt(hash(信息),服务器公钥) + 信息
服务器:encrypt(hash(信息),服务器私钥) + 信息
但是,签名在解决信息伪造和篡改的同时又引入了另外一个问题:性能消耗。
虽然只需要对摘要信息进行签名,但是,它依然给服务器带来了非常巨大的运算压力。
一般情况下,签名操作会导致服务器的处理速度变为原来的万分之一甚至更低(根据算法的不同,实际情况可能会有数量级的变化)。
并且,它还面临一个非常巨大安全问题:窃听。第三方仍然可以看到通信内容。
既然非对称加密对性能影响巨大,剩下的唯一方案是对称加密。
因为服务器需要和数量众多的浏览器进行通信,所以,每条通信用到的对称加密密钥都应该是不同的。否则,浏览器和服务器之间的通信依然会被第三方解密。
引入对称加密后,就可以再次升级通信流程:
会话的握手阶段的通信流程:
浏览器:hello
服务器:服务器公钥 + encrypt(服务器公钥,第三方私钥) + 第一个随机数
浏览器:encrypt(第二个随机数,服务器公钥)
双方根据PRF算法生成一个对称加密的密钥并用于之后的通信:
浏览器:encrypt(hash(信息),对称密钥) +encrypt(信息,对称密钥)
服务器:encrypt(hash(信息),对称密钥) +encrypt(信息,对称密钥)
这种双方各自生成一个随机数的方式可以应对浏览器或者服务器单方面出现漏洞(随机数不随机)的情况。
与此同时,在一次会话的建立中,服务器只需要解析一次就可以完成整个会话。
安全故事:1996年,研究人员就发现了网景浏览器1.1的伪随机数发生器仅仅利用了三个参数:当天的时间,进程ID和父进程ID。在1996年,利用当时的机器仅需要25秒钟的时间就可以破解一个SSL通信
OK#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char *argv[])
{
int sockfd,new_socket
int sock_value
char buf[] = "hello! China!I Love You\n"
struct sockaddr_in client_
struct sockaddr_in server_
int SIZE = sizeof(struct sockaddr_in)
if(argc != 2){
fprintf(stderr,"The two number!\n")
exit(1)
}
if((sock_value = atoi(argv[1])) <0){
fprintf(stderr,"socket error!\n")
exit(1)
}
if((sockfd = socket(PF_INET,SOCK_STREAM, 0)) == -1){
perror("socket")
exit(1)
}
bzero(&server_,SIZE)
server_.sin_family = PF_INET
server_.sin_port = htons(sock_value)
server_.sin_addr.s_addr = INADDR_ANY
if(bind(sockfd,(struct sockaddr *)(&server_),SIZE) == -1){
perror("bind")
exit(1)
}
if(listen(sockfd, 12) == -1){
perror("listen")
exit(1)
}
printf("Waiting ... ...\n")
while(1){
if((new_socket = accept(sockfd,(struct sockaddr *)(&client_),&SIZE)) == -1){
perror("accept")
exit(1)
}
printf("The client IP is %s\n",inet_ntoa(client_.sin_addr))
printf("The socket is %d\n",ntohs(client_.sin_port))
if(write(new_socket,buf,strlen(buf)) == -1){
perror("write")
exit(1)
}
int my
char mybuf[1024]
if((my = read(new_socket, mybuf,1024)) == -1){
perror("read")
exit(1)
}
mybuf[my] = '\0'
printf("#++++#++++#:%s\n",mybuf)
close(new_socket)
}
close(sockfd)
return 0
}
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int sockfd
int sock_value
char buf[1024]
char mybuf[] = "Linux\n"
int read_count
struct sockaddr_in client_
struct sockaddr_in server_
int SIZE = sizeof(struct sockaddr_in)
if(argc != 3){
fprintf(stderr,"The two number!\n")
exit(1)
}
if((sock_value = atoi(argv[2])) <0){
fprintf(stderr,"socket error!\n")
exit(1)
}
if((sockfd = socket(PF_INET,SOCK_STREAM, 0)) == -1){
perror("socket")
exit(1)
}
bzero(&client_,SIZE)
bzero(&server_,SIZE)
client_.sin_family = PF_INET
client_.sin_port = htons(52252)
client_.sin_addr.s_addr = INADDR_ANY
server_.sin_family = PF_INET
server_.sin_port = htons(sock_value)
server_.sin_addr.s_addr = inet_addr(argv[1])
if(connect(sockfd,(struct sockaddr *)(&server_),SIZE) == -1){
perror("connect")
exit(1)
}
if((read_count = read(sockfd,buf,1024)) == -1){
perror("read")
exit(1)
}
buf[read_count] = '\0'
printf("#----#----#:%s\n",buf)
if(write(sockfd, mybuf,6) == -1){
perror("write")
exit(1)
}
close(sockfd)
exit(0)
return 0
}
协议。手机终端跟后台服务器之间的交互协议,这个协议的设计是整个系统的骨架,在这一点做好设计可以使得系统的复杂度大大降低。容灾。当系统出现了若干服务器或若干支架(宕机的时候),仍然需要让系统尽可能的提供正常的服务。轻重。如何在系统架构中分布功能,在哪一个点实现哪一个功能,代表系统中间的功能配置。监控。为系统提供一个智能仪表盘。在协议设计上,移动互联网和常规互联网有很大的区别。首先有CMWAP和CMNET的不同,在中国现在有相当多的手机用户使用WMWAP连接,还有就是在线和离线的概念,当QQ下线的时候叫离线,当你登录的时候叫在线。但是在移动互联网这两个概念比较模糊。从微信的设计中,不管在线还是离线系统表现都应该是一致的。还有一个是连接不稳定的问题,由于手机信号强弱的变化,当时信号很好,5秒钟走到信号不好的地区,连接就必须断掉。这个中间带来不稳定的因素为协议设计带来较大困难。此外就是资费敏感的问题,因为移动互联网是按照流量计费的,这个计费会使得在协议设计中如何最小化传输的问题。最后就是高延迟的问题。
对此,业界标准的解决方案:Messaging And Presence Protocol:1)XMPP2)SIP/SIMPLE。它的优点是简单,大量开源实现。而缺点同样明显:1)流量大:状态初始化;2)消息不可靠。
微信在系统中做了特殊设计,叫SYNC协议,是参考Activesyec来实现的。特点首先是基于状态同步的协议,假定说收发消息本身是状态同步的过程,假定终端和服务器状态已经被迟了,在服务器端收到最新的消息,当客户端、终端向服务器对接的时候,收取消息的过程实际上可以简单的归纳为状态同步的过程,收消息以及收取你好友状态更新都是相同的。在这样的模式之下,我们会也许会把交互的模式统一化,只需要推送一个消息到达的通知就可以了,终端收到这个通知就来做消息的同步。在这样的简化模式之下,安卓和塞班都可以得到统一。这样的系统本身的实现是更为复杂的,但是获得很多额外的好处。
让剩下系统实现的部分更加简单,简化了交互模式,状态同步可以通过状态同步的差值获得最小的数据变更,通过增量的传输得到最小的数据传输量。通过这样的协议设计,微信可以确保消息是稳定到达的,而且是按序到达。引用一句俗话:比它炫的没它简单,比它简单的没它快,没谁比他更快,哪怕在GPRS下,微信也能把进度条轻易推到底。
欢迎分享,转载请注明来源:夏雨云
评论列表(0条)