4"""Create and verify the root-owned managed Python environment receipt."""
6from __future__
import annotations
21from collections.abc
import Callable
22from dataclasses
import dataclass
23from pathlib
import Path
24from types
import ModuleType
25from typing
import Any, NoReturn, Protocol
28class ManagedEnvironmentError(RuntimeError):
29 """Report a failed managed-environment authentication check."""
32@dataclass(frozen=
True)
34 """Describe the owners accepted while checking filesystem objects."""
37 route_uids: frozenset[int]
40 def production(cls) -> TrustPolicy:
41 """Return the root-only production trust policy."""
42 return cls(environment_uid=0, route_uids=frozenset({0}))
45 def selftest(cls) -> TrustPolicy:
46 """Permit the current account only inside the private selftest tree."""
48 return cls(environment_uid=uid, route_uids=frozenset({0, uid}))
51@dataclass(frozen=True)
53 """Capture fields whose change means a checked object was replaced."""
64 def from_stat(cls, value: os.stat_result) -> ObjectIdentity:
65 """Build an identity from one stat result."""
72 mtime_ns=value.st_mtime_ns,
73 ctime_ns=value.st_ctime_ns,
76 def cache_fields(self) -> tuple[int, ...]:
77 """Return every identity field used to invalidate a warm authentication."""
89@dataclass(frozen=True)
90class InterpreterProbe:
91 """Hold identity facts reported by the authenticated interpreter."""
98@dataclass(frozen=True)
99class EnvironmentTreeIdentity:
100 """Bind every trusted object and regular-file byte under the environment."""
104 regular_file_bytes: int
107@dataclass(frozen=True)
108class TreeTrustContext:
109 """Hold the trust boundary used while authenticating nested tree objects."""
112 interpreter_target: Path
116class DigestWriter(Protocol):
117 """Describe the only hash-object operation used by record framing."""
119 def update(self, data: bytes) ->
None:
120 """Append bytes to the digest state."""
123@dataclass(frozen=True)
125 """Hold captured output and the normalized process status."""
132@dataclass(frozen=True)
134 """Name the environment and locked inputs bound into one receipt."""
143 arguments: list[str], environment: dict[str, str], timeout_seconds: int
145 """Run one absolute executable without a shell and capture both streams."""
146 executable = Path(arguments[0])
147 if not executable.is_absolute():
148 _fail(f
"refusing to execute a non-absolute command: {executable}")
149 with tempfile.TemporaryFile()
as stdout_file, tempfile.TemporaryFile()
as stderr_file:
151 (os.POSIX_SPAWN_DUP2, stdout_file.fileno(), 1),
152 (os.POSIX_SPAWN_DUP2, stderr_file.fileno(), 2),
155 process = os.posix_spawn(str(executable), arguments, environment, file_actions=actions)
156 except OSError
as error:
157 _fail(f
"cannot execute {executable}: {error}")
158 deadline = time.monotonic() + timeout_seconds
161 waited, status = os.waitpid(process, os.WNOHANG)
162 if waited == process:
164 if time.monotonic() >= deadline:
166 os.waitpid(process, 0)
167 _fail(f
"command timed out after {timeout_seconds}s: {executable}")
171 stdout = stdout_file.read().decode(
"utf-8", errors=
"replace")
172 stderr = stderr_file.read().decode(
"utf-8", errors=
"replace")
173 returncode = os.waitstatus_to_exitcode(status)
174 return CommandResult(returncode, stdout, stderr)
177RECEIPT_NAME =
".ra8-managed-python-v1.json"
181def _fail(message: str) -> NoReturn:
182 """Raise one consistently typed authentication failure."""
183 raise ManagedEnvironmentError(message)
186def _load_checks() -> ModuleType:
187 """Load the adjacent QA helper explicitly even under isolated Python mode."""
188 name =
"_ra8_managed_python_env_checks"
189 loaded = sys.modules.get(name)
190 if isinstance(loaded, ModuleType):
192 path = Path(__file__).with_name(
"managed_python_env_checks.py")
193 spec = importlib.util.spec_from_file_location(name, path)
194 if spec
is None or spec.loader
is None:
195 _fail(f
"cannot load managed-environment QA helper: {path}")
196 module = importlib.util.module_from_spec(spec)
197 sys.modules[name] = module
198 spec.loader.exec_module(module)
202def _identity(path: Path, *, follow_symlinks: bool =
False) -> ObjectIdentity:
203 """Read an object's stable identity without following links by default."""
205 value = path.stat()
if follow_symlinks
else path.lstat()
206 except OSError
as error:
207 _fail(f
"cannot stat {path}: {error}")
208 return ObjectIdentity.from_stat(value)
211def _require_owner_mode(
213 identity: ObjectIdentity,
214 owners: frozenset[int],
216 allow_symlink: bool =
False,
218 """Reject an unsafe owner, writable route, or unexpected object kind."""
219 if identity.uid
not in owners:
220 _fail(f
"untrusted owner for {path}: uid {identity.uid}")
221 if stat.S_ISLNK(identity.mode):
222 if not allow_symlink:
223 _fail(f
"symlink is forbidden in managed environment route: {path}")
225 if identity.mode & 0o022:
226 _fail(f
"group/other-writable managed environment route: {path}")
229def _path_components(path: Path) -> list[Path]:
230 """Return every absolute route component from root through path."""
231 components = [Path(path.anchor)]
232 current = Path(path.anchor)
233 for part
in path.parts[1:]:
235 components.append(current)
239def _validate_route(path: Path, owners: frozenset[int]) -> dict[Path, ObjectIdentity]:
240 """Validate every route component and return its pre-use identity."""
241 snapshots: dict[Path, ObjectIdentity] = {}
242 for component
in _path_components(path):
243 identity = _identity(component)
244 if not stat.S_ISDIR(identity.mode):
245 _fail(f
"managed environment route component is not a directory: {component}")
246 _require_owner_mode(component, identity, owners)
247 snapshots[component] = identity
251def _canonical_environment(raw: str) -> Path:
252 """Require an existing absolute path already in canonical spelling."""
253 if not raw
or not Path(raw).is_absolute()
or raw == os.sep:
254 _fail(f
"managed environment must be a non-root absolute path: {raw!r}")
255 if any(unicodedata.category(character) ==
"Cc" for character
in raw):
256 _fail(
"managed environment path contains a control character")
257 normalized = os.path.normpath(raw)
258 if raw != normalized:
259 _fail(f
"managed environment path is not canonical: {raw}")
261 resolved = str(Path(raw).resolve(strict=
True))
262 except OSError
as error:
263 _fail(f
"managed environment does not resolve: {raw}: {error}")
265 _fail(f
"managed environment route contains a symlink: {raw} -> {resolved}")
269def _safe_file_bytes(path: Path) -> tuple[bytes, ObjectIdentity]:
270 """Read one regular file through a no-follow descriptor and recheck it."""
271 flags = os.O_RDONLY | getattr(os,
"O_CLOEXEC", 0) | getattr(os,
"O_NOFOLLOW", 0)
273 descriptor = os.open(path, flags)
274 except OSError
as error:
275 _fail(f
"cannot open trusted file {path}: {error}")
277 before = ObjectIdentity.from_stat(os.fstat(descriptor))
278 if not stat.S_ISREG(before.mode):
279 _fail(f
"trusted file is not regular: {path}")
280 chunks: list[bytes] = []
282 chunk = os.read(descriptor, 1024 * 1024)
286 after = ObjectIdentity.from_stat(os.fstat(descriptor))
288 _fail(f
"trusted file changed while it was read: {path}")
289 return b
"".join(chunks), before
294def _sha256_file(path: Path) -> str:
295 """Hash one no-follow regular file."""
296 data, _identity_value = _safe_file_bytes(path)
297 return hashlib.sha256(data).hexdigest()
300def _tree_digest_record(
301 digest: DigestWriter,
304 identity: ObjectIdentity,
307 """Append one length-delimited filesystem record to the tree digest."""
310 os.fsencode(str(relative)),
311 stat.S_IMODE(identity.mode).to_bytes(4,
"big"),
312 identity.uid.to_bytes(8,
"big"),
313 (identity.size
if kind != b
"d" else 0).to_bytes(8,
"big"),
317 digest.update(len(field).to_bytes(8,
"big"))
321def _trusted_regular_file_digest(path: Path, policy: TrustPolicy) -> tuple[bytes, ObjectIdentity]:
322 """Hash one immutable environment file while holding a no-follow descriptor."""
323 flags = os.O_RDONLY | getattr(os,
"O_CLOEXEC", 0) | getattr(os,
"O_NOFOLLOW", 0)
325 descriptor = os.open(path, flags)
326 except OSError
as error:
327 _fail(f
"cannot open managed environment file {path}: {error}")
329 before = ObjectIdentity.from_stat(os.fstat(descriptor))
330 if not stat.S_ISREG(before.mode):
331 _fail(f
"managed environment file is not regular: {path}")
332 _require_owner_mode(path, before, frozenset({policy.environment_uid}))
333 digest = hashlib.sha256()
334 remaining = before.size
336 chunk = os.read(descriptor,
min(1024 * 1024, remaining))
338 _fail(f
"managed environment file was truncated while hashing: {path}")
340 remaining -= len(chunk)
341 if os.read(descriptor, 1):
342 _fail(f
"managed environment file grew while hashing: {path}")
343 after = ObjectIdentity.from_stat(os.fstat(descriptor))
345 _fail(f
"managed environment file changed while hashing: {path}")
346 return digest.digest(), before
351def _trusted_tree_symlink(
354 identity: ObjectIdentity,
355 context: TreeTrustContext,
357 """Validate one link target and return its exact spelling for the digest."""
361 frozenset({context.policy.environment_uid}),
365 spelling = os.fsencode(path.readlink())
366 resolved = path.resolve(strict=
True)
367 except OSError
as error:
368 _fail(f
"cannot resolve managed environment symlink {path}: {error}")
370 resolved.relative_to(context.environment)
372 python_link = relative.parent == Path(
"bin")
and re.fullmatch(
373 r"python(?:3(?:\.\d+)?)?", relative.name
375 if not python_link
or resolved != context.interpreter_target:
376 _fail(f
"managed environment symlink escapes its root: {path} -> {resolved}")
380def _environment_tree_identity(
381 environment: Path, interpreter_target: Path, policy: TrustPolicy
382) -> EnvironmentTreeIdentity:
383 """Authenticate and hash the bounded environment tree without following links."""
384 maximum_entries = 100_000
385 maximum_bytes = 2 * 1024 * 1024 * 1024
386 digest = hashlib.sha256(b
"ra8-managed-python-tree-v1\0")
387 context = TreeTrustContext(environment, interpreter_target, policy)
388 pending = [environment]
390 regular_file_bytes = 0
392 directory = pending.pop()
393 directory_identity = _identity(directory)
394 if not stat.S_ISDIR(directory_identity.mode):
395 _fail(f
"managed environment tree entry is not a directory: {directory}")
396 _require_owner_mode(directory, directory_identity, frozenset({policy.environment_uid}))
397 relative_directory = directory.relative_to(environment)
398 _tree_digest_record(digest, b
"d", relative_directory, directory_identity, b
"")
400 if entries > maximum_entries:
401 _fail(
"managed environment exceeds the authenticated tree bounds")
403 children = sorted(directory.iterdir(), key=
lambda item: os.fsencode(item.name))
404 except OSError
as error:
405 _fail(f
"cannot enumerate managed environment directory {directory}: {error}")
406 for child
in reversed(children):
407 relative = child.relative_to(environment)
408 if relative == Path(RECEIPT_NAME):
410 identity = _identity(child)
411 if stat.S_ISDIR(identity.mode):
412 pending.append(child)
414 if stat.S_ISREG(identity.mode):
415 content_digest, stable_identity = _trusted_regular_file_digest(child, policy)
416 _tree_digest_record(digest, b
"f", relative, stable_identity, content_digest)
417 regular_file_bytes += stable_identity.size
418 elif stat.S_ISLNK(identity.mode):
419 spelling = _trusted_tree_symlink(child, relative, identity, context)
420 _tree_digest_record(digest, b
"l", relative, identity, spelling)
422 _fail(f
"unsupported object in managed environment tree: {child}")
424 if entries > maximum_entries
or regular_file_bytes > maximum_bytes:
425 _fail(
"managed environment exceeds the authenticated tree bounds")
426 return EnvironmentTreeIdentity(digest.hexdigest(), entries, regular_file_bytes)
429def _interpreter_chain(
430 interpreter: Path, policy: TrustPolicy
431) -> tuple[Path, dict[Path, ObjectIdentity]]:
432 """Resolve and validate every symlink object in the interpreter chain."""
433 current = interpreter
434 snapshots: dict[Path, ObjectIdentity] = {}
435 seen: set[Path] = set()
436 for _hop
in range(32):
438 _fail(f
"interpreter symlink loop: {current}")
440 snapshots.update(_validate_route(current.parent, policy.route_uids))
441 identity = _identity(current)
442 snapshots[current] = identity
443 _require_owner_mode(current, identity, policy.route_uids, allow_symlink=
True)
444 if stat.S_ISLNK(identity.mode):
445 target = current.readlink()
446 current = target
if target.is_absolute()
else current.parent / target
447 current = Path(os.path.normpath(current))
449 if not stat.S_ISREG(identity.mode)
or not identity.mode & 0o111:
450 _fail(f
"managed interpreter target is not executable and regular: {current}")
451 return current, snapshots
452 _fail(f
"managed interpreter symlink chain is too deep: {interpreter}")
455def _check_snapshots(snapshots: dict[Path, ObjectIdentity]) ->
None:
456 """Reject any checked object whose identity changed after validation."""
457 for path, before
in snapshots.items():
458 after = _identity(path)
459 if stat.S_ISDIR(before.mode):
460 stable = (before.device, before.inode, before.mode, before.uid)
461 current = (after.device, after.inode, after.mode, after.uid)
465 if current != stable:
466 _fail(f
"managed environment object changed during verification: {path}")
469def _probe_interpreter(interpreter: Path, environment: Path) -> InterpreterProbe:
470 """Query only standard-library facts after interpreter bytes are trusted."""
473import importlib.metadata
479 return re.sub(r"[-_.]+", "-", name).lower()
482 f"{canonical(dist.metadata['Name'])}=={dist.version}"
483 for dist in importlib.metadata.distributions()
486 "base_prefix": sys.base_prefix,
487 "executable": sys.executable,
488 "implementation": sys.implementation.name,
489 "packages_sha256": hashlib.sha256(
490 json.dumps(packages, separators=(",", ":"), ensure_ascii=True).encode("ascii")
492 "prefix": sys.prefix,
493 "version": ".".join(str(value) for value in sys.version_info[:3]),
495print(json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True))
501 "PATH":
"/usr/bin:/bin",
502 "PYTHONNOUSERSITE":
"1",
504 result = _run_command([str(interpreter),
"-I",
"-c", probe], clean_env, 30)
505 if result.returncode != 0:
506 _fail(f
"managed interpreter probe failed with exit {result.returncode}")
508 payload = json.loads(result.stdout)
509 except json.JSONDecodeError
as error:
510 _fail(f
"managed interpreter returned malformed identity: {error}")
511 if not isinstance(payload, dict):
512 _fail(
"managed interpreter returned a non-object identity")
513 prefix = str(Path(str(payload.get(
"prefix",
""))).resolve())
514 executable = str(Path(str(payload.get(
"executable",
""))).resolve())
515 if prefix != str(environment)
or payload.get(
"base_prefix") == payload.get(
"prefix"):
516 _fail(
"managed interpreter is not bound to the declared virtual environment")
517 if executable != str(interpreter.resolve()):
518 _fail(
"managed interpreter reported a different executable target")
519 implementation = payload.get(
"implementation")
520 version = payload.get(
"version")
521 packages_sha256 = payload.get(
"packages_sha256")
522 if implementation !=
"cpython" or not re.fullmatch(
r"3\.(11|12|13|14)\.\d+", str(version)):
523 _fail(f
"unsupported managed Python identity: {implementation} {version}")
524 if not re.fullmatch(
r"[0-9a-f]{64}", str(packages_sha256)):
525 _fail(
"managed interpreter returned an invalid installed-set digest")
526 return InterpreterProbe(str(implementation), str(version), str(packages_sha256))
530 sources: ReceiptSources,
531 interpreter_target: Path,
532 probe: InterpreterProbe,
533 tree: EnvironmentTreeIdentity,
535 """Build the complete deterministic receipt payload."""
537 "authority_sha256": _sha256_file(Path(__file__)),
538 "checks_sha256": _sha256_file(Path(__file__).with_name(
"managed_python_env_checks.py")),
539 "dependency_group": sources.group,
540 "environment_path": str(sources.environment),
541 "environment_tree_entries": tree.entries,
542 "environment_tree_regular_file_bytes": tree.regular_file_bytes,
543 "environment_tree_sha256": tree.sha256,
544 "installed_distributions_sha256": probe.installed_sha256,
545 "interpreter_target": str(interpreter_target),
546 "pyproject_sha256": _sha256_file(sources.pyproject),
547 "python_implementation": probe.implementation,
548 "python_version": probe.version,
549 "receipt_version": 1,
550 "uv_lock_sha256": _sha256_file(sources.lockfile),
555 environment_raw: str,
560) -> tuple[Path, dict[str, Any]]:
561 """Authenticate the environment route and collect its current identity."""
562 if not re.fullmatch(
r"[a-z][a-z0-9-]{0,31}", group):
563 _fail(f
"invalid dependency group identity: {group!r}")
564 environment = _canonical_environment(environment_raw)
565 snapshots = _validate_route(environment, policy.route_uids)
566 environment_identity = snapshots[environment]
567 if environment_identity.uid != policy.environment_uid:
568 _fail(f
"managed environment is not owned by uid {policy.environment_uid}: {environment}")
569 bin_dir = environment /
"bin"
570 bin_identity = _identity(bin_dir)
571 if not stat.S_ISDIR(bin_identity.mode)
or bin_identity.uid != policy.environment_uid:
572 _fail(f
"managed environment bin directory has an untrusted owner: {bin_dir}")
573 _require_owner_mode(bin_dir, bin_identity, frozenset({policy.environment_uid}))
574 snapshots[bin_dir] = bin_identity
575 interpreter = bin_dir /
"python3"
576 target, interpreter_snapshots = _interpreter_chain(interpreter, policy)
577 snapshots.update(interpreter_snapshots)
578 tree_before = _environment_tree_identity(environment, target, policy)
579 probe = _probe_interpreter(interpreter, environment)
580 tree_after = _environment_tree_identity(environment, target, policy)
581 if tree_before != tree_after:
582 _fail(
"managed environment tree changed while its interpreter was probed")
583 sources = ReceiptSources(environment, pyproject, lockfile, group)
584 payload = _receipt_payload(sources, target, probe, tree_after)
585 _check_snapshots(snapshots)
586 return environment, payload
590 environment: Path, policy: TrustPolicy
591) -> tuple[dict[str, Any], Path, ObjectIdentity]:
592 """Read and validate the immutable receipt through a no-follow descriptor."""
593 receipt = environment / RECEIPT_NAME
594 data, identity = _safe_file_bytes(receipt)
595 if identity.uid != policy.environment_uid:
596 _fail(f
"managed environment receipt has an untrusted owner: {receipt}")
597 if stat.S_IMODE(identity.mode) != RECEIPT_MODE:
598 _fail(f
"managed environment receipt must have mode 0444: {receipt}")
600 payload = json.loads(data.decode(
"ascii"))
601 except (UnicodeDecodeError, json.JSONDecodeError)
as error:
602 _fail(f
"managed environment receipt is malformed: {error}")
603 if not isinstance(payload, dict):
604 _fail(
"managed environment receipt must contain one JSON object")
605 return payload, receipt, identity
609 environment_raw: str,
615 """Verify the receipt and return the authenticated bin directory."""
616 environment = _canonical_environment(environment_raw)
617 route = _validate_route(environment, policy.route_uids)
618 if route[environment].uid != policy.environment_uid:
619 _fail(f
"managed environment is not owned by uid {policy.environment_uid}: {environment}")
620 receipt, receipt_path, receipt_identity = _read_receipt(environment, policy)
621 checked_environment, current = _collect_payload(
622 environment_raw, pyproject, lockfile, group, policy
624 if set(receipt) != set(current):
625 _fail(
"managed environment receipt has an unknown or incomplete schema")
626 for key
in sorted(current):
627 if receipt.get(key) != current[key]:
628 _fail(f
"managed environment receipt mismatch for {key}")
629 if _identity(receipt_path) != receipt_identity:
630 _fail(
"managed environment receipt changed during verification")
631 if _sha256_file(pyproject) != current[
"pyproject_sha256"]:
632 _fail(
"pyproject.toml changed during managed environment verification")
633 if _sha256_file(lockfile) != current[
"uv_lock_sha256"]:
634 _fail(
"uv.lock changed during managed environment verification")
635 if _sha256_file(Path(__file__)) != current[
"authority_sha256"]:
636 _fail(
"managed environment authority changed during verification")
638 _sha256_file(Path(__file__).with_name(
"managed_python_env_checks.py"))
639 != current[
"checks_sha256"]
641 _fail(
"managed environment checks helper changed during verification")
642 return checked_environment /
"bin"
645def _authentication_cache_key(
646 environment_raw: str,
652 """Key a process-local post-verification cache to every mutable input."""
653 if not re.fullmatch(
r"[a-z][a-z0-9-]{0,31}", group):
654 _fail(f
"invalid dependency group identity: {group!r}")
655 environment = _canonical_environment(environment_raw)
656 route = _validate_route(environment, policy.route_uids)
657 environment_identity = route[environment]
658 if environment_identity.uid != policy.environment_uid:
659 _fail(f
"managed environment is not owned by uid {policy.environment_uid}: {environment}")
660 _payload, receipt, receipt_identity = _read_receipt(environment, policy)
662 "authority_sha256": _sha256_file(Path(__file__)),
663 "checks_sha256": _sha256_file(Path(__file__).with_name(
"managed_python_env_checks.py")),
664 "dependency_group": group,
665 "environment_identity": environment_identity.cache_fields(),
666 "environment_path": str(environment),
667 "pyproject_sha256": _sha256_file(pyproject),
668 "receipt_identity": receipt_identity.cache_fields(),
669 "receipt_sha256": _sha256_file(receipt),
670 "uv_lock_sha256": _sha256_file(lockfile),
672 encoded = json.dumps(material, sort_keys=
True, separators=(
",",
":")).encode(
"ascii")
673 return hashlib.sha256(encoded).hexdigest()
676def _atomic_write(environment: Path, payload: dict[str, Any], policy: TrustPolicy) ->
None:
677 """Publish a read-only receipt atomically through the environment dirfd."""
678 flags = os.O_RDONLY | os.O_DIRECTORY | getattr(os,
"O_CLOEXEC", 0)
679 flags |= getattr(os,
"O_NOFOLLOW", 0)
680 directory_fd = os.open(environment, flags)
681 temporary = f
".{RECEIPT_NAME}.tmp-{os.getpid()}"
683 json.dumps(payload, sort_keys=
True, indent=2, ensure_ascii=
True) +
"\n"
686 file_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os,
"O_CLOEXEC", 0)
687 file_flags |= getattr(os,
"O_NOFOLLOW", 0)
688 descriptor = os.open(temporary, file_flags, 0o400, dir_fd=directory_fd)
690 remaining = memoryview(receipt_bytes)
692 written = os.write(descriptor, remaining)
694 _fail(
"could not finish the managed-environment receipt write")
695 remaining = remaining[written:]
697 os.fchmod(descriptor, RECEIPT_MODE)
698 identity = ObjectIdentity.from_stat(os.fstat(descriptor))
699 if identity.uid != policy.environment_uid:
700 _fail(
"temporary managed-environment receipt has an untrusted owner")
706 src_dir_fd=directory_fd,
707 dst_dir_fd=directory_fd,
709 os.fsync(directory_fd)
711 with contextlib.suppress(FileNotFoundError):
712 os.unlink(temporary, dir_fd=directory_fd)
713 os.close(directory_fd)
717 environment_raw: str,
723 """Collect, atomically publish, then re-verify one receipt."""
724 environment, payload = _collect_payload(environment_raw, pyproject, lockfile, group, policy)
725 _atomic_write(environment, payload, policy)
726 return _verify(environment_raw, pyproject, lockfile, group, policy)
729def _make_test_environment(root: Path, name: str) -> Path:
730 """Create a private stdlib-only venv for authentication selftests."""
731 environment = root / name
732 result = _run_command(
733 [str(Path(sys.executable).resolve()),
"-I",
"-m",
"venv", str(environment)],
737 if result.returncode != 0:
738 _fail(f
"selftest could not create a virtual environment: {result.stderr}")
742def _expect_rejection(label: str, operation: Callable[[], object]) ->
None:
743 """Require one hostile selftest operation to fail closed."""
746 except ManagedEnvironmentError:
748 _fail(f
"selftest accepted {label}")
751def _writable_route_selftest(
758 """Exercise writable receipt, bin, environment, and parent rejection."""
759 checks = _load_checks()
761 receipt = environment / RECEIPT_NAME
764 "a writable receipt",
765 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
767 receipt.chmod(RECEIPT_MODE)
768 (environment /
"bin").chmod(0o775)
770 "a group-writable bin directory",
771 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
773 (environment /
"bin").chmod(0o755)
774 environment.chmod(0o777)
776 "a writable environment root",
777 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
779 environment.chmod(0o755)
781 interpreter = environment /
"bin/python3"
782 original_target = interpreter.readlink()
784 shutil.copy2(Path(sys.executable).resolve(), interpreter)
785 interpreter.chmod(0o775)
787 "a group-writable interpreter target",
788 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
791 interpreter.symlink_to(original_target)
793 checks.rewrite_receipt(environment, {
"python_version":
"3.99.0"}, RECEIPT_NAME, RECEIPT_MODE)
795 "the wrong Python version",
796 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
798 checks.rewrite_receipt(environment, {
"checks_sha256":
"0" * 64}, RECEIPT_NAME, RECEIPT_MODE)
800 "the wrong checks-helper digest",
801 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
803 _write(str(environment), pyproject, lockfile,
"ci", policy)
806 "a writable parent route",
807 lambda: _verify(str(environment), pyproject, lockfile,
"ci", policy),
812def _filesystem_selftest(root: Path, policy: TrustPolicy) ->
None:
813 """Exercise valid, stale, copied, writable, and symlinked environments."""
814 checks = _load_checks()
816 pyproject = root /
"pyproject.toml"
817 lockfile = root /
"uv.lock"
818 pyproject.write_text(
"[dependency-groups]\nci=[]\n", encoding=
"ascii")
819 lockfile.write_text(
"version = 1\n", encoding=
"ascii")
820 environment = _make_test_environment(root,
"managed")
821 expected_bin = _write(str(environment), pyproject, lockfile,
"ci", policy)
822 if expected_bin != environment /
"bin":
823 _fail(
"selftest did not return the authenticated bin directory")
824 _verify(str(environment), pyproject, lockfile,
"ci", policy)
825 harness = checks.FilesystemHarness(
826 refresh=
lambda: _write(str(environment), pyproject, lockfile,
"ci", policy),
827 reject_environment=
lambda label, candidate, group: _expect_rejection(
829 lambda: _verify(str(candidate), pyproject, lockfile, group, policy),
831 make_environment=
lambda name: _make_test_environment(root, name),
832 cache_key=
lambda group: _authentication_cache_key(
833 str(environment), pyproject, lockfile, group, policy
836 receipt_name=RECEIPT_NAME,
837 receipt_mode=RECEIPT_MODE,
839 checks.cache_key_selftest(environment, pyproject, lockfile, harness)
840 checks.stale_receipt_selftest(root, environment, pyproject, lockfile, harness)
841 _writable_route_selftest(root, environment, pyproject, lockfile, policy)
842 checks.nested_tree_selftest(environment, harness)
845def _selftest() -> int:
846 """Run non-vacuous positive and hostile receipt/consumer tests."""
847 checks = _load_checks()
849 home = Path.home().resolve()
850 if _identity(home).mode & 0o022:
851 _fail(f
"selftest home route is group/other writable: {home}")
852 root = Path(tempfile.mkdtemp(prefix=
".ra8-managed-env-", dir=home))
855 _filesystem_selftest(root, TrustPolicy.selftest())
859 failures = checks.consumer_contract_selftest()
861 _fail(
"; ".join(failures))
862 print(
"managed_python_env.py --selftest: PASS")
866def _parser() -> argparse.ArgumentParser:
867 """Build the command-line parser without exposing trust-policy overrides."""
868 parser = argparse.ArgumentParser(description=__doc__)
869 subparsers = parser.add_subparsers(dest=
"command", required=
True)
870 for command
in (
"write",
"verify",
"cache-key"):
871 child = subparsers.add_parser(command)
872 child.add_argument(
"--env", required=
True)
873 child.add_argument(
"--pyproject", required=
True, type=Path)
874 child.add_argument(
"--lock", required=
True, type=Path)
875 child.add_argument(
"--group", required=
True)
876 if command ==
"verify":
877 child.add_argument(
"--print-bin", action=
"store_true")
878 check = subparsers.add_parser(
"check-consumers")
879 check.add_argument(
"--root", type=Path, default=Path.cwd())
883def main(argv: list[str] |
None =
None) -> int:
884 """Dispatch receipt creation, authentication, and contract checks."""
887 if argv == [
"--selftest"]:
889 arguments = _parser().parse_args(argv)
890 if arguments.command ==
"check-consumers":
891 checks = _load_checks()
892 root = arguments.root.resolve()
893 findings = checks.consumer_findings(root) + checks.consumer_runtime_findings(
896 for finding
in findings:
897 print(f
"managed-python-env: {finding}", file=sys.stderr)
900 print(
"managed Python environment consumers share one authenticated authority")
902 policy = TrustPolicy.production()
903 if arguments.command ==
"write":
904 if os.geteuid() != 0:
905 _fail(
"only root may create a production managed-environment receipt")
907 arguments.env, arguments.pyproject, arguments.lock, arguments.group, policy
909 print(f
"wrote authenticated managed Python environment receipt for {bin_dir.parent}")
911 if arguments.command ==
"cache-key":
913 _authentication_cache_key(
922 bin_dir = _verify(arguments.env, arguments.pyproject, arguments.lock, arguments.group, policy)
925 if arguments.print_bin
926 else f
"authenticated managed Python environment: {bin_dir.parent}"
932if __name__ ==
"__main__":
934 raise SystemExit(
main())
935 except ManagedEnvironmentError
as error:
936 print(f
"managed_python_env.py: FATAL: {error}", file=sys.stderr)
937 raise SystemExit(1)
from None
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.