#!/bin/bash
#
# Purpose: Sign the latest DKML installer
# Requirements:
#   1. Yubikey 5 Series hardware device
#       * The Diskuv signing keys must be loaded onto the hardware device.
#       * The keys are _not_ extractable. If you need another device contact
#         support AT diskuv.com.
#   2. Yubico PIV Tool
#       * Download and install yubico-piv-tool-2.3.0-win64.msi or later from
#         https://developers.yubico.com/yubico-piv-tool/Releases.
#   3. Windows machine
#       For now this script expects Yubico PIV Tool to be installed in a
#       Windows specific path.
#   4. UNIX Bash shell capability
#       * On Windows you can run this script with `with-dkml sign.sh` if you
#         already have Diskuv OCaml installed (see
#         https://github.com/diskuv/dkml-installer-ocaml/releases).
#       * On Windows you should also be able to use Git Bash, MSYS2 or Cygwin.
#         But Git Bash and Cygwin will require that you install:
#         - `osslsigncode`
#         - `7z` (from p7zip)
#         - `p11tool` (from GnuTLS)
#         - `gh` (from GitHub CLI)
#         and place them in the PATH
set -euf

# Testing flags
TEST_QUICK=OFF
TEST_BASH_ON_EXIT=OFF

# Signing tool
#   1. `osslsigncode`
#       * This is the current tool.
#   2. `jsign` (alternative)
#       * This has been tested with:
#           Java(TM) SE Runtime Environment (build 17.0.3.1+2-LTS-6)
#         Earlier versions probably work.
#       * Since certificates were generated by Sectigo with ASN1 OID
#         name (secp384r1) rather than the OID value (1.3.132.0.34), Java
#         can't seem to read it.
#         Confer: https://github.com/frohoff/jdk8u-jdk/blob/da0da73ab82ed714dc5be94acd2f0d00fbdfe2e9/src/share/classes/sun/security/ec/ECParameters.java#L138-L150
#         and error:
#            Caused by: java.io.IOException: Only named ECParameters supported
#                    at java.base/sun.security.util.ECParameters.engineInit(ECParameters.java:148)
#                    at java.base/java.security.AlgorithmParameters.init(AlgorithmParameters.java:311)
#                    at java.base/sun.security.util.ECUtil.getECParameterSpec(ECUtil.java:189)
#                    at jdk.crypto.cryptoki/sun.security.pkcs11.P11KeyStore.loadPkey(P11KeyStore.java:1374)
#                    ... 6 more
signingtool=osslsigncode

# GitHub repository to download/upload
GHREPO=diskuv/dkml-installer-ocaml

# Set HERE to this contributors/ directory, and DIRSEP to platform dir separator
HERE=$(dirname "$0")
HERE=$(cd "$HERE" && pwd)
if [ -x /usr/bin/cygpath ]; then
    HERE_NATIVE=$(/usr/bin/cygpath -aw "$HERE")
    DIRSEP="\\"
else
    HERE_NATIVE=$HERE
    DIRSEP="/"
fi

# Clone from crossplatform-functions.sh create_workdir()
my_create_workdir() {
    # Our use of mktemp needs to be portable; docs at:
    # * BSD: https://www.freebsd.org/cgi/man.cgi?query=mktemp&sektion=1
    # * GNU: https://www.gnu.org/software/autogen/mktemp.html
    if [ -n "${_CS_DARWIN_USER_TEMP_DIR:-}" ]; then # macOS (see `man mktemp`)
        make_workdir_DEFAULT="$_CS_DARWIN_USER_TEMP_DIR"
    elif [ -n "${TMPDIR:-}" ]; then # macOS (see `man mktemp`)
        make_workdir_DEFAULT=$(printf "%s" "$TMPDIR" | PATH=/usr/bin:/bin sed 's#/$##') # remove trailing slash on macOS
    elif [ -n "${TMP:-}" ]; then # MSYS2 (Windows), Linux
        make_workdir_DEFAULT="$TMP"
    else
        make_workdir_DEFAULT="/tmp"
    fi
    DKML_TMP_PARENTDIR="${DKML_TMP_PARENTDIR:-$make_workdir_DEFAULT}"
    [ ! -e "$DKML_TMP_PARENTDIR" ] && install -d "$DKML_TMP_PARENTDIR"
    WORK=$(PATH=/usr/bin:/bin mktemp -d "$DKML_TMP_PARENTDIR"/dkmlw.XXXXX)
    install -d "$WORK"
}

# Set a safe WORK dir
DKML_TMP_PARENTDIR='' my_create_workdir
trap 'PATH=/usr/bin:/bin rm -rf "$WORK"' EXIT

# Set file paths that need privacy from different users on the machine
pinfile="$WORK/yubikey.pin.txt"
if [ -x /usr/bin/cygpath ]; then
    pinfile_native=$(/usr/bin/cygpath -aw "$pinfile")
else
    pinfile_native=$pinfile
fi

# Everything will be done in WORK
cd "$WORK"

# Set YUBICOBIN to where PIV Tool is located
if [ -x /usr/bin/cygpath ]; then
    YUBICOBIN_W32="$(cygpath -wF 38)"'\Yubico\Yubico PIV Tool\bin'
    YUBICOBIN_UNIX=$(cygpath -a "$YUBICOBIN_W32")
    if [ ! -d "$YUBICOBIN_UNIX" ]; then
        printf "FATAL: You need to download and install Yubico PIV Tool from https://developers.yubico.com/yubico-piv-tool/Releases/\n" >&2
        exit 117
    fi
else
    printf "FATAL: Only Windows is supported right now\n" >&2
    exit 117
fi

finish_progress() {
    printf -- "\n╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n\n" >&2
}
progress() {
    progress_COUNT=${progress_COUNT:-1}
    progress_NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    if [ "$progress_COUNT" -gt 1 ]; then
        finish_progress
    fi
    printf -- "╭──────── %02d. %s | %70s ────────╮\n\n" \
        "$progress_COUNT" \
        "$progress_NOW" \
        "$*" >&2
    progress_COUNT=$(( progress_COUNT + 1 ))
}

# Get full crossplatform-functions.sh
progress "Getting crossplatform-functions.sh from dkml-runtime-common"
install -d drc
git -C drc init
git -C drc remote add origin https://github.com/diskuv/dkml-runtime-common.git
#   5f4e933f0a5da2cad6c24d353c4a13faeec2f3ed is tag v0.4.0-prerel90
git -C drc fetch --depth 1 origin 5f4e933f0a5da2cad6c24d353c4a13faeec2f3ed
git -C drc -c advice.detachedHead=false checkout FETCH_HEAD

# Import the functions
#   shellcheck disable=1091
. drc/unix/crossplatform-functions.sh

# Install github CLI
progress "Installing GitHub CLI"
if [ -x /usr/bin/pacman ] && is_msys2_msys_build_machine; then
    pacman -S --needed --noconfirm mingw-w64-clang-x86_64-github-cli
fi

# Download latest unsigned release
progress "Authenticating with GitHub"
if ! gh auth status; then
    gh auth login
fi

# Install winpty (optional)
progress "Installing winpty"
if [ -x /usr/bin/pacman ] && is_msys2_msys_build_machine; then
    pacman -S --needed --noconfirm winpty
fi

# Find Pre-release
progress "Finding latest release marked as Pre-release"
winpty_exe=$(command -v winpty || true)
if [ -n "$winpty_exe" ]; then
    # Unbuffering keeps the nice colors. Completely optional of course.
    # But it keeps escape codes in its output, so we have to do listing twice.
    "$winpty_exe" -Xallow-non-tty gh release list --repo "$GHREPO" --exclude-drafts
    gh release list --repo "$GHREPO" --exclude-drafts > releases.txt
else
    gh release list --repo "$GHREPO" --exclude-drafts | tee releases.txt
fi
printf "\n" >&2
#   Sample output:
#       v0.4.0-prerel67                Pre-release  (v0.4.0-prerel67)  about 18 days ago
#       v0.4.0-prerel11                Latest       (v0.4.0-prerel11)  about 1 month ago
#   Field $1 (version)
#   Field $2 (Pre-release, Latest, etc.)
RELEASE_TAG=$(awk '$2="Pre-release"{print $1; exit}' releases.txt)
if [ -z "$RELEASE_TAG" ]; then
    printf "FATAL: No release found that has been marked as Pre-release.\n" >&2
    exit 117
fi
#   Ask for confirmation
read -r -s -n 1 -p "Do you want to sign release $RELEASE_TAG? (y/N) " YESNO
case "$YESNO" in
    y | Y ) printf "YES\n\nLet's continue.\n" ;;
    *) printf "\nOK! Stopping the signing process.\n"; exit 0 ;;
esac

# Download
progress "Downloading $RELEASE_TAG"
install -d dl-unsigned

gh release download "$RELEASE_TAG" \
    --repo "$GHREPO" \
    --dir dl-unsigned \
    --pattern 'unsigned-*.exe'
set +u # workaround bash bug with empty arrays in for loops
UNSIGNED_EXE_NAMES=()
while IFS='' read -r line; do
    UNSIGNED_EXE_NAMES+=("$line")
    printf "Found executable to sign: %s\n" "$line" >&2
done < <(cd dl-unsigned && ls -1)
set -u
printf "\n" >&2

gh release download "$RELEASE_TAG" \
    --repo "$GHREPO" \
    --dir dl-sfx \
    --pattern '*.sfx'
set +u # workaround bash bug with empty arrays in for loops
while IFS='' read -r line; do
    printf "Found self-extracting executable header: %s\n" "$line" >&2
done < <(cd dl-sfx && ls -1)
set -u

# Install 7z
progress "Installing p7zip"
if [ -x /usr/bin/pacman ] && is_msys2_msys_build_machine; then
    pacman -S --needed --noconfirm p7zip
fi

for unsigned_exe_name in "${UNSIGNED_EXE_NAMES[@]}"; do
    progress "Unpacking $unsigned_exe_name"
    install -d "rel-unpack/$unsigned_exe_name"
    # Use 7zr (light-version, standalone) instead of 7z or 7za since
    # same executable used by dkml-install-api:installer_sfx.ml.
    #
    # 7za <command> [<switches>...] <archive_name> [<file_names>...]
    #   x : eXtract files with full paths
    #   -r[-|0] : Recurse subdirectories
    #   -y : assume Yes on all queries
    #   -o{Directory} : set Output directory
    7zr x -r -y -o"rel-unpack/$unsigned_exe_name" "dl-unsigned/$unsigned_exe_name"
done

# Get which embedded exes to sign
progress "Locating embedded .exe files to sign"
TO_SIGN=()
TO_SKIP=()
while IFS='' read -r line; do
    EXECBASENAME=$(basename "$line")
    case "$EXECBASENAME" in
        vc_redist*.exe)
            # Microsoft already signed these Visual Studio redistributables
            TO_SKIP+=("$line")
            ;;
        *.bc.exe)
            # Signing corrupts the OCaml magic number (Caml1999X029) that is
            # validated by ocamlrun.exe
            TO_SKIP+=("$line")
            ;;
        curl.exe)
            # Signed by https://curl.se/
            TO_SKIP+=("$line")
            ;;
        gsudo.exe)
            # This is signed by https://github.com/gerardog/gsudo.
            # We _could_ re-sign it but it would be better if we customized
            # it first (ex. let gsudo.exe elevate
            # `dkml-install-admin-runner.exe` only, or only executables that
            # have been signed by Diskuv, etc.)
            TO_SKIP+=("$line")
            ;;
        *)
            TO_SIGN+=("$line")
            ;;
    esac
done < <(cd rel-unpack && find . -name "*.exe" -type f | sed 's#^[.]/##')
set +u # workaround bash bug with empty arrays in for loops
printf "Will sign\n" >&2
printf "=========\n" >&2
printf "\n" >&2
for embedded_exe_name in "${TO_SIGN[@]}"; do
    printf "[%s\n" "$embedded_exe_name" | sed 's#/#] #' >&2
done
printf "\n" >&2
printf "Will skip\n" >&2
printf "=========\n" >&2
printf "\n" >&2
for embedded_exe_name in "${TO_SKIP[@]}"; do
    printf "[%s\n" "$embedded_exe_name" | sed 's#/#] #' >&2
done
set -u

# Sign the release ...
askpin() { # only used by sign_with_jsign()
    read -rsp 'Yubikey PIN: ' PINCODE
    printf "%s" "$PINCODE" > "$pinfile"
    printf "\n" >&2
}
runjava() { # only used by sign_with_jsign()
    if ! java --version 2>/dev/null >/dev/null; then
        printf "FATAL: Java has not been installed, or it is not in your PATH.\n" >&2
        exit 117
    fi
    # Test if Java is recent enough to support modules.
    if java --add-exports jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED --version 2>/dev/null >/dev/null; then
        #   Avoid:
        #        Exception in thread "main" java.lang.IllegalAccessError: class net.jsign.YubiKey (in unnamed module @0x4769b07b) cannot access class sun.security.pkcs11.wrapper.PKCS11 (in module jdk.crypto.cryptoki) because module jdk.crypto.cryptoki does not export sun.security.pkcs11.wrapper to unnamed module @0x4769b07b
        #                at net.jsign.YubiKey.getTokenSlot(YubiKey.java:76)
        #                at net.jsign.YubiKey.getSunPKCS11Configuration(YubiKey.java:60)
        #                at net.jsign.YubiKey.getProvider(YubiKey.java:44)
        #                at net.jsign.SignerHelper.build(SignerHelper.java:361)
        #                at net.jsign.SignerHelper.sign(SignerHelper.java:562)
        #                at net.jsign.JsignCLI.execute(JsignCLI.java:117)
        #                at net.jsign.JsignCLI.main(JsignCLI.java:40)
        #   by using https://github.com/intoolswetrust/jsignpdf/issues/70#issuecomment-990144863
        java \
            --add-exports jdk.crypto.cryptoki/sun.security.pkcs11=ALL-UNNAMED \
            --add-exports jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED \
            --add-opens java.base/java.security=ALL-UNNAMED \
            --add-exports java.base/sun.security.action=ALL-UNNAMED \
            --add-exports java.base/sun.security.rsa=ALL-UNNAMED \
            --add-opens java.base/sun.security.util=ALL-UNNAMED \
            "$@"
    else
        java "$@"
    fi
}
configure_sign_with_jsign() {
    # Get jsign
    progress "Getting jsign"
    install -d jsign
    downloadfile \
        https://github.com/ebourg/jsign/releases/download/4.1/jsign-4.1.jar \
        jsign/jsign.jar \
        4dddbc9e56bd6e15934122f16ce652f07d2110530418196898c31600e44109b6

    # Setup signing function which needs PIN
    progress "Setup signing procedure"

    #   Create pkcs11 configuration.
    #       Confer: https://docs.oracle.com/javase/8/docs/technotes/guides/security/p11guide.html#Config
    pkcs11cfg="$WORK/pkcs11.cfg"
    if [ -x /usr/bin/cygpath ]; then
        pkcs11cfg_native=$(/usr/bin/cygpath -aw "$pkcs11cfg")
    else
        pkcs11cfg_native="$pkcs11cfg"
    fi
    cat > "$pkcs11cfg" <<EOF
name = DiskuvSigner
library = $YUBICOBIN_W32\\libykcs11.dll
# PIV slot 9c is pkcs11 slot 2. Confer: https://developers.yubico.com/yubico-piv-tool/YKCS11/
slot = 2
showInfo = true
EOF

    #   Ask for Yubikey PIN. Place in $pinfile_native
    askpin

    #   Make signing function to be used later
    signfile() {
        signfile_IN=$1
        shift

        # Make absolute Windows path
        if [ -x /usr/bin/cygpath ]; then
            signfile_IN_NATIVE=$(/usr/bin/cygpath -aw "$signfile_IN")
        else
            signfile_IN_NATIVE=$signfile_IN
        fi

        # Sign. The file will be mutated
        PATH="$YUBICOBIN_UNIX:$PATH" runjava -Djava.security.debug=sunpkcs11 -Djava.security.debug=pkcs11keystore \
            -jar jsign/jsign.jar \
            --keystore "$pkcs11cfg_native" \
            --storetype YUBIKEY \
            --storepass "file:$pinfile_native" \
            --certfile "${HERE_NATIVE}${DIRSEP}full-chain.p7.pem" \
            "$signfile_IN_NATIVE"
    }
}
configure_sign_with_osslsigncode() {
    # Get libp11 DLLs
    progress "Getting libp11 DLLs"
    install -d libp11
    downloadfile \
        https://github.com/OpenSC/libp11/releases/download/libp11-0.4.11/libp11-0.4.11-windows.zip \
        libp11/libp11.zip \
        ae69d155c689de3b98b581036c9bb44e9e7f9033432d6d70a972da649ade4cd2
    unzip -q libp11/libp11.zip -d libp11
    pkcs11dll=libp11/libp11-0.4.11-windows/64bit/pkcs11.dll
    if [ -x /usr/bin/cygpath ]; then
        pkcs11dll_mixed=$(/usr/bin/cygpath -am "$pkcs11dll")
    else
        pkcs11dll_mixed="$pkcs11dll"
    fi

    # Install osslsigncode
    progress "Installing osslsigncode"
    if [ -x /usr/bin/pacman ] && is_msys2_msys_build_machine; then
        # bug in 2.5.1+: https://github.com/mtrojnar/osslsigncode/issues/180
        # so we install the buggy version to get the dependencies, and then
        # "-U" to a prior version (downgrade).
        # 1. install buggy version
        pacman -S --needed --noconfirm mingw-w64-clang-x86_64-osslsigncode
        # 2. downgrade
        install -d osslsigncode
        downloadfile \
            https://repo.msys2.org/mingw/clang64/mingw-w64-clang-x86_64-osslsigncode-2.3-1-any.pkg.tar.zst \
            osslsigncode/mingw-w64-clang-x86_64-osslsigncode-any.pkg.tar.zst \
            81f85a5d1014593c10d540d747db2ee5205b5d844d2d097ee3ad424751fbcff1
        pacman -U --noconfirm osslsigncode/mingw-w64-clang-x86_64-osslsigncode-any.pkg.tar.zst
    fi

    # Install p11tool
    progress "Installing GnuTLS p11tool"
    if [ -x /usr/bin/pacman ] && is_msys2_msys_build_machine; then
        pacman -S --needed --noconfirm mingw-w64-clang-x86_64-gnutls
    fi

    # Serial number
    progress "Finding serial number for Yubikey"
    PATH="$YUBICOBIN_UNIX:$PATH" yubico-piv-tool -a status | tee pivstatus.txt
    SERIALNUM=$(awk 'BEGIN{FS=":"} $1=="Serial Number" {print $2}' pivstatus.txt | sed 's/[^0-9]//g')
    if [ -z "$SERIALNUM" ]; then
        printf "FATAL: No Yubikey was found. Have you inserted it?" >&2
        exit 117
    fi
    SERIALCOUNT=$(awk 'BEGIN{FS=":"} $1=="Serial Number" {count=count+1} END{print count}' pivstatus.txt)
    if [ "$SERIALCOUNT" != "1" ]; then
        printf "FATAL: More than one Yubikey was found. Please remove all but one." >&2
        exit 117
    fi
    printf "\nDetected Yubikey.\n"
    printf "\nSerial Number:\n\t%s\n" "$SERIALNUM" >&2

    # URL for Digital Signature (PIV slot 9c)
    #   Theoretically we could scrape the following instead of the brittle formulation of the URL:
    #       p11tool --provider "$(cygpath -wF 38)"'\Yubico\Yubico PIV Tool\bin\libykcs11.dll' --list-privkeys --login
    #   But for now the URL matches the specific Yubikey model used by Diskuv
    DIGITALSIGN_URI="pkcs11:model=YubiKey%20YK5;manufacturer=Yubico%20%28www.yubico.com%29;serial=$SERIALNUM;token=YubiKey%20PIV%20%23$SERIALNUM;id=%02;object=Private%20key%20for%20Digital%20Signature;type=private"
    printf "PKCS url:\n\t%s\n" "$DIGITALSIGN_URI" >&2

    # Setup signing function which needs PIN
    progress "Setup signing procedure"

    #   Ask for Yubikey PIN. Place in $pinfile_native.
    #   * We should be able to use `pin-source=FILE` to say what the PIN
    #     is ... so we don't get prompted for the PIN each time we sign
    #     a single executable. However, it is up to the PKCS11 module
    #     to recognize it.
    #   * Yubikey avoids the first prompt but not the others with `pin-source=`
    #        Enter PKCS#11 token PIN for YubiKey PIV #12345678:
    #        Enter PKCS#11 key PIN for Private key for Digital Signature:
    #        Enter PKCS#11 key PIN for Private key for Digital Signature:
    #     Yubikey can use `osslsigncode -readpass PINFILE` to avoid the
    #     last two prompts.
    #   * However OpenSC has closed w/o resolving 4+ year old issues where the
    #     same PIN has to be entered multiple times (same as above).
    #     https://github.com/OpenSC/OpenSC/issues/2039 and
    #     https://github.com/OpenSC/libp11/issues/101 :(
    askpin

    #   Make signing function to be used later
    signfile() {
        signfile_IN=$1
        shift

        # Make absolute Windows path
        if [ -x /usr/bin/cygpath ]; then
            signfile_IN_NATIVE=$(/usr/bin/cygpath -aw "$signfile_IN")
        else
            signfile_IN_NATIVE=$signfile_IN
        fi

        # osslsigncode does not mutate in-place so we use rel-unpack/ as
        # a temporary staging area.
        # * When signing we keep the basename of the original file in case the
        #   basename is embedded as part of the signing.
        signfile_BASENAME=$(basename "$signfile_IN_NATIVE")
        signfile_TEMP="rel-repack/$signfile_BASENAME"

        # -comm                   = set commercial purpose (default: individual purpose)
        # -pass                   = the private key password
        # -readpass               = the private key password source
        # -t                      = specifies that the digital signature will be timestamped
        #                           by the Time-Stamp Authority (TSA) indicated by the URL
        # -pkcs11cert             = PKCS#11 URI identifies a certificate in the token
        # -pkcs11engine           = PKCS11 engine
        # -pkcs11module           = PKCS11 module
        PATH="$YUBICOBIN_UNIX:$PATH" osslsigncode sign \
            -comm \
            -h sha384 \
            -pkcs11engine "$pkcs11dll_mixed" \
            -pkcs11module "$YUBICOBIN_W32\\libykcs11.dll" \
            -pkcs11cert "$DIGITALSIGN_URI?pin-source=$pinfile_native" \
            -in "$signfile_IN_NATIVE" \
            -out "$signfile_TEMP" \
            -t http://timestamp.sectigo.com

        # Move the signed .exe back to rel-unpack/
        rm -f "$signfile_IN_NATIVE" # avoids any read-only mode
        mv "$signfile_TEMP" "$signfile_IN_NATIVE"
    }

    printf "\nNOTICE:\n" >&2
    printf "  Because of a multi-year improperly closed bug with OpenSC PIN\n" >&2
    printf "  handling (https://github.com/OpenSC/OpenSC/issues/2039) you will\n" >&2
    printf "  be asked for your PIN. The PIN you just entered will reduce the\n" >&2
    printf "  amount of PIN prompts by 1/3, but 2/3 of prompts you will have\n" >&2
    printf "  to enter manually.\n" >&2
    printf "  We suggest:\n" >&2
    printf "    1. Put your PIN into a text editor without auto-save like\n" >&2
    printf "       Notepad (not Notepad++) on Windows or 'vi' on Unix.\n" >&2
    printf "    2. Place a newline after your PIN so that the first line is\n" >&2
    printf "       your PIN and the second line is empty.\n" >&2
    printf "    3. Copy with Ctrl-C on Windows, Cmd-C on mac, Ctrl-Ins on Unix\n" >&2
    printf "       the entire file (which includes the newline).\n" >&2
    printf "    4. Paste whenever you are prompted for the PIN; it will go fast!\n" >&2
    printf "\n" >&2
    #   shellcheck disable=SC2034
    read -r -s -n 1 -p "Continue by pressing any key ... " ANYKEY
    echo
    printf "\n" >&2
}

# Configure signing
install -d rel-repack
case "$signingtool" in
    jsign)          configure_sign_with_jsign ;;
    osslsigncode)   configure_sign_with_osslsigncode ;;
esac

# Sign each embedded .exe in rel-unpack/
progress "Signing all embedded .exe files"
for EMBEDEXE in "${TO_SIGN[@]}"; do
    # Form path to embedded .exe
    EMBEDEXE_INFILE="rel-unpack/${EMBEDEXE}"
    if [ -x /usr/bin/cygpath ]; then
        EMBEDEXE_INFILE_NATIVE=$(/usr/bin/cygpath -aw "$EMBEDEXE_INFILE")
    else
        EMBEDEXE_INFILE_NATIVE="$EMBEDEXE_INFILE"
    fi

    # Sign
    printf "Signing %s ...\n" "$EMBEDEXE" >&2
    signfile "$EMBEDEXE_INFILE_NATIVE"
    #   make sure it is read-only; we did sign it!
    chmod 555 "$EMBEDEXE_INFILE"

    if [ "$TEST_QUICK" = ON ]; then
        break
    fi
done

# Make directories
install -d dist dist7z

# Re-pack contents into setup-*.exe and uninstall-*.exe
#   Same process as dkml-install-api:installer_sfx.ml
SETUP_EXE_FILES=()
UNINSTALL_EXE_FILES=()
set +u # workaround bash bug with empty arrays in for loops
for unsigned_exe_name in "${UNSIGNED_EXE_NAMES[@]}"; do
    # From names of unsigned-XXX-i-VER.exe and unsigned-XXX-u-VER.exe we want:
    # * setup-XXX-VER.exe and uninstall-XXX-VER.exe
    # * setup-XXX-VER.7z and uninstall-XXX-VER.7z
    # * XXX-i-7zS2.sfx and XXX-u-7zS2.sfx
    case $unsigned_exe_name in
        unsigned-*-windows_x86-i-*|unsigned-*-windows_x86_64-i-*|unsigned-*-windows_arm64-i-*)
            packaged_exe_name=$(printf "%s" "$unsigned_exe_name" | sed -E 's/^unsigned-/setup-/; s/windows_(x86|x86_64|arm64)-i/windows_\1/')
            packaged_exe_absfile="$WORK/dist/$packaged_exe_name"
            # Capture for signing later
            SETUP_EXE_FILES+=("$packaged_exe_absfile")
            ;;
        unsigned-*-windows_x86-u-*|unsigned-*-windows_x86_64-u-*|unsigned-*-windows_arm64-u-*)
            packaged_exe_name=$(printf "%s" "$unsigned_exe_name" | sed -E 's/^unsigned-/uninstall-/; s/windows_(x86|x86_64|arm64)-u/windows_\1/')
            packaged_exe_absfile="$WORK/dist/$packaged_exe_name"
            # Capture for signing later
            UNINSTALL_EXE_FILES+=("$packaged_exe_absfile")
            ;;
        *)
            printf "FATAL: Unsupported unsigned executable named %s\n" "$unsigned_exe_name"
            exit 106
    esac
    packaged_7z_name=$(printf "%s" "$packaged_exe_name" | sed 's/.exe$/.7z/')
    sfx_name=$(printf "%s" "$unsigned_exe_name" | sed 's/^unsigned-//; s/.exe$/.sfx/')

    sfx_file=dl-sfx/"$sfx_name"

    progress "Packaging $packaged_exe_name"
    # Use 7zr (light-version, standalone) instead of 7z or 7za since
    # same executable used by dkml-install-api:installer_sfx.ml.
    #
    # 7za <command> [<switches>...] <archive_name> [<file_names>...]
    #   a : Add files to archive
    #   -bb[0-3] : set output log level
    #   -bs{o|e|p}{0|1|2} : set output stream for output/error/progress line
    #   -m{Parameters} : set compression Method
    #     -mmt[N] : set number of CPU threads
    #     -mx[N] : set compression level: -mx1 (fastest) ... -mx9 (ultra)
    #   -y : assume Yes on all queries
    # Clarifications:
    #   * -bs0 is disable stdout
    #   * DIR/* is 7z's syntax for the contents of DIR, but only with native Windows 7z.exe (not MSYS2)
    cd "rel-unpack/$unsigned_exe_name"
    7zr a -bb0 -bso0 -mx9 -y "$WORK/dist7z/$packaged_7z_name"
    cd "$WORK"
    # Place .sfx in front of the body
    cat "$sfx_file" "dist7z/$packaged_7z_name" > "$packaged_exe_absfile"
done
set -u

# Signing self-extracting uninstall.exe
progress "Signing self-extracting uninstall.exe"
set +u # workaround bash bug with empty arrays in for loops
for packaged_exe_absfile in "${UNINSTALL_EXE_FILES[@]}"; do
    packaged_exe_absfile_BASENAME=$(basename "$packaged_exe_absfile")
    printf "Signing %s ...\n" "$packaged_exe_absfile_BASENAME" >&2
    signfile "$packaged_exe_absfile"
    #   make sure it is read-only; we did sign it!
    chmod 555 "$packaged_exe_absfile"

    # Embed signed uninstall-*.exe into upcoming setup-*.exe as
    # bin/dkml-package-uninstall.exe
    setup_exe_file_BASENAME=$(printf '%s' "$packaged_exe_absfile_BASENAME" | sed 's/^uninstall-/setup-/')
    setup_exe_absfile="$WORK/dist/$setup_exe_file_BASENAME"
    printf "\nEmbedding %s into upcoming %s ...\n" "$packaged_exe_absfile_BASENAME" "$setup_exe_file_BASENAME" >&2
    install -d re-embed/"$setup_exe_file_BASENAME"/bin
    install "$packaged_exe_absfile" re-embed/"$setup_exe_file_BASENAME"/bin/dkml-package-uninstall.exe
    # We can use 7z to add to a <sfx>.exe, not just .7z files. It knows to skip
    # over the SFX header.
    #
    # Use 7zr (light-version, standalone) instead of 7z or 7za since
    # same executable used by dkml-install-api:installer_sfx.ml.
    #
    # 7za <command> [<switches>...] <archive_name> [<file_names>...]
    #   u : Update files to archive
    #   -bb[0-3] : set output log level
    #   -bs{o|e|p}{0|1|2} : set output stream for output/error/progress line
    #   -m{Parameters} : set compression Method
    #     -mmt[N] : set number of CPU threads
    #     -mx[N] : set compression level: -mx1 (fastest) ... -mx9 (ultra)
    #   -y : assume Yes on all queries
    # Clarifications:
    #   * -bs0 is disable stdout
    #   * DIR/* is 7z's syntax for the contents of DIR, but only with native Windows 7z.exe (not MSYS2)
    cd re-embed/"$setup_exe_file_BASENAME"
    7zr u -bb0 -bso0 -mx9 -y "$setup_exe_absfile"
    cd "$WORK"
    echo >&2
done
set -u

# Signing self-extracting setup.exe
progress "Signing self-extracting setup.exe"
set +u # workaround bash bug with empty arrays in for loops
for packaged_exe_absfile in "${SETUP_EXE_FILES[@]}"; do
    packaged_exe_absfile_BASENAME=$(basename "$packaged_exe_absfile")
    printf "Signing %s ...\n" "$packaged_exe_absfile_BASENAME" >&2
    signfile "$packaged_exe_absfile"
    #   make sure it is read-only; we did sign it!
    chmod 555 "$packaged_exe_absfile"
done
set -u

# TODO: REMOVE
if [ "$TEST_BASH_ON_EXIT" = ON ]; then
    bash
fi

# gh release upload
progress "Uploading release to GitHub"
gh release upload "$RELEASE_TAG" \
    --repo "$GHREPO" \
    "${SETUP_EXE_FILES[@]}" \
    "${UNINSTALL_EXE_FILES[@]}"

# Done
finish_progress

printf "\n\n\nCongratulations!\n" >&2
printf "Go to https://github.com/diskuv/dkml-installer-ocaml/releases/edit/%s\n" "$RELEASE_TAG" >&2
printf "  to turn off the Pre-Release flag. The release now contains signed\n" >&2
printf "  setup.exe and uninstall.exe binaries\n\n" >&2
