Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/launchpad/api/update_api_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class AppleAppInfo(BaseModel):
is_code_signature_valid: Optional[bool] = None
code_signature_errors: Optional[List[str]] = None
main_binary_uuid: Optional[str] = None
profile_expiration_date: Optional[str] = None
certificate_expiration_date: Optional[str] = None
# TODO: add "date_built" field once exposed in 'AppleAppInfo'


Expand Down
2 changes: 2 additions & 0 deletions src/launchpad/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
is_code_signature_valid=app_info.is_code_signature_valid,
code_signature_errors=app_info.code_signature_errors,
main_binary_uuid=app_info.main_binary_uuid,
profile_expiration_date=app_info.profile_expiration_date,
certificate_expiration_date=app_info.certificate_expiration_date,
)
# TODO: add "date_built" and custom android fields

Expand Down
45 changes: 45 additions & 0 deletions src/launchpad/size/analyzers/apple.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import lief

from cryptography import x509

from launchpad.artifacts.apple.zipped_xcarchive import BinaryInfo, ZippedXCArchive
from launchpad.artifacts.artifact import AppleArtifact
from launchpad.parsers.apple.macho_parser import MachOParser
Expand Down Expand Up @@ -213,8 +215,15 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
provisioning_profile = xcarchive.get_provisioning_profile()
codesigning_type = None
profile_name = None
profile_expiration_date = None
certificate_expiration_date = None
if provisioning_profile:
codesigning_type, profile_name = self._get_profile_type(provisioning_profile)
expiration_date = provisioning_profile.get("ExpirationDate")
if expiration_date:
# Convert datetime to ISO format string
profile_expiration_date = expiration_date.isoformat()
certificate_expiration_date = self._extract_certificate_expiration_date(provisioning_profile)

supported_platforms = plist.get("CFBundleSupportedPlatforms", [])
is_simulator = "iphonesimulator" in supported_platforms or plist.get("DTPlatformName") == "iphonesimulator"
Expand All @@ -241,6 +250,8 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
is_simulator=is_simulator,
codesigning_type=codesigning_type,
profile_name=profile_name,
profile_expiration_date=profile_expiration_date,
certificate_expiration_date=certificate_expiration_date,
is_code_signature_valid=is_code_signature_valid,
code_signature_errors=code_signature_errors,
main_binary_uuid=xcarchive.get_main_binary_uuid(),
Expand Down Expand Up @@ -282,6 +293,40 @@ def _get_profile_type(self, profile_data: dict[str, Any]) -> Tuple[str, str]:
# If no devices are provisioned, it's an app store profile
return "appstore", profile_name

def _extract_certificate_expiration_date(self, provisioning_profile: dict[str, Any]) -> str | None:
"""Extract the earliest expiration date from developer certificates.

Args:
provisioning_profile: Dictionary containing the mobileprovision contents

Returns:
ISO format string of the earliest certificate expiration date, or None if no certificates found
"""
developer_certs = provisioning_profile.get("DeveloperCertificates", [])
if not developer_certs:
return None

earliest_expiration = None

for cert_data in developer_certs:
try:
# Parse DER certificate
cert = x509.load_der_x509_certificate(cert_data)
expiration_date = cert.not_valid_after_utc

# Track the earliest expiration date
if earliest_expiration is None or expiration_date < earliest_expiration:
earliest_expiration = expiration_date

except Exception as e:
logger.error(f"Failed to parse certificate: {e}")
continue

if earliest_expiration:
return earliest_expiration.isoformat()

return None

def _generate_insight_with_tracing(
self, insight_class: type, insights_input: InsightsInput, insight_name: str
) -> Any:
Expand Down
2 changes: 2 additions & 0 deletions src/launchpad/size/models/apple.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ class AppleAppInfo(BaseAppInfo):
None, description="Type of codesigning used (development, adhoc, appstore, enterprise)"
)
profile_name: str | None = Field(None, description="Name of the provisioning profile used")
profile_expiration_date: str | None = Field(None, description="Expiration date of the provisioning profile")
certificate_expiration_date: str | None = Field(None, description="Expiration date of the developer certificate")
is_code_signature_valid: bool = Field(True, description="Whether the app's code signature is valid")
code_signature_errors: List[str] = Field(
default_factory=list, description="List of code signature validation errors"
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/test_apple_basic_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ def test_basic_info(self) -> None:
assert basic_info.is_code_signature_valid is True
assert basic_info.code_signature_errors == []
assert basic_info.main_binary_uuid == "BEB3C0D6-2518-343D-BB6F-FF5581C544E8"
assert basic_info.profile_expiration_date is not None
assert basic_info.profile_expiration_date == "2025-12-02T18:15:00"
assert basic_info.certificate_expiration_date is not None
assert basic_info.certificate_expiration_date == "2025-01-01T17:56:11+00:00"