action-hsm-sign/signer/test.sh

120 lines
5.5 KiB
Bash
Executable file

#!/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 second token, as the OpenPGP card shows two (User PIN and User PIN (sig)): the
# slot the key is on has to be named, not left to whichever token matches its id.
softhsm2-util --init-token --free --label "hsm-sign-test (sig)" --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