1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# SPDX-License-Identifier: MIT 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun 5*4882a593Smuzhiyunfrom oeqa.selftest.case import OESelftestTestCase 6*4882a593Smuzhiyunfrom oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars, create_temp_layer 7*4882a593Smuzhiyunimport os 8*4882a593Smuzhiyunimport oe 9*4882a593Smuzhiyunimport glob 10*4882a593Smuzhiyunimport re 11*4882a593Smuzhiyunimport shutil 12*4882a593Smuzhiyunimport tempfile 13*4882a593Smuzhiyunfrom contextlib import contextmanager 14*4882a593Smuzhiyunfrom oeqa.utils.ftools import write_file 15*4882a593Smuzhiyun 16*4882a593Smuzhiyun 17*4882a593Smuzhiyunclass Signing(OESelftestTestCase): 18*4882a593Smuzhiyun 19*4882a593Smuzhiyun gpg_dir = "" 20*4882a593Smuzhiyun pub_key_path = "" 21*4882a593Smuzhiyun secret_key_path = "" 22*4882a593Smuzhiyun 23*4882a593Smuzhiyun def setup_gpg(self): 24*4882a593Smuzhiyun bitbake('gnupg-native -c addto_recipe_sysroot') 25*4882a593Smuzhiyun 26*4882a593Smuzhiyun self.gpg_dir = tempfile.mkdtemp(prefix="oeqa-signing-") 27*4882a593Smuzhiyun self.track_for_cleanup(self.gpg_dir) 28*4882a593Smuzhiyun 29*4882a593Smuzhiyun self.pub_key_path = os.path.join(self.testlayer_path, 'files', 'signing', "key.pub") 30*4882a593Smuzhiyun self.secret_key_path = os.path.join(self.testlayer_path, 'files', 'signing', "key.secret") 31*4882a593Smuzhiyun 32*4882a593Smuzhiyun nsysroot = get_bb_var("RECIPE_SYSROOT_NATIVE", "gnupg-native") 33*4882a593Smuzhiyun 34*4882a593Smuzhiyun runCmd('gpg --agent-program=`which gpg-agent`\|--auto-expand-secmem --batch --homedir %s --import %s %s' % (self.gpg_dir, self.pub_key_path, self.secret_key_path), native_sysroot=nsysroot) 35*4882a593Smuzhiyun return nsysroot + get_bb_var("bindir_native") 36*4882a593Smuzhiyun 37*4882a593Smuzhiyun 38*4882a593Smuzhiyun @contextmanager 39*4882a593Smuzhiyun def create_new_builddir(self, builddir, newbuilddir): 40*4882a593Smuzhiyun bb.utils.mkdirhier(newbuilddir) 41*4882a593Smuzhiyun oe.path.copytree(builddir + "/conf", newbuilddir + "/conf") 42*4882a593Smuzhiyun oe.path.copytree(builddir + "/cache", newbuilddir + "/cache") 43*4882a593Smuzhiyun 44*4882a593Smuzhiyun origenv = os.environ.copy() 45*4882a593Smuzhiyun 46*4882a593Smuzhiyun for e in os.environ: 47*4882a593Smuzhiyun if builddir + "/" in os.environ[e]: 48*4882a593Smuzhiyun os.environ[e] = os.environ[e].replace(builddir + "/", newbuilddir + "/") 49*4882a593Smuzhiyun if os.environ[e].endswith(builddir): 50*4882a593Smuzhiyun os.environ[e] = os.environ[e].replace(builddir, newbuilddir) 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun os.chdir(newbuilddir) 53*4882a593Smuzhiyun try: 54*4882a593Smuzhiyun yield 55*4882a593Smuzhiyun finally: 56*4882a593Smuzhiyun for e in origenv: 57*4882a593Smuzhiyun os.environ[e] = origenv[e] 58*4882a593Smuzhiyun os.chdir(builddir) 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun def test_signing_packages(self): 61*4882a593Smuzhiyun """ 62*4882a593Smuzhiyun Summary: Test that packages can be signed in the package feed 63*4882a593Smuzhiyun Expected: Package should be signed with the correct key 64*4882a593Smuzhiyun Expected: Images can be created from signed packages 65*4882a593Smuzhiyun Product: oe-core 66*4882a593Smuzhiyun Author: Daniel Istrate <daniel.alexandrux.istrate@intel.com> 67*4882a593Smuzhiyun Author: Alexander Kanavin <alex.kanavin@gmail.com> 68*4882a593Smuzhiyun AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com> 69*4882a593Smuzhiyun """ 70*4882a593Smuzhiyun import oe.packagedata 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun self.setup_gpg() 73*4882a593Smuzhiyun 74*4882a593Smuzhiyun package_classes = get_bb_var('PACKAGE_CLASSES') 75*4882a593Smuzhiyun if 'package_rpm' not in package_classes: 76*4882a593Smuzhiyun self.skipTest('This test requires RPM Packaging.') 77*4882a593Smuzhiyun 78*4882a593Smuzhiyun test_recipe = 'ed' 79*4882a593Smuzhiyun 80*4882a593Smuzhiyun feature = 'INHERIT += "sign_rpm"\n' 81*4882a593Smuzhiyun feature += 'RPM_GPG_PASSPHRASE = "test123"\n' 82*4882a593Smuzhiyun feature += 'RPM_GPG_NAME = "testuser"\n' 83*4882a593Smuzhiyun feature += 'GPG_PATH = "%s"\n' % self.gpg_dir 84*4882a593Smuzhiyun 85*4882a593Smuzhiyun self.write_config(feature) 86*4882a593Smuzhiyun 87*4882a593Smuzhiyun bitbake('-c clean %s' % test_recipe) 88*4882a593Smuzhiyun bitbake('-f -c package_write_rpm %s' % test_recipe) 89*4882a593Smuzhiyun 90*4882a593Smuzhiyun self.add_command_to_tearDown('bitbake -c clean %s' % test_recipe) 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun needed_vars = ['PKGDATA_DIR', 'DEPLOY_DIR_RPM', 'PACKAGE_ARCH', 'STAGING_BINDIR_NATIVE'] 93*4882a593Smuzhiyun bb_vars = get_bb_vars(needed_vars, test_recipe) 94*4882a593Smuzhiyun pkgdatadir = bb_vars['PKGDATA_DIR'] 95*4882a593Smuzhiyun pkgdata = oe.packagedata.read_pkgdatafile(pkgdatadir + "/runtime/ed") 96*4882a593Smuzhiyun if 'PKGE' in pkgdata: 97*4882a593Smuzhiyun pf = pkgdata['PN'] + "-" + pkgdata['PKGE'] + pkgdata['PKGV'] + '-' + pkgdata['PKGR'] 98*4882a593Smuzhiyun else: 99*4882a593Smuzhiyun pf = pkgdata['PN'] + "-" + pkgdata['PKGV'] + '-' + pkgdata['PKGR'] 100*4882a593Smuzhiyun deploy_dir_rpm = bb_vars['DEPLOY_DIR_RPM'] 101*4882a593Smuzhiyun package_arch = bb_vars['PACKAGE_ARCH'].replace('-', '_') 102*4882a593Smuzhiyun staging_bindir_native = bb_vars['STAGING_BINDIR_NATIVE'] 103*4882a593Smuzhiyun 104*4882a593Smuzhiyun pkg_deploy = os.path.join(deploy_dir_rpm, package_arch, '.'.join((pf, package_arch, 'rpm'))) 105*4882a593Smuzhiyun 106*4882a593Smuzhiyun # Use a temporary rpmdb 107*4882a593Smuzhiyun rpmdb = tempfile.mkdtemp(prefix='oeqa-rpmdb') 108*4882a593Smuzhiyun 109*4882a593Smuzhiyun runCmd('%s/rpmkeys --define "_dbpath %s" --import %s' % 110*4882a593Smuzhiyun (staging_bindir_native, rpmdb, self.pub_key_path)) 111*4882a593Smuzhiyun 112*4882a593Smuzhiyun ret = runCmd('%s/rpmkeys --define "_dbpath %s" --checksig %s' % 113*4882a593Smuzhiyun (staging_bindir_native, rpmdb, pkg_deploy)) 114*4882a593Smuzhiyun # tmp/deploy/rpm/i586/ed-1.9-r0.i586.rpm: rsa sha1 md5 OK 115*4882a593Smuzhiyun self.assertIn('digests signatures OK', ret.output, 'Package signed incorrectly.') 116*4882a593Smuzhiyun shutil.rmtree(rpmdb) 117*4882a593Smuzhiyun 118*4882a593Smuzhiyun #Check that an image can be built from signed packages 119*4882a593Smuzhiyun self.add_command_to_tearDown('bitbake -c clean core-image-minimal') 120*4882a593Smuzhiyun bitbake('-c clean core-image-minimal') 121*4882a593Smuzhiyun bitbake('core-image-minimal') 122*4882a593Smuzhiyun 123*4882a593Smuzhiyun 124*4882a593Smuzhiyun def test_signing_sstate_archive(self): 125*4882a593Smuzhiyun """ 126*4882a593Smuzhiyun Summary: Test that sstate archives can be signed 127*4882a593Smuzhiyun Expected: Package should be signed with the correct key 128*4882a593Smuzhiyun Product: oe-core 129*4882a593Smuzhiyun Author: Daniel Istrate <daniel.alexandrux.istrate@intel.com> 130*4882a593Smuzhiyun AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com> 131*4882a593Smuzhiyun """ 132*4882a593Smuzhiyun 133*4882a593Smuzhiyun test_recipe = 'ed' 134*4882a593Smuzhiyun 135*4882a593Smuzhiyun # Since we need gpg but we can't use gpg-native for sstate signatures, we 136*4882a593Smuzhiyun # build gpg-native in our original builddir then run the tests in a second one. 137*4882a593Smuzhiyun builddir = os.environ.get('BUILDDIR') + "-testsign" 138*4882a593Smuzhiyun sstatedir = os.path.join(builddir, 'test-sstate') 139*4882a593Smuzhiyun 140*4882a593Smuzhiyun nsysroot = self.setup_gpg() 141*4882a593Smuzhiyun 142*4882a593Smuzhiyun feature = 'SSTATE_SIG_KEY ?= "testuser"\n' 143*4882a593Smuzhiyun feature += 'SSTATE_SIG_PASSPHRASE ?= "test123"\n' 144*4882a593Smuzhiyun feature += 'SSTATE_VERIFY_SIG ?= "1"\n' 145*4882a593Smuzhiyun feature += 'GPG_PATH = "%s"\n' % self.gpg_dir 146*4882a593Smuzhiyun feature += 'SSTATE_DIR = "%s"\n' % sstatedir 147*4882a593Smuzhiyun # Any mirror might have partial sstate without .sig files, triggering failures 148*4882a593Smuzhiyun feature += 'SSTATE_MIRRORS:forcevariable = ""\n' 149*4882a593Smuzhiyun 150*4882a593Smuzhiyun self.write_config(feature) 151*4882a593Smuzhiyun 152*4882a593Smuzhiyun with self.create_new_builddir(os.environ['BUILDDIR'], builddir): 153*4882a593Smuzhiyun 154*4882a593Smuzhiyun os.environ["PATH"] = nsysroot + ":" + os.environ["PATH"] 155*4882a593Smuzhiyun self.add_command_to_tearDown('bitbake -c clean %s' % test_recipe) 156*4882a593Smuzhiyun self.add_command_to_tearDown('rm -rf %s' % sstatedir) 157*4882a593Smuzhiyun self.add_command_to_tearDown('rm -rf %s' % builddir) 158*4882a593Smuzhiyun 159*4882a593Smuzhiyun bitbake('-c clean %s' % test_recipe) 160*4882a593Smuzhiyun bitbake('-c populate_lic %s' % test_recipe) 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun recipe_sig = glob.glob(sstatedir + '/*/*/*:ed:*_populate_lic.tar.zst.sig') 163*4882a593Smuzhiyun recipe_archive = glob.glob(sstatedir + '/*/*/*:ed:*_populate_lic.tar.zst') 164*4882a593Smuzhiyun 165*4882a593Smuzhiyun self.assertEqual(len(recipe_sig), 1, 'Failed to find .sig file.') 166*4882a593Smuzhiyun self.assertEqual(len(recipe_archive), 1, 'Failed to find .tar.zst file.') 167*4882a593Smuzhiyun 168*4882a593Smuzhiyun ret = runCmd('gpg --homedir %s --verify %s %s' % (self.gpg_dir, recipe_sig[0], recipe_archive[0])) 169*4882a593Smuzhiyun # gpg: Signature made Thu 22 Oct 2015 01:45:09 PM EEST using RSA key ID 61EEFB30 170*4882a593Smuzhiyun # gpg: Good signature from "testuser (nocomment) <testuser@email.com>" 171*4882a593Smuzhiyun self.assertIn('gpg: Good signature from', ret.output, 'Package signed incorrectly.') 172*4882a593Smuzhiyun 173*4882a593Smuzhiyun 174*4882a593Smuzhiyunclass LockedSignatures(OESelftestTestCase): 175*4882a593Smuzhiyun 176*4882a593Smuzhiyun def test_locked_signatures(self): 177*4882a593Smuzhiyun """ 178*4882a593Smuzhiyun Summary: Test locked signature mechanism 179*4882a593Smuzhiyun Expected: Locked signatures will prevent task to run 180*4882a593Smuzhiyun Product: oe-core 181*4882a593Smuzhiyun Author: Daniel Istrate <daniel.alexandrux.istrate@intel.com> 182*4882a593Smuzhiyun AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com> 183*4882a593Smuzhiyun """ 184*4882a593Smuzhiyun 185*4882a593Smuzhiyun import uuid 186*4882a593Smuzhiyun 187*4882a593Smuzhiyun test_recipe = 'ed' 188*4882a593Smuzhiyun locked_sigs_file = 'locked-sigs.inc' 189*4882a593Smuzhiyun 190*4882a593Smuzhiyun bitbake(test_recipe) 191*4882a593Smuzhiyun # Generate locked sigs include file 192*4882a593Smuzhiyun bitbake('-S none %s' % test_recipe) 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun feature = 'require %s\n' % locked_sigs_file 195*4882a593Smuzhiyun feature += 'SIGGEN_LOCKEDSIGS_TASKSIG_CHECK = "warn"\n' 196*4882a593Smuzhiyun self.write_config(feature) 197*4882a593Smuzhiyun 198*4882a593Smuzhiyun # Build a locked recipe 199*4882a593Smuzhiyun bitbake(test_recipe) 200*4882a593Smuzhiyun 201*4882a593Smuzhiyun templayerdir = tempfile.mkdtemp(prefix='signingqa') 202*4882a593Smuzhiyun create_temp_layer(templayerdir, 'selftestsigning') 203*4882a593Smuzhiyun runCmd('bitbake-layers add-layer %s' % templayerdir) 204*4882a593Smuzhiyun 205*4882a593Smuzhiyun # Make a change that should cause the locked task signature to change 206*4882a593Smuzhiyun # Use uuid so hash equivalance server isn't triggered 207*4882a593Smuzhiyun recipe_append_file = test_recipe + '_' + get_bb_var('PV', test_recipe) + '.bbappend' 208*4882a593Smuzhiyun recipe_append_path = os.path.join(templayerdir, 'recipes-test', test_recipe, recipe_append_file) 209*4882a593Smuzhiyun feature = 'SUMMARY:${PN} = "test locked signature%s"\n' % uuid.uuid4() 210*4882a593Smuzhiyun 211*4882a593Smuzhiyun os.mkdir(os.path.join(templayerdir, 'recipes-test')) 212*4882a593Smuzhiyun os.mkdir(os.path.join(templayerdir, 'recipes-test', test_recipe)) 213*4882a593Smuzhiyun write_file(recipe_append_path, feature) 214*4882a593Smuzhiyun 215*4882a593Smuzhiyun self.add_command_to_tearDown('bitbake-layers remove-layer %s' % templayerdir) 216*4882a593Smuzhiyun self.add_command_to_tearDown('rm -f %s' % os.path.join(self.builddir, locked_sigs_file)) 217*4882a593Smuzhiyun self.add_command_to_tearDown('rm -rf %s' % templayerdir) 218*4882a593Smuzhiyun 219*4882a593Smuzhiyun # Build the recipe again 220*4882a593Smuzhiyun ret = bitbake(test_recipe) 221*4882a593Smuzhiyun 222*4882a593Smuzhiyun # Verify you get the warning and that the real task *isn't* run (i.e. the locked signature has worked) 223*4882a593Smuzhiyun patt = r'The %s:do_package sig is computed to be \S+, but the sig is locked to \S+ in SIGGEN_LOCKEDSIGS\S+' % test_recipe 224*4882a593Smuzhiyun found_warn = re.search(patt, ret.output) 225*4882a593Smuzhiyun 226*4882a593Smuzhiyun self.assertIsNotNone(found_warn, "Didn't find the expected warning message. Output: %s" % ret.output) 227