lock in apk cert

This commit is contained in:
Emil Lerch 2026-10-04 12:40:43 -07:00
parent 59feb4b5e8
commit 95b2db5e02
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 62 additions and 13 deletions

View file

@ -181,13 +181,15 @@ jobs:
# included, each logged to sigstore. One power cycle of the HSM for all of it.
# The APK's signature is the app's identity on every device that installs it, for
# good: the key is the HSM's, and its certificate is the one `make-cert` wrote to
# the token (README, "Signing").
# the token (android/signing-cert.pem; README, "Signing"). apk_cert_sha256 pins
# it: an APK signed with any other certificate fails here instead of shipping.
id: sign
uses: https://git.lerch.org/lobo/action-hsm-sign@v3
uses: https://git.lerch.org/lobo/action-hsm-sign@v3.1
with:
pin: ${{ secrets.HSM_USER_PIN }}
apk: unsigned/app-release-unsigned.apk
apk_output: dist/tally.apk
apk_cert_sha256: '4b41d0b1b3a184342fdc348e67563c8ca6cbc3765fa7341dee8a4a8fbb5d4aad'
files: dist/*
public_key: 'https://emil.lerch.org/serverpublic.pem'
uhub_control: 'true'

View file

@ -225,28 +225,47 @@ included. The action power cycles the HSM on its smart USB hub for the duration,
the private key never leaves the HSM.
An APK signature carries a certificate, and Java only offers a PKCS#11 key that has one
on the token. **Once, before the first signed build**, make a self-signed certificate for
the key and store it beside the key. On the HSM host, with the HSM powered, from a clone
of action-hsm-sign:
on the token; whatever certificate the token holds for the key is used. It is not
Tally's own: the token holds one certificate per key, so every app signed with the key
carries it, and its subject is generic. It is the identity of all of them on every
device: an APK signed with a different one is refused as an update, and users uninstall
and reinstall. So it is chosen once, kept in this repository as
[android/signing-cert.pem](android/signing-cert.pem) (it is public), and pinned in the
workflow by its SHA-256 (`apk_cert_sha256`), so a certificate replaced on the token fails
the build instead of silently changing the app's identity.
It was made on the HSM host, with the HSM powered, from a clone of action-hsm-sign. The
certificate is assembled in the container and signed on the token with the user PIN, so
the key never leaves it, and written to the card with the Admin PIN (the card's own
rule for certificates). `REPLACE_CERT=1` because the key already had a certificate (the
card's 2023 "AUT certificate", which signed the very first build, 255f239):
```
docker build -t hsm-signer signer
read -rs PKCS11_PIN && export PKCS11_PIN
read -rs ADMIN_PIN && export ADMIN_PIN
docker run --rm -v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm:ro \
-e PKCS11_PIN hsm-signer make-cert '/CN=Tally/O=lerch.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 -e REPLACE_CERT=1 hsm-signer make-cert '/CN=Emil Lerch/O=lerch.org'
```
It prints the certificate; keep a copy. The key is `03` by default, the one the detached
signatures use (`KEY_ID` changes it). The certificate is the app's identity on every
device from then on: replacing it means users uninstall and reinstall. Its 30-year
validity does not matter for APKs, whose certificate dates Android does not check.
`check-pin` tries each PIN once first; a wrong PIN costs a try, and the card locks a PIN
after three. `make-cert` prints the certificate and its `sha256 (apk_cert_sha256)`:
`4b41d0b1b3a184342fdc348e67563c8ca6cbc3765fa7341dee8a4a8fbb5d4aad` (`CN=Emil Lerch,
O=lerch.org`, valid to 2056). The key is `03` by default, the one the detached
signatures use (`KEY_ID` changes it); a new certificate does not change the key, so the
detached signatures and `serverpublic.pem` are unaffected. Its 30-year validity does not
matter for APKs, whose certificate dates Android does not check, but is long enough for
Google Play (validity past 2033-10-22).
`mise run apk-signer-test` builds the release APK and runs action-hsm-sign's
`signer/test.sh` on it, at the version the workflow uses (or a local checkout named by
`ACTION_HSM_SIGN`). That is the same signer image against SoftHSM: a token with a bare
key, a detached signature, `make-cert`, APK signing, `apksigner verify`, and a check that
the APK's signing key is the token's. What it cannot cover is the HSM itself and the
host's pcscd, so the first run on the HSM runner is the real test of those.
key, a detached signature, `make-cert` (and its refusal to replace a certificate), APK
signing with the certificate pinned, `apksigner verify`, and a check that the APK's
signing key is the token's. What it cannot cover is the HSM itself and the host's pcscd,
so the first run on the HSM runner is the real test of those.
## License

28
android/signing-cert.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE2zCCAsMCFHrZq0V4qQfkUv0emf3CkUnuZztZMA0GCSqGSIb3DQEBCwUAMCkx
EzARBgNVBAMMCkVtaWwgTGVyY2gxEjAQBgNVBAoMCWxlcmNoLm9yZzAgFw0yNjEw
MDQxOTE1MjhaGA8yMDU2MDkyNjE5MTUyOFowKTETMBEGA1UEAwwKRW1pbCBMZXJj
aDESMBAGA1UECgwJbGVyY2gub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEA0rlFG2I641ARuKWcyFVrL81pwP8O0KGSd7uYkLpf4F3ut6VUZOsaGzCz
fACXu6x57DFH1DMZkaf71vg+btRUw/b4eGmTXiAfq5D4CM5Bk1kgxOqat5JL+YF1
EunxYV3EqAhtaL9FLrafNL+guX/slQ7ol8RjlWddJqkS+tpoHu6XrmMgChosDfHY
cZeGI/B8wkiD4sLVy/VwnlyGPK1Ww4o/EnIwiRH6m3bP/bxfKslXg+Z6mUch530F
4O75fhUzBzzrvbluKEH+9uWviVHHdNM3NSmNX5xYqaVr9OdiMHO18lynNZK8DBW4
vsrpGO5GX0F5spafa5bBzTPdtJw52v5Mw5pTtKtocwn9ML26lq4KEIBW5aANKvVc
hcyFOELJFMzvuE4pxvVzM0SlpG7p7hU7cz7kE83QnZzGup1d9vxpCbBtDIsaB+B5
/43YHLc+mTCzTdHNRy91RwYiZKOL3zEsMyWHJXwoO0ELaET0gmrRymbEETdCoWNP
qVKxea+zuPpeGGamIPoATgxnVAgfYlnFhSiMDuwERiJ1HIUqlcVaFAETLw/jdvih
TgS7FJ7xfZy8+nT+sQt7P2s77A8ogX+NsRyMNic8/pCVOkfWYGCf5n5aOu9EGXiR
LHn1X28ZJIfuxJ16JlclkIxnBHp4okJ1mYWHuN2SRFvXu6tCOrkCAwEAATANBgkq
hkiG9w0BAQsFAAOCAgEAOwGr7hXIv9+2bx4ngGYAq9FRJeAesaG4inn8L9eSQq7S
peJbtyL4BO5/wAZt6gD/qTVWRVFiaLDvs2oyoh+mv74TsAyYHUFKHVptMOaG6OBu
3rGdDbQFKpi62/v+aC9zakgNx4oH+0Ev/WbWf3u+VXlZGnF51eBZh3E7B4DW5TkG
sMEaXP1wOMdx2cZWg0e84qaHNi4eSUIgB0QSwbXlw2AZEcTVcqAws7sLvhUgphxG
aH9n2+a0CCfPZRlyJFJlQ+DXohOOjVENG++PbxPeMmRi0EvmVAhbnrMGhVi6ayxH
KmoRwOfoe+Ti6kStPGaYXLIVVA2BKnBCYL3pi/ZWCKSsueJuKLUYzddsZe9exirr
y8/Jpct3KvlYaA4dMIaUlYRAGn609hWt7sAqNEnkSHsjqtycPbAi74+AulUpdcWO
GL+77YiCx1I4+rXBFLV43kz+Fw0A9btdrv4/gC+siAzDYv7Y21+rM1IxAU+xnHnm
btvc04C6CDG2UpPSSdJRm9yugtXk7AH4AtboKUvpCWbwHK0Kd9kAUbJ0Mbbuxf13
7YppxMGGSSJjG+XV917tSWG+gNNz2V9KLxsGnCJIRRHaGsBfn8p+TY05PvRFcj7Z
vxxEGvrZMBaQz2noQjKsoAsWjr199mfFKm4/rxHpHe3vI3kzfCHaU0MVVCUGrtk=
-----END CERTIFICATE-----