1. 概述
上篇讲述了秘钥的生成、存储和加载,这篇的内容就是如何生成和校验数字签名。
2. Signature类
在Java中,签名和校验,都是通过: Signature 类来实现的。
该类的主要方法如下:
- getInstance(String algorithm)
工厂方法,获取Signature实例,而参数:algorithm就是签名算法的名称,这里我们使用的是:SHA256withECDSA
更具体的参数,请浏览:https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Signature
- initSign(PrivateKey privateKey)
- initVerify(PublicKey publicKey)
初始化成签名或者校验,Signature类的实例,在同一个阶段只能完成签名或者校验之一的任务,因此调用initSign或者initVeify让Signature进入相应的状态
- update(byte[] data)
无论是计算签名,还是校验数据,都需要传入被签名或者被校验的数据,调用该方法进行计算。注意,该方法可以调用多次,通常数据都可能非常大,不可能一次性读入内存(从磁盘上或者网络),因此我们可以对数据进行分块,一次性一KB或者合适的块进行多次调用
- sign()
获取签名,当所有的数据都调用update计算后,就可以获取签名了,返回的签名是一个byte数组
- verify(byte[] signature)
校验签名,当所有的数据都调用update计算之后,调用该方法,传递签名数据,如果签名正确,那么返回true,否则返回false,说明数据可能被篡改、伪造,或者对应的私钥不正确。
3. 实例
Java中计算签名比较简单,请阅读下面代码中的注释
public static byte[] signData(String algorithm, byte[] data, PrivateKey key) throws Exception {
Signature signer = Signature.getInstance(algorithm);
signer.initSign(key);
signer.update(data);
return (signer.sign());
}
public static boolean verifySign(String algorithm, byte[] data, PublicKey key, byte[] sig) throws Exception {
Signature signer = Signature.getInstance(algorithm);
signer.initVerify(key);
signer.update(data);
return (signer.verify(sig));
}
@Test
public void testSignVerify() throws Exception {
// 需要签名的数据
byte[] data = new byte[1000];
for (int i=0; i<data.length; i++)
data[i] = 0xa;
// 生成秘钥,在实际业务中,应该加载秘钥
KeyPair keyPair = KeyUtil.createKeyPairGenerator("secp256k1");
PublicKey publicKey1 = keyPair.getPublic();
PrivateKey privateKey1 = keyPair.getPrivate();
// 生成第二对秘钥,用于测试
keyPair = KeyUtil.createKeyPairGenerator("secp256k1");
PublicKey publicKey2 = keyPair.getPublic();
PrivateKey privateKey2 = keyPair.getPrivate();
// 计算签名
byte[] sign1 = signData("SHA256withECDSA", data, privateKey1);
byte[] sign2 = signData("SHA256withECDSA", data, privateKey1);
// sign1和sign2的内容不同,因为ECDSA在计算的时候,加入了随机数k,因此每次的值不一样
// 随机数k需要保密,并且每次不同
// 用对应的公钥验证签名,必须返回true
Assert.assertTrue(verifySign("SHA256withECDSA", data, publicKey1, sign1));
// 数据被篡改,返回false
data[1] = 0xb;
Assert.assertFalse(verifySign("SHA256withECDSA", data, publicKey1, sign1));
data[1] = 0xa;
Assert.assertTrue(verifySign("SHA256withECDSA", data, publicKey1, sign1));
// 签名被篡改,返回false
// 签名为DER格式,前三个字节是标识和数据长度,如果修改了这三个会抛出异常,无效签名格式
sign1[20] = (byte)~sign1[20];
Assert.assertFalse(verifySign("SHA256withECDSA", data, publicKey1, sign1));
// 使用其他公钥验证,返回false
Assert.assertFalse(verifySign("SHA256withECDSA", data, publicKey2, sign1));
}
我们在测试用例中可以看到,只要是数据、签名被修改,或者不正确的私钥都会引发签名验证失败.
4. 签名输出数据解析
针对 SHA256withECDSA
,输出的是DER编码的签名数据,长度并非固定,但是是70到72字节,之所以会这样是因为,SHA256withECDSA
签名输出实际是两个32字节的大整数(r和s),在转换成byte[]的时候,如果为负数,那么会添加前导位0,如果当r和s都是正数,那么就是64个字节,都是负数,就是66字节。而DER编码还会包含类型以及长度字段,因此总长度就会到70到72字节。一般情况下,传输DER编码的签名值没多大问题,但如果对数据量要求十分严格,例如在BLE上传输,可以提取出r和s再打包传输