#!/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 setup for apk: a self-signed certificate for the key, # signed on the token, written to it beside the key, # read back, and printed as PEM. Refuses if the key # already has a certificate, unless REPLACE_CERT=1 # 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 # APK_CERT_SHA256 apk: the certificate the APK must be signed with (the SHA-256 of # its DER, as make-cert and apk print it); any other is an error # REPLACE_CERT make-cert: 1 to replace an existing certificate # # 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. Whatever certificate is there is used, whether # make-cert wrote it or not. Android pins an app to its signing certificate for good, # so it is chosen once and kept (and pinned with APK_CERT_SHA256). 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}" # Lower case and an even number of digits, as pkcs11-tool lists ids and as the pkcs11: # URI below needs them. key_id="$(printf '%s' "${KEY_ID:-03}" | tr 'A-F' 'a-f')" [ $((${#key_id} % 2)) -eq 0 ] || key_id="0${key_id}" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT chmod 700 "$work" # The certificate for the key, in DER, to $1; fails if there is none. Certificates are # public objects: no login. read_cert() { pkcs11-tool --module "$module" --slot-index "$slot_index" \ --read-object --type cert --id "$key_id" --output-file "$1" >/dev/null 2>&1 && [ -s "$1" ] } # How many certificates the token holds for the key. cert_count() { pkcs11-tool --module "$module" --slot-index "$slot_index" --list-objects --type cert 2>/dev/null | grep -cE "^ *ID: *${key_id}\$" || true } # A certificate (DER, $1) as a person and as apk_cert_sha256 want to see it. describe_cert() { openssl x509 -inform DER -in "$1" -noout -subject -startdate -enddate echo "sha256 (apk_cert_sha256): $(sha256sum "$1" | cut -d' ' -f1)" } # SunPKCS11's configuration: which library, which slot. cat > "$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. verified="$(apksigner verify --verbose --print-certs "$2")" echo "$verified" signers="$(echo "$verified" | sed -n 's/^Number of signers: //p')" [ "$signers" = 1 ] || die "expected 1 signer, apksigner reports '${signers}'" got="$(echo "$verified" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')" echo "hsm-sign: signed with the certificate whose sha256 (apk_cert_sha256) is ${got}" # The certificate is the app's identity: with APK_CERT_SHA256 set, an APK signed # with any other certificate (one replaced on the token, a second key) is an error, # not an update every installed copy refuses. if [ -n "${APK_CERT_SHA256:-}" ]; then want="$(printf '%s' "${APK_CERT_SHA256}" | tr -d ': ' | tr 'A-F' 'a-f')" if [ "$got" != "$want" ]; then rm -f "$2" die "the APK was signed with certificate ${got}, not the expected ${want} (APK_CERT_SHA256)" fi echo "hsm-sign: that is the expected certificate" fi ;; make-cert) [ $# -eq 2 ] || die "usage: hsm-sign make-cert '/CN=...'" subject="$2" # The certificate is the app's identity on every device that installs an APK signed # with it, so an existing one is not replaced by accident: it is shown, and kept, # unless REPLACE_CERT=1. if read_cert "$work/old.der"; then echo "hsm-sign: key ${key_id} already has a certificate:" >&2 describe_cert "$work/old.der" >&2 openssl x509 -inform DER -in "$work/old.der" >&2 [ "${REPLACE_CERT:-}" = 1 ] || die "not replacing it. Set REPLACE_CERT=1 to replace it (keep the PEM above if anything uses it)" echo "hsm-sign: REPLACE_CERT=1: replacing it" >&2 replacing=1 else replacing=0 fi # 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" <&2 || echo "hsm-sign: could not delete the old certificate; writing over it" >&2 fi 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}" >&2 # Read back: exactly one certificate for the key, and it is the new one. count="$(cert_count)" [ "$count" = 1 ] || die "the token has ${count} certificates for key ${key_id} after writing, expected 1" read_cert "$work/check.der" || die "could not read the certificate back" cmp -s "$work/cert.der" "$work/check.der" || die "the certificate read back is not the one written" echo "hsm-sign: written and read back:" >&2 describe_cert "$work/cert.der" >&2 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