1. 程式人生 > >Java加密解密之數字簽名

Java加密解密之數字簽名

上一篇帖子,我們講了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簽名的結果是一致的

上面這是把簽名以十六進位制文字輸出,下面來同時進行簽名和驗證

#把簽名結果輸出到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

輸出結果:


驗籤通過

上面的,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