返回文章列表

金鑰雜湊與資料驗證技術:HMAC 實戰與時序攻擊防禦

深入探討金鑰雜湊函式的運作原理與實務應用,從 HMAC 機制到時序攻擊防禦策略,搭配 Python 實戰範例示範如何安全地生成金鑰、驗證資料完整性與來源,建立可靠的資料認證系統。

資訊安全 密碼學 Web 開發

前言

在資料安全領域中,單純依靠雜湊函式來確保資料完整性是遠遠不足的。當攻擊者能夠同時修改資料和對應的雜湊值時,傳統的雜湊驗證機制便形同虛設,無法提供任何實質的保護。金鑰雜湊函式的引入從根本上解決了這個問題,它透過引入只有授權方才知道的密鑰,使得攻擊者即使能夠存取和修改資料,也無法產生正確的認證碼,從而同時驗證資料的完整性和來源。

本文將深入介紹金鑰雜湊函式的運作原理和實務應用。我們將從密碼學雜湊函式的基礎概念出發,探討如何使用 Python 的 hashlib、secrets 和 hmac 模組來生成安全的金鑰,並運用金鑰雜湊函式實現資料驗證機制。同時,我們也將深入分析時序攻擊這種側通道攻擊的威脅,以及相應的防禦策略。透過本文的學習,開發者將能夠建立真正安全可靠的資料認證系統,有效保護重要資訊免於未授權的存取和竄改。

資料驗證面臨的安全挑戰

在探討金鑰雜湊函式之前,我們需要先理解為什麼傳統的雜湊驗證機制存在根本性的安全缺陷。讓我們透過一個具體的應用情境來分析這個問題。假設系統管理員 Alice 建立了一套檔案管理系統,該系統的設計目標是確保儲存的檔案不會在未經授權的情況下被修改。系統的運作方式是在儲存每個新檔案之前,先計算該檔案的雜湊值並一同儲存。當需要驗證檔案完整性時,系統會重新計算檔案的雜湊值,並與原先儲存的雜湊值進行比較。如果兩個雜湊值不相符,則認為檔案已損壞或被竄改;如果相符,則檔案被視為完整且未被修改。

這套系統在檢測意外的資料損壞方面表現良好。例如,當儲存媒體發生硬體故障,導致某些位元翻轉時,重新計算的雜湊值幾乎必然會與原始雜湊值不同,系統就能夠檢測到這種損壞。然而,這套系統存在一個根本性的安全漏洞,使得它完全無法防禦有意圖的攻擊。假設惡意攻擊者 Mallory 透過某種方式取得了 Alice 檔案系統的寫入權限,她不僅可以修改檔案的內容,還可以重新計算修改後檔案的雜湊值,並用這個新的雜湊值替換原本儲存的雜湊值。當 Alice 執行驗證程序時,系統會計算被竄改檔案的雜湊值,並將其與 Mallory 提供的新雜湊值進行比較,結果當然會顯示相符,Alice 完全不會察覺檔案已經被修改。

這個情境清楚說明了單純雜湊驗證的侷限性。它只能回答「資料是否與某個特定狀態相符」這個問題,但無法回答「這個特定狀態是由誰建立的」。要建立真正安全的資料驗證機制,系統需要同時確保資料完整性和資料認證。資料完整性確保資料沒有被修改,而資料認證則確保資料確實來自預期的來源。金鑰雜湊函式正是為了滿足這兩個需求而設計的。

密碼學雜湊函式的基礎知識

在深入探討金鑰雜湊函式之前,我們需要先建立對密碼學雜湊函式的基本理解。密碼學雜湊函式是一種將任意長度的輸入資料轉換為固定長度輸出的數學函式,這個輸出通常被稱為雜湊值、摘要或指紋。密碼學雜湊函式具有幾個關鍵的安全特性,使其適合用於各種安全應用。

第一個特性是確定性,意味著對於相同的輸入,雜湊函式總是會產生相同的輸出。這個特性是雜湊函式能夠用於驗證的基礎,因為如果雜湊函式的輸出是隨機的,我們就無法透過比較雜湊值來判斷資料是否相同。第二個特性是單向性,也稱為預映像抵抗性,意味著給定一個雜湊值,在計算上不可能找到任何能產生該雜湊值的輸入。這個特性確保了攻擊者無法透過雜湊值反推原始資料。第三個特性是碰撞抵抗性,意味著在計算上不可能找到兩個不同的輸入產生相同的雜湊值。這個特性確保了攻擊者無法用一個惡意檔案替換原始檔案而保持雜湊值不變。

Python 中的 hashlib 模組提供了多種密碼學雜湊函式的實作,包括 SHA-1、SHA-256、SHA-512、MD5 以及 BLAKE2 等。其中 SHA-256 是目前最廣泛使用的選項之一,它提供了良好的安全性和效能平衡。以下範例展示了如何使用 hashlib 模組來計算訊息的 SHA-256 雜湊值,以及如何查詢雜湊函式的各種屬性。

# 匯入 hashlib 模組
# 該模組提供多種密碼學雜湊函式的實作
import hashlib

# 建立 SHA-256 雜湊物件
# 參數是要進行雜湊的位元組資料
# 注意必須傳入位元組而非字串
hash_function = hashlib.sha256(b'message')

# 查詢雜湊函式的輸出大小(以位元組為單位)
# SHA-256 的輸出大小固定為 32 位元組
# 這個大小不會隨輸入資料的大小而改變
digest_size = hash_function.digest_size
print(f'雜湊值大小: {digest_size} 位元組')

# 取得雜湊值的原始位元組表示
# 然後計算其位元長度
# 32 位元組乘以 8 等於 256 位元
digest = hash_function.digest()
bit_length = len(digest) * 8
print(f'雜湊值位元長度: {bit_length} 位元')

# 取得雜湊值的十六進位表示
# 這種格式更易於閱讀和傳輸
# 每個位元組會轉換為兩個十六進位字元
hex_digest = hash_function.hexdigest()
print(f'雜湊值(十六進位): {hex_digest}')

# 使用 update 方法可以分段處理大型資料
# 這對於處理大檔案特別有用
hash_function_2 = hashlib.sha256()
hash_function_2.update(b'first part ')
hash_function_2.update(b'second part')
print(f'分段計算的雜湊值: {hash_function_2.hexdigest()}')

密碼學雜湊函式與校驗和函式的差異

在討論雜湊函式時,很容易將密碼學雜湊函式與校驗和函式混淆,因為兩者都能產生固定長度的輸出,並且都可以用於檢測資料錯誤。然而,這兩類函式在設計目標和安全特性上存在根本性的差異,理解這些差異對於正確選擇適合的工具非常重要。

校驗和函式如 CRC-32 的設計目標是快速檢測資料傳輸或儲存過程中的意外錯誤,例如位元翻轉或資料截斷。這類函式的計算速度非常快,但並不具備密碼學安全性。攻擊者可以相對容易地構造出兩個不同的資料產生相同的 CRC 值,這種特性使得 CRC 完全不適合用於任何安全相關的應用。校驗和函式的典型應用場景包括網路封包的錯誤檢測、壓縮檔案的完整性驗證等。

相對地,密碼學雜湊函式的設計目標是抵抗各種攻擊,包括碰撞攻擊、預映像攻擊和第二預映像攻擊。這類函式的計算成本較高,但提供了強大的安全保證。在正確的使用情境下,攻擊者無法找到能夠產生相同雜湊值的不同輸入,也無法從雜湊值反推原始資料。以下範例展示了 CRC-32 校驗和函式的使用方式,以及它與密碼學雜湊函式的差異。

# 匯入 zlib 模組
# 該模組提供壓縮功能和校驗和計算
import zlib

# 建立一個包含重複內容的測試訊息
# 重複的內容可以展示壓縮的效果
message = b'this is repetitious content ' * 42

# 計算訊息的 CRC-32 校驗和
# CRC-32 產生一個 32 位元的校驗值
checksum = zlib.crc32(message)
print(f'CRC-32 校驗和: {checksum}')

# 壓縮訊息並解壓縮
# 展示校驗和在資料處理流程中的應用
compressed = zlib.compress(message)
decompressed = zlib.decompress(compressed)

# 驗證解壓縮後的資料是否與原始資料相同
# 透過比較 CRC-32 校驗和來進行驗證
is_valid = zlib.crc32(decompressed) == checksum
print(f'資料驗證結果: {is_valid}')

# 注意:CRC-32 不具備密碼學安全性
# 攻擊者可以構造碰撞
# 絕對不要用於安全相關的應用

安全金鑰的生成方法與最佳實務

金鑰的品質直接決定了整個認證系統的安全強度。一個理想的金鑰必須具備足夠的隨機性,使得攻擊者無法透過任何方法預測或猜測。在資訊理論中,我們用「熵」這個概念來衡量隨機性的程度。熵值越高,表示不確定性越大,金鑰也就越難被猜測。一般來說,用於密碼學目的的金鑰應該至少具備 128 位元的熵,這意味著攻擊者平均需要嘗試 2 的 128 次方種可能才能猜中正確的金鑰。

使用 os.urandom 生成隨機位元組

在 Python 中,傳統上使用 os.urandom 函式作為密碼學安全隨機數的來源。這個函式直接呼叫作業系統提供的密碼學安全隨機數生成器,在 Unix-like 系統中是從 /dev/urandom 讀取,在 Windows 系統中則使用 CryptGenRandom API。os.urandom 函式接受一個整數參數,指定要生成的隨機位元組數量,並返回對應數量的隨機位元組。

# 匯入 os 模組
import os

# 使用 os.urandom 生成 16 個隨機位元組
# 16 位元組等於 128 位元
# 這提供了足夠的安全強度
random_bytes = os.urandom(16)
print(f'隨機位元組: {random_bytes}')
print(f'位元組長度: {len(random_bytes)}')

# 將隨機位元組轉換為十六進位字串
# 這樣更容易閱讀和儲存
hex_string = random_bytes.hex()
print(f'十六進位表示: {hex_string}')

使用 secrets 模組的高階 API

Python 3.6 引入的 secrets 模組提供了更高階、更易用的 API 來生成密碼學安全的隨機數。這個模組是專門為安全應用設計的,提供了幾個便利的函式來生成不同格式的隨機值。相較於直接使用 os.urandom,secrets 模組的函式更加直觀易用,並且明確表達了其用途是安全相關的操作。

secrets 模組提供了三個主要的令牌生成函式:token_bytes 生成原始的隨機位元組,token_hex 生成隨機位元組的十六進位表示,token_urlsafe 則生成 URL 安全的 Base64 編碼。每個函式都接受一個可選的參數來指定要生成的位元組數量,如果不指定則使用預設值。這些函式的底層實作都是基於 os.urandom,因此具有相同的安全強度。

# 從 secrets 模組匯入令牌生成函式
from secrets import token_bytes, token_hex, token_urlsafe

# 生成 32 位元組的隨機金鑰(原始位元組格式)
# 32 位元組等於 256 位元的熵
# 適用於需要二進位金鑰的場景,如加密演算法
raw_key = token_bytes(32)
print(f'原始位元組金鑰: {raw_key}')
print(f'金鑰長度: {len(raw_key)} 位元組')

# 生成 32 位元組的隨機金鑰(十六進位格式)
# 輸出長度為 64 個字元
# 每個位元組轉換為 2 個十六進位字元
# 適用於需要純文字格式儲存的場景
hex_key = token_hex(32)
print(f'十六進位金鑰: {hex_key}')
print(f'字串長度: {len(hex_key)} 字元')

# 生成 32 位元組的隨機金鑰(URL 安全格式)
# 使用 Base64 編碼,但以 - 和 _ 取代 + 和 /
# 輸出只包含英數字和 -_ 符號
# 適用於需要在 URL 或檔案名稱中使用的場景
url_safe_key = token_urlsafe(32)
print(f'URL 安全金鑰: {url_safe_key}')

為什麼不能使用 random 模組

Python 的 random 模組使用的是梅森旋轉演算法(Mersenne Twister),這是一個高品質的偽隨機數生成器,適用於模擬、遊戲和統計抽樣等非安全應用。然而,這個演算法並不具備密碼學安全性,原因在於它的輸出是可預測的。

梅森旋轉演算法的內部狀態由 624 個 32 位元整數組成,總共是 19968 位元。攻擊者只需要觀察 624 個連續的輸出,就可以完全重建內部狀態,進而預測所有後續的輸出。這意味著如果應用程式使用 random 模組生成金鑰、令牌或其他安全敏感的值,攻擊者可能能夠透過觀察部分輸出來推斷這些值。因此,在任何安全相關的應用中,都必須使用 secrets 模組或 os.urandom,絕對不要使用 random 模組。

密碼短語的生成與應用

密碼短語是另一種形式的金鑰,由一串隨機選取的單詞組成,而非隨機的字元或位元組。相較於傳統的隨機金鑰,密碼短語更容易被人類記憶,同時仍能提供足夠的安全強度。這種方法的理論基礎是,即使每個單詞本身並不隨機,但從足夠大的字典中隨機選取足夠數量的單詞,其組合空間仍然大到無法被暴力破解。

以一個包含 10000 個單詞的字典為例,每個單詞的選取提供約 13.3 位元的熵(log2(10000) ≈ 13.29)。如果我們選取 5 個單詞,總共就有約 66.4 位元的熵,這已經提供了相當可觀的安全強度。如果選取 6 個單詞,則有約 79.7 位元的熵,足以抵抗大多數的暴力破解攻擊。

from pathlib import Path
import secrets
import math

# 從系統字典檔案載入單詞列表
# 在 macOS 和 Linux 系統上
# 這個檔案通常位於 /usr/share/dict/words
# 包含數萬個英文單詞
dict_path = Path('/usr/share/dict/words')
words = dict_path.read_text().splitlines()

# 過濾單詞列表
# 移除太短或太長的單詞
# 移除包含撇號或其他特殊字元的單詞
# 這樣可以產生更容易輸入的密碼短語
filtered_words = [
    word.lower() for word in words
    if 4 <= len(word) <= 8 and word.isalpha()
]
print(f'過濾後的字典大小: {len(filtered_words)} 個單詞')

# 使用 secrets.choice 從單詞列表中隨機選取
# 選取 6 個單詞組成密碼短語
# 以空格分隔各個單詞
word_count = 6
passphrase = ' '.join(
    secrets.choice(filtered_words)
    for _ in range(word_count)
)
print(f'生成的密碼短語: {passphrase}')

# 計算密碼短語的理論熵值
# 熵值 = log2(字典大小) * 單詞數量
entropy_per_word = math.log2(len(filtered_words))
total_entropy = entropy_per_word * word_count
print(f'每個單詞的熵: {entropy_per_word:.2f} 位元')
print(f'密碼短語總熵: {total_entropy:.2f} 位元')

隨機金鑰和密碼短語各有其適用的場景。隨機金鑰的優點是在相同長度下提供更高的熵,但缺點是難以記憶,因此適合用於不需要人類記憶的場景,例如系統間的 API 金鑰、自動化流程的認證令牌、或是臨時性的重設密碼連結。密碼短語的優點是容易記憶和輸入,適合用於需要使用者經常輸入的場景,例如登入密碼、加密金鑰或 SSH 金鑰的保護密碼。

金鑰雜湊函式的運作原理

金鑰雜湊函式是一種接受額外金鑰參數的雜湊函式,其設計目的是同時提供資料完整性驗證和資料來源認證。與普通雜湊函式不同,金鑰雜湊函式的輸出不僅取決於輸入訊息的內容,還取決於所使用的金鑰。這個特性意味著,即使兩個人有相同的訊息,如果他們使用不同的金鑰,產生的雜湊值也會完全不同。反過來說,只有知道正確金鑰的人才能為特定訊息產生正確的雜湊值,這就提供了資料來源認證的能力。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

rectangle "金鑰雜湊函式運作流程" {
  rectangle "輸入訊息" as msg
  rectangle "密鑰" as key
  rectangle "金鑰雜湊演算法" as algo
  rectangle "訊息認證碼" as mac
}

msg --> algo
key --> algo
algo --> mac

note bottom of algo
  金鑰雜湊函式的核心特性:
  相同訊息 + 相同金鑰 → 相同認證碼
  相同訊息 + 不同金鑰 → 不同認證碼
  不同訊息 + 相同金鑰 → 不同認證碼
end note

@enduml

BLAKE2 金鑰雜湊實作

BLAKE2 是一個現代的密碼學雜湊函式,由 Jean-Philippe Aumasson 等人在 2012 年設計。它是 BLAKE 的改進版本,而 BLAKE 是 SHA-3 競賽的五個最終候選者之一。BLAKE2 的設計目標是在保持高安全性的同時,提供比 MD5 和 SHA-1 更快的計算速度。BLAKE2 有兩個主要變體:BLAKE2b 針對 64 位元平台優化,可產生最多 64 位元組的雜湊值;BLAKE2s 針對 32 位元平台優化,可產生最多 32 位元組的雜湊值。

BLAKE2 的一個重要特點是原生支援金鑰輸入參數,這使得它可以直接用作金鑰雜湊函式,而不需要額外的包裝機制。以下範例展示了如何使用 BLAKE2 進行金鑰雜湊,以及金鑰如何影響雜湊輸出。

# 從 hashlib 匯入 blake2b 雜湊函式
from hashlib import blake2b

# 定義要進行雜湊的訊息
message = b'This is a secret message'

# 定義兩個不同的金鑰
# 金鑰長度最多可以是 64 位元組
key_alice = b'alice_secret_key_12345'
key_bob = b'bob_secret_key_67890'

# 使用 Alice 的金鑰對訊息進行雜湊
# 產生的雜湊值只有知道 Alice 金鑰的人才能產生
hash_alice_1 = blake2b(message, key=key_alice).digest()
hash_alice_2 = blake2b(message, key=key_alice).digest()

# 驗證相同金鑰產生相同雜湊值
print(f'相同金鑰產生相同雜湊值: {hash_alice_1 == hash_alice_2}')

# 使用 Bob 的金鑰對相同訊息進行雜湊
# 由於金鑰不同,產生的雜湊值也會不同
hash_bob = blake2b(message, key=key_bob).digest()

# 驗證不同金鑰產生不同雜湊值
print(f'不同金鑰產生不同雜湊值: {hash_alice_1 != hash_bob}')

# 展示雜湊值的十六進位表示
print(f'Alice 的雜湊值: {hash_alice_1.hex()[:32]}...')
print(f'Bob 的雜湊值: {hash_bob.hex()[:32]}...')

改進 Alice 的檔案管理系統

現在我們可以利用金鑰雜湊函式來改進 Alice 的檔案管理系統,使其能夠抵抗 Mallory 的攻擊。改進後的系統在計算雜湊值時會使用一個只有 Alice 知道的密鑰。即使 Mallory 取得了檔案系統的寫入權限並修改了檔案,她也無法在不知道金鑰的情況下產生正確的金鑰雜湊值。當 Alice 執行驗證程序時,系統會使用 Alice 的金鑰重新計算雜湊值,這個值會與 Mallory 提供的假雜湊值不符,Alice 就能夠檢測到檔案已被竄改。

import hashlib
from pathlib import Path
import hmac

def store_file(path, data, key):
    """
    儲存資料及其金鑰雜湊值到檔案系統

    這個函式會將資料寫入指定路徑
    並在相同位置建立一個副檔名為 .mac 的檔案
    儲存資料的訊息認證碼

    參數:
        path: 資料檔案的路徑
        data: 要儲存的位元組資料
        key: 用於計算訊息認證碼的金鑰
    """
    # 建立資料檔案和認證碼檔案的路徑物件
    data_path = Path(path)
    mac_path = data_path.with_suffix('.mac')

    # 使用 BLAKE2b 計算資料的金鑰雜湊值
    # digest_size 參數指定輸出長度為 32 位元組
    # 這與 SHA-256 的輸出長度相同
    mac_value = hashlib.blake2b(
        data,
        key=key,
        digest_size=32
    ).hexdigest()

    # 寫入資料檔案和認證碼檔案
    # 使用 write_bytes 寫入二進位資料
    # 使用 write_text 寫入文字格式的認證碼
    data_path.write_bytes(data)
    mac_path.write_text(mac_value)

    print(f'檔案已儲存: {data_path}')
    print(f'認證碼已儲存: {mac_path}')

def verify_file(path, key):
    """
    驗證檔案是否被修改

    這個函式會讀取指定路徑的檔案
    重新計算其訊息認證碼
    並與儲存的認證碼進行比較

    參數:
        path: 資料檔案的路徑
        key: 用於計算訊息認證碼的金鑰

    返回:
        如果檔案未被修改則返回 True
        如果檔案已被修改則返回 False
    """
    # 建立資料檔案和認證碼檔案的路徑物件
    data_path = Path(path)
    mac_path = data_path.with_suffix('.mac')

    # 讀取儲存的資料和原始認證碼
    data = data_path.read_bytes()
    stored_mac = mac_path.read_text()

    # 重新計算資料的訊息認證碼
    computed_mac = hashlib.blake2b(
        data,
        key=key,
        digest_size=32
    ).hexdigest()

    # 使用 hmac.compare_digest 進行安全比較
    # 這可以防禦時序攻擊
    is_valid = hmac.compare_digest(computed_mac, stored_mac)

    if is_valid:
        print(f'檔案驗證成功: {data_path}')
    else:
        print(f'警告:檔案可能已被竄改: {data_path}')

    return is_valid

HMAC:通用的金鑰雜湊解決方案

雖然 BLAKE2 原生支援金鑰輸入,但大多數雜湊函式並沒有這個功能。例如,SHA-256 這個最廣泛使用的雜湊函式就只接受訊息作為輸入,沒有金鑰參數。為了解決這個問題,密碼學家 Mihir Bellare、Ran Canetti 和 Hugo Krawczyk 在 1996 年提出了 HMAC(Hash-based Message Authentication Code)機制。HMAC 是一種通用的方法,可以將任何普通的密碼學雜湊函式轉換為金鑰雜湊函式。

HMAC 的運作方式是將金鑰與訊息以特定的方式組合,然後通過底層的雜湊函式進行處理。具體來說,HMAC 會對金鑰進行兩次處理:一次與內部填充值異或後與訊息連接,另一次與外部填充值異或後與第一次雜湊的結果連接。這種雙重處理的設計確保了 HMAC 具有強大的安全特性,即使底層雜湊函式存在某些弱點,HMAC 仍然可能是安全的。

HMAC 函式接受三個輸入:要認證的訊息、用於認證的金鑰,以及要使用的底層雜湊函式。輸出是一個訊息認證碼,其長度與底層雜湊函式的輸出長度相同。Python 的 hmac 模組提供了 HMAC 功能的標準實作。

import hashlib
import hmac

# 建立 HMAC-SHA256 物件
# 第一個參數是金鑰(必須是位元組)
# msg 參數是要認證的訊息
# digestmod 參數指定底層使用的雜湊函式
hmac_sha256 = hmac.new(
    key=b'my_secret_key',
    msg=b'message to authenticate',
    digestmod=hashlib.sha256
)

# 取得訊息認證碼的原始位元組表示
mac_bytes = hmac_sha256.digest()
print(f'認證碼(位元組): {mac_bytes}')

# 取得訊息認證碼的十六進位表示
# 這種格式更適合儲存和傳輸
mac_hex = hmac_sha256.hexdigest()
print(f'認證碼(十六進位): {mac_hex}')

# 查詢認證碼的大小
# HMAC-SHA256 的輸出是 32 位元組
mac_size = hmac_sha256.digest_size
print(f'認證碼大小: {mac_size} 位元組')

# 使用 update 方法可以分段處理大型訊息
hmac_obj = hmac.new(b'key', digestmod=hashlib.sha256)
hmac_obj.update(b'first part ')
hmac_obj.update(b'second part')
print(f'分段計算的認證碼: {hmac_obj.hexdigest()}')

完整的訊息認證流程

讓我們透過一個完整的範例來展示 HMAC 在訊息認證中的應用。假設 Bob 要發送一則訊息給 Alice,他們事先已經透過安全的方式交換了一個共享金鑰。Bob 使用這個金鑰對訊息計算 HMAC,並將訊息和 HMAC 值一起發送給 Alice。Alice 收到後,使用相同的金鑰重新計算 HMAC,並與 Bob 發送的 HMAC 值進行比較。如果兩者相符,Alice 就可以確信訊息確實來自 Bob(因為只有 Bob 知道金鑰),並且訊息在傳輸過程中沒有被修改。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

actor "Bob(發送方)" as bob
actor "Alice(接收方)" as alice
participant "HMAC 函式" as hmac
database "傳輸通道" as channel

bob -> bob : 準備訊息
bob -> hmac : 訊息 + 共享金鑰
hmac -> bob : 訊息認證碼
bob -> channel : 訊息 + 認證碼
channel -> alice : 訊息 + 認證碼
alice -> hmac : 收到的訊息 + 共享金鑰
hmac -> alice : 重新計算的認證碼
alice -> alice : 比較兩個認證碼

note over alice
  認證碼相符:
  訊息完整且來自 Bob

  認證碼不符:
  訊息已被竄改或非 Bob 發送
end note

@enduml

以下是完整的 Python 實作,展示了發送方和接收方如何使用 HMAC 進行訊息認證。

import hashlib
import hmac
import json
from typing import Optional

def send_authenticated_message(shared_key: bytes, message: bytes) -> bytes:
    """
    使用共享金鑰對訊息進行認證並打包

    這個函式會計算訊息的 HMAC-SHA256 認證碼
    並將訊息和認證碼打包成 JSON 格式
    便於透過網路傳輸

    參數:
        shared_key: 發送方和接收方共享的密鑰
        message: 要發送的訊息(位元組格式)

    返回:
        包含訊息和認證碼的 JSON 編碼資料
    """
    # 建立 HMAC-SHA256 物件
    # 使用共享金鑰和 SHA-256 雜湊函式
    hmac_obj = hmac.new(
        key=shared_key,
        msg=message,
        digestmod=hashlib.sha256
    )

    # 取得認證碼的十六進位表示
    mac_value = hmac_obj.hexdigest()

    # 將訊息和認證碼打包成字典
    # 訊息轉換為列表以便 JSON 序列化
    authenticated_msg = {
        'message': list(message),
        'mac': mac_value,
    }

    # 序列化為 JSON 並編碼為位元組
    return json.dumps(authenticated_msg).encode('utf-8')

def receive_authenticated_message(
    shared_key: bytes,
    data: bytes
) -> Optional[bytes]:
    """
    驗證收到的已認證訊息

    這個函式會解析收到的資料
    重新計算訊息的認證碼
    並與收到的認證碼進行比較

    參數:
        shared_key: 發送方和接收方共享的密鑰
        data: 收到的 JSON 編碼資料

    返回:
        如果驗證成功則返回訊息內容
        如果驗證失敗則返回 None
    """
    # 解析 JSON 資料
    authenticated_msg = json.loads(data.decode('utf-8'))

    # 還原訊息的位元組格式
    message = bytes(authenticated_msg['message'])
    received_mac = authenticated_msg['mac']

    # 使用相同的金鑰重新計算認證碼
    hmac_obj = hmac.new(
        key=shared_key,
        msg=message,
        digestmod=hashlib.sha256
    )
    computed_mac = hmac_obj.hexdigest()

    # 使用 compare_digest 進行安全比較
    # 這可以防禦時序攻擊
    if hmac.compare_digest(computed_mac, received_mac):
        print('訊息認證成功')
        print(f'訊息內容: {message.decode("utf-8")}')
        return message
    else:
        print('警告:訊息認證失敗')
        print('訊息可能已被竄改或來源不可信')
        return None

# 使用範例
# 假設 Alice 和 Bob 已經透過安全方式交換了共享金鑰
shared_key = b'this_is_a_shared_secret_key_32b!'
message = b'Hello Alice, this is Bob!'

# Bob 發送已認證的訊息
print('=== Bob 發送訊息 ===')
outbound_data = send_authenticated_message(shared_key, message)

# Alice 接收並驗證訊息
print('\n=== Alice 驗證訊息 ===')
verified_message = receive_authenticated_message(shared_key, outbound_data)

時序攻擊的威脅與防禦策略

時序攻擊是一種側通道攻擊,攻擊者透過精確測量系統執行特定操作所需的時間來推斷敏感資訊。這種攻擊利用的是系統實作中的時間洩漏,而非密碼學演算法本身的弱點。在訊息認證碼驗證的情境中,時序攻擊是一個特別需要注意的威脅。

時序攻擊的原理

當我們比較兩個字串是否相等時,最直觀的方法是逐個字元進行比較,一旦發現不相符的字元就立即返回 False。這種「短路」比較方式在一般應用中是合理的優化,因為它避免了不必要的比較操作。然而,在安全應用中,這種實作方式會洩漏資訊給攻擊者。

假設攻擊者想要偽造一個有效的認證碼。他可以先猜測認證碼的第一個字元,並觀察系統的回應時間。如果猜對了第一個字元,系統會繼續比較第二個字元,這會花費稍微多一點的時間。攻擊者可以透過統計大量請求的回應時間,來判斷第一個字元是否猜對。一旦確定了第一個字元,攻擊者就可以用同樣的方法猜測第二個字元,依此類推。理論上,攻擊者可以在線性時間內猜出完整的認證碼,而不需要嘗試所有可能的組合。

固定時間比較的實作

為了防禦時序攻擊,我們需要使用固定時間複雜度的比較函式。這種函式不管兩個輸入是否相同,都會執行相同數量的操作,花費相同的時間。Python 的 hmac 模組提供了 compare_digest 函式,這個函式實作了固定時間比較。

compare_digest 函式的內部實作會遍歷兩個輸入的所有字元,即使在中間發現不相符也不會提前返回。它會記錄是否發現任何不相符的字元,並在比較完所有字元後才返回最終結果。這種實作方式確保了比較操作的時間只取決於輸入的長度,而不取決於輸入的內容,從而消除了時間洩漏。

import hmac
import time

def insecure_compare(a: str, b: str) -> bool:
    """
    不安全的字串比較(僅用於展示,請勿在實際應用中使用)

    這個函式使用短路比較
    一旦發現不相符就立即返回
    這會導致時間洩漏
    """
    if len(a) != len(b):
        return False
    for i in range(len(a)):
        if a[i] != b[i]:
            return False
    return True

def secure_compare(a: str, b: str) -> bool:
    """
    安全的字串比較

    使用 hmac.compare_digest 進行固定時間比較
    無論輸入是否相同,比較時間都相同
    這可以防禦時序攻擊
    """
    return hmac.compare_digest(a, b)

# 展示時序攻擊的原理
# 注意:實際的時序攻擊需要更精確的時間測量
# 以及大量的樣本來消除雜訊
correct_mac = 'a1b2c3d4e5f6g7h8'

# 測試不安全比較的時間差異
test_cases = [
    'x1b2c3d4e5f6g7h8',  # 第一個字元不同
    'a1x2c3d4e5f6g7h8',  # 第三個字元不同
    'a1b2c3d4e5f6g7x8',  # 倒數第二個字元不同
    'a1b2c3d4e5f6g7h8',  # 完全相同
]

print('不安全比較的時間測量(僅供展示):')
for test in test_cases:
    start = time.perf_counter_ns()
    for _ in range(10000):
        insecure_compare(correct_mac, test)
    elapsed = time.perf_counter_ns() - start
    print(f'  {test}: {elapsed} ns')

print('\n安全比較(使用 compare_digest):')
for test in test_cases:
    start = time.perf_counter_ns()
    for _ in range(10000):
        secure_compare(correct_mac, test)
    elapsed = time.perf_counter_ns() - start
    print(f'  {test}: {elapsed} ns')

其他需要注意的時序洩漏

除了字串比較之外,還有其他操作可能會導致時序洩漏。例如,查詢資料庫時,如果使用者名稱不存在和密碼錯誤的回應時間不同,攻擊者就可以用時序攻擊來列舉有效的使用者名稱。為了防禦這類攻擊,系統應該確保不管認證失敗的原因是什麼,回應時間都相同。

另一個常見的時序洩漏來源是條件分支。如果程式根據敏感資料的值來決定執行不同的程式碼路徑,而這些路徑的執行時間不同,就會產生時序洩漏。在密碼學實作中,通常會使用「恆定時間」的程式設計技巧來避免這類問題,例如使用位元運算來取代條件分支。

實務應用與最佳實務

在實際應用中,金鑰雜湊函式和 HMAC 被廣泛用於各種安全相關的場景。以下是一些常見的應用案例和相應的最佳實務建議。

API 認證

許多 Web API 使用 HMAC 來認證請求。客戶端在發送請求時,會使用 API 金鑰對請求內容(包括 HTTP 方法、URL、標頭和主體)計算 HMAC,並將認證碼包含在請求標頭中。伺服器收到請求後,使用相同的金鑰重新計算 HMAC,並與客戶端提供的認證碼進行比較。這種方式確保了請求確實來自擁有正確 API 金鑰的客戶端,並且請求內容沒有被竄改。

JWT 簽章

JSON Web Token(JWT)是一種常用的身份驗證和資訊交換格式。JWT 可以使用 HMAC-SHA256(稱為 HS256)進行簽章,確保令牌的內容沒有被修改。然而,需要注意的是,使用對稱金鑰(HMAC)簽章的 JWT 需要在發行方和驗證方之間共享金鑰,這在某些情況下可能不適合。對於需要分離發行方和驗證方的場景,應該使用非對稱簽章(如 RS256)。

金鑰管理建議

在實際部署中,金鑰管理是整個系統安全性的關鍵。以下是一些重要的建議:金鑰應該定期輪換,以限制金鑰洩漏的影響範圍。金鑰不應該硬編碼在程式碼中,而應該儲存在安全的金鑰管理系統中。在分散式系統中,應該使用金鑰衍生函式從主金鑰衍生出各種用途的子金鑰,而不是到處使用同一個金鑰。此外,不再使用的金鑰應該安全地銷毀,避免留下可被攻擊者利用的痕跡。

結語

金鑰雜湊函式和 HMAC 提供了強大的機制,能夠同時確保資料的完整性和驗證資料來源。透過引入只有授權方才知道的密鑰,攻擊者即使能夠存取和修改資料,也無法產生正確的認證碼。本文從密碼學雜湊函式的基礎出發,深入探討了安全金鑰的生成方法、金鑰雜湊函式的運作原理、HMAC 機制的實作,以及時序攻擊的防禦策略。

在實作資料認證系統時,開發者需要注意幾個關鍵要點。首先是金鑰的品質,必須使用密碼學安全的隨機數生成器來產生金鑰,絕對不能使用可預測的偽隨機數生成器。其次是比較操作的安全性,必須使用固定時間複雜度的比較函式來防禦時序攻擊。第三是金鑰的管理,包括安全的儲存、定期的輪換,以及適當的存取控制。

掌握這些概念和技術後,開發者將能夠建立更加安全可靠的資料驗證系統,有效保護重要資訊免於未授權的存取和竄改。隨著資訊安全威脅的不斷演進,持續學習和更新安全知識也是每個開發者的重要責任。