120 lines
5.5 KiB
Bash
Executable file
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
|