pyencode-protector 0.3.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.
@@ -0,0 +1,1488 @@
1
+ """Portable in-memory runtime for PyEncode PYE2 distributions.
2
+
3
+ The runtime intentionally remains pure Python so one generated runtime source
4
+ works on Windows and Linux and across supported CPython releases. Marshalled
5
+ payloads are still tied to the CPython minor version recorded by the build.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import binascii
12
+ import hashlib
13
+ import importlib
14
+ import importlib.abc
15
+ import importlib.util
16
+ import json
17
+ import marshal
18
+ import os
19
+ import re
20
+ import stat
21
+ import sys
22
+ import threading
23
+ import types
24
+ import zlib
25
+ from datetime import date, datetime, time, timezone
26
+ from pathlib import Path, PurePosixPath, PureWindowsPath
27
+ from typing import Any, Iterator, Mapping, NoReturn
28
+
29
+ try:
30
+ from importlib.resources.abc import ResourceReader as _ResourceReaderABC
31
+ except ImportError: # Python 3.10
32
+ from importlib.abc import ResourceReader as _ResourceReaderABC
33
+
34
+ from . import _build
35
+
36
+
37
+ _MAGIC = b"PYE2"
38
+ _NONCE_SIZE = 12
39
+ _TAG_SIZE = 16
40
+ _KEY_SIZE = 32
41
+ _MAX_INDEX_SIZE = 16 * 1024 * 1024
42
+ _MAX_MARSHALLED_SIZE = 256 * 1024 * 1024
43
+ _MASK_PREFIX = b"pyencode-key-mask\0"
44
+ _ROOT_KEY_INFO = b"pyencode-root-key-v2\0"
45
+ _INDEX_KEY_INFO = b"pyencode-module-index-v1\0"
46
+ _INDEX_COMMITMENT_DOMAIN = b"pyencode-module-index-commitment-v1\0"
47
+ _MODULE_ROOT_INFO = b"pyencode-module-root-v2\0"
48
+ _ARTIFACT_KEY_INFO = b"pyencode-artifact-v2\0"
49
+ _BOUND_FILES_DOMAIN = b"pyencode-bound-files-v1\0"
50
+ _MANIFEST_NAME = ".pyencode-manifest.json"
51
+ _TOKEN_RE = re.compile(r"[0-9a-f]{32}\Z")
52
+ _DIGEST_RE = re.compile(r"[0-9a-f]{64}\Z")
53
+ _PYTHON_TAG_RE = re.compile(r"cp[0-9]{2,}\Z")
54
+ _WINDOWS_FORBIDDEN_CHARACTERS = frozenset('<>:"|?*')
55
+ _WINDOWS_RESERVED_BASENAMES = frozenset(
56
+ {
57
+ "aux",
58
+ "con",
59
+ "conin$",
60
+ "conout$",
61
+ "nul",
62
+ "prn",
63
+ *(f"com{index}" for index in range(1, 10)),
64
+ *(f"lpt{index}" for index in range(1, 10)),
65
+ }
66
+ )
67
+ _CODE_SUFFIXES = frozenset(
68
+ {".py", ".pyc", ".pyo", ".pyd", ".so", ".dll", ".dylib"}
69
+ )
70
+ _RUNTIME_PATHS = frozenset(
71
+ {
72
+ "pyencode_runtime/__init__.py",
73
+ "pyencode_runtime/_mp_main.py",
74
+ "pyencode_runtime/_runtime.py",
75
+ "pyencode_runtime/_build.py",
76
+ }
77
+ )
78
+
79
+
80
+ class PyEncodeRuntimeError(ImportError):
81
+ """Raised when protected output is invalid, incompatible, or expired."""
82
+
83
+
84
+ class IntegrityError(PyEncodeRuntimeError):
85
+ """Raised when signed or authenticated bytes do not match."""
86
+
87
+
88
+ class CompatibilityError(PyEncodeRuntimeError):
89
+ """Raised when a distribution targets another Python runtime."""
90
+
91
+
92
+ class PolicyError(PyEncodeRuntimeError):
93
+ """Raised when a signed policy prevents execution."""
94
+
95
+
96
+ def _fail(message: str, *, cause: BaseException | None = None) -> NoReturn:
97
+ error = PyEncodeRuntimeError(f"pyencode runtime: {message}")
98
+ if cause is None:
99
+ raise error
100
+ raise error from cause
101
+
102
+
103
+ def _canonical_json(value: Any) -> bytes:
104
+ try:
105
+ return json.dumps(
106
+ value,
107
+ sort_keys=True,
108
+ separators=(",", ":"),
109
+ ensure_ascii=False,
110
+ allow_nan=False,
111
+ ).encode("utf-8")
112
+ except (TypeError, ValueError, UnicodeEncodeError) as exc:
113
+ _fail("metadata cannot be encoded as canonical JSON", cause=exc)
114
+
115
+
116
+ def _decode_json(raw: bytes, description: str) -> Any:
117
+ try:
118
+ return json.loads(raw.decode("utf-8"))
119
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
120
+ _fail(f"invalid {description} JSON", cause=exc)
121
+
122
+
123
+ def _decode_b64(value: object, description: str, *, minimum: int = 1) -> bytes:
124
+ if not isinstance(value, str) or not value:
125
+ _fail(f"{description} must be a non-empty base64 string")
126
+ try:
127
+ decoded = base64.b64decode(value, validate=True)
128
+ except (ValueError, binascii.Error) as exc:
129
+ _fail(f"{description} is not valid base64", cause=exc)
130
+ if len(decoded) < minimum:
131
+ _fail(f"{description} is too short")
132
+ return decoded
133
+
134
+
135
+ def _decode_hex(value: object, description: str, *, size: int) -> bytes:
136
+ expected_length = size * 2
137
+ if (
138
+ not isinstance(value, str)
139
+ or len(value) != expected_length
140
+ or value != value.lower()
141
+ ):
142
+ _fail(
143
+ f"{description} must be exactly {expected_length} lowercase hexadecimal characters"
144
+ )
145
+ try:
146
+ decoded = bytes.fromhex(value)
147
+ except ValueError as exc:
148
+ _fail(f"{description} is not valid hexadecimal", cause=exc)
149
+ if len(decoded) != size:
150
+ _fail(f"{description} must decode to {size} bytes")
151
+ return decoded
152
+
153
+
154
+ def _runtime_python_tag() -> str:
155
+ if getattr(sys.implementation, "name", None) != "cpython":
156
+ raise CompatibilityError("pyencode runtime: protected modules require CPython")
157
+ return f"cp{sys.version_info.major}{sys.version_info.minor}"
158
+
159
+
160
+ def _validate_build_config() -> bytes:
161
+ required_strings = {
162
+ "BUILD_ID": _build.BUILD_ID,
163
+ "PYTHON_TAG": _build.PYTHON_TAG,
164
+ "PUBLIC_KEY_B64": _build.PUBLIC_KEY_B64,
165
+ "MANIFEST_NAME": _build.MANIFEST_NAME,
166
+ "LAUNCHER_NAME": _build.LAUNCHER_NAME,
167
+ }
168
+ for name, value in required_strings.items():
169
+ if not isinstance(value, str) or not value:
170
+ _fail(f"runtime build configuration {name} is missing")
171
+ build_id = _decode_hex(_build.BUILD_ID, "BUILD_ID", size=16)
172
+ if _PYTHON_TAG_RE.fullmatch(_build.PYTHON_TAG) is None:
173
+ _fail("runtime build configuration PYTHON_TAG is invalid")
174
+ if _build.MANIFEST_NAME != _MANIFEST_NAME:
175
+ _fail("runtime build configuration MANIFEST_NAME is invalid")
176
+ actual_tag = _runtime_python_tag()
177
+ if _build.PYTHON_TAG != actual_tag:
178
+ raise CompatibilityError(
179
+ "pyencode runtime: protected output targets "
180
+ f"{_build.PYTHON_TAG}, but this interpreter is {actual_tag}"
181
+ )
182
+ if not isinstance(_build.KEY_PARTS, (tuple, list)) or not _build.KEY_PARTS:
183
+ _fail("runtime build configuration KEY_PARTS is missing")
184
+ if not isinstance(_build.KEY_ORDER, (tuple, list)) or not _build.KEY_ORDER:
185
+ _fail("runtime build configuration KEY_ORDER is missing")
186
+ if (
187
+ not isinstance(_build.BOOTSTRAP_PATHS, (tuple, list))
188
+ or any(path != "_vendor" for path in _build.BOOTSTRAP_PATHS)
189
+ or len(set(_build.BOOTSTRAP_PATHS)) != len(_build.BOOTSTRAP_PATHS)
190
+ ):
191
+ _fail("runtime build configuration BOOTSTRAP_PATHS is invalid")
192
+ launcher = PurePosixPath(_build.LAUNCHER_NAME)
193
+ if (
194
+ len(launcher.parts) != 1
195
+ or launcher.suffix.lower() != ".py"
196
+ or not launcher.stem.isidentifier()
197
+ ):
198
+ _fail("runtime build configuration LAUNCHER_NAME is invalid")
199
+ return build_id
200
+
201
+
202
+ def _embedded_master_key(build_id: bytes) -> bytearray:
203
+ parts = _build.KEY_PARTS
204
+ order = _build.KEY_ORDER
205
+ if any(isinstance(index, bool) or not isinstance(index, int) for index in order):
206
+ _fail("KEY_ORDER entries must be integers")
207
+ if len(order) != len(parts) or sorted(order) != list(range(len(parts))):
208
+ _fail("KEY_ORDER must be a permutation of all KEY_PARTS indexes")
209
+ decoded_parts: list[bytes] = []
210
+ for index in order:
211
+ part = parts[index]
212
+ if not isinstance(part, (str, bytes)) or not part:
213
+ _fail("KEY_PARTS entries must be non-empty base85 strings")
214
+ try:
215
+ decoded_parts.append(base64.b85decode(part))
216
+ except (ValueError, binascii.Error) as exc:
217
+ _fail("a KEY_PARTS entry is not valid base85", cause=exc)
218
+ material = b"".join(decoded_parts)
219
+ if len(material) != _KEY_SIZE:
220
+ _fail(f"decoded KEY_PARTS must contain exactly {_KEY_SIZE} bytes")
221
+ mask = hashlib.sha256(_MASK_PREFIX + build_id).digest()
222
+ return bytearray(left ^ right for left, right in zip(material, mask))
223
+
224
+
225
+ def _hkdf(root_key: bytes | bytearray, build_id: bytes, info: bytes) -> bytes:
226
+ try:
227
+ from cryptography.hazmat.primitives import hashes
228
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
229
+ except ImportError as exc: # pragma: no cover - installation dependent
230
+ _fail("the 'cryptography' package is required", cause=exc)
231
+ return HKDF(
232
+ algorithm=hashes.SHA256(),
233
+ length=_KEY_SIZE,
234
+ salt=build_id,
235
+ info=info,
236
+ ).derive(bytes(root_key))
237
+
238
+
239
+ def _derive_support_root(
240
+ master_key: bytes | bytearray, build_id: bytes, commitment: bytes
241
+ ) -> bytes:
242
+ if len(commitment) != 32:
243
+ _fail("bound-files commitment must be exactly 32 bytes")
244
+ return _hkdf(master_key, build_id, _ROOT_KEY_INFO + commitment)
245
+
246
+
247
+ def _index_aad(build_id_text: str, python_tag: str) -> bytes:
248
+ return _canonical_json(
249
+ {
250
+ "build_id": build_id_text,
251
+ "cipher": "aes-256-gcm",
252
+ "compression": "zlib",
253
+ "format": 1,
254
+ "python_tag": python_tag,
255
+ }
256
+ )
257
+
258
+
259
+ def _index_commitment(
260
+ nonce: bytes, ciphertext: bytes, build_id_text: str, python_tag: str
261
+ ) -> bytes:
262
+ return hashlib.sha256(
263
+ _INDEX_COMMITMENT_DOMAIN
264
+ + _index_aad(build_id_text, python_tag)
265
+ + nonce
266
+ + ciphertext
267
+ ).digest()
268
+
269
+
270
+ def _derive_index_key(support_root: bytes, build_id: bytes) -> bytes:
271
+ return _hkdf(support_root, build_id, _INDEX_KEY_INFO)
272
+
273
+
274
+ def _derive_module_root(
275
+ support_root: bytes, build_id: bytes, index_commitment: bytes
276
+ ) -> bytes:
277
+ if len(index_commitment) != 32:
278
+ _fail("module-index commitment must be exactly 32 bytes")
279
+ return _hkdf(support_root, build_id, _MODULE_ROOT_INFO + index_commitment)
280
+
281
+
282
+ def _derive_artifact_key(
283
+ module_root: bytes, build_id: bytes, token: str, fullname: str
284
+ ) -> bytes:
285
+ return _hkdf(
286
+ module_root,
287
+ build_id,
288
+ _ARTIFACT_KEY_INFO
289
+ + bytes.fromhex(token)
290
+ + b"\0"
291
+ + fullname.encode("utf-8"),
292
+ )
293
+
294
+
295
+ def _artifact_aad(
296
+ *, fullname: str, package: bool, token: str, build_id: str, python_tag: str
297
+ ) -> bytes:
298
+ return _canonical_json(
299
+ {
300
+ "artifact": token,
301
+ "build_id": build_id,
302
+ "compression": "zlib",
303
+ "format": 2,
304
+ "marshal_version": marshal.version,
305
+ "module": fullname,
306
+ "package": package,
307
+ "python_tag": python_tag,
308
+ }
309
+ )
310
+
311
+
312
+ def _manifest_path() -> Path:
313
+ package_dir = Path(__file__).resolve().parent
314
+ return package_dir.parent / _MANIFEST_NAME
315
+
316
+
317
+ def _read_manifest() -> tuple[dict[str, Any], Path]:
318
+ path = _manifest_path()
319
+ try:
320
+ raw = path.read_bytes()
321
+ except OSError as exc:
322
+ _fail(f"cannot read manifest {path}", cause=exc)
323
+ wrapper = _decode_json(raw, "manifest wrapper")
324
+ if not isinstance(wrapper, dict) or set(wrapper) != {"manifest", "signature"}:
325
+ _fail("manifest wrapper must contain only 'manifest' and 'signature'")
326
+ payload = wrapper["manifest"]
327
+ if not isinstance(payload, dict):
328
+ _fail("signed manifest payload must be an object")
329
+ signature = _decode_b64(wrapper["signature"], "manifest signature", minimum=64)
330
+ public_key = _decode_b64(_build.PUBLIC_KEY_B64, "manifest public key", minimum=32)
331
+ if len(signature) != 64 or len(public_key) != 32:
332
+ _fail("manifest signature or public key has an invalid length")
333
+ try:
334
+ from cryptography.exceptions import InvalidSignature
335
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
336
+ except ImportError as exc: # pragma: no cover - installation dependent
337
+ _fail("the 'cryptography' package is required", cause=exc)
338
+ try:
339
+ Ed25519PublicKey.from_public_bytes(public_key).verify(
340
+ signature, _canonical_json(payload)
341
+ )
342
+ except InvalidSignature as exc:
343
+ raise IntegrityError("pyencode runtime: manifest signature is invalid") from exc
344
+ except ValueError as exc:
345
+ _fail("invalid Ed25519 public key or signature", cause=exc)
346
+ return payload, path.parent.resolve()
347
+
348
+
349
+ def _validate_module_name(value: object) -> str:
350
+ if not isinstance(value, str) or not value:
351
+ _fail("module name must be a non-empty string")
352
+ if any(not part or not part.isidentifier() for part in value.split(".")):
353
+ _fail(f"invalid protected module name {value!r}")
354
+ return value
355
+
356
+
357
+ def _validate_inventory_path(value: object, description: str) -> str:
358
+ if (
359
+ not isinstance(value, str)
360
+ or not value
361
+ or "\0" in value
362
+ or "\\" in value
363
+ ):
364
+ _fail(f"{description} must be a canonical POSIX relative path")
365
+ posix = PurePosixPath(value)
366
+ windows = PureWindowsPath(value)
367
+ if (
368
+ posix.is_absolute()
369
+ or windows.is_absolute()
370
+ or bool(windows.drive)
371
+ or any(
372
+ part in ("", ".", "..")
373
+ or part.endswith((".", " "))
374
+ or any(
375
+ ord(character) < 32 or character in _WINDOWS_FORBIDDEN_CHARACTERS
376
+ for character in part
377
+ )
378
+ or part.partition(".")[0].casefold() in _WINDOWS_RESERVED_BASENAMES
379
+ for part in posix.parts
380
+ )
381
+ or posix.as_posix() != value
382
+ ):
383
+ _fail(f"unsafe {description}: {value!r}")
384
+ return value
385
+
386
+
387
+ def _is_code_path(path: str) -> bool:
388
+ name = PurePosixPath(path).name.casefold()
389
+ return any(name.endswith(suffix) for suffix in _CODE_SUFFIXES) or ".so." in name
390
+
391
+
392
+ def _is_link_or_reparse(metadata: os.stat_result) -> bool:
393
+ if stat.S_ISLNK(metadata.st_mode):
394
+ return True
395
+ flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
396
+ attributes = getattr(metadata, "st_file_attributes", 0)
397
+ return bool(flag and attributes & flag)
398
+
399
+
400
+ def _walk_files(
401
+ directory: Path,
402
+ root: Path,
403
+ prefix: tuple[str, ...] = (),
404
+ directories: set[str] | None = None,
405
+ ) -> Iterator[str]:
406
+ try:
407
+ with os.scandir(directory) as iterator:
408
+ entries = sorted(iterator, key=lambda item: item.name)
409
+ except OSError as exc:
410
+ raise IntegrityError(
411
+ f"pyencode runtime: cannot scan distribution directory {'/'.join(prefix) or '.'!r}"
412
+ ) from exc
413
+ folded: dict[str, str] = {}
414
+ for entry in entries:
415
+ previous = folded.get(entry.name.casefold())
416
+ if previous is not None and previous != entry.name:
417
+ raise IntegrityError(
418
+ "pyencode runtime: case-folding file collision: "
419
+ f"{previous!r} and {entry.name!r}"
420
+ )
421
+ folded[entry.name.casefold()] = entry.name
422
+ for entry in entries:
423
+ relative = "/".join((*prefix, entry.name))
424
+ _validate_inventory_path(relative, "distribution path")
425
+ try:
426
+ metadata = entry.stat(follow_symlinks=False)
427
+ except OSError as exc:
428
+ raise IntegrityError(
429
+ f"pyencode runtime: cannot stat distribution path {relative!r}"
430
+ ) from exc
431
+ if _is_link_or_reparse(metadata):
432
+ raise IntegrityError(
433
+ f"pyencode runtime: distribution contains a link or reparse point: {relative!r}"
434
+ )
435
+ if stat.S_ISDIR(metadata.st_mode):
436
+ if directories is not None:
437
+ directories.add(relative)
438
+ yield from _walk_files(
439
+ Path(entry.path), root, (*prefix, entry.name), directories
440
+ )
441
+ elif stat.S_ISREG(metadata.st_mode):
442
+ try:
443
+ Path(entry.path).resolve(strict=True).relative_to(root)
444
+ except (OSError, ValueError) as exc:
445
+ raise IntegrityError(
446
+ f"pyencode runtime: distribution file escapes its root: {relative!r}"
447
+ ) from exc
448
+ yield relative
449
+ else:
450
+ raise IntegrityError(
451
+ f"pyencode runtime: distribution contains a non-regular entry: {relative!r}"
452
+ )
453
+
454
+
455
+ def _scan_distribution(root: Path) -> tuple[dict[str, Path], frozenset[str]]:
456
+ """Scan the tree once and retain exact, already-validated file paths."""
457
+
458
+ directories: set[str] = set()
459
+ files = {
460
+ relative: root.joinpath(*PurePosixPath(relative).parts)
461
+ for relative in _walk_files(root, root, directories=directories)
462
+ }
463
+ return files, frozenset(directories)
464
+
465
+
466
+ def _exact_file(root: Path, relative: str) -> Path:
467
+ parts = PurePosixPath(relative).parts
468
+ current = root
469
+ for index, part in enumerate(parts):
470
+ try:
471
+ with os.scandir(current) as iterator:
472
+ entries = tuple(iterator)
473
+ except OSError as exc:
474
+ raise IntegrityError(
475
+ f"pyencode runtime: signed file {relative!r} is missing or unreadable"
476
+ ) from exc
477
+ exact = next((entry for entry in entries if entry.name == part), None)
478
+ if exact is None:
479
+ folded = next(
480
+ (entry.name for entry in entries if entry.name.casefold() == part.casefold()),
481
+ None,
482
+ )
483
+ if folded is not None:
484
+ raise IntegrityError(
485
+ f"pyencode runtime: signed file {relative!r} has mismatched path casing"
486
+ )
487
+ raise IntegrityError(f"pyencode runtime: signed file {relative!r} is missing")
488
+ try:
489
+ metadata = exact.stat(follow_symlinks=False)
490
+ except OSError as exc:
491
+ raise IntegrityError(
492
+ f"pyencode runtime: cannot inspect signed file {relative!r}"
493
+ ) from exc
494
+ if _is_link_or_reparse(metadata):
495
+ raise IntegrityError(
496
+ f"pyencode runtime: signed file {relative!r} uses a link or reparse point"
497
+ )
498
+ last = index == len(parts) - 1
499
+ if last and not stat.S_ISREG(metadata.st_mode):
500
+ raise IntegrityError(
501
+ f"pyencode runtime: signed file {relative!r} is not a regular file"
502
+ )
503
+ if not last and not stat.S_ISDIR(metadata.st_mode):
504
+ raise IntegrityError(
505
+ f"pyencode runtime: parent of signed file {relative!r} is not a directory"
506
+ )
507
+ current = Path(exact.path)
508
+ try:
509
+ resolved = current.resolve(strict=True)
510
+ resolved.relative_to(root)
511
+ except (OSError, ValueError) as exc:
512
+ raise IntegrityError(
513
+ f"pyencode runtime: signed file {relative!r} escapes the distribution"
514
+ ) from exc
515
+ return resolved
516
+
517
+
518
+ def _bound_files_commitment(file_digests: Mapping[str, str]) -> bytes:
519
+ descriptor = {"format": 1, "files": dict(sorted(file_digests.items()))}
520
+ return hashlib.sha256(_BOUND_FILES_DOMAIN + _canonical_json(descriptor)).digest()
521
+
522
+
523
+ def _validate_bound_files(
524
+ payload: Mapping[str, Any], root: Path, inventory: Mapping[str, Path]
525
+ ) -> tuple[bytes, frozenset[str]]:
526
+ integrity = payload.get("integrity")
527
+ if not isinstance(integrity, dict) or set(integrity) != {
528
+ "format",
529
+ "algorithm",
530
+ "files",
531
+ "commitment_sha256",
532
+ }:
533
+ _fail("manifest integrity metadata is missing or has invalid fields")
534
+ if type(integrity["format"]) is not int or integrity["format"] != 1:
535
+ _fail("unsupported manifest integrity format")
536
+ if integrity["algorithm"] != "sha256":
537
+ _fail("unsupported manifest integrity algorithm")
538
+ raw_files = integrity["files"]
539
+ if not isinstance(raw_files, dict) or not raw_files:
540
+ _fail("manifest integrity files must be a non-empty object")
541
+
542
+ expected_mandatory = set(_RUNTIME_PATHS)
543
+ expected_mandatory.add(_build.LAUNCHER_NAME)
544
+ if not expected_mandatory.issubset(raw_files):
545
+ missing = sorted(expected_mandatory.difference(raw_files))
546
+ _fail("manifest integrity file set is missing " + ", ".join(missing))
547
+
548
+ signed: dict[str, str] = {}
549
+ folded_paths: dict[str, str] = {}
550
+ for raw_path, raw_digest in raw_files.items():
551
+ path = _validate_inventory_path(raw_path, "signed support file path")
552
+ previous = folded_paths.get(path.casefold())
553
+ if previous is not None and previous != path:
554
+ _fail(f"case-folding signed path collision: {previous!r} and {path!r}")
555
+ folded_paths[path.casefold()] = path
556
+ if not isinstance(raw_digest, str) or _DIGEST_RE.fullmatch(raw_digest) is None:
557
+ _fail(f"invalid sha256 for signed support file {path!r}")
558
+ signed[path] = raw_digest
559
+
560
+ actual: dict[str, str] = {}
561
+ for relative, expected_digest in sorted(signed.items()):
562
+ path = inventory.get(relative)
563
+ if path is None:
564
+ raise IntegrityError(
565
+ f"pyencode runtime: signed support file {relative!r} is missing"
566
+ )
567
+ try:
568
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
569
+ except OSError as exc:
570
+ raise IntegrityError(
571
+ f"pyencode runtime: cannot read signed support file {relative!r}"
572
+ ) from exc
573
+ if digest != expected_digest:
574
+ raise IntegrityError(
575
+ f"pyencode runtime: signed support file {relative!r} failed its hash check"
576
+ )
577
+ actual[relative] = digest
578
+
579
+ commitment = _bound_files_commitment(actual)
580
+ expected_commitment = _decode_hex(
581
+ integrity["commitment_sha256"], "bound-files commitment", size=32
582
+ )
583
+ if commitment != expected_commitment:
584
+ raise IntegrityError("pyencode runtime: bound-files commitment is invalid")
585
+
586
+ actual_code = {path for path in inventory if _is_code_path(path)}
587
+ allowed_code = {path for path in signed if _is_code_path(path)}
588
+ unexpected = sorted(actual_code.difference(allowed_code))
589
+ if unexpected:
590
+ raise IntegrityError(
591
+ "pyencode runtime: distribution contains unsigned executable file(s): "
592
+ + ", ".join(unexpected)
593
+ )
594
+ return commitment, frozenset(signed)
595
+
596
+
597
+ def _expiry_value(payload: Mapping[str, Any]) -> object | None:
598
+ policy = payload.get("policy")
599
+ if not isinstance(policy, dict):
600
+ _fail("manifest policy must be an object")
601
+ for name in ("expiry", "expires_at", "expires"):
602
+ if name in policy and policy[name] is not None:
603
+ return policy[name]
604
+ return None
605
+
606
+
607
+ def _parse_expiry(value: object) -> datetime:
608
+ if isinstance(value, bool):
609
+ _fail("policy expiry must be an ISO-8601 string or Unix timestamp")
610
+ if isinstance(value, (int, float)):
611
+ try:
612
+ return datetime.fromtimestamp(value, timezone.utc)
613
+ except (OverflowError, OSError, ValueError) as exc:
614
+ _fail("policy expiry timestamp is invalid", cause=exc)
615
+ if not isinstance(value, str) or not value:
616
+ _fail("policy expiry must be an ISO-8601 string or Unix timestamp")
617
+ try:
618
+ if len(value) == 10:
619
+ return datetime.combine(date.fromisoformat(value), time.max, timezone.utc)
620
+ normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value
621
+ parsed = datetime.fromisoformat(normalized)
622
+ except ValueError as exc:
623
+ _fail("policy expiry is not valid ISO-8601", cause=exc)
624
+ if parsed.tzinfo is None:
625
+ parsed = parsed.replace(tzinfo=timezone.utc)
626
+ return parsed.astimezone(timezone.utc)
627
+
628
+
629
+ def _check_policy(payload: Mapping[str, Any]) -> None:
630
+ value = _expiry_value(payload)
631
+ if value is None:
632
+ return
633
+ expiry = _parse_expiry(value)
634
+ if datetime.now(timezone.utc) > expiry:
635
+ raise PolicyError(
636
+ "pyencode runtime: this protected build expired at "
637
+ f"{expiry.isoformat().replace('+00:00', 'Z')}"
638
+ )
639
+
640
+
641
+ def _allow_extra_data(payload: Mapping[str, Any]) -> bool:
642
+ policy = payload.get("policy")
643
+ if not isinstance(policy, dict):
644
+ _fail("manifest policy must be an object")
645
+ value = policy.get("allow_extra_data", False)
646
+ if type(value) is not bool:
647
+ _fail("manifest allow_extra_data policy must be boolean")
648
+ return value
649
+
650
+
651
+ def _normalize_artifacts(value: object) -> dict[str, str]:
652
+ if not isinstance(value, dict) or not value:
653
+ _fail("manifest artifacts must be a non-empty object")
654
+ result: dict[str, str] = {}
655
+ for token, digest in value.items():
656
+ if not isinstance(token, str) or _TOKEN_RE.fullmatch(token) is None:
657
+ _fail("artifact tokens must be 32 lowercase hexadecimal characters")
658
+ if not isinstance(digest, str) or _DIGEST_RE.fullmatch(digest) is None:
659
+ _fail(f"sha256 for artifact {token!r} is invalid")
660
+ result[token] = digest
661
+ return dict(sorted(result.items()))
662
+
663
+
664
+ def _normalize_index_envelope(value: object) -> tuple[bytes, bytes, bytes]:
665
+ if not isinstance(value, dict) or set(value) != {
666
+ "format",
667
+ "cipher",
668
+ "compression",
669
+ "nonce_b64",
670
+ "ciphertext_b64",
671
+ "commitment_sha256",
672
+ }:
673
+ _fail("manifest module_index metadata has invalid fields")
674
+ if type(value["format"]) is not int or value["format"] != 1:
675
+ _fail("unsupported module-index format")
676
+ if value["cipher"] != "aes-256-gcm" or value["compression"] != "zlib":
677
+ _fail("unsupported module-index cipher or compression")
678
+ nonce = _decode_b64(value["nonce_b64"], "module-index nonce")
679
+ ciphertext = _decode_b64(
680
+ value["ciphertext_b64"], "module-index ciphertext", minimum=_TAG_SIZE
681
+ )
682
+ if len(nonce) != _NONCE_SIZE:
683
+ _fail(f"module-index nonce must decode to {_NONCE_SIZE} bytes")
684
+ commitment = _decode_hex(
685
+ value["commitment_sha256"], "module-index commitment", size=32
686
+ )
687
+ return nonce, ciphertext, commitment
688
+
689
+
690
+ def _validate_manifest(
691
+ payload: dict[str, Any], build_id: bytes
692
+ ) -> tuple[dict[str, str], bytes, bytes, bytes]:
693
+ required = {
694
+ "format",
695
+ "tool_version",
696
+ "build_id",
697
+ "python_tag",
698
+ "created_utc",
699
+ "module_index",
700
+ "artifacts",
701
+ "policy",
702
+ "integrity",
703
+ }
704
+ if set(payload) != required:
705
+ _fail("manifest format-3 fields are incomplete or unknown")
706
+ if type(payload["format"]) is not int or payload["format"] != 3:
707
+ _fail("unsupported manifest format")
708
+ if payload["build_id"] != _build.BUILD_ID:
709
+ raise IntegrityError("pyencode runtime: manifest build_id does not match runtime")
710
+ if payload["python_tag"] != _build.PYTHON_TAG:
711
+ raise CompatibilityError(
712
+ "pyencode runtime: manifest Python tag does not match runtime configuration"
713
+ )
714
+ if _decode_hex(payload["build_id"], "manifest build_id", size=16) != build_id:
715
+ _fail("manifest build_id is malformed")
716
+ _check_policy(payload)
717
+ artifacts = _normalize_artifacts(payload["artifacts"])
718
+ nonce, ciphertext, commitment = _normalize_index_envelope(payload["module_index"])
719
+ actual_commitment = _index_commitment(
720
+ nonce, ciphertext, _build.BUILD_ID, _build.PYTHON_TAG
721
+ )
722
+ if commitment != actual_commitment:
723
+ raise IntegrityError("pyencode runtime: module-index commitment is invalid")
724
+ return artifacts, nonce, ciphertext, commitment
725
+
726
+
727
+ def _decompress_strict(compressed: bytes, maximum: int, description: str) -> bytes:
728
+ decompressor = zlib.decompressobj()
729
+ try:
730
+ value = decompressor.decompress(compressed, maximum + 1)
731
+ if len(value) > maximum or decompressor.unconsumed_tail:
732
+ _fail(f"{description} exceeds the runtime safety limit")
733
+ value += decompressor.flush()
734
+ except zlib.error as exc:
735
+ _fail(f"{description} is not valid zlib data", cause=exc)
736
+ if len(value) > maximum:
737
+ _fail(f"{description} exceeds the runtime safety limit")
738
+ if not decompressor.eof or decompressor.unused_data:
739
+ _fail(f"{description} contains incomplete or trailing compressed data")
740
+ return value
741
+
742
+
743
+ def _decrypt_index(
744
+ nonce: bytes,
745
+ ciphertext: bytes,
746
+ support_root: bytes,
747
+ build_id: bytes,
748
+ ) -> tuple[str, dict[str, dict[str, Any]]]:
749
+ key = _derive_index_key(support_root, build_id)
750
+ try:
751
+ from cryptography.exceptions import InvalidTag
752
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
753
+ except ImportError as exc: # pragma: no cover - installation dependent
754
+ _fail("the 'cryptography' package is required", cause=exc)
755
+ try:
756
+ compressed = AESGCM(key).decrypt(
757
+ nonce,
758
+ ciphertext,
759
+ _index_aad(_build.BUILD_ID, _build.PYTHON_TAG),
760
+ )
761
+ except InvalidTag as exc:
762
+ raise IntegrityError("pyencode runtime: module-index authentication failed") from exc
763
+ raw = _decompress_strict(compressed, _MAX_INDEX_SIZE, "module index")
764
+ index = _decode_json(raw, "module index")
765
+ if not isinstance(index, dict) or _canonical_json(index) != raw:
766
+ _fail("module-index JSON is not canonical")
767
+ if set(index) != {"format", "entry_module", "modules"}:
768
+ _fail("module index has invalid fields")
769
+ if type(index["format"]) is not int or index["format"] != 1:
770
+ _fail("unsupported decrypted module-index format")
771
+ entry = _validate_module_name(index["entry_module"])
772
+ raw_modules = index["modules"]
773
+ if not isinstance(raw_modules, dict) or not raw_modules:
774
+ _fail("module index modules must be a non-empty object")
775
+ modules: dict[str, dict[str, Any]] = {}
776
+ tokens: set[str] = set()
777
+ for raw_name, metadata in raw_modules.items():
778
+ name = _validate_module_name(raw_name)
779
+ if not isinstance(metadata, dict) or set(metadata) != {"artifact", "package"}:
780
+ _fail(f"module-index metadata for {name!r} is invalid")
781
+ token = metadata["artifact"]
782
+ package = metadata["package"]
783
+ if not isinstance(token, str) or _TOKEN_RE.fullmatch(token) is None:
784
+ _fail(f"artifact token for module {name!r} is invalid")
785
+ if token in tokens:
786
+ _fail(f"duplicate artifact token {token!r}")
787
+ if type(package) is not bool:
788
+ _fail(f"package flag for module {name!r} must be boolean")
789
+ tokens.add(token)
790
+ modules[name] = {"artifact": token, "package": package}
791
+ if entry not in modules:
792
+ _fail("module index does not contain its configured entry module")
793
+ return entry, dict(sorted(modules.items()))
794
+
795
+
796
+ def _artifact_relative_path(fullname: str, package: bool, token: str) -> str:
797
+ parts = fullname.split(".")
798
+ directories = parts if package else parts[:-1]
799
+ return "/".join((*directories, f"{token}.pye"))
800
+
801
+
802
+ def _validate_artifacts(
803
+ root: Path,
804
+ modules: dict[str, dict[str, Any]],
805
+ artifacts: Mapping[str, str],
806
+ inventory: Mapping[str, Path],
807
+ directories: frozenset[str],
808
+ signed_paths: frozenset[str],
809
+ allow_extra_data: bool,
810
+ ) -> None:
811
+ index_tokens = {metadata["artifact"] for metadata in modules.values()}
812
+ if index_tokens != set(artifacts):
813
+ missing = sorted(index_tokens.difference(artifacts))
814
+ extra = sorted(set(artifacts).difference(index_tokens))
815
+ details: list[str] = []
816
+ if missing:
817
+ details.append("missing " + ", ".join(missing))
818
+ if extra:
819
+ details.append("unexpected " + ", ".join(extra))
820
+ _fail("artifact token set does not match module index (" + "; ".join(details) + ")")
821
+
822
+ expected_paths: dict[str, tuple[str, str]] = {}
823
+ for fullname, metadata in modules.items():
824
+ token = metadata["artifact"]
825
+ relative = _artifact_relative_path(fullname, metadata["package"], token)
826
+ if relative in expected_paths:
827
+ _fail(f"multiple protected modules map to artifact path {relative!r}")
828
+ expected_paths[relative] = (token, fullname)
829
+
830
+ actual_pye = {path for path in inventory if path.casefold().endswith(".pye")}
831
+ if actual_pye != set(expected_paths):
832
+ missing = sorted(set(expected_paths).difference(actual_pye))
833
+ extra = sorted(actual_pye.difference(expected_paths))
834
+ details = []
835
+ if missing:
836
+ details.append("missing " + ", ".join(missing))
837
+ if extra:
838
+ details.append("unexpected " + ", ".join(extra))
839
+ raise IntegrityError(
840
+ "pyencode runtime: protected artifact file set failed integrity check ("
841
+ + "; ".join(details)
842
+ + ")"
843
+ )
844
+
845
+ for relative, (token, fullname) in sorted(expected_paths.items()):
846
+ path = inventory[relative]
847
+ try:
848
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
849
+ except OSError as exc:
850
+ _fail(f"cannot read protected module {fullname!r}", cause=exc)
851
+ if digest != artifacts[token]:
852
+ raise IntegrityError(
853
+ f"pyencode runtime: protected module {fullname!r} failed its hash check"
854
+ )
855
+
856
+ expected_files = signed_paths | set(expected_paths) | {_build.MANIFEST_NAME}
857
+ actual_files = set(inventory)
858
+ missing_files = sorted(expected_files.difference(actual_files))
859
+ unexpected_files = (
860
+ []
861
+ if allow_extra_data
862
+ else sorted(actual_files.difference(expected_files))
863
+ )
864
+ if missing_files or unexpected_files:
865
+ details = []
866
+ if missing_files:
867
+ details.append("missing " + ", ".join(missing_files))
868
+ if unexpected_files:
869
+ details.append("unexpected " + ", ".join(unexpected_files))
870
+ raise IntegrityError(
871
+ "pyencode runtime: immutable distribution file set changed ("
872
+ + "; ".join(details)
873
+ + ")"
874
+ )
875
+
876
+ expected_directories: set[str] = set()
877
+ for relative in expected_files:
878
+ parts = PurePosixPath(relative).parts
879
+ for end in range(1, len(parts)):
880
+ expected_directories.add("/".join(parts[:end]))
881
+ missing = sorted(expected_directories.difference(directories))
882
+ extra = (
883
+ []
884
+ if allow_extra_data
885
+ else sorted(set(directories).difference(expected_directories))
886
+ )
887
+ if missing or extra:
888
+ details = []
889
+ if missing:
890
+ details.append("missing " + ", ".join(missing))
891
+ if extra:
892
+ details.append("unexpected " + ", ".join(extra))
893
+ raise IntegrityError(
894
+ "pyencode runtime: immutable distribution directory set changed ("
895
+ + "; ".join(details)
896
+ + ")"
897
+ )
898
+
899
+
900
+ class _ResourceReader(_ResourceReaderABC):
901
+ def __init__(self, directory: Path, root: Path) -> None:
902
+ self._directory = directory
903
+ self._root = root
904
+
905
+ def _resource(self, resource: str) -> Path:
906
+ if (
907
+ not isinstance(resource, str)
908
+ or not resource
909
+ or resource in (".", "..")
910
+ or "/" in resource
911
+ or "\\" in resource
912
+ ):
913
+ raise FileNotFoundError(resource)
914
+ path = (self._directory / resource).resolve()
915
+ try:
916
+ path.relative_to(self._root)
917
+ except ValueError as exc:
918
+ raise FileNotFoundError(resource) from exc
919
+ return path
920
+
921
+ def open_resource(self, resource: str):
922
+ return self._resource(resource).open("rb")
923
+
924
+ def resource_path(self, resource: str) -> str:
925
+ path = self._resource(resource)
926
+ if not path.is_file():
927
+ raise FileNotFoundError(resource)
928
+ return str(path)
929
+
930
+ def is_resource(self, name: str) -> bool:
931
+ try:
932
+ return self._resource(name).is_file()
933
+ except (FileNotFoundError, OSError):
934
+ return False
935
+
936
+ def contents(self) -> Iterator[str]:
937
+ try:
938
+ return (item.name for item in self._directory.iterdir())
939
+ except OSError:
940
+ return iter(())
941
+
942
+ def files(self):
943
+ """Expose a Traversable root for nested importlib.resources paths."""
944
+
945
+ return self._directory
946
+
947
+
948
+ class _ProtectedLoader(importlib.abc.Loader):
949
+ def __init__(self, finder: "_ProtectedFinder", fullname: str) -> None:
950
+ self._finder = finder
951
+ self._fullname = fullname
952
+
953
+ def create_module(self, spec):
954
+ return None
955
+
956
+ def exec_module(self, module: types.ModuleType) -> None:
957
+ self._finder._execute(self._fullname, module.__dict__)
958
+
959
+ def get_code(self, fullname: str) -> types.CodeType:
960
+ """Return only a tiny runpy trampoline, never decrypted application code."""
961
+
962
+ if fullname != self._fullname:
963
+ raise ImportError(f"loader for {self._fullname!r} cannot load {fullname!r}")
964
+ source = (
965
+ "from pyencode_runtime._runtime import _exec_runpy as __pye_exec\n"
966
+ f"__pye_exec({fullname!r}, globals())\n"
967
+ "del __pye_exec\n"
968
+ )
969
+ return compile(
970
+ source,
971
+ f"<pyencode-runpy:{fullname}>",
972
+ "exec",
973
+ dont_inherit=True,
974
+ optimize=2,
975
+ )
976
+
977
+ def get_source(self, fullname: str) -> None:
978
+ if fullname != self._fullname:
979
+ raise ImportError(f"loader for {self._fullname!r} cannot load {fullname!r}")
980
+ return None
981
+
982
+ def is_package(self, fullname: str) -> bool:
983
+ return bool(self._finder._metadata(fullname)["package"])
984
+
985
+ def get_filename(self, fullname: str) -> str:
986
+ return str(self._finder._artifact_path(fullname))
987
+
988
+ def get_data(self, path: str) -> bytes:
989
+ candidate = Path(path).resolve()
990
+ try:
991
+ candidate.relative_to(self._finder._root)
992
+ except ValueError as exc:
993
+ raise OSError(f"resource is outside the protected distribution: {path}") from exc
994
+ return candidate.read_bytes()
995
+
996
+ def get_resource_reader(self, fullname: str):
997
+ if fullname != self._fullname or not self.is_package(fullname):
998
+ return None
999
+ return _ResourceReader(
1000
+ self._finder._artifact_path(fullname).parent,
1001
+ self._finder._root,
1002
+ )
1003
+
1004
+
1005
+ class _ProtectedFinder(importlib.abc.MetaPathFinder):
1006
+ def __init__(
1007
+ self,
1008
+ payload: dict[str, Any],
1009
+ entry: str,
1010
+ modules: dict[str, dict[str, Any]],
1011
+ artifacts: dict[str, str],
1012
+ root: Path,
1013
+ build_id: bytes,
1014
+ support_commitment: bytes,
1015
+ index_commitment: bytes,
1016
+ ) -> None:
1017
+ self._payload = payload
1018
+ self._entry = entry
1019
+ self._modules = modules
1020
+ self._artifacts = artifacts
1021
+ self._root = root
1022
+ self._build_id = build_id
1023
+ self._support_commitment = support_commitment
1024
+ self._index_commitment = index_commitment
1025
+
1026
+ def _parent_path_represented(self, fullname: str, path: object) -> bool:
1027
+ if "." not in fullname:
1028
+ return True
1029
+ if path is None:
1030
+ return False
1031
+ logical_parent = self._root.joinpath(*fullname.split(".")[:-1]).resolve()
1032
+ try:
1033
+ locations = iter(path)
1034
+ except TypeError:
1035
+ return False
1036
+ for raw_location in locations:
1037
+ try:
1038
+ if Path(raw_location).resolve() == logical_parent:
1039
+ return True
1040
+ except (OSError, TypeError, ValueError):
1041
+ continue
1042
+ return False
1043
+
1044
+ def find_spec(self, fullname: str, path=None, target=None):
1045
+ metadata = self._modules.get(fullname)
1046
+ if metadata is None:
1047
+ return None
1048
+ if not self._parent_path_represented(fullname, path):
1049
+ return None
1050
+ loader = _ProtectedLoader(self, fullname)
1051
+ artifact = self._artifact_path(fullname)
1052
+ spec = importlib.util.spec_from_loader(
1053
+ fullname,
1054
+ loader,
1055
+ origin=str(artifact),
1056
+ is_package=bool(metadata["package"]),
1057
+ )
1058
+ spec.has_location = True
1059
+ if spec.submodule_search_locations is not None:
1060
+ location = str(artifact.parent)
1061
+ if location not in spec.submodule_search_locations:
1062
+ spec.submodule_search_locations.append(location)
1063
+ return spec
1064
+
1065
+ def invalidate_caches(self) -> None:
1066
+ return None
1067
+
1068
+ def _metadata(self, fullname: str) -> dict[str, Any]:
1069
+ try:
1070
+ return self._modules[fullname]
1071
+ except KeyError as exc:
1072
+ raise ImportError(f"unknown protected module {fullname!r}") from exc
1073
+
1074
+ def _artifact_path(self, fullname: str) -> Path:
1075
+ metadata = self._metadata(fullname)
1076
+ relative = _artifact_relative_path(
1077
+ fullname, metadata["package"], metadata["artifact"]
1078
+ )
1079
+ return _exact_file(self._root, relative)
1080
+
1081
+ def _execute(self, fullname: str, namespace: dict[str, Any]) -> None:
1082
+ _check_policy(self._payload)
1083
+ metadata = self._metadata(fullname)
1084
+ path = self._artifact_path(fullname)
1085
+ try:
1086
+ artifact = path.read_bytes()
1087
+ except OSError as exc:
1088
+ _fail(f"cannot read protected module {fullname!r}", cause=exc)
1089
+ token = metadata["artifact"]
1090
+ if hashlib.sha256(artifact).hexdigest() != self._artifacts[token]:
1091
+ raise IntegrityError(
1092
+ f"pyencode runtime: protected module {fullname!r} failed its hash check"
1093
+ )
1094
+ minimum = len(_MAGIC) + _NONCE_SIZE + _TAG_SIZE
1095
+ if len(artifact) < minimum or artifact[: len(_MAGIC)] != _MAGIC:
1096
+ _fail(f"protected module {fullname!r} has invalid PYE2 framing")
1097
+ nonce_start = len(_MAGIC)
1098
+ ciphertext_start = nonce_start + _NONCE_SIZE
1099
+ nonce = artifact[nonce_start:ciphertext_start]
1100
+ ciphertext = artifact[ciphertext_start:]
1101
+ embedded = _embedded_master_key(self._build_id)
1102
+ try:
1103
+ support_root = _derive_support_root(
1104
+ embedded, self._build_id, self._support_commitment
1105
+ )
1106
+ module_root = _derive_module_root(
1107
+ support_root, self._build_id, self._index_commitment
1108
+ )
1109
+ finally:
1110
+ for index in range(len(embedded)):
1111
+ embedded[index] = 0
1112
+ key = _derive_artifact_key(module_root, self._build_id, token, fullname)
1113
+ aad = _artifact_aad(
1114
+ fullname=fullname,
1115
+ package=metadata["package"],
1116
+ token=token,
1117
+ build_id=_build.BUILD_ID,
1118
+ python_tag=_build.PYTHON_TAG,
1119
+ )
1120
+ try:
1121
+ from cryptography.exceptions import InvalidTag
1122
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
1123
+ except ImportError as exc: # pragma: no cover - installation dependent
1124
+ _fail("the 'cryptography' package is required", cause=exc)
1125
+ try:
1126
+ compressed = AESGCM(key).decrypt(nonce, ciphertext, aad)
1127
+ except InvalidTag as exc:
1128
+ raise IntegrityError(
1129
+ f"pyencode runtime: authentication failed for module {fullname!r}"
1130
+ ) from exc
1131
+ marshalled = _decompress_strict(
1132
+ compressed, _MAX_MARSHALLED_SIZE, f"module payload for {fullname!r}"
1133
+ )
1134
+ try:
1135
+ code = marshal.loads(marshalled)
1136
+ except (EOFError, TypeError, ValueError) as exc:
1137
+ _fail(f"invalid marshalled code for module {fullname!r}", cause=exc)
1138
+ if not isinstance(code, types.CodeType):
1139
+ _fail(f"payload for module {fullname!r} is not a Python code object")
1140
+ exec(code, namespace)
1141
+
1142
+ def _resolved_entry(self) -> tuple[str, dict[str, Any]]:
1143
+ entry = self._entry
1144
+ metadata = self._metadata(entry)
1145
+ if metadata["package"]:
1146
+ candidate = f"{entry}.__main__"
1147
+ if candidate not in self._modules:
1148
+ _fail(f"entry package {entry!r} has no protected __main__ module")
1149
+ entry = candidate
1150
+ metadata = self._metadata(entry)
1151
+ return entry, metadata
1152
+
1153
+ def _protected_entry_names(self) -> frozenset[str]:
1154
+ entry, _metadata = self._resolved_entry()
1155
+ return frozenset({self._entry, entry})
1156
+
1157
+ def _prepare_parent(self, entry: str) -> str:
1158
+ parent = entry.rpartition(".")[0]
1159
+ if not parent:
1160
+ return ""
1161
+ parts = parent.split(".")
1162
+ for end in range(1, len(parts) + 1):
1163
+ name = ".".join(parts[:end])
1164
+ child_name = name + ".__pyencode_child__"
1165
+ existing = sys.modules.get(name)
1166
+ if existing is not None:
1167
+ locations = getattr(existing, "__path__", None)
1168
+ if not self._parent_path_represented(child_name, locations):
1169
+ raise IntegrityError(
1170
+ f"pyencode runtime: parent package {name!r} for {entry!r} "
1171
+ "does not include the protected distribution path"
1172
+ )
1173
+ continue
1174
+
1175
+ try:
1176
+ spec = importlib.util.find_spec(name)
1177
+ except (ImportError, AttributeError, ValueError) as exc:
1178
+ raise IntegrityError(
1179
+ f"pyencode runtime: cannot resolve parent package {name!r} "
1180
+ f"for {entry!r}"
1181
+ ) from exc
1182
+ locations = None if spec is None else spec.submodule_search_locations
1183
+ if not self._parent_path_represented(child_name, locations):
1184
+ raise IntegrityError(
1185
+ f"pyencode runtime: parent package {name!r} for {entry!r} "
1186
+ "does not include the protected distribution path"
1187
+ )
1188
+ importlib.import_module(name)
1189
+ return parent
1190
+
1191
+ def _multiprocessing_main(self, namespace: dict[str, Any]) -> None:
1192
+ entry, metadata = self._resolved_entry()
1193
+ parent = self._prepare_parent(entry)
1194
+ loader = _ProtectedLoader(self, entry)
1195
+ artifact = self._artifact_path(entry)
1196
+ spec = importlib.util.spec_from_loader(
1197
+ entry,
1198
+ loader,
1199
+ origin=str(artifact),
1200
+ is_package=bool(metadata["package"]),
1201
+ )
1202
+ spec.has_location = True
1203
+ namespace.update(
1204
+ {
1205
+ "__file__": str(artifact),
1206
+ "__cached__": None,
1207
+ "__loader__": loader,
1208
+ "__package__": entry if metadata["package"] else parent,
1209
+ "__spec__": spec,
1210
+ }
1211
+ )
1212
+ self._execute(entry, namespace)
1213
+
1214
+ def _run_entry(self) -> dict[str, Any]:
1215
+ entry, metadata = self._resolved_entry()
1216
+ parent = self._prepare_parent(entry)
1217
+ loader = _ProtectedLoader(self, entry)
1218
+ artifact = self._artifact_path(entry)
1219
+ spec = importlib.util.spec_from_loader(
1220
+ entry,
1221
+ loader,
1222
+ origin=str(artifact),
1223
+ is_package=bool(metadata["package"]),
1224
+ )
1225
+ spec.has_location = True
1226
+ namespace: dict[str, Any] = {
1227
+ "__name__": "__main__",
1228
+ "__file__": str(artifact),
1229
+ "__cached__": None,
1230
+ "__loader__": loader,
1231
+ "__package__": entry if metadata["package"] else parent,
1232
+ "__spec__": spec,
1233
+ }
1234
+ main_module = types.ModuleType("__main__")
1235
+ main_module.__dict__.update(namespace)
1236
+ previous_main = sys.modules.get("__main__")
1237
+ previous_argv0 = sys.argv[0] if sys.argv else None
1238
+ sys.modules["__main__"] = main_module
1239
+ if sys.argv:
1240
+ sys.argv[0] = str(artifact)
1241
+ try:
1242
+ self._execute(entry, main_module.__dict__)
1243
+ return main_module.__dict__
1244
+ finally:
1245
+ if previous_main is None:
1246
+ sys.modules.pop("__main__", None)
1247
+ else:
1248
+ sys.modules["__main__"] = previous_main
1249
+ if sys.argv and previous_argv0 is not None:
1250
+ sys.argv[0] = previous_argv0
1251
+
1252
+
1253
+ _install_lock = threading.RLock()
1254
+ _installed_finder: _ProtectedFinder | None = None
1255
+ _multiprocessing_patch_installed = False
1256
+
1257
+
1258
+ def _exec_runpy(fullname: str, namespace: dict[str, Any]) -> None:
1259
+ """Execute protected code into runpy's namespace without returning it."""
1260
+
1261
+ finder = _installed_finder
1262
+ if finder is None:
1263
+ _fail("runpy requested a protected module before runtime installation")
1264
+ finder._execute(fullname, namespace)
1265
+
1266
+
1267
+ def _multiprocessing_bootstrap(namespace: dict[str, Any]) -> None:
1268
+ """Populate multiprocessing's ``__mp_main__`` with the protected entry."""
1269
+
1270
+ install()
1271
+ finder = _installed_finder
1272
+ if finder is None: # pragma: no cover - defensive invariant
1273
+ _fail("multiprocessing bootstrap could not install the runtime")
1274
+ finder._multiprocessing_main(namespace)
1275
+
1276
+
1277
+ def _validate_preloaded_modules(
1278
+ modules: Mapping[str, Mapping[str, Any]], root: Path, finder: _ProtectedFinder | None
1279
+ ) -> None:
1280
+ missing = object()
1281
+ protected_names = set(modules)
1282
+ parent_names: set[str] = set()
1283
+ for fullname in protected_names:
1284
+ parts = fullname.split(".")
1285
+ parent_names.update(".".join(parts[:end]) for end in range(1, len(parts)))
1286
+
1287
+ for fullname in sorted(protected_names):
1288
+ existing = sys.modules.get(fullname, missing)
1289
+ if existing is missing:
1290
+ continue
1291
+ spec = getattr(existing, "__spec__", None)
1292
+ loader = getattr(spec, "loader", None)
1293
+ if (
1294
+ finder is not None
1295
+ and isinstance(loader, _ProtectedLoader)
1296
+ and loader._finder is finder
1297
+ and loader._fullname == fullname
1298
+ and getattr(spec, "name", None) == fullname
1299
+ and getattr(existing, "__loader__", None) is loader
1300
+ and getattr(spec, "origin", None) == str(finder._artifact_path(fullname))
1301
+ ):
1302
+ continue
1303
+ raise IntegrityError(
1304
+ f"pyencode runtime: protected module {fullname!r} was preloaded "
1305
+ "outside the verified finder"
1306
+ )
1307
+
1308
+ for parent in sorted(parent_names.difference(protected_names)):
1309
+ existing = sys.modules.get(parent, missing)
1310
+ if existing is missing:
1311
+ continue
1312
+ expected = root.joinpath(*parent.split(".")).resolve()
1313
+ locations = getattr(existing, "__path__", None)
1314
+ represented = False
1315
+ if locations is not None:
1316
+ for raw_location in locations:
1317
+ try:
1318
+ if Path(raw_location).resolve() == expected:
1319
+ represented = True
1320
+ break
1321
+ except (OSError, TypeError, ValueError):
1322
+ continue
1323
+ if not represented:
1324
+ raise IntegrityError(
1325
+ f"pyencode runtime: protected parent namespace {parent!r} was "
1326
+ "preloaded outside the verified distribution"
1327
+ )
1328
+
1329
+
1330
+ def _enable_multiprocessing_bootstrap() -> None:
1331
+ global _multiprocessing_patch_installed
1332
+ if _multiprocessing_patch_installed:
1333
+ return
1334
+ try:
1335
+ import multiprocessing.spawn as spawn
1336
+ except ImportError: # pragma: no cover - CPython always ships multiprocessing
1337
+ return
1338
+ original = spawn.get_preparation_data
1339
+
1340
+ def protected_preparation_data(name):
1341
+ data = original(name)
1342
+ finder = _installed_finder
1343
+ initial_name = data.get("init_main_from_name")
1344
+ if finder is not None and initial_name in finder._protected_entry_names():
1345
+ data.pop("init_main_from_path", None)
1346
+ data["init_main_from_name"] = "pyencode_runtime._mp_main"
1347
+ return data
1348
+
1349
+ spawn.get_preparation_data = protected_preparation_data
1350
+ # This is consumed when a fresh spawn interpreter starts, before it imports
1351
+ # the signed runtime package and could otherwise create an unsigned cache.
1352
+ os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
1353
+ _multiprocessing_patch_installed = True
1354
+
1355
+
1356
+ def _verified_build_state() -> tuple[
1357
+ dict[str, Any],
1358
+ str,
1359
+ dict[str, dict[str, Any]],
1360
+ dict[str, str],
1361
+ Path,
1362
+ bytes,
1363
+ bytes,
1364
+ bytes,
1365
+ ]:
1366
+ build_id = _validate_build_config()
1367
+ payload, root = _read_manifest()
1368
+ inventory, directories = _scan_distribution(root)
1369
+ artifacts, nonce, ciphertext, index_commitment = _validate_manifest(
1370
+ payload, build_id
1371
+ )
1372
+ allow_extra_data = _allow_extra_data(payload)
1373
+ support_commitment, signed_paths = _validate_bound_files(payload, root, inventory)
1374
+ embedded = _embedded_master_key(build_id)
1375
+ try:
1376
+ support_root = _derive_support_root(embedded, build_id, support_commitment)
1377
+ entry, modules = _decrypt_index(nonce, ciphertext, support_root, build_id)
1378
+ finally:
1379
+ for index in range(len(embedded)):
1380
+ embedded[index] = 0
1381
+ if {metadata["artifact"] for metadata in modules.values()} != set(artifacts):
1382
+ _fail("artifact token set does not match decrypted module index")
1383
+ _validate_artifacts(
1384
+ root,
1385
+ modules,
1386
+ artifacts,
1387
+ inventory,
1388
+ directories,
1389
+ signed_paths,
1390
+ allow_extra_data,
1391
+ )
1392
+ return (
1393
+ payload,
1394
+ entry,
1395
+ modules,
1396
+ artifacts,
1397
+ root,
1398
+ build_id,
1399
+ support_commitment,
1400
+ index_commitment,
1401
+ )
1402
+
1403
+
1404
+ def install() -> None:
1405
+ """Verify this distribution and install its exec-only import hook.
1406
+
1407
+ Repeated calls fully revalidate signed support files and encrypted metadata.
1408
+ The function intentionally returns no finder or key-bearing state.
1409
+ """
1410
+
1411
+ global _installed_finder
1412
+ with _install_lock:
1413
+ (
1414
+ payload,
1415
+ entry,
1416
+ modules,
1417
+ artifacts,
1418
+ root,
1419
+ build_id,
1420
+ support_commitment,
1421
+ index_commitment,
1422
+ ) = _verified_build_state()
1423
+ _validate_preloaded_modules(modules, root, _installed_finder)
1424
+ if _installed_finder is not None:
1425
+ finder = _installed_finder
1426
+ stable = (
1427
+ finder._entry == entry
1428
+ and finder._modules == modules
1429
+ and finder._artifacts == artifacts
1430
+ and finder._root == root
1431
+ and finder._build_id == build_id
1432
+ and finder._support_commitment == support_commitment
1433
+ and finder._index_commitment == index_commitment
1434
+ )
1435
+ if not stable:
1436
+ raise IntegrityError(
1437
+ "pyencode runtime: verified build state changed after installation"
1438
+ )
1439
+ finder._payload = payload
1440
+ if finder not in sys.meta_path:
1441
+ sys.meta_path.insert(0, finder)
1442
+ _enable_multiprocessing_bootstrap()
1443
+ return None
1444
+ finder = _ProtectedFinder(
1445
+ payload,
1446
+ entry,
1447
+ modules,
1448
+ artifacts,
1449
+ root,
1450
+ build_id,
1451
+ support_commitment,
1452
+ index_commitment,
1453
+ )
1454
+ sys.meta_path.insert(0, finder)
1455
+ _installed_finder = finder
1456
+ _enable_multiprocessing_bootstrap()
1457
+ return None
1458
+
1459
+
1460
+ def run() -> dict[str, Any]:
1461
+ """Verify, install, and execute the encrypted entry as ``__main__``."""
1462
+
1463
+ install()
1464
+ finder = _installed_finder
1465
+ if finder is None: # pragma: no cover - defensive invariant
1466
+ _fail("protected-module finder was not installed")
1467
+ return finder._run_entry()
1468
+
1469
+
1470
+ def load_entry() -> types.ModuleType:
1471
+ """Verify, install, and import the encrypted entry for an embedding host."""
1472
+
1473
+ install()
1474
+ finder = _installed_finder
1475
+ if finder is None: # pragma: no cover - defensive invariant
1476
+ _fail("protected-module finder was not installed")
1477
+ return importlib.import_module(finder._entry)
1478
+
1479
+
1480
+ __all__ = [
1481
+ "CompatibilityError",
1482
+ "IntegrityError",
1483
+ "PolicyError",
1484
+ "PyEncodeRuntimeError",
1485
+ "install",
1486
+ "load_entry",
1487
+ "run",
1488
+ ]