#!/bin/bash
set -e

progname="$(basename "$0")"

error() {
    echo "$@" >&2
    exit 1
}

quiet_message() {
    $QUIET || echo "$@"
}

quiet_error() {
    quiet_message "$@" >&2
    exit 1
}

_usage() {
cat << END
$progname [options] <source> <destination> [signing key]

This script will prepare an unsigned kernel for use on a system with
UEFI Secure Boot enabled.  The kernel located at <source> will be
signed using the signing key and written to <destination>.  It will then
register the public component of the signing key for enrollment in the
system MOK if it is not already enrolled.

If [signing key] is unspecified and the working directory is a kernel
build directory, the signing key will be pulled from .config.

If an autodetected signing key has been autogenerated by the kernel build
process and Secure Boot is not enabled, enrollment will be skipped.

This script has several exit values:
0 - success
1 - failure
2 - skipped - kernel not copied into place

options:
    -q|--quiet: do not report errors for missing dependencies, just exit with
                error
    -e|--enroll: queue certificate for enrollment in system MOK.  If the
                 certificate is autodetected, sbtool-enroll-key -a will be
                 used to skip enrollment of kernel-generated signing keys
    -f|--force: queue the certificate for enrollment even if it is
                kernel-generated and secure boot is disabled
END
}

help() {
    _usage
    exit 0
}

usage() {
    _usage >&2
    exit 1
}

check_commands() {
    for command in "$@"; do
        if ! command -v "$command" > /dev/null; then
            for i in /usr/src/linux-obj/$(uname -m)/*/scripts/"$command" ; do
                if [ -x "$i" ] ; then
                    scriptdir="$(dirname "$i")"
                    quiet_message "Using $command from $scriptdir"
                    PATH="$PATH:$scriptdir"
                    continue 2
                fi
            done
            quiet_error "$command is missing"
        fi
    done
}

cert_subject_hash() {
    local cert=$1
    openssl x509 -in $cert -noout -subject_hash
}

cert_kernel_generated() {
    local cert=$1

    # Based on "CN = Build time autogenerated kernel key"
    # as defined in linux/certs/Makefile
    KERNEL_GENERATED_CERT_HASH=0926ef54

    test "$(cert_subject_hash "$cert")" = "$KERNEL_GENERATED_CERT_HASH"
}

secure_boot_enabled() {
    mokutil --sb-state | grep -q "enabled"
}

options=$(getopt -o qhef --long quiet,help,enroll,force -- "$@")

eval set -- $options

QUIET=false
ENROLL=false
FORCE=false
while true; do
    case "$1" in
    -q|--quiet)
        QUIET=true
        ;;
    -e|--enroll)
        ENROLL=true
        ;;
    -f|--force)
        FORCE=true
        ;;
    -h|--help)
        help ;;
    --)
        shift
        break ;;
    *)
        usage ;;
    esac
    shift
done

arch="$(rpm -E %{_arch})"
case "$arch" in
    i?86|x86_64|aarch64|arm*|ia64|riscv64) sign_tools="pesign pk12util certutil" ;;
    ppc*|s390*) sign_tools=sign-file ;;
    *) echo "Don't know how to sign a kernel on architecture '$arch'."
        exit 1
        ;;
esac

check_commands $sign_tools openssl

UNSIGNED=$1
SIGNED=$2
CERT=$3

test -z "$UNSIGNED" -o -z "$SIGNED" && usage 1
test -f "$UNSIGNED" || error "$UNSIGNED does not exist."
test -d "$(dirname "$SIGNED")" || error "Target directory for $SIGNED does not exist."

read_cert_config() {
    sed -n '/^CONFIG_MODULE_SIG_KEY=/s///p' $1 |tr -d '"'
}

DETECTED_KEY=false
FOUND_CONFIG=false
if test -z "$CERT"; then
    for path in .config "$(dirname "$UNSIGNED")/.config"; do
        if test -e $path; then
            FOUND_CONFIG=true
            CERT=$(read_cert_config $path)
            if test -n "$CERT"; then
                DETECTED_KEY=true
                break
            fi
        fi
    done
    if test -z "$CERT"; then
        if $FOUND_CONFIG; then
            echo "Module signing not enabled for this kernel.  Skipping."
            exit 2
        else
            error "Couldn't autodetect signing key, no config found."
        fi
    elif ! test -f "$CERT"; then
        error "Certificate \"$CERT\" found in config but does not exist."
    fi
fi

cleanup() {
	test -n "$tmpdir" && rm -rf "$tmpdir"
}

trap cleanup EXIT
tmpdir=$(mktemp -d /tmp/signkernel.XXXXXX)

if ! openssl x509 -in $CERT -ext keyUsage,extendedKeyUsage -noout | \
	grep -q "Code Signing"; then
    error "Certificate must have Code Signing extended key usage defined for Secure Boot."
fi

# certutil has no facility to import a private key directly, so we have to
# use the pkcs12 interface instead.
certutil_import_key() {
    local certdir=$1
    local cert=$2
    local P12="$tmpdir/cert.p12"

    uuidgen > $tmpdir/passwd
    openssl pkcs12 -export -password "file:$tmpdir/passwd" -inkey $cert \
                   -in $cert -name kernel-cert -out $P12

    # pk12util has no silent mode
    if ! pk12util -w $tmpdir/passwd -d $certdir -i $P12 > $tmpdir/output; then
        cat $tmpdir/output
        exit 1
    fi
    rm -f $tmpdir/passwd $P12 $tmpdir/output
}

case "$sign_tools" in
    pesign*)
        certutil -N -d $tmpdir --empty-password
        certutil_import_key $tmpdir $CERT

        pesign -n $tmpdir -c kernel-cert -i $UNSIGNED -o $SIGNED -s --force

        ;;
    sign-file)
        openssl x509 -in $CERT -outform DER -out "$tmpdir/cert.crt"
        sign-file sha256 $CERT $tmpdir/cert.crt $UNSIGNED $SIGNED
        ;;
esac

echo "Signed $UNSIGNED with $CERT and installed to $SIGNED"

$ENROLL || exit 0

if $QUIET; then
    ARGS="-q"
fi

if $DETECTED_KEY && ! $FORCE && \
   cert_kernel_generated "$CERT" && ! secure_boot_enabled; then
    echo "Skipping enrollment of kernel-generated certificate on" \
         "system without Secure Boot enabled."
    quiet_message "Override with --force."
    echo ""
    exit 0
fi

/usr/sbin/sbtool-enroll-key $ARGS $CERT
ret=$?

# Skipping enrollment doesn't mean we've skipped
# signing and copying so return 0.
if test $? -eq 2; then
    ret=0
fi

exit $ret
