[PATCH 2/3] New lib module for handling GPG signing

Markus Lehtonen
2016-01-11T16:13:22Z

Add a new Python module (oe.gpg_sign) for handling GPG signing
operations, i.e. currently package and package feed signing. The
purpose is to be able to more easily support various signing backends.
Currently, only local on-the-build-host signing is implemented.

[YOCTO #8755]

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
---
meta/classes/sign_package_feed.bbclass | 6 +++
meta/classes/sign_rpm.bbclass | 47 +++++----------------
meta/lib/oe/gpg_sign.py | 76 ++++++++++++++++++++++++++++++++++
meta/lib/oe/package_manager.py | 31 +++++---------
meta/recipes-core/meta/signing-keys.bb | 26 ++++++------
5 files changed, 116 insertions(+), 70 deletions(-)
create mode 100644 meta/lib/oe/gpg_sign.py

diff --git a/meta/classes/sign_package_feed.bbclass b/meta/classes/sign_package_feed.bbclass
index d89bc0b..d5df8af 100644
--- a/meta/classes/sign_package_feed.bbclass
+++ b/meta/classes/sign_package_feed.bbclass
@@ -6,6 +6,10 @@
# Path to a file containing the passphrase of the signing key.
# PACKAGE_FEED_GPG_NAME
# Name of the key to sign with. May be key id or key name.
+# PACKAGE_FEED_GPG_BACKEND
+# Optional variable for specifying the backend to use for signing.
+# Currently the only available option is 'local', i.e. local signing
+# on the build host.
# GPG_BIN
# Optional variable for specifying the gpg binary/wrapper to use for
# signing.
@@ -15,6 +19,8 @@
inherit sanity

PACKAGE_FEED_SIGN = '1'
+PACKAGE_FEED_GPG_BACKEND ?= 'local'
+

python () {
# Check sanity of configuration
diff --git a/meta/classes/sign_rpm.bbclass b/meta/classes/sign_rpm.bbclass
index 7906b64..8bcabee 100644
--- a/meta/classes/sign_rpm.bbclass
+++ b/meta/classes/sign_rpm.bbclass
@@ -5,6 +5,10 @@
# Path to a file containing the passphrase of the signing key.
# RPM_GPG_NAME
# Name of the key to sign with. May be key id or key name.
+# RPM_GPG_BACKEND
+# Optional variable for specifying the backend to use for signing.
+# Currently the only available option is 'local', i.e. local signing
+# on the build host.
# GPG_BIN
# Optional variable for specifying the gpg binary/wrapper to use for
# signing.
@@ -14,6 +18,7 @@
inherit sanity

RPM_SIGN_PACKAGES='1'
+RPM_GPG_BACKEND ?= 'local'

python () {
@@ -27,47 +32,17 @@ python () {
'RPM-GPG-PUBKEY'))
}

-
-def rpmsign_wrapper(d, files, passphrase, gpg_name=None):
- import pexpect
-
- # Find the correct rpm binary
- rpm_bin_path = d.getVar('STAGING_BINDIR_NATIVE', True) + '/rpm'
- cmd = rpm_bin_path + " --addsign --define '_gpg_name %s' " % gpg_name
- if d.getVar('GPG_BIN', True):
- cmd += "--define '%%__gpg %s' " % d.getVar('GPG_BIN', True)
- if d.getVar('GPG_PATH', True):
- cmd += "--define '_gpg_path %s' " % d.getVar('GPG_PATH', True)
- cmd += ' '.join(files)
-
- # Need to use pexpect for feeding the passphrase
- proc = pexpect.spawn(cmd)
- try:
- proc.expect_exact('Enter pass phrase:', timeout=15)
- proc.sendline(passphrase)
- proc.expect(pexpect.EOF, timeout=900)
- proc.close()
- except pexpect.TIMEOUT as err:
- bb.warn('rpmsign timeout: %s' % err)
- proc.terminate()
- else:
- if os.WEXITSTATUS(proc.status) or not os.WIFEXITED(proc.status):
- bb.warn('rpmsign failed: %s' % proc.before.strip())
- return proc.exitstatus
-
-
python sign_rpm () {
import glob
+ from oe.gpg_sign import get_signer

- with open(d.getVar("RPM_GPG_PASSPHRASE_FILE", True)) as fobj:
- rpm_gpg_passphrase = fobj.readlines()[0].rstrip('\n')
-
- rpm_gpg_name = (d.getVar("RPM_GPG_NAME", True) or "")
-
+ signer = get_signer(d,
+ d.getVar('RPM_GPG_BACKEND', True),
+ d.getVar('RPM_GPG_NAME', True),
+ d.getVar('RPM_GPG_PASSPHRASE_FILE', True))
rpms = glob.glob(d.getVar('RPM_PKGWRITEDIR', True) + '/*')

- if rpmsign_wrapper(d, rpms, rpm_gpg_passphrase, rpm_gpg_name) != 0:
- raise bb.build.FuncFailed("RPM signing failed")
+ signer.sign_rpms(rpms)
}

do_package_index[depends] += "signing-keys:do_export_public_keys"
diff --git a/meta/lib/oe/gpg_sign.py b/meta/lib/oe/gpg_sign.py
new file mode 100644
index 0000000..55abad8
--- /dev/null
+++ b/meta/lib/oe/gpg_sign.py
@@ -0,0 +1,76 @@
+"""Helper module for GPG signing"""
+import os
+
+import bb
+import oe.utils
+
+class LocalSigner(object):
+ """Class for handling local (on the build host) signing"""
+ def __init__(self, d, keyid, passphrase_file):
+ self.keyid = keyid
+ self.passphrase_file = passphrase_file
+ self.gpg_bin = d.getVar('GPG_BIN', True) or \
+ bb.utils.which(os.getenv('PATH'), 'gpg')
+ self.gpg_path = d.getVar('GPG_PATH', True)
+ self.rpm_bin = bb.utils.which(os.getenv('PATH'), "rpm")
+
+ def export_pubkey(self, output_file):
+ """Export GPG public key to a file"""
+ cmd = '%s --batch --yes --export --armor -o %s ' % \
+ (self.gpg_bin, output_file)
+ if self.gpg_path:
+ cmd += "--homedir %s " % self.gpg_path
+ cmd += self.keyid
+ status, output = oe.utils.getstatusoutput(cmd)
+ if status:
+ raise bb.build.FuncFailed('Failed to export gpg public key (%s): %s' %
+ (self.keyid, output))
+
+ def sign_rpms(self, files):
+ """Sign RPM files"""
+ import pexpect
+
+ cmd = self.rpm_bin + " --addsign --define '_gpg_name %s' " % self.keyid
+ if self.gpg_bin:
+ cmd += "--define '%%__gpg %s' " % self.gpg_bin
+ if self.gpg_path:
+ cmd += "--define '_gpg_path %s' " % self.gpg_path
+ cmd += ' '.join(files)
+
+ # Need to use pexpect for feeding the passphrase
+ proc = pexpect.spawn(cmd)
+ try:
+ proc.expect_exact('Enter pass phrase:', timeout=15)
+ with open(self.passphrase_file) as fobj:
+ proc.sendline(fobj.readline().rstrip('\n'))
+ proc.expect(pexpect.EOF, timeout=900)
+ proc.close()
+ except pexpect.TIMEOUT as err:
+ bb.error('rpmsign timeout: %s' % err)
+ proc.terminate()
+ if os.WEXITSTATUS(proc.status) or not os.WIFEXITED(proc.status):
+ bb.error('rpmsign failed: %s' % proc.before.strip())
+ raise bb.build.FuncFailed("Failed to sign RPM packages")
+
+ def detach_sign(self, input_file):
+ """Create a detached signature of a file"""
+ cmd = "%s --detach-sign --armor --batch --no-tty --yes " \
+ "--passphrase-file '%s' -u '%s' " % \
+ (self.gpg_bin, self.passphrase_file, self.keyid)
+ if self.gpg_path:
+ gpg_cmd += "--homedir %s " % self.gpg_path
+ cmd += input_file
+ status, output = oe.utils.getstatusoutput(cmd)
+ if status:
+ raise bb.build.FuncFailed("Failed to create signature for '%s': %s" %
+ (input_file, output))
+
+
+def get_signer(d, backend, keyid, passphrase_file):
+ """Get signer object for the specified backend"""
+ # Use local signing by default
+ if backend == 'local':
+ return LocalSigner(d, keyid, passphrase_file)
+ else:
+ bb.fatal("Unsupported signing backend '%s'" % backend)
+
diff --git a/meta/lib/oe/package_manager.py b/meta/lib/oe/package_manager.py
index 32afeaf..c7df4bc 100644
--- a/meta/lib/oe/package_manager.py
+++ b/meta/lib/oe/package_manager.py
@@ -9,6 +9,7 @@ import bb
import tempfile
import oe.utils
import string
+from oe.gpg_sign import get_signer

# this can be used by all PM backends to create the index files in parallel
def create_index(arg):
@@ -109,16 +110,14 @@ class RpmIndexer(Indexer):

rpm_createrepo = bb.utils.which(os.getenv('PATH'), "createrepo")
if self.d.getVar('PACKAGE_FEED_SIGN', True) == '1':
- pkgfeed_gpg_name = self.d.getVar('PACKAGE_FEED_GPG_NAME', True)
- pkgfeed_gpg_pass = self.d.getVar('PACKAGE_FEED_GPG_PASSPHRASE_FILE', True)
+ signer = get_signer(self.d,
+ self.d.getVar('PACKAGE_FEED_GPG_BACKEND', True),
+ self.d.getVar('PACKAGE_FEED_GPG_NAME', True),
+ self.d.getVar('PACKAGE_FEED_GPG_PASSPHRASE_FILE', True))
else:
- pkgfeed_gpg_name = None
- pkgfeed_gpg_pass = None
- gpg_bin = self.d.getVar('GPG_BIN', True) or \
- bb.utils.which(os.getenv('PATH'), "gpg")
-
+ signer = None
index_cmds = []
- repo_sign_cmds = []
+ repomd_files = []
rpm_dirs_found = False
for arch in archs:
dbpath = os.path.join(self.d.getVar('WORKDIR', True), 'rpmdb', arch)
@@ -130,15 +129,7 @@ class RpmIndexer(Indexer):

index_cmds.append("%s --dbpath %s --update -q %s" % \
(rpm_createrepo, dbpath, arch_dir))
- if pkgfeed_gpg_name:
- repomd_file = os.path.join(arch_dir, 'repodata', 'repomd.xml')
- gpg_cmd = "%s --detach-sign --armor --batch --no-tty --yes " \
- "--passphrase-file '%s' -u '%s' " % \
- (gpg_bin, pkgfeed_gpg_pass, pkgfeed_gpg_name)
- if self.d.getVar('GPG_PATH', True):
- gpg_cmd += "--homedir %s " % self.d.getVar('GPG_PATH', True)
- gpg_cmd += repomd_file
- repo_sign_cmds.append(gpg_cmd)
+ repomd_files.append(os.path.join(arch_dir, 'repodata', 'repomd.xml'))

rpm_dirs_found = True

@@ -151,9 +142,9 @@ class RpmIndexer(Indexer):
if result:
bb.fatal('%s' % ('\n'.join(result)))
# Sign repomd
- result = oe.utils.multiprocess_exec(repo_sign_cmds, create_index)
- if result:
- bb.fatal('%s' % ('\n'.join(result)))
+ if signer:
+ for repomd in repomd_files:
+ signer.detach_sign(repomd)
# Copy pubkey(s) to repo
distro_version = self.d.getVar('DISTRO_VERSION', True) or "oe.0"
if self.d.getVar('RPM_SIGN_PACKAGES', True) == '1':
diff --git a/meta/recipes-core/meta/signing-keys.bb b/meta/recipes-core/meta/signing-keys.bb
index cc401f3..d7aa79d 100644
--- a/meta/recipes-core/meta/signing-keys.bb
+++ b/meta/recipes-core/meta/signing-keys.bb
@@ -20,26 +20,24 @@ do_populate_sysroot[noexec] = "1"

EXCLUDE_FROM_WORLD = "1"

-def export_gpg_pubkey(d, keyid, path):
- import bb
- gpg_bin = d.getVar('GPG_BIN', True) or \
- bb.utils.which(os.getenv('PATH'), "gpg")
- cmd = '%s --batch --yes --export --armor -o %s %s' % \
- (gpg_bin, path, keyid)
- status, output = oe.utils.getstatusoutput(cmd)
- if status:
- raise bb.build.FuncFailed('Failed to export gpg public key (%s): %s' %
- (keyid, output))

python do_export_public_keys () {
+ from oe.gpg_sign import get_signer
+
if d.getVar("RPM_SIGN_PACKAGES", True):
# Export public key of the rpm signing key
- export_gpg_pubkey(d, d.getVar("RPM_GPG_NAME", True),
- d.getVar('RPM_GPG_PUBKEY', True))
+ signer = get_signer(d,
+ d.getVar('RPM_GPG_BACKEND', True),
+ d.getVar('RPM_GPG_NAME', True),
+ d.getVar('RPM_GPG_PASSPHRASE_FILE', True))
+ signer.export_pubkey(d.getVar('RPM_GPG_PUBKEY', True))

if d.getVar('PACKAGE_FEED_SIGN', True) == '1':
# Export public key of the feed signing key
- export_gpg_pubkey(d, d.getVar("PACKAGE_FEED_GPG_NAME", True),
- d.getVar('PACKAGE_FEED_GPG_PUBKEY', True))
+ signer = get_signer(d,
+ d.getVar('PACKAGE_FEED_GPG_BACKEND', True),
+ d.getVar('PACKAGE_FEED_GPG_NAME', True),
+ d.getVar('PACKAGE_FEED_GPG_PASSPHRASE_FILE', True))
+ signer.export_pubkey(d.getVar('PACKAGE_FEED_GPG_PUBKEY', True))
}
addtask do_export_public_keys before do_build
--
2.1.4