先用一句話抓住這篇
給第一次設計雜湊 IP 的電機系與研究所學生:先看簡明 TOP view,再從 padding、W 排程、64 輪資料路徑走到跨區塊累積、FSM、握手與逐拍驗證。
先把這篇當成一張閱讀地圖:上方摘要說明問題,圖片先建立直覺,下面文字再補上真正的技術取捨。
如果第一次讀覺得名詞很多,可以先記住比喻與結論;第二次再回頭看名詞,會順很多。
晶片收到一份新的韌體。Flash 不會一次交出整份映像,而是一段一段送來。系統需要的是整份映像的 SHA-256 摘要,不是每段資料各自的摘要。最後一段不足一個區塊,也不能直接丟掉。
如果用軟體,一次函式呼叫會替你安排這些工作。換成硬體,就要明確分工:誰補齊最後一塊?誰留下上一塊的結果?核心還在算時,下一塊由誰保管?這些問題決定暫存器與介面的設計。
本篇建立一個 SHA-256 iterative block core。它每次接收已補位的 512-bit 區塊,重複使用同一套電路完成 64 輪,再累積結果。先理解資料怎麼走,再接上公式。讀者需要同步邏輯、位元運算與基本 Verilog 概念。
本篇交付的是設計教學、介面與時序契約,以及可執行的參考模型。RTL 片段用來說明關鍵接線,並非已完成綜合、時序收斂或安全認證的完整 IP。想先建立雜湊的安全概念,可讀 SHA 漫畫小教室;熟悉本篇後,也可比較 AES RTL 入門 的資料路徑。
1. 先分清楚 SHA-2、SHA-256 與加密
SHA-2 是演算法家族,SHA-256 是其中一個成員。 本篇選擇 32-bit 運算、512-bit 區塊、256-bit 摘要的 SHA-256,讓你先把一種架構做清楚。FIPS 180-4 定義了相關函式、常數與處理流程。
| 成員 | Word | 輸入區塊 | 輪數 | 摘要 |
|---|---|---|---|---|
| SHA-224 | 32 bits | 512 bits | 64 | 224 bits |
| SHA-256 | 32 bits | 512 bits | 64 | 256 bits |
| SHA-384 | 64 bits | 1024 bits | 80 | 384 bits |
| SHA-512 | 64 bits | 1024 bits | 80 | 512 bits |
| SHA-512/224、SHA-512/256 | 64 bits | 1024 bits | 80 | 224/256 bits |
這些變體不只是改輸出寬度。SHA-224 與 SHA-256 的初始值不同;SHA-512/256 也不是直接把一般 SHA-512 的結果截成 256 bits。本文不把家族成員混在同一份控制器裡。
SHA-256 不是把資料加密成 256 bits,也沒有一把用來還原原文的金鑰。輸入可以有很多不同長度,輸出固定是摘要;摘要無法唯一表示所有可能訊息。普通 SHA-256 沒有金鑰輸入,後面的 K[t] 是公開輪常數,不是 secret key。
在韌體情境裡,摘要可用來和可信的預期值比對;若攻擊者連預期摘要也能一起替換,單靠雜湊不能證明發布者身分。簽章驗證與可信 metadata 是系統的另一層責任,本篇先處理摘要如何正確算出。
2. TOP view:先看整個任務,再打開核心
先用兩層來看。外層整理訊息,內層壓縮區塊。 外層知道原始訊息多長、何時結束;內層只需要知道眼前這個 512-bit 區塊是不是第一塊、是不是最後一塊。
訊息整理層負責補位、記錄原始 bit 長度、排列成 512-bit 區塊。這個功能最終可以做成硬體串流 wrapper,但第一版由 testbench 或軟體先準備,較容易把演算法錯誤與介面錯誤分開。不要把「已 padding 的區塊核心」誤稱成可直接接任意 bytes 的完整串流 SHA IP。
打開區塊核心後,先認識四個角色:W 排程器每輪提供一個 32-bit 訊息 word;Round 資料路徑計算下一組 a~h;H 暫存器保存跨區塊累積值;Controller決定何時接收、計算、累加與輸出。它們都在同一個任務裡,卻不應共用一個含糊的「state」名稱。
跟著一份訊息走一次
以很短的 abc 為例。外層先把三個 bytes 補成一個 64-byte 區塊,標記 first=1、last=1。核心接收後存住區塊、載入初始值,接著每拍完成一輪,總共 64 輪。輪運算結束,還要把工作結果加回原來的 H,這一步叫 feed-forward。完成後才可以宣告 digest 有效。
如果訊息跨兩個區塊,第一塊算完不交出最終 digest,而是留下 H,等待第二塊繼續。第二塊不能重新從初始值開始,也不能把兩塊各自的 SHA-256 串在一起。下游若還沒準備好收最終摘要,核心就保持摘要與 valid,直到交付成立。
3. Padding:最後不足一塊,誰來補?
SHA-256 處理的訊息 bit 長度 L 必須小於 2⁶⁴。規則是先接一個 1 bit,再補 k 個 0 bits,使 (L + 1 + k) mod 512 = 448,最後接 64-bit 大端序的原始 bit 長度 L。長度不包含 padding 本身;不要把 bytes 數直接寫進去。這對應 FIPS 180-4 §5.1.1。
本篇先處理 byte-aligned 訊息,所以接上那個 1 bit 表現為一個 0x80 byte。abc 的 bytes 是 61 62 63,原始長度是 24 bits;它後面接 80、52 個零 bytes,最後接 00 00 00 00 00 00 00 18。
| 原始訊息長度 | Padding 後區塊數 | 為什麼 |
|---|---|---|
| 0 bytes | 1 | 空訊息仍要補 1 bit 與長度欄位 |
| 55 bytes | 1 | 55 + 1 + 8 = 64,剛好放得下 |
| 56 bytes | 2 | 第一塊已放不下 8-byte 長度欄位 |
| 63 bytes | 2 | 補位與長度仍需下一塊 |
| 64 bytes | 2 | 原始資料剛好一整塊,仍必須另加 padding |
因此 block_last 的意思是最後一個 padded block,不是「最後一塊含原始資料的 block」。原始資料已經 64 bytes 對齊時,這兩者尤其容易被混淆。
回到韌體讀取流程,Flash 告訴你「原始資料送完了」,不代表核心也收完了。整理層還要送出補位與長度欄位。若過早標記 last,核心即使每一輪都算對,得到的也不是規定的 SHA-256 摘要。
4. 固定 byte 與 word 排列
先固定「最早的 byte 放哪裡」,再設計取 word 的電路。本篇定義 block_data[511:0] = {B0, B1, …, B63},B0 是訊息中最早的 byte。第一個 word 是 W0 = {B0,B1,B2,B3} = block_data[511:480],最後一個 word 是 W15 = block_data[31:0]。每個 word 都採大端序組合。
// Interface convention; i is a constant generate index, 0..15.
assign word_i = block_data[511 - 32*i -: 32];
abc 的第一個 word 必須是 32'h61626380,不是 32'h80636261。這是核心介面約定;若 CPU 或匯流排的 byte lanes 不同,轉換應由 wrapper 明確處理。不要在 W 排程器與 round 電路各自交換一次 byte,讓兩個錯誤暫時互相抵銷。
測試時先看 W0,而不是直接等摘要。W0 就像交給核心的第一份資料清單。這裡排錯了,後面再正確的旋轉與加法也無法補救。介面、參考模型與波形顯示必須使用同一份排列約定。
5. H 與 a~h:兩組暫存器,不能省成一組
SHA-256 有八個 32-bit 的 chaining words,本文稱為 H0…H7。它們保存「目前整份訊息已處理到哪裡」;另有八個工作暫存器 a…h,負責「眼前這一塊正在算哪一輪」。注意小寫 h 只是工作暫存器之一,大寫 H 是整組跨區塊狀態。
第一塊從下面的 IV 開始。IV 是公開固定常數,不是每次隨機產生的數值。第二塊與後續區塊,則從上一塊完成 feed-forward 的 H 載入工作暫存器。
H0 = 6a09e667 H1 = bb67ae85
H2 = 3c6ef372 H3 = a54ff53a
H4 = 510e527f H5 = 9b05688c
H6 = 1f83d9ab H7 = 5be0cd19
為什麼不能讓 H 直接跟著每輪變動?因為最後要做 H0 + a_final、H1 + b_final,一路到 H7 + h_final。如果早把 H 蓋掉,就失去加回去的基準。本架構在 ROUND 階段只更新 a~h,等到獨立的 ACCUM 階段才更新 H。
6. 先認識硬體積木:旋轉、選擇、多數決與加法
SHA-256 沒有 AES S-box。主要工作是固定旋轉、邏輯位移、XOR、AND、NOT 與 32-bit 加法。所有加法都取模 2³²,也就是只保留低 32 bits;這裡的加法有 carry,不能用 XOR 代替。
ROTRⁿ(x) 是向右循環旋轉,被移出的低位繞回高位;SHRⁿ(x) 是邏輯右移,高位補零。固定旋轉在 RTL 中通常是接線;可變 n 的通用旋轉器可能引入不必要的選擇電路。
// Example: fixed ROTR^7 and SHR^3, with unsigned 32-bit x.
assign r7 = {x[6:0], x[31:7]};
assign s3 = x >> 3;
| 函式 | 定義 | 用在哪裡 |
|---|---|---|
| Ch(x,y,z) | (x & y) ^ (~x & z) | 每一 bit 由 x 選擇 y 或 z |
| Maj(x,y,z) | (x & y) ^ (x & z) ^ (y & z) | 每一 bit 取三者多數值 |
| Σ0(x) | ROTR² ⊕ ROTR¹³ ⊕ ROTR²² | Round 電路,輸入 a |
| Σ1(x) | ROTR⁶ ⊕ ROTR¹¹ ⊕ ROTR²⁵ | Round 電路,輸入 e |
| σ0(x) | ROTR⁷ ⊕ ROTR¹⁸ ⊕ SHR³ | W 排程 |
| σ1(x) | ROTR¹⁷ ⊕ ROTR¹⁹ ⊕ SHR¹⁰ | W 排程 |
大寫 Σ 與小寫 σ 是不同函式,旋轉量不同,小寫還有一項 SHR。命名時使用 big_sigma0、small_sigma0 等明確名稱,比只用 sigma 更不容易接錯。
可以先只測一個 bit 的位置,確認旋轉會繞回、位移會補零。再測一組會溢位的加數,確認電路丟掉第 33 bit。把這些小積木分開驗證,能避免日後看到摘要不符,卻不知道該查接線還是算術。
先用小數值認識這些運算
以下 hex 值都固定為 32 bits。ROTR¹(00000001)=80000000,因為低位的 1 繞回最高位;SHR¹(00000001)=00000000,則直接丟掉它。模加法 ffffffff + 00000001 = 00000000,只保留低 32 bits;XOR 卻得到 fffffffe,兩者不可互換。
Ch 是逐 bit 選擇:x=1 選 y,x=0 選 z。取 x=ffffffff、y=12345678、z=9abcdef0,Ch=y;x=00000000 時 Ch=z。Maj 是逐 bit 多數:三個輸入中至少兩個為 1,輸出才是 1。這些函式不會把八個 words 當成八個整數投票。下面回到同一份 abc,使用完整 IV 計算第一輪。
7. 一輪資料路徑:先算 T1、T2,再一起更新
把 round 當成一個純組合函式:輸入目前的 a~h、這一輪的 W[t] 與公開常數 K[t],輸出下一組 a~h。t 從 0 到 63。K 是 64 個固定的 32-bit 值,例如 K[0]=428a2f98、K[63]=c67178f2;完整表見 FIPS 180-4 §4.2.2 與文末參考模型。
T1 = h + Σ1(e) + Ch(e,f,g) + K[t] + W[t]
T2 = Σ0(a) + Maj(a,b,c)
a_next = T1 + T2 e_next = d + T1
b_next = a f_next = e
c_next = b g_next = f
d_next = c h_next = g
這個圖的重點是全部右側都讀同一組舊值。如果先覆寫 a,再把 a 傳給 b,b 就拿到了錯誤的新 a。時序區塊使用 nonblocking assignments,可表達同一個上升緣共同取樣。
// Conceptual ROUND branch. t1 and t2 are combinational 32-bit signals.
a <= t1 + t2;
b <= a;
c <= b;
d <= c;
e <= d + t1;
f <= e;
g <= f;
h <= g;
組合電路在暫存器輸出改變後持續反應,時脈只決定何時保存下一組值。T1 涉及多個加數,通常比固定旋轉更值得注意時序。本文假設組合讀取 K 常數;如果改用同步 ROM,必須安排提前讀取或增加階段,不能直接沿用第 11 節的拍數。
從 abc 的舊值算出第一輪
所有值採十六進位,所有加法保留低 32 bits。Round 0 的舊值來自第 5 節 IV,W0=61626380、K0=428a2f98。依第 6 節三個旋轉量計算:
Σ1(510e527f)=3587272b;Ch(510e527f,9b05688c,1f83d9ab)=1f85c98c。
所以 T1 = 5be0cd19 + 3587272b + 1f85c98c + 428a2f98 + 61626380 = 54da50e8 (mod 2³²)。同樣,Σ0(6a09e667)=ce20b47e、Maj(6a09e667,bb67ae85,3c6ef372)=3a6fe667,得到 T2=08909ae5。
再算 a_next=54da50e8+08909ae5=5d6aebcd;e_next=a54ff53a+54da50e8=fa2a4622 (mod 2³²)。b_next 接舊 a=6a09e667,不能接新 a=5d6aebcd。這是「一起更新」的可手算檢查。
在互動台載入 abc,LOAD 顯示 IV,下一步 ROUND t=0 顯示上述 T1、T2 與新 a/e。它對應第 11 節 E1;LOAD 是 E0。若只核對摘要,就失去判斷錯誤先發生在公式還是暫存器更新的機會。
8. W 排程:16 個輸入 words 如何供應 64 輪?
區塊原本只有 W[0]~W[15]。SHA-256 依下式產生 W[16]~W[63],每輪消耗一個 W[t]:
W[t] = σ1(W[t-2]) + W[t-7] + σ0(W[t-15]) + W[t-16]
第一個容易理解的實作是保存全部 64 words,先展開再運算;但那會需要額外排程時間或較大的組合網路。本文選擇 16-word 滑動視窗,保存 512 bits,讓產生未來 word 與目前 round 並行。
在第 t 輪開始,q[0] 是本輪用的 W[t]。前 48 輪還有未來 word 要產生,此時 q[1]、q[9]、q[14] 分別是 W[t+1]、W[t+9]、W[t+14],所以:
next_w = σ1(q[14]) + q[9] + σ0(q[1]) + q[0]
round_w = q[0]
每次輪末,把 q[1] 搬到 q[0],一路移到 q[14],尾端 q[15] 補 next_w。t=0 時計算 W16,t=47 時計算 W63;t=48~63 不再需要新 word,可在尾端補零。最後十六輪仍從 q[0] 依序讀完既有的 W48~W63。
// Inside ROUND; load all 16 words separately on block acceptance.
for (int j = 0; j < 15; j++) q[j] <= q[j+1];
q[15] <= (round_t < 6'd48) ? next_w : 32'b0;
所有右側都讀移動前的視窗。若先移動再算 next_w,四個 taps 就全部錯一格。這個排程器應先獨立驗證:把實際送入 round 的 64 個 words,逐一對照完整展開陣列,而不只是看最後 digest。
W16 不需要猜:沿同一份 padding 算
對 abc,W14=W9=W1=0、W0=61626380。代入展開式:W16=σ1(W14)+W9+σ0(W1)+W0=0+0+0+61626380=61626380。在 t=0 的視窗,q14、q9、q1、q0 正是這四個值,所以同拍產生相同的 next_w。E1 把它放進 q15,再經過十五次移動,E16 更新後到達 q0。t=16 的 round 在 E17 捕捉使用它的運算結果。
這並不表示 W16 永遠等於 W0;那是 abc 的三個零項造成的特例。換成 56-byte 範例時,先查看該區塊 W1、W9、W14,再算 W16。圖中的 taps、程式的 q 索引與下載模型的完整 W 陣列,必須得到同一個 word。
9. Feed-forward 與跨區塊累積
64 輪後,a~h 是工作結果,還不是最終 digest。需要逐 word 做:
H0_new = H0_old + a_final H4_new = H4_old + e_final
H1_new = H1_old + b_final H5_new = H5_old + f_final
H2_new = H2_old + c_final H6_new = H6_old + g_final
H3_new = H3_old + d_final H7_new = H7_old + h_final
這是八次獨立的 32-bit 模加法,不是一個帶著跨 word carry 的 256-bit 大加法。這個架構給它獨立一拍 ACCUM,因此讀到的 a~h 已是 t=63 完成後的值。
若還有下一塊,新 H 留著供下一次載入;若是最後一塊,輸出 {H0,H1,…,H7},H0 在 digest[255:224]。這也說明為什麼同一份訊息的區塊有前後相依性:第二塊的起點必須等待第一塊完成。
因此,不能把同一份韌體的兩塊各送到一個核心,再把兩個摘要接起來。那是兩份不同的雜湊計算。本篇核心保存的 H,是連接這份訊息前後區塊的依據;first 旗標只在新訊息開始時要求回到 IV。
用第一個 word 看出 feed-forward 的作用
abc 的第 64 輪完成後,a_final=506e3058。原來的 H0=6a09e667 仍未被覆蓋,所以 H0_new=6a09e667+506e3058=ba7816bf。這才是摘要開頭。若直接把 a_final 當作摘要,就會錯從 506e3058 開始;若把所有 words 串成一次 256-bit 加法,也會錯把低 word 的 carry 傳到其他 word。
在互動台先停在最後 ROUND,查看工作值;再前進 ACCUMULATE,比對 H base 與 H next。多區塊訊息的下一次 LOAD 必須使用 H next。這是算法狀態的串接;畫面前進不量測實際 RTL 的運算延遲。
10. Top-Level 介面:先把交付規則寫下來
第一版不加入 AXI、DMA 或任意位元長度串流。上游把 padded block 準備好,與 first、last 旗標一起送入。輸入資料與兩個旗標只有在 block_valid && block_ready 的上升緣一起被接收。
| 訊號 | 方向 | 寬度 | 約定 |
|---|---|---|---|
| clk、rst_n | In | 1 各一 | 上升緣;低有效同步 reset |
| block_data | In | 512 | 已 padding,B0 在最高 byte |
| block_first、block_last | In | 1 各一 | 訊息的第一/最後 padded block |
| block_valid | In | 1 | 上游資料與旗標有效 |
| block_ready | Out | 1 | 核心可在本拍接收 |
| digest_data | Out | 256 | H0 在最高 word |
| digest_valid | Out | 1 | 最後一塊 feed-forward 已完成 |
| digest_ready | In | 1 | 下游可接收摘要 |
| busy | Out | 1 | ROUND、ACCUM、OUTPUT_HOLD 為 1 |
Controller 使用 WAIT_FIRST、WAIT_NEXT、ROUND、ACCUM、OUTPUT_HOLD 五個狀態。為了拒絕不合順序的 first 旗標,本設計定義:
block_ready = rst_n && (
(fsm == WAIT_FIRST && block_first) ||
(fsm == WAIT_NEXT && !block_first)
)
digest_valid = rst_n && (fsm == OUTPUT_HOLD)
來源不可等 ready 才提出 valid、data 與 first;要先提出請求,等待握手。valid=1 且尚未握手時,data、first、last 與 valid 都必須保持。ready 組合相依於 first,wrapper 不可反過來由 ready 決定 first,造成組合回路。WAIT_FIRST 只接 first=1,WAIT_NEXT 只接 first=0;旗標錯誤時 ready 保持 0,不會悄悄改用錯的初始值。這是刻意簡化的協定,不是錯誤回報介面;testbench 應用 assertion 或 timeout 抓出違規來源。
first=last=1 表示一塊完成的訊息。last 會在輸入握手時鎖住,不能等到第 64 輪才讀外部 block_last。WAIT_NEXT 時 busy=0 代表核心可接下一塊,不代表整份訊息已結束。
11. 精確時序:64 輪不等於 64 拍交付
E0 是區塊接受的上升緣。下表列出每個邊緣完成更新後的狀態。第一塊把 H 與 a~h 都載入 IV;續塊保持 H,將 H 載入 a~h。兩者都把 16 個輸入 words 載入 q,counter 設為 0。
| 邊緣 | 動作 | 邊緣後狀態 |
|---|---|---|
| E0 | 接收 block,載入 H/work/q,t=0 | ROUND |
| E1~E63 | 完成 t=0~62;工作值與 q 同步更新 | ROUND,t 增加 |
| E64 | 完成 t=63;counter 保持 63 | ACCUM |
| E65,last=0 | H 加回 work | WAIT_NEXT |
| E65,last=1 | H 加回 work,digest_valid=1 | OUTPUT_HOLD |
| E66 或更晚 | 非末塊可接下一塊;末塊可完成摘要握手 | ROUND 或 WAIT_FIRST |
| E67 或更晚 | 上一筆摘要已於 E66 送出時,最早開始新訊息 | ROUND |
表中 E66 的轉移以握手成功為前提:若續塊尚未提出,保持 WAIT_NEXT;若下游不接摘要,保持 OUTPUT_HOLD。E0 只載入,不同時執行 t=0 或移動 q;ACCUM 也不因 digest_ready=1 就跳過 OUTPUT_HOLD。用「目前 FSM 狀態」選擇載入/ROUND/ACCUM 分支,避免同一個邊緣重複更新。
從最後區塊接受到 digest_valid 是 65 cycles;最早摘要握手在 E66。無 stall 的續塊接受間隔是 66 cycles,長訊息的核心區塊處理率約 512 × f_clk / 66 bit/s,未扣 padding、輸入供應與輸出等待。單一區塊訊息連續送入時,接受間隔最少 67 cycles,不能把 66 套到所有情況。
這些數字假設一輪一拍、組合常數讀取、八個 feed-forward 加法同拍完成,以及沒有額外 input/output pipeline。若增加暫存階段或共用一個加法器,就必須重寫這張表。實際 f_clk 由綜合、佈局與時序分析決定,不從算法輪數猜出。
12. 背壓、Reset 與容易忘記的狀態
摘要算完,只代表核心已有答案。下游可能還在處理上一件工作。digest_valid=1 && digest_ready=0 時,核心必須保管 H、digest_data 與 digest_valid,不能先清掉它們,也不能接新區塊來覆蓋 H。
兩者同為 1 的上升緣才完成傳送,之後回 WAIT_FIRST。本文不在輸出握手同拍接受新訊息,所以有 E67 的邊界。這個空拍是本篇的介面選擇,不是 SHA-256 的數學要求。
rst_n=0 時的 clk 上升緣清除 H、a~h、q、counter 與 latched last,回 WAIT_FIRST,取消尚未完成的訊息。reset 期間 ready/valid 都為 0。解除後第一筆必須 first=1,不能接著送被取消訊息的續塊。清成 0 不影響 SHA IV:第一塊握手時才載入規定的 IV。
上游也要知道這份訊息已被取消。若它仍從中間區塊接著送,核心會拒收,不能把這種等待誤當成硬體算得太慢。測試環境應清掉取消的期待結果,並要求來源重新開始一份完整訊息。
資料 register、演算法 round counter 與 FSM 不要混成一個 always block 裡難以辨認的流程。可以用一個清楚的 sequential block 統一更新,但 next-value 計算與各階段的 enable 必須可逐一追蹤。所有組合輸出都要完整賦值,避免意外推導 latch。
13. 用 abc 找出第一個分歧點
先不要用「看起來像亂碼」當正確性判斷。NIST SHA-256 中間值範例 給出了 abc 的每輪 a~h,也有兩個區塊的範例。
Input bytes = 61 62 63
W0 = 61626380
W1 … W14 = 00000000
W15 = 00000018
K0 = 428a2f98
After t=0:
a=5d6aebcd b=6a09e667 c=bb67ae85 d=3c6ef372
e=fa2a4622 f=510e527f g=9b05688c h=1f83d9ab
Final digest:
ba7816bf8f01cfea414140de5dae2223
b00361a396177a9cb410ff61f20015ad
上面摘要分兩行顯示,實際是連續 64 個 hex digits。NIST 的「t=0」表示第一輪完成後,對應本篇 E1,不是 E0 初始化的值。對照 trace 前先對齊取樣時點,否則會把正確結果誤認為永遠差一輪。
| 第一個錯的位置 | 優先查哪裡 |
|---|---|
| W0 就錯 | 原始 bytes、padding、大小端 |
| W0~W15 對,W16 錯 | σ 大小寫、四個 taps、更新前後順序 |
| W 都對,t=0 的 a/e 錯 | K0、Ch/Maj、Σ、32-bit 模加 |
| t=0 對,後面平移一輪 | counter、同步 ROM 延遲、nonblocking 更新 |
| 64 輪都對,digest 錯 | 忘記 feed-forward、加到新 H、跨 word carry |
| 短訊息對,長訊息錯 | 每塊重載 IV、first/last、padding 邊界 |
| 數值對,輸出筆數錯 | 背壓下 valid 提早消失或重複交付 |
14. 可執行的參考模型與驗證作業
下載 SHA-256 參考模型(Node.js) 與 abc 每輪 trace/邊界測試結果。在有 Node.js 的環境執行:
node sha256-rtl-reference.mjs sha256-trace.json
模型會獨立展開 64-word W 表,再逐輪確認 16-word 視窗消耗的 word 相同;同時計算 digest,與 Node/OpenSSL 的 SHA-256 比較。271 組訊息涵蓋空字串、abc、NIST 兩區塊訊息、55/56/63/64-byte 邊界及不同資料樣式。另核對 NIST 的第一輪 a~h。這是演算法與排程參考驗證,不是 RTL 模擬通過的證明。
真正寫 testbench 時,先送軟體已 padding 的 block,scoreboard 只在有效握手邊緣記錄收送。從正確數值擴充到控制情境:
- 空訊息、一區塊、兩區塊、三區塊;續塊中間插入空拍。
- digest_ready 持續為 1,以及隨機拉低數拍;摘要與 valid 必須保持。
- 前一份訊息結束後接不同的新訊息;確認 H 回到 IV 起點。
- ROUND、ACCUM、OUTPUT_HOLD 時 reset;清掉 scoreboard 取消的期待結果。
- WAIT_FIRST 故意送 first=0、WAIT_NEXT 故意送 first=1;確認不接受違規區塊,測試要有 timeout。
- 比對所有 W[t]、每輪 a~h、每塊 feed-forward H,再比對最終摘要。
15. 建議模組切法與第一次實作順序
| 模組 | 責任 | 先驗證什麼 |
|---|---|---|
| sha256_round | 舊 a~h、Wt、Kt → 新 a~h | NIST t=0,再核對全部 rounds |
| sha256_schedule | 載入 16 words,維護 q 視窗 | 全部 64 個消耗 words |
| sha256_constants | t → 32-bit Kt | 64 個常數與讀取延遲 |
| sha256_core | H/work/q、FSM、握手與累加 | 多區塊、stall、reset |
| message wrapper(下一階段) | bytes、長度、padding、first/last | 55/56-byte 邊界與串流停頓 |
先完成純組合 round,再做 schedule,最後加 state、FSM 與介面。不要一開始就把 AXI、DMA、padding、SHA-224 切換與高效能 pipeline 全塞進來;每增加一個共享資源或階段,都會改變你必須證明的時序。
這個順序能把錯誤逐層縮小。Round 測試只問算式是否正確;schedule 測試只問每輪拿到哪個 W。兩者各自通過後,整合測試才集中檢查載入、累加與交付。若一開始就只比對最後摘要,資料與控制的錯誤會混在一起。
粗看資料儲存,H 256 bits、work 256 bits、q 512 bits,共 1024 bits,另外還有 counter、旗標與控制狀態;K 常數表 64×32=2048 bits,可由組合查表實現,不等於一定占用 2048 個 flip-flops。第一版做對後再看 synthesis report,找真正的加法 critical path、面積與切換功耗。
16. 完成本篇後,應該能回答什麼?
- 為什麼需要 message wrapper?因為區塊核心不負責猜原始訊息長度與 padding。
- 為什麼 H 不能每輪都覆寫?因為每塊結束還要加回該塊起始的 H。
- 為什麼 K 不是 AES 的 round key?因為它是每個人相同的公開常數。
- 為什麼 64 輪還要到 E65 才有效?因為還有 feed-forward。
- 為什麼原始 64-byte 訊息仍需兩塊?因為 padding 與長度欄位不可省略。
把它接回韌體更新場景:核心先保證完整訊息的摘要正確,系統再把摘要放進簽章驗證與可信啟動流程。HMAC 也需要額外的 key 處理與內外層雜湊,不能把一把 key 接到本篇的 K 常數輸入就當成 HMAC。
17. 參考資料與設計範圍
- FIPS 180-4:Secure Hash Standard:§4.1.2 函式、§4.2.2 常數、§5.1.1 padding、§5.3.3 IV、§6.2 SHA-256。
- NIST SHA-256 中間值範例:單區塊
abc與兩區塊訊息的逐輪結果。 - NIST FIPS 180-4 文件頁:標準版本與文件狀態。
本篇的 16-word 視窗、五狀態 FSM、first 旗標拒收策略與 E0~E67 時序,是為入門選定的硬體架構;不是標準強制規定的介面,也不代表特定製程的效能承諾。
本篇先不實作串流 padding、AXI/DMA 與多訊息並行。時序收斂、側通道與故障防護也需另外驗證。完成本核心後,可先補 message wrapper;若要分析摘要如何參與授權,再讀 RTL 防竄改第一課。
MY ACADEMY · LESSON FILM
教學影片
影片依序說明本課的資料路徑。看完一段,可以回到下面的互動練習,改變輸入或故障條件。動畫呈現教學模型;它沒有替代 RTL 模擬。
旁白使用合成聲音。互動教學與動畫均有模型邊界;請以本課的來源與驗證範圍解讀結果。
MY ACADEMY · RTL LAB
操作 SHA-256:追蹤區塊與逐輪暫存器
先用 abc 走到第 0 輪,查看 W、K、T1、T2 與 a~h。改用 56 bytes 範例,再看 padding 為何產生兩個區塊,以及前一區塊如何更新 H。
教學排程包含 LOAD、64 次 ROUND 與 ACCUMULATE。每個 ROUND 視為一次暫存器更新;LOAD/ACCUMULATE 各自獨立。實際 RTL 的 latency 仍由自己的 FSM、排程與介面決定。
證據範圍:瀏覽器功能教學模型。獨立比對使用瀏覽器 Web Crypto。沒有執行 RTL、綜合、形式證明或側通道測試。
運算前
運算後
查看區塊、排程與輪金鑰
輸出握手:valid/ready
走到 OUTPUT 後,輸出保持不變。切換 ready,再按「取樣一拍」才能記錄交付。設定 ready 本身不會交付。這個握手是另外定義的教學介面。
遷移練習:如果 downstream 尚未 ready,RTL 能否清除 output_valid 或覆寫結果?先用一句話寫出保持規則:除非 reset 取消交易,valid 與資料必須保持到交握成立。SVA 語法可作後續練習。
學習指南
密碼演算法 RTL 設計
查看課程大綱 → · 進度只計入已發布課程
先備知識
- 同步暫存器、位元運算與基本 Verilog
我學會了什麼
- 區分訊息整理層與區塊核心
- 建立並驗證 16-word 排程
- 協調 64 輪、feed-forward 與跨區塊累積
- 明確定義拍數、背壓與 reset