本篇的主要目的在於實現pdf的數字簽名問題,只是作為我學習知識的總結。
1、數字簽名算法的概述
本部分主要參考於:https://blog.csdn.net/lovelichao12/article/details/75007189
數字簽名:私鑰用於簽名,公鑰用於驗證。
數字簽名的作用:
驗證數據的完整性,認證數據來源,抗否認。
數字簽名實現的具體原理:
1、 將報文按雙方約定的HASH算法計算得到一個固定位數的報文摘要。在數學上保證,只要改動報文中任何一位,重新計算出的報文摘要值就會與原先的值不相符。這樣就保證了報文的不可更改性。(詳見參考資料的"公鑰密碼技術原理"章節)
2、 將該報文摘要值用發送者的私人密鑰加密,然后連同原報文和數字證書(包含公鑰)一起發送給接收者而產生的報文即稱數字簽名。
3、接收方收到數字簽名后,用同樣的HASH算法對報文計算摘要值,然后與用發送者的公開密鑰進行解密解開的報文摘要值相比較,如相等則說明報文確實來自所稱的發送者。
4、同時通過證書頒發機構CA確認證書的有效性即可確認發送的真實身份。
常用的數字簽名有:RSA、DSA、ECDSA
2、RSA算法概述
RSA是目前為止應用最為廣泛的非對稱加密算法。非對稱加密算法簡單的說就是分成公鑰和私鑰。加密和解密采用不同的算法實現,這樣的好處是不需要像傳統對稱加密算法一樣將相同算法的密鑰分發給對方,從而減少密鑰被獲取所帶來的嚴重危害,目前基本上都是采用非對稱算法,而RSA是最為廣泛的。理論上1024位以上的RSA是無法破解的(或者未公開)。
基本原理:
非對稱算法將密碼將密碼分為公鑰和私鑰,公鑰發送給用戶(可以是多個),用戶用公鑰加密想要發送的數據,然后發送給服務器,服務器通過私鑰解密加密后的數據。
基本步驟:
生成公鑰和私鑰步驟:
- 隨機選擇兩個不相等的質數p和q
- 計算p和q的乘積n (n的長度就是密鑰長度。3233寫成二進制是110010100001,一共有12位,所以這個密鑰就是12位。實際應用中,RSA密鑰一般是1024位,重要場合則為2048位。)
- 計算n的歐拉數ϕ(n)=(p-1)(q-1)(歐拉數即為小於等於n的所有質數
- 隨機選擇一個整數e,條件時1<e<ϕ(n)1<e<ϕ(n),且e與ϕ(n)ϕ(n)互質。
- 計算e對於的模反元素d(模反元素,就是指有個整數可以使得e×d/ϕ(n)e×d/ϕ(n)的余數是1。 ed - 1 = kφ(n) )
- (n,e)為公鑰,(n,d)為私鑰。
加密過程:
- 求需要加密數據m的e次方,然后用結果對n求膜,結果為c。
解密過程:
- 對需要解密的數據c求d次方,然后用結果對n求膜,結果為m。
RSA算法的限制:
首先一般的RSA算法膜長位1024位,也就是128個字節。加密的信息必須小於這個值。而且有時候會需要一些額外的信息比如java的實現還需要11個字節的額外信息,隱藏加密數據不能超過過117個字節。
解決方案有兩種一種是對信息分段加密,另一種是先選擇一種”對稱性加密算法”(比如DES),用這種算法的密鑰加密信息,再用RSA公鑰加密DES密鑰。
3、數字簽名和數字證書
如果說RSA解決的是信息加密傳輸的問題,那數字簽名解決的就是用戶驗證回送數據是否來自於服務器的問題,數字證書解決的是用戶驗證當前通信的服務器是否是用戶期望的服務器的問題。
服務器接收到用戶加密信息后需要給用戶回信,為了使得用戶確認所發送的信息是來自服務器的,需要采用數字簽名。
步驟:
- 服務器先使用散列函數生成摘要。
- 服務器然后使用摘要和其他驗證信息,使用私鑰加密生成數字簽名。
- 服務器將數字簽名附在回送數據上一起發回給用戶
- 用戶使用公鑰解密數字簽名得到摘要和,得以確定回送數據來自服務器。
- 用戶對回送數據使用相同的散列函數與摘要對比,如果相同則回送數據未被修改過。
數字證書
更為復雜的情況:由於公鑰是很容易獲取的,如果一位用戶a將另一位用戶b的服務器公鑰給修改成為用戶a自己生成的公鑰則用戶a可以冒充服務器域用戶b通信。
用戶b如何驗證當前通信的服務器是否是正確的服務器?
用戶b需要去CA(證書中心 certificate authority),為公鑰做認證。
CA用自己的私鑰對服務器的公鑰和相關驗證信息進行加密,生成數字證書(Digital Certificate)
這樣服務器拿到數字證書,再給用戶回信的時候附上這個數字證書,用戶收到這個數字證書然后用的CA的公鑰解密,就可以拿到服務器的公鑰了,然后就能證明數字簽名是否是服務器的。
BASE64算法:
BASE64是一種將二進制數轉換成ACSii碼的可打印字符的表示方式。一共需要表示64個可打印字符,大小寫字母52個加上10個數字0-9加上+號和斜杠/一共64字符,另外需要一個等號=作為后綴。
BASE64將3個字符換成4個字符。3*8=4*6;
轉換后數據大小會變大三分之一左右
為何需要進行BASE64的轉換?
BASE64轉換主要是為了避免敏感字符。比如有一些%或者回車等等在網絡協議中有特殊的含義,為了避免解析錯誤通常發送數據都會進行BASE64的轉換。
其次,有些數據時不看可以見的數據,有時候需要復制粘貼,使用BASE64編碼可以將不可見的數據轉成可見的數據,更容易復制粘貼或其他操作。
另外,進行轉換也可以避免數據直接暴露,算是一種非正式的加密算法。
RSA、數字簽名和base64的實現
主要參考:Java使用RSA加密解密簽名及校驗
RSA的java實現
密鑰的獲取
首先按照java面向對象的慣例,任何東西都有對象,也就是說公鑰和私鑰都是需要用類(或者接口)表示的。公鑰和私鑰的類型很多。java提供了一個接口RSAPrivateKey和RSAPublicKey。用於分別引用RSA類型的公鑰和私鑰接口。
通常來說密鑰都有兩個接口,一個接口說明時公鑰還是私鑰如PrivateKey和PublicKey,另一個接口說明了的是什么算法如RSA或者DSA等等。類似多重繼承。
其次再來看獲得密鑰的方式有兩類,一類是自己生成,一類是從網絡中得到的字符串轉換。
自己生成密鑰:
自己生成密鑰使用的是KeyPairGenerator這是一個java自帶的密鑰生成類,生成步驟分為三步:
- 通過
getInstance(算法名)
的方式獲得特點算法的生成類, - 然后用
initialize
初始化參數,對於RSA來說通常就是指定一個位數1024,然后指定一個隨機數生成器如new SecureRandom。 -
最后用
generateKeyPair()
生成KeyPair密鑰對,再對密鑰對KeyPair使用get方法獲得公鑰和私鑰。從網絡中獲取:
通常來說從網絡中獲取的都是字符串或者時byte[]格式,注意這里通常會使用base編碼,使用時通常需要轉換。
這樣的數據都有固定的數據格式典型的如ASN.1格式。X509EncodedKeySpec是RSA的公鑰ASN.1格式,PKCS8EncodedKeySpec是RSA的私鑰的ASN.1的格式。這樣對不同的密鑰不同的格式使用對於的類進行轉換就可以得到PrivateKey和PublicKey的格式。
這里還需要用到一個KeyFactory類用於得到轉換器。
轉換的步驟:
- 首先通過
KeyFactory.getInstance("RSA");
獲得一個RSA的工廠類。 - 通過X509EncodedKeySpec或者PKCS8EncodedKeySpec的帶參構造器(參數為byte[]的密鑰數據)獲得一個指定表示特定格式的類。
- 最后通過
keyFactory.generatePublic(keySpec)
和(RSAPrivateCrtKey) keyFactory.generatePrivate(keySpec)
來獲得,記得類型轉換
密鑰的加密和解密
對於公鑰和私鑰,無論時那種類型的密鑰,無論是進行加密還是進行解密,過程是一致的。而Cipher是完成這個任務的核心類。
基本過程:
- 首先通過
Cipher.getInstance("RSA")
獲得一個Cipher。 - 初始化:
cipher.init(Cipher.ENCRYPT_MODE, privatekey);
只有兩個參數,第一個設定模式,加密為ENCRYPT_MODE,解密為DECRYPT_MODE,第二個為公鑰或者私鑰。 - 最后通過
dofine
實現流程,輸出為byte[];
數字簽名的java實現
數字簽名的過程和上述第二小節加密解密的過程時類似的,甚至更簡化,因為數字簽名只用私鑰簽名,公鑰證人。而signature是完成這個任務的核心類:
基本過程:
- 獲得key,流程如上述:
Signature.getInstance("SHA256withRSA");
使用getInstance獲得Signature類。並指定簽名的算法。常用的算法java reference中有指定。- 使用signatur實例的
initSign(PriKey)
進行簽名初始化,或者initVerify(pubkey)
進行認證的初始化。 - 使用signatur實例的
update(bytes)
輸入需要進行簽名的數據。 - 使用signatur實例的
sign()
或者verify()
實現簽名或者認證。注意前者返回byte[]后者返回boolean。
base64
目前來說有三類方法
- Apache Commons Codec
- sun.misc
- java8之后自帶的BASE64.Decoder和Encoder。
這里之介紹最后一個,其他版本的應用網上也很多教程。其實都很簡單。
基本步驟:
- 首先你要生成一編碼和解碼器通過,
Encoder encoder = Base64.getEncoder();
和Decoder decoder = Base64.getDecoder();
- 通過
encoder.encodeToString(bytes);
編碼通過ecoder.decode(string)
解碼