feat(crypto): implement build_session_region() with TFIT-WB-AES DH key encryption

Add session_region builder for key 33.6 scheme_data (plaintext[128:300] = 172B).
Emits 7B constant CBOR prefix + 128B TFIT-WB-AES-ECB(DH_pub_key) + 37B zero-filled
MGK tail (CBOR encoding TBD). Falls back to bytes(172) if NFWebCrypto binary absent.
Update test_appboot_e2e.py to call build_session_region() instead of hardcoded zeros.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tkgstrator
2026-04-09 15:47:05 +00:00
co-authored by Claude Sonnet 4.6
parent 9be376736d
commit 455de1ebee
3 changed files with 139 additions and 9 deletions
+5
View File
@@ -119,6 +119,11 @@ IOS_SHARKBOOT_KEY_DER = bytes.fromhex(
"7591de8897f6764ff4ad1fb552"
)
# key 33.6 session_region の先頭 7B CBOR プレフィックス (iPhone デバイス共通)
# 実測: 全 352B appboot キャプチャで session_region[0:7] が全セッションで同一。
# CBOR データストリームの継続部分 — ヘッダーと TFIT データの間のフレーム。
IOS_KEY336_SESSION_REGION_PREFIX: bytes = bytes.fromhex("6260c8a117cf31")
# key 33.6 scheme_data の固定デバイスヘッダー (128B)
# 180 個の 352B appboot サンプルのうち 165 個 (標準 iPhone) で共通の定数。
# plaintext[0:128] の値 (XOR 復号後)。
+108
View File
@@ -25,6 +25,7 @@ from netflix_msl.constants import (
IOS_KDF_NONCE,
IOS_KDF_PSK,
IOS_KEY336_DEVICE_HEADER,
IOS_KEY336_SESSION_REGION_PREFIX,
RSA_KEYPAIR_ID,
)
@@ -478,6 +479,113 @@ class NetflixCrypto:
prk = hmac_mod.new(mgk, IOS_KDF_PSK, hashlib.sha256).digest()
return hmac_mod.new(prk, IOS_KDF_NONCE, hashlib.sha256).digest()
# ---- key 33.6 session_region 構築 (TFIT-WB-AES-128-ECB) ----
@staticmethod
def build_session_region(
dh_pub_key: bytes,
enc_key_0: bytes,
sign_key_0: bytes,
) -> bytes:
"""key 33.6 の session_region (172B) を TFIT エミュレーションで構築する.
session_region (172B) の構成:
[0:7] 7B CBOR プレフィックス (iPhone デバイス共通定数)
[7:135] 128B TFIT-WB-AES-128-ECB(DH_pub_key) — 8 ブロック × 16B
[135:172] 37B MGK テール — TFIT 暗号化 enc_key_0/sign_key_0 と CBOR フレーム
※ 詳細な CBOR エンコーディングは未解明のためゼロ埋め
TFIT エミュレーションの前提:
- NFWebCrypto.framework バイナリが
/tmp/nfwc/Payload/Argo.app/Frameworks/NFWebCrypto.framework/NFWebCrypto
に存在する必要がある。
- バイナリが存在しない場合は 172B ゼロ埋めにフォールバック (警告表示)。
Args:
dh_pub_key: DH 公開鍵 (128 bytes, big-endian)
enc_key_0: Phase 0 MGK 暗号化鍵 (16 bytes)
sign_key_0: Phase 0 MGK 署名鍵 (32 bytes)
Returns:
172 bytes の session_region
Raises:
ValueError: dh_pub_key が 128B でない場合
ValueError: enc_key_0 が 16B でない場合
ValueError: sign_key_0 が 32B でない場合
"""
if len(dh_pub_key) != 128:
raise ValueError(f"dh_pub_key must be 128 bytes, got {len(dh_pub_key)}")
if len(enc_key_0) != 16:
raise ValueError(f"enc_key_0 must be 16 bytes, got {len(enc_key_0)}")
if len(sign_key_0) != 32:
raise ValueError(f"sign_key_0 must be 32 bytes, got {len(sign_key_0)}")
import sys
from pathlib import Path
BINARY_PATH = Path(
"/tmp/nfwc/Payload/Argo.app/Frameworks/NFWebCrypto.framework/NFWebCrypto"
)
if not BINARY_PATH.exists():
print(
" [WARN] build_session_region: NFWebCrypto binary not found at "
f"{BINARY_PATH}. session_region はゼロ埋めにフォールバック。"
)
return bytes(172)
try:
# tools/emulate_tfit.py をモジュールとしてインポート
tools_dir = str(Path(__file__).resolve().parent.parent.parent / "tools")
if tools_dir not in sys.path:
sys.path.insert(0, tools_dir)
import importlib.util
spec = importlib.util.spec_from_file_location(
"emulate_tfit", Path(tools_dir) / "emulate_tfit.py"
)
if spec is None or spec.loader is None:
raise ImportError("emulate_tfit.py のロードに失敗")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # type: ignore[arg-type]
import lief
binary_data = BINARY_PATH.read_bytes()
binary = lief.MachO.parse(str(BINARY_PATH)).at(0)
emu = mod.TFITEmulator(binary_data, binary)
ks = mod.load_key_schedule(mod.MGK_TYPE_IPHONE, binary_data)
# TFIT-WB-AES-128-ECB: 8 ブロック × 16B = 128B
tfit_dh_pub = bytearray()
for i in range(8):
block = dh_pub_key[i * 16 : (i + 1) * 16]
tfit_dh_pub.extend(emu.encrypt_block(ks, block))
except Exception as e:
print(
f" [WARN] build_session_region: TFIT エミュレーション失敗 ({e}). "
"session_region はゼロ埋めにフォールバック。"
)
return bytes(172)
# session_region[135:172] = 37B MGK テール
# 構成: CBOR フレーム + TFIT(enc_key_0) + TFIT(sign_key_0[:16]) の一部
# ※ 正確な CBOR エンコーディングは未解明のためゼロ埋め
# TODO: MGK テールの正確な CBOR エンコーディングを解明して実装する
mgk_tail = bytes(37)
result = (
IOS_KEY336_SESSION_REGION_PREFIX # 7B: 定数 CBOR プレフィックス
+ bytes(tfit_dh_pub) # 128B: TFIT(DH_pub_key)
+ mgk_tail # 37B: MGK テール (未解明)
)
assert len(result) == 172, f"session_region length {len(result)} != 172"
return result
# ---- key 33.6 scheme_data 構築 (Scheme 3 / appboot) ----
@staticmethod
+26 -9
View File
@@ -460,15 +460,32 @@ def run_e2e_test(
print(f" DH pub_key: {dh_pub_key[:16].hex()}... ({len(dh_pub_key)}B)")
# ------------------------------------------------------------------
# session_region を DH 公開鍵からゼロパディングで仮構築
# session_region を TFIT-WB-AES で構築 (NFWebCrypto.framework が必要)
# ------------------------------------------------------------------
# TODO: 本来は TFIT-WB-AES (tools/emulate_tfit.py の session_region 導出) が必要。
# 現在は 172B のゼロ埋めプレースホルダーを使用。
# このため key 33.6 の内容は正しくなく、サーバーは鍵交換を拒否する。
# Phase 2 以降の DH 鍵合意は成立しない。
# 正式フローでは emulate_tfit.py で DH 公開鍵を TFIT-WB-AES 暗号化し
# session_region (172B) に格納する。
session_region_placeholder = b"\x00" * 172
# TFIT エミュレーションで DH 公開鍵を WB-AES-128-ECB 暗号化して session_region を構築。
# NFWebCrypto バイナリが存在しない場合は 172B ゼロ埋めにフォールバック。
# NOTE: session_region[135:172] (37B MGK テール) は CBOR エンコーディングが未解明のため
# 現状はゼロ埋め。サーバーが鍵交換を拒否する可能性がある。
print()
print("[Phase 1a'] session_region を TFIT エミュレーションで構築中...")
session_region = NetflixCrypto.build_session_region(
dh_pub_key=dh_pub_key,
enc_key_0=enc_key_0,
sign_key_0=sign_key_0,
)
is_zero_filled = session_region == bytes(172)
if is_zero_filled:
print(
" session_region: ゼロ埋め (TFIT バイナリ未検出またはエミュレーション失敗)"
)
else:
print(
f" session_region: TFIT 暗号化済み {session_region[:7].hex()}..."
f" ({len(session_region)}B)"
)
print(f" prefix (7B): {session_region[:7].hex()}")
print(f" TFIT[0] (16B): {session_region[7:23].hex()}")
print(f" TFIT[-1] (16B): {session_region[119:135].hex()}")
# セパレータは実測キャプチャから取得した既知の値を使用
# (セッション固有値のため、実際の接続では Frida キャプチャが必要)
@@ -489,7 +506,7 @@ def run_e2e_test(
)
key_request_bytes, k9_xor_nonce, nonce_7b = build_key_request_data(
session_region=session_region_placeholder,
session_region=session_region,
s1=s1,
s2=s2,
s3=s3,