- msl_key_relationship: Phase 4 (ログイン鍵配送) を追加 enc_key_1でAES-CBC復号 → enc_key_2/sign_key_2を取得する流れを図示 シーケンス図、検証データ、二重署名構造、フック制約表を追加 - msl_kdf_analysis: ログイン鍵配送の検証データ、二重署名構造を追記 KDFは常にenc_key_0/sign_key_0を入力とすることを明記 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.8 KiB
Netflix iOS MSL KDF (Key Derivation Function) 解析
解析日: 2026-04-08
ソース: Tweak AppbootKeyExtract v39 HMAC streaming hooks ログ
1. 概要
Netflix iOS アプリの MSL セッション鍵更新は、標準 HKDF ではなく独自の HMAC-SHA256 チェーン で実装されている。
OpenSSL の EVP HKDF API (EVP_PKEY_CTX_set_hkdf_*) は使用されておらず、
低レベルの HMAC_Init_ex / HMAC_Update / HMAC_Final を直接呼び出している。
2. KDF アルゴリズム (鍵更新)
2.1 入力パラメータ
| パラメータ | サイズ | 説明 |
|---|---|---|
| PSK (Pre-Shared Key) | 16 bytes | マスター鍵。DH 鍵交換時に導出される (§3 参照) |
| enc_key | 16 bytes | 現在の AES-128-CBC 暗号化鍵 |
| sign_key | 32 bytes | 現在の HMAC-SHA256 署名鍵 |
| nonce | 16 bytes | サーバーレスポンス key 33.9 から取得 |
2.2 演算ステップ (6段階)
Step 1: session_check = HMAC-SHA256(PSK, enc_key || sign_key)
Step 2: session_bind = HMAC-SHA256(session_check, nonce)
Step 3: enc_temp = HMAC-SHA256(PSK, enc_key)
Step 4: new_enc_full = HMAC-SHA256(enc_temp, nonce)
new_enc_key = new_enc_full[:16]
Step 5: sign_temp = HMAC-SHA256(PSK, sign_key)
Step 6: new_sign_key = HMAC-SHA256(sign_temp, nonce)
2.3 出力
| 出力 | ステップ | サイズ |
|---|---|---|
| new_enc_key | Step 4 の先頭 16 bytes | 16 bytes (AES-128) |
| new_sign_key | Step 6 の全 32 bytes | 32 bytes (HMAC-SHA256) |
| session_bind | Step 2 の全 32 bytes | 32 bytes (セッションバインド検証用?) |
2.4 検証データ
PSK = 027617984f6227539a630b897c017d69
enc_key = 0817065e29e6d1c8668473af9e13b3c2
sign_key = 91f752f76d7ab4c2dc6e5b3ec1c0e5a16864421fe449be5457459602e298ebc1
nonce = 809f82a7addf548d3ea9dd067ff9bb91
Step 1: 19def2f90d06bc8dfd04a19dbd4588d4e7b8aa6ccacb200f9ae6acc49355917d
Step 2: add2d4c818426aee3dfbbbb783a85262ee7c8cc1936013b53d5f4cb53d6baee0
Step 3: e60e376f37d7d962512aea2f29a353c28b0fb95b1e77c43baf7459b21d1df649
Step 4: 97b99f4e88e8e73779aa20ac11877c5dfe06b76df3e1dfe1378d6d9223f5b511
Step 5: 58c4e3d1cc2ce7bd73e846a1c3b00a9986aa039302d7bbf1a5508d5f9a49120f
Step 6: d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0
new_enc_key = 97b99f4e88e8e73779aa20ac11877c5d ← Step 4[:16]
new_sign_key = d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0
3. 初期鍵導出 (DH → セッション鍵) — 未解明
3.1 判明している事実
- DH パラメータ: 1024-bit, g=5, p は Netflix 固有値 (
9694e9d8...) - DH 共有秘密 = 128 bytes
- PSK
027617984f6227539a630b897c017d69(16 bytes) の由来は不明
3.2 MSL Java 参照実装との差異
MSL Java 参照実装 (DiffieHellmanExchange.java) の KDF:
// 1. correct_null_bytes: 先頭に 0x00 を1つだけ付与
// 2. SHA-384(shared_secret)
// 3. enc_key = hash[0:16], hmac_key = hash[16:48]
iOS Scheme 5 はこれとは完全に異なる:
- Java 参照: SHA-384 一発。ラップキーなし。両者が独立に計算
- iOS Scheme 5: HMAC-SHA256 チェーン (§2)。PSK を使用
- SHA-384(DH_shared_secret) の出力は PSK
0276...とも既知セッション鍵とも一致しない
3.3 解決: PSK と nonce はバイナリにハードコードされた定数
Tweak v42/v43 の実験で確定:
- アプリの Library/Caches/Preferences/odb 等のファイルをすべて削除 → PSK は変わらない
- Keychain の全エントリを
SecItemDeleteで削除 (status=0 成功) → PSK は変わらない - NFWebCrypto.framework バイナリを検索 → PSK と nonce がバイナリに連続して埋め込まれている
Offset 0x1ac8f5: 02 76 17 98 4f 62 27 53 9a 63 0b 89 7c 01 7d 69 ← PSK (16B)
Offset 0x1ac905: 80 9f 82 a7 ad df 54 8d 3e a9 dd 06 7f f9 bb 91 ← nonce (16B)
Offset 0x1ac915: N7netflix14AppleWebCryptoE ← C++ mangled name
直後の C++ シンボル名 netflix::AppleWebCrypto から、このクラスの静的定数として
コンパイルされたことがわかる。
結論: PSK と nonce はデバイス固有ではなく、同じバージョンの NFWebCrypto.framework を 持つ全デバイスで共通の固定値。Python 実装にそのままハードコードできる。
4. レスポンスキー交換データ構造
4.1 key 33 (key_response_data) の sub-key
| sub-key | 型 | サイズ | 説明 |
|---|---|---|---|
| 6 | bytes | 96 | 暗号化されたセッション鍵 |
| 7 | bytes | 1 | ステータスフラグ (0x00) |
| 8 | string | 1 | スキーム ID ('5') |
| 9 | bytes | 16 | サーバー nonce (KDF 入力) |
4.2 key 33.6 (96 bytes) の推定構造
[IV: 16 bytes][CT: 48 bytes][HMAC: 32 bytes]
- IV (16B): AES-CBC 初期化ベクトル
- CT (48B): AES-CBC 暗号文 → 平文 = enc_key(16B) + sign_key(32B)
- HMAC (32B): HMAC-SHA256(PSK?, IV||CT) による認証
5. 鍵の使用パターン
v39 ログから観測された暗号化/署名パターン:
[AES_set_encrypt_key bits=128] key=0817065e... ← セッション暗号化鍵
[AES_cbc_encrypt] dir=ENC len=336 iv=... ← MSL ペイロード暗号化
[HMAC] key_len=32 key=91f752f7... ← セッション署名鍵で署名
全メッセージが同じ enc_key / sign_key で暗号化・署名されている。 鍵更新の結果 (97b99f4e / d45443fa) は次のセッションで使用される。
5.1 KDF の入力は常に enc_key_0 / sign_key_0
v59 ログ (ログインフロー含む) で確認: KDF は毎回 enc_key_0 / sign_key_0 を入力として使用する。 enc_key_1 → enc_key_2 のようなチェーン更新は行われない。
5.2 ログイン時の鍵配送 (v59 で解明)
ログイン後、サーバーの key_response_data から新しいセッション鍵が配送される。 enc_key_1 を復号鍵として AES-128-CBC で復号することで取得:
enc_key_2 の復号:
key = enc_key_1 = 97b99f4e88e8e73779aa20ac11877c5d
iv = d85aee3d39bfb1a6a38307fc61cbcccf
ct = 004e5f4b76443f81337c63ccc90be86e
pt = 0d968f3aa8cb79f85d9135760d63c93a (enc_key_2)
sign_key_2 の復号:
key = enc_key_1 = 97b99f4e88e8e73779aa20ac11877c5d
iv = d9ce8161058196b60cee9b81e8fff399
ct = 830fdc90b712b43d60087887f7aef42a956fd8ad92dd9b82fcc771a247a3f5b3
pt = 4eea8df1b3a59b20690739dc2e4080813438ef172c80ea8d0cc3d5298dd05a4e (sign_key_2)
5.3 二重署名構造
MSL メッセージには 2 種類の HMAC-SHA256 署名が付与される:
| 署名鍵 | 対象サイズ | 用途 |
|---|---|---|
| sign_key (セッション) | 76-499 bytes | MSL ヘッダー/チャンク署名 |
bootstrap_key (38b2030d...) |
6000-8500 bytes | ペイロード全体署名 |
bootstrap_key の由来は不明 (バイナリにも存在せず、既知鍵から導出不可)。
6. Python 実装
import hashlib
import hmac
def netflix_msl_kdf_renew(
psk: bytes, # 16 bytes
enc_key: bytes, # 16 bytes
sign_key: bytes, # 32 bytes
nonce: bytes, # 16 bytes
) -> tuple[bytes, bytes]:
"""Netflix MSL セッション鍵更新 KDF."""
# Step 1-2: セッションバインド (検証用)
session_check = hmac.new(psk, enc_key + sign_key, hashlib.sha256).digest()
session_bind = hmac.new(session_check, nonce, hashlib.sha256).digest()
# Step 3-4: 新しい暗号化鍵
enc_temp = hmac.new(psk, enc_key, hashlib.sha256).digest()
new_enc_key = hmac.new(enc_temp, nonce, hashlib.sha256).digest()[:16]
# Step 5-6: 新しい署名鍵
sign_temp = hmac.new(psk, sign_key, hashlib.sha256).digest()
new_sign_key = hmac.new(sign_temp, nonce, hashlib.sha256).digest()
return new_enc_key, new_sign_key