Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sign_node/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(self, config_file=None, **cmd_args):
"development_mode": False,
"is_community_sign_node": False,
"pgp_keys": [],
"yubikey_keyids": [],
"jwt_token": DEFAULT_JWT_TOKEN,
"master_url": DEFAULT_MASTER_URL,
"ws_master_url": DEFAULT_WS_MASTER_URL,
Expand All @@ -81,6 +82,7 @@ def __init__(self, config_file=None, **cmd_args):
"development_mode": {"type": "boolean", "default": False},
"is_community_sign_node": {"type": "boolean", "default": False},
"pgp_keys": {"type": "list", "required": True},
"yubikey_keyids": {"type": "list", "required": False},
"node_id": {"type": "string", "required": True},
"master_url": {"type": "string", "required": True},
"ws_master_url": {"type": "string", "required": True},
Expand Down
49 changes: 46 additions & 3 deletions sign_node/package_sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
RPM packages signing functions.
"""

import contextlib
import logging
import traceback
from typing import List, Optional

import pexpect

from sign_node.utils.locking import exclusive_lock
from sign_node.utils.locking import (
GPG_AGENT_LOCK_FILENAME,
exclusive_lock,
shared_lock,
)
from sign_node.utils.pgp_utils import restart_gpg_agent

__all__ = [
Expand All @@ -22,13 +28,43 @@ class PackageSignError(Exception):
pass


@contextlib.contextmanager
def gpg_sign_locks(
keyid,
gpg_locks_dir,
yubikey_keyids,
):
"""
Acquire the locks needed for a signing operation with ``keyid``.

A shared lock on the gpg-agent file is always held during signing
so that no other process can restart gpg-agent mid-sign. When
``keyid`` is a Yubikey-backed key, an additional per-key exclusive
lock serializes hardware access, and gpg-agent is reloaded (under
the exclusive gpg-agent lock) after the sign region exits.
"""
yubikey_keyids = yubikey_keyids or []
is_yubikey = keyid in yubikey_keyids
with shared_lock(gpg_locks_dir, GPG_AGENT_LOCK_FILENAME):
if is_yubikey:
with exclusive_lock(gpg_locks_dir, keyid):
yield
else:
yield
if is_yubikey:
with exclusive_lock(
gpg_locks_dir, GPG_AGENT_LOCK_FILENAME,
):
restart_gpg_agent()

def sign_rpm_package(
path,
keyid,
password,
sign_files=False,
sign_files_cert_path='/etc/pki/ima/ima-sign.key',
locks_dir_path: str = '/tmp/gpg_locks',
yubikey_keyids: Optional[List[str]] = None,
):
"""
Signs an RPM package.
Expand All @@ -45,6 +81,10 @@ def sign_rpm_package(
Flag to indicate if file signing is needed
sign_files_cert_path : str
Path to the certificate used for files signing
locks_dir_path : str
Path to a dir with lock files
yubikey_keyids: list
List of YubiKey IDs

Raises
------
Expand All @@ -71,15 +111,18 @@ def sign_rpm_package(
raise PackageSignError(
f'Cannot delete package signature: {full_out}'
)
with exclusive_lock(locks_dir_path, keyid):
with gpg_sign_locks(
keyid=keyid,
gpg_locks_dir=locks_dir_path,
yubikey_keyids=yubikey_keyids,
):
out, status = pexpect.run(
command=final_cmd,
events={"Enter passphrase:.*": f"{password}\r"},
env={"LC_ALL": "en_US.UTF-8"},
timeout=100000,
withexitstatus=True,
)
restart_gpg_agent()
if status is None:
message = (
f"The RPM signing command is failed with timeout."
Expand Down
2 changes: 2 additions & 0 deletions sign_node/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ def download_package(pkg: dict):
sign_files=sign_files,
sign_files_cert_path=self.__config.files_sign_cert_path,
locks_dir_path=self.__config.locks_dir_path,
yubikey_keyids=self.__config.yubikey_keyids,
)
packages_to_sign = []
if packages_to_sign:
Expand All @@ -455,6 +456,7 @@ def download_package(pkg: dict):
sign_files=sign_files,
sign_files_cert_path=self.__config.files_sign_cert_path,
locks_dir_path=self.__config.locks_dir_path,
yubikey_keyids=self.__config.yubikey_keyids,
)
finish_time = datetime.utcnow()
stats['sign_packages_time'] = self.timedelta_seconds(
Expand Down
15 changes: 15 additions & 0 deletions sign_node/utils/locking.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pathlib import Path


GPG_AGENT_LOCK_FILENAME = '.gpg-agent'


@contextlib.contextmanager
def exclusive_lock(path: str, filename: str):
locks_dir = Path(path)
Expand All @@ -13,3 +16,15 @@ def exclusive_lock(path: str, filename: str):
yield
finally:
fcntl.flock(file, fcntl.LOCK_UN)


@contextlib.contextmanager
def shared_lock(path: str, filename: str):
locks_dir = Path(path)
locks_dir.mkdir(exist_ok=True, parents=True)
with Path(locks_dir, filename).open('w+') as file:
fcntl.flock(file, fcntl.LOCK_SH)
try:
yield
finally:
fcntl.flock(file, fcntl.LOCK_UN)
Loading