\( \def\vector#1{\boldsymbol{#1}} \)

ハッシュ関数

Takami Torao #SHA3 #SHAKE
  • このエントリーをはてなブックマークに追加

概要

ハッシュ関数 (hash function) は任意長のビット列をある固定の長さのビット列に変換する関数。この操作は \(h\) は数学記号で \(h: \{0,1\}^* \to \{0,1\}^k=\{0,\ldots,2^k-1\}\) と記述することができる。ここで出力ビット長 \(k\) は 256 や 512 といった比較的小さな値である。

あるハッシュ関数 \(h\) において異なる \(x_1\) と \(x_2\) に対して \(h(x_1) = h(x_2)\) が起きることを衝突 (collision) と呼ぶ。強衝突耐性 (strong collision resistance) は衝突するような \(x_1\) と \(x_2\) を見つけることが困難であることを意味し、弱衝突耐性 (weak collision resistance) は特定の入力 \(x_1\) に対して \(h(x_1)=h(x_2)\) となるような \(x_2\) を見つけることが困難であることを意味する。

Table of Contents

  1. 概要
    1. ユニバーサルハッシュ関数族
    2. ドメイン分離
    3. ハッシュ関数の独立性
  2. 暗号論的ハッシュ関数
    1. 攻撃耐性
  3. SHA-3
    1. 可変長出力ハッシュ関数
    2. ドメイン分離ハッシュ関数
    3. タプル型ハッシュ関数
  4. パフォーマンス評価
  5. 参考文献

ユニバーサルハッシュ関数族

ハッシュ関数 \(h\) の入力が取り得るすべての値の集合をユニバース (universe) と呼び \(U\) で表す。つまり \(h: U \to \{0,1\}^k\) である。ユニバースは大規模な有限集合または無限集合を暗示しており、例えば任意長のビット列を入力できるのであれば無限集合 \(U = \{0,1\}^*\) を示している。

定義: ハッシュ関数 \(h:U \to \{0,\ldots,m-1\}\) を構築するランダムアルゴリズム \(\mathcal{H}\) は、\(U\) 内の任意の値 \(x_1 \ne x_1\) について式 (\(\ref{prob}\)) が成り立つときユニバーサル (universal) である。\[ \begin{equation} \underset{h \leftarrow H}{\rm Pr}\left[ h(x_1) = h(x_2) \right] \le \frac{1}{m} \label{prob} \end{equation} \] また \(h \in \mathcal{H}\) をランダムに選択する手続きがユニバーサルであれば、ハッシュ関数の集合 \(\mathcal{H}\) はユニバーサルハッシュ関数族 (universal hash function family) であると言う。

\(U\) に対して我々が実際に扱う入力の有限集合を \(S\) とする。つまり \(S \subseteq U\) である。その大きさを \(n = |S| \leq |U|\) とする。

定理: \(\mathcal{H}\) がユニバーサルであれば、サイズ \(n\) の任意の集合 \(S \subseteq U\) について、もしハッシュ関数 \(h\) が \(\mathcal{H}\) にしたがってランダムに構築されたなら、\(x \in S\) と衝突する \(S\) 内の他の要素の個数は多くて \(n/m\) である [3]。

集合 \(S\) に対して衝突が完全に発生しないとき、ハッシュ関数は完全 (perfect) であると言う。\(\mathcal{H}\) がユニバーサルで \(m=n^2\) のとき、\(\mathcal{H}\) からランダムに選ばれたハッシュ関数 \(h\) において \(S\) 上で衝突が発生しない確率は \(1/2\) 以上である [3]。

ドメイン分離

実用の観点では、ハッシュ関数は同じアルゴリズムに基づいていても目的によって異なる変種 (variant) を使用すべきである。例えば同じ入力値 "secret" に対してユーザ ID 用途のハッシュ関数 \(h_u\) とパスワード用途とのハッシュ関数 \(h_p\) では異なるハッシュ値を生成することが望ましい。このような方法をドメイン分離 (domain separation) と呼ぶ。

単純な例では、共通のハッシュ関数 \(\mathcal{H}\) (例えば SHA3-256 など) を拡張してすべての入力に対してドメインごとに異なる固定値を付加してドメイン分離されたハッシュ関数を生成する方法が考えられる。\[ \begin{eqnarray*} h_u(x) & = & \mathcal{H}(x\,||\,\text{"u"}) \\ h_p(x) & = & \mathcal{H}(x\,||\,\text{"p"}) \\ \end{eqnarray*} \] Keccak を使用する cSHAKE ではドメイン分離の機能が標準化されている。また Salt を使用したパスワードのハッシュ化は Salt によってユーザごとにドメインを分離していると考えることができる。

ドメイン分離は \(\mathcal{H}\) というハッシュ関数の集合 (ハッシュ関数族) から \(h_u\) や \(h_p\) といったハッシュ関数を選択していると考えることができる。

ハッシュ関数の独立性

ハッシュ関数における X-wise independent とは、ハッシュ関数の出力が特定の変数 (入力) に対して独立であることを表している。この独立性は X においてハッシュ値が他の値に影響されずランダムに分布することを保証している。

\(k\)-wise independent は、任意の \(k\) 個のハッシュ関数によって生成されるハッシュ値がすべて互いに独立であることを示している。現実的な実装例としては式 (\(\ref{kwise}\)) のように生成された \(k\) 個のハッシュ関数が挙げられる。\[ \begin{equation} h_i(x) = {\rm SHA3\_512}(x \ || \ i) \label{kwise} \end{equation} \] 一方、式 (\(\ref{notpairwise}\)) のように構築された 2 つのハッシュ関数は互いに独立でないことから pair-wise independent (2-wise independent) ではない。\[ \begin{equation} \left\{ \begin{array}{rcl} h_1(x) & = & {\rm SHA3\_512}(x) \\ h_2(x) & = & h_1(x) \oplus {\rm SHA3\_512}(h_1(x)) \end{array} \right. \label{notpairwise} \end{equation} \] 他には min-wise independent はさまざまな入力に対するハッシュ値を比較してそれらの最小値を得たとき、その最小値が独立であることを意味している。このように X-wise independent はハッシュ関数が X において独立であることを示している。

このようなハッシュ関数の独立性は、アルゴリズムで使用する (ユニバーサルハッシュ関数族から任意の選択された) ハッシュ関数が保証すべき独立性の下限を指定するためにしばしば使われる。

暗号論的ハッシュ関数

セキュリティの文脈で以下の特性を満たすハッシュ関数を暗号論的ハッシュ関数 (cryptographically-secure hash function) と呼ぶ。

  • 原像耐性 (preimage resistance) - ハッシュ関数 \(h\) のある出力 \(y\) が与えられたとき、\(y=h(x)\) となるような値 \(x \in \{0,1\}^*\) を見つけることが計算上不可能であること。
  • 第二原像耐性 (2nd preimage resistance) (弱衝突耐性) - ハッシュ関数へのある入力 \(x_1\) が与えられたとき、\(h(x_1)=h(x_2)\) となるような値 \(x_2 \in \{0,1\}^*\) を見つけることが計算上不可能であること。
  • 衝突耐性 (collision resistance) (強衝突耐性) - \(h(x_1)=h(x_2)\) となるような 2 つの値 \(x_1\), \(x_2\) を見つけることが計算上不可能であること。

原像耐性 (第一、第二) と衝突耐性 (弱衝突、強衝突) は似ているが、原像耐性が「与えられたある値 \(x\) に対して」であるのに対して衝突耐性は「任意の値 \(x\) に対して」であるという点で異なる。

\(k\) ビットの理想的な暗号論的ハッシュ関数に対して原像攻撃を成功させるには \(O(2^k)\) 回の試行を行う必要がある。しかし、同等の確率で \(h(x_1)=h(x_2)\) となるような衝突攻撃を成功させるためには、バースデーパラドックス (birthday paradox) と同じ理由により、より少ない \(O(2^{k/2})\) 回程度の試行で済む。言い換えると、出力値が \(k\) ビットの理想的な暗号論的ハッシュ関数は \(k/2\) ビットセキュリティを持つ

攻撃耐性

コンピュータの性能向上や新しいアルゴリズムの報告などによって、かつて暗号論的ハッシュ関数として扱われていたアルゴリズムのいくつかはすでに安全ではないと認識されている。MD2 は早い時期に原像攻撃と衝突攻撃の両方が見つかっている。MD5 も同様に現在では短時間で衝突を発見することができており、また SHA-1 は \(2^{63}\) 程度の操作で衝突が報告されている。

\(y=h(x)\) となるようなハッシュ値 \(y\) が与えられたとき、ある入力 \(x'\) に対して \(y'=h(x \ || \ x')\) となるような \(y'\) が計算可能であることを利用した攻撃を伸長攻撃 (length extension attach) と呼ぶ。MD5, SHA-1, RIPEMD-160, Whirlpool, SHA-2 (SHA-224~512) など多くの暗号論的ハッシュ関数は Merkle–Damgård 構成法に基づいているが、Merkle–Damgård 構成法の完全な出力を使用すると伸長攻撃に対して脆弱となる (ただし SHA-2 は秘密をハッシュ化しないケースであれば今でも広く使われている)。SHA-3 や BLAKE2, BLAKE3、または SHA-2 でも出力の切り捨てを伴うタイプは伸長攻撃に対して安全である。

SHA-3

SHA-3 (secure hash algorithm 3) は FIPS 202 [1] で NIST によって標準化された暗号論的ハッシュ関数である。これは Keccak(ケッチャック)と呼ばれるハッシュ関数に基づいており、SHA-3 策定時に指摘されたいくつかのセキュリティの強化やパフォーマンスの向上の変更が加えられている (したがって選択可能な場合は当初の Keccak-f よりも SHA-3 を使用すべきである)。\[ \left\{ \begin{eqnarray*} \mbox{SHA3-224}(M) & = & \mbox{Keccak}[448](M\,||\,{\tt 01},224) \\ \mbox{SHA3-256}(M) & = & \mbox{Keccak}[512](M\,||\,{\tt 01},256) \\ \mbox{SHA3-384}(M) & = & \mbox{Keccak}[768](M\,||\,{\tt 01},384) \\ \mbox{SHA3-512}(M) & = & \mbox{Keccak}[1024](M\,||\,{\tt 01},512) \end{eqnarray*} \right. \] SHA-3 の基となる Keccak は、入出力が固定長のハッシュ化基礎関数である Keccak-p と、そのような関数の入出力を任意長に拡張するフレームワークのスポンジ構造の 2 つで構成されている。ここで:

  • \(\mbox{Keccak}[c](N,d) = \mbox{SPONGE}[\mbox{Keccak-}p[1600,24],\mbox{pad10*1},1600-c](N,d)\)

  • \(\mbox{SPONGE}[f,\mbox{pad},r](N,d)\) は基礎関数 \(f\) とパディング関数 \(\mbox{pad}\)、レート \(r\) に基づいて任意長の入力 \(N\) から長さ \(d\) のビット列を生成するスポンジ構造

  • \(\mbox{Keccak-}p[b,n_r](S)\) は長さ \(b\) のビット列 \(S\) を転置・置換して同じ長さ \(b\) のビット列を出力する基礎関数 (\(n_r\) は内部的な反復回数)

  • \(\mbox{pad10*1}(x,m)\) は長さ \(m\) のビット列に対して長さが \(x\) の整数倍となるように付加すべきビット列を生成するパディング関数

である。Keccak のスポンジ構造は任意長の出力を生成することができるが、SHA-3 では出力長を固定値として使用している。

また cSHAKE, KMAC, TupleHash などの SHA-3 (Keccak) 派生関数が NIST によって標準化されている [2]。

可変長出力ハッシュ関数

SHA-3 の可変長出力関数 (XOF; extendable-output function) である派生関数 SHAKE ("secure hash algorithm" with "KECCAK") が FIPS 202 [1] で標準化されている。これは特定の長さの乱数列や鍵を生成するときに余計な考慮を省略でき Keccak の転置関数を利用し高速に生成することのできるため安全で利便性が高い。\[ \left\{ \begin{eqnarray*} \mbox{SHAKE128}(M,d) & = & \mbox{Keccak}[256](M\,||\,{\tt 1111},d) \\ \mbox{SHAKE256}(M,d) & = & \mbox{Keccak}[512](M\,||\,{\tt 1111},d) \end{eqnarray*} \right. \]

ドメイン分離ハッシュ関数

NIST SP 800-185 [2] で仕様化されている cSHAKE は SHAKE をドメイン分離が可能なように拡張したもので、基礎に Keccak プリミティブを使ったハッシュ関数の変種を生成することができる。例えば cSHAKE128 では以下のように適用される。\[ \left\{ \begin{eqnarray*} \mbox{cSHAKE128}(M,d,"","") & = & \mbox{SHAKE128}(M,d) \\ \mbox{cSHAKE128}(M,d,N,S) & = & \mbox{Keccak}[256](\mbox{bytepad}(\mbox{encode_string}(N)\,|| \\ & & \hspace{.4cm} \mbox{encode_string}(S),168))\,||\,M\,||\,{\tt 00},d) \\ \end{eqnarray*} \right. \] ここで \(N\) は長さ 2040 未満のビット列で NIST の定義する関数名を表す (cSHAKE として使用するのであれば長さ 0 のビット列でよい)。\(S\) は長さ 2040 未満のビット列でユーザがカスタマイズ可能なドメインを指定する。\(N_1 \ne N_2\) または \(S_1 \ne S_2\) であれば双方の cSHAKE 出力に関連性はない。

タプル型ハッシュ関数

複数の値から一つのハッシュ値を生成する場合、単純に文字列を連結した値をハッシュ化する方法では衝突を発生させやすく安全ではない。例えば宛先 addr と金額 amt からなるトランザクションをハッシュ化するケースで単純に \(h({\tt addr}\,||\,{\tt amt})\) のようにすると、addr=123 宛ての amt=20 送金と、addr=12 宛ての amt=320 送金で同じハッシュ値 \(h(\mbox{"12320"})\) が生成されることになる。

cSHAKE と共に標準化された TupleHash は、このような複数の値からなるタプルをハッシュ化するための標準である。TupleHash は cSHAKE に基づいて複数の値リストから曖昧さのない方法でハッシュ値を生成することができる。

パフォーマンス評価

Table 1 は、すべてのバイト値が 0 に初期化されている 1MB のバイト配列を入力 \(x\) とし、それぞれのアルゴリズムを適用してハッシュ値 \(y=h(x)\) の算出にかかった時間を表している。ここでは比較のため単純なチェックサムの CRC32 も含めている。

アルゴリズム 算出時間 [μsec/1MB] (±95%信頼区間) 出力サイズ \(k\) 🔒
CRC32 1,821.801 (±1.7%) 32-bit (4B)
MurmurHash64A 171.591 (±2.3%) 64-bit (8B)
MurmurHash3-128 (x64) 499.188 (±2.0%) 128-bit (16B)
aHash 65.593 (±3.6%) 64-bit (8B)
MD2 74,384.134 (±0.7%) 128-bit (16B)
MD5 1,316.656 (±0.3%) 128-bit (16B)
MD6 5,358.656 (±1.9%) 512-bit (64B) 🌞
SHA-1 428.569 (±0.3%) 160-bit (20B)
RIPEMD-160 2,520.825 (±0.9%) 160-bit (20B)
RIPEMD-320 2,169.878 (±2.2%) 320-bit (40B)
Whirlpool 5,922.141 (±0.9%) 512-bit (64B)
SHA-224 456.999 (±0.3%) 224-bit (28B)
SHA-256 456.920 (±0.5%) 256-bit (32B)
SHA-386 1,364.148 (±4.5%) 384-bit (48B)
SHA-512 1,367.632 (±4.5%) 512-bit (64B)
SHA-512/224 1,364.047 (±2.5%) 224-bit (28B) 🌞
SHA-512/256 1,365.937 (±2.7%) 256-bit (32B) 🌞
SHA3-224 1,964.872 (±4.1%) 224-bit (28B) 🌞
SHA3-256 2,069.398 (±1.7%) 256-bit (32B) 🌞
SHA3-384 2,730.082 (±1.7%) 384-bit (48B) 🌞
SHA3-512 3,949.316 (±3.6%) 512-bit (64B) 🌞
SHAKE128 1,675.281 (±1.0%) 512-bit (64B) 🌞
SHAKE256 2,061.755 (±1.5%) 512-bit (64B) 🌞
Keccak-256 2,079.445 (±5.1%) 256-bit (32B) 🌞
Keccak-512 3,935.405 (±1.3%) 512-bit (64B) 🌞
BLAKE2b 731.625 (±3.4%) 512-bit (64B) 🌞
BLAKE2s 1,210.488 (±0.6%) 256-bit (32B) 🌞
BLAKE3 177.786 (±0.7%) 256-bit (32B) 🌞
SipHash 304.354 (±1.9%) 64-bit (8B) 🌞
SipHash128 318.930 (±13.4%) 128-bit (16B) 🌞
HighwayHash64 55.596 (±0.7%) 64-bit (8B) 🌞
HighwayHash128 55.643 (±0.8%) 128-bit (16B) 🌞
HighwayHash256 56.187 (±2.4%) 256-bit (32B) 🌞
Table 1. アルゴリズムの公開されているハッシュ関数のパフォーマンス。
# AMD Ryzen 7 5700X 8C/16T 3.4GHz + Windows 11 Pro
> rustc +nightly --version
rustc 1.74.0-nightly (58e967a9c 2023-09-03)

計測の結果から AVX/SSE 拡張命令を使用する HighwayHash や AES 拡張命令を使用する aHash が高いパフォーマンスを持つことが分かる。特に Ryzen 5700X と Core i7-8569U での SHA-1, SHA-224, SHA-256 の速度差は CPU が Intel SHAX 拡張命令をサポートしているかの違いによるものと思われる。また AVX 命令を持たない M1 (ARM64) では Ryzen や Core ほど HighwayHash の速度が伸びていないことが分かる。

この結果から近年のハッシュ関数の性能はハードウェアアクセラレーションを活用できるかが大きな鍵となっていることが分かる。

参考文献

  1. DWORKIN, Morris J. SHA-3 standard: Permutation-based hash and extendable-output functions. 2015.
  2. KELSEY, John; CHANG, Shu-jen; PERLNER, Ray. SHA-3 derived functions: cSHAKE, KMAC, TupleHash, and ParallelHash. NIST special publication, 2016, 800: 185.
  3. Avrim Blum, Manuel Blum. Universal and Perfect Hashing, CMU 15-451 Algorithms Lecture 10, 2011 Fall.