128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""Root-only preparation of the private application material shipped with K1.
|
|
|
|
An autonomous private installer supplies one fixed, root-readable file. The
|
|
worker receives only a systemd credential encrypted on this board. Reinstalling
|
|
the same material is harmless; changing it requires explicit rotation.
|
|
"""
|
|
|
|
import hmac
|
|
import os
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
PROFILE_ID = "lixelgo.application.k1-fw-3.0.2.v1"
|
|
STORE = Path("/etc/credstore.encrypted")
|
|
BUNDLE = Path("/usr/share/mission-core-node/k1/private-application-key")
|
|
|
|
|
|
class CredentialInstallError(ValueError):
|
|
"""Secret-free failure; input and subprocess diagnostics never escape."""
|
|
|
|
|
|
def validate(secret):
|
|
if len(secret) != 36 or any(v < 33 or v > 126 for v in secret):
|
|
raise CredentialInstallError("Invalid material for the reviewed K1 profile")
|
|
|
|
|
|
def read_bundle(path=BUNDLE):
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
with os.fdopen(fd, "rb") as stream:
|
|
info = os.fstat(stream.fileno())
|
|
if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o077:
|
|
raise CredentialInstallError(
|
|
"Private material must be a root-owned private regular file"
|
|
)
|
|
secret = bytearray(stream.read(1025))
|
|
try:
|
|
validate(secret)
|
|
return secret
|
|
except CredentialInstallError:
|
|
secret[:] = b"\0" * len(secret)
|
|
raise
|
|
|
|
|
|
def install(secret, *, root=STORE, runner=subprocess.run):
|
|
if os.geteuid() != 0:
|
|
raise CredentialInstallError("Administrator authentication required")
|
|
validate(secret)
|
|
root.mkdir(mode=0o700, exist_ok=True)
|
|
if root.is_symlink() or root.stat().st_uid != 0 or root.stat().st_mode & 0o022:
|
|
raise CredentialInstallError("Unsafe credential store")
|
|
path = root / "k1-application"
|
|
if path.is_symlink():
|
|
raise CredentialInstallError("Unsafe credential target")
|
|
if path.exists():
|
|
info = path.stat()
|
|
if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o077:
|
|
raise CredentialInstallError("Unsafe installed credential")
|
|
result = runner(
|
|
["/usr/bin/systemd-creds", "decrypt", "--name=k1-application", str(path), "-"],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
current = bytearray(result.stdout)
|
|
try:
|
|
if result.returncode or not hmac.compare_digest(current, secret):
|
|
raise CredentialInstallError(
|
|
"Existing material differs; explicit rotation required"
|
|
)
|
|
return "unchanged"
|
|
finally:
|
|
current[:] = b"\0" * len(current)
|
|
with tempfile.TemporaryDirectory(prefix=".k1-", dir=root) as directory:
|
|
staged = Path(directory) / "encrypted"
|
|
result = runner(
|
|
[
|
|
"/usr/bin/systemd-creds",
|
|
"encrypt",
|
|
"--name=k1-application",
|
|
"--with-key=host",
|
|
"-",
|
|
str(staged),
|
|
],
|
|
input=secret,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
if result.returncode:
|
|
raise CredentialInstallError("Application material preparation failed")
|
|
staged.chmod(0o600)
|
|
with staged.open("rb") as stream:
|
|
os.fsync(stream.fileno())
|
|
os.link(staged, path)
|
|
directory_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(directory_fd)
|
|
finally:
|
|
os.close(directory_fd)
|
|
return "installed"
|
|
|
|
|
|
def main(*, bundled=False):
|
|
import sys
|
|
|
|
secret = bytearray()
|
|
try:
|
|
if os.geteuid() != 0 or len(sys.argv) != 1:
|
|
raise CredentialInstallError("Administrator-only preparation required")
|
|
if bundled:
|
|
if not BUNDLE.exists() and not BUNDLE.is_symlink():
|
|
return
|
|
secret = read_bundle()
|
|
else:
|
|
secret = bytearray(sys.stdin.buffer.read(1025).strip())
|
|
install(secret)
|
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
raise SystemExit(
|
|
"K1 application profile was not installed; "
|
|
"check the private release or existing profile"
|
|
) from None
|
|
finally:
|
|
secret[:] = b"\0" * len(secret)
|