From e6e5157c94a4db738573dfb776c69325b79efeb3 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sun, 4 Oct 2026 12:35:56 -0700 Subject: [PATCH] handle pins with special characters, use pkcs15-init to write certs, add check-in subcommand --- README.md | 18 +++++++-- signer/hsm-sign.sh | 91 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 95 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 272a7a7..98442c4 100644 --- a/README.md +++ b/README.md @@ -78,16 +78,28 @@ every device, and an APK signed with a different certificate is refused as an update (users uninstall and reinstall). To give the key a certificate, on the HSM host, with the HSM powered, from a -clone of this repository (the certificate is assembled in the container and -signed on the token, so the key never leaves it): +clone of this repository. The certificate is assembled in the container and +signed on the token with the user PIN, so the key never leaves it. Writing it +to an OpenPGP card (such as the Nitrokey Pro) takes the card's Admin PIN as +well: give it as `ADMIN_PIN`, and `make-cert` writes the certificate with +OpenSC's `pkcs15-init`, as Nitrokey documents for those cards. Without +`ADMIN_PIN`, the user PIN writes it over PKCS#11 (SoftHSM and similar tokens). ```sh docker build -t hsm-signer signer read -rs PKCS11_PIN && export PKCS11_PIN +read -rs ADMIN_PIN && export ADMIN_PIN +# Each PIN tried once: a wrong one costs a try, and a card locks a PIN after three docker run --rm -v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm:ro \ - -e PKCS11_PIN hsm-signer make-cert '/CN=Your Name/O=example.org' + -e PKCS11_PIN -e ADMIN_PIN hsm-signer check-pin +docker run --rm -v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm:ro \ + -e PKCS11_PIN -e ADMIN_PIN hsm-signer make-cert '/CN=Your Name/O=example.org' ``` +`check-pin` and `make-cert` refuse to try a PIN that is on its last try. A +correct PIN resets the count, so a normal signing run (with the PIN CI uses) +gets the tries back. + If the key already has a certificate, `make-cert` shows it and stops. Add `-e REPLACE_CERT=1` to replace it (keep the PEM it showed, if anything else uses that certificate). The new certificate is read back from the token to diff --git a/signer/hsm-sign.sh b/signer/hsm-sign.sh index c1ad07a..5d7da95 100755 --- a/signer/hsm-sign.sh +++ b/signer/hsm-sign.sh @@ -8,6 +8,8 @@ # 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 check-pin check PKCS11_PIN (and ADMIN_PIN, if set) with one login +# each, before make-cert needs them # hsm-sign list what Java sees on the token, for diagnosing an alias # # Environment: @@ -20,6 +22,8 @@ # 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 +# ADMIN_PIN make-cert: the PIN that may write objects (pkcs15-init, auth id 3); +# required on OpenPGP cards (their Admin PIN). Unset: the user PIN # # 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 @@ -72,6 +76,13 @@ token_label() { s == n && /^[[:space:]]*token label[[:space:]]*:/ { sub(/^[^:]*:[[:space:]]*/, ""); print; exit }' } +# The flags of that token (PIN tries among them), as pkcs11-tool prints them. +token_flags() { + pkcs11-tool --module "$module" --list-slots 2>/dev/null | awk -v n="$slot_index" ' + /^Slot [0-9]+ / { s = $2 } + s == n && /^[[:space:]]*token flags[[:space:]]*:/ { sub(/^[^:]*:[[:space:]]*/, ""); print; exit }' +} + # $1 percent-encoded, every byte, for a pkcs11: URI (RFC 7512). uri_encode() { printf '%s' "$1" | od -An -v -tx1 | tr -d ' \n' | tr 'a-f' 'A-F' | sed 's/../%&/g' @@ -145,6 +156,15 @@ case "${1:-}" in make-cert) [ $# -eq 2 ] || die "usage: hsm-sign make-cert '/CN=...'" subject="$2" + # make-cert logs in up to three times, and a wrong PIN costs a try each time: never + # start on a token with a PIN on its last try. (A correct PIN resets the count, so a + # normal signing run with the known-good PIN is the way back.) + flags="$(token_flags)" + case "$flags" in + *"final user PIN try"* | *"user PIN locked"* | *"final SO PIN try"* | *"SO PIN locked"*) + die "the token reports '${flags}': not risking a login. Sign once with the correct PIN to reset the count, then try again" + ;; + esac # 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. @@ -160,7 +180,10 @@ case "${1:-}" in 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. + # the HSM's key. The PIN is in a file only this process can read, not an argument, + # with every character but letters and digits backslash-escaped: in OpenSSL's config + # syntax # starts a comment and $ a variable, which would change the PIN sent. + pin_conf="$(printf '%s' "$PKCS11_PIN" | sed 's/[^A-Za-z0-9]/\\&/g')" cat > "$work/openssl.cnf" <&2 || + die "pkcs15-init could not store the certificate; the read-back of what the card has now: $(read_cert "$work/now.der" && sha256sum "$work/now.der" | cut -d' ' -f1 || echo none). If none, put the old one back (the PEM above): pkcs15-init --store-certificate OLD.pem --id ${key_id} --auth-id 3 --verify-pin --pin env:ADMIN_PIN" + else + if [ "$replacing" = 1 ]; then + # Removed first, so tokens that keep several objects with one id do not end up + # with two certificates. + pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \ + --delete-object --type cert --id "$key_id" >&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 \ - --delete-object --type cert --id "$key_id" >&2 || - echo "hsm-sign: could not delete the old certificate; writing over it" >&2 + --write-object "$work/cert.der" --type cert --id "$key_id" --label "${KEY_LABEL:-apk-cert}" >&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" @@ -212,12 +251,42 @@ EOF cat "$work/cert.pem" ;; + check-pin) + # One login with PKCS11_PIN and, if ADMIN_PIN is set, one admin PIN check: says + # whether each is right before make-cert needs them. A wrong PIN costs a try, as + # it would anywhere, so a PIN on its last try is not checked. + flags="$(token_flags)" + echo "token flags: ${flags}" + case "$flags" in + *"final user PIN try"* | *"user PIN locked"*) + die "user PIN is on its last try or locked: not checking it" ;; + esac + if pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \ + --list-objects --type privkey >/dev/null 2>&1; then + echo "user PIN (PKCS11_PIN): correct" + else + die "user PIN (PKCS11_PIN): WRONG (one try used)" + fi + if [ -n "${ADMIN_PIN:-}" ]; then + case "$flags" in + *"final SO PIN try"* | *"SO PIN locked"*) + die "admin PIN is on its last try or locked: not checking it" ;; + esac + # The OpenPGP card's own VERIFY for PW3, the Admin PIN; OpenPGP cards only. + if openpgp-tool --verify CHV3 --pin env:ADMIN_PIN >/dev/null 2>&1; then + echo "admin PIN (ADMIN_PIN): correct" + else + die "admin PIN (ADMIN_PIN): WRONG (one try used)" + fi + fi + ;; + 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" + die "usage: hsm-sign detached IN OUT | apk IN OUT | make-cert SUBJECT | check-pin | list" ;; esac