我们可以通过验证消息摘要,来检测文件的完整性。但是消息摘要机制是否可以检测出有人恶意的对文件进行篡改呢?并不能完全检测出来。原因是这样的:首先消息摘要算法是公开的(因为需要用于验证消息的完整性,所以消息摘要算法必须要公开)。其次,尽管某些消息摘要算法提供了密钥机制——比如Hmac算法,但是,其密钥也是对文件接收方公开的。公开的消息摘要算法加上公开的密钥,就给了心存歹意的人修改文件,并且重新计算消息摘要的可能。举个例子:
- A给B发送了一个消息M1,并且附带了M1的消息摘要D1
- B收到消息,通过消息摘要D1验证了M1的完整性
- B把消息M1改成了M2,为了跟加具有欺骗性,B使用和A相同的消息摘要算法重新计算M2的消息摘要D2
- B把M2以及D2发送给了C,并且生成M2就是A发送给我的消息,如果不相信,你可以比对消息摘要
- C收到M2后,进行消息摘要,发现果然和D2是匹配的。但是实际上消息已经被B篡改了。
如何防止消息被人恶意篡改呢?其实,这里消息可以被篡改的主要原因就是算法和密钥是公开的。但是,如果算法和密钥不公开,那么收到消息的人如何对消息进行验证呢?问题似乎陷入了一个死循环中。终于,非对称加密算法帮助我们完美的解决了这一问题。
所谓非对称加密算法(Asymmetric Algorithm),实际上是指加密和解密所使用的密钥(Key)是不一样的。非对称加密算法的Key是成对出现的,分为公钥(Public Key)和私钥(Private Key)。如果使用公钥加密,则需要使用私钥解密;反之,如果使用私钥加密,则需要使用公钥解密。一般情况下私钥自己保存,公钥公开给对方。
这种利用非对称加密算法对消息进行摘要的方法叫做“签名(Signature)”
因为有了非对称加密算法,因此我们可以设计这样一个验证流程:
- 使用私钥对消息进行消息摘要(签名)
- 将消息/消息摘要(签名)/公钥,一起发送给接收方
- 接收方利用公钥验证消息摘要(签名)
这样一来,消息接收方可以利用公钥对消息摘要(签名)进行验证,但不可能在修改消息后重新计算签名,因为消息接收方是没有消息发送方的私钥的。
我们知道普通的消息摘要算法,无论数据有多长,它们的消息摘要都是固定长度的(MD5-128Bits, SHA1-160Bits)。但是,非对称加密算法主要是用于数据加密用的,因此它的计算结果的长度和原始数据的长度有关,也就是说原始数据越长,计算出的结果也就越长(一般会大于原始数据的长度,因为在加密之前需要进行Padding,稍后详细解释)。显然,这种加密结果并不适合用于消息摘要,因为这产生了很大的资源浪费,也降低了验证签名的效率。
为了解决这个问题,非对称加密算法通常和消息摘要算法一起使用,共同构成了签名算法。签名算法的流程基本上是这样的:
- 使用一种消息摘要算法(MD5/SHA...)对需要签名的消息M(不定长)进行消息摘要,得到消息摘要D(定长)。
- 使用非对称加密算法(DSA/RSA)对消息摘要D进行加密,并得到签名S。
也就是这样一个流程:M -> D -> S
通过将消息摘要算法和非对称加密算法进行组合使用,我们就完美的达到了计算消息签名的目的。
Java中的使用方法
在Java中需要借助Signature这个Engine类对消息进行签名和验证。
- 通过调用Signature.getInstance方法,向JCA请求一个Signature的实例
- 针对使用Signature的目的不同(签名或者验证),调用不同的初始化方法,并传入对应的Key(initSign/initVerify)。签名的时候需要传入Private Key,验证的时候需要传入Public Key
- 调用update方法,将待签名的数据传入Signature对象
- 调用sign方法对数据进行签名;或者,调用verify方法,对传入的签名进行验证。
另外,Signature这个类的对象是有状态的,状态信息保存在内部的state属性中。它有三种可能的状态:
- UNINITIALIZED - Signature类被实例化之后,没有调用init方法之前,signature就是这个状态
- SIGN - 调用initSign方法之后,signature的状态变成SIGN
- VERIFY - 调用initVerify方法之后,signature的状态变成VERIFY
根据Signature状态的不同,可以进行的操作也不同:
- UNINITIALIZED - 只能进行initSign或者initVerify
- SIGN - 只能进行update以及sign
- VERIFY - 只能进行update以及verify
- 但是允许在任何时候调用init方法,这样将会重新初始化signature,并且清空之前的数据,改变Signature的状态。
签名
public static byte[] sign(byte[] data, PrivateKey key, String algorithm) { try { Signature signature = Signature.getInstance(algorithm); signature.initSign(key); signature.update(data); byte[] sign = signature.sign(); return sign; } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { throw new RuntimeException(ex); } } public static String sign(String text, PrivateKey key, String algorithm) { try { byte[] data = text.getBytes("UTF-8"); byte[] sign = sign(data, key, algorithm); String signHex = DatatypeConverter.printHexBinary(sign); return signHex; } catch (UnsupportedEncodingException ex) { throw new RuntimeException(ex); } } public static void main(String[] args) { String message = "Hello this is a sensitive message"; KeyPair keyPair = generateKeyPair("RSA", 512); String sign = sign(message, keyPair.getPrivate(), "SHA256WithRSA"); System.out.println("Signature: " + sign); }
执行上面的程序,我的环境输出如下:
Signature: 61447BDAD564E65015CFEDE904AD2BEE7CA0B65CB83FCC09FCE6F18F16A4B54920DBDBBA5F416567F82D57D53360E142DC25DB5185C5E35BB37191DCF6168AD2
因为KeyPair每次都是随机生成的,所以每次运行输出都不一样。
下面我们来分析一下这个输出。目前使用的签名算法是SHA256WithRSA——先使用SHA256计算消息摘要,然后对消息摘要的结果进行RSA加密。SHA256的结果是256 Bits,也就是32 Bytes。32 Bytes如果用16进制表示法输出,应该是一个长度为64的字符串。可是为什么这个字符串的长度是128呢?原因就在RSA加密之前,其实还有一个补全(padding)的过程,这一步会将消息摘要的结果补全到一个特定的长度,之后在补全的基础上再进行加密。在Java的默认实现中,这个补全长度和key的长度是一致的。上例中是512 Bits。因此最后加密出来的结果也是512 Bits也就是64 Bytes。使用16进制表示就是长度为128的字符串。
验证
public static boolean verify(byte[] data, byte[] sign, PublicKey key, String algorithm) { try { Signature signature = Signature.getInstance(algorithm); signature.initVerify(key); signature.update(data); boolean verified = signature.verify(sign); return verified; } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { throw new RuntimeException(ex); } } public static boolean verify(String text, String signHex, PublicKey key, String algorithm) { try { byte[] data = text.getBytes("UTF-8"); byte[] sign = DatatypeConverter.parseHexBinary(signHex); boolean verified = verify(data, sign, key, algorithm); return verified; } catch (UnsupportedEncodingException ex) { throw new RuntimeException(ex); } } public static void main(String[] args) { String message = "Hello this is a sensitive message"; String sign = ""; //使用上例中计算出的签名 PublicKey publicKey; //使用上例中对应的PublicKey boolean verified = verify(message, sign, publicKey, "SHA256WithRSA"); System.out.println("Verified: " + verified); }
如果一切正常,运行程序将会得到如下输出:
Verified: true
尝试改变message的值,将会得到:
Verified: false
这样我们就可以验证消息是否曾被被恶意修改过。
关于密钥
一般而言签名所需要的公钥和私钥都不是在程序中临时生成的,而是之前已经生成好,并且以特定形式保存在文件中的。想要做到这一点有很多种方法,可以通过Key Management API来实现,也可以通过获取key中的属性,自己操作文件进行保存/读取。但是,目前这里暂时不讨论具体的实现方法。
总结
Signature的使用比较简单,在了解了Key的生成之后,基本没有什么特殊的问题。以下内容比较重要:
- 为什么要使用Signature,它是为了解决什么问题的?
- 什么是非对称加密算法,它有什么特点?
- 对数据进行签名的过程是怎样的?
No comments :
Post a Comment