Skip to content

Commit

Permalink
Allow specifying OpenPGP implementation to use for signing
Browse files Browse the repository at this point in the history
Fixes: #3042
  • Loading branch information
wiktor-k committed Oct 8, 2024
1 parent 4005997 commit 658bcbf
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 5 deletions.
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 @@ -2233,6 +2233,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 @@ -2270,6 +2278,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 @@ -1676,7 +1676,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 @@ -2920,6 +2921,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 @@ -4562,6 +4569,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 @@ -61,6 +61,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 @@ -76,10 +77,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 @@ -102,7 +108,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 @@ -111,6 +117,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 @@ -198,6 +198,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 @@ -457,6 +458,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
90 changes: 90 additions & 0 deletions tests/test_signing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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 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 PATH to perform sop tests.")
with tempfile.TemporaryDirectory() as path, Image(config) as image:
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(0o744)

# 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(0o744)

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)
tmp_path.chmod(0o755)

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(0o744)

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 658bcbf

Please sign in to comment.