关于什么是数字签名,可以看阮一峰的这篇博客
说简单些就是一个为了确保信息完整性和正确性的技术,他的原理就是在要传输的数据上附带一个经过发件人私钥加密的原文摘要数据,这个摘要可以用发件人的公钥解密,之后与接收到数据的摘要核对就可以验证准确性了.
通常数字签名的作用是
- 确保信息的签名来自特定的来源
- 确保信息未经修改
它常用于:
- cd-key,让用户获得特定服务的使用权
- 用户登录,在无服务的服务器上用户传递用户信息
在上一篇加密算法中我们已经介绍过非对称加密算法在数字签名中的应用.下面这个例子我们可以简单回顾下创建签名的流程.
例:使用PyCryto的RSA算法创建简单签名
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA
from Crypto.Signature import PKCS1_v1_5 as Signature_pkcs1_v1_5
import base64
- 创建签名
#使用自己的秘钥对内容进行签名
message = b'hello , this is a test text'
with open('source/private.pem',"rb") as f:
key = f.read()
rsakey = RSA.importKey(key)
signer = Signature_pkcs1_v1_5.new(rsakey)# 构建签名
digest = SHA.new()
digest.update(message) # 使用SHA算法获得摘要
sign = signer.sign(digest) # 使用摘要签名
signature = base64.b64encode(sign) #序列化
signature
b'Gr6PuLUAjz8QnAVh58wA26BnR9CEfUPsYImu5wmQTt1DUsSZcqy7Yq7WxtFaB3wXyEu5I3cSy41puzWdxJ03/Nw+/JWvZ8nUc1rcVkow8vAdsipOXEY+pgf/mhApn4ebWrlyBGqkZRmj1EC/sZ9eZYLwxqehmacOhAxy/6MKKnqBrv4zbLSoW9EFSTIOG+9ILuFlSH2fuafesjzk5U356xJRWHkRdHI6FwWffROW+bbtk6J0/R6v4zu8Z+mWOwgyPLzCB+hlJHo6qslMBqrhLfCDTzcEJrEoo2FEEj8PfLmNgpABgXpmlGcDnKKQD9A5nf2VQbqJ9kNdcbJ5ya5b9w=='
- 签名验签
#使用公钥验签
with open('source/public.pem',"rb") as f:
key = f.read()
rsakey = RSA.importKey(key)
verifier = Signature_pkcs1_v1_5.new(rsakey)
digest = SHA.new()
# Assumes the data is base64 encoded to begin with
digest.update(message)
is_verify = signer.verify(digest, base64.b64decode(signature))#对比解码后的签名和原文的摘要已确认
print(is_verify)
True
itsdangerous是一个序列化数据生成签名的工具,它内部使用hmac和sha1来签名,支持jsonweb签名
from itsdangerous import Signer
s = Signer('secret-key')
l=s.sign('my string')
l
'my string.wh6tMHxLgJqB6oY1uT73iMlyrOA'
签名会被加在字符串尾部,中间由句号 (.)分隔。验证字符串,使用 unsign() 方法:
s.unsign(l)
'my string'
如果被签名的是一个unicode字符串,那么它将隐式地被转换成utf-8。然而,在反签名时,你没法知道它原来是unicode还是字节串。因此一个好习惯是用统一的字符串形式
如果你想要可以过期的签名,可以使用 TimestampSigner 类,它会加入时间戳信息并签名。在反签名时,你可以验证时间戳有没有过期:
from itsdangerous import TimestampSigner
s = TimestampSigner('secret-key')
string = s.sign('foo')
s.unsign(string, max_age=5)
'foo'
s.unsign(string, max_age=5)
'foo'
所有的类都接受一个盐的参数。这名字可能会误导你,因为通常你会认为,密码学中的盐会是一个和被签名的字符串储存在一起的东西,用来防止彩虹表查找。这种盐是公开的。
与Django中的原始实现类似,itsdangerous中的盐,是为了一个截然不同的目的而产生的。你可以将它视为成命名空间。就算你泄露了它,也不是很严重的问题,因为没有密钥的话,它对攻击者没什么帮助。
假设你想签名两个链接。你的系统有个激活链接,用来激活一个用户账户,并且你有一个升级链接,可以让一个用户账户升级为付费用户,这两个链接使用email发送。在这两种情况下,如果你签名的都是用户ID,那么该用户可以在激活账户和升级账户时,复用URL的可变部分。现在你可以在你签名的地方加上更多信息(如升级或激活的意图),但是你也可以用不同的盐:
from itsdangerous import URLSafeSerializer
s1 = URLSafeSerializer('secret-key', salt='activate-salt')
s1.dumps(42)
'NDI.kubVFOOugP5PAIfEqLJbXQbfTxs'
s2 = URLSafeSerializer('secret-key', salt='upgrade-salt')
s2.dumps(42)
'NDI.7lx-N1P-z2veJ7nT1_2bnTkjGTE'
s2.loads(s1.dumps(42))
---------------------------------------------------------------------------
BadSignature Traceback (most recent call last)
<ipython-input-16-5be376055ed7> in <module>()
----> 1 s2.loads(s1.dumps(42))
/Users/huangsizhe/LIB/CONDA/anaconda/envs/py2/lib/python2.7/site-packages/itsdangerous.pyc in loads(self, s, salt)
580 """
581 s = want_bytes(s)
--> 582 return self.load_payload(self.make_signer(salt).unsign(s))
583
584 def load(self, f, salt=None):
/Users/huangsizhe/LIB/CONDA/anaconda/envs/py2/lib/python2.7/site-packages/itsdangerous.pyc in unsign(self, signed_value)
372 return value
373 raise BadSignature('Signature %r does not match' % sig,
--> 374 payload=value)
375
376 def validate(self, signed_value):
BadSignature: Signature 'kubVFOOugP5PAIfEqLJbXQbfTxs' does not match
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
s = Serializer('SECRET_KEY', 30)
token = s.dumps({'confirm': "hsz"})
token
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ5ODgzMDU5NiwiaWF0IjoxNDk4ODMwNTY2fQ.eyJjb25maXJtIjoiaHN6In0.4Rrx-XF5XH2fEWitw9eQjEeffcKhNWcSE8s81FYtw1g'
data = s.loads(token)
data
{u'confirm': u'hsz'}
data = s.loads(token)
JWT是json web token的缩写,是一种针对http协议的数字签名规范,它主要解决的是http服务水平扩展的问题.
我们知道HTTP协议本身是无状态的,但实际使用中我们往往需要保存用户状态,为了保存状态,我们想了很多方法:
-
将用户状态直接保存在cookies中,这种方式确实可以保存用户的一些状态信息,但用户也可以自己修改cookie来伪造信息.因此现在基本不会有人去用
-
用户的每次会话设定一个session_id,并用这个session_id作为key来保存会话过程中的状态信息,这种方式通常借助redis实现,为每个session_id设置一个过期时间.用户可以通过一些接口延长过期时间.这个方法的好处是每次会话只用传递一个session_id即可,实际的状态信息其实保存在服务器端,相对安全,而且服务器可以更加方便的对会话进行操作.缺点是不利于横向扩展,我们必然需要引入一个共享内存的redis来让多个服务实例可以共享会话信息,这就可能造成资源争抢,而这个共享内存我们就必须保证其健壮不会崩溃.简而言之这种方式会引入副作用,从而让服务不是一个无状态的服务.
-
使用JWT,这种方式类似第一种方法的进化,用户第一次登陆会获得一个JWT,这个token可以被服务验证签名,同时也可以用于传递信息,每次用户对服务的访问都必须带上这个token,这样就可以让服务做到完全的无状态,可以随意做水平扩展.它的坏处是每次都会带一串信息,并且这串信息通畅不可变,使每次请求的数据都变大,同时限制了其灵活性,对于追求极限性能的系统这种方式并不适用.
python下可以使用模块pyjwt做相关的操作
JWT分为3个部分
Header
描述token的元数据,比如使用什么签名/验签算法呀,JWT版本这些.Payload
负载,也就是要传递保存的消息.Signature
签名,对前两部分进行签名以防止数据篡改,一般通过Sha256算法做签名:$$Signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)$$其中secret是盐.当然有时候为了认证签名来源也可以使用上面写的非对称加密的算法做签名和验签
在所有这些都有了以后再最后使用base64UrlEncode
将各个部分已.
号分割连起来
{base64UrlEncode(header)}.{base64UrlEncode(payload)}.{base64UrlEncode(signature)}
我们可以看到JWT是明文的(base64UrlEncode其实也是明文).所以当有敏感数据时我们会再使用一些加密算法比如对称加密的AES算法为token加密.
import jwt
encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
print(encoded)
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U'
decoded = jwt.decode(encoded, 'secret', algorithms=['HS256'])
decoded
{'some': 'payload'}
with open('source/private.pem',"rb") as f:
private_key = f.read()
with open('source/public.pem',"rb") as f:
public_key = f.read()
encoded = jwt.encode({'some': 'payload'}, private_key, algorithm='RS256')
encoded
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.r5fkbniW_a_EuL9O5WrSRj2zOJrSAuWlXnRqJlbIZ1p4fz6gKo6xPDMYQKT9mNXhvmzJuTSwK93hgH0oD0wdENLwWJOUawnGISIzyCzicAcW8muCjSZHwCsxUurc1zayPWTkOXx30Vg9v_sNNnUxJuZyiGWkwn6k93nci_GFIdI7XBfoK4vvYGDjrMYg_osMVzudvAAvo1qMIDhJZNSs9EHT_bq8Zr4k1LHUSHJOmrQ43JPN2gAt1IT5C_kho0bPKkLoGI3K2avUmcmzKU1d5WTnR0C3v7IatE-AAPkoA0n1_aBkvy2iVUfnZMIdqyASRlw82qEjnVRlgbDU2T8-ww'
decoded = jwt.decode(encoded, public_key, algorithms='RS256')
decoded
{'some': 'payload'}