Created
February 20, 2026 06:31
-
-
Save darcyliu/0f15d22751829f2fa4f5c811e15b67c8 to your computer and use it in GitHub Desktop.
Building a Command-Line Tool to Verify iOS AASA Files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import sys | |
| import json | |
| import urllib.request | |
| import urllib.error | |
| import ssl | |
| def verify_aasa(domain): | |
| # Ensure domain doesn't have http/https prefix | |
| if domain.startswith('http://') or domain.startswith('https://'): | |
| domain = domain.split('://')[1].split('/')[0] | |
| # Check direct paths and Apple's CDN | |
| paths = [ | |
| f"https://{domain}/.well-known/apple-app-site-association", | |
| f"https://{domain}/apple-app-site-association", | |
| f"https://app-site-association.cdn-apple.com/a/v1/{domain}" | |
| ] | |
| aasa_content = None | |
| success_url = None | |
| # Apple requires a valid cert, but local python environments on macOS often lack root certs. | |
| # We will try to use certifi if available, otherwise disable verification with a warning. | |
| try: | |
| import certifi | |
| ctx = ssl.create_default_context(cafile=certifi.where()) | |
| except ImportError: | |
| ctx = ssl.create_default_context() | |
| ctx.check_hostname = False | |
| ctx.verify_mode = ssl.CERT_NONE | |
| print("β οΈ Warning: 'certifi' module not found. SSL certificate verification is disabled.") | |
| print(f"π Verifying AASA file for domain: {domain}\n") | |
| for url in paths: | |
| print(f"Checking {url} ...") | |
| try: | |
| req = urllib.request.Request(url, headers={'User-Agent': 'AASA-Verifier/1.0'}) | |
| with urllib.request.urlopen(req, timeout=10, context=ctx) as response: | |
| if response.status >= 200 and response.status < 300: | |
| print(f"β Found AASA file at {url}") | |
| aasa_content = response.read().decode('utf-8') | |
| success_url = url | |
| content_type = response.headers.get('Content-Type', 'None') | |
| print(f"βΉοΈ Content-Type: {content_type}") | |
| if 'application/json' not in content_type.lower() and 'text/json' not in content_type.lower() and content_type != 'None': | |
| print("β οΈ Note: Apple generally expects standard JSON response (or no content-type), but other types were found.") | |
| break | |
| except urllib.error.HTTPError as e: | |
| print(f"β HTTP Error: {e.code} - {e.reason}") | |
| except urllib.error.URLError as e: | |
| print(f"β URL Error: {e.reason}") | |
| except Exception as e: | |
| print(f"β Error: {e}") | |
| if not aasa_content: | |
| print("\nβ Failed to find a valid AASA file on the provided domain.") | |
| return | |
| # Try parsing as JSON | |
| try: | |
| data = json.loads(aasa_content) | |
| print("\nβ Successfully parsed as JSON.") | |
| except json.JSONDecodeError as e: | |
| print(f"\nβ Failed to parse as JSON: {e}") | |
| print("Note: If the file is PKCS7 signed, this tool currently expects plain JSON (which is the modern standard for iOS 9+ Universal Links).") | |
| return | |
| # Validate structure | |
| print("\n--- Validating AASA Structure ---") | |
| # Applinks | |
| if 'applinks' in data: | |
| print("β Found 'applinks' key.") | |
| applinks = data['applinks'] | |
| if 'details' in applinks: | |
| print("β Found 'applinks.details' key.") | |
| details = applinks['details'] | |
| if isinstance(details, list): | |
| if len(details) > 0: | |
| for i, app in enumerate(details): | |
| appID = app.get('appID') | |
| appIDs = app.get('appIDs') | |
| if appID: | |
| print(f"\n π± App {i+1}: appID = {appID}") | |
| elif appIDs: | |
| print(f"\n π± App {i+1}: appIDs = {appIDs}") | |
| else: | |
| print(f"\n β App {i+1}: Missing 'appID' or 'appIDs'. Expected format <TeamIdentifier>.<BundleIdentifier>") | |
| paths = app.get('paths') | |
| components = app.get('components') | |
| if components: | |
| print(f" β Uses modern 'components' routing.") | |
| elif paths: | |
| print(f" β Uses legacy 'paths' routing.") | |
| else: | |
| print(f" β οΈ Warning: No 'paths' or 'components' defined for this app.") | |
| else: | |
| print("β οΈ Note: 'applinks.details' is an empty list.") | |
| else: | |
| print("β 'applinks.details' should be an array (list).") | |
| else: | |
| print("β Missing 'details' inside 'applinks'.") | |
| else: | |
| print("β οΈ Warning: No 'applinks' key found for Universal Links.") | |
| # Webcredentials | |
| if 'webcredentials' in data: | |
| print("\nβ Found 'webcredentials' key.") | |
| apps = data['webcredentials'].get('apps', []) | |
| print(f" Includes {len(apps)} app(s) for shared web credentials.") | |
| # Appclips | |
| if 'appclips' in data: | |
| print("\nβ Found 'appclips' key.") | |
| apps = data['appclips'].get('apps', []) | |
| print(f" Includes {len(apps)} app(s) for App Clips.") | |
| print("\nπ Summary: AASA file syntax looks good!") | |
| if __name__ == '__main__': | |
| if len(sys.argv) != 2: | |
| print("Usage: python3 verify_aasa.py <domain>") | |
| print("Example: python3 verify_aasa.py github.com") | |
| sys.exit(1) | |
| verify_aasa(sys.argv[1]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment