1. 概述
我們考慮幾個現(xiàn)實中的業(yè)務(wù)場景:
案例一:
當更新Android手機上的微信APP,系統(tǒng)怎么判斷新的安裝包就是騰訊公司發(fā)布的安裝包?系統(tǒng)怎么判斷即使是騰訊發(fā)布的安裝包,但是安裝包卻沒有被修改?這顯然是非常重要的事情,如果安裝包被修改過,那么用戶口令、數(shù)據(jù)、銀行卡等信息都可能會被竊取。
案例二:
現(xiàn)在很多企業(yè)內(nèi)部都是通過郵件、IM來溝通,甚至下發(fā)財務(wù)、采購等指令,相關(guān)人員如何鑒別,郵件、電子合同(文檔)就是老板本人發(fā)出來的呢?而不是假冒的。
如果存在一種機制,發(fā)送者對數(shù)據(jù)(安裝包、文檔等)進行一個“簽名”或者“蓋章”,而接收者根據(jù)這個簽名或者蓋章進行驗證,從而判斷數(shù)據(jù)是否正確的發(fā)送者發(fā)送的,以及數(shù)據(jù)是否被篡改,那么這些問題就迎刃而解了。并且,這種驗證機制是公告開的。
這種機制是存在的,密碼學(xué)上叫:數(shù)字簽名。數(shù)字簽名的實現(xiàn),通常使用公鑰算法(又稱非對稱加密算法),該算法的特點就是秘鑰有一對:公鑰和私鑰,公鑰是公開的,可以廣播出去告訴大家,私鑰是保密的,只能是自己知道,保密。因此,在實現(xiàn)數(shù)字簽名,流程是使用私鑰對數(shù)據(jù)進行簽名,輸出一段特定長度的數(shù)字簽名(指紋),驗證著使用對應(yīng)的公鑰、原始數(shù)據(jù)、數(shù)字簽名進行運算,從而校驗數(shù)據(jù)是否被篡改或者發(fā)行者身份的合法性
2. 簽名算法、非對稱加密、ECC與secp256k1
簽名算法有比較多的選擇,例如:RSA、DSA、ECC(ECDSA)等。前兩者因為秘鑰長度和性能的關(guān)系,現(xiàn)在使用越來越少,例如常見的RSA2048,秘鑰長度就達到了2048bit,也就是2KB大小,在一些嵌入式場合消耗比較大,而ECC只需要224bit,因此比特幣在保證數(shù)據(jù)安全性基礎(chǔ)的算法選擇上選擇了ECC。
ECC也就是橢圓曲線密碼學(xué),原理上不多說了,現(xiàn)在很多應(yīng)用場合選擇了它,例如區(qū)塊鏈,足以看出它的火熱程度。
在使用ECC進行數(shù)字簽名的時候,需要構(gòu)造一條曲線,也可以選擇標準曲線,諸如:prime256v1、secp256r1、nistp256、secp256k1等等。我們需要使用的是secp256k1,也就是比特幣選擇的加密曲線。
3. 秘鑰的產(chǎn)生和載入
公鑰算法的秘鑰,通常不可能和我們認知的口令對等,例如:secp256k1,秘鑰長度就達到了256bit,也就是32字節(jié),記憶在腦海里,顯然是不現(xiàn)實的。通常,我們通過程序來生成秘鑰,存儲到磁盤、安全設(shè)備上,然后再通過程序載入使用。
3.1 秘鑰生成
在Java中,生成ECC秘鑰很簡單,只需要使用:KeyPairGenerator
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
// curveName這里取值:secp256k1
ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(curveName);
keyPairGenerator.initialize(ecGenParameterSpec, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 獲取公鑰
keyPari.getPublic();
// 獲取私鑰
keyPair.getPrivate();
KeyPairGenerator可以設(shè)置一些算法參數(shù),因為我們需要指定標準曲線,因此使用:ECGenParameterSpec("secp256k1")來指定曲線。
這里顯然有個問題存在,在業(yè)務(wù)的生命周期當中,秘鑰始終是同一個,而上述代碼,每運行一次,就重新產(chǎn)生一個,顯然是不現(xiàn)實的,在實際業(yè)務(wù)中的做法就是:第一次產(chǎn)生一個(或者使用諸如OpenSSL一類的工具,生成一個),然后存儲到磁盤上或者特殊的存儲介質(zhì)上,然后在程序中加載。
3.2 秘鑰的存儲
Java中要序列化秘鑰,也是相當簡單的,只要調(diào)用:getEncoded(),它返回特定格式的byte[]數(shù)據(jù),該格式屬于標準格式,可以在大部分程序/軟件中通用。
PrivateKey.getEncoded() 返回 PKCS #8 格式并且以DER編碼輸出;對于 PublicEncode.getEncoded()返回 X.509 格式并且以DER編碼輸出的byte[],這個時候,可以直接存儲到磁盤上了。
測試代碼:
KeyPair keyPair = KeyUtil.createKeyPairGenerator("secp256k1");
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
KeyUtil.savePublicKey(publicKey, "publickey.der");
KeyUtil.savePrivateKey(privateKey, "privatekey.der");
為了驗證一下,我們使用:OpenSSL命令來驗證一下:
打印公鑰:
$ openssl pkey -inform DER -pubin -in publickey.der -text
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEDNeUU82FtdEOUjDjiX9PqRTi2HD2Dq7x
TrnTVY3Q52j+FtSJtBLp6RmEJ0dCmxd3y1igSMCx9nOrAO0vqEdBTA==
-----END PUBLIC KEY-----
Public-Key: (256 bit)
pub:
04:0c:d7:94:53:cd:85:b5:d1:0e:52:30:e3:89:7f:
4f:a9:14:e2:d8:70:f6:0e:ae:f1:4e:b9:d3:55:8d:
d0:e7:68:fe:16:d4:89:b4:12:e9:e9:19:84:27:47:
42:9b:17:77:cb:58:a0:48:c0:b1:f6:73:ab:00:ed:
2f:a8:47:41:4c
ASN1 OID: secp256k1
打印私鑰:
$ openssl pkey -inform DER -in privatekey.der -text
-----BEGIN PRIVATE KEY-----
MD4CAQAwEAYHKoZIzj0CAQYFK4EEAAoEJzAlAgEBBCA9ONwt9uitCK04sqbs3MvH
3wj8B4ZIzhKDTzY2NqfDzQ==
-----END PRIVATE KEY-----
Private-Key: (256 bit)
priv:
3d:38:dc:2d:f6:e8:ad:08:ad:38:b2:a6:ec:dc:cb:
c7:df:08:fc:07:86:48:ce:12:83:4f:36:36:36:a7:
c3:cd
pub:
04:0c:d7:94:53:cd:85:b5:d1:0e:52:30:e3:89:7f:
4f:a9:14:e2:d8:70:f6:0e:ae:f1:4e:b9:d3:55:8d:
d0:e7:68:fe:16:d4:89:b4:12:e9:e9:19:84:27:47:
42:9b:17:77:cb:58:a0:48:c0:b1:f6:73:ab:00:ed:
2f:a8:47:41:4c
ASN1 OID: secp256k1
這說明,秘鑰可以被別的工具識別
3.3 PEM編碼的秘鑰
getEncoded()方法輸出的是DER編碼的二進制文件,在很多時候,我們可能為了便于交互,需要以文本編碼的方式輸出,這個時候PEM編碼可以滿足。PEM編碼結(jié)構(gòu)大致為BEGIN-END塊結(jié)構(gòu),中間內(nèi)容為Base64轉(zhuǎn)換后的的DER編碼內(nèi)容。
Java標準庫不支持PEM格式的讀寫,但可以使用 bouncycastle 來實現(xiàn)。不過,針對私鑰和公鑰,我們可以簡單的寫代碼實現(xiàn),這樣避免引入過多的依賴。簡單實現(xiàn)的話,只需要將:getEncoded() 輸出進行Base64編碼(64個字節(jié)添加換行符),然后首尾添加響應(yīng)的分割字符串。下面是實現(xiàn)代碼:
public static void savePublicKeyAsPEM(PublicKey publicKey, String name) throws Exception {
String content = Base64Util.encode(publicKey.getEncoded());
File file = new File(name);
if ( file.isFile() && file.exists() )
throw new IOException("file already exists");
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
randomAccessFile.write("-----BEGIN PUBLIC KEY-----\n".getBytes());
int i = 0;
for (; i<(content.length() - (content.length() % 64)); i+=64) {
randomAccessFile.write(content.substring(i, i + 64).getBytes());
randomAccessFile.write('\n');
}
randomAccessFile.write(content.substring(i, content.length()).getBytes());
randomAccessFile.write('\n');
randomAccessFile.write("-----END PUBLIC KEY-----".getBytes());
}
}
public static void savePrivateKeyAsPEM(PrivateKey privateKey, String name) throws Exception {
String content = Base64Util.encode(privateKey.getEncoded());
File file = new File(name);
if ( file.isFile() && file.exists() )
throw new IOException("file already exists");
try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {
randomAccessFile.write("-----BEGIN PRIVATE KEY-----\n".getBytes());
int i = 0;
for (; i<(content.length() - (content.length() % 64)); i+=64) {
randomAccessFile.write(content.substring(i, i + 64).getBytes());
randomAccessFile.write('\n');
}
randomAccessFile.write(content.substring(i, content.length()).getBytes());
randomAccessFile.write('\n');
randomAccessFile.write("-----END PRIVATE KEY-----".getBytes());
}
}
為了驗證生成的PEM的合法性,我們依然使用OpenSSL命令來驗證:
# 打印公鑰
$ openssl ec -in publickey.pem -pubin -text -noout
Private-Key: (256 bit)
pub:
04:47:e4:24:c3:97:fb:77:5d:5d:43:f0:04:c2:fe:
13:bf:f4:97:0e:b9:79:43:c9:bf:bb:72:1c:d1:ee:
bd:9a:27:42:aa:a0:d8:c0:00:9c:6f:d9:38:da:67:
0e:75:9a:8e:e4:e7:c4:67:86:f3:c0:19:64:22:47:
e9:53:f7:09:f4
ASN1 OID: secp256k1
read EC key
# 打印私鑰
$ openssl ec -in privatekey.pem -text -noout
Private-Key: (256 bit)
priv:
00:83:00:e5:1c:7b:a0:34:ee:67:3c:3e:07:a1:64:
de:cc:80:d3:59:4e:a1:14:bb:86:81:f3:2e:8a:b1:
51:de:d2
pub:
04:47:e4:24:c3:97:fb:77:5d:5d:43:f0:04:c2:fe:
13:bf:f4:97:0e:b9:79:43:c9:bf:bb:72:1c:d1:ee:
bd:9a:27:42:aa:a0:d8:c0:00:9c:6f:d9:38:da:67:
0e:75:9a:8e:e4:e7:c4:67:86:f3:c0:19:64:22:47:
e9:53:f7:09:f4
ASN1 OID: secp256k1
read EC key
3.3 秘鑰的加載
加載公鑰和私鑰,需要先從磁盤中讀取成byte[],然后使用:X509EncodedKeySpec 和 PKCS8EncodedKeySpec 轉(zhuǎn)換成公鑰和私鑰。
實例代碼:
// 讀取公鑰, encodedKey為從文件中讀取到的byte[]數(shù)組
public static PublicKey loadPublicKey(byte[] encodedKey, String algorithm)
throws NoSuchAlgorithmException, InvalidKeySpecException {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encodedKey);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return keyFactory.generatePublic(keySpec);
}
// 讀取私鑰
public static PrivateKey loadPrivateKey(byte[] encodedKey, String algorithm)
throws NoSuchAlgorithmException, InvalidKeySpecException{
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return keyFactory.generatePrivate(keySpec);
}
例如加載私鑰:
PrivateKey privateKey1 = KeyUtil.loadPrivateKey(IOUtils.readBytes(
new FileInputStream("privatekey.der")), "EC");
// readBytes代碼
public static byte[] readBytes(final InputStream inputStream) throws IOException {
final int BUFFER_SIZE = 1024;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int readCount;
byte[] data = new byte[BUFFER_SIZE];
while ((readCount = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, readCount);
}
buffer.flush();
return buffer.toByteArray();
}
上述兩個方法,只能處理DER編碼的秘鑰,如果是PEM,我們移除掉"BEGIN-END"以及換行符,然后進行Base64解碼后進行處理
public static PrivateKey loadECPrivateKey(String content, String algorithm) throws Exception {
String privateKeyPEM = content.replace("-----BEGIN PRIVATE KEY-----\n", "")
.replace("-----END PRIVATE KEY-----", "").replace("\n", "");
byte[] asBytes = Base64Util.decode(privateKeyPEM);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(asBytes);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return keyFactory.generatePrivate(spec);
}
public static PublicKey loadECPublicKey(String content, String algorithm) throws Exception {
String strPublicKey = content.replace("-----BEGIN PUBLIC KEY-----\n", "")
.replace("-----END PUBLIC KEY-----", "").replace("\n", "");
byte[] asBytes = Base64Util.decode(strPublicKey);
X509EncodedKeySpec spec = new X509EncodedKeySpec(asBytes);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return (ECPublicKey) keyFactory.generatePublic(spec);
}
4. 小結(jié)
在大部分系統(tǒng)業(yè)務(wù)系統(tǒng)里面,頻繁生成、加載秘鑰的業(yè)務(wù)是不多的,但是如果做一個開放性API體系,可能用的就比較多了(例如微信、支付寶一些業(yè)務(wù)接入就需要提供公鑰),而且秘鑰來源軟件比較多,這里可能需要深入了解:PKCS系列標準、X.509等。大家可以自行搜索相關(guān)內(nèi)容。