130 lines
5.3 KiB
Bash
Executable file
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
|