diff options
Diffstat (limited to 'debian/bin/gencontrol_signed.py')
-rwxr-xr-x | debian/bin/gencontrol_signed.py | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/debian/bin/gencontrol_signed.py b/debian/bin/gencontrol_signed.py new file mode 100755 index 000000000..75d9112cb --- /dev/null +++ b/debian/bin/gencontrol_signed.py @@ -0,0 +1,369 @@ +#!/usr/bin/python3 + +import codecs +import hashlib +import io +import json +import os.path +import re +import ssl +import subprocess +import sys + +from debian_linux.config import ConfigCoreDump +from debian_linux.debian import PackageRelation, VersionLinux +from debian_linux.gencontrol import Gencontrol as Base, merge_packages, \ + iter_flavours +from debian_linux.utils import Templates, read_control + + +class Gencontrol(Base): + def __init__(self, arch): + super(Gencontrol, self).__init__( + ConfigCoreDump(fp=open('debian/config.defines.dump', 'rb')), + Templates(['debian/signing_templates', 'debian/templates'])) + + image_binary_version = self.changelog[0].version.complete + + config_entry = self.config[('version',)] + self.version = VersionLinux(config_entry['source']) + + # Check config version matches changelog version + assert self.version.complete == re.sub(r'\+b\d+$', r'', + image_binary_version) + + self.abiname = config_entry['abiname'] + self.vars = { + 'template': 'linux-image-%s-signed-template' % arch, + 'upstreamversion': self.version.linux_upstream, + 'version': self.version.linux_version, + 'source_basename': re.sub(r'-[\d.]+$', '', + self.changelog[0].source), + 'source_upstream': self.version.upstream, + 'abiname': self.abiname, + 'imagebinaryversion': image_binary_version, + 'imagesourceversion': self.version.complete, + 'arch': arch, + } + self.vars['source_suffix'] = \ + self.changelog[0].source[len(self.vars['source_basename']):] + + self.package_dir = 'debian/%(template)s' % self.vars + self.template_top_dir = (self.package_dir + + '/usr/share/code-signing/%(template)s' + % self.vars) + self.template_debian_dir = (self.template_top_dir + + '/source-template/debian') + os.makedirs(self.template_debian_dir, exist_ok=True) + + self.image_packages = [] + + def do_main_setup(self, vars, makeflags, extra): + makeflags['VERSION'] = self.version.linux_version + makeflags['GENCONTROL_ARGS'] = ( + '-v%(imagebinaryversion)s ' + '-DBuilt-Using="linux (= %(imagesourceversion)s)"' % + vars) + makeflags['PACKAGE_VERSION'] = vars['imagebinaryversion'] + + self.installer_packages = {} + + if os.getenv('DEBIAN_KERNEL_DISABLE_INSTALLER'): + if self.changelog[0].distribution == 'UNRELEASED': + import warnings + warnings.warn('Disable installer modules on request ' + '(DEBIAN_KERNEL_DISABLE_INSTALLER set)') + else: + raise RuntimeError( + 'Unable to disable installer modules in release build ' + '(DEBIAN_KERNEL_DISABLE_INSTALLER set)') + elif self.config.merge('packages').get('installer', True): + # Add udebs using kernel-wedge + kw_env = os.environ.copy() + kw_env['KW_DEFCONFIG_DIR'] = 'debian/installer' + kw_env['KW_CONFIG_DIR'] = 'debian/installer' + kw_proc = subprocess.Popen( + ['kernel-wedge', 'gen-control', vars['abiname']], + stdout=subprocess.PIPE, + env=kw_env) + if not isinstance(kw_proc.stdout, io.IOBase): + udeb_packages = read_control(io.open(kw_proc.stdout.fileno(), + closefd=False)) + else: + udeb_packages = read_control(io.TextIOWrapper(kw_proc.stdout)) + kw_proc.wait() + if kw_proc.returncode != 0: + raise RuntimeError('kernel-wedge exited with code %d' % + kw_proc.returncode) + + for package in udeb_packages: + for arch in package['Architecture']: + if self.config.merge('build', arch) \ + .get('signed-code', False): + self.installer_packages.setdefault(arch, []) \ + .append(package) + + def do_main_packages(self, packages, vars, makeflags, extra): + # Assume that arch:all packages do not get binNMU'd + packages['source']['Build-Depends'].append( + 'linux-support-%(abiname)s (= %(imagesourceversion)s)' % vars) + + def do_main_recurse(self, packages, makefile, vars, makeflags, extra): + # Each signed source package only covers a single architecture + self.do_arch(packages, makefile, vars['arch'], vars.copy(), + makeflags.copy(), extra) + + def do_extra(self, packages, makefile): + pass + + def do_arch_setup(self, vars, makeflags, arch, extra): + super(Gencontrol, self).do_main_setup(vars, makeflags, extra) + + if self.version.linux_modifier is None: + abiname_part = '-%s' % self.config.merge('abi', arch)['abiname'] + else: + abiname_part = '' + makeflags['ABINAME'] = vars['abiname'] = \ + self.config['version', ]['abiname_base'] + abiname_part + + def do_arch_packages(self, packages, makefile, arch, vars, makeflags, + extra): + udeb_packages = self.installer_packages.get(arch, []) + if udeb_packages: + merge_packages(packages, udeb_packages, arch) + + # These packages must be built after the per-flavour/ + # per-featureset packages. Also, this won't work + # correctly with an empty package list. + if udeb_packages: + makefile.add( + 'binary-arch_%s' % arch, + cmds=["$(MAKE) -f debian/rules.real install-udeb_%s %s " + "PACKAGE_NAMES='%s'" % + (arch, makeflags, + ' '.join(p['Package'] for p in udeb_packages))]) + + def do_featureset_setup(self, vars, makeflags, arch, featureset, extra): + self.default_flavour = self.config.merge('base', arch, featureset) \ + .get('default-flavour') + if self.default_flavour is not None: + if featureset != 'none': + raise RuntimeError("default-flavour set for %s %s," + " but must only be set for featureset none" + % (arch, featureset)) + if self.default_flavour \ + not in iter_flavours(self.config, arch, featureset): + raise RuntimeError("default-flavour %s for %s %s does not exist" + % (self.default_flavour, arch, featureset)) + + def do_flavour_setup(self, vars, makeflags, arch, featureset, flavour, + extra): + super(Gencontrol, self).do_flavour_setup(vars, makeflags, arch, + featureset, flavour, extra) + + config_description = self.config.merge('description', arch, featureset, + flavour) + config_image = self.config.merge('image', arch, featureset, flavour) + + vars['flavour'] = vars['localversion'][1:] + vars['class'] = config_description['hardware'] + vars['longclass'] = (config_description.get('hardware-long') + or vars['class']) + + vars['image-stem'] = config_image.get('install-stem') + makeflags['IMAGE_INSTALL_STEM'] = vars['image-stem'] + + def do_flavour_packages(self, packages, makefile, arch, featureset, + flavour, vars, makeflags, extra): + if not (self.config.merge('build', arch, featureset, flavour) + .get('signed-code', False)): + return + + image_suffix = '%(abiname)s%(localversion)s' % vars + image_package_name = 'linux-image-%s-unsigned' % image_suffix + + # Verify that this flavour is configured to support Secure Boot, + # and get the trusted certificates filename. + with open('debian/%s/boot/config-%s' % + (image_package_name, image_suffix)) as f: + kconfig = f.readlines() + assert 'CONFIG_EFI_STUB=y\n' in kconfig + assert 'CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT=y\n' in kconfig + cert_re = re.compile(r'CONFIG_SYSTEM_TRUSTED_KEYS="(.*)"$') + cert_file_name = None + for line in kconfig: + match = cert_re.match(line) + if match: + cert_file_name = match.group(1) + break + assert cert_file_name + if featureset != "none": + cert_file_name = os.path.join('debian/build/source_%s' % + featureset, + cert_file_name) + + self.image_packages.append((image_suffix, image_package_name, + cert_file_name)) + + packages['source']['Build-Depends'].append( + image_package_name + + ' (= %(imagebinaryversion)s) [%(arch)s]' % vars) + + packages_own = self.process_packages( + self.templates['control.image'], vars) + assert len(packages_own) == 1 + cmds_binary_arch = ["$(MAKE) -f debian/rules.real install-signed " + "PACKAGE_NAME='%s' %s" % + (packages_own[0]['Package'], makeflags)] + + if self.config.merge('packages').get('meta', True): + packages_meta = self.process_packages( + self.templates['control.image.meta'], vars) + assert len(packages_meta) == 1 + packages_meta += self.process_packages( + self.templates['control.headers.meta'], vars) + assert len(packages_meta) == 2 + + # Don't pretend to support build-profiles + for package in packages_meta: + del package['Build-Profiles'] + + if flavour == self.default_flavour \ + and not self.vars['source_suffix']: + packages_meta[0].setdefault('Provides', PackageRelation()) \ + .append('linux-image-generic') + packages_meta[1].setdefault('Provides', PackageRelation()) \ + .append('linux-headers-generic') + + packages_own.extend(packages_meta) + + cmds_binary_arch += [ + "$(MAKE) -f debian/rules.real install-meta " + "PACKAGE_NAME='%s' LINK_DOC_PACKAGE_NAME='%s' %s" % + (package['Package'], package['Depends'][0][0].name, makeflags) + for package in packages_meta + ] + + self.substitute_debhelper_config( + 'image.meta', vars, + 'linux-image%(localversion)s' % vars, + output_dir=self.template_debian_dir) + self.substitute_debhelper_config( + 'headers.meta', vars, + 'linux-headers%(localversion)s' % vars, + output_dir=self.template_debian_dir) + + merge_packages(packages, packages_own, arch) + makefile.add('binary-arch_%s_%s_%s_real' % (arch, featureset, flavour), + cmds=cmds_binary_arch) + + self.substitute_debhelper_config( + 'image', vars, + 'linux-image-%(abiname)s%(localversion)s' % vars, + output_dir=self.template_debian_dir) + + def write(self, packages, makefile): + self.write_changelog() + self.write_control(packages.values(), + name=(self.template_debian_dir + '/control')) + self.write_makefile(makefile, + name=(self.template_debian_dir + '/rules.gen')) + self.write_files_json() + + def write_changelog(self): + # Copy the linux changelog, but: + # * Change the source package name and version + # * Insert a line to refer to refer to the linux source version + vars = self.vars.copy() + vars['source'] = self.changelog[0].source + vars['distribution'] = self.changelog[0].distribution + vars['urgency'] = self.changelog[0].urgency + vars['signedsourceversion'] = \ + re.sub(r'\+b(\d+)$', r'.b\1', + re.sub(r'-', r'+', vars['imagebinaryversion'])) + + with codecs.open(self.template_debian_dir + '/changelog', 'w', + 'utf-8') as f: + f.write(self.substitute('''\ +linux-signed-@arch@ (@signedsourceversion@) @distribution@; urgency=@urgency@ + + * Sign kernel from @source@ @imagebinaryversion@ + +''', + vars)) + + with codecs.open('debian/changelog', 'r', 'utf-8') as changelog_in: + # Ignore first two header lines + changelog_in.readline() + changelog_in.readline() + + for d in changelog_in.read(): + f.write(d) + + def write_files_json(self): + # Can't raise from a lambda function :-( + def raise_func(e): + raise e + + # Some functions in openssl work with multiple concatenated + # PEM-format certificates, but others do not. + def get_certs(file_name): + certs = [] + BEGIN, MIDDLE = 0, 1 + state = BEGIN + with open(file_name) as f: + for line in f: + if line == '-----BEGIN CERTIFICATE-----\n': + assert state == BEGIN + certs.append([]) + state = MIDDLE + elif line == '-----END CERTIFICATE-----\n': + assert state == MIDDLE + state = BEGIN + else: + assert line[0] != '-' + assert state == MIDDLE + certs[-1].append(line) + assert state == BEGIN + return [''.join(cert_lines) for cert_lines in certs] + + def get_cert_fingerprint(cert, algo): + hasher = hashlib.new(algo) + hasher.update(ssl.PEM_cert_to_DER_cert(cert)) + return hasher.hexdigest() + + all_files = {'packages': {}} + + for image_suffix, image_package_name, cert_file_name in \ + self.image_packages: + package_dir = 'debian/%s' % image_package_name + package_files = [] + package_modules = [] + package_files.append({'sig_type': 'efi', + 'file': 'boot/vmlinuz-%s' % image_suffix}) + for root, dirs, files in os.walk('%s/lib/modules' % package_dir, + onerror=raise_func): + for name in files: + if name.endswith('.ko'): + package_modules.append( + '%s/%s' % + (root[(len(package_dir) + 1):], name)) + package_modules.sort() + for module in package_modules: + package_files.append( + {'sig_type': 'linux-module', + 'file': module}) + package_certs = [get_cert_fingerprint(cert, 'sha256') + for cert in get_certs(cert_file_name)] + assert len(package_certs) >= 1 + all_files['packages'][image_package_name] = { + 'trusted_certs': package_certs, + 'files': package_files + } + + with codecs.open(self.template_top_dir + '/files.json', 'w') as f: + json.dump(all_files, f) + + +if __name__ == '__main__': + Gencontrol(sys.argv[1])() |