#!/bin/bash
# SPDX-License-Identifier: GPL-2.0+
# Copyright: 2018 Luca Boccassi <bluca@debian.org>

set -e

# Build-Depend on dh-signobs.
# Enable by using dh --with=signobs.
# If the certificate is necessary at build time, for example to include in the kernel or shim,
# annotate the Source stanza of debian/control with:
#  XS-Obs: needssslcertforbuild
# Then it can be fetched in PEM format with:
#  dh_signobs_getcert DESTINATION
# Append DER to retrieve it in binary format instead.
#
# This debhelper will run in 2 steps - first after the dh_install phase, when the source
# package will have built and installed the unsigned binaries. The second step during the
# followup build after OBS has signed the binaries, before dh_auto_install runs.
#
# During the first step dh_signobs_pack will be called to get the hashes (or kernel modules)
# and pack them into a cpio archive in ../OTHER that OBS can use. At the same time a new
# source package for the OBS followup build will be generated, either from a source-template
# that can be specified by the package being signed or automatically generated by this debhelper.
#
# The source template can be in any form it wants, as long as it is placed in a directory
# called "source-template" in the debian/ directory (can be a subdirectory, and it can be
# static or generated). Also a files.json list of the binaries to sign must be placed in the
# same PARENT directory with the following format:
#  {"unsigned-binary-package": {
#	"files": [
#		{"sig_type": "efi", "file": "usr/lib/foo/bar.efi"},
#		{"sig_type": "linux-module", "file": "lib/modules/1.2.3/baz.ko"}
#	]
#  }}
# The advantage of the templated build is that it can sign only a subset of files - for
# example, a kernel build with CONFIG_MODULE_SIG_ALL=y does not need to sign the modules and
# can save a lot of time by just signing the vmlinuz.
#
# The autogenerated package will simply look for any .efi, .ko or vmlinuz- file in the debian
# directory (excluding debian/tmp) and considers the first subdirectory to be the package name
# (IOW: it covers the common case where built files are first installed in debian/tmp/path and
# then moved by dh_install into debian/pkg/path), and then generates a new source package that
# unpacks the packages that contain them and repacks them after signing the binaries.
# The files.json can be used also with the autogenerated package, simply to restrict the set
# of files that get signed, or in case there is an uncommon build directory structure.
#
# During the second step dh_signobs_unpack will be called and it will unpack the signed
# binaries that OBS places in the cpio archive in ../SOURCES and it will copy them to
# debian/tmp/ respecting the subdirectories path they where picked from (eg: debian/tmp/boot/vmlinuz).
#
# Following common conventions, if an EFI binary has an .efi suffix, a .signed suffix will be
# appended. Kernel modules and images will not be renamed.
if [ "$(basename "$0")" = "dh_signobs_pack" ] && [ -d ../OTHER ] && ! ls ../SOURCES/*cpio.rsasign.sig &> /dev/null
then
	# remove any dsc and other source artefacts, obs-build might place them there
	rm -rf ../OTHER/*

	# if a template is provided, create the directory tree and copy the sources
	# otherwise generate it on-the-fly
	SOURCE_TEMPLATE="$(find debian -type d -name source-template)"
	if [ -d "$SOURCE_TEMPLATE/debian" ]
	then
		SOURCE_PKG=$(cd "$SOURCE_TEMPLATE"; dpkg-parsechangelog -SSource)
		mkdir -p "../OTHER/$SOURCE_PKG/debian/signatures"
		cp -r "$SOURCE_TEMPLATE"/debian/* "../OTHER/$SOURCE_PKG/debian/"
		# inject dh_signobs if not present
		if ! grep -qs signobs "../OTHER/$SOURCE_PKG/debian/rules"
		then
			sed -i "s/dh \$@/dh \$@ --with signobs/" "../OTHER/$SOURCE_PKG/debian/rules"
		fi
	else
		SOURCE_PKG="$(dpkg-parsechangelog -S Source)-signed"
		distribution="$(dpkg-parsechangelog -S Distribution)"
		urgency="$(dpkg-parsechangelog -S Urgency)"
		# lintian will complain that the date is the same, so bump by 1 second,
		# we want to avoid build time dates to keep the build reproducible
		# requires dpkg-dev >= 1.18.8 for "-S Timestamp"
		date="$(date --rfc-2822 --date=@$(($(dpkg-parsechangelog -S Timestamp) + 1)))"
		version_binary="$(dpkg-parsechangelog -S Version)"
		# make the source package native by removing the "-" separator
		version_mangled="$(dpkg-parsechangelog -S Version | tr '-' '+')+signed"
		mkdir -p "../OTHER/$SOURCE_PKG/debian/signatures"
		mkdir -p "../OTHER/$SOURCE_PKG/debian/source"
		echo "3.0 (native)" > "../OTHER/$SOURCE_PKG/debian/source/format"
		# lintian will complain that there is no source, but it's in the previous build
		echo "$SOURCE_PKG: source-is-missing" > "../OTHER/$SOURCE_PKG/debian/source/lintian-overrides"
		echo "9" > "../OTHER/$SOURCE_PKG/debian/compat"
		cp debian/copyright "../OTHER/$SOURCE_PKG/debian/"
		# copy the changelog and add a new entry, changing the version to native
		cp debian/changelog "../OTHER/$SOURCE_PKG/debian/"
		sed -i "1i\\ " "../OTHER/$SOURCE_PKG/debian/changelog"
		sed -i "1i\\ -- OBS signing service <obssign@obs.service>  $date" "../OTHER/$SOURCE_PKG/debian/changelog"
		sed -i "1i\\ " "../OTHER/$SOURCE_PKG/debian/changelog"
		sed -i "1i\\  * Sign version $version_binary" "../OTHER/$SOURCE_PKG/debian/changelog"
		sed -i "1i\\ " "../OTHER/$SOURCE_PKG/debian/changelog"
		sed -i "1i\\$SOURCE_PKG ($version_mangled) $distribution; urgency=$urgency" \
			"../OTHER/$SOURCE_PKG/debian/changelog"
		# the dummy binary package is necessary otherwise dpkg-source will error out,
		# it will be removed during the followup build
		cat > "../OTHER/$SOURCE_PKG/debian/control" << EOF
Source: $SOURCE_PKG
Section: admin
Priority: optional
Maintainer: OBS signing service <obssign@obs.service>
Standards-Version: 3.9.8
Build-Depends: debhelper (>= 9~), dh-signobs

Package: $SOURCE_PKG-dummy
Architecture: all
EOF
		# the dh_auto_install override is there so that more actions
		# can be appended later after the install phase, when
		# the signatures get unpacked and reattached to the binaries
		# in order to generate the binary packages and remove the dummy
		# only when doing a binary build, rather than a source build,
		# per-signed-binary-package rules will be dynamically added later
		# so place a marker for easier substitution
		#
		# the changelog and copyright files will be copied from the unsigned package
		cat > "../OTHER/$SOURCE_PKG/debian/rules" << EOF
#!/usr/bin/make -f
SHELL := bash -e

%:
	if [ -d ../SOURCES ] && ls ../SOURCES/*cpio.rsasign.sig &> /dev/null && grep -qs $SOURCE_PKG-dummy debian/control; then \\
	sed -i "/Package:/d" debian/control; \\
	sed -i "/Architecture:/d" debian/control; \\
	fi #CONTROLMARKER
	dh \$@ --with signobs

override_dh_gencontrol:
	#GENCONTROLMARKER

override_dh_installchangelogs:

override_dh_installdocs:

override_dh_auto_install:
	dh_auto_install
EOF
		chmod +x "../OTHER/$SOURCE_PKG/debian/rules"
	fi

	# pesign wants an empty certutil DB - use sql as legacy mode fails often
	nss_db="$PWD/nss-db"
	# certutil will fail if it's called twice
	rm -rf "$nss_db"
	mkdir -p "$nss_db" hashes
	certutil -N -d sql:"$nss_db" --empty-password

	# get hashes via pesign from kernel image and EFI binaries
	# store into directories named after the packages, and OBS will respect
	# the directory structure when we unpack after the signing
	declare -a UNSIGNED

	# if the source describes what files to sign, parse it, otherwise do everything
	JSON="$(find debian -type f -name files.json)"
	if [ -f "$JSON" ]
	then
		for PKG in $(jq --raw-output 'to_entries[]? | .key' < "$JSON")
		do
			for f in $(jq --raw-output ".\"$PKG\".files[]? | .file" < "$JSON")
			do
				UNSIGNED+=("$PKG/$f")
			done
		done
	else
		# shellcheck disable=SC2207
		UNSIGNED=( $(find debian -path debian/tmp -prune -o -type f \( -name "*.efi" -o -name "vmlinuz-*" -o -name "*.ko" \) -printf '%P\n') )
	fi

	for f in "${UNSIGNED[@]}"
	do
		mkdir -p "hashes/$(dirname "$f")"
		if [[ $(basename "$f") = *.ko ]]
		then
			# kernel modules have to be copied wholesale, cannot get just the hash
			cp "debian/$f" "hashes/$f"
		else
			pesign --force -n sql:"$nss_db" -i "debian/$f" -E "hashes/$f"
		fi

		# copy the unsigned binaries to be able to re-attach signatures later
		mkdir -p "../OTHER/$SOURCE_PKG/debian/signatures/$(dirname "$f")"
		cp "debian/$f" "../OTHER/$SOURCE_PKG/debian/signatures/$f"

		# if there is no template, generate the package-specific parts of the signed source
		if [ ! -d "$SOURCE_TEMPLATE/debian" ]
		then
			# generate the new package metadata, but only once, as multiple binaries
			# might be in one package
			if ! grep -qs "${f%%/*}" "../OTHER/$SOURCE_PKG/debian/rules"
			then
				# to ensure the signed package is identical to the unsigned one,
				# simply extract the content of the unsigned one, the signed
				# binaries will simply overwrite the unsigned ones
				echo "	dpkg -x ../SOURCES/${f%%/*}_*.deb debian/${f%%/*}" >> "../OTHER/$SOURCE_PKG/debian/rules"
				echo "	dpkg -e ../SOURCES/${f%%/*}_*.deb debian/${f%%/*}/DEBIAN" >> ."./OTHER/$SOURCE_PKG/debian/rules"
				echo "	for script in debian/${f%%/*}/DEBIAN/*; do mv \$\$script debian/${f%%/*}.\$\${script##*/}; done" >> ."./OTHER/$SOURCE_PKG/debian/rules"
				# then delete the unsigned package, which the unpack step will copy
				# over
				echo "	rm -f ../DEBS/${f%%/*}_*.deb" >> "../OTHER/$SOURCE_PKG/debian/rules"

				# generate rules to extract unsigned package control files
				sed -i "s|fi #CONTROLMARKER|mkdir -p debian/${f%%/*}/DEBIAN; dpkg -e ../SOURCES/${f%%/*}_*.deb debian/${f%%/*}/DEBIAN; \\\\\\n\\tfi #CONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
				# save version for gencontrol - pkg/DEBIAN will be cleaned up in-between
				sed -i "s@fi #CONTROLMARKER@grep 'Version:' debian/${f%%/*}/DEBIAN/control | sed 's/Version:\\\\s*//' > debian/${f%%/*}.version; \\\\\\n\\tfi #CONTROLMARKER@" "../OTHER/$SOURCE_PKG/debian/rules"
				# generate rule to prune unwanted metadata from unsigned control file
				sed -i "s|fi #CONTROLMARKER|sed -i '/Source/d' debian/${f%%/*}/DEBIAN/control; \\\\\\n\\tfi #CONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
				sed -i "s|fi #CONTROLMARKER|sed -i '/Version/d' debian/${f%%/*}/DEBIAN/control; \\\\\\n\\tfi #CONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
				sed -i "s|fi #CONTROLMARKER|sed -i '/Maintainer/d' debian/${f%%/*}/DEBIAN/control; \\\\\\n\\tfi #CONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
				sed -i "s|fi #CONTROLMARKER|sed -i '/Installed-Size/d' debian/${f%%/*}/DEBIAN/control; \\\\\\n\\tfi #CONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
				# generate rule to create signed package metadata from the unsigned one
				sed -i "s|fi #CONTROLMARKER|cat debian/control debian/${f%%/*}/DEBIAN/control > debian/control.tmp; mv debian/control.tmp debian/control; echo \"\" >> debian/control; \\\\\\n\\tfi #CONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
				# generate rule set the version from the unsigned one
				sed -i "s|#GENCONTROLMARKER|dh_gencontrol -p ${f%%/*} -- -v\$(shell cat debian/${f%%/*}.version); rm -f debian/${f%%/*}.version\\n\\t#GENCONTROLMARKER|" "../OTHER/$SOURCE_PKG/debian/rules"
			fi

			# special case: the conffiles lists the file in /etc, but it does
			# get regenerated and it does not prune duplicates, triggering a
			# Lintian error, so remove the old ones
			echo "	rm -f debian/*.conffiles" >> "../OTHER/$SOURCE_PKG/debian/rules"

			# finally copy the signed binary in the package build dir
			# note that by convention bootloaders are renamed to .signed but
			# kernel modules and images are not
			if [[ $(basename "$f") = *.efi ]]
			then
				# unsigned bootloaders are installed by the unsigned package
				# so there must not be a conflict on the filesystem
				echo "	rm -f debian/$f" >> "../OTHER/$SOURCE_PKG/debian/rules"
				echo "	cp debian/tmp/${f#*/}.signed debian/${f%/*}" >> "../OTHER/$SOURCE_PKG/debian/rules"
			else
				echo "	cp debian/tmp/${f#*/} debian/${f%/*}" >> "../OTHER/$SOURCE_PKG/debian/rules"
			fi
		fi
	done
	rm -rf "$nss_db"

	# pack everything into a CPIO archive and place it where OBS expects it
	pushd hashes
	find . -type f | cpio -H newc -o > "../../OTHER/$SOURCE_PKG.cpio.rsasign"
	popd
	rm -rf hashes

	# assemble the source package that OBS will build on the second pass
	pushd "../OTHER/$SOURCE_PKG"
	dpkg-buildpackage -uc -us -d -S
	popd
elif [ "$(basename "$0")" = "dh_signobs_unpack" ] && [ -d ../SOURCES ] && ls ../SOURCES/*cpio.rsasign.sig &> /dev/null
then
	# copy packages built on first pass, if any
	cp ../SOURCES/*.deb ../DEBS/ 2>/dev/null || :
	cp ../SOURCES/*.tar.* ../DEBS/
	cp ../SOURCES/*.dsc ../DEBS/
	cp ../SOURCES/*.changes ../DEBS/
	# but not the current source package, otherwise build-recipe-dsc will fail
	rm -f ../DEBS/"$(dpkg-parsechangelog -SSource)"*

	# unpack in debian/signatures and create pesign db, where the template source expects them
	pushd debian/signatures
	cpio -idm < ../../../SOURCES/*.cpio.rsasign.sig

	# OBS signs a hash without certificate informations so it cannot simply be
	# attached to the PE binaries, certificate metadata has to be provided separately
	# so we need to create a certutil db and import the certificate manually
	rm -rf nss-db
	mkdir nss-db
	nss_db="$PWD/nss-db"
	certutil -N -d sql:"$nss_db" --empty-password
	certutil -A -d sql:"$nss_db" -n cert -t CT,CT,CT -i ../../../SOURCES/_projectcert.crt

	while read -r SIG
	do
		export infile="${SIG%.sig}"
		cpio -i --to-stdout "${infile}" < ../../../SOURCES/*.cpio.rsasign > "$(basename "${infile}").sattrs"
		test -s "$(basename "${infile}").sattrs" || exit 1

		# install the signed file into debian/tmp so that the package can pick it up
		# remove the unsigned package name from the path
		DEST="../tmp/${SIG#*/}"
		# kernel images do not have the .signed prefix, only bootloaders do
		if [[ $(basename "$DEST") = vmlinuz* ]]
		then
			DEST="${DEST%%.sig}"
		else
			DEST="${DEST}ned"
		fi
		mkdir -p "../tmp/$(dirname "${DEST}")"

		# ensure the EFI hash matches before and after attaching the signature
		old_hash=$(pesign -n sql:"$nss_db" -h -P -i "${SIG%.sig}")

		pesign -n sql:"$nss_db" -c cert -i "${SIG%.sig}" -o "$DEST" -d sha256 -I "$(basename "${infile}").sattrs" -R "$SIG"

		new_hash=$(pesign -n sql:"$nss_db" -h -i "$DEST")
		if [ "$old_hash" != "$new_hash" ]
		then
		    echo "Pesign hash mismatch error: $old_hash $new_hash"
		    exit 1
		fi

		rm -f "$(basename "${infile}").sattrs" "$SIG" "${SIG%.sig}"
	done < <(find . -type f \( -name '*efi.sig' -o -name 'vmlinuz*.sig' \) -printf '%P\n')
	rm -rf nss-db

	# the kernel-sign-file script wants DER format
	openssl x509 -in ../../../SOURCES/_projectcert.crt -inform pem -outform der -out _projectcert.der
	while read -r SIG
	do
		# install the signed file into debian/tmp so that the package can pick it up
		# remove the unsigned package name from the path
		DEST="../tmp/${SIG#*/}"
		DEST="${DEST%%.sig}"
		mkdir -p "../tmp/$(dirname "${DEST}")"
		/usr/lib/rpm/pesign/kernel-sign-file -i pkcs7 -s "$SIG" sha256 _projectcert.der "${SIG%.sig}" "${DEST}"
		rm -f "$SIG" "${SIG%.sig}"
	done < <(find . -type f -name '*ko.sig' -printf '%P\n')
	rm -f _projectcert.der

	popd
elif [ "$(basename "$0")" = "dh_signobs_getcert" ] && [ -d ../SOURCES ] && [ -e ../SOURCES/_projectcert.crt ]
then
	if [ "$#" -lt 1 ]
	then
		echo "Missing destination filename parameter!"
		exit 1
	fi
	mkdir -p "$(dirname "$1")"
	if [ "$#" -ge 2 ] && [ "$2" = "DER" ]
	then
		openssl x509 -in ../SOURCES/_projectcert.crt -inform pem -outform der -out "$1"
	else
		cp ../SOURCES/_projectcert.crt "$1"
	fi
fi
