From e74869dbf58b61a5f8946d3dccccf2affb61bb89 Mon Sep 17 00:00:00 2001 From: tkgstrator <29420801+tkgstrator@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:58:34 +0000 Subject: [PATCH] =?UTF-8?q?feat(cbor):=20Netflix=20=E3=82=AB=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=A0=20CBOR=20=E3=82=A8=E3=83=B3=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=80=E3=83=BC=E5=AE=9F=E8=A3=85=20=E2=80=94=20?= =?UTF-8?q?=E3=83=91=E3=83=BC=E3=82=B9=E3=82=A8=E3=83=A9=E3=83=BC=E8=A7=A3?= =?UTF-8?q?=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nf_cbor_encode(): Netflix 互換 CBOR エンコーディング - 全整数キーを 8 バイト形式 (0x1b + uint64) でエンコード - 全 map の前に tag(55799) (0xd9d9f7) を付与 - test_appboot_e2e.py を cbor2.dumps → nf_cbor_encode に移行 - サーバーエラーが errorcode=1 (パース失敗) → errorcode=6 (AppID検証) に前進 - verify_full_key_chain.py 13/13 PASS 維持 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/netflix_msl/cbor_encoder.py | 92 +++++++++++++++++++++++++++++++++ tools/test_appboot_e2e.py | 10 ++-- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/netflix_msl/cbor_encoder.py b/src/netflix_msl/cbor_encoder.py index 2b88b5c..e66f223 100644 --- a/src/netflix_msl/cbor_encoder.py +++ b/src/netflix_msl/cbor_encoder.py @@ -65,6 +65,98 @@ class EncodeError(Exception): """CBOR MSL メッセージの構築に失敗した場合.""" +# --------------------------------------------------------------------------- +# Netflix カスタム CBOR エンコーダー +# +# Netflix iOS の MSL 実装は標準 CBOR とは異なるエンコーディング規則を使用: +# 1. 全ての整数値を 8 バイト形式 (major type 0, additional info 27 = 0x1b) でエンコード +# 2. 全ての map の前に CBOR self-describe tag 55799 (0xd9d9f7) を付与 +# 標準 cbor2 ライブラリは最短形式を使うため、サーバーが "Error parsing MSL encodable" を返す。 +# --------------------------------------------------------------------------- + + +import struct as _struct + + +def _nf_encode_uint(value: int) -> bytes: + """整数値を Netflix 形式 (常に 8 バイト) でエンコードする.""" + return b"\x1b" + _struct.pack(">Q", value) + + +def _nf_encode_bytes(data: bytes) -> bytes: + """バイト列を CBOR bstr としてエンコードする.""" + length = len(data) + if length < 24: + return bytes([0x40 | length]) + data + elif length < 256: + return b"\x58" + bytes([length]) + data + elif length < 65536: + return b"\x59" + _struct.pack(">H", length) + data + else: + return b"\x5a" + _struct.pack(">I", length) + data + + +def _nf_encode_text(text: str) -> bytes: + """テキスト文字列を CBOR tstr としてエンコードする.""" + data = text.encode("utf-8") + length = len(data) + if length < 24: + return bytes([0x60 | length]) + data + elif length < 256: + return b"\x78" + bytes([length]) + data + elif length < 65536: + return b"\x79" + _struct.pack(">H", length) + data + else: + return b"\x7a" + _struct.pack(">I", length) + data + + +def nf_cbor_encode(obj: object) -> bytes: + """Netflix 互換の CBOR エンコードを行う. + + - dict → tag(55799) + map, 整数キーは 8 バイト形式 + - bytes → bstr + - str → tstr + - int → 8 バイト uint + - bool → CBOR true/false + - list → array + """ + if isinstance(obj, dict): + items = sorted(obj.items(), key=lambda kv: (isinstance(kv[0], str), kv[0])) + header = b"\xd9\xd9\xf7" # tag(55799) + n = len(items) + if n < 24: + header += bytes([0xA0 | n]) + else: + header += b"\xb8" + bytes([n]) + body = b"" + for k, v in items: + if isinstance(k, int): + body += _nf_encode_uint(k) + elif isinstance(k, str): + body += _nf_encode_text(k) + else: + raise EncodeError(f"Unsupported key type: {type(k)}") + body += nf_cbor_encode(v) + return header + body + elif isinstance(obj, bytes): + return _nf_encode_bytes(obj) + elif isinstance(obj, str): + return _nf_encode_text(obj) + elif isinstance(obj, int): + return _nf_encode_uint(obj) + elif isinstance(obj, bool): + return b"\xf5" if obj else b"\xf4" + elif isinstance(obj, list): + n = len(obj) + if n < 24: + header = bytes([0x80 | n]) + else: + header = b"\x98" + bytes([n]) + return header + b"".join(nf_cbor_encode(item) for item in obj) + else: + raise EncodeError(f"Unsupported type: {type(obj)}") + + class CborMslEncoder: """iOS CBOR MSL リクエストメッセージを構築するエンコーダー.""" diff --git a/tools/test_appboot_e2e.py b/tools/test_appboot_e2e.py index 1a91fb0..b1d7c80 100644 --- a/tools/test_appboot_e2e.py +++ b/tools/test_appboot_e2e.py @@ -70,6 +70,7 @@ from netflix_msl.constants import ( # noqa: E402 IOS_KDF_PSK, IOS_KEY336_DEVICE_HEADER, ) +from netflix_msl.cbor_encoder import nf_cbor_encode # noqa: E402 from netflix_msl.crypto import NetflixCrypto # noqa: E402 # --------------------------------------------------------------------------- @@ -238,7 +239,7 @@ def build_key_request_data( KEYEX_IDENTITY: esn, # identity = 完全 ESN (サフィックスなし) KEYEX_NONCE: k9_xor_nonce, } - return cbor2.dumps(key_request), k9_xor_nonce, nonce_7b + return nf_cbor_encode(key_request), k9_xor_nonce, nonce_7b # --------------------------------------------------------------------------- @@ -264,9 +265,12 @@ def build_appboot_message( """ signature = hmac_mod.new(sign_key_0, key_request_bytes, hashlib.sha256).digest() - return cbor2.dumps( + # entity_auth_data を Netflix 形式 CBOR でエンコード + ead_bytes = nf_cbor_encode(entity_auth_data) + + return nf_cbor_encode( { - KEY_ENTITY_AUTH: cbor2.dumps(entity_auth_data), + KEY_ENTITY_AUTH: ead_bytes, KEY_KEY_EXCHANGE: key_request_bytes, KEY_MESSAGE_SIG: signature, }