[暗号技術入門] メッセージ認証コード (MAC) と鍵導出関数

目次の表示
  1. メッセージ認証コード (MAC)
  2. MACアルゴリズム
    1. MACベースの疑似乱数生成器
  3. 認証付き暗号化 (AEAD)
  4. HMAC
  5. 鍵導出関数(KDF)
  6. 鍵導出関数
    1. PBKDF2
    2. Scrypt
    3. Argon2

メッセージ認証コード (MAC) や HMAC(ハッシュベースのメッセージ認証コード)および KDF(鍵導出関数)は、暗号学において重要な役割を果たします。ここでは、どのような場面でMACが必要となるか、HMACの計算方法、およびそれが鍵導出関数とどのように関連しているかについて説明します。

メッセージ認証コード (MAC)

メッセージ認証コード (MAC) は、与えられた鍵とメッセージによって計算される暗号コードです:

auth_code = MAC(key, msg)

通常、これはハッシュ関数のように振る舞います。メッセージや鍵のわずかな変更でも、全く異なるMAC値が得られます。 鍵やメッセージを変更しても同じMAC値を取得することは実質的に不可能であるようにMACアルゴリズムは設計されています。 つまり、MAC値はハッシュと同様に逆変換不可能です。 MAC値から元のメッセージや鍵を復元することは不可能です。 また、MACアルゴリズムは「鍵付きハッシュ関数」としても知られており、ハッシュ関数と同様に鍵を使用します。

例えば、MAC値は以下のようにHMAC-SHA256アルゴリズムで計算できます:

HMAC-SHA256('key', 'some msg') = 32885b49c8a1009e6d66662f8462e7dd5df769a7b725d1d546574e6d5d6e76ad

上記のHMAC-SHA256の計算は、Pythonで以下のようにプログラムで書くことができます:

import hashlib
import hmac
import binascii
mac = hmac.new(b'key', b'some msg', hashlib.sha256).digest()
print(binascii.hexlify(mac))

MAC値はデジタル認証コードであり、デジタル署名の技術と似ているものですが、事前共有鍵を使用する点が異なります。

MACアルゴリズム

現代の暗号学には、メッセージ認証コード (MAC) を計算するための多くのアルゴリズムが存在します。最も一般的なものは、ハッシュアルゴリズムに基づいており、HMAC (ハッシュベースのMAC、例:HMAC-SHA256) やKMAC (KeccakベースのMAC) があります。他にも、対称鍵暗号に基づいたものとして、CMAC (暗号ベースのMAC)、GMAC (Galois MAC)、および Poly1305 (Bernsteinによって設計されたMAC) があります。その他のMACアルゴリズムには、UMAC (ユニバーサルハッシングに基づくMAC)、VMAC (ブロック暗号ベースのMAC)、および SipHash (シンプルで高速で安全なMAC) が含まれます。

MAC値を利用したメッセージの改ざん検知は、大まかに次のような流れになります:

  1. 2つの当事者がある特定の秘密のMAC鍵(事前共有鍵)を何らかの方法で事前に交換します
  2. 別の場所(例:インターネット、ブロックチェーン、または電子メールメッセージ)から msg + auth_code を取得します
  3. msgが改ざんされていないことを確認します(つまり、鍵とmsgが正しく一致し、MAC値も一致していることを確認します)
  4. MAC値は正しくないとき、メッセージは改ざんされたことを検知できます。

MACベースの疑似乱数生成器

MAC値は、疑似乱数生成器関数にも応用されています。 特定のソルト(定数または現在の日付と時刻または他のランダム性)と任意のシード番号(最後に生成されたランダム番号、例えば 0)から始めることができます。 例えば、次のように next_seed を計算できます:

next_seed = MAC(salt, seed)

次の疑似乱数は、上記の式の計算ごとにランダムに変化し、特定の範囲内で次の乱数を生成するために使用できます。

認証付き暗号化 (AEAD)

MAC値を使用する他の例で有名なものは、認証付き暗号化 (AEAD) です。 認証付き暗号化とは、メッセージを暗号化/復号化するときにMACも使用して暗号文の改ざん検知を行う仕組みを持つ暗号のことです。

メッセージを暗号化する場合は、次のように処理を行います:

  1. まず、パスワードから鍵を導出します。この鍵をMAC計算アルゴリズムに使用します。
  2. 次に、導出された鍵を使用してメッセージを暗号化し、暗号文を出力に保存します。
  3. 最後に、導出された鍵と元のメッセージを使用してMAC値を計算し、出力に追加します。

暗号化されたメッセージ(暗号文 + MAC)を復号化する場合は、次のように処理を行います:

  1. まず、ユーザーが入力したパスワードから鍵を導出します。これは正しいパスワードか誤ったパスワードかを後で確認します。
  2. 次に、導出された鍵を使用してメッセージを復号化します。これは元のメッセージか誤ったメッセージか(入力されたパスワードに依存)です。
  3. 最後に、導出された鍵 + 復号化されたメッセージを使用してMAC値を計算します。
    • 計算されたMAC値が暗号化されたメッセージ内のMAC値と一致するとき「パスワードは正しい」と「暗号文は改竄されていない」ことが証明されます。
    • それ以外のとき「パスワードが間違っている」または「暗号文が改竄されている」ことのどちらかが示されます。

一部の認証付き暗号化アルゴリズム(例:AES-GCMおよびChaCha20-Poly1305)は、MAC計算を暗号化アルゴリズムに統合し、MAC検証を復号化アルゴリズムに統合しています。 MACは暗号文とともに保存されますが、MAC値からパスワードや元のメッセージに復元することは不可能です。 つまり、誰にでも見える場所にMAC値を保存すること自体は安全上の問題はありません。

HMAC

単純に hash_func(key + msg) を計算してMAC(メッセージ認証コード)を取得することは安全ではないと考えられます (例:伸長攻撃、詳細はHMAC#設計原則を参照)。 代わりにHMACアルゴリズムを使用することが推奨されています。 例えば、HMAC-SHA256 や HMAC-SHA3-512 などの安全なMACアルゴリズムを使用することが推奨されます。

HMAC とは、Hash-based Message Authentication Code を省略した言葉で、暗号ハッシュ関数を使用して計算されたMAC値という意味になります。 HMACは鍵とメッセージとハッシュ関数からMAC値を生成する関数です:

HMAC(key, msg, hash_func) -> hash

結果のMAC値は、秘密鍵と混合されたメッセージハッシュです。 これは不可逆性衝突耐性などのハッシュの暗号特性を持ちます。 hash_func は、SHA-256、SHA-512、RIPEMD-160、SHA3-256、BLAKE2s などの暗号ハッシュ関数を使用できます。 HMACは、メッセージの真正性、メッセージの整合性、そして時には鍵の導出にも利用されます。

鍵導出関数(KDF)

鍵導出関数(KDF)は、可変長のパスワードを固定長のキー(ビットのシーケンス)に変換する関数です:

function(password) -> key

非常に単純なKDF関数として、SHA256を使用することができます:単にパスワードをハッシュ化します。これは安全ではないので、行わないでください。単純なハッシュは辞書攻撃に対して脆弱です。 より複雑なKDF関数として、saltと呼ばれるランダムな値を使用してHMAC(salt, msg, SHA256)を計算することで、パスワードを導出することができます。このsaltは導出されたキーと一緒に保存され、後で同じキーを再度パスワードから導出するために使用されます。 鍵導出にHKDF(HMACベースの鍵導出)を使用することは、現代のKDFよりも安全性が低いため、専門家はPBKDF2BcryptScryptArgon2などのより強力な鍵導出関数の使用を推奨しています。これらのKDF関数については後で詳しく説明します。

例えば、HMACを使ってパスワードから鍵を導出をする例を紹介します。 以下を計算してみてください:

HMAC('sample message', '12345', 'SHA256') =
  'ee40ca7bc90df844d2f5b5667b27361a2350fad99352d8a6ce061c69e41e5d32'

Pythonで実装すると以下のようになります。

import hashlib
import hmac
import binascii

def hmac_sha256(key, msg):
  return hmac.new(key, msg, hashlib.sha256).digest()

key = b"12345"
msg = b"sample message"
print(binascii.hexlify(hmac_sha256(key, msg)))

hmac_sha256 の返り値 (16進数変換して出力) がパスワードから導出された新しい鍵となります。

暗号学では、バイナリ鍵の代わりにパスワードを使用することがよくあります。 なぜなら、パスワードは覚えやすく、書き留めやすく、短くすることができるからです。 特定のアルゴリズムが(たとえば暗号化やデジタル署名のため)を必要とする場合、鍵導出関数(パスワードから鍵への変換)が必要です。

なお、SHA-256(パスワード) を鍵導出として使用するのは安全ではありません。 これは多くの攻撃に対して脆弱であり、総当たり攻撃辞書攻撃レインボーアタックなどが可能であり、実際にハッシュを逆引きして攻撃者がパスワードを取得できる可能性があるためです。

鍵導出関数

PBKDF2BcryptScrypt、およびArgon2は、強力な鍵導出関数であり、パスワードの推測(総当たり攻撃)に耐えられるように設計されています。 安全な鍵導出関数は、ソルト(各鍵導出ごとに異なるランダムな数値)+ 多数の反復処理(最終的なパスワード推測プロセスを遅くするため)を使用するように設計されています。 これは、鍵ストレッチングとして知られるプロセスです。 安全なKDFを計算するには、鍵を導出するのにある程度のCPU時間(たとえば0.2秒)とある程度のメモリ(RAM)が必要です。 したがって、鍵の導出は「計算的に高価」であり、パスワードの解読も計算的に高価になります。 適切な構成パラメータを使用して現代のKDF関数を使用すると、パスワードの解読が遅くなります。 上記のすべての鍵導出アルゴリズム(PBKDF2、Bcrypt、Scrypt、Argon2)は特許を取得しておらず、一般に誰でも使用することができます。

PBKDF2

PBKDF2は、辞書攻撃やレインボーテーブル攻撃に耐性を持つ単純な暗号鍵導出関数です。 PBKDF2では、何度もHMACをいくつかのパディングとともに反復的に導出することに基づいています。 PBKDF2アルゴリズムは、インターネット標準のRFC 8018 (PKCS #5)で説明されています。 PBKDF2はいくつかの入力パラメータを取り、導出された鍵を出力します:

key = pbkdf2(password, salt, iterations-count, hash-function, derived-key-len)

技術的には、PBKDF2の入力データは以下のようになります:

  • password – バイトの配列 / 文字列、例: "p@$Sw0rD~3" (8-10文字の最小長が推奨)
  • salt – 安全に生成されたランダムなバイト、例: "df1f2d3f4d77ac66e9c5a6c3d8f921b6" (最小64ビット、128ビットが推奨)
  • iterations-count、例: 1024回の反復
  • HMACを計算するためのhash-function、例: SHA256
  • 出力用のderived-key-len、例: 32バイト (256ビット)

出力データは、要求された長さの導出された鍵です (例: 256ビット)。

PBKDF2は、イテレーション数を設定することができ、それによって鍵を導出するために必要な時間を設定することができます。

  • 鍵の導出が遅いとは、ログイン時間が長いこと / 復号が遅いこと / などを意味し、パスワードクラック攻撃に対する高い耐性を持つことを意味します。
  • 鍵の導出が速いとは、ログイン時間が短いこと / 復号が速いこと / などを意味し、パスワードクラック攻撃に対する低い耐性を持つことを意味します。
  • PBKDF2 は GPU攻撃(ビデオカードを使用した並列パスワードクラック)や ASIC攻撃(専用のパスワードクラックハードウェア)に耐性がないです。これがより現代的なKDF関数の背後にある主な動機です。

PBKDF2は古い技術であり、現代のKDF関数よりも安全性が低いと考えられているため、代わりにBcrypt、Scrypt、またはArgon2を使用することが推奨されています。

Scrypt

Scrypt (RFC 7914) は強力な暗号鍵導出関数(KDF)です。 GPU、ASIC、FPGA攻撃(高効率のパスワードクラッキングハードウェア)を防ぐように設計されたメモリ集約型です。 Scryptアルゴリズムはいくつかの入力パラメータを取り、導出された鍵を出力します:

key = Scrypt(password, salt, N, r, p, derived-key-len)

Scryptの設定パラメータは以下の通りです:

  • N – イテレーション回数(メモリとCPU使用量に影響)、例: 16384 または 2048
  • r – ブロックサイズ(メモリとCPU使用量に影響)、例: 8
  • p – 並列度ファクター(並列で実行するスレッド数 - メモリとCPU使用量に影響)、通常は1
  • password – 入力パスワード(8-10文字の最小長が推奨されます)
  • salt – 安全に生成されたランダムバイト(最小64ビット、推奨128ビット)
  • derived-key-length - 出力として生成するバイト数、例: 32バイト(256ビット)

Scryptにおけるメモリは各ステップで強く依存する順序でアクセスされるため、メモリアクセス速度がアルゴリズムのボトルネックとなります。 Scrypt鍵導出を計算するために必要なメモリは以下のように計算されます:

必要なメモリ = 128 * N * r * p バイト

例: 128 * N * r * p = 128 * 16384 * 8 * 1 = 16 MB

パラメータの選択は、どれだけ鍵導出に時間をかけたいかとどの程度のセキュリティ(パスワードクラックへの耐性)を達成したいかに依存します:

  • 対話的ログインのサンプルパラメータ : N=16384, r=8, p=1(RAM = 2 MB)。対話的ログインではおそらく0.5秒以上待ちたくないため、計算は非常に遅くする必要があります。また、サーバーサイドでは多くのユーザーが同時にログインできるため、遅いScrypt計算はシステム全体を遅くします。
  • ファイル暗号化のサンプルパラメータ : N=1048576, r=8, p=1(RAM = 1 GB)。ハードドライブを暗号化する場合、暗号化されたデータを解除する頻度は稀であり、通常1日に2-3回程度であるため、セキュリティを高めるために2-3秒鍵導出に時間をかけたいかもしれません。

アプリやシステムの設計と開発中に、自身でScryptパラメータをテストして選択することができます。クラッカーは確実にそれを使用するため、常に言語やプラットフォームにおける最速のScrypt実装を使用するようにしてください。一部の実装(例: Python)は最速のものよりも100倍遅い場合があります。

適切にパラメータが設定されたとき、Scryptは非常に安全なKDF(Key Derivation Function)関数と見なされるため、ウォレット、ファイル、またはアプリのパスワードを暗号化する際など、一般的な目的のパスワードから鍵を導出するアルゴリズムとして使用できます。

Argon2

Argon2は、現代のASICとGPUに耐性がある安全な鍵導出関数です。 PBKDF2、Bcrypt、Scryptよりも、適切に構成された場合には、パスワードの解読耐性が向上しています(CPUとRAMの使用に関する類似の構成パラメータに対して)。

Argon2 関数にはいくつかの種類(バリアント)があります:

  • Argon2d : 強力なGPU耐性を提供しますが、潜在的なサイドチャネル攻撃があります(非常に特殊な状況で可能です)。
  • Argon2i : GPU耐性が少ないですが、サイドチャネル攻撃がありません。
  • Argon2id : 推奨(Argon2d と Argon2i を組み合わせたもの)。

Argon2には、以下の設定パラメータがあります。これらはScryptと非常に似ています:

  • password P: ハッシュ化するパスワード(またはメッセージ)
  • salt S: ランダム生成されたソルト(パスワードハッシングには16バイトが推奨されています)
  • iterations t: 実行する反復回数
  • memorySizeKB m: 使用するメモリ量(キロバイト単位)
  • parallelism p: 並列度(つまり、スレッド数)
  • outputKeyLength T: 返されるバイト数の希望値

多くのアプリケーション、フレームワーク、ツールでは、Argon2で暗号化されたパスワードは、アルゴリズム設定とソルトと一緒に保存されます。これらは、$文字で区切られた複数の部分からなる単一の文字列に格納されます。たとえば、パスワード p@ss~123 は、Argon2の標準形式で以下のように保存されます(説明のために複数のパターンの例を示しています):

$argon2d$v=19$m=1024,t=16,p=4$c2FsdDEyM3NhbHQxMjM$2dVtFVPCezhvjtyu2PaeXOeBR+RUZ6SqhtD/+QF4F1o
$argon2d$v=19$m=1024,t=16,p=4$YW5vdGhlcnNhbHRhbm90aGVyc2FsdA$KB7Nj7kK21YdGeEBQy7R3vKkYCz1cdR/I3QcArMhl/Q
$argon2i$v=19$m=8192,t=32,p=1$c21hbGxzYWx0$lmO1aPPy3x0CcvrKpFLi1TL/uSVJ/eO5hPHiWZFaWvY

上記のすべてのハッシュは同じパスワードを保持していますが、異なるアルゴリズム設定と異なるソルトを使用しています。

適切に構成された場合、Argon2 は業界で利用可能な中でも最高のセキュアなKDF関数と見なされるため、ウォレット、文書、ファイル、またはアプリのパスワードを暗号化する際など、一般的な目的のパスワードから鍵導出アルゴリズムとして使用できます。一般的なケースではArgon2の方が、Scrypt、Bcrypt、およびPBKDF2よりも推奨されています。