Skip to content

Instantly share code, notes, and snippets.

@darcyliu
Created February 20, 2026 06:31
Show Gist options
  • Select an option

  • Save darcyliu/0f15d22751829f2fa4f5c811e15b67c8 to your computer and use it in GitHub Desktop.

Select an option

Save darcyliu/0f15d22751829f2fa4f5c811e15b67c8 to your computer and use it in GitHub Desktop.
Building a Command-Line Tool to Verify iOS AASA Files
#!/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