很多人应该了解所谓数字签名算法,其实很简单,无外乎就是先计算哈希再使用私钥加密,这样可以得到一个具有不可否认性的数字签名。
我们以 SHA256withRSA 算法来举例说明,假设明文数据是 data ,然后经过以下过程:
- 使用 SHA256 算法对 data 计算出它的哈希值
- 再使用 RSA256 算法对哈希值进行加密(私钥加密)
这样就可以得到 data 的数字签名了,可事实果真如此吗?
为了验证上面的过程是否正确,我们来设计一个小实验来验证它。我们先用 JDK 计算一个 SHA256withRSA 算法的签名,然后再自己分步骤使用 SHA256 和 RSA256 来计算一个签名,两个签名对比一下,看看是否一致。
下面我们来设计这段程序,首先我们先生成一对 RSA 秘钥对,这里就不贴生成秘钥的源码了,直接把生成后的结果发出来:
/tmp/rsa_pub.pem
-----BEGIN RSA PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwMBtz2j9vpBgVi83KuoIeEmm5oBTG t1W UKz1EbCgDQCebOShNlq1gFPqxk2k6X5P9iLn703wh1FPiZmOIMIDbSCCPfcAGmBpH3odqAcoZgPAtb3g1hGtDx vBbGLLzOY1zXeTeA2QNoyiPN hS4qehLAlv6qqe5lbMkhlDvu0XiIlcqEVpX9CJDK5rVB085wHmq1hb8C4cPg6ToQaiHVRbWOoiem0Jw5F89XP7LwLjsA3W/VO1JYrM7G1OM1zNunTLBus 18n9bKwLaCOd4Cd8l0qwrEE0j /cH2tze 9fI6bduOXgNLShTmRMnEe49lc2mq8bk1JKmqd12uHSxwIDAQAB
-----END RSA PUBLIC KEY-----
/tmp/rsa_pri.pem
-----BEGIN RSA PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDAwG3PaP2 kGBWLzcq6gh4SabmgFMb63Vb5QrPURsKANAJ5s5KE2WrWAU rGTaTpfk/2IufvTfCHUU JmY4gwgNtIII99wAaYGkfeh2oByhmA8C1veDWEa0PH68FsYsvM5jXNd5N4DZA2jKI836FLip6EsCW/qqp7mVsySGUO 7ReIiVyoRWlf0IkMrmtUHTznAearWFvwLhw DpOhBqIdVFtY6iJ6bQnDkXz1c/svAuOwDdb9U7UliszsbU4zXM26dMsG6z7Xyf1srAtoI53gJ3yXSrCsQTSP79wfa3N7718jpt245eA0tKFOZEycR7j2VzaarxuTUkqap3Xa4dLHAgMBAAECggEAYcD1r vKTFwCT5MwglYgp4iK2XmZLJ60bT9yxQOYF/GjkHH6ivzdYhGIz2k02LZlOGEAlR4T6Azs/A68LxntFmVXDYPL7I0Ze1mJ4g7jd7GImssT80CLz8LKBf7h5FvVGIoRSTwqEEQs2mNWhv8PEh37kk7S8ItJfP3mT 36Opg3zPy7K6 awaB6yjzF FNyc0pqbNk9tzCjzp9tIGuZlpKDMmabjl3aLew7e LxqRwSvlc5OnHJ3MkXU814Nm4zEUiVtNNjzvPpPZLERGHUEQH8ND4NT/uPpVQJEnATz2FFa aFz1sdXibhD8kmW8q956b2WePLlb9qRwCuvzHJOQKBgQDsC9Bccc6nKwXcEY08tmGdWS57m74r6pJ6pIm2oge SLIBMeq3w/xhUkhzshdSpXJ7BgLKiLwRKxKnsUbqkJYn6I/Vn/zwZH8fka78/OWigKMq5C0iecvg9aClaEvBb0iopNxIiX eAGMlJLQ7N9Wj8ZKu0k3ffkdP4 ehFCAXrQKBgQDRC7ITRcsKgWtvHlz1Lfw9PpAcP9gYEIbphXfdINGCGGXcQnM4fIYM92jGKP5qv9oHNklBY07vwape chOVQis5QGgYaGIe/ekaIJ/y4b 8r7DIwUE/SD7Y7/Dhj5 s9/VmH6EeOEsY7dSmKkI38YAJHih3xW/t6M41kpXwxAywwKBgQDGsj8nwklBoM6i7EdmxuOur0aYmIZhs2iwQlcGXKiF/e2RYfKB1EFbrwb8FPrbABg5BNtOoAEntolSjcDzbNhpKbQCEFW8CeyUp26U2VF4FC7FySNRNRNw/3LGKeAzKTkRdQ1VJiE94HeU6aupeZumEJD4BmG08ziWQHNXvXgyVQKBgQC/5p6odo93q2r2bMclA/vkNQSSCkHThYhz4uQwCKqLZN5NHmsrVZSxXoW M2 qi0gZCsqgzgtuqTg/S8mHryPxo6CknDtvUW36bT4vFqVscWaROBqpg729SMqHMTs5kOJP8FdkQJtk5n0pw56Y2OOoydI7ttD WBPsXzuL6TN7hQKBgQC6DwuS5hyVYAEj/2k/SOtOVgo8oq0OSlJxnzYkogkVoi4og3uQi/6n/cP7GUJWCx4e89aX1taWQ BmVgxLdTb9oIThPyt2wKCDKMOjGB3XAhd95L11fMt54Flc/PnwYjrKTUEwcTbQ18zL0Om0Wl NkejrMe9U7bvCEzlmdQySZQ==
-----END RSA PRIVATE KEY-----
Java 测试程序源码(部分):
...
private static final String algorithm = "RSA";
private static String signatureAlgorithm = "SHA256withRSA";
static {
Security.addProvider(
new org.bouncycastle.jce.provider.BouncyCastleProvider()
);
}
private static byte[] pemFile2Bytes(File pemFile) {
Pemreader reader = null;
try {
if (!pemFile.isFile() || !pemFile.exists()) {
throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
}
reader = new PemReader(new FileReader(pemFile));
PemObject pemObject = reader.readPemObject();
byte[] content = pemObject.getContent();
return content;
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static PublicKey bytes2PublicKey(byte[] keyBytes) {
try {
PublicKey publicKey = null;
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
publicKey = kf.generatePublic(keySpec);
return publicKey;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Could not reconstruct the public key, the given algorithm could not be found. " e.getMessage());
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Could not reconstruct the public key. " e.getMessage());
}
}
private static PrivateKey bytes2PrivateKey(byte[] keyBytes) {
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
return kf.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Could not reconstruct the private key, the given algorithm could not be found. " e.getMessage());
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Could not reconstruct the private key. " e.getMessage());
}
}
public static PublicKey pemFile2PublicKey(String filepath) {
byte[] bytes = pemFile2Bytes(new File(filepath));
return bytes2PublicKey(bytes);
}
public static PrivateKey pemFile2PrivateKey(String filepath) {
byte[] bytes = pemFile2Bytes(new File(filepath));
return bytes2PrivateKey(bytes);
}
public static byte[] sign(byte[] data, PrivateKey privateKey) {
try {
Signature privateSignature = Signature.getInstance(signatureAlgorithm);
privateSignature.initSign(privateKey);
privateSignature.update(data);
return privateSignature.sign();
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new RuntimeException("an error occurred while signing a text");
}
}
public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) {
try {
Signature publicSignature = Signature.getInstance(signatureAlgorithm);
publicSignature.initVerify(publicKey);
publicSignature.update(data);
return publicSignature.verify(signature);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e.getMessage());
} catch (InvalidKeyException | SignatureException e) {
return false;
}
}
public static byte[] encrypt(byte[] data, PrivateKey privateKey) {
try {
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, privateKey);
return encryptCipher.doFinal(data);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException(e.getMessage());
}
}
public static void main(String[] args) throws Exception {
final PublicKey publicKey = pemFile2PublicKey("/tmp/rsa_pub.pem");
final PrivateKey privateKey = pemFile2PrivateKey("/tmp/rsa_pri.pem");
byte[] data = "I Love Beijing".getBytes(UTF_8);
byte[] signature1 = sign(data, privateKey);
System.out.println("signature1: " Hex.encodeHexString(signature1));
System.out.println("is signature1 valid? " verify(data, signature1, publicKey));
System.out.println("--------------");
final byte[] sha = Hashing.sha256().hashBytes(data).asBytes();
System.out.println("sha (hex): " Hex.encodeHexString(sha));
final byte[] signature2 = encrypt(sha, privateKey);
System.out.println("signature2: " Hex.encodeHexString(signature2));
System.out.println("is signature2 valid? " verify(data, signature2, publicKey));
System.out.println("are signature1 and signature2 equal? " Arrays.equals(signature1, signature2));
}
...
完整代码请参见 https://gist.github.com/rocketk/4958d33b54e26a3ce068e05cc5b48608
运行的结果:
signature1: 194027d696054bf673c409493e3098b515a77f49e6f20d8eea35ed1f3ed4cf2409fa1e185eb5499c9394dacd0bb4422f89d620d67d66c081a759a16a43cda1d269db34ec8877c1d614ac1bd7dc15491e1f50dca9bca370225feeb5bb02fc9d6fc697b5b4f24b48421505a526f99c5d29909f89d75967055dfe2fa57d3330d5b723c827c67abc224b739a03cf1bfeac28c28541ab9dd63e799372295eae143939777c7017c6e46678f39dc9d675945667ec42d8bf91a4f0274de3a302af65fc6720182b63b615cf9cff57af436c5d54ec072e1debd0ac43308ca02b80ff757c7f589c0fad806aeb982663c23efd5f08dbc03324301b30942739a76e336c4a254e
is signature1 valid? true
--------------
sha (hex): ffc25c9e7dab979f4ee431d4c68881285378ad523c5e00db3db9e7d62b5f5560
signature2: 1e612e2cdfbb4c5338c8cead1490b7694cb975361c734feb642cfc367e49e2ae54d6802860a30f1222d1be7b2a1c01ea45a3f6c11178908d471f638db29e68b8ff5ce1aa47bf81e9de820bb22a09ecc37feee8c61dea8e9ec4025ce1b871cb8666e126d1d605eaf4b2abbf26c4841f4c2599865a396130e6d9bff5c1094fc0780a68a54c1937fd5a26a9beb2ba76e06087c63ff177df96106a64e5e3495c65917249121cb72fb6d41cdee9a1965266a7ff76f2e05f9c9f64b3ac108d879d1127884fc3b551d223b1d9ad110e74acb0dd53f433ec3a4bdb9fa345890e0c0ee8649a4fa776a0f76a0189878d3a4fc710615e6e0c4f4398591abc317f111448064b
is signature2 valid? false
are signature1 and signature2 equal? false
其中 signature1 表示的是使用 JDK 工具生成的签名(实际上是 bouncycastle 提供的),而 signature2 则是先通过计算哈希再使用 RSA 私钥加密得到的,两个签名并不一致,并且 signature2 在验签的时候是无法验通过的,这是为什么呢?我们一直理解的签名过程中哪里出了问题?
其实在写这段代码验证这个逻辑之前我也以为签名就等同于哈希 加密,但当我运行完上面的程序后我发现我的理解是有错误的,于是我在 Stack Overflow 上去寻找答案,最终把他搞明白了。
原来在计算哈希和加密这两个步骤之间,还要有一个所谓 padding 的过程,即真正的步骤应该是这样:
- 使用 SHA256 算法对 data 计算出它的哈希值
- 在哈希值前面加上一个固定前缀,用于表示它所使用的哈希算法, SHA256 对应的固定前缀是 '3031300D060960864801650304020105000420' (16进制表示)
- 再使用 RSA256 算法对已经加上前缀的哈希值进行加密(私钥加密)
那么我们对测试程序稍加调整来验证以上所说的是否正确:
...
public static void main(String[] args) throws Exception {
final PublicKey publicKey = pemFile2PublicKey("/tmp/rsa_pub.pem");
final PrivateKey privateKey = pemFile2PrivateKey("/tmp/rsa_pri.pem");
byte[] data = "I Love Beijing".getBytes(UTF_8);
byte[] signature1 = sign(data, privateKey);
System.out.println("signature1: " Hex.encodeHexString(signature1));
System.out.println("is signature1 valid? " verify(data, signature1, publicKey));
System.out.println("--------------");
final byte[] sha = Hashing.sha256().hashBytes(data).asBytes();
System.out.println("sha (hex): " Hex.encodeHexString(sha));
final byte[] signature2 = encrypt(sha, privateKey);
System.out.println("signature2: " Hex.encodeHexString(signature2));
System.out.println("is signature2 valid? " verify(data, signature2, publicKey));
System.out.println("are signature1 and signature2 equal? " Arrays.equals(signature1, signature2));
// 以下为新增部分
System.out.println("--------------");
final byte[] paddingSha = Hex.decodeHex("3031300D060960864801650304020105000420" Hex.encodeHexString(sha));
final byte[] signature3 = encrypt(paddingSha, privateKey);
System.out.println("signature3: " Hex.encodeHexString(signature3));
System.out.println("is signature3 valid? " verify(data, signature3, publicKey));
System.out.println("are signature1 and signature3 equal? " Arrays.equals(signature1, signature3));
}
...
signature3 表示按新的方法生成的签名。
运行结果:
signature1: 194027d696054bf673c409493e3098b515a77f49e6f20d8eea35ed1f3ed4cf2409fa1e185eb5499c9394dacd0bb4422f89d620d67d66c081a759a16a43cda1d269db34ec8877c1d614ac1bd7dc15491e1f50dca9bca370225feeb5bb02fc9d6fc697b5b4f24b48421505a526f99c5d29909f89d75967055dfe2fa57d3330d5b723c827c67abc224b739a03cf1bfeac28c28541ab9dd63e799372295eae143939777c7017c6e46678f39dc9d675945667ec42d8bf91a4f0274de3a302af65fc6720182b63b615cf9cff57af436c5d54ec072e1debd0ac43308ca02b80ff757c7f589c0fad806aeb982663c23efd5f08dbc03324301b30942739a76e336c4a254e
is signature1 valid? true
--------------
sha (hex): ffc25c9e7dab979f4ee431d4c68881285378ad523c5e00db3db9e7d62b5f5560
signature2: 1e612e2cdfbb4c5338c8cead1490b7694cb975361c734feb642cfc367e49e2ae54d6802860a30f1222d1be7b2a1c01ea45a3f6c11178908d471f638db29e68b8ff5ce1aa47bf81e9de820bb22a09ecc37feee8c61dea8e9ec4025ce1b871cb8666e126d1d605eaf4b2abbf26c4841f4c2599865a396130e6d9bff5c1094fc0780a68a54c1937fd5a26a9beb2ba76e06087c63ff177df96106a64e5e3495c65917249121cb72fb6d41cdee9a1965266a7ff76f2e05f9c9f64b3ac108d879d1127884fc3b551d223b1d9ad110e74acb0dd53f433ec3a4bdb9fa345890e0c0ee8649a4fa776a0f76a0189878d3a4fc710615e6e0c4f4398591abc317f111448064b
is signature2 valid? false
are signature1 and signature2 equal? false
--------------
signature3: 194027d696054bf673c409493e3098b515a77f49e6f20d8eea35ed1f3ed4cf2409fa1e185eb5499c9394dacd0bb4422f89d620d67d66c081a759a16a43cda1d269db34ec8877c1d614ac1bd7dc15491e1f50dca9bca370225feeb5bb02fc9d6fc697b5b4f24b48421505a526f99c5d29909f89d75967055dfe2fa57d3330d5b723c827c67abc224b739a03cf1bfeac28c28541ab9dd63e799372295eae143939777c7017c6e46678f39dc9d675945667ec42d8bf91a4f0274de3a302af65fc6720182b63b615cf9cff57af436c5d54ec072e1debd0ac43308ca02b80ff757c7f589c0fad806aeb982663c23efd5f08dbc03324301b30942739a76e336c4a254e
is signature3 valid? true
are signature1 and signature3 equal? true
我们可以看到此时 signature3 已经可以被成功验签了,并且它和 signature1 (也就是使用 JDK 生成的)是完全一致的。
那么你可以要问了,除了 SHA256 之外,还有其他的哈希算法呀,它们分别对应哪些固定前缀呢?这里列出来一些常见的算法与它们的固定前缀:
For the six hash functions mentioned in Appendix B.1, the DER
encoding T of the DigestInfo value is equal to the following:
MD2: (0x)30 20 30 0c 06 08 2a 86 48 86 f7 0d 02 02 05 00 04
10 || H.
MD5: (0x)30 20 30 0c 06 08 2a 86 48 86 f7 0d 02 05 05 00 04
10 || H.
SHA-1: (0x)30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 || H.
SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00
04 20 || H.
SHA-384: (0x)30 41 30 0d 06 09 60 86 48 01 65 03 04 02 02 05 00
04 30 || H.
SHA-512: (0x)30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00
04 40 || H.