Skip to content

Commit

Permalink
Allow specifying OpenPGP implementation to use for signing
Browse files Browse the repository at this point in the history
  • Loading branch information
wiktor-k committed Oct 4, 2024
1 parent 6912dc0 commit efc0a77
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:
- 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: ./
Expand Down
1 change: 1 addition & 0 deletions mkosi.conf.d/20-arch.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Packages=
python
qemu-user-static
shim
sequoia-sop
1 change: 1 addition & 0 deletions mkosi.conf.d/20-debian/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Repositories=non-free-firmware
[Content]
Packages=
linux-perf
sqop
1 change: 1 addition & 0 deletions mkosi.conf.d/20-fedora/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Packages=
perf
qemu-user-static
rpmautospec
sequoia-sop
1 change: 1 addition & 0 deletions mkosi.conf.d/20-ubuntu/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Repositories=universe
[Content]
Packages=
linux-tools-generic
sqop
1 change: 1 addition & 0 deletions mkosi.conf.d/30-debian-kali-ubuntu/mkosi.conf
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Packages=
python3
qemu-user-static
shim-signed
sqop
39 changes: 39 additions & 0 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2226,6 +2226,14 @@ def calculate_signature(context: Context) -> None:
if not context.config.sign or not context.config.checksum:
return

pgptool = context.config.openpgp_tool
if pgptool == "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
Expand Down Expand Up @@ -2263,6 +2271,37 @@ def calculate_signature(context: Context) -> None:
)


def calculate_signature_sop(context: Context) -> None:
pgptool = context.config.openpgp_tool
signing_key = context.config.key
if signing_key is None:
die("Signing key is mandatory when using SOP signing")

cmdline: list[PathString] = [pgptool, "sign", "/signing-key.pgp"]

options: list[PathString] = [
"--bind", signing_key, "/signing-key.pgp",
"--bind", context.staging, workdir(context.staging),
"--bind", "/run", "/run",
] # fmt: skip

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(
cmdline,
env=context.config.environment,
stdin=i,
stdout=o,
sandbox=context.sandbox(
binary=pgptool,
options=options,
),
)


def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
dir_sum = 0
for entry in os.scandir(path):
Expand Down
10 changes: 9 additions & 1 deletion mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,8 @@ class Config:
passphrase: Optional[Path]
checksum: bool
sign: bool
key: Optional[str]
openpgp_tool: str
key: Optional[PathString]

tools_tree: Optional[Path]
tools_tree_distribution: Optional[Distribution]
Expand Down Expand Up @@ -2809,6 +2810,12 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
section="Validation",
help="GPG key to use for signing",
),
ConfigSetting(
dest="openpgp_tool",
section="Validation",
default="gpg",
help="OpenPGP implementation to use for signing",
),
# Build section
ConfigSetting(
dest="tools_tree",
Expand Down Expand Up @@ -4450,6 +4457,7 @@ def summary(config: Config) -> str:
Passphrase: {none_to_none(config.passphrase)}
Checksum: {yes_no(config.checksum)}
Sign: {yes_no(config.sign)}
OpenPGP Tool: ({config.openpgp_tool or "gpg"})
GPG Key: ({"default" if config.key is None else config.key})
"""

Expand Down
1 change: 1 addition & 0 deletions mkosi/resources/mkosi-tools/mkosi.conf.d/10-arch.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Packages=
qemu-base
reprepro
sbsigntools
sequoia-sop
shadow
squashfs-tools
systemd-ukify
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Packages=
qemu-system
reprepro
sbsigntool
sqop
squashfs-tools
swtpm-tools
systemd-container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ Packages=
qemu-system-ppc-core
qemu-system-s390x-core
reprepro
sequoia-sop
ubu-keyring
zypper
15 changes: 11 additions & 4 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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
Expand Down Expand Up @@ -60,6 +60,7 @@ def mkosi(
user: Optional[int] = None,
group: Optional[int] = None,
check: bool = True,
env: Optional[Mapping[str, str]] = None,
) -> CompletedProcess:
return run(
[
Expand All @@ -74,10 +75,15 @@ def mkosi(
stdout=sys.stdout,
user=user,
group=group,
env=os.environ,
env=env or os.environ,
) # fmt: skip

def build(self, options: Sequence[PathString] = (), args: Sequence[str] = ()) -> CompletedProcess:
def build(
self,
options: Sequence[PathString] = (),
args: Sequence[str] = (),
env: Optional[Mapping[str, str]] = None,
) -> CompletedProcess:
kcl = [
"loglevel=6",
"systemd.log_level=debug",
Expand All @@ -100,7 +106,7 @@ def build(self, options: Sequence[PathString] = (), args: Sequence[str] = ()) ->
*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",
Expand All @@ -109,6 +115,7 @@ def build(self, options: Sequence[PathString] = (), args: Sequence[str] = ()) ->
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:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def test_config() -> None:
"MinimumVersion": "123",
"Mirror": null,
"NSpawnSettings": null,
"OpenpgpTool": "gpg",
"Output": "outfile",
"OutputDirectory": "/your/output/here",
"OutputMode": 83,
Expand Down Expand Up @@ -437,6 +438,7 @@ def test_config() -> None:
minimum_version=GenericVersion("123"),
mirror=None,
nspawn_settings=None,
openpgp_tool="gpg",
output="outfile",
output_dir=Path("/your/output/here"),
output_format=OutputFormat.uki,
Expand Down
97 changes: 97 additions & 0 deletions tests/test_signing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# SPDX-License-Identifier: LGPL-2.1-or-later


import tempfile
from collections.abc import Mapping
from pathlib import Path

import pytest

from mkosi.run import run

from . import Image, ImageConfig

pytestmark = pytest.mark.integration


@pytest.mark.skipif(
not Path("/usr/bin/sqop").exists(),
reason="requires sqop for signing but not all distros package it (e.g. not in opensuse)",
)
def test_signing_checksums_with_sop(config: ImageConfig) -> None:
with tempfile.TemporaryDirectory() as path:
tmp_path = Path(path)
tmp_path.chmod(0o755)

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)

signing_key.chmod(0o755)

# 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)

signing_cert.chmod(0o755)

with Image(config) as image:
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:
tmp_path = Path(path)
tmp_path.chmod(0o777)

signing_key = "mkosi-test@example.org"
signing_cert = tmp_path / "signing-cert.pgp"
gnupghome = tmp_path / ".gnupg"

env: Mapping[str, str] = 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()
(gnupghome / "common.conf").touch()

# create a brand new signing key
run(cmdline=["gpg", "--quick-gen-key", "--batch", "--passphrase", "", signing_key], env=env)

# GnuPG will set 0o700 permissions so that the secret files are not available
# to other users. Since this is for tests only and we need that keyring for signing
# enable all permissions. We need write permissions since GnuPG creates temporary
# files in this directory during operation.
gnupghome.chmod(0o777)
for p in gnupghome.rglob("*"):
p.chmod(0o777)

# export public key (certificate)
with open(signing_cert, "wb") as o:
run(cmdline=["gpg", "--export", signing_key], env=env, stdout=o)

signing_cert.chmod(0o755)

with open("/tmp/list", "wb") as o:
run(["ls", "-la", gnupghome], stdout=o)

with Image(config) as image:
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"

with open(signed_file, "rb") as i:
run(cmdline=["sqop", "verify", signature, signing_cert], stdin=i)

0 comments on commit efc0a77

Please sign in to comment.