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 2, 2024
1 parent e6a3e23 commit 88fd737
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 33 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
84 changes: 55 additions & 29 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2157,42 +2157,68 @@ def calculate_signature(context: Context) -> None:
if not context.config.sign or not context.config.checksum:
return

cmdline: list[PathString] = ["gpg", "--detach-sign", "--pinentry-mode", "loopback"]
tool = context.config.openpgp_tool
if tool is None or tool == "gpg":
cmdline: list[PathString] = ["gpg", "--detach-sign", "--pinentry-mode", "loopback"]

# Need to specify key before file to sign
if context.config.key is not None:
cmdline += ["--default-key", context.config.key]
# Need to specify key before file to sign
if context.config.key is not None:
cmdline += ["--default-key", context.config.key]

cmdline += [
"--output",
workdir(context.staging / context.config.output_signature),
workdir(context.staging / context.config.output_checksum),
]
cmdline += [
"--output",
workdir(context.staging / context.config.output_signature),
workdir(context.staging / context.config.output_checksum),
]

home = Path(context.config.environment.get("GNUPGHOME", INVOKING_USER.home() / ".gnupg"))
if not home.exists():
die(f"GPG home {home} not found")
home = Path(context.config.environment.get("GNUPGHOME", INVOKING_USER.home() / ".gnupg"))
if not home.exists():
die(f"GPG home {home} not found")

env = dict(GNUPGHOME=os.fspath(home))
if sys.stderr.isatty():
env |= dict(GPG_TTY=os.ttyname(sys.stderr.fileno()))
env = dict(GNUPGHOME=os.fspath(home))
if sys.stderr.isatty():
env |= dict(GPG_TTY=os.ttyname(sys.stderr.fileno()))

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

with complete_step("Signing SHA256SUMS…"):
run(
cmdline,
env=env,
sandbox=context.sandbox(
binary="gpg",
options=options,
),
)
with (complete_step("Signing SHA256SUMS…")):
run(
cmdline,
env=env,
sandbox=context.sandbox(
binary="gpg",
options=options,
)
)

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

options: list[PathString] = [
"--bind", context.config.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=tool,
options=options,
)
)

def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
dir_sum = 0
Expand Down
9 changes: 9 additions & 0 deletions mkosi/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,7 @@ class Config:
passphrase: Optional[Path]
checksum: bool
sign: bool
openpgp_tool: Optional[str]
key: Optional[str]

tools_tree: Optional[Path]
Expand Down Expand Up @@ -2860,6 +2861,13 @@ 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 @@ -4540,6 +4548,7 @@ def summary(config: Config) -> str:
Passphrase: {none_to_none(config.passphrase)}
Checksum: {yes_no(config.checksum)}
Sign: {yes_no(config.sign)}
OpenPGP Tool: ({"gpg" if config.openpgp_tool is None else config.openpgp_tool})
GPG Key: ({"default" if config.key is None else config.key})
"""

Expand Down
8 changes: 5 additions & 3 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def mkosi(
user: Optional[int] = None,
group: Optional[int] = None,
check: bool = True,
env: Optional[dict] = None,
) -> CompletedProcess:
return run(
[
Expand All @@ -76,10 +77,10 @@ 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[dict] = None) -> CompletedProcess:
kcl = [
"loglevel=6",
"systemd.log_level=debug",
Expand Down Expand Up @@ -109,14 +110,15 @@ def build(self, options: Sequence[PathString] = (), args: Sequence[str] = ()) ->
*options,
] # fmt: skip

self.mkosi("summary", options, user=self.uid, group=self.uid)
self.mkosi("summary", options, user=self.uid, group=self.uid, env=env)

return self.mkosi(
"build",
opt,
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": null,
"Output": "outfile",
"OutputDirectory": "/your/output/here",
"OutputMode": 83,
Expand Down Expand Up @@ -434,6 +435,7 @@ def test_config() -> None:
minimum_version=GenericVersion("123"),
mirror=None,
nspawn_settings=None,
openpgp_tool=None,
output="outfile",
output_dir=Path("/your/output/here"),
output_format=OutputFormat.uki,
Expand Down
72 changes: 72 additions & 0 deletions tests/test_signing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SPDX-License-Identifier: LGPL-2.1-or-later

import contextlib
import os
import subprocess
import tempfile
import textwrap
from collections.abc import Iterator
from pathlib import Path

import pytest

from mkosi.distributions import Distribution
from mkosi.run import run
from mkosi.sandbox import umask
from mkosi.tree import copy_tree
from mkosi.types import PathString

from . import Image, ImageConfig

pytestmark = pytest.mark.integration

def test_signing_checksums_with_sop(config: ImageConfig, tmp_path) -> None:
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)

with Image(config) as image:
image.build(options=["--checksum=true", "--openpgp-tool=rsop", "--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, tmp_path) -> None:
signing_key = "[email protected]"
signing_cert = tmp_path / "signing-cert.pgp"

env = dict(GNUPGHOME=tmp_path / ".gnupg")

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

# export public key (certificate)
with open(signing_cert, "wb") as o:
run(cmdline = ["gpg", "--export", signing_key],
env = env,
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 88fd737

Please sign in to comment.