#!/bin/sh # The signer, end to end, against SoftHSM instead of the real HSM. # # test.sh detached signatures only # test.sh UNSIGNED.apk [SIGNED.apk] and APK signing; the second keeps the signed APK # # Builds the signer image exactly as the action does, adds SoftHSM to it, and makes a # token with an RSA key and no certificate, the state the real HSM's key is in. Then it # makes a detached signature and checks it with openssl against the token's public key # and, given an APK, runs make-cert and apk, and has apksigner verify the result. # Everything but pcscd and the hardware is the code that runs in CI. Uses docker, or # podman when there is no docker. Scratch space comes from mktemp, so TMPDIR moves it. set -eu usage() { echo "usage: test.sh [UNSIGNED.apk [SIGNED.apk]]" >&2 exit 2 } [ $# -le 2 ] || usage apk_in="" apk_out="${2:-}" if [ $# -ge 1 ]; then [ -f "$1" ] || usage apk_in="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" fi here="$(cd "$(dirname "$0")" && pwd)" engine="$(command -v docker || command -v podman)" || { echo "need docker or podman" >&2; exit 2; } "$engine" build -q -t action-hsm-sign-signer:test "$here" >/dev/null "$engine" build -q -t action-hsm-sign-signer:softhsm - >/dev/null <<'EOF' FROM action-hsm-sign-signer:test USER root RUN apt-get update && apt-get install -y --no-install-recommends softhsm2 && rm -rf /var/lib/apt/lists/* USER user EOF out_dir="$(mktemp -d)" trap 'rm -rf "$out_dir"' EXIT chmod 777 "$out_dir" set -- -v "$out_dir:/out" if [ -n "$apk_in" ]; then set -- "$@" -v "$apk_in:/home/user/in.apk:ro" fi # shellcheck disable=SC2016 # expanded inside the container "$engine" run --rm --entrypoint /bin/sh "$@" action-hsm-sign-signer:softhsm -euc ' export SOFTHSM2_CONF="$HOME/softhsm2.conf" mkdir -p "$HOME/tokens" echo "directories.tokendir = $HOME/tokens" > "$SOFTHSM2_CONF" export PKCS11_MODULE="$(find /usr/lib -name libsofthsm2.so | head -n 1)" export PKCS11_PIN=123456 KEY_ID=03 softhsm2-util --init-token --free --label hsm-sign-test --pin "$PKCS11_PIN" --so-pin 87654321 >/dev/null # A key and nothing else, as on the real HSM before make-cert. pkcs11-tool --module "$PKCS11_MODULE" --login --pin env:PKCS11_PIN \ --keypairgen --key-type rsa:4096 --id "$KEY_ID" --label test-key >/dev/null pkcs11-tool --module "$PKCS11_MODULE" --read-object --type pubkey --id "$KEY_ID" | openssl pkey -pubin -inform DER -out "$HOME/pub.pem" want="$(openssl pkey -pubin -in "$HOME/pub.pem" -outform DER | sha256sum | cut -d" " -f1)" echo "== detached" echo "an artifact" > artifact hsm-sign detached artifact /out/artifact.sig # What rekor and users do with the published public key. openssl dgst -sha256 -verify "$HOME/pub.pem" -signature /out/artifact.sig artifact [ -f in.apk ] || exit 0 certsha() { openssl x509 -in "$1" -outform DER | sha256sum | cut -d" " -f1; } certs() { pkcs11-tool --module "$PKCS11_MODULE" --list-objects --type cert 2>/dev/null | grep -cE "^ *ID: *$KEY_ID\$" || true; } echo "== make-cert" hsm-sign make-cert "/CN=hsm-sign test signer" > /out/cert.pem openssl x509 -in /out/cert.pem -noout -subject -fingerprint -sha256 first="$(certsha /out/cert.pem)" echo "== make-cert again, without REPLACE_CERT: must refuse" if hsm-sign make-cert "/CN=another signer" > /out/refused.pem 2> /out/refused.err; then echo "make-cert replaced a certificate without REPLACE_CERT=1"; exit 1 fi grep -q "already has a certificate" /out/refused.err [ "$(certs)" = 1 ] echo "refused, and the token still has one certificate" echo "== make-cert with REPLACE_CERT=1" REPLACE_CERT=1 hsm-sign make-cert "/CN=hsm-sign test signer 2" > /out/cert2.pem second="$(certsha /out/cert2.pem)" [ "$first" != "$second" ] [ "$(certs)" = 1 ] echo "replaced, and the token has one certificate: $second" echo "== apk, pinned to the current certificate" APK_CERT_SHA256="$second" hsm-sign apk in.apk /out/signed.apk echo "== apk, pinned to the replaced certificate: must fail" if APK_CERT_SHA256="$first" hsm-sign apk in.apk /out/wrong.apk > /out/wrong.log 2>&1; then echo "apk signed with a certificate other than the pinned one"; exit 1 fi grep -q "not the expected" /out/wrong.log [ ! -e /out/wrong.apk ] echo "refused, and no APK left behind" echo "== independent verify" apksigner verify --verbose /out/signed.apk # The APK is signed by the key, not merely by a certificate naming it: the public key # apksigner reports is the token'"'"'s, and the certificate is the current one. printed="$(apksigner verify --verbose --print-certs /out/signed.apk)" got="$(echo "$printed" | sed -n "s/^Signer #1 public key SHA-256 digest: //p")" gotcert="$(echo "$printed" | sed -n "s/^Signer #1 certificate SHA-256 digest: //p")" echo "token public key sha256: $want" echo "APK signer key sha256: $got" echo "APK certificate sha256: $gotcert" [ "$want" = "$got" ] [ "$gotcert" = "$second" ] ' echo "test.sh: detached signature verified" if [ -n "$apk_in" ]; then if [ -n "$apk_out" ]; then cp "$out_dir/signed.apk" "$apk_out" fi echo "test.sh: signed and verified $(basename "$apk_in")" fi