diff mbox series

[7/9] UBUNTU: [Packaging] linux-restricted-signatures -- publish clean signatures

Message ID 20210308150004.1746089-8-apw@canonical.com
State New
Headers show
Series LP#1918134 -- LRMv4 switch to signing with Ubuntu Kernel Modules signing key | expand

Commit Message

Andy Whitcroft March 8, 2021, 3 p.m. UTC
Consume the signatures produced by the signing service from the signing
custom binary upload and express those in a linux-signatures-nvidia-*
package.  These are split by flavour for simplicity of consumption by
linux-restricted-modules; consumed via a simple runtime depedency.

BugLink: https://bugs.launchpad.net/bugs/1918134
Signed-off-by: Andy Whitcroft <apw@canonical.com>
---
 debian/rules.lrs             |  12 +++
 debian/scripts/gen-rules     |   1 +
 debian/scripts/gen-rules.lrs | 100 +++++++++++++++++++
 download-signed              | 183 +++++++++++++++++++++++++++++++++++
 4 files changed, 296 insertions(+)
 create mode 100755 debian/rules.lrs
 create mode 100755 debian/scripts/gen-rules.lrs
 create mode 100755 download-signed
diff mbox series

Patch

diff --git a/debian/rules.lrs b/debian/rules.lrs
new file mode 100755
index 0000000..1c22d8e
--- /dev/null
+++ b/debian/rules.lrs
@@ -0,0 +1,12 @@ 
+##export DH_VERBOSE := 1
+
+arch = $(shell dpkg-architecture -qDEB_HOST_ARCH)
+
+%:
+	dh $@
+
+override_dh_auto_build:
+	./download-signed "$(src_genr_package)" "$(src_genr_version)" "$(src_genr_package)"
+
+override_dh_auto_install: nvidia-$(arch)
+	dh_install
diff --git a/debian/scripts/gen-rules b/debian/scripts/gen-rules
index 8952f4b..4894481 100755
--- a/debian/scripts/gen-rules
+++ b/debian/scripts/gen-rules
@@ -3,6 +3,7 @@ 
 src_package=$(LC_ALL=C dpkg-parsechangelog -SSource)
 case "$src_package" in
 linux-restricted-generate*)	pkg='lrg' ;;
+linux-restricted-signature*)	pkg='lrs' ;;
 linux-restricted-modules*)	pkg='lrm' ;;
 esac
 
diff --git a/debian/scripts/gen-rules.lrs b/debian/scripts/gen-rules.lrs
new file mode 100755
index 0000000..8298b0e
--- /dev/null
+++ b/debian/scripts/gen-rules.lrs
@@ -0,0 +1,100 @@ 
+#!/bin/bash
+
+# Pick out relevant version and package information including our predecessor
+# packages: linux-restricted-generate -> linux-restricted-signatures -> linux-restricted-modules
+src_package=$(LC_ALL=C dpkg-parsechangelog -SSource)
+src_version=$(LC_ALL=C dpkg-parsechangelog -SVersion)
+src_abi=$(echo "${src_version}" | sed -ne 's/\([0-9]*\.[0-9]*\.[0-9]*\-[0-9]*\)\..*/\1/p')
+src_series=$(LC_ALL=C dpkg-parsechangelog -SDistribution | sed -e 's/-\(security\|updates\|proposed\)$//')
+
+# linux/5.8.0-41.46
+src_main_package=$(echo "${src_package}" | sed -e 's/-restricted-signature//')
+src_main_version=$(echo ${src_version} | sed -e 's/+[0-9][0-9\.]*$//')
+
+# linux-restricted-generate/5.8.0-41.46[+1]
+src_genr_package=$(echo "${src_package}" | sed -e 's/-restricted-signatures/-restricted-generate/')
+src_genr_version=${src_version}
+
+# linux-restricted-signatures/5.8.0-41.46[+1]
+
+# linux-restricted-modules/5.8.0-41.46[+1]
+
+cat - "debian/rules.lrs" >"debian/rules.gen" <<EOL
+#! /usr/bin/make -f
+
+src_package := ${src_package}
+src_version = ${src_version}
+src_abi = ${src_abi}
+src_series = ${src_series}
+src_genr_package = ${src_genr_package}
+src_genr_version = ${src_genr_version}
+
+EOL
+
+echo " ${src_genr_package} (= ${src_genr_version})," >"debian/control.interlock-up"
+
+build_archs=
+while read command flavour archs
+do
+	case "$command" in
+	build)		;;
+	*)		continue ;;
+	esac
+
+	for arch in $archs
+	do
+		case " $build_archs " in
+		*\ $arch\ *)	;;
+		*)		build_archs="$buildarchs $arch" ;;
+		esac
+	done
+done <"debian/package.config"
+
+while read command flavour archs
+do
+	case "$command" in
+	build)		;;
+	*)		continue ;;
+	esac
+
+	targets=$(echo "$archs" | sed -e 's/\</nvidia-/g')
+
+	echo "II: build linux-signatures-nvidia-${src_abi}-${flavour} for ${archs}"
+
+	cat - >>"debian/rules.gen" <<EOL
+
+$targets::
+	for stream in "${src_genr_version}/${src_abi}-${flavour}/signatures"/*;		\
+	do										\
+		nvidia=\$\$(basename "\$\$stream");						\
+		echo "\$\$stream/* /lib/modules/${src_abi}-${flavour}/kernel/\$\$nvidia/bits" >>debian/linux-signatures-nvidia-${src_abi}-${flavour}.install;	\
+	done
+EOL
+
+	cat - >>"debian/control.signatures" <<EOL
+
+Package: linux-signatures-nvidia-${src_abi}-${flavour}
+Build-Profiles: <!stage1>
+Architecture: ${archs}
+Section: kernel
+Priority: optional
+Built-Using: \${linux:BuiltUsing}
+Description: Linux kernel signatures for nvidia modules for version ${src_abi}-${flavour}
+ This package contains the Linux kernel signatures nvidia modules for
+ version ${src_abi}-${flavour}.
+ .
+ You likely do not want to install this package directly.
+EOL
+done <"debian/package.config"
+
+cat "debian/control.common" "debian/control.signatures" | sed \
+	-e "/@BUILD-INTERLOCK@/{"		\
+	-e " r debian/control.interlock-up"	\
+	-e " d"					\
+	-e " }"					\
+	-e "s/@SRCPKGNAME@/${src_package}/g"	\
+	-e "s/@ABI@/${src_abi}/g"		\
+	-e "s/@SERIES@/${src_series}/g"		\
+    >"debian/control"
+
+rm -f "debian/control.interlock-up" "debian/control.signatures"
diff --git a/download-signed b/download-signed
new file mode 100755
index 0000000..0793696
--- /dev/null
+++ b/download-signed
@@ -0,0 +1,183 @@ 
+#! /usr/bin/python3
+
+import hashlib
+import argparse
+import os
+import re
+import sys
+import tarfile
+from urllib import request
+from urllib.error import HTTPError
+from urllib.parse import (
+    urlparse,
+    urlunparse,
+    )
+
+import apt
+
+# package_name: package containing the objects we signed
+# package_version: package version containing the objects we signed
+# src_package: source package name in dists
+# signed_type: 'signed' or 'uefi' schema in the url
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "package_name",
+    help="package containining the objects we signed")
+parser.add_argument(
+    "package_version",
+    help="package version containing the objects we signed, or 'current'")
+parser.add_argument(
+    "src_package",
+    help="source package name in dists")
+parser.add_argument(
+    "signed_type",
+    nargs='?',
+    default='signed',
+    help="subdirectory type in the url, 'signed' or 'uefi'")
+args = parser.parse_args()
+
+
+class SignedDownloader:
+    """Download a block of signed information from dists.
+
+    Find a block of signed information as published in dists/*/signed
+    and download the contents.  Use the contained checksum files to
+    identify the members and to validate them once downloaded.
+    """
+
+    def __init__(self, package_name, package_version, src_package, signed_type='signed'):
+        self.package_name = package_name
+        self.package_version = package_version
+        self.src_package = src_package
+
+        # Find the package in the available archive repositories.  Use a _binary_
+        # package name and version to locate the appropriate archive.  Then use the
+        # URI there to look for and find the appropriate binary.
+        cache = apt.Cache()
+
+        self.package = None
+        if self.package_version == "current":
+            self.package = cache[package_name].candidate
+        else:
+            for version in cache[package_name].versions:
+                if version.version == self.package_version:
+                    self.package = version
+                    break
+
+        if not self.package:
+            raise KeyError("{0}: package version not found".format(self.package_name))
+
+        origin = self.package.origins[0]
+        pool_parsed = urlparse(self.package.uri)
+        self.package_dir = "%s/%s/%s/%s-%s/%s/" % (
+            origin.archive, 'main', signed_type,
+            self.src_package, self.package.architecture, self.package_version)
+
+        # Prepare the master url stem and pull out any username/password.  If present
+        # replace the default opener with one which offers that password.
+        dists_parsed_master = list(pool_parsed)
+        if '@' in dists_parsed_master[1]:
+            (username_password, host) = pool_parsed[1].split('@', 1)
+            (username, password) = username_password.split(':', 1)
+
+            dists_parsed_master[1] = host
+
+            # Work out the authentication domain.
+            domain_parsed = [ dists_parsed_master[0], dists_parsed_master[1], '/', None, None, None ]
+            auth_uri = urlunparse(domain_parsed)
+
+            # create a password manager
+            password_mgr = request.HTTPPasswordMgrWithDefaultRealm()
+
+            # Add the username and password.
+            # If we knew the realm, we could use it instead of None.
+            password_mgr.add_password(None, auth_uri, username, password)
+
+            handler = request.HTTPBasicAuthHandler(password_mgr)
+
+            # create "opener" (OpenerDirector instance)
+            opener = request.build_opener(handler)
+
+            # Now all calls to urllib.request.urlopen use our opener.
+            request.install_opener(opener)
+
+        self.dists_parsed = dists_parsed_master
+
+    def download_one(self, member, filename, hash_factory=None):
+        directory = os.path.dirname(filename)
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+
+        dists_parsed = list(self.dists_parsed)
+        dists_parsed[2] = re.sub(r"/pool/.*", "/dists/" + self.package_dir + \
+            member, dists_parsed[2])
+        dists_uri = urlunparse(dists_parsed)
+
+        print("Downloading %s ... " % dists_uri, end='')
+        sys.stdout.flush()
+        try:
+            with request.urlopen(dists_uri) as dists, open(filename, "wb") as out:
+                hashobj = None
+                if hash_factory:
+                    hashobj = hash_factory()
+                for chunk in iter(lambda: dists.read(256 * 1024), b''):
+                    if hashobj:
+                        hashobj.update(chunk)
+                    out.write(chunk)
+                checksum = True
+                if hashobj:
+                    checksum = hashobj.hexdigest()
+        except HTTPError as e:
+            if e.code == 404:
+                print("not found")
+            else:
+                raise
+        else:
+            print("found")
+            return checksum
+        return None
+
+    def download(self, base):
+        """Download an entire signed result from dists."""
+
+        # Download the checksums and use that to download the contents.
+        sums = 'SHA256SUMS'
+        sums_local = os.path.join(base, self.package_version, sums)
+        if not self.download_one(sums, sums_local):
+            print('download-signed: {0}: not found'.format(sums))
+            sys.exit(1)
+
+        # Read the checksum file and download the files it mentions.
+        here = os.path.abspath(base)
+        with open(sums_local) as sfd:
+            for line in sfd:
+                line = line.strip()
+                (checksum_expected, member) = (line[0:64], line[66:])
+                filename = os.path.abspath(os.path.join(base, self.package_version, member))
+                if not filename.startswith(here):
+                    print('download-signed: {0}: member outside output directory'.format(member))
+                    sys.exit(1)
+
+                # Download and checksum this member.
+                checksum_actual = self.download_one(member, filename, hashlib.sha256)
+                if checksum_expected != checksum_actual:
+                    print('download-signed: {0}: member checksum invalid'.format(member))
+                    sys.exit(1)
+
+        # If this is a tarball result then extract it.
+        here = os.path.abspath(os.path.join(base, self.package_version))
+        tarball_filename = os.path.join(base, self.package_version, 'signed.tar.gz')
+        if os.path.exists(tarball_filename):
+            with tarfile.open(tarball_filename) as tarball:
+                for tarinfo in tarball:
+                    if not filename.startswith(here):
+                        print('download-signed: {0}: tarball member outside output directory'.format(member))
+                        sys.exit(1)
+                for tarinfo in tarball:
+                    print('Extracting {0} ...'.format(tarinfo.name))
+                    tarball.extract(tarinfo, base)
+
+
+downloader = SignedDownloader(**vars(args))
+downloader.download('.')