- name: Install
run: |
sudo apt-get update
- sudo apt-get install python3-pytest lvm2 cryptsetup-bin btrfs-progs
+ sudo apt-get install python3-pytest lvm2 cryptsetup-bin btrfs-progs sqop
# Make sure the latest changes from the pull request are used.
sudo ln -svf $PWD/bin/mkosi /usr/bin/mkosi
working-directory: ./
if not context.config.sign or not context.config.checksum:
return
+ if context.config.openpgp_tool == "gpg":
+ calculate_signature_gpg(context)
+ else:
+ calculate_signature_sop(context)
+
+
+def calculate_signature_gpg(context: Context) -> None:
cmdline: list[PathString] = ["gpg", "--detach-sign", "--pinentry-mode", "loopback"]
# Need to specify key before file to sign
)
+def calculate_signature_sop(context: Context) -> None:
+ if context.config.key is None:
+ die("Signing key is mandatory when using SOP signing")
+
+ with (
+ complete_step("Signing SHA256SUMS…"),
+ open(context.staging / context.config.output_checksum, "rb") as i,
+ open(context.staging / context.config.output_signature, "wb") as o,
+ ):
+ run(
+ [context.config.openpgp_tool, "sign", "/signing-key.pgp"],
+ env=context.config.environment,
+ stdin=i,
+ stdout=o,
+ sandbox=context.sandbox(
+ binary=context.config.openpgp_tool,
+ options=[
+ "--bind", context.config.key, "/signing-key.pgp",
+ "--bind", context.staging, workdir(context.staging),
+ "--bind", "/run", "/run",
+ ],
+ ),
+ ) # fmt: skip
+
+
def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
dir_sum = 0
for entry in os.scandir(path):
passphrase: Optional[Path]
checksum: bool
sign: bool
+ openpgp_tool: str
key: Optional[str]
tools_tree: Optional[Path]
section="Validation",
help="GPG key to use for signing",
),
+ ConfigSetting(
+ name="OpenPGPTool",
+ dest="openpgp_tool",
+ section="Validation",
+ default="gpg",
+ help="OpenPGP implementation to use for signing",
+ ),
# Build section
ConfigSetting(
dest="tools_tree",
Passphrase: {none_to_none(config.passphrase)}
Checksum: {yes_no(config.checksum)}
Sign: {yes_no(config.sign)}
+ OpenPGP Tool: {config.openpgp_tool}
GPG Key: ({"default" if config.key is None else config.key})
"""
`Sign=`, `--sign`
: Sign the generated `SHA256SUMS` using `gpg` after completion.
+`OpenPGPTool=`, `--openpgp-tool`
+: OpenPGP implementation to use for signing. `gpg` is the default.
+ Selecting a value different than the default will use the given Stateless
+ OpenPGP (SOP) tool for signing the `SHA256SUMS` file.
+
+ Exemplary choices are `sqop` and `rsop`, but any implementation from
+ https://www.openpgp.org/about/sop/ that can be installed locally will work.
+
`Key=`, `--key=`
: Select the `gpg` key to use for signing `SHA256SUMS`. This key must
be already present in the `gpg` keyring.
import subprocess
import sys
import uuid
-from collections.abc import Iterator, Sequence
+from collections.abc import Iterator, Mapping, Sequence
from pathlib import Path
from types import TracebackType
from typing import Any, Optional
user: Optional[int] = None,
group: Optional[int] = None,
check: bool = True,
+ env: Mapping[str, str] = os.environ,
) -> CompletedProcess:
return run(
[
stdout=sys.stdout,
user=user,
group=group,
- env=os.environ,
+ env=env,
) # fmt: skip
- def build(self, options: Sequence[PathString] = (), args: Sequence[str] = ()) -> CompletedProcess:
+ def build(
+ self,
+ options: Sequence[PathString] = (),
+ args: Sequence[str] = (),
+ env: Mapping[str, str] = os.environ,
+ ) -> CompletedProcess:
kcl = [
"loglevel=6",
"systemd.log_level=debug",
*options,
] # fmt: skip
- self.mkosi("summary", opt, user=self.uid, group=self.uid)
+ self.mkosi("summary", opt, user=self.uid, group=self.uid, env=env)
return self.mkosi(
"build",
stdin=sys.stdin if sys.stdin.isatty() else None,
user=self.uid,
group=self.gid,
+ env=env,
)
def boot(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> CompletedProcess:
"MinimumVersion": "123",
"Mirror": null,
"NSpawnSettings": null,
+ "OpenPGPTool": "gpg",
"Output": "outfile",
"OutputDirectory": "/your/output/here",
"OutputMode": 83,
minimum_version=GenericVersion("123"),
mirror=None,
nspawn_settings=None,
+ openpgp_tool="gpg",
output="outfile",
output_dir=Path("/your/output/here"),
output_format=OutputFormat.uki,
--- /dev/null
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+
+import os
+import tempfile
+from pathlib import Path
+
+import pytest
+
+from mkosi.run import find_binary, run
+
+from . import Image, ImageConfig
+
+pytestmark = pytest.mark.integration
+
+
+def test_signing_checksums_with_sop(config: ImageConfig) -> None:
+ if find_binary("sqop", root=config.tools) is None:
+ pytest.skip("Needs 'sqop' binary in tools tree PATH to perform sop tests.")
+
+ if find_binary("sqop") is None:
+ pytest.skip("Needs 'sqop' binary in host system PATH to perform sop tests.")
+
+ with tempfile.TemporaryDirectory() as path, Image(config) as image:
+ tmp_path = Path(path)
+ os.chown(tmp_path, image.uid, image.gid)
+
+ signing_key = tmp_path / "signing-key.pgp"
+ signing_cert = tmp_path / "signing-cert.pgp"
+
+ # create a brand new signing key
+ with open(signing_key, "wb") as o:
+ run(cmdline=["sqop", "generate-key", "--signing-only", "Test"], stdout=o)
+
+ # extract public key (certificate)
+ with open(signing_key, "rb") as i, open(signing_cert, "wb") as o:
+ run(cmdline=["sqop", "extract-cert"], stdin=i, stdout=o)
+
+ image.build(
+ options=["--checksum=true", "--openpgp-tool=sqop", "--sign=true", f"--key={signing_key}"]
+ )
+
+ signed_file = image.output_dir / "image.SHA256SUMS"
+ signature = image.output_dir / "image.SHA256SUMS.gpg"
+
+ with open(signed_file, "rb") as i:
+ run(cmdline=["sqop", "verify", signature, signing_cert], stdin=i)
+
+
+def test_signing_checksums_with_gpg(config: ImageConfig) -> None:
+ with tempfile.TemporaryDirectory() as path, Image(config) as image:
+ tmp_path = Path(path)
+ os.chown(tmp_path, image.uid, image.gid)
+
+ signing_key = "mkosi-test@example.org"
+ signing_cert = tmp_path / "signing-cert.pgp"
+ gnupghome = tmp_path / ".gnupg"
+
+ env = dict(GNUPGHOME=str(gnupghome))
+
+ # Creating GNUPGHOME directory and appending an *empty* common.conf
+ # file stops GnuPG from spawning keyboxd which causes issues when switching
+ # users. See https://stackoverflow.com/a/72278246 for details
+ gnupghome.mkdir()
+ os.chown(gnupghome, image.uid, image.gid)
+ (gnupghome / "common.conf").touch()
+
+ # create a brand new signing key
+ run(
+ cmdline=["gpg", "--quick-gen-key", "--batch", "--passphrase", "", signing_key],
+ env=env,
+ user=image.uid,
+ group=image.gid,
+ )
+
+ # export public key (certificate)
+ with open(signing_cert, "wb") as o:
+ run(
+ cmdline=["gpg", "--export", signing_key],
+ env=env,
+ stdout=o,
+ user=image.uid,
+ group=image.gid,
+ )
+
+ image.build(options=["--checksum=true", "--sign=true", f"--key={signing_key}"], env=env)
+
+ signed_file = image.output_dir / "image.SHA256SUMS"
+ signature = image.output_dir / "image.SHA256SUMS.gpg"
+
+ run(cmdline=["gpg", "--verify", signature, signed_file], env=env)