protool 1.1.2__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,28 +1,28 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: protool
3
- Version: 1.1.2
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.7.2,<4.0.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
- Classifier: Programming Language :: Python :: 3
22
- Classifier: Programming Language :: Python :: 3.7
23
23
  Classifier: Topic :: Software Development
24
24
  Classifier: Topic :: Utilities
25
- Requires-Dist: pyOpenSSL (>=21.0.0)
25
+ Requires-Dist: pyOpenSSL (>=25.1.0,<26.0.0)
26
26
  Project-URL: Repository, https://github.com/Microsoft/protool
27
27
  Description-Content-Type: text/markdown
28
28
 
@@ -12,8 +12,55 @@ import shutil
12
12
  import subprocess
13
13
  import sys
14
14
  import tempfile
15
- from typing import Any, cast, Dict, Iterable, List, Optional
16
- import OpenSSL
15
+ from typing import Any, cast
16
+ from OpenSSL import crypto
17
+
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
+ }
17
64
 
18
65
 
19
66
  class ProvisioningType(Enum):
@@ -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[OpenSSL.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[OpenSSL.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
- loaded_cert: OpenSSL.crypto.X509 = OpenSSL.crypto.load_certificate(
78
- OpenSSL.crypto.FILETYPE_ASN1, cert_item
125
+ loaded_cert: crypto.X509 = crypto.load_certificate(
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()
@@ -0,0 +1,55 @@
1
+ [tool.poetry]
2
+ name = "protool"
3
+ version = "3.0.0"
4
+ description = "A tool for dealing with provisioning profiles"
5
+
6
+ license = "MIT"
7
+
8
+ authors = ["Dale Myers <dalemy@microsoft.com>"]
9
+
10
+ readme = 'README.md'
11
+
12
+ repository = "https://github.com/Microsoft/protool"
13
+ homepage = "https://github.com/Microsoft/protool"
14
+
15
+ keywords = [
16
+ 'provisioning',
17
+ 'profiles',
18
+ 'apple',
19
+ 'ios',
20
+ 'xcode',
21
+ 'mobileprovision',
22
+ ]
23
+
24
+ classifiers = [
25
+ 'Development Status :: 3 - Alpha',
26
+ 'Environment :: Console',
27
+ 'Environment :: MacOS X',
28
+ 'Intended Audience :: Developers',
29
+ 'Programming Language :: Python :: 3',
30
+ 'Programming Language :: Python :: 3.8',
31
+ 'Programming Language :: Python :: 3.9',
32
+ 'Programming Language :: Python :: 3.10',
33
+ 'Programming Language :: Python :: 3.11',
34
+ 'Topic :: Software Development',
35
+ 'Topic :: Utilities',
36
+ ]
37
+
38
+ [tool.poetry.scripts]
39
+ protool = 'protool:command_line.run'
40
+
41
+ [tool.poetry.dependencies]
42
+ python = "^3.11"
43
+ pyOpenSSL = "^25.1.0"
44
+
45
+ [tool.poetry.dev-dependencies]
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"
52
+
53
+ [build-system]
54
+ requires = ["poetry-core"]
55
+ build-backend = "poetry.core.masonry.api"
@@ -1,46 +0,0 @@
1
- [tool.poetry]
2
- name = "protool"
3
- version = "1.1.2"
4
- description = "A tool for dealing with provisioning profiles"
5
-
6
- license = "MIT"
7
-
8
- authors = [
9
- "Dale Myers <dalemy@microsoft.com>"
10
- ]
11
-
12
- readme = 'README.md'
13
-
14
- repository = "https://github.com/Microsoft/protool"
15
- homepage = "https://github.com/Microsoft/protool"
16
-
17
- keywords = ['provisioning', 'profiles', 'apple', 'ios', 'xcode', 'mobileprovision']
18
-
19
- classifiers = [
20
- 'Development Status :: 3 - Alpha',
21
- 'Environment :: Console',
22
- 'Environment :: MacOS X',
23
- 'Intended Audience :: Developers',
24
- 'Programming Language :: Python :: 3',
25
- 'Programming Language :: Python :: 3.7',
26
- 'Topic :: Software Development',
27
- 'Topic :: Utilities'
28
- ]
29
-
30
- [tool.poetry.scripts]
31
- protool = 'protool:command_line.run'
32
-
33
- [tool.poetry.dependencies]
34
- python = "^3.7.2"
35
- pyOpenSSL =">=21.0.0"
36
-
37
- [tool.poetry.dev-dependencies]
38
- black = "=23.1.0"
39
- mypy = "=1.0.1"
40
- pylint = "=2.16.2"
41
- pytest = "=7.2.1"
42
- pytest-cov = "=4.0.0"
43
-
44
- [build-system]
45
- requires = ["poetry>=0.12"]
46
- build-backend = "poetry.masonry.api"
protool-1.1.2/setup.py DELETED
@@ -1,34 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from setuptools import setup
3
-
4
- packages = \
5
- ['protool']
6
-
7
- package_data = \
8
- {'': ['*']}
9
-
10
- install_requires = \
11
- ['pyOpenSSL>=21.0.0']
12
-
13
- entry_points = \
14
- {'console_scripts': ['protool = protool:command_line.run']}
15
-
16
- setup_kwargs = {
17
- 'name': 'protool',
18
- 'version': '1.1.2',
19
- 'description': 'A tool for dealing with provisioning profiles',
20
- 'long_description': '# protool \n\n[![PyPi Version](https://img.shields.io/pypi/v/protool.svg)](https://pypi.org/project/protool/)\n[![License](https://img.shields.io/pypi/l/protool.svg)](https://github.com/Microsoft/protool/blob/master/LICENSE)\n\nA tool for dealing with provisioning profiles.\n\nWhat can it do? \n\n* Read profiles as XML or as a dictionary\n* Read the values from the profile\n* Diff two profiles to see what has changed\n\n### Installation\n\n pip install protool\n\n### Examples:\n\n import protool\n profile = protool.ProvisioningProfile("/path/to/profile")\n\n # Get the diff of two profiles\n diff = protool.diff("/path/to/first", "/path/to/second", tool_override="diff")\n\n # Get the UUID of a profile\n print profile.uuid\n\n # Get the full XML of the profile\n print profile.xml\n\n # Get the parsed contents of the profile as a dictionary\n print profile.contents()\n\n\nAlternatively, from the command line:\n\n # Get the diff\n protool diff --profiles /path/to/profile1 /path/to/profile2 --tool diff\n\n # Get the UUID of a profile\n protool read --profile /path/to/profile --key UUID\n\n # Get the raw XML (identical to using `security cms -D -i /path/to/profile`)\n protool decode --profile /path/to/profile\n\n\n# Contributing\n\nThis project welcomes contributions and suggestions. Most contributions require you to agree to a\nContributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\nthe rights to use your contribution. For details, visit https://cla.microsoft.com.\n\nWhen you submit a pull request, a CLA-bot will automatically determine whether you need to provide\na CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions\nprovided by the bot. You will only need to do this once across all repos using our CLA.\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\nFor more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\ncontact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n',
21
- 'author': 'Dale Myers',
22
- 'author_email': 'dalemy@microsoft.com',
23
- 'maintainer': 'None',
24
- 'maintainer_email': 'None',
25
- 'url': 'https://github.com/Microsoft/protool',
26
- 'packages': packages,
27
- 'package_data': package_data,
28
- 'install_requires': install_requires,
29
- 'entry_points': entry_points,
30
- 'python_requires': '>=3.7.2,<4.0.0',
31
- }
32
-
33
-
34
- setup(**setup_kwargs)
File without changes
File without changes