handle pins with special characters, use pkcs15-init to write certs, add check-in subcommand
This commit is contained in:
parent
c032498244
commit
e6e5157c94
2 changed files with 95 additions and 14 deletions
18
README.md
18
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
|
||||
|
|
|
|||
|
|
@ -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" <<EOF
|
||||
openssl_conf = conf
|
||||
[conf]
|
||||
|
|
@ -170,7 +193,7 @@ pkcs11 = p11
|
|||
[p11]
|
||||
engine_id = pkcs11
|
||||
MODULE_PATH = $module
|
||||
PIN = $PKCS11_PIN
|
||||
PIN = $pin_conf
|
||||
init = 0
|
||||
[req]
|
||||
distinguished_name = dn
|
||||
|
|
@ -191,16 +214,32 @@ EOF
|
|||
-subj "$subject" -engine pkcs11 -keyform engine \
|
||||
-key "$uri" -out "$work/cert.pem"
|
||||
openssl x509 -in "$work/cert.pem" -outform DER -out "$work/cert.der"
|
||||
if [ "$replacing" = 1 ]; then
|
||||
# Removed first, so tokens that keep several objects with one id do not end up
|
||||
# with two certificates. Cards with one certificate per key may refuse; the
|
||||
# write below then replaces it, and the check after it says whether that worked.
|
||||
if [ -n "${ADMIN_PIN:-}" ]; then
|
||||
# OpenPGP cards (the Nitrokey Pro): writing a certificate needs the Admin PIN, and
|
||||
# goes through OpenSC's pkcs15-init with it (auth id 3), as Nitrokey documents for
|
||||
# the Pro, rather than PKCS#11. OpenSC will not store a certificate under an id
|
||||
# that already has one, so a replacement deletes it in the same run, as
|
||||
# Nitrokey's own instructions do: the store then selects the key's certificate
|
||||
# DO and writes over it. The read-back below checks the result either way.
|
||||
if [ "$replacing" = 1 ]; then
|
||||
set -- --delete-objects cert
|
||||
else
|
||||
set --
|
||||
fi
|
||||
pkcs15-init "$@" --store-certificate "$work/cert.pem" --format pem --id "$key_id" \
|
||||
--auth-id 3 --verify-pin --pin env:ADMIN_PIN >&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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue