Enhance macOS build scripts for provisioning profile handling and identity management
This commit improves the `find_mas_provisioning_profile.sh` script by expanding the search for provisioning profiles to include both `.provisionprofile` and `.mobileprovision` files. It also introduces a new function to print SHA-1 values for identities, helping to avoid ambiguity when multiple identities share the same display name. Additionally, the `check_signing_identities.sh` script is updated to provide clearer recommendations for using SHA-1 hashes with codesigning and installer identities, enhancing the overall usability and clarity for developers working with macOS builds.
This commit is contained in:
210
build_tools/macos/find_mas_provisioning_profile.py
Normal file
210
build_tools/macos/find_mas_provisioning_profile.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import plistlib
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileInfo:
|
||||
path: Path
|
||||
uuid: str
|
||||
name: str
|
||||
team_id: str
|
||||
expiration: Optional[dt.datetime]
|
||||
app_id: str
|
||||
platforms: List[str]
|
||||
|
||||
|
||||
def run(cmd: List[str]) -> Tuple[int, bytes, bytes]:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
return p.returncode, out, err
|
||||
|
||||
|
||||
def decode_profile_plist_bytes(profile_path: Path) -> Optional[bytes]:
|
||||
# Provisioning profiles are typically CMS/PKCS#7 SignedData blobs whose payload is a plist.
|
||||
# However, some tools store them as plain XML plists already. Also, LibreSSL/OpenSSL behavior
|
||||
# differs: LibreSSL usually requires an explicit '-verify' to emit the embedded content.
|
||||
data = profile_path.read_bytes()
|
||||
|
||||
# Fast path: already a plist (XML).
|
||||
if b"<plist" in data:
|
||||
return data
|
||||
|
||||
# Decode CMS/PKCS7 to extract embedded plist payload.
|
||||
# Try a small matrix of commands/inform formats for compatibility.
|
||||
candidates: List[List[str]] = []
|
||||
for inform in ("DER", "PEM"):
|
||||
candidates.append(["/usr/bin/openssl", "cms", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
||||
candidates.append(["/usr/bin/openssl", "smime", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
||||
|
||||
for cmd in candidates:
|
||||
rc, out, _err = run(cmd)
|
||||
if rc == 0 and b"<plist" in out:
|
||||
return out
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_plist(plist_bytes: bytes) -> Dict[str, Any]:
|
||||
return plistlib.loads(plist_bytes)
|
||||
|
||||
|
||||
def iso(dt_obj: Optional[dt.datetime]) -> str:
|
||||
if not dt_obj:
|
||||
return "(unknown)"
|
||||
# Force UTC-ish display if tz-aware, otherwise as-is.
|
||||
try:
|
||||
return dt_obj.isoformat().replace("+00:00", "Z")
|
||||
except Exception:
|
||||
return str(dt_obj)
|
||||
|
||||
|
||||
def safe_str(v: Any) -> str:
|
||||
if v is None:
|
||||
return ""
|
||||
if isinstance(v, bytes):
|
||||
try:
|
||||
return v.decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
return repr(v)
|
||||
return str(v)
|
||||
|
||||
|
||||
def profile_info_from_plist(path: Path, p: Dict[str, Any]) -> ProfileInfo:
|
||||
uuid = safe_str(p.get("UUID", "")) or "(unknown)"
|
||||
name = safe_str(p.get("Name", "")) or "(unknown)"
|
||||
team_ids = p.get("TeamIdentifier") or []
|
||||
team_id = safe_str(team_ids[0]) if isinstance(team_ids, list) and team_ids else ""
|
||||
if not team_id:
|
||||
prefixes = p.get("ApplicationIdentifierPrefix") or []
|
||||
team_id = safe_str(prefixes[0]) if isinstance(prefixes, list) and prefixes else "(unknown)"
|
||||
exp = p.get("ExpirationDate")
|
||||
expiration = exp if isinstance(exp, dt.datetime) else None
|
||||
ent = p.get("Entitlements") or {}
|
||||
app_id = safe_str(ent.get("application-identifier") or ent.get("com.apple.application-identifier") or "") or "(unknown)"
|
||||
platforms = p.get("Platform") or []
|
||||
if isinstance(platforms, str):
|
||||
platforms = [platforms]
|
||||
platforms = [safe_str(x) for x in platforms if x is not None]
|
||||
return ProfileInfo(path=path, uuid=uuid, name=name, team_id=team_id or "(unknown)", expiration=expiration, app_id=app_id, platforms=platforms)
|
||||
|
||||
|
||||
def score(profile: ProfileInfo, bundle_id: str, now: dt.datetime) -> Tuple[int, str]:
|
||||
# Prefer non-expired.
|
||||
if profile.expiration and profile.expiration < now:
|
||||
return (-1, "expired")
|
||||
|
||||
score = 0
|
||||
reason = []
|
||||
|
||||
# Prefer exact app id match TEAMID.bundle_id
|
||||
if profile.team_id != "(unknown)" and profile.app_id != "(unknown)":
|
||||
exact = f"{profile.team_id}.{bundle_id}"
|
||||
if profile.app_id == exact:
|
||||
score += 100
|
||||
reason.append(f"exact {profile.app_id}")
|
||||
elif profile.app_id.endswith(f".{bundle_id}"):
|
||||
score += 60
|
||||
reason.append(f"endswith {profile.app_id}")
|
||||
elif "*" in profile.app_id and profile.app_id.startswith(f"{profile.team_id}."):
|
||||
score += 40
|
||||
reason.append(f"wildcard {profile.app_id}")
|
||||
|
||||
# Heuristic: name suggests MAS.
|
||||
n = profile.name.lower()
|
||||
if "mac app store" in n or "app store" in n or "appstore" in n:
|
||||
score += 5
|
||||
reason.append("name looks like MAS")
|
||||
|
||||
# Prefer macOS platform if present.
|
||||
plats = [p.lower() for p in profile.platforms]
|
||||
if any("macos" in p for p in plats):
|
||||
score += 2
|
||||
reason.append("platform macos")
|
||||
|
||||
return (score, ", ".join(reason) if reason else "")
|
||||
|
||||
|
||||
def find_profiles() -> List[Path]:
|
||||
dirs = [
|
||||
Path.home() / "Library" / "Developer" / "Xcode" / "UserData" / "Provisioning Profiles",
|
||||
Path.home() / "Library" / "MobileDevice" / "Provisioning Profiles",
|
||||
]
|
||||
out: List[Path] = []
|
||||
for d in dirs:
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for p in d.iterdir():
|
||||
if p.is_file() and (p.name.endswith(".provisionprofile") or p.name.endswith(".mobileprovision")):
|
||||
out.append(p)
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bundle-id", required=True)
|
||||
args = ap.parse_args()
|
||||
bundle_id = args.bundle_id
|
||||
|
||||
if not Path("/usr/bin/openssl").exists():
|
||||
print("Error: /usr/bin/openssl not found.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
candidates = find_profiles()
|
||||
if not candidates:
|
||||
print("No provisioning profiles found in common locations.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Scanning {len(candidates)} provisioning profile(s) for bundle id: {bundle_id}")
|
||||
print()
|
||||
print(f"{'No.':<4} {'UUID':<36} {'TeamID':<10} {'Expires':<25} {'AppID':<45} Path")
|
||||
print(f"{'-'*4} {'-'*36} {'-'*10} {'-'*25} {'-'*45} ----")
|
||||
|
||||
infos: List[ProfileInfo] = []
|
||||
for i, p in enumerate(candidates, start=1):
|
||||
plist_bytes = decode_profile_plist_bytes(p)
|
||||
if not plist_bytes:
|
||||
continue
|
||||
try:
|
||||
pl = parse_plist(plist_bytes)
|
||||
info = profile_info_from_plist(p, pl)
|
||||
infos.append(info)
|
||||
print(f"{i:<4} {info.uuid:<36} {info.team_id:<10} {iso(info.expiration):<25} {info.app_id:<45} {info.path}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not infos:
|
||||
print("\nCould not decode any provisioning profiles with openssl cms.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
now = dt.datetime.now(dt.timezone.utc)
|
||||
best: Optional[Tuple[int, str, ProfileInfo]] = None
|
||||
for info in infos:
|
||||
sc, why = score(info, bundle_id, now)
|
||||
if best is None or sc > best[0]:
|
||||
best = (sc, why, info)
|
||||
|
||||
print()
|
||||
if best is None or best[0] <= 0:
|
||||
print(f"Could not confidently auto-select a profile for {bundle_id}.", file=sys.stderr)
|
||||
print("Pick the profile whose AppID is TEAMID.<bundle-id> and is a macOS Mac App Store profile.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
_, why, info = best
|
||||
print("Recommended profile:")
|
||||
print(f" {info.path}")
|
||||
print(f" reason: {why}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user