feat(crypto): add compute_apphmac() and IOS_APPID/IOS_APPKEYVERSION constants

AppleWebCrypto::HKDF を再実装した compute_apphmac() を NetflixCrypto に追加。
  Extract: prk = HMAC-SHA256(key=MGK, data=PSK)
  Expand:  okm = HMAC-SHA256(key=prk, data=Nonce)

- crypto.py: NetflixCrypto.compute_apphmac(enc_key_0, sign_key_0) → 32B apphmac
- constants.py: IOS_APPID, IOS_APPKEYVERSION を追加
- ios_client.py: apphmac フィールドのドキュメントを更新、IOS_APPKEYVERSION をデフォルト値に使用
- tools/verify_full_key_chain.py: apphmac テストベクタを追加 (9/9 PASS)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tkgstrator
2026-04-09 12:11:40 +00:00
co-authored by Claude Sonnet 4.6
parent 7cd95f6631
commit 7ca4183a08
4 changed files with 46 additions and 8 deletions
+4
View File
@@ -64,6 +64,10 @@ IOS_DH_G = 5
# appboot エンドポイント
IOS_APPBOOT_ENDPOINT = "https://appboot.netflix.com/appboot/"
# iOS アプリ識別子 (NFWebCrypto.framework から抽出)
IOS_APPID = "a2becfec-b286-535c-b884-903a384caee6"
IOS_APPKEYVERSION = 1
# iOS appboot 署名検証鍵 (NFWebCrypto.framework からの静的抽出)
# kAppBootKey: RSA-4096 SPKI/DER (550B) — RSASSA-PKCS1-v1_5 検証用
# バイナリオフセット: NFWebCrypto @ 0x0020cd31 (__TEXT.__cstring, Base64)
+21
View File
@@ -457,6 +457,27 @@ class NetflixCrypto:
sign_key = digest[16:48]
return enc_key, sign_key
# ---- apphmac 計算 (FAIRPLAY_MGK_APPID entity_auth_data) ----
@staticmethod
def compute_apphmac(enc_key_0: bytes, sign_key_0: bytes) -> bytes:
"""apphmac を HKDF(key=MGK, ikm=PSK, info=Nonce) で計算する.
AppleWebCrypto::HKDF の再実装:
Extract: prk = HMAC-SHA256(key=MGK, data=PSK)
Expand: okm = HMAC-SHA256(key=prk, data=Nonce)
Args:
enc_key_0: Phase 0 MGK 暗号化鍵 (16 bytes)
sign_key_0: Phase 0 MGK 署名鍵 (32 bytes)
Returns:
32 バイトの apphmac 値
"""
mgk = enc_key_0 + sign_key_0 # 48B
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 scheme_data 構築 (Scheme 3 / appboot) ----
@staticmethod
+5 -5
View File
@@ -32,7 +32,7 @@ appboot から MSL セッション鍵確立までの End-to-end フローを実
- enc_key_0 / sign_key_0: TFIT エミュレーション (emulate_tfit.py) で導出可能
- devicetoken: Frida/Tweak `AppbootKeyExtract` でキャプチャ
- apphmac: Frida/Tweak `AppbootKeyExtract` でキャプチャ
- apphmac: NetflixCrypto.compute_apphmac(enc_key_0, sign_key_0) で計算可能
- device_key_data: Frida/Tweak `AppbootKeyExtract` でキャプチャ (~6,576 bytes)
- session_region: TFIT エミュレーション (emulate_tfit.py) または Frida キャプチャ
- s1, s2, s3: Frida キャプチャで取得
@@ -48,7 +48,7 @@ import requests
from netflix_msl.cbor_decoder import CborMslDecoder
from netflix_msl.cbor_encoder import CborMslEncoder
from netflix_msl.constants import IOS_APPBOOT_ENDPOINT
from netflix_msl.constants import IOS_APPBOOT_ENDPOINT, IOS_APPID, IOS_APPKEYVERSION
from netflix_msl.crypto import NetflixCrypto, SessionKeys
@@ -77,7 +77,7 @@ class iOSAppbootParams:
apphmac: str
"""アプリ認証 HMAC-SHA256 (hex string).
TODO: Frida/Tweak AppbootKeyExtract でキャプチャ."""
NetflixCrypto.compute_apphmac(enc_key_0, sign_key_0).hex() で計算可能."""
device_key_data: bytes
"""デバイス固有鍵データ (~6,576 bytes).
@@ -114,7 +114,7 @@ class iOSAppbootParams:
TODO: キャプチャから取得した値を使用すること."""
# オプション
appkeyversion: int = 1
appkeyversion: int = IOS_APPKEYVERSION
esn_prefix: str = ""
scheme_suffix: str = "_3"
@@ -158,7 +158,7 @@ class iOSMslClient:
sign_key_0=bytes.fromhex("91f752f7..."),
appid="a2becfec-b286-535c-b884-903a384caee6",
devicetoken="0608a1b7...", # TODO: Frida キャプチャ
apphmac="a18bf28f...", # TODO: Frida キャプチャ
apphmac=NetflixCrypto.compute_apphmac(enc_key_0, sign_key_0).hex(),
device_key_data=b"...", # TODO: Frida キャプチャ (~6576B)
session_region=b"...", # TODO: TFIT エミュレーション or Frida
s1=b"...", # TODO: Frida キャプチャ
+16 -3
View File
@@ -59,6 +59,14 @@ EXPECTED_SIGN_KEY_1 = bytes.fromhex(
"d45443fa11efec622c83b27c55f7a73143bdfa0d51820ac597b9e3fb5c28dbb0"
)
# apphmac expected output
# compute_apphmac(enc_key_0, sign_key_0):
# prk = HMAC-SHA256(key=MGK, data=PSK)
# okm = HMAC-SHA256(key=prk, data=Nonce)
EXPECTED_APPHMAC = bytes.fromhex(
"4c142e4b82b3ad21e2dcdbcc007c27a4787adc0568959080b004b5daa5a7385a"
)
# ============================================================================
# Test runner
@@ -114,9 +122,7 @@ def main() -> int:
session_check = hmac_mod.new(
IOS_KDF_PSK, enc_key_0 + sign_key_0, hashlib.sha256
).digest()
session_bind = hmac_mod.new(
session_check, IOS_KDF_NONCE, hashlib.sha256
).digest()
session_bind = hmac_mod.new(session_check, IOS_KDF_NONCE, hashlib.sha256).digest()
check(
"session_bind[:16] (KDF intermediate)",
session_bind[:16],
@@ -142,6 +148,13 @@ def main() -> int:
check("new enc_key (16B)", new_enc_key, EXPECTED_NEW_ENC_KEY)
check("bootstrap_key = new sign_key (32B)", new_sign_key, EXPECTED_BOOTSTRAP_KEY)
# ==== apphmac: HKDF(key=MGK, ikm=PSK, info=Nonce) ====
print()
print("=== apphmac: HKDF(MGK, PSK, Nonce) ===")
apphmac = NetflixCrypto.compute_apphmac(enc_key_0, sign_key_0)
check("apphmac (32B)", apphmac, EXPECTED_APPHMAC)
# ==== Summary ====
print()
print(f"Result: {passed}/{total} passed")