feat(cbor): Netflix カスタム CBOR エンコーダー実装 — パースエラー解消

- 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) <noreply@anthropic.com>
This commit is contained in:
tkgstrator
2026-04-09 15:58:34 +00:00
co-authored by Claude Opus 4.6
parent 455de1ebee
commit e74869dbf5
2 changed files with 99 additions and 3 deletions
+92
View File
@@ -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 リクエストメッセージを構築するエンコーダー."""
+7 -3
View File
@@ -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,
}