Skip to content

Commit

Permalink
main/ckms: add basic module signing support
Browse files Browse the repository at this point in the history
  • Loading branch information
q66 committed Jan 21, 2025
1 parent 8c715db commit 633a61e
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 10 deletions.
6 changes: 5 additions & 1 deletion Packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -2888,7 +2888,7 @@ This is useful if you have e.g. some personal authentication token needed
to fetch particular sources, and you do not want to paste the token directly
to the template.

##### def do(self, cmd, *args, env = None, wrksrc = None, capture_output = False, stdout = None, stderr = None, input = None, check = True, allow_network = False, path = None)
##### def do(self, cmd, *args, env = None, wrksrc = None, capture_output = False, stdout = None, stderr = None, input = None, check = True, allow_network = False, path = None, tmpfiles = None)

Execute a command in the build container, sandboxed. Does not spawn a shell,
instead directly runs `cmd`, passing it `*args`. You can use `env` to provide
Expand Down Expand Up @@ -2927,6 +2927,10 @@ that if needed.
The `stdout` and `stderr` arguments work the same as for Python `subprocess.run`,
likewise with `input`.

The `tmpfiles` argument can be a list of `pathlib.Path` specifying host-filesystem
file paths to be bound into the sandbox in `/tmp`. The target filenames will be
the same as the source filenames.

The return value is the same as from Python `subprocess.run`. There you can
access the return code as well as possibly captured `stdout`.

Expand Down
228 changes: 228 additions & 0 deletions main/ckms/patches/sign.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
commit a39fae03f004aa4b2bba06f1416cd99ef27f04ba
Author: q66 <[email protected]>
Date: Tue Jan 21 02:30:21 2025 +0100

implement basic module signing support

diff --git a/ckms b/ckms
index 8f51208..934d41a 100755
--- a/ckms
+++ b/ckms
@@ -31,6 +31,8 @@ opt_depmod = True
opt_machine = None
opt_jobs = None
opt_comp = None
+opt_signkey = None
+opt_signcert = None

use_colors = True

@@ -135,6 +137,7 @@ class Package:
self.cfgdata = cfgdata
self.pkgpath = pkgpath
self.pkgconf = pkgconf
+ self.sig_hash = None
# whether to strip the modules, true by default
try:
self.strip = cfgdata["general"].getboolean(
@@ -240,11 +243,23 @@ class Package:
self.add_cfg_env()
return
with open(cfg) as cf:
+ comp_over = False
for l in cf:
- # we only care about the first category
+ # we only care about the first category for the compiler opts
if l == "\n":
- break
+ comp_over = True
+ continue
l = l.strip()
+ # break at the earliest point we can
+ if l == "# CONFIG_MODULE_SIG is not set":
+ break
+ # found module signing support
+ if l.startswith("CONFIG_MODULE_SIG_HASH="):
+ self.sig_hash = l.removeprefix("CONFIG_MODULE_SIG_HASH=")[1:-1]
+ break
+ # these are all in the first block
+ if comp_over:
+ continue
if l == "CONFIG_CC_IS_CLANG=y":
margs.append("LLVM=1")
self.env["LLVM"] = "1"
@@ -452,6 +467,42 @@ def get_compsfx():

return compsfx

+def do_sign(pkg, path):
+ dosig = True
+ hdrpath = kern_path / opt_kernver / "build"
+ certpath = hdrpath / "certs"
+ sign_tool = shutil.which(str(hdrpath / "scripts/sign-file"))
+
+ signkey = certpath / "signing_key.pem"
+ signcert = certpath / "signing_key.x509"
+
+ if not signkey.is_file() or not signcert.is_file():
+ signkey = opt_signkey
+ signcert = opt_signcert
+
+ if not pkg.sig_hash:
+ # no signing in kernel
+ log(f"signing not enabled in {opt_kernver}, not signing...")
+ dosig = False
+ elif not signkey or not signcert:
+ # no signkeys...
+ log("signing keys not available, not signing...")
+ dosig = False
+ elif not sign_tool:
+ # no sign tool
+ log("signing tool not available, not signing...")
+ dosig = False
+
+ if not dosig:
+ path.chmod(0o644)
+ return
+
+ log(f"signing module '{path.name}'...")
+ if pkg.do(sign_tool, pkg.sig_hash, signkey, signcert, path).returncode != 0:
+ raise CkmsError(f"signing failed for '{path}'")
+
+ path.chmod(0o644)
+
def do_build(cmd, quiet = False):
check_has_action(cmd)

@@ -513,7 +564,8 @@ def do_build(cmd, quiet = False):
# then copy
destf.mkdir(parents = True, exist_ok = True)
shutil.copy(modf, destf)
- (destf / f"{modn}.ko").chmod(0o644)
+ # sign if needed
+ do_sign(pkg, destf / f"{modn}.ko")

# clean build dir
shutil.rmtree(bdir)
@@ -731,7 +783,7 @@ def main():
global opt_confpath, opt_kernver, opt_pkgconf, opt_quiet
global opt_depmod, opt_machine, opt_jobs, opt_comp, opt_statedir, opt_destdir
global use_colors, opt_stripcmd, opt_makeargs, opt_makeenv, opt_initramfs
- global opt_depmodh
+ global opt_depmodh, opt_signkey, opt_signcert

parser = argparse.ArgumentParser(
description = "Chimera Kernel Module System"
@@ -753,6 +805,9 @@ def main():
"-x", "--compression", default = None,
help = "Compression method for modules (gz, xz or zst)"
)
+ parser.add_argument(
+ "--sign", default = None, help = "Signing key to use"
+ )
parser.add_argument(
"-q", "--quiet", action = "store_const", const = True,
default = opt_quiet, help = "Do not log build output to stdout."
@@ -797,6 +852,8 @@ def main():
opt_makeargs = ckcfg.get("make_build_args", fallback = opt_makeargs)
opt_initramfs = ckcfg.get("initramfs_hook", fallback = opt_initramfs)
opt_depmodh = ckcfg.get("depmod_hook", fallback = opt_depmodh)
+ opt_signkey = ckcfg.get("sign_key", fallback = opt_signkey)
+ opt_signcert = ckcfg.get("sign_cert", fallback = opt_signcert)
if "build_env" in gcfg:
opt_makeenv = gcfg["build_env"]

@@ -828,6 +885,16 @@ def main():
if cmdline.compression:
opt_comp = cmdline.compression

+ if cmdline.sign:
+ sk = cmdline.sign.split(",")
+ if len(sk) == 1:
+ opt_signkey, opt_signcert = f"{sk[0]}.pem", f"{sk[0]}.x509"
+ elif len(sk) == 2:
+ opt_signkey, opt_signcert = sk
+ else:
+ log_red(f"ERROR: invalid parameter to --sign")
+ return 1
+
# some reasonable defaults

if not opt_jobs:
@@ -857,6 +924,16 @@ def main():
log_red(f"ERROR: invalid compression method {opt_comp}")
return 1

+ if not opt_signkey or not opt_signcert:
+ # ignore if unset
+ opt_signkey = opt_signcert = None
+ elif not os.path.isfile(opt_signkey) or not os.path.isfile(opt_signcert):
+ # ignore if nonexistent
+ opt_signkey = opt_signcert = None
+ else:
+ opt_signkey = pathlib.Path(opt_signkey)
+ opt_signcert = pathlib.Path(opt_signcert)
+
# match the action

try:
diff --git a/ckms-config.ini.5.scd b/ckms-config.ini.5.scd
index 6277a02..5eab14e 100644
--- a/ckms-config.ini.5.scd
+++ b/ckms-config.ini.5.scd
@@ -61,6 +61,24 @@ This is everything in the section _[ckms]_.
environment of a package. They are always added to the very end, after
any arguments implicitly set up by CKMS.

+*sign\_key*
+ The private key used for module signing. If unset, _certs/signing\_key.pem_
+ will be checked in the kernel headers directory. If neither exist, the
+ module will not be signed.
+
+ For the signing to happen, the kernel headers directory additionally needs
+ to contain the _sign-file_ binary in its _scripts_ directory. The kernel
+ dotconfig also needs to have signing enabled.
+
+ In general, the kernel key will exist when using self-built kernels, while
+ the config key will be something like the user's custom key enrolled in the
+ MOK. If the kernel key exists, it will be preferred first, as that is the
+ key used to sign the rest of the kernel's modules, which is always better.
+
+*sign\_cert*
+ The x509 certificate counterpart of _sign\_key_. Both have to exist for the
+ signing to happen. The kernel path is _certs/signing\_key.x509_.
+
# BUILD ENVIRONMENT

It is additionally possible to globally influence the build environment of
diff --git a/ckms.8.scd b/ckms.8.scd
index 878d0f9..00471b1 100644
--- a/ckms.8.scd
+++ b/ckms.8.scd
@@ -88,6 +88,14 @@ the commands.
The compression method to use for modules. By default, no compression
is used. The valid methods are _gz_, _xz_ and _zst_.

+*--sign* _KEY,CERT_
+ Use the given private key and cert. Equivalent to the _sign\_key_ and
+ _sign\_cert_ options in the configuration file. Used unless the kernel
+ headers provide a key. You also need to provide the x509 certificate and
+ both have to exist, or this gets ignored. Can be specified either as two
+ comma-separated paths, or a single path, in which case the _.pem_ and
+ _.x509_ suffixes get appended.
+
# COMMANDS

These commands are permitted, along with their options.
diff --git a/config.ini b/config.ini
index af92b90..00031e7 100644
--- a/config.ini
+++ b/config.ini
@@ -6,5 +6,7 @@ quiet = no
strip = strip -g
initramfs_hook = /etc/ckms/refresh-initramfs.sh
#make_build_args = ...
+#sign_key = /path/to/signing_key.pem
+#sign_cert = /path/to/signing_key.x509
[build_env]
#FOO = bar
2 changes: 1 addition & 1 deletion main/ckms/template.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pkgname = "ckms"
pkgver = "0.1.1"
pkgrel = 2
pkgrel = 3
build_style = "makefile"
hostmakedepends = ["scdoc"]
depends = ["python"]
Expand Down
2 changes: 1 addition & 1 deletion src/cbuild/core/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ def _build_locked(
mount_binpkgs=True,
fakeroot=True,
binpkgs_rw=True,
signkey=asign.get_keypath(),
tmpfiles=[asign.get_keypath()],
)
# handle whatever error
if ret.returncode != 0:
Expand Down
10 changes: 5 additions & 5 deletions src/cbuild/core/chroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ def enter(
fakeroot=False,
new_session=True,
binpkgs_rw=False,
signkey=None,
tmpfiles=None,
binpath=None,
lldargs=None,
term=False,
Expand Down Expand Up @@ -806,11 +806,11 @@ def enter(
# extra file descriptors to pass to sandbox and bind to a file
fdlist = []

if signkey:
for tmpf in tmpfiles or []:
# reopen as file descriptor to pass
signfd = os.open(signkey, os.O_RDONLY)
fdlist.append(signfd)
bcmd += ["--ro-bind-data", str(signfd), f"/tmp/{signkey.name}"]
tmpfd = os.open(tmpf, os.O_RDONLY)
fdlist.append(tmpfd)
bcmd += ["--ro-bind-data", str(tmpfd), f"/tmp/{tmpf.name}"]

if lldargs:
rfd, wfd = os.pipe()
Expand Down
2 changes: 2 additions & 0 deletions src/cbuild/core/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,7 @@ def do(
check=True,
allow_network=False,
path=None,
tmpfiles=None,
):
cpf = self.profile()

Expand Down Expand Up @@ -1856,6 +1857,7 @@ def do(
lldargs=lld_args,
binpath=path,
term=True,
tmpfiles=tmpfiles,
)

def stamp(self, name):
Expand Down
21 changes: 19 additions & 2 deletions src/cbuild/util/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def get_modsrc(pkg, modname, modver):
return paths.bldroot() / f"usr/src/{modname}-{modver}"


def _call_ckms(pkg, kver, *args):
def _call_ckms(pkg, kver, *args, tmpfiles=None):
pkg.do(
"ckms",
"-s",
Expand All @@ -156,6 +156,7 @@ def _call_ckms(pkg, kver, *args):
kver,
*args,
env={"CBUILD_BYPASS_STRIP_WRAPPER": "1"},
tmpfiles=tmpfiles,
)


Expand All @@ -164,7 +165,23 @@ def ckms_configure(pkg, modname, modver, kver):


def ckms_build(pkg, modname, modver, kver):
_call_ckms(pkg, kver, "build", f"{modname}={modver}")
from cbuild.core import paths

# check if we have the stuff available
kpath = paths.distdir() / "etc/keys/kernel"
pkey = kpath / f"{kver}-signing_key.pem"
cert = kpath / f"{kver}-signing_key.x509"
tfiles = None
cargs = []

# pass the signing key ephemerally via file descriptors
if pkey.is_file() and cert.is_file():
tfiles = [pkey, cert]
cargs += [f"--sign=/tmp/{kver}-signing_key"]

_call_ckms(
pkg, kver, *cargs, "build", f"{modname}={modver}", tmpfiles=tfiles
)


def ckms_install(pkg, modname, modver, kver):
Expand Down

0 comments on commit 633a61e

Please sign in to comment.