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.
- {protool-2.0.0 → protool-3.0.0}/PKG-INFO +7 -5
- {protool-2.0.0 → protool-3.0.0}/protool/__init__.py +108 -31
- {protool-2.0.0 → protool-3.0.0}/protool/command_line.py +12 -1
- {protool-2.0.0 → protool-3.0.0}/pyproject.toml +19 -14
- {protool-2.0.0 → protool-3.0.0}/LICENSE +0 -0
- {protool-2.0.0 → protool-3.0.0}/README.md +0 -0
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: protool
|
|
3
|
-
Version:
|
|
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.
|
|
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 (>=
|
|
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
|
|
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:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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) ->
|
|
117
|
+
def developer_certificates(self) -> list[crypto.X509]:
|
|
70
118
|
"""Returns developer certificates as a list of PyOpenSSL X509."""
|
|
71
|
-
dev_certs:
|
|
72
|
-
raw_cert_items:
|
|
73
|
-
|
|
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
|
|
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__(
|
|
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
|
-
|
|
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:
|
|
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:
|
|
163
|
-
tool_override:
|
|
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) ->
|
|
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(
|
|
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 = "
|
|
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 = [
|
|
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.
|
|
38
|
-
pyOpenSSL ="^
|
|
42
|
+
python = "^3.11"
|
|
43
|
+
pyOpenSSL = "^25.1.0"
|
|
39
44
|
|
|
40
45
|
[tool.poetry.dev-dependencies]
|
|
41
|
-
black = "^
|
|
42
|
-
mypy = "^1.
|
|
43
|
-
pylint = "^
|
|
44
|
-
pytest = "^
|
|
45
|
-
pytest-cov = "^
|
|
46
|
-
types-pyOpenSSL = "^
|
|
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
|