Skip to content

Signing keys ​

Four keys stand between this project and its users, and they are not equally replaceable. This page says what each one is, where it lives, what it costs to lose, and how the backup is made and checked.

Nothing secret is on this page. Fingerprints are public by design: they are what clients pin, and a fingerprint you cannot quote is a fingerprint nobody can verify against.

The keys ​

KeySignsFingerprint
Android release keystoreevery published APK6dbe5c8e…8024589d (SHA-256 of the certificate)
F-Droid index keythe repository index on Pagescc43554e…79f10769
Flatpak repository keythe Flatpak repo on Pages4851E453 0D6C836F D971C3BF 0CB66E5B E9D28FB2
Git tag keyannotated release tags7616A94E 6FEC9EAB 682790D0 414534E1 D89CB2B2

What losing each one costs ​

The Android release keystore is unrecoverable. Android refuses to install an update whose signature does not match the installed app. The Android build is standalone, so every user's calendar lives on their own device: a new key means uninstall, reinstall, and their data is gone. It would also end the arrangement described in Install on Android, where F-Droid distributes this APK instead of re-signing its own build, which exists precisely so that nobody ever meets that wall. scripts/fetch-published-apks.py says the same thing at the line that pins it, and means it.

The F-Droid index key and the Flatpak repository key are painful, not fatal. Clients pin them when they add the repository and reject any later index signed by another key, so a new key means every subscriber removes and re-adds the repository by hand. The Flatpak key is a GPG key, so its revocation certificate sits in the backup, which is the difference between announcing a loss and leaving a live key unaccounted for. The F-Droid index key has no equivalent: a self-signed keystore certificate has no revocation path any client consults, so the only remedy there is saying so on the install page.

The git tag key costs the least. Past tags stay verifiable against the published public key; only future tags under the same identity are affected.

CI variables ​

Set under Settings → CI/CD → Variables, all Protected so only tag and protected-branch pipelines read them, and every secret Masked so an accidental echo is redacted.

VariableHolds
ANDROID_KEYSTORE_BASE64the release keystore, base64, masked
ANDROID_KEYSTORE_PASSWORDstorePassword, masked
ANDROID_KEY_ALIASkeyAlias
ANDROID_KEY_PASSWORDkeyPassword, masked
FDROID_KEYSTORE_BASE64the index keystore, base64, masked
FDROID_KEYSTORE_PASSWORDmasked
FDROID_KEY_ALIAS
FDROID_KEY_PASSWORDmasked
FLATPAK_GPG_KEY_BASE64the armored secret key, base64, masked
FLATPAK_GPG_PASSPHRASEmasked
FLATPAK_GPG_KEY_IDthe fingerprint above, public, neither masked nor secret

The Android certificate fingerprint is deliberately not a variable. It is committed as RELEASE_CERT in scripts/fetch-published-apks.py, because it is public and because a pin that lives beside the code cannot be quietly unset. ANDROID_CERT_SHA256 exists only so a fork can publish a repository of its own without patching the script.

Two things about these variables are worth being blunt about. They are readable, not write-only: any Maintainer can reveal them through the UI or the API. And they are the live copy, not the backup, because they vanish with the project or the account. GitLab also offers hidden variables, which cannot be read back after they are set; that is stronger, and it costs you GitLab as a recovery copy, so it is a choice rather than an upgrade.

When setting one by hand, pipe the value in rather than passing it as an argument. Command-line arguments are readable through /proc/<pid>/cmdline for as long as the process lives:

sh
printf '%s' "$value" | glab variable set NAME -R kreuz-com-group/hyper-calendar -m -p

The Flatpak repository key ​

Generated as a key of its own rather than reusing the tag key, for the same reason the F-Droid index key is separate from the APK key: a repository key has to live in CI, where it is readable, and a tag-signing identity should never have to go there.

RSA 4096 rather than ed25519. Flatpak verifies through gpgme on the client's own GnuPG, and a repository some clients cannot verify is a bad failure for a few kilobytes.

No expiry. An expired key does not prompt anyone to update, it makes clients reject the repository, so the control that matters is the revocation certificate, which is in the backup.

The backup ​

One bundle holds every key, both keystore passwords, the Flatpak passphrase and the tag key passphrase, a revocation certificate for each of the two GPG keys, a README.txt on blast radius, and a MANIFEST.txt of fingerprints and checksums.

Assemble it in $XDG_RUNTIME_DIR, which is a tmpfs, so the plaintext keys never reach a disk. Take the F-Droid fingerprint for the manifest from the published install page rather than from the keystore beside it, because what clients pinned is what that page told them to pin. Run the drill below against the plaintext bundle, then encrypt it, check the result decrypts, and keep only the encrypted file.

Two properties are load-bearing:

  • It decrypts with a passphrase alone. Encrypt it symmetrically (gpg --symmetric --cipher-algo AES256), never to a GPG key. The tag key is inside the bundle, and encrypting the bundle to the key it contains is the circular dependency this exists to remove: the password that opens a keystore must not be sealed with a key that is itself unbacked.
  • The manifest names identities, not just checksums. Checksums prove the files did not rot. Fingerprints prove they are the right keys.

The drill ​

A backup nobody has restored is a guess. Run it when you make a bundle, and again whenever you move it. Unpack the bundle somewhere temporary and check, in throwaway keyrings:

  • each keystore opens with the password stored beside it,
  • the Flatpak key signs with the passphrase stored beside it,
  • the Android certificate matches RELEASE_CERT in scripts/fetch-published-apks.py, not the bundle's own copy. A bundle holding the wrong keystore then fails here instead of at the next release,
  • each fingerprint in MANIFEST.txt matches the table at the top of this page.