1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun 5*4882a593Smuzhiyun"""Helper module for GPG signing""" 6*4882a593Smuzhiyunimport os 7*4882a593Smuzhiyun 8*4882a593Smuzhiyunimport bb 9*4882a593Smuzhiyunimport subprocess 10*4882a593Smuzhiyunimport shlex 11*4882a593Smuzhiyun 12*4882a593Smuzhiyunclass LocalSigner(object): 13*4882a593Smuzhiyun """Class for handling local (on the build host) signing""" 14*4882a593Smuzhiyun def __init__(self, d): 15*4882a593Smuzhiyun self.gpg_bin = d.getVar('GPG_BIN') or \ 16*4882a593Smuzhiyun bb.utils.which(os.getenv('PATH'), 'gpg') 17*4882a593Smuzhiyun self.gpg_cmd = [self.gpg_bin] 18*4882a593Smuzhiyun self.gpg_agent_bin = bb.utils.which(os.getenv('PATH'), "gpg-agent") 19*4882a593Smuzhiyun # Without this we see "Cannot allocate memory" errors when running processes in parallel 20*4882a593Smuzhiyun # It needs to be set for any gpg command since any agent launched can stick around in memory 21*4882a593Smuzhiyun # and this parameter must be set. 22*4882a593Smuzhiyun if self.gpg_agent_bin: 23*4882a593Smuzhiyun self.gpg_cmd += ["--agent-program=%s|--auto-expand-secmem" % (self.gpg_agent_bin)] 24*4882a593Smuzhiyun self.gpg_path = d.getVar('GPG_PATH') 25*4882a593Smuzhiyun self.rpm_bin = bb.utils.which(os.getenv('PATH'), "rpmsign") 26*4882a593Smuzhiyun self.gpg_version = self.get_gpg_version() 27*4882a593Smuzhiyun 28*4882a593Smuzhiyun 29*4882a593Smuzhiyun def export_pubkey(self, output_file, keyid, armor=True): 30*4882a593Smuzhiyun """Export GPG public key to a file""" 31*4882a593Smuzhiyun cmd = self.gpg_cmd + ["--no-permission-warning", "--batch", "--yes", "--export", "-o", output_file] 32*4882a593Smuzhiyun if self.gpg_path: 33*4882a593Smuzhiyun cmd += ["--homedir", self.gpg_path] 34*4882a593Smuzhiyun if armor: 35*4882a593Smuzhiyun cmd += ["--armor"] 36*4882a593Smuzhiyun cmd += [keyid] 37*4882a593Smuzhiyun subprocess.check_output(cmd, stderr=subprocess.STDOUT) 38*4882a593Smuzhiyun 39*4882a593Smuzhiyun def sign_rpms(self, files, keyid, passphrase, digest, sign_chunk, fsk=None, fsk_password=None): 40*4882a593Smuzhiyun """Sign RPM files""" 41*4882a593Smuzhiyun 42*4882a593Smuzhiyun cmd = self.rpm_bin + " --addsign --define '_gpg_name %s' " % keyid 43*4882a593Smuzhiyun gpg_args = '--no-permission-warning --batch --passphrase=%s --agent-program=%s|--auto-expand-secmem' % (passphrase, self.gpg_agent_bin) 44*4882a593Smuzhiyun if self.gpg_version > (2,1,): 45*4882a593Smuzhiyun gpg_args += ' --pinentry-mode=loopback' 46*4882a593Smuzhiyun cmd += "--define '_gpg_sign_cmd_extra_args %s' " % gpg_args 47*4882a593Smuzhiyun cmd += "--define '_binary_filedigest_algorithm %s' " % digest 48*4882a593Smuzhiyun if self.gpg_bin: 49*4882a593Smuzhiyun cmd += "--define '__gpg %s' " % self.gpg_bin 50*4882a593Smuzhiyun if self.gpg_path: 51*4882a593Smuzhiyun cmd += "--define '_gpg_path %s' " % self.gpg_path 52*4882a593Smuzhiyun if fsk: 53*4882a593Smuzhiyun cmd += "--signfiles --fskpath %s " % fsk 54*4882a593Smuzhiyun if fsk_password: 55*4882a593Smuzhiyun cmd += "--define '_file_signing_key_password %s' " % fsk_password 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun # Sign in chunks 58*4882a593Smuzhiyun for i in range(0, len(files), sign_chunk): 59*4882a593Smuzhiyun subprocess.check_output(shlex.split(cmd + ' '.join(files[i:i+sign_chunk])), stderr=subprocess.STDOUT) 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun def detach_sign(self, input_file, keyid, passphrase_file, passphrase=None, armor=True, output_suffix=None, use_sha256=False): 62*4882a593Smuzhiyun """Create a detached signature of a file""" 63*4882a593Smuzhiyun 64*4882a593Smuzhiyun if passphrase_file and passphrase: 65*4882a593Smuzhiyun raise Exception("You should use either passphrase_file of passphrase, not both") 66*4882a593Smuzhiyun 67*4882a593Smuzhiyun cmd = self.gpg_cmd + ['--detach-sign', '--no-permission-warning', '--batch', 68*4882a593Smuzhiyun '--no-tty', '--yes', '--passphrase-fd', '0', '-u', keyid] 69*4882a593Smuzhiyun 70*4882a593Smuzhiyun if self.gpg_path: 71*4882a593Smuzhiyun cmd += ['--homedir', self.gpg_path] 72*4882a593Smuzhiyun if armor: 73*4882a593Smuzhiyun cmd += ['--armor'] 74*4882a593Smuzhiyun if output_suffix: 75*4882a593Smuzhiyun cmd += ['-o', input_file + "." + output_suffix] 76*4882a593Smuzhiyun if use_sha256: 77*4882a593Smuzhiyun cmd += ['--digest-algo', "SHA256"] 78*4882a593Smuzhiyun 79*4882a593Smuzhiyun #gpg > 2.1 supports password pipes only through the loopback interface 80*4882a593Smuzhiyun #gpg < 2.1 errors out if given unknown parameters 81*4882a593Smuzhiyun if self.gpg_version > (2,1,): 82*4882a593Smuzhiyun cmd += ['--pinentry-mode', 'loopback'] 83*4882a593Smuzhiyun 84*4882a593Smuzhiyun cmd += [input_file] 85*4882a593Smuzhiyun 86*4882a593Smuzhiyun try: 87*4882a593Smuzhiyun if passphrase_file: 88*4882a593Smuzhiyun with open(passphrase_file) as fobj: 89*4882a593Smuzhiyun passphrase = fobj.readline(); 90*4882a593Smuzhiyun 91*4882a593Smuzhiyun job = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE) 92*4882a593Smuzhiyun (_, stderr) = job.communicate(passphrase.encode("utf-8")) 93*4882a593Smuzhiyun 94*4882a593Smuzhiyun if job.returncode: 95*4882a593Smuzhiyun bb.fatal("GPG exited with code %d: %s" % (job.returncode, stderr.decode("utf-8"))) 96*4882a593Smuzhiyun 97*4882a593Smuzhiyun except IOError as e: 98*4882a593Smuzhiyun bb.error("IO error (%s): %s" % (e.errno, e.strerror)) 99*4882a593Smuzhiyun raise Exception("Failed to sign '%s'" % input_file) 100*4882a593Smuzhiyun 101*4882a593Smuzhiyun except OSError as e: 102*4882a593Smuzhiyun bb.error("OS error (%s): %s" % (e.errno, e.strerror)) 103*4882a593Smuzhiyun raise Exception("Failed to sign '%s" % input_file) 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun 106*4882a593Smuzhiyun def get_gpg_version(self): 107*4882a593Smuzhiyun """Return the gpg version as a tuple of ints""" 108*4882a593Smuzhiyun try: 109*4882a593Smuzhiyun cmd = self.gpg_cmd + ["--version", "--no-permission-warning"] 110*4882a593Smuzhiyun ver_str = subprocess.check_output(cmd).split()[2].decode("utf-8") 111*4882a593Smuzhiyun return tuple([int(i) for i in ver_str.split("-")[0].split('.')]) 112*4882a593Smuzhiyun except subprocess.CalledProcessError as e: 113*4882a593Smuzhiyun bb.fatal("Could not get gpg version: %s" % e) 114*4882a593Smuzhiyun 115*4882a593Smuzhiyun 116*4882a593Smuzhiyun def verify(self, sig_file, valid_sigs = ''): 117*4882a593Smuzhiyun """Verify signature""" 118*4882a593Smuzhiyun cmd = self.gpg_cmd + ["--verify", "--no-permission-warning", "--status-fd", "1"] 119*4882a593Smuzhiyun if self.gpg_path: 120*4882a593Smuzhiyun cmd += ["--homedir", self.gpg_path] 121*4882a593Smuzhiyun 122*4882a593Smuzhiyun cmd += [sig_file] 123*4882a593Smuzhiyun status = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 124*4882a593Smuzhiyun # Valid if any key matches if unspecified 125*4882a593Smuzhiyun if not valid_sigs: 126*4882a593Smuzhiyun ret = False if status.returncode else True 127*4882a593Smuzhiyun return ret 128*4882a593Smuzhiyun 129*4882a593Smuzhiyun import re 130*4882a593Smuzhiyun goodsigs = [] 131*4882a593Smuzhiyun sigre = re.compile(r'^\[GNUPG:\] GOODSIG (\S+)\s(.*)$') 132*4882a593Smuzhiyun for l in status.stdout.decode("utf-8").splitlines(): 133*4882a593Smuzhiyun s = sigre.match(l) 134*4882a593Smuzhiyun if s: 135*4882a593Smuzhiyun goodsigs += [s.group(1)] 136*4882a593Smuzhiyun 137*4882a593Smuzhiyun for sig in valid_sigs.split(): 138*4882a593Smuzhiyun if sig in goodsigs: 139*4882a593Smuzhiyun return True 140*4882a593Smuzhiyun if len(goodsigs): 141*4882a593Smuzhiyun bb.warn('No accepted signatures found. Good signatures found: %s.' % ' '.join(goodsigs)) 142*4882a593Smuzhiyun return False 143*4882a593Smuzhiyun 144*4882a593Smuzhiyun 145*4882a593Smuzhiyundef get_signer(d, backend): 146*4882a593Smuzhiyun """Get signer object for the specified backend""" 147*4882a593Smuzhiyun # Use local signing by default 148*4882a593Smuzhiyun if backend == 'local': 149*4882a593Smuzhiyun return LocalSigner(d) 150*4882a593Smuzhiyun else: 151*4882a593Smuzhiyun bb.fatal("Unsupported signing backend '%s'" % backend) 152