diff options
author | CoprDistGit <infra@openeuler.org> | 2025-03-21 17:37:41 +0000 |
---|---|---|
committer | CoprDistGit <infra@openeuler.org> | 2025-03-21 17:37:41 +0000 |
commit | bed3ab1ed2c9a92b20bae1d0f11f17d9051bfff9 (patch) | |
tree | 76779ef5ca00cab259eb516c4c44eef0b3048cc9 /support_signatrust_backend.patch | |
parent | 2132b310609fdedcd5427676a98f1174869b3a82 (diff) |
automatic import of copr-backend
Diffstat (limited to 'support_signatrust_backend.patch')
-rw-r--r-- | support_signatrust_backend.patch | 1150 |
1 files changed, 1150 insertions, 0 deletions
diff --git a/support_signatrust_backend.patch b/support_signatrust_backend.patch new file mode 100644 index 0000000..d803d26 --- /dev/null +++ b/support_signatrust_backend.patch @@ -0,0 +1,1150 @@ +diff --git a/copr_backend/actions.py b/backend/copr_backend/actions.py +index 39722b843..6da2dfb65 100644 +--- a/copr_backend/actions.py ++++ b/copr_backend/actions.py +@@ -21,13 +21,13 @@ from copr_common.worker_manager import WorkerManager + + from copr_backend.worker_manager import BackendQueueTask + +-from .sign import create_user_keys, CoprKeygenRequestError ++from .sign import CoprKeygenRequestError + from .exceptions import CreateRepoError, CoprSignError, FrontendClientException + from .helpers import (get_redis_logger, silent_remove, ensure_dir_exists, + get_chroot_arch, format_filename, + uses_devel_repo, call_copr_repo, build_chroot_log_name, + copy2_but_hardlink_rpms) +-from .sign import sign_rpms_in_dir, unsign_rpms_in_dir, get_pubkey ++from .sign import new_signer + + + class Action(object): +@@ -92,6 +92,8 @@ class Action(object): + + self.log = log if log else get_redis_logger(self.opts, "backend.actions", "actions") + ++ self.signer = new_signer(opts) ++ + def __str__(self): + return "<{}(Action): {}>".format(self.__class__.__name__, self.data) + +@@ -147,7 +149,7 @@ class GPGMixin(object): + # skip key creation, most probably sign component is unused + return True + try: +- create_user_keys(ownername, projectname, self.opts) ++ self.signer.create_user_keys(ownername, projectname, self.opts) + return True + except CoprKeygenRequestError as e: + self.log.exception(e) +@@ -176,7 +178,7 @@ class Fork(Action, GPGMixin): + # Generate brand new gpg key. + self.generate_gpg_key(data["user"], data["copr"]) + # Put the new public key into forked build directory. +- get_pubkey(data["user"], data["copr"], self.log, self.opts.sign_domain, pubkey_path) ++ self.signer.get_pubkey(data["user"], data["copr"], self.log, self.opts.sign_domain, pubkey_path) + + chroot_paths = set() + for chroot, src_dst_dir in builds_map.items(): +@@ -206,9 +208,9 @@ class Fork(Action, GPGMixin): + continue + + # Drop old signatures coming from original repo and re-sign. +- unsign_rpms_in_dir(dst_path, opts=self.opts, log=self.log) ++ self.signer.unsign_rpms_in_dir(dst_path, opts=self.opts, log=self.log) + if sign: +- sign_rpms_in_dir(data["user"], data["copr"], dst_path, ++ self.signer.sign_rpms_in_dir(data["user"], data["copr"], dst_path, + chroot, opts=self.opts, log=self.log) + + self.log.info("Forked build %s as %s", src_path, dst_path) +diff --git a/copr_backend/background_worker.py b/backend/copr_backend/background_worker.py +index 4b5f13135..bc05edbd7 100644 +--- a/copr_backend/background_worker.py ++++ b/copr_backend/background_worker.py +@@ -9,7 +9,7 @@ import logging + from copr_common.background_worker import BackgroundWorker + from copr_backend.frontend import FrontendClient + from copr_backend.helpers import (BackendConfigReader, get_redis_logger) +- ++from copr_backend.sign import new_signer + + class BackendBackgroundWorker(BackgroundWorker): + """ +@@ -28,6 +28,7 @@ class BackendBackgroundWorker(BackgroundWorker): + + self.frontend_client = FrontendClient(self.opts, self.log, + try_indefinitely=True) ++ self.signer = new_signer(self.opts) + + def _switch_logger_to_redis(self): + logger_name = '{}.{}.pid-{}'.format( +diff --git a/copr_backend/background_worker_build.py b/backend/copr_backend/background_worker_build.py +index 0eee39ab8..c12d4921d 100644 +--- a/copr_backend/background_worker_build.py ++++ b/copr_backend/background_worker_build.py +@@ -27,8 +27,8 @@ from copr_backend.helpers import ( + call_copr_repo, run_cmd, register_build_result, format_evr, + ) + from copr_backend.job import BuildJob +-from copr_backend.msgbus import MessageSender +-from copr_backend.sign import sign_rpms_in_dir, get_pubkey ++from copr_backend.euler_msgbus import MessageSender ++from copr_backend.sign import new_signer + from copr_backend.sshcmd import SSHConnection, SSHConnectionError + from copr_backend.vm_alloc import ResallocHostFactory + +@@ -622,7 +622,7 @@ class BuildBackgroundWorker(BackendBackgroundWorker): + self.log.info("Going to sign pkgs from source: %s in chroot: %s", + self.job.task_id, self.job.chroot_dir) + +- sign_rpms_in_dir( ++ self.signer.sign_rpms_in_dir( + self.job.project_owner, + self.job.project_name, + os.path.join(self.job.chroot_dir, self.job.target_dir_name), +@@ -736,7 +736,7 @@ class BuildBackgroundWorker(BackendBackgroundWorker): + # TODO: uncomment this when key revoke/change will be implemented + # if os.path.exists(pubkey_path): + # return +- get_pubkey(user, project, self.log, self.opts.sign_domain, pubkey_path) ++ self.signer.get_pubkey(user, project, self.log, self.opts.sign_domain, pubkey_path) + self.log.info("Added pubkey for user %s project %s into: %s", + user, project, pubkey_path) + +diff --git a/copr_backend/constants.py b/backend/copr_backend/constants.py +index a529be28a..83bcb8fbe 100644 +--- a/copr_backend/constants.py ++++ b/copr_backend/constants.py +@@ -13,6 +13,7 @@ DEF_BUILD_USER = "mockbuilder" + DEF_DESTDIR = os.getcwd() + DEF_MACROS = {} + DEF_BUILDROOT_PKGS = "" ++DEF_SIGN_BACKEND = "obs-signd" + + + DEF_CONSECUTIVE_FAILURE_THRESHOLD = 10 +diff --git a/copr_backend/exceptions.py b/backend/copr_backend/exceptions.py +index 21afb14c6..0865fcc8c 100644 +--- a/copr_backend/exceptions.py ++++ b/copr_backend/exceptions.py +@@ -48,8 +48,8 @@ class CoprKeygenRequestError(Exception): + + def __str__(self): + out = super(CoprKeygenRequestError, self).__str__() +- out += "\nrequest to copr-keygen: {}\n".format(self.request) +- if self.response: ++ out += "\nrequest to key_backend: {}\n".format(self.request) ++ if self.response is not None: + out += "status code: {}\n" "response content: {}\n" \ + .format(self.response.status_code, self.response.content) + return out +diff --git a/copr_backend/helpers.py b/backend/copr_backend/helpers.py +index 75fa5e62d..291228912 100644 +--- a/copr_backend/helpers.py ++++ b/copr_backend/helpers.py +@@ -31,7 +31,7 @@ from munch import Munch + from copr_common.redis_helpers import get_redis_connection + from copr.v3 import Client + from copr_backend.constants import DEF_BUILD_USER, DEF_BUILD_TIMEOUT, DEF_CONSECUTIVE_FAILURE_THRESHOLD, \ +- CONSECUTIVE_FAILURE_REDIS_KEY, default_log_format ++ CONSECUTIVE_FAILURE_REDIS_KEY, default_log_format, DEF_SIGN_BACKEND + from copr_backend.exceptions import CoprBackendError + + from . import constants +@@ -309,6 +309,18 @@ class BackendConfigReader(object): + opts.sign_domain = _get_conf( + cp, "backend", "sign_domain", DOMAIN) + ++ opts.sign_backend = _get_conf( ++ cp, "backend", "sign_backend", DEF_SIGN_BACKEND) ++ ++ opts.signatrust_host = _get_conf( ++ cp, "backend", "signatrust_host", "") ++ ++ opts.signatrust_token = _get_conf( ++ cp, "backend", "signatrust_token", "") ++ ++ opts.signatrust_key_expire = _get_conf( ++ cp, "backend", "signatrust_key_expire", 1825, mode="int") ++ + opts.build_groups = [] + for group_id in range(opts.build_groups_count): + archs = _get_conf(cp, "backend", +diff --git a/copr_backend/sign.py b/backend/copr_backend/sign.py +index e21653e78..95f674255 100644 +--- a/copr_backend/sign.py ++++ b/copr_backend/sign.py +@@ -25,78 +25,6 @@ def create_gpg_email(username, projectname, domain): + + return "{}#{}@copr.{}".format(username, projectname, domain) + +- +-def call_sign_bin(cmd, log): +- """ +- Call /bin/sign and return (rc, stdout, stderr). Re-try the call +- automatically upon certain failures (if that makes sense). +- """ +- cmd_pretty = ' '.join(cmd) +- for attempt in [1, 2, 3]: +- log.info("Calling '%s' (attempt #%s)", cmd_pretty, attempt) +- try: +- handle = Popen(cmd, stdout=PIPE, stderr=PIPE, encoding="utf-8") +- stdout, stderr = handle.communicate() +- except (SubprocessError, OSError) as err: +- new_err = CoprSignError("Failed to invoke '{}'".format(cmd_pretty)) +- raise new_err from err +- +- if handle.returncode != 0: +- log.warning("Command '%s' failed with: %s", +- cmd_pretty, stderr.rstrip()) +- sleeptime = 20 +- log.warning("Going to sleep %ss and re-try.", sleeptime) +- time.sleep(sleeptime) +- continue +- break +- return handle.returncode, stdout, stderr +- +- +-def get_pubkey(username, projectname, log, sign_domain, outfile=None): +- """ +- Retrieves public key for user/project from signer host. +- +- :param sign_domain: the domain name of the sign key +- :param outfile: [optional] file to write obtained key +- :return: public keys +- +- :raises CoprSignError: failed to retrieve key, see error message +- :raises CoprSignNoKeyError: if there are no such user in keyring +- """ +- usermail = create_gpg_email(username, projectname, sign_domain) +- cmd = [SIGN_BINARY, "-u", usermail, "-p"] +- +- returncode, stdout, stderr = call_sign_bin(cmd, log) +- if returncode != 0: +- if "unknown key:" in stderr: +- raise CoprSignNoKeyError( +- "There are no gpg keys for user {} in keyring".format(username), +- return_code=returncode, +- cmd=cmd, stdout=stdout, stderr=stderr) +- raise CoprSignError( +- msg="Failed to get user pubkey\n" +- "sign stdout: {}\n sign stderr: {}\n".format(stdout, stderr), +- return_code=returncode, +- cmd=cmd, stdout=stdout, stderr=stderr) +- +- if outfile: +- with open(outfile, "w") as handle: +- handle.write(stdout) +- +- return stdout +- +- +-def _sign_one(path, email, hashtype, log): +- cmd = [SIGN_BINARY, "-4", "-h", hashtype, "-u", email, "-r", path] +- returncode, stdout, stderr = call_sign_bin(cmd, log) +- if returncode != 0: +- raise CoprSignError( +- msg="Failed to sign {} by user {}".format(path, email), +- return_code=returncode, +- cmd=cmd, stdout=stdout, stderr=stderr) +- return stdout, stderr +- +- + def gpg_hashtype_for_chroot(chroot, opts): + """ + Given the chroot name (in "mock format", like "fedora-rawhide-x86_64") +@@ -136,133 +64,436 @@ def gpg_hashtype_for_chroot(chroot, opts): + return "sha256" + + +-def sign_rpms_in_dir(username, projectname, path, chroot, opts, log): +- """ +- Signs rpms using obs-signd. ++# a sign interface ++class Signer(object): ++ @classmethod ++ def get_pubkey(cls, username, projectname, log, sign_domain, outfile=None): ++ """get public key""" ++ raise NotImplementedError + +- If some some pkgs failed to sign, entire build marked as failed, +- but we continue to try sign other pkgs. ++ @classmethod ++ def sign_rpms_in_dir(cls, username, projectname, path, chroot, opts, log): ++ """batch sign rpms""" ++ raise NotImplementedError + +- :param username: copr username +- :param projectname: copr projectname +- :param path: directory with rpms to be signed +- :param chroot: chroot name where we sign packages, affects the hash type +- :param Munch opts: backend config ++ @classmethod ++ def create_user_keys(username, projectname, opts, try_indefinitely=False): ++ """create user key pair""" ++ raise NotImplementedError + +- :type log: logging.Logger ++ @classmethod ++ def _sign_one(cls, path, email, hashtype, log): ++ """sign one rpm""" ++ raise NotImplementedError + +- :raises: :py:class:`backend.exceptions.CoprSignError` failed to sign at least one package +- """ ++ @classmethod ++ def _unsign_one(cls, path): ++ # Requires rpm-sign package ++ cmd = ["/usr/bin/rpm", "--delsign", path] ++ handle = Popen(cmd, stdout=PIPE, stderr=PIPE, encoding="utf-8") ++ stdout, stderr = handle.communicate() ++ ++ if handle.returncode != 0: ++ err = CoprSignError( ++ msg="Failed to unsign {}".format(path), ++ return_code=handle.returncode, ++ cmd=cmd, stdout=stdout, stderr=stderr) + +- rpm_list = [ +- os.path.join(path, filename) +- for filename in os.listdir(path) +- if filename.endswith(".rpm") +- ] ++ raise err ++ ++ return stdout, stderr ++ ++ @classmethod ++ def call_sign_bin(cls, cmd, log): ++ """ ++ Call sign_cmd and return (rc, stdout, stderr). Re-try the call ++ automatically upon certain failures (if that makes sense). ++ """ ++ cmd_pretty = ' '.join(cmd) ++ for attempt in [1, 2, 3]: ++ log.info("Calling '%s' (attempt #%s)", cmd_pretty, attempt) ++ try: ++ handle = Popen(cmd, stdout=PIPE, stderr=PIPE, encoding="utf-8") ++ stdout, stderr = handle.communicate() ++ except (SubprocessError, OSError) as err: ++ new_err = CoprSignError("Failed to invoke '{}'".format(cmd_pretty)) ++ raise new_err from err ++ ++ if handle.returncode != 0: ++ log.warning("Command '%s' failed with: %s", ++ cmd_pretty, stderr.rstrip()) ++ sleeptime = 20 ++ log.warning("Going to sleep %ss and re-try.", sleeptime) ++ time.sleep(sleeptime) ++ continue ++ break ++ return handle.returncode, stdout, stderr ++ ++ @classmethod ++ def sign_rpms_in_dir(cls, username, projectname, path, chroot, opts, log): ++ """ ++ Signs rpms using obs-signd. ++ ++ If some some pkgs failed to sign, entire build marked as failed, ++ but we continue to try sign other pkgs. ++ ++ :param username: copr username ++ :param projectname: copr projectname ++ :param path: directory with rpms to be signed ++ :param chroot: chroot name where we sign packages, affects the hash type ++ :param Munch opts: backend config ++ ++ :type log: logging.Logger ++ ++ :raises: :py:class:`backend.exceptions.CoprSignError` failed to sign at least one package ++ """ ++ rpm_list = [ ++ os.path.join(path, filename) ++ for filename in os.listdir(path) ++ if filename.endswith(".rpm") ++ ] + +- if not rpm_list: +- return ++ if not rpm_list: ++ return + +- hashtype = gpg_hashtype_for_chroot(chroot, opts) ++ hashtype = gpg_hashtype_for_chroot(chroot, opts) + +- try: +- get_pubkey(username, projectname, log, opts.sign_domain) +- except CoprSignNoKeyError: +- create_user_keys(username, projectname, opts, try_indefinitely=True) ++ try: ++ cls.get_pubkey(username, projectname, log, opts.sign_domain) ++ except CoprSignNoKeyError: ++ cls.create_user_keys(username, projectname, opts, try_indefinitely=True) ++ ++ errors = [] # tuples (rpm_filepath, exception) ++ for rpm in rpm_list: ++ try: ++ cls._sign_one(rpm, create_gpg_email(username, projectname, opts.sign_domain), ++ hashtype, log) ++ log.info("signed rpm: %s", rpm) ++ ++ except CoprSignError as e: ++ log.exception("failed to sign rpm: %s", rpm) ++ errors.append((rpm, e)) ++ ++ if errors: ++ raise CoprSignError("Rpm sign failed, affected rpms: {}" ++ .format([err[0] for err in errors])) ++ ++ @classmethod ++ def unsign_rpms_in_dir(cls, path, opts, log): ++ """ ++ :param path: directory with rpms to be signed ++ :param Munch opts: backend config ++ :type log: logging.Logger ++ :raises: :py:class:`backend.exceptions.CoprSignError` failed to sign at least one package ++ """ ++ rpm_list = [ ++ os.path.join(path, filename) ++ for filename in os.listdir(path) ++ if filename.endswith(".rpm") ++ ] ++ ++ if not rpm_list: ++ return ++ ++ errors = [] # tuples (rpm_filepath, exception) ++ for rpm in rpm_list: ++ try: ++ cls._unsign_one(rpm) ++ log.info("unsigned rpm: %s", rpm) ++ ++ except CoprSignError as e: ++ log.exception("failed to unsign rpm: %s", rpm) ++ errors.append((rpm, e)) ++ ++ if errors: ++ raise CoprSignError("Rpm unsign failed, affected rpms: {}" ++ .format([err[0] for err in errors])) ++ ++def new_signer(opts): ++ if opts.sign_backend == "signatrust": ++ Signatrust.signatrust_token = opts.signatrust_token ++ Signatrust.signatrust_host = opts.signatrust_host ++ return Signatrust ++ else: # keep obs-signd as default backend ++ return ObsSign ++ ++class ObsSign(Signer): ++ sign_cmd = "/bin/sign" ++ ++ @classmethod ++ def get_pubkey(cls, username, projectname, log, sign_domain, outfile=None): ++ """ ++ Retrieves public key for user/project from signer host. ++ ++ :param sign_domain: the domain name of the sign key ++ :param outfile: [optional] file to write obtained key ++ :return: public keys ++ ++ :raises CoprSignError: failed to retrieve key, see error message ++ :raises CoprSignNoKeyError: if there are no such user in keyring ++ """ ++ usermail = create_gpg_email(username, projectname, sign_domain) ++ cmd = [cls.sign_cmd, "-u", usermail, "-p"] ++ ++ returncode, stdout, stderr = cls.call_sign_bin(cmd, log) ++ if returncode != 0: ++ if "unknown key:" in stderr: ++ raise CoprSignNoKeyError( ++ "There are no gpg keys for user {} in keyring".format(username), ++ return_code=returncode, ++ cmd=cmd, stdout=stdout, stderr=stderr) ++ raise CoprSignError( ++ msg="Failed to get user pubkey\n" ++ "sign stdout: {}\n sign stderr: {}\n".format(stdout, stderr), ++ return_code=returncode, ++ cmd=cmd, stdout=stdout, stderr=stderr) + +- errors = [] # tuples (rpm_filepath, exception) +- for rpm in rpm_list: ++ if outfile: ++ with open(outfile, "w") as handle: ++ handle.write(stdout) ++ ++ return stdout ++ ++ @classmethod ++ def _sign_one(cls, path, email, hashtype, log): ++ cmd = [cls.sign_cmd, "-4", "-h", hashtype, "-u", email, "-r", path] ++ returncode, stdout, stderr = cls.call_sign_bin(cmd, log) ++ if returncode != 0: ++ raise CoprSignError( ++ msg="Failed to sign {} by user {}".format(path, email), ++ return_code=returncode, ++ cmd=cmd, stdout=stdout, stderr=stderr) ++ return stdout, stderr ++ ++ @classmethod ++ def create_user_keys(cls, username, projectname, opts, try_indefinitely=False): ++ """ ++ Generate a new key-pair at sign host ++ ++ :param username: ++ :param projectname: ++ :param opts: backend config ++ ++ :return: None ++ """ ++ data = { ++ "name_real": "{}_{}".format(username, projectname), ++ "name_email": create_gpg_email(username, projectname, opts.sign_domain) ++ } ++ ++ log = get_redis_logger(opts, "sign", "actions") ++ keygen_url = "http://{}/gen_key".format(opts.keygen_host) ++ query = dict(url=keygen_url, data=data, method="post") ++ try: ++ request = SafeRequest(log=log, try_indefinitely=try_indefinitely) ++ response = request.send(**query) ++ except Exception as e: ++ raise CoprKeygenRequestError( ++ msg="Failed to create key-pair for user: {}," ++ " project:{} with error: {}" ++ .format(username, projectname, e), request=query) ++ ++ if response.status_code >= 400: ++ raise CoprKeygenRequestError( ++ msg="Failed to create key-pair for user: {}, project:{}, status_code: {}, response: {}" ++ .format(username, projectname, response.status_code, response.text), ++ request=query, response=response) ++ ++class Signatrust(Signer): ++ sign_cmd = "/usr/local/bin/client" ++ prefix = "" ++ signatrust_host = "" ++ signatrust_token = "" ++ ++ @classmethod ++ def get_prefix(cls): ++ """ ++ Get prefix of the user ++ ++ As in copr, we set key attr with visibility=private ++ These keys' name were prefixed by user's email like: ++ tommylikehu@gmail.com:mywaaagh_admin_test ++ """ ++ headers = { ++ "accept": "application/json", ++ "Authorization": cls.signatrust_token ++ } ++ try: ++ r = requests.get("{}/api/v1/users/info".format(cls.signatrust_host), headers=headers).json() ++ cls.prefix = r.get("email") ++ except Exception as e: ++ raise CoprKeygenRequestError( ++ msg="Failed to get userinfo", request="/api/v1/users/info") ++ ++ @classmethod ++ def get_key_name(cls, username, projectname, prefix=True): ++ """ ++ copr key_name rule in signatrust: ++ <user prefix>:<user_name>_<project_name> ++ """ ++ if not cls.prefix: ++ cls.get_prefix() ++ if prefix: ++ return "{}:{}_{}".format(cls.prefix, username, projectname) ++ return "{}_{}".format(username, projectname) ++ ++ @classmethod ++ def get_pubkey(cls, username, projectname, log, sign_domain, outfile=None): ++ """ ++ get public key ++ ++ https://domain:port/api/v1/keys/<key_name>/public_key ++ """ ++ if not cls.prefix: ++ cls.get_prefix() ++ ++ headers = { ++ "accept": "application/json", ++ "Authorization": cls.signatrust_token ++ } ++ ++ key_name = cls.get_key_name(username, projectname) ++ url = "{}/api/v1/keys/{}/public_key".format(cls.signatrust_host, key_name) + try: +- _sign_one(rpm, create_gpg_email(username, projectname, opts.sign_domain), +- hashtype, log) +- log.info("signed rpm: %s", rpm) ++ r = requests.get(url, headers=headers) ++ except Exception as e: ++ raise CoprKeygenRequestError( ++ msg="Failed to get public_key", request="/api/v1/keys/{}/public_key".format(key_name), response=r) + +- except CoprSignError as e: +- log.exception("failed to sign rpm: %s", rpm) +- errors.append((rpm, e)) ++ if r.status_code == 404: ++ raise CoprSignNoKeyError( ++ "There are no gpg keys for user {} in keyring".format(username), ++ return_code=r.status_code, ++ cmd="GET {}".format(url), stdout="", stderr="") ++ elif r.status_code >= 400: ++ raise CoprKeygenRequestError( ++ msg="Failed to get user pubkey\n", ++ request="/api/v1/keys/{}/public_key".format(key_name), response=r) ++ ++ if outfile: ++ with open(outfile, "wb") as handle: ++ handle.write(r.content) ++ ++ return r.content ++ ++ @classmethod ++ def sign_rpms_in_dir(cls, username, projectname, path, chroot, opts, log): ++ """batch sign rpms""" ++ if not cls.prefix: ++ cls.get_prefix() ++ ++ if not cls._key_existed(username, projectname, opts): ++ cls.create_user_keys(username, projectname, opts) ++ ++ # when we migrate copr keys into signatrust ++ # we fellow the rules: ++ # key_name = <user_email>:<user_name>_<project_name> ++ cmd = [cls.sign_cmd, "-c", "/etc/signatrust.toml", "add", "--file-type", "rpm", "--key-type", "pgp", "--key-name", cls.get_key_name(username, projectname), path] ++ ++ returncode, stdout, stderr = cls.call_sign_bin(cmd, log) ++ if returncode != 0: ++ raise CoprSignError( ++ msg="Failed to sign rpms\n" ++ "sign stdout: {}\n sign stderr: {}\n".format(stdout, stderr), ++ return_code=returncode, ++ cmd=cmd, stdout=stdout, stderr=stderr) + +- if errors: +- raise CoprSignError("Rpm sign failed, affected rpms: {}" +- .format([err[0] for err in errors])) ++ if stderr: ++ # signatrust client will print error message for one rpm per line ++ failed_list = re.findall(r"failed to sign file (.*.rpm) due to error", stderr) ++ ++ if failed_list: ++ failed_rpms = " ".join(map(os.path.basename, failed_list)) ++ log.exception("failed to sign rpm: %s".format(failed_rpms)) ++ raise CoprSignError("Rpm sign failed, affected rpms: {}" ++ .format(failed_rpms)) ++ ++ @classmethod ++ def unsign_rpms_in_dir(cls, path, opts, log): ++ """ ++ signatrust will replace the signature infomation defaultly, ++ so there is no need to unsign, just return ++ """ ++ return + ++ @classmethod ++ def _key_existed(cls, username, projectname, opts): ++ """ ++ check keyname existence + +-def create_user_keys(username, projectname, opts, try_indefinitely=False): +- """ +- Generate a new key-pair at sign host ++ HEAD /api/v1/keys/ ++ """ ++ if not cls.prefix: ++ cls.get_prefix() + +- :param username: +- :param projectname: +- :param opts: backend config ++ key_name = cls.get_key_name(username, projectname, prefix=False) + +- :return: None +- """ +- data = { +- "name_real": "{}_{}".format(username, projectname), +- "name_email": create_gpg_email(username, projectname, opts.sign_domain) +- } +- +- log = get_redis_logger(opts, "sign", "actions") +- keygen_url = "http://{}/gen_key".format(opts.keygen_host) +- query = dict(url=keygen_url, data=data, method="post") +- try: +- request = SafeRequest(log=log, try_indefinitely=try_indefinitely) +- response = request.send(**query) +- except Exception as e: +- raise CoprKeygenRequestError( +- msg="Failed to create key-pair for user: {}," +- " project:{} with error: {}" +- .format(username, projectname, e), request=query) +- +- if response.status_code >= 400: +- raise CoprKeygenRequestError( +- msg="Failed to create key-pair for user: {}, project:{}, status_code: {}, response: {}" +- .format(username, projectname, response.status_code, response.text), +- request=query, response=response) +- +- +-def _unsign_one(path): +- # Requires rpm-sign package +- cmd = ["/usr/bin/rpm", "--delsign", path] +- handle = Popen(cmd, stdout=PIPE, stderr=PIPE, encoding="utf-8") +- stdout, stderr = handle.communicate() +- +- if handle.returncode != 0: +- err = CoprSignError( +- msg="Failed to unsign {}".format(path), +- return_code=handle.returncode, +- cmd=cmd, stdout=stdout, stderr=stderr) +- +- raise err +- +- return stdout, stderr +- +- +-def unsign_rpms_in_dir(path, opts, log): +- """ +- :param path: directory with rpms to be signed +- :param Munch opts: backend config +- :type log: logging.Logger +- :raises: :py:class:`backend.exceptions.CoprSignError` failed to sign at least one package +- """ +- rpm_list = [ +- os.path.join(path, filename) +- for filename in os.listdir(path) +- if filename.endswith(".rpm") +- ] ++ query = { ++ "name": key_name, ++ "visibility": "private" ++ } + +- if not rpm_list: +- return ++ headers = { ++ "accept": "application/json", ++ "Authorization": cls.signatrust_token ++ } + +- errors = [] # tuples (rpm_filepath, exception) +- for rpm in rpm_list: + try: +- _unsign_one(rpm) +- log.info("unsigned rpm: %s", rpm) +- +- except CoprSignError as e: +- log.exception("failed to unsign rpm: %s", rpm) +- errors.append((rpm, e)) ++ res = requests.head("{}/api/v1/keys/name_identical".format(opts.signatrust_host), headers=headers, params=query) ++ except Exception as e: ++ raise CoprKeygenRequestError( ++ msg="Failed to check key existence", request="/api/v1/keys/name_identical", response=res) ++ ++ # signatrust return 200 means key name available ++ if res.status_code == 200: ++ return False ++ # signatrust return 409 means key name redundant ++ elif res.status_code == 409: ++ return True ++ else: ++ raise CoprKeygenRequestError( ++ msg="Failed to check key existence", request="/api/v1/keys/name_identical", response=res) ++ ++ @classmethod ++ def create_user_keys(cls, username, projectname, opts, try_indefinitely=False): ++ """ ++ create user key pair ++ ++ POST /api/v1/keys/ ++ """ ++ if not cls.prefix: ++ cls.get_prefix() ++ ++ if cls._key_existed(username, projectname, opts): ++ return ++ time_format = "%Y-%m-%d %H:%M:%S%z" ++ expire = datetime.now(datetime.now(timezone.utc).astimezone().tzinfo) + timedelta(days=opts.signatrust_key_expire) ++ data = { ++ "name": cls.get_key_name(username, projectname, prefix=False), ++ "description": "gpg key to sign rpms in {}/{}".format(username, projectname), ++ "key_type": "pgp", ++ "visibility": "private", # we use private key type for those key will not be seen by other users ++ "attributes": { ++ "digest_algorithm": "sha2_256", ++ "key_type": "rsa", ++ "key_length": "2048", ++ "email": "{}".format(create_gpg_email(username, projectname, opts.sign_domain)), ++ }, ++ "expire_at": datetime.strftime(expire, time_format) ++ } ++ ++ headers = { ++ "content-type": "application/json", ++ "accept": "application/json", ++ "Authorization": opts.signatrust_token ++ } + +- if errors: +- raise CoprSignError("Rpm unsign failed, affected rpms: {}" +- .format([err[0] for err in errors])) ++ try: ++ res = requests.post("{}/api/v1/keys/".format(opts.signatrust_host), headers=headers, json=data) ++ except Exception as e: ++ raise CoprKeygenRequestError( ++ msg="Failed to get userinfo", request="/api/v1/keys/", response=res) ++ ++ if res.status_code >= 400: ++ raise CoprKeygenRequestError( ++ msg="Failed to create user payload: {}".format(data), request="/api/v1/keys/", response=res) +diff --git a/tests/daemons/test_log.py b/backend/tests/daemons/test_log.py +index 73b1a777a..c68b7d918 100644 +--- a/tests/daemons/test_log.py ++++ b/tests/daemons/test_log.py +@@ -45,7 +45,8 @@ class TestLog(object): + self.log_file = os.path.join(self.log_dir, "copr.log") + self.opts = Munch( + verbose=False, +- log_dir=self.log_dir ++ log_dir=self.log_dir, ++ sign_backend = "obs-signd" + ) + print("\n log dir: {}".format(self.log_dir)) + self.queue = MagicMock() +diff --git a/tests/daemons/unused_test_job_grab.py b/backend/tests/daemons/unused_test_job_grab.py +index be3e64c56..79f6472b7 100644 +--- a/tests/daemons/unused_test_job_grab.py ++++ b/tests/daemons/unused_test_job_grab.py +@@ -87,6 +87,7 @@ class TestJobGrab(object): + redis_host="127.0.0.1", + redis_port=6379, + redis_db=0, ++ sign_backend = "obs-signd" + ) + + self.queue = MagicMock() +diff --git a/tests/run/test_copr_prune_results.py b/backend/tests/run/test_copr_prune_results.py +index 6620f01b1..8c9ef2369 100644 +--- a/tests/run/test_copr_prune_results.py ++++ b/tests/run/test_copr_prune_results.py +@@ -59,7 +59,8 @@ class TestPruneResults(object): + self.opts = Munch( + prune_days=14, + frontend_base_url = '<frontend_url>', +- destdir=self.testresults_dir ++ destdir=self.testresults_dir, ++ sign_backend = "obs-signd" + ) + + def teardown_method(self, method): +diff --git a/tests/test_action.py b/backend/tests/test_action.py +index 7da3ab09c..4c935eb53 100644 +--- a/tests/test_action.py ++++ b/tests/test_action.py +@@ -57,6 +57,7 @@ class TestAction(object): + results_baseurl=RESULTS_ROOT_URL, + + do_sign=False, ++ sign_backend = "obs-signd", + + keygen_host="example.com" + ) +@@ -136,7 +137,7 @@ class TestAction(object): + @mock.patch("copr_backend.actions.os.makedirs") + @mock.patch("copr_backend.actions.copy_tree") + @mock.patch("copr_backend.actions.os.path.exists") +- @mock.patch("copr_backend.actions.unsign_rpms_in_dir") ++ @mock.patch("copr_backend.sign.ObsSign.unsign_rpms_in_dir") + @mock.patch("copr_backend.helpers.subprocess.Popen") + def test_action_handle_forks(self, mc_popen, mc_unsign_rpms_in_dir, + mc_exists, mc_copy_tree, _mc_os_makedirs, +diff --git a/tests/test_background_worker_build.py b/backend/tests/test_background_worker_build.py +index 1dfe19563..258564067 100644 +--- a/tests/test_background_worker_build.py ++++ b/tests/test_background_worker_build.py +@@ -77,6 +77,7 @@ def _reset_build_worker(): + # Don't waste time with mocking. We don't want to log anywhere, and we want + # to let BuildBackgroundWorker adjust the handlers. + worker.log.handlers = [] ++ worker.opts.sign_backend = "obs-signd" + return worker + + def _fake_host(): +diff --git a/tests/test_frontend.py b/backend/tests/test_frontend.py +index eeb8dc8f4..06a6e605b 100644 +--- a/tests/test_frontend.py ++++ b/tests/test_frontend.py +@@ -48,6 +48,7 @@ class TestFrontendClient(object): + self.opts = Munch( + frontend_base_url="http://example.com/", + frontend_auth="12345678", ++ sign_backend = "obs-signd" + ) + self.fc = FrontendClient(self.opts) + +diff --git a/tests/test_helpers.py b/backend/tests/test_helpers.py +index 8da70269d..e23565054 100644 +--- a/tests/test_helpers.py ++++ b/tests/test_helpers.py +@@ -31,6 +31,7 @@ class TestHelpers(object): + self.opts = Munch( + redis_db=9, + redis_port=7777, ++ sign_backend = "obs-signd" + ) + + self.rc = get_redis_connection(self.opts) +diff --git a/tests/test_sign.py b/backend/tests/test_sign.py +index bf2dd1b8c..ebb8f2be3 100644 +--- a/tests/test_sign.py ++++ b/tests/test_sign.py +@@ -10,10 +10,10 @@ import pytest + + from copr_backend.exceptions import CoprSignError, CoprSignNoKeyError, CoprKeygenRequestError + from copr_backend.sign import ( +- get_pubkey, _sign_one, sign_rpms_in_dir, create_user_keys, ++ new_signer, + gpg_hashtype_for_chroot, +- call_sign_bin, + ) ++from copr_backend.constants import DEF_SIGN_BACKEND + + STDOUT = "stdout" + STDERR = "stderr" +@@ -33,6 +33,8 @@ class TestSign(object): + self.opts = Munch(keygen_host="example.com") + self.opts.gently_gpg_sha256 = False + self.opts.sign_domain = "fedorahosted.org" ++ self.opts.sign_backend = DEF_SIGN_BACKEND ++ self.signer = new_signer(self.opts) + + def teardown_method(self, method): + if self.tmp_dir_path: +@@ -60,7 +62,7 @@ class TestSign(object): + mc_handle.returncode = 0 + mc_popen.return_value = mc_handle + +- result = get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) ++ result = self.signer.get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) + assert result == STDOUT + assert mc_popen.call_args[0][0] == ['/bin/sign', '-u', self.usermail, '-p'] + +@@ -70,7 +72,7 @@ class TestSign(object): + mc_popen.side_effect = IOError(STDERR) + + with pytest.raises(CoprSignError): +- get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) ++ self.signer.get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) + + + @mock.patch("copr_backend.sign.time.sleep") +@@ -82,7 +84,7 @@ class TestSign(object): + mc_popen.return_value = mc_handle + + with pytest.raises(CoprSignNoKeyError) as err: +- get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) ++ self.signer.get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) + + assert "There are no gpg keys for user foo in keyring" in str(err) + +@@ -95,7 +97,7 @@ class TestSign(object): + mc_popen.return_value = mc_handle + + with pytest.raises(CoprSignError) as err: +- get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) ++ self.signer.get_pubkey(self.username, self.projectname, MagicMock(), self.opts.sign_domain) + + assert "Failed to get user pubkey" in str(err) + +@@ -108,7 +110,7 @@ class TestSign(object): + + outfile_path = os.path.join(self.tmp_dir_path, "out.pub") + assert not os.path.exists(outfile_path) +- result = get_pubkey(self.username, self.projectname, MagicMock(), ++ result = self.signer.get_pubkey(self.username, self.projectname, MagicMock(), + self.opts.sign_domain, outfile_path) + assert result == STDOUT + assert os.path.exists(outfile_path) +@@ -124,7 +126,7 @@ class TestSign(object): + mc_popen.return_value = mc_handle + + fake_path = "/tmp/pkg.rpm" +- result = _sign_one(fake_path, self.usermail, "sha1", MagicMock()) ++ result = self.signer._sign_one(fake_path, self.usermail, "sha1", MagicMock()) + assert STDOUT, STDERR == result + + expected_cmd = ['/bin/sign', "-4", "-h", "sha1", "-u", self.usermail, +@@ -137,7 +139,7 @@ class TestSign(object): + + fake_path = "/tmp/pkg.rpm" + with pytest.raises(CoprSignError): +- _sign_one(fake_path, self.usermail, "sha256", MagicMock()) ++ self.signer._sign_one(fake_path, self.usermail, "sha256", MagicMock()) + + @mock.patch("copr_backend.sign.time.sleep") + @mock.patch("copr_backend.sign.Popen") +@@ -149,12 +151,11 @@ class TestSign(object): + + fake_path = "/tmp/pkg.rpm" + with pytest.raises(CoprSignError): +- _sign_one(fake_path, self.usermail, "sha256", MagicMock()) ++ self.signer._sign_one(fake_path, self.usermail, "sha256", MagicMock()) + +- @staticmethod + @mock.patch("copr_backend.sign.time.sleep") + @mock.patch("copr_backend.sign.Popen") +- def test_call_sign_bin_repeatedly(mc_popen, _sleep): ++ def test_call_sign_bin_repeatedly(self, mc_popen, _sleep): + """ + Test that we attempt to run /bin/sign multiple times if it returns + non-zero exit status +@@ -163,13 +164,13 @@ class TestSign(object): + mc_handle.communicate.return_value = (STDOUT, STDERR) + mc_handle.returncode = 1 + mc_popen.return_value = mc_handle +- call_sign_bin(cmd=[], log=MagicMock()) ++ self.signer.call_sign_bin(cmd=[], log=MagicMock()) + assert mc_popen.call_count == 3 + + @mock.patch("copr_backend.sign.SafeRequest.send") + def test_create_user_keys(self, mc_request): + mc_request.return_value.status_code = 200 +- create_user_keys(self.username, self.projectname, self.opts) ++ self.signer.create_user_keys(self.username, self.projectname, self.opts) + + assert mc_request.called + expected_call = mock.call( +@@ -183,7 +184,7 @@ class TestSign(object): + def test_create_user_keys_error_1(self, mc_request): + mc_request.side_effect = IOError() + with pytest.raises(CoprKeygenRequestError) as err: +- create_user_keys(self.username, self.projectname, self.opts) ++ self.signer.create_user_keys(self.username, self.projectname, self.opts) + + assert "Failed to create key-pair" in str(err) + +@@ -195,16 +196,16 @@ class TestSign(object): + mc_request.return_value.content = "error: {}".format(code) + + with pytest.raises(CoprKeygenRequestError) as err: +- create_user_keys(self.username, self.projectname, self.opts) ++ self.signer.create_user_keys(self.username, self.projectname, self.opts) + assert "Failed to create key-pair for user: foo, project:bar" in str(err) + +- @mock.patch("copr_backend.sign._sign_one") +- @mock.patch("copr_backend.sign.create_user_keys") +- @mock.patch("copr_backend.sign.get_pubkey") ++ @mock.patch("copr_backend.sign.ObsSign._sign_one") ++ @mock.patch("copr_backend.sign.ObsSign.create_user_keys") ++ @mock.patch("copr_backend.sign.ObsSign.get_pubkey") + def test_sign_rpms_id_dir_nothing(self, mc_gp, mc_cuk, mc_so, + tmp_dir): + # empty target dir doesn't produce error +- sign_rpms_in_dir(self.username, self.projectname, ++ self.signer.sign_rpms_in_dir(self.username, self.projectname, + self.tmp_dir_path, "epel-8-x86_64", self.opts, + log=MagicMock()) + +@@ -212,13 +213,13 @@ class TestSign(object): + assert not mc_cuk.called + assert not mc_so.called + +- @mock.patch("copr_backend.sign._sign_one") +- @mock.patch("copr_backend.sign.create_user_keys") +- @mock.patch("copr_backend.sign.get_pubkey") ++ @mock.patch("copr_backend.sign.ObsSign._sign_one") ++ @mock.patch("copr_backend.sign.ObsSign.create_user_keys") ++ @mock.patch("copr_backend.sign.ObsSign.get_pubkey") + def test_sign_rpms_id_dir_ok(self, mc_gp, mc_cuk, mc_so, + tmp_dir, tmp_files): + +- sign_rpms_in_dir(self.username, self.projectname, ++ self.signer.sign_rpms_in_dir(self.username, self.projectname, + self.tmp_dir_path, "fedora-rawhide-x86_64", + self.opts, log=MagicMock()) + +@@ -234,15 +235,15 @@ class TestSign(object): + assert os.path.join(self.tmp_dir_path, name) in pathes + assert len(pathes) == count + +- @mock.patch("copr_backend.sign._sign_one") +- @mock.patch("copr_backend.sign.create_user_keys") +- @mock.patch("copr_backend.sign.get_pubkey") ++ @mock.patch("copr_backend.sign.ObsSign._sign_one") ++ @mock.patch("copr_backend.sign.ObsSign.create_user_keys") ++ @mock.patch("copr_backend.sign.ObsSign.get_pubkey") + def test_sign_rpms_id_dir_error_on_pubkey( + self, mc_gp, mc_cuk, mc_so, tmp_dir, tmp_files): + + mc_gp.side_effect = CoprSignError("foobar") + with pytest.raises(CoprSignError): +- sign_rpms_in_dir(self.username, self.projectname, ++ self.signer.sign_rpms_in_dir(self.username, self.projectname, + self.tmp_dir_path, "epel-7-x86_64", self.opts, + log=MagicMock()) + +@@ -250,15 +251,15 @@ class TestSign(object): + assert not mc_cuk.called + assert not mc_so.called + +- @mock.patch("copr_backend.sign._sign_one") +- @mock.patch("copr_backend.sign.create_user_keys") +- @mock.patch("copr_backend.sign.get_pubkey") ++ @mock.patch("copr_backend.sign.ObsSign._sign_one") ++ @mock.patch("copr_backend.sign.ObsSign.create_user_keys") ++ @mock.patch("copr_backend.sign.ObsSign.get_pubkey") + def test_sign_rpms_id_dir_no_pub_key( + self, mc_gp, mc_cuk, mc_so, tmp_dir, tmp_files): + + mc_gp.side_effect = CoprSignNoKeyError("foobar") + +- sign_rpms_in_dir(self.username, self.projectname, ++ self.signer.sign_rpms_in_dir(self.username, self.projectname, + self.tmp_dir_path, "rhel-7-x86_64", self.opts, + log=MagicMock()) + +@@ -266,9 +267,9 @@ class TestSign(object): + assert mc_cuk.called + assert mc_so.called + +- @mock.patch("copr_backend.sign._sign_one") +- @mock.patch("copr_backend.sign.create_user_keys") +- @mock.patch("copr_backend.sign.get_pubkey") ++ @mock.patch("copr_backend.sign.ObsSign._sign_one") ++ @mock.patch("copr_backend.sign.ObsSign.create_user_keys") ++ @mock.patch("copr_backend.sign.ObsSign.get_pubkey") + def test_sign_rpms_id_dir_sign_error_one( + self, mc_gp, mc_cuk, mc_so, tmp_dir, tmp_files): + +@@ -276,7 +277,7 @@ class TestSign(object): + None, CoprSignError("foobar"), None + ] + with pytest.raises(CoprSignError): +- sign_rpms_in_dir(self.username, self.projectname, ++ self.signer.sign_rpms_in_dir(self.username, self.projectname, + self.tmp_dir_path, "fedora-36-x86_64", self.opts, + log=MagicMock()) + +@@ -285,15 +286,15 @@ class TestSign(object): + + assert mc_so.called + +- @mock.patch("copr_backend.sign._sign_one") +- @mock.patch("copr_backend.sign.create_user_keys") +- @mock.patch("copr_backend.sign.get_pubkey") ++ @mock.patch("copr_backend.sign.ObsSign._sign_one") ++ @mock.patch("copr_backend.sign.ObsSign.create_user_keys") ++ @mock.patch("copr_backend.sign.ObsSign.get_pubkey") + def test_sign_rpms_id_dir_sign_error_all( + self, mc_gp, mc_cuk, mc_so, tmp_dir, tmp_files): + + mc_so.side_effect = CoprSignError("foobar") + with pytest.raises(CoprSignError): +- sign_rpms_in_dir(self.username, self.projectname, ++ self.signer.sign_rpms_in_dir(self.username, self.projectname, + self.tmp_dir_path, "fedora-36-i386", self.opts, + log=MagicMock()) + |