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 2, 2024
1 parent e6a3e23 commit 3b88281
Show file tree
Hide file tree
Showing 6 changed files with 137 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
87 changes: 58 additions & 29 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2157,41 +2157,70 @@ 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
cmdline: list[PathString] = []
options: list[PathString] = []
if tool is None or tool == "gpg":
cmdline += ["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 += [
"--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 += [tool, "sign", "/signing-key.pgp"]

options += [
"--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:
Expand Down
8 changes: 8 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,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 @@ -4540,6 +4547,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
13 changes: 10 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[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[dict[str, str]] = None,
) -> CompletedProcess:
kcl = [
"loglevel=6",
"systemd.log_level=debug",
Expand Down Expand Up @@ -109,14 +115,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
58 changes: 58 additions & 0 deletions tests/test_signing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# SPDX-License-Identifier: LGPL-2.1-or-later


import pytest

from mkosi.run import run
from mkosi.types import PathString

from . import Image, ImageConfig

pytestmark = pytest.mark.integration


def test_signing_checksums_with_sop(config: ImageConfig, tmp_path: PathString) -> 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: PathString) -> None:
signing_key = "mkosi-test@example.org"
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 3b88281

Please sign in to comment.