DRMLab/tools/playready_rev.py
2026-02-07 19:39:32 +02:00

207 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
PlayReady public key hash revocation checker.
- Fetches Microsoft's revocation XML.
- Extracts the PlayReady Silverlight Runtime revocation list.
- Prints all 32-byte public key hashes (Base64).
- If a hash is provided (argv[1] or prompted), reports whether it's revoked.
- If 'bgroupcert.dat' in directory, extract the hash and reports whether it's revoked.
Compatible with Python 3.8+.
"""
from __future__ import annotations
import base64
import struct
import sys
import uuid
from typing import Iterable, List, Optional
import requests
import xmltodict
# --- Configuration ----------------------------------------------------------------
REV_URL = "https://go.microsoft.com/fwlink/?LinkId=110086"
# GUID for PlayReady Silverlight Runtime (as raw bytes like original code)
GUID_PR_SILVERLIGHT_RUNTIME = bytes([
0x4E, 0x9D, 0x8C, 0x8A, 0xB6, 0x52, 0x45, 0xA7,
0x97, 0x91, 0x69, 0x25, 0xA6, 0xB4, 0x79, 0x1F
])
# If construct is available, well use it to parse the binary blob declaratively.
try:
from construct import Struct as CStruct, Bytes, Int32ub
_HAS_CONSTRUCT = True
except Exception:
_HAS_CONSTRUCT = False
# --- Helpers ----------------------------------------------------------------------
def _expected_guid_le_hex(guid_bytes: bytes) -> str:
"""
The source blob compares the first 16 bytes against UUID(...).bytes_le.
Keep that exact behavior to match the original script.
"""
return uuid.UUID(guid_bytes.hex()).bytes_le.hex()
def _parse_revocation_blob_with_construct(blob: bytes) -> Optional[List[bytes]]:
"""
Parse a revocation list blob using construct (if installed).
Format per original code:
[0:16] GUID
[16:20] unknown / reserved
[20:24] entries_count (u32 big-endian)
[24: ] entries_count * 32-byte hashes
"""
if not _HAS_CONSTRUCT:
return None
RevHeader = CStruct(
"type_guid" / Bytes(16),
"reserved" / Bytes(4),
"entries_count" / Int32ub,
)
# First parse header to get count
if len(blob) < 24:
return []
header = RevHeader.parse(blob[:24])
# Verify GUID (same logic/endianness as original)
if blob[:16].hex() != _expected_guid_le_hex(GUID_PR_SILVERLIGHT_RUNTIME):
return []
# Extract entries
start = 24
end = start + (header.entries_count * 32)
if end > len(blob):
# Truncated blob; be defensive
end = min(len(blob), start + ((len(blob) - start) // 32) * 32)
entries = [blob[i:i+32] for i in range(start, end, 32)]
return entries
def _parse_revocation_blob_manually(blob: bytes) -> List[bytes]:
"""
Manual parser (no construct). Mirrors the original byte slicing exactly.
"""
if len(blob) < 24:
return []
# GUID check with the same endianness trick as original
if blob[:16].hex() != _expected_guid_le_hex(GUID_PR_SILVERLIGHT_RUNTIME):
return []
# entries_count is big-endian u32 at offset 20
entries_count = struct.unpack_from(">I", blob, 20)[0]
cursor = 24
out: List[bytes] = []
for _ in range(entries_count):
if cursor + 32 > len(blob):
break
out.append(blob[cursor:cursor+32])
cursor += 32
return out
def _iter_revocation_lists(xml_root: dict) -> Iterable[bytes]:
"""
Yields each decoded revocation list (ListData) as raw bytes.
Handles the cases where Revocation is a dict or a list of dicts.
"""
rev = xml_root.get("RevInfo", {}).get("Revocation")
if rev is None:
return
items = rev if isinstance(rev, list) else [rev]
for item in items:
data_b64 = item.get("ListData")
if not data_b64:
continue
try:
yield base64.b64decode(data_b64)
except Exception:
continue
def _fetch_revocation_index(url: str = REV_URL) -> dict:
resp = requests.get(url, timeout=20)
resp.raise_for_status()
# xmltodict produces nested OrderedDict-like structures; dict() is fine
return xmltodict.parse(resp.text)
def _collect_public_key_hashes(xml_root: dict) -> List[str]:
"""
Returns Base64-encoded 32-byte public key hashes from the
PlayReady Silverlight Runtime revocation list.
"""
hashes_b64: List[str] = []
for blob in _iter_revocation_lists(xml_root):
# Try construct first (if available), then fallback
entries = _parse_revocation_blob_with_construct(blob) if _HAS_CONSTRUCT else None
if entries is None:
entries = _parse_revocation_blob_manually(blob)
if not entries:
continue
for entry in entries:
hashes_b64.append(base64.b64encode(entry).decode("ascii"))
return hashes_b64
# --- CLI --------------------------------------------------------------------------
def main(argv: List[str]) -> int:
try:
xml_root = _fetch_revocation_index(REV_URL)
except Exception as e:
print(f"Failed to fetch or parse revocation index: {e}", file=sys.stderr)
return 2
publickey_hashes = _collect_public_key_hashes(xml_root)
# Print the list (like the original)
if publickey_hashes:
print("\n".join(publickey_hashes))
# Determine the input hash
pubkey_hash: Optional[str] = None
if len(argv) > 1 and argv[1]:
pubkey_hash = argv[1].strip()
print()
print("Provided hash:", pubkey_hash)
if not pubkey_hash:
bcert = open("bgroupcert.dat", "rb").read()
pubkey_hash = base64.b64encode(bcert[0x48:0x68]).decode()
print()
print("Calculated hash:", pubkey_hash)
if not pubkey_hash:
print("No hash provided.")
return 1
if pubkey_hash in publickey_hashes:
print("one of hashes in CRL match the input hash.")
print("PlayReady Cert is revoked.")
return 0
else:
print("PlayReady Cert is valid.")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))