
Chapter 2 第2章 通信加密
我们在第1章大体描述了使用Python作为网络服务器的编码方法和思路,作为网络服务器,首先要考虑的是针对具体的项目,我们将会采用何种方式来编写服务器代码。这其中,HTTP或Socket形式,TCP或UDP的选择,阻塞或非阻塞的方式都会对每一个项目产生巨大的影响,在本书的后续章节中,我们也会探讨数据库对于网络服务器的影响,以及单物理服务器和多物理服务器对于网络服务架构的影响。
在本章中,我们将具体阐述网络通信的加密,诚然,在这个地球村互联通信的时代,加密、隐私是重中之重,不可否认地说,程序员扮演了救世主的角色,也扮演了毁灭者的角色,若代码写得不严谨,轻者泄露个人信息,重者影响到整个公司的信誉和基础建设。加密,不仅仅是网络编程的重头,更是在这个网络时代中不可或缺的一个要素。
所谓加密,就是将人类或者电脑本来能直接“看懂”的信息数据,通过一定的手段,变成其他人无法辨识的信息数据。当然,在信息的接收方,通过发送方和接收方所约定的解码方式,将中间人无法看懂的数据解码成能“看懂”的数据,这就是加密所经过的流程。
摩尔斯电码就是一种加密方式,它通过将英文和数字字符编码成点和划进行加密,接收方需要一本点和划的代码表才能拼装组合成完整的语句,如图2-1所示。
第二次世界大战中,德军将字母顺序通过一定的规律重新排列,然后发送摩尔斯电报,也算是经过了第二道加密手续。据传言,中越战争期间,中方使用浙江温州方言作为通信语言,为的就是让中间方的越南人听不懂通信内容,而电影《风语者》中,美国人使用的土著纳瓦霍语,也是一种加密方式。简而言之,通过某种方式使发送方和接收方能“知晓”对方的内容,而中间攻击者却无从知晓该信息,这种方式就可以称为加密方式。

图2-1 摩尔斯电码
在二进制的世界里,加密就更是一种常见的手段,不严谨地说,文件压缩也可以算一种加密方式,因为压缩已经改变了原有文件的格式,只有通过解压缩才能还原成原来的文件内容。
当然随着电脑技术的进步,数学基础建设的长足发展,计算机加密、网络加密已经越来越普遍,相应的,更好的类库、框架、引擎几乎应有尽有,但是我们仍然要从最基础、最普通的部分讲起。
2.1 软件、通信加密的几种常用方案
我们知道,不管在什么环境下,计算机类的任何数据,都是通过高低电平来传输的,所以常见的一种加密方式,就是按照比特位来加密。
在计算机中,一个字(Byte),占计算机中的八个比特位(Bit),如果我们考虑加密后的解密,那么有编程基础的读者就知道了,用什么呢?对了,就是用异或加密,也就是对每一个比特位进行异或(ⅩOR)运算。
2.1.1 异或位运算加密
我们来看图2-2,就能明白异或加密是怎么进行的。

图2-2 异或运算
可以看到,异或行为是每次对Bit位的运算,逢位相同为0,逢位不同为1,于是在解密的过程中,进行密钥的异或解密,可以得到与初始值相同的结果,这种算法,也可以称作对称加密的其中一种。当然,所谓的对称加密,不仅仅指异或运算,而是加密和解密双方都采用同样的密钥来进行加密和解密运算。
笔者曾经在1998年~2009年期间,制作维护过一款在当时也算有相当口碑的加密工具EasyCode和EasyCode Pro,其中大部分算法都是对称加密算法,最简单的算法就是使用异或加密,当然也有非对称加密算法,具体将在后续章节进行讨论。
但是也不要小看异或加密运算,虽然异或运算安全度不高,但是如果加入一些复杂的运算,还是可以带来一定程度的保障,但是在重要项目中,并不推荐使用异或算法。
我们来看一下Python字符串异或的代码:
def xor(s, k): return ''.join(chr(ord(i) ^ ord(j)) for i, j in zip(s, k))
Python不像C或者Java语言,严谨地讲,它的字符串是string,当然如果要对每一位进行异或,那就需要内建函数zip。
zip函数的意义就是将传入的参数打包成tuple,然后返回由这些tuple组成的list。如果两个参数长度不相等,那返回的list长度就由传入最短的那个元素长度决定;而ord则是将传入长度为1的字符串转化为ASCII值。目前这个demo函数xor只能异或一样长度的字符串,但是我们已经可以一探究竟了,来看下面的代码:
s = 'hello' key = 'world' s2 = xor(s, key) print s2 print xor(s2, key)
我们将s作为传入需要加密的内容,而key是相同长度的world,我们在IDE中看下两次print的结果,如图2-3所示。
我们看到,第一次的结果是一串被转换过的乱码(由于其中的乱码包含了回车符,所以变成了两行),而最后一行就是异或返回的结果。

图2-3 运算结果
这种加密方式既简单又实用,但是破解方式也很简单,所以我们可以考虑使用其他对称加密方式。
2.1.2 其他对称加密
我们在前面一个小节介绍了异或加密,这种方式将key当作密码与被加密内容进行逐位异或运算,运算量小,速度极快,然而却容易被人破解。
那么有没有其他对称加密算法可以弥补这个缺点呢?答案是有的,我们耳熟能详的DES、AES、Blowfish等知名算法,都是对称加密算法,这几种算法拥有完善的数学模型基础,从应用上来讲,安全级别并不算低。
DES分CBC和ECB模式,ECB模式指的是电码本模式,也就是将8个字节一段进行DES加密得到密文,或者明文(密钥相同解密),而CBC模式则复杂得多,每段数据之间都有联系,比如数据段N就与数据段N-1相联系,具体算法可以参考DES的具体算法和实现。
在Python中,有pyDes库可以拿来就用,当然在后面,我们会着重讲解更加重量级的加密库来保证我们的数据安全。
AES(The Advanced Encryption Standard)加密算法和DES一样如雷贯耳,它是美国国家标准与技术研究所用于加密数据的规范,用于替代DES算法。AES所使用的是Rijndael加密算法,该算法由两位比利时人设计,具体的算法历史可以在很多地方查询到。
在Python中,我们可以使用PyCrypto和PyAES等类库,就像pyDes一样在Python中方便地使用AES加密算法而不用去管任何其他的东西。
对称加密还有不少算法,但是具体的机理都是使用可逆算法,只要密钥一致,就能还原出明文,但是这样的机制还是不够安全,在安全级别要求非常高的情况下,我们需要用到非对称加密。
2.1.3 非对称加密
我们在网上可以看到这样关于非对称加密的描述:
公开密钥加密,该思想最早由雷夫·莫寇在1974年提出。之后在1976年,狄菲与赫尔曼两位学者以单向函数与单向暗门函数为基础,为发讯与收讯的两方创建密钥。非对称密钥,是指一对加密密钥与解密密钥,这两个密钥是数学相关的,用某用户密钥加密后所得的信息,只能用该用户的解密密钥才能解密。如果知道了其中一个,并不能计算出另外一个。因此如果公开了一对密钥中的一个,并不会影响到另外一个密钥的秘密性质。我们称公开的密钥为公钥;不公开的密钥为私钥。如果加密密钥是公开的,且用于客户给私钥所有者上传加密的数据,这被称为公开密钥加密,例如,网络银行的客户发给银行网站用于确认账户操作的加密数据。
如果解密密钥是公开的,用私钥加密的信息,可以用公钥对其解密,用于客户验证持有私钥一方发布的数据或文件是完整准确的,接收者由此可知这条信息确实来自于拥有私钥的某人,这被称作数字签名,公钥的形式就是数字证书。例如,从网上下载的安装程序,一般都带有程序制作者的数字签名,可以证明该程序的确是该作者(公司)发布的,而不是第三方伪造的且未被篡改过(身份认证/验证)。
因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
目前大多数网上应用都采用非对称加密算法、对称加密算法和我们熟悉的MD5和SHA1的文件散列算法结合的加密方式。
由于非对称加密拥有公钥和私钥两把钥匙,从数学的角度看,要通过公钥去推导出密钥的可能性几乎为零,所以从安全性上来看,比对称加密更为安全,但是它的缺点也很明显,那就是它并不适合计算大文件和大量字符,所以一般使用对称加密加密明文,用非对称加密进行数字签名验证。
我们来考虑两个应用场景:
场景1:假设有用户A和用户B, A用户制作了A本人的公钥和私钥,然后将公钥给B,告诉B以后传文件都用公钥或者私钥加密,这时候A持有私钥,而B持有公钥,于是,B用户通过公钥要向A用户传送加密文件,应该怎么做呢?
我们知道,非对称加密并不适合大文件的加密,所以B用户通过对称加密,比如AES算法使用密码1234将需要被传送的文件加密,然后用公钥加密1234这个密码,然后将被公钥加密过的密码和密文传送给A, A拿到密文和密码后,用私钥解开密码,再用密码通过AES解码密文,如果不能解开那就说明公钥错了,或者被篡改过,如果能解开就说明是正确的。
场景2:用户A需要通过私钥给B传送密文,这时候B怎么知道A传送的密码和密文是通过A的私钥加密传送且传送途中没有被任何中间攻击的人篡改过呢?首先A需要将被发送的明文文件用AES通过密码加密,比如1234,加密后的密文用散列算法散列一次,得到散列值,然后将散列值用私钥再加密一次,再将1234这个密码用私钥加密,将加密后的散列值、密文和加密后的1234密码传送给B, B用A的公钥解开散列值,比对密文的散列值是否一致,如果一致,就认为文件没有被篡改,然后用公钥解密被加密的1234密码,再次用1234使用AES解密密文,最终得到正确的明文。
说到这里,似乎事情都很顺利,可是眼尖的读者会发现这里存在一个问题。什么问题呢?我们几乎可以保证私钥加密后的文件传给公钥一方是安全的,然而因为公钥是公开的,所以黑客可以利用公钥伪造文件内容传送给私钥一方,我们考虑下列场景:
B通过A的公钥加密了密钥密码,将密文一起传送给A,此时黑客C截获了这个文件,于是C将B正确的密文全部扔掉(因为没办法解密,干脆全扔了),自己通过A的公钥(因为公钥是公开的,谁都可以拿到),通过相同的流程,加密了一遍完全假的文件给A, A拿到文件后,以为是B给他的文件,解密也没问题,但是A拿到的却是C的伪造文件,这个时候该怎么办呢?
于是,这就需要我们的证书机构(Certificate Authority, CA)华丽登场了,所谓证书机构,就是第三方安保措施,它会颁发一个证书,这个证书里面写明了B的一些信息,以及A的公钥信息,可以证明B才是正确的发送密文的一方,而其他任何伪造方都是无效的。那么我们又如何保证证书机构的证书不被C这个黑客所篡改呢?当然,证书机构会用它自己的私钥加密自己的证书给A,让A安装它的证书。于是,B每次传送文件给A,为了保证安全性,除了应用场景1中的内容外,还需要将密文再散列一次,将散列值用A的公钥进行加密(实际应用中这一步也可以省略),保证这是B传送的文件,这样,A每次拿到的数据就是这样一些内容:
❑B用密码1234进行AES对称加密的密文;
❑B用A的公钥加密密码1234后的短密文;
❑B用A的公钥加密的AES对称加密后的密文的散列值(可省略);
❑CA用CA自己的私钥加密后的B的证书。
当A拿到这一整套内容后,首先使用CA的公钥,解开CA证书,核对B传送过来的CA证书是否与B的个人信息匹配,如果匹配,就可以用A的私钥解开B用公钥加密后的密文散列值,取得散列值后,将密文散列比对,如果一致,则A再用私钥解密短密文,得到AES密码1234,最后用AES密码1234解密密文得到真正的文件。
虽然步骤看似很长,但基本能保证安全性。
当然,CA的证书也可能被篡改。举个简单的例子,当我们需要科学上网登录谷歌的时候,浏览器发现代理服务器传来的流量并没有谷歌的证书,于是拒绝传送数据,这时候我们需要导入谷歌的假证书(这个证书由代理服务器自己提供),通过代理服务器自己提供的私钥加密的证书,告诉浏览器,我代理服务器的IP地址和证书才是正确的,所有从我代理服务器过来的流量都是真的,于是浏览器认可了这份证书,用代理服务器的公钥解密证书,这样浏览器就可以通过代理服务器进行网页浏览了。
小结
对称加密和非对称加密结合使用,可以达到安全性的最大化,如果要保证99%的安全性,证书最好是介质证书,也就是物理介质的证书,比如网银的U盾,这样我们不用担心物理证书被篡改,除非自己把它给弄丢了。
2.2 OpenSSL
从这一节起,我们将讲解OpenSSL。OpenSSL是一款应用颇为广泛的开源程序库,用于网页服务、网络传输和网络服务器,其使用范围非常之广。
OpenSSL开源库主要分为三个方面,SSL协议库(libssl)、应用程序(openssl)和密码算法库(libcrypto)。其中包括了主要的加密算法,如我们在前面介绍过的对称加密和非对称加密还有证书的制作,程序库中都有提供,它还提供了丰富的接口和应用程序供我们做测试。
既然本书的主干是使用Python来编写服务器和其他代码,我们就省却了下载源码包并使用C/C++编译器编译的步骤,只需要pyOpenSSL库即可。在Linux下输入:
pip install pyOpenSSL
在Windows下,如果没有路径,可以输入:
你的Python安装路径\scripts\pip install pyOpenSSL
通过上面的步骤,我们便成功安装了pyOpenSSL库。在安装的同时,如果依赖库不满足,pip将会安装一系列依赖库:six、ipaddress、enum34、pycparser、cffi、pyasn1、idna、cryptography等,最后安装pyOpenSSL,如图2-4所示。
当然也可以去https://pypi.python.org/pypi/pyOpenSSL下载适合自己的版本进行安装,github的主页在https://github.com/pyca/pyopenssl。

图2-4 依赖库以及pyOpenSSL的安装
当我们安装完pyOpenSSL后,可以写一个小程序测试一下是否可以运行:
import socket, sys from OpenSSL import SSL context = SSL.Context(SSL.SSLv23_METHOD) print "creating socket" s_handle = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssl = SSL.Connection(context, s_handle) print "Connecting…" ssl.connect(("www.so.com", 443))
代码浅显易懂,建立一个SSL标准的连接,使用第1章我们介绍过的方式来创建Socket链接,然后进行SSL的连接,当然HTTPS的连接端口是443,这就是一段标准进行SSL网页连接的演示。
除了pyOpenSSL外,我们还可以安装另一个比较好用的库M2Crypto,它的底层是结合OpenSSL制作而成,使用pip install m2crypto或者在Windows下直接去pypi.python.org下载M2Crypto的Windows安装包即可完成安装。
我们将在后续几个章节详细介绍SSL方式的网页连接。
2.2.1 生成证书
1. Open SSL提供的证书类型
我们先来看一下OpenSSL提供的几种证书类型(部分内容摘自互联网):
1)PEM:即Privacy Enhanced Mail,它是OpenSSL默认的加密信息存放格式,PEM文件一般包含以下的信息。
类型:表明本文件内存放的是何信息类型,它由BEGIN和END进行结尾,看起来内容像是这样:
—-BEGIN XXX—- BASE64 ENCODE STUFF —-END XXX—-
头部信息:告诉用户数据是如何处理的,比如某种加密算法以及初始化向量。
信息内容:里面包含了BASE64编码后的内容,包括私钥、公钥和x509证书。
我们来看几个例子,首先是证书内容:
—-BEGIN CERTIFICATE—- Nl08dGlkOjE3MDg+MWVlOGFiMWYxZmQxZjk5YWY0ZmZmNTU3MDIyMDdjZGZj Mjg1MTU3ZjI4ZDBlOTRjMWQ0YTUwZmU2ODZjMmQ3Nzg5MjJhNjEwNDdkNGU3 MDBhNDM0ZDdkYjJkYzhlMWUzNzcxNzQ4NjRjY2Q5YWM5ODZiMTc3MTlmZGE4 —-END CERTIFICATE—-
私钥内容:
—-BEGIN RSA PRIVATE KEY—- YjE2ZGUwOGMzYmEzNjMzODc4MDI1ZmUyZDUzMmQxNWZjNTYzMzNkYTU0YTlj YzUxN2U4Y2IxN2FlYzNjMmNhYzNhN2I5MDE1Mzg0NGRkMTM4NWY4ZWMzNDlk YjI2MDk3M2QxYWNiZGNkMGYwNjUxYjE0YjVmZTAxODhmNzgxMDQ4MTFjOTBk —-END RSA PRIVATE KEY—-
证书请求文件:
—-BEGIN CERTIFICATE REQUEST—- IDE2IDEzOjQ0OjAzIDIwMTZdPHRpZDoxNzA4PiAsIFNIQSA6IA0KW1NhdCBK dWwgMTYgMTM6NDQ6MDMgMjAxNl08dGlkOjE3MDg+NDcwNjk4YmZmMzc1Y2Vh M2U5NzEwM2RkNjg3NzE3MDMyYjIyNGRlNA0KW1NhdCBKdWwgMTYgMTM6NDQ6 MDMgMjAxNl08dGlkOjE3MDg+QWRkVGFpbCwgb2ZmOiA3ODgwMjQ2NCwgb2Zm aDogMA0KW1NhdCBKdWwgMTYgMTM6NDQ6MDMgMjAxNl08dGlkOjE3MDg+QWRk —-END CERTIFICATE REQUEST—-
我们可以看到,每个BEGIN和END开始之后的字符串都是不一样的,这就是区分PEM文件存储内容的方式。
2)DER:辨别编码规则(DER)包含私钥、公钥和证书。它是大多数浏览器的默认格式,并按ASN1 DER的格式存储。换句话说,DER是PEM的二进制版本。
3)PFⅩ 、P12:公钥加密标准#12(PKCS#12)包含私钥、公钥和证书。二进制格式存储,也称为PFⅩ文件。可将Apache/OpenSSL使用的“Key +Crt”合并转换为标准的PFⅩ文件,你可以将PFⅩ文件格式导入IIS 5/6、MS ISA、MS Exchange Server等软件。转换时需要输入PFⅩ文件的加密密码。
4)JKS:即Java Key Store,可以将Apache/OpenSSL使用的“Key + Crt”转换为标准的JKS格式。JKS文件格式被广泛应用在基于Java的Web服务器、应用服务器、中间件中(如Tomcat, WebLogic等)。
5)KDB:可以将Apache/OpenSSL使用的“Key + Crt”转换为标准的IBM KDB格式。KDB格式被广泛应用于IBM的Web服务器、应用服务器、中间件(如WebSphere等)。
6)CSR:即Certificate Signing Request。生成x509数字证书前,先由用户提交证书申请文件,然后由CA来签发证书。大致生成的过程如下(x509证书申请的格式标准为pkcs#10和rfc2314):
❑用户生成自己的公钥私钥;
❑构造自己的证书申请文件,符合PKCS#10标准。该文件主要包括了用户信息、公钥以及一些可选的属性信息,并用自己的私钥给该内容签名;
❑用户将证书申请文件提交给CA;
❑CA验证签名,提取用户信息,并加上其他信息(比如颁发者等信息),用CA的私钥签发数字证书。
说明:数字证书(如x.509)是将用户(或其他实体)身份与公钥绑定的信息载体。一个合法的数字证书不仅要符合x509格式规范,还必须有CA的签名。用户不仅有自己的数字证书,还必须有对应的私钥。x509 v3数字证书主要包含的内容有:证书版本、证书序列号、签名算法、颁发者信息、有效时间、持有者信息、公钥信息、颁发者ID、持有者ID和扩展项。
7)6OCSP:在线证书状态协议(Online Certificate Status Protocol, rfc2560)用于实时表明证书状态。OCSP客户端通过查询OCSP服务来确定一个证书的状态,可以为使用者提供一个或多个数字证书的有效性资料。它建立了一个可实时响应的机制,让用户可以实时确认每一张证书的有效性,解决由CRL引发的安全问题。OCSP可以通过HTTP协议来实现。rfc2560定义了OCSP客户端和服务端的消息格式。
8)CER:一般指使用DER格式的证书。
9)CRT:证书文件。可以是PEM的格式。
10)KEY:一般指PEM格式的私钥文件。
11)CRL:证书吊销列表(Certification Revocation List)是一种包含撤销的证书列表的签名数据结构。CRL是证书撤销状态的公布形式,CRL就像信用卡的黑名单,用于公布某些数字证书不再有效。CRL是一种离线的证书状态信息。它以一定的周期进行更新。CRL可以分为完全CRL和增量CRL。在完全CRL中包含了所有的被撤销证书信息,增量CRL由一系列的CRL来表明被撤销的证书信息,它每次发布的CRL都是对前面发布CRL的增量扩充。基本的CRL信息有:被撤销证书序列号、撤销时间、撤销原因、签名者以及CRL签名等信息。基于CRL的验证是一种不严格的证书认证。CRL能证明在CRL中被撤销的证书是无效的。但是,它不能给出不在CRL中的证书的状态。如果执行严格的认证,需要采用在线方式进行认证,即OCSP认证。一般是由CA签名的一组电子文档,包括了被废除证书的唯一标识(证书序列号), CRL用来列出已经过期或废除的数字证书。它每隔一段时间就会更新,因此必须定期下载该清单,才会取得最新信息。
12)SCEP:简单证书注册协议(Simple Certificate Enrollment Protocol)。基于文件的证书登记方式,需要从你的本地计算机将文本文件复制和粘贴到证书发布中心,和从证书发布中心复制和粘贴到你的本地计算机。SCEP可以自动处理这个过程,但是CRLs仍然需要手工地在本地计算机和CA发布中心之间进行复制和粘贴。
13)PKCS7:是一种加密消息语法(PKCS7),是各种消息存放的格式标准。这些消息包括:数据、签名数据、数字信封、签名数字信封、摘要数据和加密数据。
14)PKCS12:PKCS12(个人数字证书标准)用于存放用户证书、CRL、用户私钥以及证书链。PKCS12中的私钥是加密存放的。
2.生成证书
在生成证书之前,我们必须要安装和编译完OpenSSL的所有程序和类库,确保已经生成了OpenSSL自带的命令行程序,在Linux环境下默认OpenSSL是安装好的,但是在Windows下则需要我们自己编译OpenSSL。在这里我将稍微提示一下如何编译和安装OpenSSL。如果读者手头有MingW或者Cygwin编译器,当你从http://www.openssl.org上下载源码包并解开后,可以通过config来配置文件,随后使用make和make install编译安装完成,这是最简单最直接的步骤,如何配置参数并不在本书范畴之内(1.1.x版本开始已经不支持MingW环境,可以使用Cygwin)。
如果是完整的MingW和Cygwin模拟的UNIⅩ环境,编译安装完OpenSSL后,将在你的shell环境的local目录下生成ssl目录,或者如果觉得麻烦,可以去http://gnuwin32.sourceforge.net/packages/openssl.htm下载已经编译好的Windows版本。我们以这个版本为基础,开始制作证书。
在命令行模式下,假定OpenSSL的bin文件夹已经被设置到path环境变量,所有exe文件都可以在任何路径下被正常调用运行,在这之前,要设置一下OpenSSL的config文件,我们必须得告诉OpenSSL正确的cnf文件在哪里,如果是类UNIⅩ环境,openssl.cnf配置文件默认会存放在/usr/local/ssl下,几乎不用配置OpenSSL就能找到,而Windows版本却要去傻傻地寻找/usr/local/ssl/openssl.cnf,这显然是不正确的,所以这个时候我们需要设置一个环境值为OPENSSL_CONF的变量,指定cnf文件的位置,看起来像是下面这样的赋值:
set OPENSSL_CONF=c:/MyOpenSSL_Install_Folder/bin/openssl.cnf
设置完环境变量后,就可以开始制作证书了。
我们先制作一个私钥,输入下列命令行:
openssl genrsa -out ca-key.pem 1024
通过这个pem文件来制作创建证书:
openssl req -new -out ca-req.csr -key ca-key.pem
然后就会出现如图2-5所示的prompt对话框,按着它提示的内容输入即可。

图2-5 提示输入对话框
请注意,在Windows环境下,如果有后续extra内容的输入,openssl则会报错,如图2-6所示。

图2-6 输入extra属性后,Windows版本报错
所以请注意,在Windows下,extra属性请直接回车跳过,对证书的制作没有影响。
接下来生成自我签署的证书,有效期为10年:
openssl x509-days 3650-req -in ca-req.csr -out ca-cert.pem -signkey ca-key.pem
如果需要将证书导出为浏览器的格式,则可以转换为.p12格式,当然如果不需要就可以不转换格式:
openssl pkcs12-export -clcerts -in ca-cert.pem -inkey ca-key.pem -out ca.p12
以下是证书制作的一些命令行,当然还有证书的撤销等操作,在这里就不进行一一阐述了,网上有很多例程可以找到。
2.2.2 公钥和私钥的配置
前一个小节我们介绍了证书的制作,那么有人就要问,如何配置公钥和私钥呢?其实非常简单。
私钥(指定为1024位,默认2048位):
openssl genrsa -out folder/rsa_private_key.pem 1024
根据私钥生成配对的公钥:
openssl rsa -in folder/rsa_private_key.pem -pubout -out folder/rsa_public_key.pem
这样,在folder里,就出现了公钥和私钥两个文件。
当然,除了使用标准的OpenSSL之外,我们可以使用Python程序来制作公钥和密钥。先前介绍过M2Crypto类库,考虑下列代码:
#encoding: utf8 import os import M2Crypto M2Crypto.Rand.rand_seed(os.urandom(1024)) obj = M2Crypto.RSA.gen_key(1024, 65535) obj.save_key('myprivate.pem', None) obj.save_pub_key('mypublic.pem') rsa = M2Crypto.RSA.load_pub_key('mypublic.pem') enc_text = rsa.public_encrypt("this is my message to encrypt", M2Crypto.RSA. pkcs1_oaep_padding) print "encrypt text is:" print enc_text.encode('base64') msg = M2Crypto.EVP.MessageDigest('sha1') msg.update(enc_text) signature = obj.sign_rsassa_pss(msg.digest()) print "the signature is" print signature.encode('base64') rrsa = M2Crypto.RSA.load_key ('myprivate.pem') try: dec_text = rrsa.private_decrypt (enc_text, M2Crypto.RSA.pkcs1_oaep_padding) except: dec_text = "" if dec_text : print dec_text msg = M2Crypto.EVP.MessageDigest('sha1') msg.update(enc_text) if obj.verify_rsassa_pss(msg.digest(), signature) == 1: print "signature ok" else: print "signature error"
读者在Linux环境下跑上述代码是没有问题的,但是,如果读者在Windows下跑M2Crypto的话,将会发生一个错误,如图2-7所示。

图2-7 Windows下的OpenSSL错误
这种情况是因为连接M2Crypto的OpenSSL的底层库没有将uplink.c链连进去。
void OPENSSL_Uplink (volatile void **table, int index) { HANDLE h = GetModuleHandle(NULL) GetProcAddress(h, "OPENSSL_Applink") }
这种情况下,该怎么办呢,我们不要直接使用save_key,而替换为单纯的读写操作,将key复制到内存并用标准输入输出进行读写,修改后的代码如下:
#! /usr/bin/env python # -*- coding: utf-8-*- import os import M2Crypto from M2Crypto import BIO M2Crypto.Rand.rand_seed(os.urandom(1024)) membuf = BIO.MemoryBuffer() # 定义内存缓冲 obj = M2Crypto.RSA.gen_key(1024, 65535) obj.save_key_bio(membuf, None) with open('myprivate.pem', 'w') as f: #写入 f.write(membuf.read()) obj.save_pub_key_bio(membuf) with open('mypublic.pem', 'w') as f: f.write(membuf.read()) with open('mypublic.pem', 'r') as f: membuf.write(f.read()) rsa = M2Crypto.RSA.load_pub_key_bio(membuf) enc_text = rsa.public_encrypt("this is my message to encrypt ", M2Crypto. RSA.pkcs1_oaep_padding) print "encrypt text is:" print enc_text.encode('base64') msg = M2Crypto.EVP.MessageDigest('sha1') msg.update(enc_text) signature = obj.sign_rsassa_pss(msg.digest()) print "the signature is" print signature.encode('base64') with open('myprivate.pem', 'r') as f: # 读取 membuf.write(f.read()) rrsa = M2Crypto.RSA.load_key_bio (membuf) try: dec_text = rrsa.private_decrypt (enc_text, M2Crypto.RSA.pkcs1_oaep_ padding) except: dec_text = "" if dec_text : print dec_text msg = M2Crypto.EVP.MessageDigest('sha1') msg.update(enc_text) if obj.verify_rsassa_pss(msg.digest(), signature) == 1: print "signature ok" else: print "signature error"
小结
使用OpenSSL进行加密解密,以及证书的制作,由于OpenSSL的原生代码是在Linux环境下编写的,所以在Windows环境下会遇到一些莫名的错误,但是问题并不是太大,也有不少解决方案。OpenSSL提供了三个大的包,能解决绝大部分应用场景,在Python中,使用M2Crypto和pyOpenSSL就可以直接进行OpenSSL编程了。
2.3 SSL/TLS通信
我们在上面几个小节介绍了加密的几种基本方案和如何使用OpenSSL的知识,在这些知识的基础上,我们接下来学习SSL/TLS的通信模式。
为什么要学习SSL通信模式?因为我们在服务器代码的编写当中,是有很大的几率接触到这些知识并加之运用的,比如要和对方进行HTTPS通信,或者和对方的Tomcat服务进行通信,都有可能接触到SSL协议。
那么什么是SSL协议呢?
SSL(Secure Socket Layer,安全套接层协议),也就是我们在前面介绍过的公钥和私钥的技术组合的安全网络通信协议。至于它的历史我就不在书里介绍了,SSL协议指定了一种在应用协议(比如HTTP、Telnet、NMTP和FTP等这些协议)和TCP/IP协议之间提供数据安全性分层的机制,为TCP/IP的连接提供数据加密、服务器认证、消息完整性以及可选的客户机认证。同时,SSL提高和保障了应用程序之间数据的安全性,对传送的数据进行加密和隐藏,确保数据在传送中不被修改,确保数据的完整性,当然相对的,付出的代价就是所在服务的程序处理速度会相对变慢(因为要进行认证,加解密等运算)。
SSL保证了网络通信的三个目标——隐秘、完整和认证。密文加密防止攻击,散列算法保证内容完整,利用证书保证客户端和服务器端能认证对方的身份,当然我们在编写代码的过程中,不需要考虑SSL协议的存在,因为SSL在应用通信之前就已经完成了加、通信密钥协商、服务器认证工作这些过程。我们来看图2-8所示的SSL/TLS协议的基础模型图。

图2-8 SSL/TLS协议模型图
那么TLS是什么呢?TLS其实就是SSL的v3版本,TLS全称是Transport Layer Security,即传输层安全协议。
SSL实际上是共同工作的两层协议,在TCP/IP之上,有SSL记录协议,在记录协议之上封装的,还有SSL握手包/改变密码格式/SSL警告/各种应用协议,如HTTP、FTP等。
在SSL协议中,所有传输的数据都被pack在记录中,被记录的内容有header和data,也就是协议的头部和协议内容部分,这些协议内容在我们后续会讲到的游戏服务器之协议编写中会有阐述。
这个协议中封装了上层的握手、警告、密码改变格式、应用数据协议。所以经过封装完成后的SSL协议包看起来像是这些内容:协议类型、主版本号、次版本号、数据包长度、内容数据、MAC。
而SSL的握手包协议格式是这样:类型、长度、内容。
其他的改变密码和警告信息这边暂不阐述。
而TLS协议在SSL协议的基础上,做了一些修改,除了版本号外,它提供了报文鉴别码、伪随机函数,补充了一些警告代码,修改了MAC算法、一些密文、加密算法和其他的填充字节。
下面我们通过图2-9和图2-10来看一下SSL/TLS的握手协议和记录协议的流程。

图2-9 SSL/TLS握手协议流程

图2-10 SSL/TLS记录协议流程
2.3.1 SSL/TLS连接
我们在上一小节讲到了SSL/TLS的基本流程和基础知识,可能有些读者还是会比较迷茫,虽然如此,我们可以将注意力集中在如何实现SSL/TLS的交互上,而不要将注意力放在那些具体的底层细节上面,那么最好的方法就是我们使用Python来做一遍具体的SSL/TLS连接。
服务器部分:
# coding=utf-8 import socket import ssl import _ssl detail = ssl.SSLContext(_ssl.PROTOCOL_TLSv1) # ssl.PROTOCOL_TLSv1 detail.load_cert_chain(certfile="cert.pem", keyfile="key.pem") bindsocket = socket.socket() bindsocket.bind(('127.0.0.1', 1234)) bindsocket.listen(5) while 1: conn, addr = bindsocket.accept() stream = detail.wrap_socket(conn, server_side=True) try: do_something (stream) # do something with socket handle“stream” finally: stream.shutdown(socket.SHUT_RDWR) conn_stream.close()
客户端部分:
import socket import ssl s_handle = socket.socket(socket.AF_INET, socket.SOCK_STREAM) detail = ssl.SSLContext(ssl.PROTOCOL_TLSv1) detail.check_hostname = True detail.load_verify_locations('cert.pem') ssl_sock = context.wrap_socket(s, server_hostname='test') ssl_sock.connect(('127.0.0.1', 1234)) do_something with ssl_sock… ssl_sock.close()
可以看到,我们利用了OpenSSL的密钥制作,在制作完密钥后,使用SSL模块将之载入进来,之后就可以按着在第1章描述的Socket的流程完成我们的步骤,在代码里面,没有特殊的加密解密的步骤,因为在Python里面,一切都已经封装完毕,我们只需要将注意力集中在具体的业务逻辑上即可。
所以很多读者都认为,SSL/TLS仅仅在HTTPS上看到,似乎这已经成为了HTTPS的一种代名词,事实上并不是如此,SSL/TLS是在Socket上层的一种封装和加密,并不仅仅应用于HTTPS网站,更能适合游戏服务器的加密传输,当没有特别合适的加密算法的时候,我们可以利用非对称加密的私钥和公钥,将服务器部分使用私钥解密从游戏客户端发过来的公钥加密后的内容,而CA证书文件可以使用另外一台合适的服务器来保证证书文件的正确性,防止中间人进行攻击。
在后续的几个章节中我们将会介绍在游戏服务器编写和架构的过程中的其他加密方式,对称加密以及非对称加密的用法,其中非对称加密的用法除了SSL/TLS的方式,还有其他不同的方式实现,但是目的只有一个,那就是防止黑客攻击,静态或动态防止人为破坏协议代码,成为外挂泛滥的聚集地。
2.3.2 SSL/TLS HTTPS通信
我们在前面介绍了SSL/TLS的基础知识,以及如何利用Python编写SSL/TLS通信,即利用Python来进行SSL/TLS的编程,并给出了Socket模式的编程的例子,在本节中,我将介绍HTTPS通信的内容。
因为上面的几个小节都是基础,有了这些知识打底,所以现在讲到HTTPS,读者就容易接受得多。
我们知道,SSL/TLS协议包装了一些握手、加密、证书认证等内容,那么在这个基础上,在上层加入HTTP协议,那就是我们今天在很多网站上看到的HTTPS协议的来由了。
HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer),即安全Socket层的HTTP协议,据调查,随着全世界互联网安全的需求,HTTPS的应用量会越来越高,将逐步替代传统HTTP网站。
HTTPS与HTTP的区别在于:
❑由于SSL/TLS协议,HTTPS需要CA证书。
❑HTTPS具有SSL加密传输协议。
❑HTTPS默认端口是443。.
❑由于SSL/TLS协议,HTTPS具有身份认证和加密传输,安全性更高。
SSL/TLS服务器和客户端的握手过程如下:
❑浏览器向服务器传送客户端SSL协议版本号、加密算法类型、随机数,以及其他信息。
❑服务器向客户端传送SSL协议版本号、加密算法类型、随机数以及其他相关信息,同时服务器向客户端传送证书。
❑客户端利用服务器传过来的信息验证服务器的合法性,包括:证书是否过期,发行服务器证书的CA是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者数字签名”,服务器证书上的域名是否和服务器的实际域名匹配。如果合法性验证没有通过,连接将断开。
❑客户端随机产生一个“对称密码”,然后用服务器的公钥对其加密,将加密后的“预备密码”传给服务器。
❑如果服务器要求客户端身份认证(在握手过程中为可选),用户建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户端证书以及加密过的“预备密码”一起传给服务器。
❑如果服务器端要求客户端进行身份认证,服务器必须检验客户端证书和签名随机数的合法性,合法性验证过程包括:客户端证书使用日期是否有效,为客户端提供证书的CA是否可靠,发行CA的公钥能否正确解开客户端证书发行CA的数字签名,检查客户端证书是否在证书废止列表(CRL)中(可参看前几个小节)。如检验没有通过,连接中断;如果验证通过,服务器将用自己的私钥解开加密的“预备密码”,然后生成主密码(客户端也将通过同样的方法产生相同的主密码)。
❑服务器和客户端用相同的主密码,一个对称密钥用于SSL协议的加解密通信。同时在SSL通信过程中要完成数据通信的完整性,防止数据通信中的任何变化(被中间人攻击等)。
❑客户端向服务器端发出信息,指明后面的数据通信将使用主密码为对称密钥,通知服务器的握手过程结束。
❑服务器向客户端发出信息,指明后面的数据通信将使用主密码为对称密钥,通知客户端的握手过程结束。
❑SSL的握手部分结束,SSL安全通道的正式数据通信开始,客户端和服务器开始使用相同的对称密钥进行数据通信,同时进行通信完整性的检验。
我们使用Python来看一下HTTPS的通信过程,结合前面所学到的所有知识,将这些类库和知识点都整合起来:
import httplib, ssl, urllib2, socket class HTTPS(httplib.HTTPSConnection): def __init__(self, *args, **kwargs): httplib.HTTPSConnection.__init__(self, *args, **kwargs) def connect(self): sock = socket.create_connection((self.host, self.port), self.timeout) if self._tunnel_host: self.sock = sock self._tunnel() self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_ version=ssl.PROTOCOL_SSLv3) class HTTPS_Handle(urllib2.HTTPSHandler): def https_open(self, req): return self.do_open(HTTPS, req) urllib2.install_opener(urllib2.build_opener(HTTPS_Handle())) if __name__ == "__main__": r = urllib2.urlopen("https://www.so.com")
HTTPS和HTTPS_Handle这两个类都是继承自httplib和urllib2中的类,urllib2的install_opener我们在第1章已经讲解过,最主要的作用是安装用户自定义的网页打开设置,比如代理服务器等。
小结
通过Socket基础和加密解密的基础知识,结合OpenSSL的工具以及Python库,我们就可以很容易地用Python编写SSL/TLS代码。在后续的一些章节中,我们会比较各种不同的加密模式,而SSL/TLS也是一个不错的选择,我们已经在前一个小节中看到SSL/TLS的Socket编码方法,这对于编写游戏服务器是很有帮助的。
2.4 其他加密方式
我们在前面的小节介绍了加密方式,包括对称加密和非对称加密,也介绍了如何使用OpenSSL进行证书的制作以及公钥密钥的制作,还包括了在Python代码中直接进行证书的制作,Windows版和Linux版之间的区别,以及如何规避在Windows下发生的问题。那么在本节中,我们将看到其他几种不同的加密方式,包括散列算法、BASE64等一些最普遍的网上流行的加密方式。
2.4.1 散列算法
事实上,虽然散列算法属于密码学领域,但散列算法在严格意义上来讲并不能算作加密算法,散列通俗地讲是给文件或某个目标字符串通过算法形成一个特征签名字符串(指纹或称为摘要),如果目标内容不同,散列出来的摘要内容也不同,比如MD5和SHA-1等,但是2005年左右,来自中国的四位专家已经找到了MD5算法中的碰撞,网上有专门公开的两个exe文件,在console控制台打印不同的字符串,但对这两个文件进行MD5却列出相同的MD5指纹。显然在目前的情况下,如果是特别重要和要求严谨的应用,最好不要用MD5做文件签名,而改用SHA-256(因为最近SHA-1也被发现出现了算法碰撞)或者结合CRC32、MD5等,以保证数据摘要的正确性和唯一性。
我们来大致地检阅一下各种散列算法。
1. CRC8、CRC16、CRC32
CRC(Cyclic Redundancy Check,循环冗余校验)算法出现时间较长,应用也十分广泛,尤其是通信领域,现在应用最多的就是CRC32算法,它产生一个4字节(32位)的校验值,一般是用8位十六进制数表示,如FA 12 CD 45等。CRC算法的优点在于简便、速度快。
2. MD2、MD4、MD5
MD系列应用十分广泛,尤其是MD5(Message-Digest Algorithm 5,消息摘要算法Ⅴ5),它由MD2、MD3、MD4发展而来,由Ron Rivest(RSA公司)在1992年提出,目前被广泛应用于数据完整性校验、数据(消息)摘要、数据加密等。MD5产生一个16字节(128位)的校验值,一般用32位十六进制数表示。
不过随着MD5碰撞的出现,MD系列算法已经逐渐被SHA算法替代。
3. SHA1(已出现碰撞)、SHA256、SHA384、SHA512
SHA(Secure Hash Algorithm)是由美国国家标准技术研究院(NIST)制定的,SHA系列算法的摘要长度分别为:SHA为20字节(160位)、SHA256为32字节(256位)、SHA384为48字节(384位)、SHA512为64字节(512位),由于它产生的数据摘要的长度更长,更难以发生碰撞,因此也更为安全,是未来数据摘要算法的发展方向。但是SHA系列算法的运算速度与MD5相比,也相对较慢。
目前SHA系列的应用较为广泛,主要应用于CA和数字证书中,另外在目前互联网中流行的BT软件中,也是使用SHA来进行文件校验的。
4. RIPEMD、PANAMA、TIGER、ADLER32等
RIPEMD是Hans Dobbertin等3人在对MD4, MD5缺陷分析基础上,于1996年提出来的,有4个标准(128、160、256和320),其对应输出长度分别为16字节、20字节、32字节和40字节。
TIGER由Ross在1995年提出,号称是最快的Hash算法,专门为64位机器做了优化。
如果想在Python中使用代码进行散列算法该怎么做呢?
在Python中,我们可以使用hashlib库,下面通过打印来看一看这个库究竟支持多少散列算法。考虑下列代码:
import hashlib print hashlib.algorithms_available print hashlib.algorithms_guaranteed
打印的结果是:
set(['SHA1', 'SHA224', 'SHA', 'SHA384', 'ecdsa-with-SHA1', 'SHA256', 'SHA512', 'md4', 'md5', 'sha1', 'dsaWithSHA', 'DSA-SHA', 'sha224', 'dsaEncryption', 'DSA', 'ripemd160', 'sha', 'MD5', 'MD4', 'sha384', 'sha256', 'sha512', 'RIPEMD160', 'whirlpool']) set(['sha1', 'sha224', 'sha384', 'sha256', 'sha512', 'md5'])
从打印的结果看,hashlib在Python 2.7.x的版本中,支持20种以上的散列算法,但是第二行打印的是在所有平台支持比较稳定的算法,我们可以尝试选择某几种算法进行散列:
m = hashlib.sha1() m.update(b'test string') print m.hexdigest()
如此使用即可完成散列算法,当然在hashlib.algorithms_available中所打印出来的一些散列算法则需要OpenSSL的支持,有些操作系统平台并不一定都实现,需要看hashlib或者文档才能知道。
2.4.2 BASE64
BASE64是网上最为常见的8位字符串编码方式之一,它把每三个8位的字节转换为四个6位的字节(3×8 = 4×6 = 24),然后把6位再添两位高位0,组成四个8位的字节,也就是说,转换后的字符串理论上将比原来的长三分之一,每76个字符加入一个换行符,最后结束符做一次处理。
比如我们对字符“ABC”进行BASE64编码,过程如下:
❑取ABC的二进制值A为01000001、B为01000010、C为01000011。
❑把这三个字节的二进制码接起来为010000010100001001000011。
❑以6位为单位分成4个数据块,并在最高位填充两个0形成4个字节的编码:00010000、00010100、00001001、00000011,粗体部分为原数据。
❑再把这四个字节数据转化成十进制数:16、20、9、3。
❑最后根据BASE64给出的64个基本字符索引,查出对应的ASCII码字符为Q、U、J、D,这里的值就是数据在字符表中的索引。
BASE64字符索引表(从0开始计数):ABCDEFGHIJKLMNOPQRSTUⅤWⅩYZabcd efghijklmnopqrstuvwxyz0123456789+/
编码完成后在结尾添加=号。当然不同的应用有不同的BASE64变体,在URL中,会将BASE64编码中的+和/转化为“%N”的形式,比如正则表达式以及ⅩML中的某些数据也将BASE64做成了某种变种格式,将+和/改为“_-”或者“._”。
接下来,我们使用Python来写一段BASE64编码和解码的代码:
import base64 a = "this is test" b = base64.encodestring(a) print b print base64.decodestring(b)
你看,Python完成任务的效率就是如此之高,我们明白了理论基础之后,用Python编写代码,能快速完成任务,没有任何拖泥带水。
2.4.3 多国语言
虽然多国语言不属于加密解密的范畴,但是我仍希望将多国语言放在加密和解密这一章来讲。因为在现代编程中,除非写到非常底层的操作,比如图形图像操作,内存、CPU寄存器操作,否则多字节语言都是一道迈不过去的坎儿。
在Python中,设置多国语言只需要在抬头加上#encoding: utf8之类的标签即可,但是在很多平台中,比如Windows平台,默认的格式是CP936,所以打印在控制台的时候仍然是乱码,这不仅仅局限在打印,也出现在网页内容的保存等其他业务中,而在Linux系统中,默认是UTF-8编码的。所以我们在Windows平台用Python进行多国语言编码的时候,最好加上这一句:
#! /usr/bin/env python # -*- coding: utf-8-*- import os, sys reload(sys) sys.setdefaultencoding('utf-8')
也就是说,让Python内部的编码方式重新进行编码。这样基本能解决问题,但是仍然可能出现Windows控制台输出的问题,此时你就需要输入下面这种形式的解码。
xxx.decode(‘UTF-8')
当然在互联网上能找到各种格式编码的解决方案,这里不再一一列举,但是如果读者是在Windows下进行编码,这时候又发现传输的内容编码方式不同的话,请记住Windows默认是使用CP936/GBK格式编码的,所以传输的内容最好经过reload(sys)之后再进行编码。
小结
我们在第一部分的第2章介绍了加密解密的各种方式,以及CA证书、对称和非对称加密、信息摘要和BASE64编码格式,可以说经过了第1章和第2章的学习,我们已经可以解决游戏服务器的很多问题,包括如何进行证书的制作、SSL/TLS加密传输。
在下一章中,我们将介绍不带数据库存储的游戏服务器的实作内容,在下一章中,我将对游戏服务器的内部逻辑以及更加底层的知识进行普及和教学,读完整个第一部分后,读者心中将会对游戏服务器有一个整体的了解。