From 3caf2dfdef74d059a4e73ca0ff029a01d720fbf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20TERROT?= Date: Sun, 25 Apr 2021 18:03:48 +0200 Subject: [PATCH] signature helpers --- bin/gpgsign.py | 278 +++++++++++++++++++++++++++++++++++++++++++++++++ bin/jarsign.sh | 55 ++++++++++ 2 files changed, 333 insertions(+) create mode 100644 bin/gpgsign.py create mode 100644 bin/jarsign.sh diff --git a/bin/gpgsign.py b/bin/gpgsign.py new file mode 100644 index 0000000..29d78e1 --- /dev/null +++ b/bin/gpgsign.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" + + gpgsign.py - Helper to "GPG" sign Mylyn Gitea Release files + + Limitations: + Too many limitations to be listed here. + + Usage: + 1. Set GPG configuration in signature.conf file + 2. run gpgsign.py + + Caveats: + Must be run AFTER jarsign.sh to avoid gpg signature modification + + Generated files are similar to maven-gpg-plugin ones (default configuration) + +""" +import logging +import subprocess +from pathlib import Path, PurePath +from dataclasses import dataclass +from typing import Tuple +from enum import Enum +import configparser + + + +# pylint: disable=logging-fstring-interpolation, line-too-long + +__PROG__ = "gpgsign" + +WORKSPACEDIR = Path(__file__).parent.parent # /bin/gpgsign.py +ASC_DIRECTORY = "gpg" + +class GPGLockMode(Enum): + ONCE = "once" + MULTIPLE = "multiple" + NEVER = "never" +@dataclass +class GPGConfiguration: + keyname: str + passphrase: str = "" + verbose:bool = False + skip: bool = False + interactive : bool = True + executable : Path = None + use_agent: bool = False + home_dir: str = "" + secret_keyring : str = "" + public_keyring : str = "" + lock_mode : GPGLockMode = None # gpg lock mode, could be : None -> lock mode not used, "once", "multiple", "never" => SyntaxError + args = "" # list, of additional arguments as list or tuple or comma separated as string + +@dataclass +class Configuration: + gpg: GPGConfiguration + +class GPGSigner: + """Compute signature. + """ + SIGNATURE_EXTENSION : str = ".asc" + + def __init__(self, gpg:GPGConfiguration , output_dir: Path ): + self.output_dir = output_dir + self.gpg = gpg + self.interactive = False + + def generate_signature_for_file(self, file: Path, signature: Path) -> int: + """Generate a detached signature file for provided file. + + Args: + file: Path of file to be signed + signature: Path of file where the signature will be written. + + Returns: + return code of signature application + """ + # Configure command line + cmd = ['gpg'] + if self.gpg.executable: + cmd = [str(self.gpg.executable)] + + if self.gpg.args: + if isinstance(self.gpg.args, (list, tuple)): + cmd += self.gpg.args + else: + cmd += self.gpg.args.split(",") + + if self.gpg.verbose: + cmd.append("-v") + + if self.gpg.home_dir: + cmd += [ "--homedir", self.gpg.home_dir] + + cmd.append(f"--{'' if self.gpg.use_agent else 'no-'}use-agent") + + # required for subprocess input: + # 1. to disable input when self.gpg.passphrase is "" (run is waiting None) + # 2. run is waiting bytes for input + in_passphrase=None + if self.gpg.passphrase: + cmd += ["--batch", "--passphrase-fd", "0"] + in_passphrase = bytes(self.gpg.passphrase,"UTF-8") + + if self.gpg.keyname: + cmd += ["--local-user", self.gpg.keyname] + + cmd.append("--armor") + cmd.append("--detach-sign") + + if not self.gpg.interactive: + cmd.append("--no-tty") # + + if self.gpg.secret_keyring or self.gpg.public_keyring: + cmd.append("--no-default-keyring") + + if self.gpg.secret_keyring: + cmd += ["--secret-keyring", self.gpg.secret_keyring] + + if self.gpg.public_keyring: + cmd +=[ "--keyring", self.gpg.public_keyring] + + if self.gpg.lock_mode: + cmd.append(f"--lock-{self.gpg.lock_mode}") + + cmd += ["--output", str(signature)] + + cmd.append(str(file)) + + logging.debug(f"GPGSigner> {cmd}") + + # Execute command line + completed_process = subprocess.run(cmd, input=in_passphrase) + + logging.debug(str(completed_process)) + + return completed_process.returncode + + def generate_signature_for_artifact (self, file: Path) -> int: + """Create a detached signature file for the provided file + managing valide signature file destination. + """ + # Setup file and directory for signature + + signature = file.with_suffix ( file.suffix + GPGSigner.SIGNATURE_EXTENSION) + + if self.output_dir: + + # ugly but working way to get common parts + for i,part in enumerate(signature.parts): + try: + if part == self.output_dir.parts[i]: + continue + except IndexError: # output_dir is smaller than signature + pass + break + + common_dir = Path(*signature.parts[:i]) + + if common_dir: + signature = self.output_dir / signature.relative_to(common_dir) + + # Create destination directory + logging.debug(f"GPGSigner> artifact:{file}") + logging.debug(f"GPGSigner> signature:{signature}") + if not signature.parent.is_dir(): + signature.parent.mkdir(parents=True, exist_ok=True) + + if signature.exists(): + signature.unlink() + + # Generate signature file + return self.generate_signature_for_file(file, signature) + + def get_passphrase(self): + return "" + +class Project: + """ Project as list of artifacts """ + ARTIFACTS_EXTENSIONS = ('.jar', '.xml', '.md') + DEFAULT_EXCLUDES = ('.asc', ".gpg", ".sig", ".git", ".externalToolBuilders") + + def __init__(self, projectdir): + self.projectdir = projectdir + logging.debug(f"Project> {projectdir}") + + @property + def name(self): + return self.projectdir.name + + def get_attached_artifacts(self, iter_in_dir :Path = None) -> Path: + """ + Yields: + artifact + """ + iter_in_dir = self.projectdir if iter_in_dir is None else iter_in_dir + for e in iter_in_dir.iterdir(): + if e.is_dir() : + logging.debug(f"Project.get_artifacts> entering in {str(e)}") + yield from self.get_attached_artifacts(e) + if e.suffix.lower() in Project.DEFAULT_EXCLUDES: + logging.debug(f"Project.get_artifacts> excluding {e}") + continue + if e.suffix.lower() in Project.ARTIFACTS_EXTENSIONS or e.name == 'LICENSE': + logging.debug(f"Project.get_artifacts> {e}") + yield e + +class GPGSignAttached: + """Sign artifacts attached to the project.""" + def __init__(self, project: Project, config: Configuration): + self.project = project + self.config = config + + def execute(self): + if self.config.gpg.skip: + logging.warning(f"GPGSign> Skipping GPG Signature for {self.project.name}") + return + + logging.info(f"GPGSign> Signing {self.project.name} artifacts") + signer = GPGSigner(config.gpg, self.project.projectdir / ASC_DIRECTORY) + for artifact in self.project.get_attached_artifacts(): + signer.generate_signature_for_artifact(artifact) + +if __name__ == "__main__": + # pylint: disable=invalid-name + import argparse + import sys + import os + # backup working directory + OLDPWD = os.path.curdir + + # Configure the command line arguments parser + parser = argparse.ArgumentParser(prog=__PROG__, + description=__PROG__ + " Sign (Java + GPG) updatesite artifacts", + prefix_chars="-+") + + parser.add_argument("-c", "--config-file", + help="Configuration file (default /signature.conf", + dest="config_file", + default=str((WORKSPACEDIR / "signature.conf").absolute()), + required=False) + + parser.add_argument("-x", + help="Active 'debug' level for logging", + dest='debug', + action='store_true', + default=False, + required=False) + + # Parse the command line + args = parser.parse_args() + + # Configuration file + config_ini = configparser.ConfigParser() + config_ini.read(args.config_file) + config = Configuration( + gpg=GPGConfiguration( + skip = config_ini.getboolean('GPG','skip', fallback=True), + verbose = config_ini.getboolean('GPG','verbose', fallback=False), + keyname = config_ini.get('GPG', 'keyname', fallback=None), + passphrase= config_ini.get('GPG','passphrase', fallback=None), + ) + ) + + + # Configure logging + # see https://docs.python.org/2/howto/logging.html + logging.basicConfig( + format='%(asctime)s:%(levelname)s:%(message)s' if args.debug else '%(levelname)s:%(message)s', + level=logging.DEBUG if args.debug else logging.INFO) + + siteupdate_project = Project(WORKSPACEDIR / "io.gitea.mylyn.updatesite") + GPGSignAttached(siteupdate_project, config).execute() + + sys.exit(0) + + # vim: tabstop=4:shiftwidth=4:expandtab diff --git a/bin/jarsign.sh b/bin/jarsign.sh new file mode 100644 index 0000000..5a0a822 --- /dev/null +++ b/bin/jarsign.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Helper script to sign Mylyn Gitea eclipse plugin +# +# Usage: +# 1. Configure the .sign.conf file +# 2. jarsign.sh [-x] +# +# Inspired from https://nirmalsasidharan.wordpress.com/2010/09/04/signing_eclipse_plugins/ +# + +# set debug +case "x$1" in "x-x" ) shift ; set -x ;; esac + +# +# Sign all "not already signed" jar in a directory +# usage: jarsign_dir +# +jarsign_dir() { + for jarfile in ${1}/*.jar ; do + [ -e "$jarfile" ] || continue + unset result + case ${result:-$(jarsigner -verify -verbose ${opt_keystore} ${opt_storepass} ${opt_alias} ${jarfile} ${KEYALIAS} | egrep 'jar is unsigned|jar verified')} in + 'jar is unsigned.' ) + jarsigner ${opt_keystore} ${opt_storepass} -verbose ${opt_alias} ${jarfile} ${KEYALIAS} ;; + 'jar verified.' ) + echo "$jarfile already signed" + ;; + * ) + echo "***ERROR Raised by ${jarfile} signature verification..." + echo $result + ;; + esac + done +} + +WORKSPACEDIR=$(realpath "$(dirname $0)/.." ) +SIGNPARAMS=${WORKSPACEDIR}/.sign.conf +if [ -x $SIGNPARAMS ] ; then + source $SIGNPARAMS +else + echo "Missing configuration file" && exit 1 +fi + +if (( $JARSIGN == 1)) ; then + [ -f "${KEYSTORE}" ] && opt_keystore="-keystore ${KEYSTORE}" + [ -n "${STOREPASS}" ] && opt_storepass="-storepass ${STOREPASS}" + + jarsign_dir ${WORKSPACEDIR}/io.gitea.mylyn.updatesite + jarsign_dir ${WORKSPACEDIR}/io.gitea.mylyn.updatesite/features + jarsign_dir ${WORKSPACEDIR}/io.gitea.mylyn.updatesite/plugins + + +fi +