转载自:http://besteam.im/blogs/article/57/。
加解密数据、操作密钥、操作SSL协议普遍使用了OpenSSL。虽然还有其它的使用C/C++开发的加密处理库,但是Python环境下支持最好的使用最广泛的还是OpenSSL。
据python.org官方网站,目前有几个库提供了加密功能。
EVP_CipherUpdate()
函数的时候,输出大小没有计算正确。此错误会造成数据不正确,是一个比较严重的BUG。我估计应该还有其它的BUG存在,可能比较不成熟。有使用过的来说一下吧。考虑到OpenSSL使用很广泛。我决定在开发中使用M2Crypto。
M2Crypto的API手册处于:http://www.heikkitoivonen.net/m2crypto/api/
目前,截止到2009年10月23日,官网上提供的M2Crypto for Python 2.6(win32)安装包是不正确的。因为它提供的0.19版本并没有兼容0.20。所以需要下载M2Crypto的源代码自行编译。以下是编译的步骤:
include
文件夹复制到Python的include
文件夹内。把OpenSSL的几个库文件(*.a
)复制到mingw32
的lib
文件夹内。libeay32.dll.a
改名liblibeay32.a
,把libssl32.dll.a
改名libssleay32.a
。测试的版本是0.9.8h
setup.py build -c mingw32 bdist_wininst
(附记)M2Crypto主页提供了一处描述如何在windows平台下使用msvc编译openssl和M2Crypto的链接。经过试验,该方法不能在mingw32下成功。不过在一个用户评论上描述了mingw32下的方法,当时没仔细看,害我搞了半天没成功。(再附记)截止2012年1月1日止,在M2Crypto提供了最新版本的Windows安装程序。所以自己编译通常是没必要的了。
经过我测试,编译后的M2Crypto虽然导入正常,但是一旦使用BIO进行文件操作,M2Crypto就会异常退出。并打印出No AppLink这样的错误信息。如果不使用BIO的话,好像又没啥问题。
安装程序缺少openssl的几个动态链接库,如果要发布这些个安装程序,应该把将OpenSSL的动态链接库一起打包进去。
下面是几个模块的大致介绍:
M2Crypto.BIO
用于操作IO抽象类型。M2Crypto.BN
用于操作大数M2Crypto.DH
用于操作Diffie-Hellman key exchange protocolM2Crypto.EVP
高级的加密解密接口。与直接使用具体的加密算法不同。使用该接口,可以用相同的编程方式,调用不同的算法处理数据。它包含了对称加密算法与非对称加密算法的支持。M2Crypto.EC
椭圆曲线非对称加密算法M2Crypto.DSA
DSA非对称加密算法M2Crypto.RSA
RSA非对称加密算法M2Crypto.Rand
操作随机数M2Crypto.SSL
操作SSL协议M2Crypto.X509
操作X509接下来,我们通过日常的编程任务来看看如何使用这些接口。
如何使用MD5、SHA1等消息散列算法。
虽然OpenSSL提供了直接操作MD5、SHA1算法以及blowfish等各种对称加密算法的API,但是M2Crypto并没有将其包含进来。不过也好,各种算法都有各自的API,记起来麻烦。通过M2Crypto.EVP
,我们仍然可以调用这些算法。下面是一个MD5的例子:
def md5(s): #在构造函数中传入算法的名字可以选择不同的消息散列算法 m=EVP.MessageDigest("md5") m.update(s) return m.digest() #或者m.final()
常用的散列算法还有SHA1。使用方法与MD5类似,只是构造函数是:
m=EVP.MessageDigest("sha1")
使用对称加密算法加密数据。
如前所述,我们需要使用EVP.Cipher这个比较抽象的API,而不是具体的算法。与EVP.MessageDigest()
类似,EVP.Cipher
主要提供四个函数:
EVP.Cipher.__init__(self, alg, key, iv, op, key_as_bytes=0, \ d='md5', salt='12345678', i=1, padding=1) EVP.Cipher.update(self, data) EVP.Cipher.final() EVP.Cipher.set_padding(self, padding=1)
下面是一段使用blowfish算法将明文"fish is here"
加密成密文的函数代码:
def blowfish_encrypt(s, password): out=StringIO() m=EVP.Cipher("bf_ecb", password, "123456", 1, 1, "sha1", "saltsalt", 5, 1) out.write(m.update(s)) out.write(m.final()) return out.getvalue()
可以发现,最主要的是Cipher
的构造函数:
EVP.Cipher.__init__(self, alg, key, iv, op, key_as_bytes=0,\ d='md5', salt='12345678', i=1, padding=1)
alg
是指算法的名字,OpenSSL支持以下算法:
可以注意到各种算法的名字除了算法本身的名字外,还带有各种后缀。比如blowfish有bf_cbc
, bf_ecb
多个名字。这些后缀是什么意思呢?在本文的最后附带了我另一份学习笔记,其中有不完整的解释。现在先来看看技术上如何使用这些算法吧。
key
是加密所用的密钥。传入的是一段二进制数据,其长度是密钥的长度。不过,如果后面的参数key_as_bytes==1
,那key
是一个普通的任意长度的字符串,将与salt
, i
参数一起生成一个真正的密钥。比如说,假设算法alg
的密钥长度是16
,如果key_as_bytes==0
,那么key
应该传入"\xff\xff"
两个字节的字符串。如果key_as_bytes==1
,则可以传入类似于123456
这样子的字符串。
iv
是指初始向量。与加密算法所使用的加密块的长度一致。有些加密算法并不使用iv
这个变量。如果key_as_bytes==1
。虽然OpenSSL的key_to_bytes()
函数可以使用alt
, key
,salt
,d
, i
四个参数生成真正的密钥和iv
。但是M2Crypto内部并没有这样子做。而是直接使用原来的iv
.如果iv
的长度超过了加密算法所使用的加密块的长度,超过的长度会被截取。
op
用于指示解密或者加密操作。op==1
表示加密操作;op==0
表示解密操作。在做逆操作的时候,除了op
不一样,其它参数应当保持一致。
key_as_bytes
参数如前所述。如果key_as_bytes==1
。M2Crypto会使用alg
, key
, d
, salt
, i
五个参数生成真正的密钥(注意,没有使用IV)。如果key_as_bytes==0
,表示传入的是真正的密钥,d
, salt
, i
三个参数就没有意义了。
d
是指生成密钥时所使用的散列算法。可以选择md5
, sha1
等。最好使用sha1
,因为md5
的破解看来只是时间问题了。
salt
是指生成密钥时所使用的盐。M2Crypto默认是123456。
i
是指生成密钥时所迭代的次数。迭代次数越多,使用暴力攻击就越不容易。
padding
是指填充加密块。大多数加密算法是以块为单位进行加密的。明文被切分为一个个固定大小的块。然后分别进行加密,得到与原来大小一致的加密块。但是明文的长度并不一定是加密块长度的整数倍。因此在处理最后一个块时需要进行填充。常用的填充算法是PKCS padding.如果没有允许padding并且最后一段明文不足以达到加密块的长度。EVP_EncryptFinal_ex()
会返回一个错误。如果padding
是允许的,但是密文最后并没有包含一个正确的填充块,EVP_DecryptoFinal()
就会返回一个错误。padding
默认是允许的。
生成RSA密钥
DSA与RSA是比较常用的两种非对称加密算法。他们的使用方法与特性正如他们的名字,基本上大同小异。在OpenSSL内,使用与其它名字一样的结构体来表示这两个算法的密钥。在M2Crypto里,也是如此。只是在M2Crypto里DSA与RSA是两个类,带有签名、验证等方法。
一般并不构造RSA与DSA类。而使用相应的工厂方法。比如生成RSA密钥:
from M2Crypto import BIO, RSA #此函数生成一个1024位的RSA密钥,将其转化成PEM格式返回 def genrsa(): bio=BIO.MemoryBuffer() rsa=RSA.gen_key(1024, 3, lambda *arg:None) rsa.save_key_bio(bio, None) return bio.read_all()
RSA.gen_key()
是一个工厂方法,它返回一个存储了新的RSA密钥的RSA.RSA()
实例。它的方法签名是:gen_key(bits, e, callback=keygen_callback)
bits
参数是指RSA密钥的长度,1024以下的RSA密钥虽然还没有被破解,但是已经认为是不安全的了。作为CA使用的RSA密钥通常要求达到2048位以上。e
是RSA算法的"public exponent"。功能是什么?我也不大清楚,据OpenSSL的文档说,这个函数通常是三个奇数3
,17
,65537
之一。callback
是一个回调函数。用于显示生成密钥的进度。具体请查阅OpenSSL的文档。
这里是OpenSSL中对应的函数原型:
#include <openssl/rsa.h> RSA *RSA_generate_key(int num, unsigned long e, void (*callback)(int,int,void *), void *cb_arg);
生成DSA密钥
DSA算法相关的估计是另外一伙人开发的。API有些不大一样。它首先需要生成参数,然后才能生成密钥。以下是一段代码:
from M2Crypto import BIO, DSA #此函数生成一个1024位的DSA密钥,将其转化成PEM格式返回 def gendsa(): bio=BIO.MemoryBuffer() dsa = DSA.gen_params(1024, lambda *arg: None) dsa.gen_key() dsa.save_key_bio(bio,None) return bio.read_all()
可以发现生成DSA密钥时需要首先使用DSA.gen_params()
生成DSA参数。gen_params()
函数的第一个参数是DSA密钥的长度,第二个密钥与RSA.gen_key()
的回调函数相同。DSA.gen_params()
返回一个DSA类的实例。调用DSA.gen_key()
方法生成密钥。其它的与RSA类似。
载入DSA密钥与RSA密钥
RSA:
返回RSA类型:
load_key(file, callback=util.passphrase_callback) load_key_bio(bio, callback=util.passphrase_callback) load_key_string(string, callback=util.passphrase_callback)
返回RSA_pub类型:
load_pub_key(file) load_pub_key_bio(bio)
DSA:
返回DSA类型:
load_params(file, callback=util.passphrase_callback) load_params_bio(bio, callback=util.passphrase_callback) load_key(file, callback=util.passphrase_callback) load_key_bio(bio, callback=util.passphrase_callback)
返回DSA_pub类型:
load_pub_key(file, callback=util.passphrase_callback) load_pub_key_bio(bio, callback=util.passphrase_callback)
这些函数大同小异。如果参数名字是file
的话,代表的是一个文件名。如果参数名字是bio
的话,代表的是一个BIO对像。BIO对象与Python的file
对象类似都是用于表示一个可以读写的类似于文件的类型。BIO对象除了可以是一个普通的文件,还可以是一个ssh连接,还可以是一段内存(BIO.MemoryBuffer
)。BIO.MemoryBuffer
与Python的StringIO.StringIO
类似。因为之前我们提到我编译的M2Crypto在进行文件IO的时候会异常退出,所以最好只使用BIO.MemoryBuffer
。
在本文里,提到密钥,是同时指公钥与私钥。
在OpenSSL及大多数软件里,因为公钥会被单独分发出去,所以公钥可以单独保存在公钥文件里。而密钥的所有者既然保存私钥,肯定也会同时保存公钥。故而私钥并不会单独保存到一个私钥文件里,而是和公钥一起保存在密钥文件里。
RSA类型的操作——使用RSA加密、解密、签名、认证;保存RSA密钥
RSA类型封装了一些可以使用RSA密钥进行的操作。
首先,可以使用len(rsa)
获得RSA密钥的长度。单位是位,通常使用1024位以上的密钥才是安全的。
def public_encrypt(self, data, padding):
使用公钥进行加密。data
是数据,是一段普通的字符串。而padding
参数是指填充加密块的模式。它是一个枚举类型。根据OpenSSL的文档,padding
有四个取值:no_padding
,pkcs1_padding
,sslv23_padding
,pkcs1_oaep_padding
。最好是只使用pkcs1_oaep_padding
,因为它会在加密过程中插入一些随机数,防止相同的明文产生相同的密文,缺点是密文肯定会比明文长很多。要注意的是,一次加密的明文不能太长,只能等于或者小于密钥的长度。当padding==RSA.no_padding
时因为没有填充,所以要求len(data)==len(rsa)//8
,不能多不能少。对于pkcs1_oaep_padding
,必须小于等于len(rsa)//8-42
。如前所述,len(rsa)
是密钥的长度,单位是位,换算成字节要除8。
def public_decrypt(self, data, padding):
使用公钥进行解密。
def private_encrypt(self, data, padding):
使用私钥进行加密。
def private_decrypt(self, data, padding):
使用公钥进行加密。
因为RSA是一种非对称加密算法。所以用私钥加密的数据,要用公钥才能解密。反之,用公钥加密的数据,要用私钥才能解密。通常在通信中,发送方使用接收方的公钥加密数据。_只有_接收方才有私钥能够解密数据。因为非对称加密算法的速度一般比对称加密算法慢,所以在一个连续的通信过程中,经常是发送方随机生成一个对称加密算法的密钥,然后使用非对称加密算法发送给接收方,以后所有的通信过程都是使用这个随机密钥。只要保证每隔一段时间就换一个密钥,这个通信过程就跟直接使用非对称加密算法一样安全了。直接使用非对称加密算法的情况很少见。
非对称算法还经常用于对数据进行签名。签名可以保证发送方不能否认自己发送的数据是自己的。比如,在一个电子商务交易中,客户下了一个订单,不能等工厂已经生产完了才否认这个订单是自己下的。签名最简单的办法当然是使用发送方的私钥进行加密。如果不使用发送方的公钥就不能解密数据。反之,也可以说,凡是可以使用发送方的公钥解密出数据,就说明数据是使用发送方的私钥加密的。在现实生活中,人们一般是使用SHA1之类的散列算法算出数据的散列值,然后再用私钥加密这个散列值。接收方接收到数据与散列值之后,同样使用SHA1算法算出数据的散列值,与使用公钥解密出来的散列值作对比。如果是一样的,说明数据正确。如果不一样,或者是在传输过程中被更改了,或者根本不是发送方所发送的。
RSA算法提供了两种签名的方式,其分别可能是不同的国际标准。我还不是很清楚。
第一组:
sign_rsassa_pss(self, digest, algo='sha1', salt_length=20) verify_rsassa_pss(self, data, signature, algo='sha1', salt_length=20)
这组API与下面两个函数类似。看起来差不多的样子,不过我没有进行过测试。实际上OpenSSL中并没有sign_rsassa_pss()
这样函数。它实际上是分为两个步骤:RSA_padding_add_PKCS1_PSS()
和RSA_private_encrypt()
。而verify_rsassa_pss()
函数则分为RSA_public_decrypt()
与RSA_verify_PKCS1_PSS()
两个步骤
第二组:
sign(self, digest, algo='sha1') verify(self, data, signature, algo='sha1')
这组API对应于OpenSSL中的RSA_sign()
与RSA_verify()
函数。分别是签名与验证。虽然sign()
方法接收散列算法的名字作为名字,但实际上digest
参数应该是已经计算出的散列值。以下是对一段数据进行签名的代码:
#发送方对数据进行签名 from M2Crypto import * m=EVP.EVP.MessageDigest("sha1") #先计算散列值 m.update("fish is here") digest=m.final() key_str=file("fish_private.pem", "rb").read() #读入私钥 key=RSA.load_key_string(key_str, util.no_passphrase_callback) #签名后得到的数据。与原始数据一起发送出去。 result=key.sign(digest, "sha1") :::python #接收方验证数据 from M2Crypto import * m=EVP.EVP.MessageDigest("sha1") #先计算散列值 m.update("fish is here") digest=m.final() #先计算散列值 cert_str=file("fish_public.pem", "rb").read() #读入公钥 mb=BIO.MemoryBuffer(cert_str) #RSA模式没有load_pub_key_string()方法,需自行使用MemoryBuffer cert=RSA.load_pub_key_bio(mb) cert.verify(digest, result, "sha1") #返回True/False
一个小型的CA,电子证书。
说到CA,不得不说到PKI认证体系。PKI体系是一个概念性的认证系统。它的基本原理是,通信系统内所有节点都承认一个权威的机构,称为CA。所有参与通信的节点都有一个电子证书,由该节点的公钥和身份认证信息组成。CA核查这个电子证书的身份信息是否正确。如果正确的话,就使用CA的秘钥进行签名。这样,所有的通信节点就可以使用电子证书内包含的身份信息而不必亲自核查了。
所有通信节点都持有CA的电子证书。CA的电子证书是CA自己签名的。在PKI系统运行之前,人们会事先设置好CA证书。
通信节点生成电子证书的过程是:
生成非对称加密算法的密钥在之前已经有提到过了。接下来是如何生成证书请求文件的代码:
from M2Crypto import * #首先载入密钥文件。此文件同时保存了通信节点的私钥与公钥。 #这里并不像之前直接使用 pkey_str=file("fish_private.pem", "rb").read() pkey=EVP.load_key_string(pkey_str, util.no_passphrase_callback) req=X509.Request() req.set_pubkey(pkey) #包含公钥 #req.set_version(1) #身份信息不是简单的字符串。而是X509_Name对象。 name=X509.X509_Name() #CN是Common Name的意思。如果是一个网站的电子证书,就要写成网站的域名 name.CN="Goldfish" #Organization Unit,通常是指部门吧,组织内单元 name.OU="Dev Department" #Organization。通常是指公司 name.O="Besteam" #State or Province。州或者省 name.ST="Fujian" #Locale。 name.L="Quanzhou" #国家。不能直接写国家名字,比如China之类的,而应该是国家代码。 #CN代表中国。US代表美国,JP代表日本 name.C="CN" req.set_subject(name) #包含通信节点的身份信息 req.sign(pkey, "sha1") #使用通信节点的密钥进行签名 file("fish_req.pem", "wb").write(req.as_pem()) #写入到文件
可以发现,如果简化那些设置身份信息的代码,实际上就是三步:包含公钥、包含身份信息、签名。
接下来,我们看看CA是如何给一个通信节点发放证书的。
from M2Crypto import * import time #首先读取证书请求文件。 req_str=file("fish_req.pem", "rb").read() #返回一个X509.Request类型代表证书请求文件 req=X509.load_request_string(req_str) #首先验证一下,是不是真的是使用它本身的私钥签名的。 #如果是,返回非0值。如果不是,说明这是一个非法的证书请求文件。 print req.verify(req.get_pubkey()) #接下来载入CA的电子证书。与CA的密钥不一样,CA的电子证书包含了CA的身份信息。 #CA的电子证书会分发给各个通信节点。 ca_str=file("ca.pem", "rb").read() ca=X509.load_cert_string(ca_str) #可以使用check_ca()方法判断这个证书文件是不是CA。 #本质是判断它是不是自签名。如果是的话,就返回非0值。如果不是的话就返回0。 #print ca.check_ca() #接下来载入CA的密钥 cakey_str=file("cakey.pem", "rb").read() #一般CA的密钥要加密保存。回调函数返回密码 cakey=EVP.load_key_string(cakey_str, lambda *args:"1234") #接下来开始生成电子证书 cert=X509.X509() #首先,设定开始生效时间与结束生效时间 t = long(time.time()) + time.timezone #当前时间,单位是秒 #开始生效时间。证书的时间类型不是普通的Python datetime类型。 now = ASN1.ASN1_UTCTIME() now.set_time(t) nowPlusYear = ASN1.ASN1_UTCTIME() #结束生效时间 nowPlusYear.set_time(t + 60 * 60 * 24 * 365) #一年以后。 cert.set_not_before(now) cert.set_not_after(nowPlusYear) #把证书请求附带的身份信息复制过来 cert.set_subject(req.get_subject()) #设置颁发者的身份信息,把CA电子证书内身份信息复制过来 cert.set_issuer(ca.get_subject()) #序列号是指,CA颁发的第几个电子证书文件 cert.set_serial_number(2) #把证书请求内的公钥复制过来 cert.set_pubkey(req.get_pubkey()) #使用CA的秘钥进行签名。 cert.sign(cakey, "sha1") file("fishcert2.pem", "wb").write(cert.as_pem())#保存文件。
补充:加密模式与初始向量,为什么对称加密算法有各种后缀?
对称加密算法除了要指明填充方式以外,在使用块加密前还需要指定它所使用的模式。这个模式定义了Cipher如何应用加密算法。改变模式可以容许一个块加密程序变为流加密程序。
分组密码每次加密一个数据分组,这个分组的位数可以是随意的,一般选择64或者128位。另一方面,流加密程序每次可以加密或解密一个字节的数据,这就使它比流加密的应用程序更为有用。
ECB(Electronic Code Book:电码本)
ECB是最简单的模式,同样的明文分组总是加密成相同的密文分组。这对于发送单一的块数据来说是非常好的,如密钥。但对执行一个加密的信息流来说不是很好,因为如果相同的明文多次发送以后,同样的密文也会被多次发送。
ECB最大的弱点是对每一个块用相同的方式进行加密。如果我们的密钥或者数据不断发生变化,ECB是完全安全的。但是如果类似的块经过同样的密钥加密发出以后,攻击者可能获得一些我们并不想让别人知道的信息。
CBC(Cipher Block Chaining:密码分组链接)
CBC模式改变了加密方式,同样的明文分组不一定加密或解密同样的密文块,因此解决了ECB存在的主要问题。CBC使用前一分组的信息加密当前分组。因此和ECB模式大不相同。这个方法依然存在问题,那就是相同的信息仍然加密成相同的密文,因为所有的分组是同时变成密文分组的。为了解决这个问题,我们引入一个Initialization Vector(初始化向量),也就是前不久有人问到的IV问题。IV仅仅是一个初始化加密程序的随机数。它无需秘密保存,但队每一个信息来说它都是不同的,通过这个方式,即使有两条相同的信息,只要他们有不同的IV,那么他们加密后的密文也是不同的。从这个意义上来说,初始化向量无疑就和口令加密过程中使用的盐值是一样的。
CBC很适合文本传输,但它每一次都需要传送一个完整的数据块,一般选8个字符。
CFB(Cipher FeedBack:密码反馈)
CFB的工作方式与CBC类似,但它可以执行更小的数据块,典型的有8位,这非常适合加密像聊天对话这样的信息,因为每次可以发送单一的字节数据块。
和CBC一样,CFB也需要一个IV,且相同及钥发送的每条信息的IV都必须是唯一的。
OFB(Output FeedBack:输出反馈)
OFB除了在传输中能给数据提供更好的保护,防止数据丢失外,其他和CFB类似。密文中一位出错,也只造成明文中的一位出错,其他的方式会造成整个块丢失。
和CBC以及CFB一样,OFB也需要一个IV。
各种加密模式一般在密码学的教程中可以找到。
补充:X509电子证书的文件格式。
X509定义了用于存储公钥电子证书,证书撤回列表的文件格式。还定义了一个电子证书验证的路径算法。X509通常也称为PKIX for Public Key Infrastructure (X.509),这说明了它是PKI体系的一种实现方式。X509所使用的多种文件格式:
其中,PKCS12是指RSA提出的一种X509文件格式标准。X509还有很多的文件格式标准,比如PEM,不过PKCS12的好处是可以存储使用密码加密保存的私钥。它用于代替微软的PFX格式。
PEM实际上DER的BASE64形式,并且在文件头与文件尾各加上:"-----BEGIN CERTIFICATE-----"和"-----END CERTIFICATE-----"。OpenSSL可以在这两种格式之间互转。OpenSSL在PEM文件里保存私钥的时候,文件头与文件尾分别是:"-----BEGIN RSA PRIVATE KEY-----"和"-----END RSA PRIVATE KEY-----"。