diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 3bdb6b2..64f40ec 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -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' diff --git a/README.md b/README.md index 4361b35..6acaf57 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/android/signing-cert.pem b/android/signing-cert.pem new file mode 100644 index 0000000..2a05686 --- /dev/null +++ b/android/signing-cert.pem @@ -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-----