JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

為什麼雜湊是一枚偽造不了的指紋

密碼雜湊能把任意大小的資料,壓縮成一枚短短的、無法偽造的指紋。本篇帶你認識它遵守的規則,親眼看一個字母如何攪亂整串輸出,並理解為什麼正是這一招,讓區塊鏈難以被竄改。

一枚沒人能複製的火漆印

幾百年前,國王會用一團融化的火漆封住信件,再把他的印戒壓上去。任何人一眼看到這枚印,就能確定兩件事:這封信來自國王,而且一路上沒人拆過它。要想偷看裡頭的內容就得弄破火漆,而一旦弄破,就再也封不出一模一樣的印。密碼雜湊就是這枚印章的數位版——只不過壓上去的不是戒指對火漆,而是一串算術運算壓在你的資料上。

雜湊函式能吃下任何輸入——一個單字、一部 4 GB 的電影、人類寫過的所有書的全文——再回傳一串固定長度的短字串,稱為摘要(digest,或就叫雜湊值)。餵它同一個輸入,你永遠會拿到同一個摘要,每次都一樣,在地球上任何一臺機器上都一樣。把輸入改動一個逗號,摘要就會徹底改變。那串短字串,就是你資料的指紋:短到可以隨手抄下,卻又和原資料綁得如此緊密,以至於至今沒有人能找出兩個不同的輸入,共用同一枚指紋。

好的雜湊立下的五個承諾

不是每個能把資料壓小的函式,都安全到足以拿來承載金錢。用來抓出信用卡號打錯一位的那種校驗碼,也算是一種雜湊,但你幾秒鐘就能偽造出一個對得上的號碼。密碼雜湊被要求達到高得多的標準——尤其是以下五個承諾:

  1. 確定性。 同一個輸入永遠得出同一個摘要。裡頭沒有隨機性、不看時鐘、沒有藏起來的鹽值——給定那串位元組,答案就是固定的。
  2. 固定且精簡的長度。 一個字母還是一整座圖書館,摘要長度都一樣(SHA-256 是 256 位元)。指紋不會隨資料變大而變長。
  3. 正向計算很快。 即使是大檔案,算雜湊也只需幾毫秒。困難從來不在做出一枚摘要——而在於從摘要反推回去。
  4. 單向(原像抗性)。 給你一枚摘要,沒有捷徑能反推出產生它的輸入。你唯一的辦法,是不斷猜輸入、做雜湊,直到撞中一個——而要試的數量多到天文等級。
  5. 碰撞抗性 沒人能找出兩個不同的輸入,雜湊到同一個摘要。理論上碰撞一定存在(輸入無限多、摘要有限),但要找到哪怕一組,都必須昂貴到絕望。

藏在最後那條性質裡的,還有第六個近乎魔法的行為,叫做雪崩效應(avalanche effect):翻動輸入的一個位元,平均而言輸出也會有一半的位元跟著翻轉——而且翻得無法預測。這裡沒有什麼平滑的旋鈕,讓輸入小改一點、摘要就只偏移一點。任何改動,無論多微小,都會把整枚指紋甩進一個全新、看似隨機的樣貌。這正是我們接下來要親眼見證的事。

親眼看一場雪崩

我們用 SHA-256 雜湊幾個短字串,讀讀真正的摘要。這些指令你都能在任何 Mac 或 Linux 終端機上自己跑一遍——下面的數字不是我編的,正是這個函式真實回傳的結果。

$ printf 'hello' | shasum -a 256
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

$ printf 'Hello' | shasum -a 256     # only the 'h' became 'H'
185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969

$ printf 'hello.' | shasum -a 256    # added a single period
1589999b0ca6ef8814283026a9f166d51c70a910671c3d44049755f07f2eb910

$ printf '' | shasum -a 256          # the empty string still has a hash
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
三個幾乎相同的輸入,三串毫不相干的摘要。每一個輸出都恰好是 64 個十六進位字元。

仔細看 `hello` 對上 `Hello`。我們只改了恰好一個位元——小寫 `h` 是位元組 `0x68`,大寫 `H` 是 `0x48`,差在一個二進位數字。然而兩串摘要完全看不出任何關聯。前者開頭是 `2cf24dba…`,後者是 `185f8db3…`。你無法看著其中一個,去猜出另一個的半點線索。這就是雪崩效應:沒有「接近」、沒有「越來越熱/越來越冷」。一枚摘要,要嘛分毫不差地對上,要嘛就只是一團雜訊。

SHA-256 內部:是算術,不是魔法

雪崩看起來像巫術,但 SHA-256 其實是一份固定、公開、人人都能實作的食譜——裡頭沒有任何祕方。它屬於一類稱為 Merkle–Damgård 的設計,會把訊息一次咀嚼 512 位元一塊,把每一塊都攪進一個持續更新的內部狀態裡。整個流程的高層概覽如下:

  1. 填補訊息。 先補上一個 `1` 位元,再補足夠多的 `0` 位元,最後接上一個記錄原始長度的 64 位元數字,使總長度剛好是 512 位元的整數倍。(把長度也填進去,正是空字串也有豐富摘要的原因。)
  2. 從八個固定的字開始。 把八個 32 位元暫存器(叫它們 a…h)初始化為一組常數,這些常數取自前八個質數的平方根的小數部分。
  3. 展開每一塊。 把一塊裡的 16 個字,透過位元旋轉與 XOR 混合先前的字,擴展成 64 個字。
  4. 跑 64 輪。 每一輪都用加法、旋轉、XOR,以及非線性的 `Ch` 與 `Maj` 函式,再加上一個輪常數(取自前 64 個質數的立方根),把八個暫存器攪一遍。一個被改動的位元,就是在這裡擴散到所有地方的。
  5. 串接並收尾。 把每輪的結果加回持續更新的狀態裡,再處理下一塊。最後一塊跑完後,把八個暫存器串接起來——那串 256 位元,就是你的摘要。
H = INITIAL_EIGHT_WORDS            # from sqrt of primes
for block in pad(message).blocks_of_512_bits():
    w = expand(block)              # 16 words -> 64 words
    a,b,c,d,e,f,g,h = H
    for t in range(64):            # 64 rounds of stirring
        T1 = h + Sigma1(e) + Ch(e,f,g) + K[t] + w[t]
        T2 = Sigma0(a) + Maj(a,b,c)
        h,g,f,e = g, f, e, (d + T1)
        d,c,b,a = c, b, a, (T1 + T2)
    H = [ (x + y) mod 2**32 for x,y in zip(H, [a,b,c,d,e,f,g,h]) ]
digest = concat(H)                 # 8 x 32 bits = 256-bit fingerprint
SHA-256 的虛擬碼。每一步都只是模 2^32 的整數算術——完全確定,但這 64 輪讓輸出既無法預測、也無法反推。

2^256 這道牆——以及 SHA-1 倒下的那天

為什麼反推雜湊是絕望的?因為 SHA-256 有 2^256 種可能的輸出,而這個數字大到幾乎難以想像:約 1.16 × 10^77——和可觀測宇宙裡的原子數量相當。要攻破原像抗性,你只能不停猜輸入、做雜湊,直到撞中目標。就算一臺機器每秒能試一兆兆(10^24)次,平均下來,要命中哪怕一次,所需的時間都遠遠超過宇宙的年齡。

找碰撞比找原像稍微容易些,這要歸功於生日悖論:一個房間裡只要有 23 個人,大概就會有兩人同一天生日,因為你只需要任意一對對上即可。同樣的數學意味著:一個 n 位元的摘要,大約 2^(n/2) 次嘗試就能撞出碰撞,而不是 2^n。所以 SHA-256 的碰撞抗性大約值 2^128 的計算量——仍約 3.4 × 10^38,對任何人來說都遠在能力之外。

但「今天很難」不等於「永遠很難」,而較弱的雜湊確實已經倒下。MD5 被攻破得如此徹底,以致 2012 年的 Flame 間諜惡意程式,利用一次 MD5 碰撞偽造了一張微軟的程式碼簽章憑證。SHA-1 撐得久一些,但 2017 年 2 月,Google 與阿姆斯特丹的 CWI 公布了 SHAttered:兩個不同的 PDF 檔案,竟有相同的 SHA-1 摘要,是用約 2^63 次計算找到的——昂貴,但不再是科幻小說。這正是為什麼正經系統都改用了 SHA-256 及其同族,也是為什麼誠實的工程師會說某個雜湊「就我們目前所知是安全的」,而絕不會說「永遠安全」。

從一枚指紋,到一段無法竄改的歷史

現在來收割成果。區塊鏈,本質上是一疊區塊,其中每個區塊標頭都儲存了前一個區塊的雜湊值。每個區塊都用一枚指紋封印,而這枚指紋同時取決於它自己的內容以及前一個區塊的雜湊,於是區塊被串成一條鏈:第 100 塊的指紋被烤進第 101 塊裡,第 101 塊的指紋又被烤進第 102 塊,如此一路回溯,直到最初的那一塊。

假設有個賊想悄悄改掉埋在第 100 塊裡的一筆付款——比方說,把他收到的金額調高。哪怕只改一個數字,都會改變第 100 塊的內容,而這(雪崩!)會把它的指紋變成毫不相干的東西。但第 101 塊裡記的還是舊的指紋,於是這條鏈會明顯斷開。要藏住這次竄改,賊就必須重新封印第 100 塊,並且重算其後的每一塊,好讓所有指紋重新對得上。此外,每一塊還透過一棵 Merkle 樹承諾了它所有的交易,所以連在區塊內部偷偷動手腳,都瞞不過那枚印。

這就是整個地基,而你現在已經握住了它。雜湊是一枚你偽造不了、反推不了、也撞不出碰撞的指紋——小到可以隨手傳遞,卻又敏感到一個位元翻轉就把它整個改寫。把這些指紋疊成一條鏈,你就得到一段會自己揭發竄改的歷史。下一篇,我們就用這把同樣的工具來搭建 Merkle 樹——正是這個結構,讓一支手機不必下載整個區塊,也能驗證某一筆付款確實在區塊之中。