ipasign 1.0__py3-none-any.whl
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.
- ipasign/__init__.py +40 -0
- ipasign/_apple_ca.py +284 -0
- ipasign/_plist.py +28 -0
- ipasign/archive.py +173 -0
- ipasign/blobs.py +362 -0
- ipasign/bundle.py +282 -0
- ipasign/cms.py +164 -0
- ipasign/credentials.py +259 -0
- ipasign/errors.py +56 -0
- ipasign/key.py +251 -0
- ipasign/macho.py +504 -0
- ipasign/signer.py +275 -0
- ipasign-1.0.dist-info/METADATA +96 -0
- ipasign-1.0.dist-info/RECORD +17 -0
- ipasign-1.0.dist-info/WHEEL +5 -0
- ipasign-1.0.dist-info/licenses/LICENSE +21 -0
- ipasign-1.0.dist-info/top_level.txt +1 -0
ipasign/cms.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""CMS / PKCS#7 signature generation.
|
|
2
|
+
|
|
3
|
+
Apple seals a code signature with a detached CMS ``SignedData`` whose content is
|
|
4
|
+
the CodeDirectory. The structure carries a handful of signed attributes, three of
|
|
5
|
+
them standard and two Apple specific, and one detail decides whether the result
|
|
6
|
+
verifies at all: the RSA signature is computed over the signed attributes
|
|
7
|
+
encoded as a ``SET OF`` (tag ``0x31``), not over the ``[0]`` implicit form the
|
|
8
|
+
SignerInfo carries them in. RFC 5652 section 5.4 signs the SET value; signing the
|
|
9
|
+
implicit form produces a blob that parses cleanly and that ``codesign`` rejects
|
|
10
|
+
with "code or signature have been modified".
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import datetime
|
|
16
|
+
import hashlib
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from asn1crypto import algos, cms, core, x509 as asn1x509
|
|
20
|
+
from cryptography import x509
|
|
21
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
23
|
+
|
|
24
|
+
from .credentials import Identity
|
|
25
|
+
from .errors import BlobError
|
|
26
|
+
|
|
27
|
+
OID_APPLE_CDHASHES_PLIST = "1.2.840.113635.100.9.1"
|
|
28
|
+
OID_APPLE_CDHASHES = "1.2.840.113635.100.9.2"
|
|
29
|
+
|
|
30
|
+
# Content bytes of OID 2.16.840.1.101.3.4.2.1 (sha256). The wrapping SEQUENCE
|
|
31
|
+
# carries no parameters: adding a NULL there changes the bytes and breaks
|
|
32
|
+
# parity with Apple's own output.
|
|
33
|
+
_SHA256_OID_CONTENT = bytes.fromhex("608648016503040201")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _der_length(length: int) -> bytes:
|
|
37
|
+
if length < 0x80:
|
|
38
|
+
return bytes([length])
|
|
39
|
+
raw = length.to_bytes((length.bit_length() + 7) // 8, "big")
|
|
40
|
+
return bytes([0x80 | len(raw)]) + raw
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _der_tlv(tag: int, body: bytes) -> bytes:
|
|
44
|
+
return bytes([tag]) + _der_length(len(body)) + body
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _apple_cdhash_attribute(digest: bytes) -> bytes:
|
|
48
|
+
"""``SEQUENCE { OID sha256, OCTET STRING <digest> }`` for attribute 9.2."""
|
|
49
|
+
inner = _der_tlv(0x06, _SHA256_OID_CONTENT)
|
|
50
|
+
return _der_tlv(0x30, inner + _der_tlv(0x04, digest))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _asn1_certificate(cert: x509.Certificate) -> asn1x509.Certificate:
|
|
54
|
+
return asn1x509.Certificate.load(cert.public_bytes(serialization.Encoding.DER))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(slots=True)
|
|
58
|
+
class CmsParts:
|
|
59
|
+
"""The inputs the CMS builder needs, already hashed by the caller."""
|
|
60
|
+
|
|
61
|
+
code_directory: bytes
|
|
62
|
+
cdhashes_plist: bytes
|
|
63
|
+
apple_hash: bytes
|
|
64
|
+
signing_time: datetime.datetime | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_cms(identity: Identity, parts: CmsParts) -> bytes:
|
|
68
|
+
"""Build the detached CMS ``SignedData`` for a signed CodeDirectory."""
|
|
69
|
+
code_directory = parts.code_directory
|
|
70
|
+
if not code_directory:
|
|
71
|
+
raise BlobError("cannot sign an empty CodeDirectory")
|
|
72
|
+
if len(parts.apple_hash) != hashes.SHA256.digest_size:
|
|
73
|
+
raise BlobError("Apple cdhashes attribute needs a full SHA-256 digest")
|
|
74
|
+
|
|
75
|
+
digest = hashlib.sha256(code_directory).digest()
|
|
76
|
+
signing_time = parts.signing_time or datetime.datetime.now(datetime.timezone.utc)
|
|
77
|
+
|
|
78
|
+
signed_attrs = cms.CMSAttributes(
|
|
79
|
+
[
|
|
80
|
+
cms.CMSAttribute({"type": "content_type", "values": ["data"]}),
|
|
81
|
+
cms.CMSAttribute(
|
|
82
|
+
{
|
|
83
|
+
"type": "signing_time",
|
|
84
|
+
"values": [cms.Time({"utc_time": signing_time})],
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
cms.CMSAttribute(
|
|
88
|
+
{"type": "message_digest", "values": [core.OctetString(digest)]}
|
|
89
|
+
),
|
|
90
|
+
cms.CMSAttribute(
|
|
91
|
+
{
|
|
92
|
+
"type": OID_APPLE_CDHASHES,
|
|
93
|
+
"values": [core.Any.load(_apple_cdhash_attribute(parts.apple_hash))],
|
|
94
|
+
}
|
|
95
|
+
),
|
|
96
|
+
cms.CMSAttribute(
|
|
97
|
+
{
|
|
98
|
+
"type": OID_APPLE_CDHASHES_PLIST,
|
|
99
|
+
"values": [core.OctetString(parts.cdhashes_plist)],
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# RFC 5652: the signature covers the SET-encoded attributes, never the
|
|
106
|
+
# implicit [0] form the SignerInfo stores them in.
|
|
107
|
+
signature = identity.private_key.sign(
|
|
108
|
+
signed_attrs.dump(), padding.PKCS1v15(), hashes.SHA256()
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
leaf = identity.certificate
|
|
112
|
+
signer = cms.SignerInfo(
|
|
113
|
+
{
|
|
114
|
+
"version": "v1",
|
|
115
|
+
"sid": cms.SignerIdentifier(
|
|
116
|
+
{
|
|
117
|
+
"issuer_and_serial_number": cms.IssuerAndSerialNumber(
|
|
118
|
+
{
|
|
119
|
+
"issuer": asn1x509.Name.load(leaf.issuer.public_bytes()),
|
|
120
|
+
"serial_number": leaf.serial_number,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
"digest_algorithm": algos.DigestAlgorithm(
|
|
126
|
+
{"algorithm": "sha256", "parameters": None}
|
|
127
|
+
),
|
|
128
|
+
"signed_attrs": signed_attrs,
|
|
129
|
+
"signature_algorithm": algos.SignedDigestAlgorithm(
|
|
130
|
+
{"algorithm": "rsassa_pkcs1v15"}
|
|
131
|
+
),
|
|
132
|
+
"signature": signature,
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
certificates = cms.CertificateSet(
|
|
137
|
+
[cms.CertificateChoices({"certificate": _asn1_certificate(cert)}) for cert in identity.chain]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
signed_data = cms.SignedData(
|
|
141
|
+
{
|
|
142
|
+
"version": "v1",
|
|
143
|
+
"digest_algorithms": cms.DigestAlgorithms(
|
|
144
|
+
[algos.DigestAlgorithm({"algorithm": "sha256", "parameters": None})]
|
|
145
|
+
),
|
|
146
|
+
"encap_content_info": cms.ContentInfo({"content_type": "data"}),
|
|
147
|
+
"certificates": certificates,
|
|
148
|
+
"signer_infos": cms.SignerInfos([signer]),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return cms.ContentInfo(
|
|
153
|
+
{"content_type": "signed_data", "content": signed_data}
|
|
154
|
+
).dump()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def cms_content(cms_bytes: bytes) -> bytes | None:
|
|
158
|
+
"""Return the detached content of a CMS blob, or None when absent."""
|
|
159
|
+
info = cms.ContentInfo.load(cms_bytes)
|
|
160
|
+
content = info["content"]["encap_content_info"]["content"]
|
|
161
|
+
return content.native if content.native else None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = ["CmsParts", "build_cms"]
|
ipasign/credentials.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Signing identity and provisioning profile loading.
|
|
2
|
+
|
|
3
|
+
A ``.p12`` holds the private key and the leaf certificate. A
|
|
4
|
+
``.mobileprovision`` is a CMS-signed plist carrying the entitlements to seal and
|
|
5
|
+
the team identifier. This module turns both into the pieces the signature
|
|
6
|
+
builders want, and picks the intermediate certificate that completes the chain.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import plistlib
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from cryptography import x509
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
17
|
+
from cryptography.hazmat.primitives.serialization import pkcs12
|
|
18
|
+
|
|
19
|
+
from . import _apple_ca
|
|
20
|
+
from .errors import CredentialError, ProfileError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class ProvisioningProfile:
|
|
25
|
+
"""The parts of a provisioning profile signing needs."""
|
|
26
|
+
|
|
27
|
+
data: bytes
|
|
28
|
+
entitlements: dict
|
|
29
|
+
entitlements_plist: bytes
|
|
30
|
+
team_id: str
|
|
31
|
+
application_id: str
|
|
32
|
+
developer_certificates: list[bytes] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def get_task_allow(self) -> bool:
|
|
36
|
+
"""True only when ``get-task-allow`` is present and set to a true value."""
|
|
37
|
+
return bool(self.entitlements.get("get-task-allow", False))
|
|
38
|
+
|
|
39
|
+
def matches(self, bundle_id: str) -> bool:
|
|
40
|
+
"""Whether this profile was issued for ``bundle_id``."""
|
|
41
|
+
return bool(self.application_id) and self.application_id.endswith(bundle_id)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _plist_from_cms(data: bytes) -> dict:
|
|
45
|
+
"""Pull the plist payload out of a CMS-wrapped provisioning profile."""
|
|
46
|
+
from asn1crypto import cms
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
info = cms.ContentInfo.load(data)
|
|
50
|
+
except Exception as exc: # asn1crypto raises several unrelated types
|
|
51
|
+
raise ProfileError(f"provisioning profile is not a CMS structure: {exc}") from exc
|
|
52
|
+
|
|
53
|
+
if info["content_type"].native != "signed_data":
|
|
54
|
+
raise ProfileError(f"provisioning profile content is {info['content_type'].native}")
|
|
55
|
+
|
|
56
|
+
content = info["content"]["encap_content_info"]["content"]
|
|
57
|
+
if content.native is None:
|
|
58
|
+
raise ProfileError("provisioning profile has no plist payload")
|
|
59
|
+
|
|
60
|
+
payload = content.native
|
|
61
|
+
try:
|
|
62
|
+
parsed = plistlib.loads(payload)
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
raise ProfileError(f"provisioning profile payload is not a plist: {exc}") from exc
|
|
65
|
+
if not isinstance(parsed, dict):
|
|
66
|
+
raise ProfileError("provisioning profile payload is not a dictionary")
|
|
67
|
+
return parsed
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_profile(path: str | Path) -> ProvisioningProfile:
|
|
71
|
+
"""Read a ``.mobileprovision`` file and pull out the signing inputs."""
|
|
72
|
+
try:
|
|
73
|
+
data = Path(path).read_bytes()
|
|
74
|
+
except FileNotFoundError as exc:
|
|
75
|
+
raise ProfileError(f"provisioning profile not found: {path}") from exc
|
|
76
|
+
except OSError as exc:
|
|
77
|
+
raise ProfileError(f"cannot read provisioning profile {path}: {exc}") from exc
|
|
78
|
+
|
|
79
|
+
if not data:
|
|
80
|
+
raise ProfileError(f"provisioning profile is empty: {path}")
|
|
81
|
+
|
|
82
|
+
plist = _plist_from_cms(data)
|
|
83
|
+
|
|
84
|
+
team_ids = plist.get("TeamIdentifier") or []
|
|
85
|
+
if not team_ids:
|
|
86
|
+
raise ProfileError("provisioning profile carries no TeamIdentifier")
|
|
87
|
+
team_id = team_ids[0]
|
|
88
|
+
|
|
89
|
+
entitlements = plist.get("Entitlements")
|
|
90
|
+
if not isinstance(entitlements, dict):
|
|
91
|
+
raise ProfileError("provisioning profile carries no Entitlements dictionary")
|
|
92
|
+
|
|
93
|
+
certificates = [
|
|
94
|
+
cert
|
|
95
|
+
for cert in (plist.get("DeveloperCertificates") or [])
|
|
96
|
+
if isinstance(cert, (bytes, bytearray))
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
return ProvisioningProfile(
|
|
100
|
+
data=data,
|
|
101
|
+
entitlements=entitlements,
|
|
102
|
+
entitlements_plist=plistlib.dumps(entitlements, fmt=plistlib.FMT_XML, sort_keys=False),
|
|
103
|
+
team_id=team_id,
|
|
104
|
+
application_id=str(entitlements.get("application-identifier", "")),
|
|
105
|
+
developer_certificates=certificates,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _embedded_intermediate(leaf: x509.Certificate) -> x509.Certificate | None:
|
|
110
|
+
"""The known Apple intermediate that issued ``leaf``, if any."""
|
|
111
|
+
for pem in _apple_ca.WWDR_BY_ISSUER_HASH.values():
|
|
112
|
+
candidate = x509.load_pem_x509_certificate(pem.encode("ascii"))
|
|
113
|
+
if candidate.subject == leaf.issuer:
|
|
114
|
+
return candidate
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(slots=True)
|
|
119
|
+
class Identity:
|
|
120
|
+
"""A loaded signing identity and the chain that completes it."""
|
|
121
|
+
|
|
122
|
+
private_key: rsa.RSAPrivateKey
|
|
123
|
+
certificate: x509.Certificate
|
|
124
|
+
chain: list[x509.Certificate]
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def subject_cn(self) -> str:
|
|
128
|
+
"""The leaf certificate's common name, pinned by the requirement."""
|
|
129
|
+
for attr in self.certificate.subject:
|
|
130
|
+
if attr.oid == x509.oid.NameOID.COMMON_NAME:
|
|
131
|
+
return attr.value
|
|
132
|
+
raise CredentialError("leaf certificate has no subject common name")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_der_certificates(blobs: list[bytes]) -> list[x509.Certificate]:
|
|
136
|
+
certs: list[x509.Certificate] = []
|
|
137
|
+
for blob in blobs:
|
|
138
|
+
try:
|
|
139
|
+
certs.append(x509.load_der_x509_certificate(blob))
|
|
140
|
+
except Exception:
|
|
141
|
+
continue
|
|
142
|
+
return certs
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _select_chain(
|
|
146
|
+
leaf: x509.Certificate,
|
|
147
|
+
from_p12: list[x509.Certificate],
|
|
148
|
+
from_profile: list[x509.Certificate],
|
|
149
|
+
) -> list[x509.Certificate]:
|
|
150
|
+
"""Pick the certificates to embed, intermediates first and the leaf last.
|
|
151
|
+
|
|
152
|
+
A chain carried inside the p12 is preferred, but only when it actually
|
|
153
|
+
contains the leaf's issuer. A p12 holding an unrelated or root-only chain
|
|
154
|
+
must not shadow the intermediate that matches the leaf.
|
|
155
|
+
"""
|
|
156
|
+
candidates = from_p12 or from_profile
|
|
157
|
+
chain: list[x509.Certificate] = []
|
|
158
|
+
for cert in candidates:
|
|
159
|
+
if cert == leaf:
|
|
160
|
+
continue
|
|
161
|
+
if cert not in chain:
|
|
162
|
+
chain.append(cert)
|
|
163
|
+
|
|
164
|
+
issuer = leaf.issuer
|
|
165
|
+
if not any(cert.subject == issuer for cert in chain):
|
|
166
|
+
chain = []
|
|
167
|
+
|
|
168
|
+
if not chain:
|
|
169
|
+
embedded = _embedded_intermediate(leaf)
|
|
170
|
+
if embedded is None:
|
|
171
|
+
raise CredentialError(
|
|
172
|
+
"no certificate in the p12 or profile issued the leaf, and no "
|
|
173
|
+
f"known Apple intermediate matches its issuer ({issuer.rfc4514_string()})"
|
|
174
|
+
)
|
|
175
|
+
chain = [embedded]
|
|
176
|
+
|
|
177
|
+
# Terminate the chain with the Apple root that issued one of its members.
|
|
178
|
+
for pem in _apple_ca.ROOTS:
|
|
179
|
+
root = x509.load_pem_x509_certificate(pem.encode("ascii"))
|
|
180
|
+
if root in chain:
|
|
181
|
+
continue
|
|
182
|
+
if any(cert.issuer == root.subject for cert in chain):
|
|
183
|
+
chain.append(root)
|
|
184
|
+
|
|
185
|
+
chain.append(leaf)
|
|
186
|
+
return chain
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def load_identity(
|
|
190
|
+
p12_path: str | Path,
|
|
191
|
+
password: str | None,
|
|
192
|
+
profile: ProvisioningProfile | None = None,
|
|
193
|
+
) -> Identity:
|
|
194
|
+
"""Read a ``.p12`` and build the signing chain.
|
|
195
|
+
|
|
196
|
+
The key and leaf come from the p12. Intermediates come from the p12 when it
|
|
197
|
+
carries a usable chain, otherwise from the profile's developer certificates,
|
|
198
|
+
otherwise from the embedded Apple intermediates.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
data = Path(p12_path).read_bytes()
|
|
202
|
+
except FileNotFoundError as exc:
|
|
203
|
+
raise CredentialError(f"identity file not found: {p12_path}") from exc
|
|
204
|
+
except OSError as exc:
|
|
205
|
+
raise CredentialError(f"cannot read identity file {p12_path}: {exc}") from exc
|
|
206
|
+
|
|
207
|
+
secret = None if password is None else password.encode("utf-8")
|
|
208
|
+
try:
|
|
209
|
+
key, leaf, extras = pkcs12.load_key_and_certificates(data, secret)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
raise CredentialError(
|
|
212
|
+
f"cannot open {p12_path}: wrong password or not a PKCS#12 file ({exc})"
|
|
213
|
+
) from exc
|
|
214
|
+
|
|
215
|
+
if key is None:
|
|
216
|
+
raise CredentialError(f"{p12_path} carries no private key")
|
|
217
|
+
if leaf is None:
|
|
218
|
+
raise CredentialError(f"{p12_path} carries no certificate")
|
|
219
|
+
if not isinstance(key, rsa.RSAPrivateKey):
|
|
220
|
+
raise CredentialError(f"only RSA signing keys are supported, got {type(key).__name__}")
|
|
221
|
+
|
|
222
|
+
if leaf.public_key().public_numbers() != key.public_key().public_numbers():
|
|
223
|
+
raise CredentialError(f"private key in {p12_path} does not match its certificate")
|
|
224
|
+
|
|
225
|
+
from_profile = _load_der_certificates(profile.developer_certificates) if profile else []
|
|
226
|
+
chain = _select_chain(leaf, list(extras or []), from_profile)
|
|
227
|
+
return Identity(private_key=key, certificate=leaf, chain=chain)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def load_entitlements(path: str | Path) -> tuple[dict, bytes]:
|
|
231
|
+
"""Read an entitlements plist supplied by the caller.
|
|
232
|
+
|
|
233
|
+
Returns the parsed dictionary and the plist bytes to seal, with the caller's
|
|
234
|
+
key order preserved.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
raw = Path(path).read_bytes()
|
|
238
|
+
except FileNotFoundError as exc:
|
|
239
|
+
raise CredentialError(f"entitlements file not found: {path}") from exc
|
|
240
|
+
except OSError as exc:
|
|
241
|
+
raise CredentialError(f"cannot read entitlements file {path}: {exc}") from exc
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
parsed = plistlib.loads(raw)
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
raise CredentialError(f"entitlements file {path} is not a plist: {exc}") from exc
|
|
247
|
+
if not isinstance(parsed, dict):
|
|
248
|
+
raise CredentialError(f"entitlements file {path} is not a dictionary")
|
|
249
|
+
|
|
250
|
+
return parsed, plistlib.dumps(parsed, fmt=plistlib.FMT_XML, sort_keys=False)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
"Identity",
|
|
255
|
+
"ProvisioningProfile",
|
|
256
|
+
"load_entitlements",
|
|
257
|
+
"load_identity",
|
|
258
|
+
"load_profile",
|
|
259
|
+
]
|
ipasign/errors.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Exception types raised by the public API.
|
|
2
|
+
|
|
3
|
+
Everything the library raises derives from :class:`IpasignError`, so callers
|
|
4
|
+
can catch one base class and still tell the failure modes apart.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IpasignError(Exception):
|
|
11
|
+
"""Base class for every error ipasign raises."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidInputError(IpasignError):
|
|
15
|
+
"""The input is not something this library knows how to sign."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MachOError(IpasignError):
|
|
19
|
+
"""A Mach-O file is malformed or cannot be rewritten."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BlobError(IpasignError):
|
|
23
|
+
"""A code signature blob could not be built or parsed."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CredentialError(IpasignError):
|
|
27
|
+
"""A signing identity, password or provisioning profile is unusable."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProfileError(CredentialError):
|
|
31
|
+
"""A provisioning profile could not be read or is missing required data."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BundleError(IpasignError):
|
|
35
|
+
"""A bundle is missing the structure signing requires."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ArchiveError(IpasignError):
|
|
39
|
+
"""An archive could not be unpacked or repacked."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NotEnoughSpaceError(MachOError):
|
|
43
|
+
"""The load commands have no room for a new LC_CODE_SIGNATURE."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"IpasignError",
|
|
48
|
+
"InvalidInputError",
|
|
49
|
+
"MachOError",
|
|
50
|
+
"BlobError",
|
|
51
|
+
"CredentialError",
|
|
52
|
+
"ProfileError",
|
|
53
|
+
"BundleError",
|
|
54
|
+
"ArchiveError",
|
|
55
|
+
"NotEnoughSpaceError",
|
|
56
|
+
]
|