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).
|
update (users uninstall and reinstall).
|
||||||
|
|
||||||
To give the key a certificate, on the HSM host, with the HSM powered, from a
|
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
|
clone of this repository. The certificate is assembled in the container and
|
||||||
signed on the token, so the key never leaves it):
|
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
|
```sh
|
||||||
docker build -t hsm-signer signer
|
docker build -t hsm-signer signer
|
||||||
read -rs PKCS11_PIN && export PKCS11_PIN
|
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 \
|
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
|
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
|
`-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
|
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,
|
# signed on the token, written to it beside the key,
|
||||||
# read back, and printed as PEM. Refuses if the key
|
# read back, and printed as PEM. Refuses if the key
|
||||||
# already has a certificate, unless REPLACE_CERT=1
|
# 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
|
# hsm-sign list what Java sees on the token, for diagnosing an alias
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
|
|
@ -20,6 +22,8 @@
|
||||||
# APK_CERT_SHA256 apk: the certificate the APK must be signed with (the SHA-256 of
|
# 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
|
# its DER, as make-cert and apk print it); any other is an error
|
||||||
# REPLACE_CERT make-cert: 1 to replace an existing certificate
|
# 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
|
# 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, 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 }'
|
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).
|
# $1 percent-encoded, every byte, for a pkcs11: URI (RFC 7512).
|
||||||
uri_encode() {
|
uri_encode() {
|
||||||
printf '%s' "$1" | od -An -v -tx1 | tr -d ' \n' | tr 'a-f' 'A-F' | sed 's/../%&/g'
|
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)
|
make-cert)
|
||||||
[ $# -eq 2 ] || die "usage: hsm-sign make-cert '/CN=...'"
|
[ $# -eq 2 ] || die "usage: hsm-sign make-cert '/CN=...'"
|
||||||
subject="$2"
|
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
|
# 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,
|
# with it, so an existing one is not replaced by accident: it is shown, and kept,
|
||||||
# unless REPLACE_CERT=1.
|
# unless REPLACE_CERT=1.
|
||||||
|
|
@ -160,7 +180,10 @@ case "${1:-}" in
|
||||||
replacing=0
|
replacing=0
|
||||||
fi
|
fi
|
||||||
# OpenSSL's PKCS#11 engine, pointed at the same module, signs the certificate with
|
# 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
|
cat > "$work/openssl.cnf" <<EOF
|
||||||
openssl_conf = conf
|
openssl_conf = conf
|
||||||
[conf]
|
[conf]
|
||||||
|
|
@ -170,7 +193,7 @@ pkcs11 = p11
|
||||||
[p11]
|
[p11]
|
||||||
engine_id = pkcs11
|
engine_id = pkcs11
|
||||||
MODULE_PATH = $module
|
MODULE_PATH = $module
|
||||||
PIN = $PKCS11_PIN
|
PIN = $pin_conf
|
||||||
init = 0
|
init = 0
|
||||||
[req]
|
[req]
|
||||||
distinguished_name = dn
|
distinguished_name = dn
|
||||||
|
|
@ -191,16 +214,32 @@ EOF
|
||||||
-subj "$subject" -engine pkcs11 -keyform engine \
|
-subj "$subject" -engine pkcs11 -keyform engine \
|
||||||
-key "$uri" -out "$work/cert.pem"
|
-key "$uri" -out "$work/cert.pem"
|
||||||
openssl x509 -in "$work/cert.pem" -outform DER -out "$work/cert.der"
|
openssl x509 -in "$work/cert.pem" -outform DER -out "$work/cert.der"
|
||||||
if [ "$replacing" = 1 ]; then
|
if [ -n "${ADMIN_PIN:-}" ]; then
|
||||||
# Removed first, so tokens that keep several objects with one id do not end up
|
# OpenPGP cards (the Nitrokey Pro): writing a certificate needs the Admin PIN, and
|
||||||
# with two certificates. Cards with one certificate per key may refuse; the
|
# goes through OpenSC's pkcs15-init with it (auth id 3), as Nitrokey documents for
|
||||||
# write below then replaces it, and the check after it says whether that worked.
|
# 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 \
|
pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \
|
||||||
--delete-object --type cert --id "$key_id" >&2 ||
|
--write-object "$work/cert.der" --type cert --id "$key_id" --label "${KEY_LABEL:-apk-cert}" >&2
|
||||||
echo "hsm-sign: could not delete the old certificate; writing over it" >&2
|
|
||||||
fi
|
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.
|
# Read back: exactly one certificate for the key, and it is the new one.
|
||||||
count="$(cert_count)"
|
count="$(cert_count)"
|
||||||
[ "$count" = 1 ] || die "the token has ${count} certificates for key ${key_id} after writing, expected 1"
|
[ "$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"
|
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)
|
list)
|
||||||
# shellcheck disable=SC2046 # java_store prints one argument per line
|
# shellcheck disable=SC2046 # java_store prints one argument per line
|
||||||
keytool -list -v -storepass:env PKCS11_PIN -keystore NONE $(java_store)
|
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
|
esac
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue