protool 2.0.0__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,28 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: protool
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: A tool for dealing with provisioning profiles
5
5
  Home-page: https://github.com/Microsoft/protool
6
6
  License: MIT
7
7
  Keywords: provisioning,profiles,apple,ios,xcode,mobileprovision
8
8
  Author: Dale Myers
9
9
  Author-email: dalemy@microsoft.com
10
- Requires-Python: >=3.8,<4.0
10
+ Requires-Python: >=3.11,<4.0
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Environment :: MacOS X
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.10
17
21
  Classifier: Programming Language :: Python :: 3.8
18
22
  Classifier: Programming Language :: Python :: 3.9
19
- Classifier: Programming Language :: Python :: 3.10
20
- Classifier: Programming Language :: Python :: 3.11
21
23
  Classifier: Topic :: Software Development
22
24
  Classifier: Topic :: Utilities
23
- Requires-Dist: pyOpenSSL (>=23.2.0,<24.0.0)
25
+ Requires-Dist: pyOpenSSL (>=25.1.0,<26.0.0)
24
26
  Project-URL: Repository, https://github.com/Microsoft/protool
25
27
  Description-Content-Type: text/markdown
26
28
 
@@ -12,10 +12,57 @@ import shutil
12
12
  import subprocess
13
13
  import sys
14
14
  import tempfile
15
- from typing import Any, cast, Dict, List, Optional
15
+ from typing import Any, cast
16
16
  from OpenSSL import crypto
17
17
 
18
18
 
19
+ def _extract_certificate_properties(cert: crypto.X509) -> dict[str, Any]:
20
+ """Extract key properties from an X509 certificate."""
21
+ subject = cert.get_subject()
22
+ issuer = cert.get_issuer()
23
+
24
+ # Helper to safely get X509Name components
25
+ def get_component(name_obj, component: str) -> str | None:
26
+ try:
27
+ return getattr(name_obj, component, None)
28
+ except AttributeError:
29
+ return None
30
+
31
+ # Parse ASN.1 TIME to ISO 8601 format
32
+ def parse_asn1_time(time_bytes: bytes | None) -> str | None:
33
+ if time_bytes is None:
34
+ return None
35
+ time_str = time_bytes.decode("utf-8")
36
+ # ASN.1 format: YYYYMMDDhhmmssZ
37
+ # Convert to ISO 8601: YYYY-MM-DDThh:mm:ssZ
38
+ try:
39
+ return f"{time_str[0:4]}-{time_str[4:6]}-{time_str[6:8]}T{time_str[8:10]}:{time_str[10:12]}:{time_str[12:14]}Z"
40
+ except (IndexError, ValueError):
41
+ return time_str # Return as-is if parsing fails
42
+
43
+ return {
44
+ "CommonName": get_component(subject, "CN"),
45
+ "Organization": get_component(subject, "O"),
46
+ "OrganizationalUnit": get_component(subject, "OU"),
47
+ "Country": get_component(subject, "C"),
48
+ "IssuerCommonName": get_component(issuer, "CN"),
49
+ "IssuerOrganization": get_component(issuer, "O"),
50
+ "SerialNumber": str(
51
+ cert.get_serial_number() # Other libs struggle with large integers
52
+ ),
53
+ "NotBefore": parse_asn1_time(cert.get_notBefore()),
54
+ "NotAfter": parse_asn1_time(cert.get_notAfter()),
55
+ "SignatureAlgorithm": (
56
+ cert.get_signature_algorithm().decode("utf-8")
57
+ if cert.get_signature_algorithm()
58
+ else None
59
+ ),
60
+ "Fingerprint": (
61
+ cert.digest("sha256").decode("utf-8") if hasattr(cert, "digest") else None
62
+ ),
63
+ }
64
+
65
+
19
66
  class ProvisioningType(Enum):
20
67
  """Enum representing the type of provisioning profile."""
21
68
 
@@ -32,22 +79,23 @@ class ProvisioningProfile:
32
79
  file_path: str
33
80
  file_name: str
34
81
  xml: str
35
- _contents: Dict[str, Any]
36
-
37
- app_id_name: Optional[str]
38
- application_identifier_prefix: Optional[str]
39
- creation_date: Optional[datetime.datetime]
40
- platform: Optional[List[str]]
41
- entitlements: Dict[str, Any]
42
- expiration_date: Optional[datetime.datetime]
43
- name: Optional[str]
44
- team_identifier: Optional[List[str]]
45
- team_name: Optional[str]
46
- time_to_live: Optional[int]
47
- uuid: Optional[str]
48
- version: Optional[int]
49
- provisioned_devices: Optional[List[str]]
50
- provisions_all_devices: Optional[bool]
82
+ _contents: dict[str, Any]
83
+ _decode_certificates: bool
84
+
85
+ app_id_name: str | None
86
+ application_identifier_prefix: str | None
87
+ creation_date: datetime.datetime | None
88
+ platform: list[str] | None
89
+ entitlements: dict[str, Any]
90
+ expiration_date: datetime.datetime | None
91
+ name: str | None
92
+ team_identifier: list[str] | None
93
+ team_name: str | None
94
+ time_to_live: int | None
95
+ uuid: str | None
96
+ version: int | None
97
+ provisioned_devices: list[str] | None
98
+ provisions_all_devices: bool | None
51
99
 
52
100
  @property
53
101
  def profile_type(self) -> ProvisioningType:
@@ -66,24 +114,31 @@ class ProvisioningProfile:
66
114
 
67
115
  raise Exception("Unable to determine provisioning profile type")
68
116
 
69
- def developer_certificates(self) -> List[crypto.X509]:
117
+ def developer_certificates(self) -> list[crypto.X509]:
70
118
  """Returns developer certificates as a list of PyOpenSSL X509."""
71
- dev_certs: List[crypto.X509] = []
72
- raw_cert_items: List[str] = cast(
73
- List[str], self._contents.get("DeveloperCertificates", [])
119
+ dev_certs: list[crypto.X509] = []
120
+ raw_cert_items: list[bytes] = cast(
121
+ list[bytes], self._contents.get("DeveloperCertificates", [])
74
122
  )
75
123
 
76
124
  for cert_item in raw_cert_items:
77
125
  loaded_cert: crypto.X509 = crypto.load_certificate(
78
- crypto.FILETYPE_ASN1, cert_item.encode()
126
+ crypto.FILETYPE_ASN1, cert_item
79
127
  )
80
128
  dev_certs.append(loaded_cert)
81
129
 
82
130
  return dev_certs
83
131
 
84
- def __init__(self, file_path: str, *, sort_keys: bool = True) -> None:
132
+ def __init__(
133
+ self,
134
+ file_path: str,
135
+ *,
136
+ sort_keys: bool = True,
137
+ decode_certificates: bool = False,
138
+ ) -> None:
85
139
  self.file_path = os.path.abspath(file_path)
86
140
  self.file_name = os.path.basename(self.file_path)
141
+ self._decode_certificates = decode_certificates
87
142
  self.load_from_disk(sort_keys=sort_keys)
88
143
 
89
144
  def load_from_disk(self, *, sort_keys: bool = True) -> None:
@@ -96,7 +151,16 @@ class ProvisioningProfile:
96
151
 
97
152
  self._parse_contents()
98
153
 
99
- def contents(self) -> Dict[str, Any]:
154
+ # If we decoded certificates, we need to regenerate the XML to include them
155
+ if self._decode_certificates:
156
+ contents_copy = copy.deepcopy(self._contents)
157
+ del contents_copy["DeveloperCertificates"]
158
+ del contents_copy["DER-Encoded-Profile"]
159
+ self.xml = plistlib.dumps(contents_copy, sort_keys=sort_keys).decode(
160
+ "utf-8"
161
+ )
162
+
163
+ def contents(self) -> dict[str, Any]:
100
164
  """Return a copy of the content dict."""
101
165
  return copy.deepcopy(self._contents)
102
166
 
@@ -119,6 +183,19 @@ class ProvisioningProfile:
119
183
  self.provisioned_devices = self._contents.get("ProvisionedDevices")
120
184
  self.provisions_all_devices = self._contents.get("ProvisionsAllDevices", False)
121
185
 
186
+ # Decode certificates if requested
187
+ if self._decode_certificates:
188
+ decoded_certs: list[dict[str, Any]] = []
189
+ for cert in self.developer_certificates():
190
+ try:
191
+ decoded_certs.append(_extract_certificate_properties(cert))
192
+ except Exception as ex:
193
+ # Log error but continue with other certificates
194
+ print(
195
+ f"Warning: Failed to decode certificate: {ex}", file=sys.stderr
196
+ )
197
+ self._contents["DecodedDeveloperCertificates"] = decoded_certs
198
+
122
199
  def _get_xml(self) -> str:
123
200
  """Load the XML contents of a provisioning profile."""
124
201
  if not os.path.exists(self.file_path):
@@ -133,7 +210,7 @@ class ProvisioningProfile:
133
210
  # pylint: enable=too-many-instance-attributes
134
211
 
135
212
 
136
- def profiles(profiles_dir: Optional[str] = None) -> List[ProvisioningProfile]:
213
+ def profiles(profiles_dir: str | None = None) -> list[ProvisioningProfile]:
137
214
  """Returns a list of all currently installed provisioning profiles."""
138
215
  if profiles_dir:
139
216
  dir_path = os.path.expanduser(profiles_dir)
@@ -143,7 +220,7 @@ def profiles(profiles_dir: Optional[str] = None) -> List[ProvisioningProfile]:
143
220
  user_path, "Library", "MobileDevice", "Provisioning Profiles"
144
221
  )
145
222
 
146
- all_profiles = []
223
+ all_profiles: list[ProvisioningProfile] = []
147
224
  for profile in os.listdir(dir_path):
148
225
  full_path = os.path.join(dir_path, profile)
149
226
  _, ext = os.path.splitext(full_path)
@@ -159,8 +236,8 @@ def diff(
159
236
  b_path: str,
160
237
  *,
161
238
  sort_keys: bool = True,
162
- ignore_keys: Optional[List[str]] = None,
163
- tool_override: Optional[str] = None,
239
+ ignore_keys: list[str] | None = None,
240
+ tool_override: str | None = None,
164
241
  ) -> str:
165
242
  """Diff two provisioning profiles."""
166
243
 
@@ -223,7 +300,7 @@ def diff(
223
300
  return diff_contents
224
301
 
225
302
 
226
- def value_for_key(profile_path: str, key: str) -> Optional[Any]:
303
+ def value_for_key(profile_path: str, key: str) -> Any | None:
227
304
  """Return the value for a given key"""
228
305
 
229
306
  profile = ProvisioningProfile(profile_path)
@@ -235,10 +312,10 @@ def value_for_key(profile_path: str, key: str) -> Optional[Any]:
235
312
  return None
236
313
 
237
314
 
238
- def decode(profile_path: str, xml: bool = True):
315
+ def decode(profile_path: str, xml: bool = True, *, decode_certificates: bool = False):
239
316
  """Decode a profile, returning as a dictionary if xml is set to False."""
240
317
 
241
- profile = ProvisioningProfile(profile_path)
318
+ profile = ProvisioningProfile(profile_path, decode_certificates=decode_certificates)
242
319
 
243
320
  if xml:
244
321
  return profile.xml
@@ -94,7 +94,9 @@ def _handle_read(args: argparse.Namespace) -> int:
94
94
  def _handle_decode(args: argparse.Namespace) -> int:
95
95
  """Handle the decode sub command."""
96
96
  try:
97
- print(protool.decode(args.profile))
97
+ print(
98
+ protool.decode(args.profile, decode_certificates=args.decode_certificates)
99
+ )
98
100
  except Exception as ex:
99
101
  print(f"Could not decode: {ex}", file=sys.stderr)
100
102
  return 1
@@ -227,6 +229,15 @@ def _handle_arguments() -> int:
227
229
  help="The profile to read the value from",
228
230
  )
229
231
 
232
+ decode_parser.add_argument(
233
+ "-d",
234
+ "--decode-certificates",
235
+ dest="decode_certificates",
236
+ action="store_true",
237
+ default=False,
238
+ help="Decode and extract certificate properties",
239
+ )
240
+
230
241
  decode_parser.set_defaults(subcommand="decode")
231
242
 
232
243
  args = parser.parse_args()
@@ -1,20 +1,25 @@
1
1
  [tool.poetry]
2
2
  name = "protool"
3
- version = "2.0.0"
3
+ version = "3.0.0"
4
4
  description = "A tool for dealing with provisioning profiles"
5
5
 
6
6
  license = "MIT"
7
7
 
8
- authors = [
9
- "Dale Myers <dalemy@microsoft.com>"
10
- ]
8
+ authors = ["Dale Myers <dalemy@microsoft.com>"]
11
9
 
12
10
  readme = 'README.md'
13
11
 
14
12
  repository = "https://github.com/Microsoft/protool"
15
13
  homepage = "https://github.com/Microsoft/protool"
16
14
 
17
- keywords = ['provisioning', 'profiles', 'apple', 'ios', 'xcode', 'mobileprovision']
15
+ keywords = [
16
+ 'provisioning',
17
+ 'profiles',
18
+ 'apple',
19
+ 'ios',
20
+ 'xcode',
21
+ 'mobileprovision',
22
+ ]
18
23
 
19
24
  classifiers = [
20
25
  'Development Status :: 3 - Alpha',
@@ -27,23 +32,23 @@ classifiers = [
27
32
  'Programming Language :: Python :: 3.10',
28
33
  'Programming Language :: Python :: 3.11',
29
34
  'Topic :: Software Development',
30
- 'Topic :: Utilities'
35
+ 'Topic :: Utilities',
31
36
  ]
32
37
 
33
38
  [tool.poetry.scripts]
34
39
  protool = 'protool:command_line.run'
35
40
 
36
41
  [tool.poetry.dependencies]
37
- python = "^3.8"
38
- pyOpenSSL ="^23.2.0"
42
+ python = "^3.11"
43
+ pyOpenSSL = "^25.1.0"
39
44
 
40
45
  [tool.poetry.dev-dependencies]
41
- black = "^23.7.0"
42
- mypy = "^1.4.1"
43
- pylint = "^2.17.5"
44
- pytest = "^7.4.0"
45
- pytest-cov = "^4.1.0"
46
- types-pyOpenSSL = "^23.2.0.2"
46
+ black = "^26.1.0"
47
+ mypy = "^1.19.1"
48
+ pylint = "^4.0.4"
49
+ pytest = "^9.0.2"
50
+ pytest-cov = "^7.0.0"
51
+ types-pyOpenSSL = "^24.1.0.20240722"
47
52
 
48
53
  [build-system]
49
54
  requires = ["poetry-core"]
File without changes
File without changes