上一篇帖子,我们讲了MAC(消息认证码),它可以验证身份和防篡改。
它的机制是通过通信双方都持有相同的秘钥去实现,秘钥相同摘要才相同,没有秘钥就不能生成正确的摘要信息。


但是,它有个缺点,就是 通信双方必须持有相同的秘钥,解决方法就是使用数字签名


数字签名(又称公钥数字签名、电子签章)是一种类似写在纸上的普通的物理签名,但是使用了非对称加密领域的技术实现,用于鉴别数字信息的方法。
一套数字签名通常定义两种互补的运算, 私钥用于签名,公钥用于验证(验签)

数字签名是非对称密钥加密技术数字摘要技术的应用。


既然是非对称加密,就需要有一对秘钥,公钥和私钥

下面演示一下,用OpenSSL生成一对秘钥

#生成RSA私钥,默认是编码方式为PEM的PKCS#1格式
#PKCS#1格式是传统的私钥格式
openssl genrsa -out key.pem 1024

#从私钥中生成公钥,给OpenSSL验签用的
openssl rsa -in key.pem -out pub.pem -pubout

#把PEM编码格式的私钥转换成DER编码的私钥,同时进行PKCS#1转换成PKCS#8(Java默认只能处理PKCS#8的格式)
#-nocrypt 意思是不加密
#给Java用
openssl pkcs8 -topk8 -in key.pem -out pkcs8_prikey.der -inform PEM -outform DER -nocrypt

#从私钥中导出DER编码的公钥
#给Java用
openssl rsa -in key.pem -pubout -outform DER -out pubkey.der



这样,就会生成四个文件,其中pkcs8_prikey.der、pubkey.der是给Java用的

有了秘钥对之后,就可以对文件进行签名了

下面使用Java(1.8.0_144)演示计算apache-tomcat-8.5.23.zip文件的数字签名

package com.security.sign;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.bouncycastle.util.encoders.Hex;

public class SignatureTest {

	public static KeyPair getKeyPair() throws Exception {
		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
		
		byte[] publicKeyData = Files.readAllBytes(Paths.get("c:/tmp/pubkey.der"));
		byte[] privateKeyData = Files.readAllBytes(Paths.get("c:/tmp/pkcs8_prikey.der"));
		
		X509EncodedKeySpec publicKeySpec= new X509EncodedKeySpec(publicKeyData);
		PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyData);
		
		PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
		PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
		
		return new KeyPair(publicKey, privateKey);
	}
	
	/**
	 * 用私钥生成签名
	 * 
	 * Signature.getInstance(algorithm) 算法格式为 <digest>with<encryption>
	 * 支持的算法有:MD5withRSA、SHA256withRSA、SHA256withDSA等等
	 * 
	 * 全部支持的算法见官方文档:
	 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Signature
	 */
	public static byte[] sign(String signatureAlgorithm, PrivateKey privateKey, byte[] data) throws Exception {
		Signature sign = Signature.getInstance(signatureAlgorithm);
		sign.initSign(privateKey);
		sign.update(data);
		byte[] result = sign.sign();
		return result;
	}
	
	/**
	 * 用公钥验签
	 */
	public static boolean verify(String signatureAlgorithm, PublicKey publicKey, byte[] data, byte[] signature) throws Exception {
		Signature sign = Signature.getInstance(signatureAlgorithm);
		sign.initVerify(publicKey);
		sign.update(data);
		return sign.verify(signature);
	}
	
	public static void main(String[] args) throws Exception {
		KeyPair keyPair = getKeyPair();
		
		PrivateKey privateKey = keyPair.getPrivate();
		PublicKey publicKey = keyPair.getPublic();
		
		String signatureAlgorithm = "SHA256withRSA";
		
		//需要签名的数据
		byte[] data = Files.readAllBytes(Paths.get("c:/tmp/apache-tomcat-8.5.23.zip"));
		
		//数据+私钥签名
		byte[] signatureData = sign(signatureAlgorithm, privateKey, data);
		
		//把签名转换成十六进制的文本
		System.out.println(Hex.toHexString(signatureData));
		
		//数据+公钥+签名结果进行验证
		boolean result = verify(signatureAlgorithm, publicKey, data, signatureData);
		System.out.println(result);
	}
}



执行之后,输出结果为:

a4a68c93f811192fe96f5746c486fa37db6746a6f71b482d7c6a371078b99a567220b3eaf5a984fe
7626dd35eb806adf4cbf63b6e081631172babe8f1785d6f56ddeb9ce5c809f921ac10332cb02c8be
2de304ac20d5ef1c0d9cf7a0874615d27defff751a1fd8dc13849aeeb4ddd0f1ba5d7766e96e9be64
7294ff4a3224033
 true

可以看到,签名很长,输出true表示验证通过。


同样的,我们来使用OpenSSL来进行对apache-tomcat-8.5.23.zip进行数字签名

签名的命令:

openssl dgst -sign key.pem -sha256 -hex /tmp/apache-tomcat-8.5.23.zip

结果:

java 文本文件数字签字 java实现数字签名_java


可以看到,和Java签名的结果是一致的

上面这是把签名以十六进制文本输出,下面来同时进行签名和验证

#把签名结果输出到sign.sig
openssl dgst -sign key.pem -sha256 -out sign.sig /tmp/apache-tomcat-8.5.23.zip

#验签
openssl dgst -verify pub.pem -sha256 -signature sign.sig /tmp/apache-tomcat-8.5.23.zip



输出结果:

java 文本文件数字签字 java实现数字签名_数字签名_02

验签通过


上面的,Signature.getInstance(algorithm)  参数algorithm可以支持的值除了参考官方文档,还可以通过如下代码得出

Security.getAlgorithms("Signature").forEach(System.out::println);



在Java8中,输出结果如下:


NONEWITHDSA
SHA384WITHECDSA
SHA224WITHDSA
SHA256WITHRSA
MD5WITHRSA
SHA1WITHRSA
SHA512WITHRSA
MD2WITHRSA
SHA256WITHDSA
SHA1WITHECDSA
MD5ANDSHA1WITHRSA
SHA224WITHRSA
NONEWITHECDSA
NONEWITHRSA
SHA256WITHECDSA
SHA224WITHECDSA
SHA384WITHRSA
SHA512WITHECDSA
SHA1WITHDSA