Package onboard K1 separately with a private portable installer
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user