2  數位簽章:私鑰簽、公鑰驗

序章結尾我們把整個供應鏈問題壓縮成一句話:「我手上這份東西,是不是我信任的人做出來的那份?」 這一章介紹第一塊、也是最重要的一塊積木:數位簽章(digital signature)

先預測:你大概聽過「公鑰加密、私鑰解密」。那簽章呢——簽的時候用哪把鑰匙?驗的時候用哪把? 如果你的答案是「呃,應該也是公鑰加密那套吧」,這章就是為你寫的。

2.1 一句話版本

加密是「公鑰加密、私鑰解密」;簽章是反過來——用私鑰簽、用公鑰驗

目的 誰用哪把鎖 誰用哪把開 得到什麼
加密(保密) 收件人的 收件人的 機密性(只有他能讀)
簽章(證明身分) 我的 大家手上我的 真實性 + 完整性 + 不可否認

第一次看到這張表,多數人的反應是:「等等,公鑰竟然能『解』私鑰做的東西?」 這個困惑值得認真對待,因為它踩在一個被教窄的觀念上。

2.2 為什麼方向是這樣:金鑰對的「數學對稱性」

常見的教法是「公鑰=加密專用、私鑰=解密專用」。這是被教窄的。正確的圖像是:

一對金鑰是互為逆運算的兩把鑰匙。其中一把「鎖」的東西,只有另一把能開。 沒有哪一把天生是「加密鑰」——兩把都能拿來鎖。

差別只在哪一把是秘密的,而這決定你得到什麼性質:

  • 秘密在收件端 → 得到機密性(別人鎖給你,只有你能開)
  • 秘密在發送端 → 得到真實性(只有你能產生,全世界能驗)

方向不是誰規定的,是「你要什麼目的」逼出來的。要保密,秘密就得在收的那端; 要證明身分,秘密就得在發的那端。同一套數學,兩種擺法,兩種完全不同的安全性質。

警告用詞護欄

這裡說的「對稱」指「金鑰對互為逆運算」這個數學對稱。RSA 本身仍是 非對稱加密(asymmetric),別跟「對稱式加密(symmetric,例如 AES, 同一把鑰匙加解密)」搞混——那是完全不同的東西,第 4 章會再遇到它。

對 Java 工程師,最直白的鐵證是:JCA(Java Cryptography Architecture)根本不禁止你用私鑰「加密」——

Cipher c = Cipher.getInstance("RSA");
c.init(Cipher.ENCRYPT_MODE, privateKey);  // private key can ENCRYPT too!

如果「私鑰只能解密」是鐵律,這行程式碼不該存在。它存在,因為 RSA 的簽與驗本來就是 同一套指數運算、兩個指數對調。

注记誠實補丁

「簽章=用私鑰加密」是 RSA 專屬的教學簡化。像 ECDSA 那類簽章演算法根本沒有 「加密」的對應動作,它純粹是數學上證明「我握有私鑰」。用 RSA 這個圖像入門完全 OK, 但別硬套到所有簽章演算法上。

2.3 簽章保證的三件事(都不是「機密」)

  1. 真實性(authenticity):確實是「持有那把私鑰的人」發的——只有他的公鑰驗得開。
  2. 完整性(integrity):內容一個 byte 都沒被改——改了驗章就失敗。
  3. 不可否認(non-repudiation):他事後不能賴帳說「不是我簽的」——因為私鑰只有他有。
重要簽章不加密、不保密

被簽的文件仍是明文,任何人拿公鑰都能驗、也都能讀。簽章給的是「這是誰做的、有沒有被動過」, 不是「別人看不到」。要「又保密又能驗身分」,得加密+簽章一起用——而那裡有一個著名的順序陷阱, 留到第 4 章。

對照序章的問題:「我手上這份是不是我信任的人做的那份」——真實性回答「是不是他做的」, 完整性回答「是不是那份」。兩個問題,一個原語同時回答。這就是為什麼供應鏈安全的每個環節 最後都會走到簽章。

2.4 為什麼實務上簽的是 hash,不是原文

先預測:一份 500 MB 的安裝檔要簽章,你覺得私鑰運算的對象是這 500 MB 本身嗎?

實務上不是。私鑰簽的是文件的 hash(雜湊值)——先把整份文件壓成一個定長摘要 (SHA-256 是 32 bytes),再對這個摘要做私鑰運算。原因有二:

  1. 效能:非對稱運算很貴。對 500 MB 做 RSA 是災難;對 32 bytes 做 RSA 是瞬間。 hash 函數快得多,讓它去啃大檔案。
  2. 安全:定長摘要也避免直接對長訊息做數學運算時的某些截斷/結構攻擊。

演算法名字就寫著這件事——SHA256withRSA =「先 SHA-256、再用 RSA 簽」。 這也埋了一個伏筆:簽章的完整性保證,實際上是站在 hash 的抗碰撞性上的。 hash 若被攻破(想想 MD5 的下場),上面蓋的所有簽章跟著陪葬。原語是會疊的, 下層塌了上層一起塌——這個「疊」的結構,第 5 章會畫成完整的地圖。

2.5 Java 對照

把上面全部收進你熟悉的 API:

// Sign: private key + SHA-256 under the hood
Signature s = Signature.getInstance("SHA256withRSA");
s.initSign(privateKey);   // sign with the PRIVATE key
s.update(data);           // hashes internally with SHA-256
byte[] sig = s.sign();

// Verify: anyone holding the public key can do this
Signature v = Signature.getInstance("SHA256withRSA");
v.initVerify(publicKey);  // verify with the PUBLIC key
v.update(data);
boolean ok = v.verify(sig);

2.6 親手弄壞一次:改一個 byte

光讀不算數。附錄的 examples/tiny_sign_verify.shopenssl 做一件事: 產生金鑰對 → 簽一個檔案 → 驗證成功 → 把檔案改掉一個 byte → 再驗一次。

先預測:改了一個 byte 之後,驗章會「有機率失敗」還是「必定失敗」?錯誤訊息會告訴你 是哪個 byte 被改了嗎?

cd book/examples && ./tiny_sign_verify.sh

跑完你會看到:驗章必定失敗(hash 的雪崩效應——改一個 bit,整個摘要面目全非), 而且 openssl 只說 Failure,不說哪裡被改。簽章是「全有或全無」的完整性: 它能告訴你「這份不對」,不能告訴你「哪裡不對」。這個特性在供應鏈上剛好是對的—— 成品被動過一個 byte,就該整份丟掉,沒有「部分信任」這回事。


本章帶走的東西:金鑰對互為逆運算,秘密擺在哪端決定你得到機密性還是真實性; 簽章給真實性+完整性+不可否認,但不保密;實務簽 hash 不簽原文,所以簽章的地基是 hash 的抗碰撞性;驗章是全有或全無。

往下一章的橋:整章我們都假設「你手上有一把正確的公鑰」。但公鑰是公開傳遞的—— 如果攻擊者在半路把它換成自己的公鑰,你會拿著假鑰匙、把假簽章驗成真的, 而且流程上完全無感。簽章把「信任內容」的問題解決了,卻把問題推給了「信任公鑰」。 下一章:PKI,就是為了堵這個洞而存在的一整套建築。