WinterGram/scripts/generate-fake-profiles.py

181 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Generate fake provisioning profiles for the WinterGram dev bundle id."""
import datetime
import json
import os
import plistlib
import subprocess
import sys
import uuid
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7
REPO_ROOT = Path(__file__).resolve().parent.parent
CONFIG_PATH = REPO_ROOT / "build-system/wintergram-development-configuration.json"
SOURCE_PROFILES_DIR = REPO_ROOT / "build-system/fake-codesigning/profiles"
SOURCE_CERTS_DIR = REPO_ROOT / "build-system/fake-codesigning/certs"
SELF_SIGNED_P12 = SOURCE_CERTS_DIR / "SelfSigned.p12"
# mapping from application-identifier suffix -> output file name (without .mobileprovision)
PROFILE_NAME_MAPPING = {
".SiriIntents": "Intents",
".NotificationContent": "NotificationContent",
".NotificationService": "NotificationService",
".Share": "Share",
"": "Telegram",
".watchkitapp": "WatchApp",
".watchkitapp.watchkitextension": "WatchExtension",
".Widget": "Widget",
".BroadcastUpload": "BroadcastUpload",
}
def load_self_signed_cert_and_key():
with open(SELF_SIGNED_P12, "rb") as f:
key, cert, _ = pkcs12.load_key_and_certificates(f.read(), password=b"")
if key is None or cert is None:
raise RuntimeError("Could not load key/cert from SelfSigned.p12")
return key, cert
def extract_plist(profile_path: Path) -> dict:
result = subprocess.run(
["openssl", "smime", "-inform", "der", "-verify", "-noverify", "-in", str(profile_path)],
capture_output=True,
check=True,
)
return plistlib.loads(result.stdout)
def replace_in_string(s: str, old_team: str, new_team: str, old_bundle: str, new_bundle: str) -> str:
s = s.replace(old_team + "." + old_bundle, new_team + "." + new_bundle)
s = s.replace(old_bundle, new_bundle)
s = s.replace(old_team, new_team)
return s
def replace_in_value(value, old_team: str, new_team: str, old_bundle: str, new_bundle: str):
if isinstance(value, str):
return replace_in_string(value, old_team, new_team, old_bundle, new_bundle)
if isinstance(value, list):
return [replace_in_value(v, old_team, new_team, old_bundle, new_bundle) for v in value]
if isinstance(value, dict):
return {k: replace_in_value(v, old_team, new_team, old_bundle, new_bundle) for k, v in value.items()}
if isinstance(value, bytes):
try:
text = value.decode("utf-8")
replaced = replace_in_string(text, old_team, new_team, old_bundle, new_bundle)
return replaced.encode("utf-8")
except UnicodeDecodeError:
return value
return value
def profile_suffix_for(plist_data: dict) -> str:
app_id = plist_data["Entitlements"]["application-identifier"]
# We expect <old_team>.<old_bundle><suffix>
return app_id.split(".", 2)[2] if "." in app_id else ""
def generate_profile(source_path: Path, new_team: str, new_bundle: str, key, cert) -> tuple[str, bytes]:
plist_data = extract_plist(source_path)
old_team = plist_data["TeamIdentifier"][0]
old_bundle = plist_data["Entitlements"]["application-identifier"][len(old_team) + 1 :]
# strip known suffixes to find the base bundle id
old_bundle_base = old_bundle
for suffix in sorted(PROFILE_NAME_MAPPING.keys(), key=len, reverse=True):
if suffix and old_bundle.endswith(suffix):
old_bundle_base = old_bundle[: -len(suffix)]
break
new_bundle_base = new_bundle
plist_data = replace_in_value(plist_data, old_team, new_team, old_bundle_base, new_bundle_base)
# Some fields can't be string-replaced blindly; set them explicitly.
plist_data["ApplicationIdentifierPrefix"] = [new_team]
plist_data["TeamIdentifier"] = [new_team]
plist_data["UUID"] = str(uuid.uuid4()).upper()
plist_data["Name"] = f"WinterGram {source_path.stem}"
plist_data["TeamName"] = "WinterGram Self-Signed"
plist_data["IsXcodeManaged"] = False
now = datetime.datetime.now(datetime.timezone.utc)
plist_data["CreationDate"] = now
plist_data["ExpirationDate"] = now + datetime.timedelta(days=365)
# Replace embedded developer certificate with the self-signed one.
cert_der = cert.public_bytes(serialization.Encoding.DER)
plist_data["DeveloperCertificates"] = [cert_der]
# Strip capabilities we don't want/need for a sideload dev build.
entitlements = plist_data.get("Entitlements", {})
entitlements["get-task-allow"] = True
entitlements.pop("beta-reports-active", None)
entitlements["aps-environment"] = "development"
plist_data["Entitlements"] = entitlements
# Determine output name from the original suffix.
original_suffix = ""
for suffix in sorted(PROFILE_NAME_MAPPING.keys(), key=len, reverse=True):
if suffix and old_bundle.endswith(suffix):
original_suffix = suffix
break
output_name = PROFILE_NAME_MAPPING.get(original_suffix, source_path.stem) + ".mobileprovision"
# Re-sign as CMS SignedData (DER) using the self-signed certificate.
plist_bytes = plistlib.dumps(plist_data)
builder = pkcs7.PKCS7SignatureBuilder(
data=plist_bytes,
signers=[(cert, key, hashes.SHA256(), None)],
additional_certs=[cert],
)
signed = builder.sign(serialization.Encoding.DER, [pkcs7.PKCS7Options.Binary])
return output_name, signed
def main():
if len(sys.argv) > 1:
output_dir = Path(sys.argv[1])
else:
output_dir = REPO_ROOT / "build-system/fake-codesigning-generated"
output_profiles_dir = output_dir / "profiles"
output_certs_dir = output_dir / "certs"
output_profiles_dir.mkdir(parents=True, exist_ok=True)
output_certs_dir.mkdir(parents=True, exist_ok=True)
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
config = json.load(f)
new_team = config["team_id"]
new_bundle = config["bundle_id"]
print(f"Generating fake profiles for team={new_team} bundle={new_bundle} ...")
key, cert = load_self_signed_cert_and_key()
# Copy the self-signed cert material so the output dir is self-contained.
for src in SOURCE_CERTS_DIR.iterdir():
if src.is_file():
(output_certs_dir / src.name).write_bytes(src.read_bytes())
generated = []
for source_path in sorted(SOURCE_PROFILES_DIR.glob("*.mobileprovision")):
output_name, signed = generate_profile(source_path, new_team, new_bundle, key, cert)
out_path = output_profiles_dir / output_name
out_path.write_bytes(signed)
generated.append(output_name)
print(f" {source_path.name} -> {output_name}")
print(f"Wrote {len(generated)} profiles to {output_profiles_dir}")
if __name__ == "__main__":
main()