action-hsm-sign/signer/hsm-sign.sh

130 lines
5.3 KiB
Bash
Executable file

#!/bin/sh
# Sign with a key held in an HSM, through PKCS#11.
#
# hsm-sign detached IN OUT a detached signature of IN (RSA PKCS#1 v1.5 over
# SHA-256), written to OUT
# hsm-sign apk IN.apk OUT.apk sign an APK (signature schemes v2 and v3), then verify
# hsm-sign make-cert SUBJECT one-time setup for apk: a self-signed certificate for
# the key, written to the token beside it, and printed
# as PEM
# hsm-sign list what Java sees on the token, for diagnosing an alias
#
# Environment:
# PKCS11_PIN the user PIN (required)
# PKCS11_MODULE the PKCS#11 library; default: opensc's
# PKCS11_SLOT_INDEX which slot, by position in the slot list; default 0
# KEY_ID the key's CKA_ID in hex; default 03
# KEY_LABEL the label make-cert gives the certificate; default apk-cert
# KEY_ALIAS which key apksigner uses, when the token holds more than one
#
# Why apk needs a certificate on the token: an APK signature embeds the signer's
# certificate, and Java's PKCS#11 key store only offers a private key that has a
# certificate with the same CKA_ID. A key used only for detached signatures has none
# until make-cert writes one. Android pins an app to its signing certificate for good,
# so make-cert runs once and its certificate is kept.
set -eu
die() {
echo "hsm-sign: $*" >&2
exit 1
}
[ -n "${PKCS11_PIN:-}" ] || die "PKCS11_PIN is not set"
module="${PKCS11_MODULE:-$(find /usr/lib -name opensc-pkcs11.so | head -n 1)}"
[ -f "$module" ] || die "no PKCS#11 module at '$module'"
slot_index="${PKCS11_SLOT_INDEX:-0}"
key_id="${KEY_ID:-03}"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
chmod 700 "$work"
# SunPKCS11's configuration: which library, which slot.
cat > "$work/pkcs11.cfg" <<EOF
name = HSM
library = $module
slotListIndex = $slot_index
EOF
# What apksigner and keytool need to reach the token through Java. The PIN goes by
# environment variable name, never on a command line.
java_store() {
printf '%s\n' -storetype PKCS11 -providerClass sun.security.pkcs11.SunPKCS11 \
-providerArg "$work/pkcs11.cfg"
}
# apksigner's --provider-class instantiates the provider by reflection, which the module
# system refuses for SunPKCS11 on Java 9 and later. The JDK's own list of providers
# already names SunPKCS11, unconfigured; this points that same entry at the token, so
# the provider is installed by the JVM and apksigner only has to ask for a PKCS11 store.
java_security() {
conf="$(dirname "$(dirname "$(readlink -f "$(command -v java)")")")/conf/security/java.security"
entry="$(grep -o '^security\.provider\.[0-9]*=SunPKCS11' "$conf" | cut -d= -f1)" ||
die "no SunPKCS11 entry in $conf"
printf '%s=SunPKCS11 %s\n' "$entry" "$work/pkcs11.cfg" > "$work/java.security"
echo "-Djava.security.properties=$work/java.security"
}
case "${1:-}" in
detached)
[ $# -eq 3 ] || die "usage: hsm-sign detached IN OUT"
# The signature action-hsm-sign has always made, so signatures verify (and match
# sigstore entries) the same across versions: PKCS#1 v1.5 is deterministic.
pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \
--sign --id "$key_id" --mechanism SHA256-RSA-PKCS --input-file "$2" --output-file "$3"
;;
apk)
[ $# -eq 3 ] || die "usage: hsm-sign apk IN.apk OUT.apk"
set -- "$2" "$3"
alias_args=""
[ -n "${KEY_ALIAS:-}" ] && alias_args="--ks-key-alias ${KEY_ALIAS}"
# shellcheck disable=SC2086 # alias_args is two words or none, by construction
JAVA_TOOL_OPTIONS="$(java_security)" apksigner sign \
--ks NONE --ks-type PKCS11 --ks-pass env:PKCS11_PIN \
$alias_args \
--in "$1" --out "$2"
# Proof, in the log, of what was signed and by whom.
apksigner verify --verbose --print-certs "$2"
;;
make-cert)
[ $# -eq 2 ] || die "usage: hsm-sign make-cert '/CN=...'"
subject="$2"
# OpenSSL's PKCS#11 engine, pointed at the same module, signs the certificate with
# the HSM's key. The PIN is in a file only this process can read, not an argument.
cat > "$work/openssl.cnf" <<EOF
openssl_conf = conf
[conf]
engines = engines
[engines]
pkcs11 = p11
[p11]
engine_id = pkcs11
MODULE_PATH = $module
PIN = $PKCS11_PIN
init = 0
[req]
distinguished_name = dn
[dn]
EOF
uri_id="$(printf '%s' "$key_id" | sed 's/../%&/g')"
# Thirty years: the certificate's dates are not checked for APK signatures, and
# replacing it would mean a new app identity on every device.
OPENSSL_CONF="$work/openssl.cnf" openssl req -new -x509 -sha256 -days 10950 \
-subj "$subject" -engine pkcs11 -keyform engine \
-key "pkcs11:id=${uri_id};type=private" -out "$work/cert.pem"
openssl x509 -in "$work/cert.pem" -outform DER -out "$work/cert.der"
pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \
--write-object "$work/cert.der" --type cert --id "$key_id" --label "${KEY_LABEL:-apk-cert}"
cat "$work/cert.pem"
;;
list)
# shellcheck disable=SC2046 # java_store prints one argument per line
keytool -list -v -storepass:env PKCS11_PIN -keystore NONE $(java_store)
;;
*)
die "usage: hsm-sign detached IN OUT | apk IN OUT | make-cert SUBJECT | list"
;;
esac