Merge pull request 'Take sign-apk.sh out of the default publish path; document the never-an-app-key invariant' (#10) from builder-signs-repo-distributes into main
This commit was merged in pull request #10.
This commit is contained in:
@@ -1,10 +1,30 @@
|
|||||||
# fdroid-repo-ci
|
# fdroid-repo-ci
|
||||||
|
|
||||||
Shared tooling to sign a prebuilt APK, build a signed F-Droid repo index over
|
Shared tooling to build a signed F-Droid repo index over already-signed APKs
|
||||||
it, and publish the result to an S3-compatible bucket -- consumed as a
|
and publish the result to an S3-compatible bucket -- consumed as a composite
|
||||||
composite action by whichever repository currently holds the signing keys
|
action by whichever repository currently holds the (index) signing key and
|
||||||
and runs the publish. Which repository that is has changed more than once,
|
runs the publish. Which repository that is has changed more than once, so
|
||||||
so nothing here names it; see "Not hard-coded" below.
|
nothing here names it; see "Not hard-coded" below.
|
||||||
|
|
||||||
|
**The builder signs; this repo only distributes.** Settled on
|
||||||
|
weblib-viewer#19: an APK can reach a device via a git release *and* via
|
||||||
|
F-Droid, and updates from either route have to install over each other,
|
||||||
|
which needs the *same* app signature on both -- so signing has to happen
|
||||||
|
once, at the build, not again here. This is also simply how F-Droid's
|
||||||
|
binary-repo mode works: `fdroid update` **reads** each APK's existing
|
||||||
|
signature and never creates one (see "The repo index key is not the APK
|
||||||
|
signing key" below). `sign-apk.sh` exists only for the exceptional consumer
|
||||||
|
whose build genuinely emits an unsigned artifact -- it is not part of the
|
||||||
|
default pipeline and `action.yml` does not call it.
|
||||||
|
|
||||||
|
**This repository is never handed an app signing key.** The only key it
|
||||||
|
ever receives is the **repo index key**, which signs `index-v1.jar`/
|
||||||
|
`entry.jar` and identifies the repo itself to a client (pinned via
|
||||||
|
`?fingerprint=` in the repo URL) -- never an individual app. A workflow
|
||||||
|
change that starts passing an APK keystore or its password into this repo's
|
||||||
|
scripts (other than as an explicit, opt-in `sign-apk.sh` argument for the
|
||||||
|
unsigned-artifact exception above) is a sign the invariant has been broken,
|
||||||
|
not a new normal.
|
||||||
|
|
||||||
Same shape as [weblib-ci], and public for the same measured reason: a
|
Same shape as [weblib-ci], and public for the same measured reason: a
|
||||||
private repository cannot be consumed cross-repo via `uses:` on this Gitea
|
private repository cannot be consumed cross-repo via `uses:` on this Gitea
|
||||||
@@ -22,7 +42,7 @@ here.
|
|||||||
| script | what it does |
|
| script | what it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `next-version-code.sh` | print the next monotonic `versionCode` for an application id and a commit's committer date, read from a repo index -- called before the build, CI and a manual build alike (see "One versionCode rule" below) |
|
| `next-version-code.sh` | print the next monotonic `versionCode` for an application id and a commit's committer date, read from a repo index -- called before the build, CI and a manual build alike (see "One versionCode rule" below) |
|
||||||
| `sign-apk.sh` | check alignment, then sign a prebuilt unsigned release APK with a keystore the caller supplies |
|
| `sign-apk.sh` | **the exception, not the default flow** (see above) -- check alignment, then sign a prebuilt unsigned release APK with a keystore the caller supplies, for a consumer whose build does not already produce a signed APK |
|
||||||
| `fdroid-update.sh` | run `fdroid update` over a `repo/` directory of already-signed APKs and produce the signed index |
|
| `fdroid-update.sh` | run `fdroid update` over a `repo/` directory of already-signed APKs and produce the signed index |
|
||||||
| `publish.sh` | upload the resulting repo tree to an S3-compatible bucket -- files first, index last, and refuses to overwrite an already-published APK (see "Publish ordering and safety" below) |
|
| `publish.sh` | upload the resulting repo tree to an S3-compatible bucket -- files first, index last, and refuses to overwrite an already-published APK (see "Publish ordering and safety" below) |
|
||||||
| `with-nixpkgs.sh` | run a command with one or more nixpkgs packages on PATH, pinned to the revision in `nixpkgs-pin` |
|
| `with-nixpkgs.sh` | run a command with one or more nixpkgs packages on PATH, pinned to the revision in `nixpkgs-pin` |
|
||||||
@@ -40,6 +60,11 @@ below for what that costs and why it is still the right call here.
|
|||||||
|
|
||||||
## Using it
|
## Using it
|
||||||
|
|
||||||
|
This is the default flow: the APK arrives at this action **already signed**
|
||||||
|
by the builder (weblib-viewer#19), so the pipeline only computes the next
|
||||||
|
`versionCode`, indexes and publishes. It never touches an app signing key --
|
||||||
|
only the repo index key, in the `fdroid-update.sh` step.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- id: ci
|
- id: ci
|
||||||
@@ -49,14 +74,8 @@ below for what that costs and why it is still the right call here.
|
|||||||
version_code=$(bash "${{ steps.ci.outputs.path }}/next-version-code.sh" \
|
version_code=$(bash "${{ steps.ci.outputs.path }}/next-version-code.sh" \
|
||||||
"$FDROID_REPO_INDEX_URL" "$APPLICATION_ID" "$(git log -1 --format=%ct)")
|
"$FDROID_REPO_INDEX_URL" "$APPLICATION_ID" "$(git log -1 --format=%ct)")
|
||||||
echo "version_code=$version_code" >> "$GITHUB_ENV"
|
echo "version_code=$version_code" >> "$GITHUB_ENV"
|
||||||
# ... feed $version_code into whatever builds app-release-unsigned.apk
|
# ... feed $version_code into whatever builds the already-signed
|
||||||
|
# app-release.apk
|
||||||
- env:
|
|
||||||
APK_KEYSTORE_PASSWORD: ${{ secrets.APK_KEYSTORE_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
bash "${{ steps.ci.outputs.path }}/sign-apk.sh" \
|
|
||||||
app-release-unsigned.apk app-release.apk \
|
|
||||||
"$APK_KEYSTORE" release-key APK_KEYSTORE_PASSWORD
|
|
||||||
|
|
||||||
- env:
|
- env:
|
||||||
INDEX_KEYSTORE_PASSWORD: ${{ secrets.INDEX_KEYSTORE_PASSWORD }}
|
INDEX_KEYSTORE_PASSWORD: ${{ secrets.INDEX_KEYSTORE_PASSWORD }}
|
||||||
@@ -78,17 +97,43 @@ the bare `owner/repo` form resolves against the instance's default actions
|
|||||||
URL rather than this host and fails outright. The full-URL form is also the
|
URL rather than this host and fails outright. The full-URL form is also the
|
||||||
only one that works while a referenced repo is private -- irrelevant for
|
only one that works while a referenced repo is private -- irrelevant for
|
||||||
*this* repo, which is public on purpose, but relevant for whatever repo is
|
*this* repo, which is public on purpose, but relevant for whatever repo is
|
||||||
consuming it, since that one holds the keys.
|
consuming it, since that one holds the index key.
|
||||||
|
|
||||||
`$APK_KEYSTORE`/`$INDEX_KEYSTORE`/`$FDROID_REPO_ROOT`/`$BUCKET`/
|
`$INDEX_KEYSTORE`/`$FDROID_REPO_ROOT`/`$BUCKET`/`$S3_ENDPOINT_URL`/
|
||||||
`$S3_ENDPOINT_URL`/`$FDROID_REPO_INDEX_URL`/`$APPLICATION_ID` above are the
|
`$FDROID_REPO_INDEX_URL`/`$APPLICATION_ID` above are the consumer's own
|
||||||
consumer's own paths and values -- nothing in this repo defaults or hints at
|
paths and values -- nothing in this repo defaults or hints at them. The env
|
||||||
them. Env var *names* like `APK_KEYSTORE_PASSWORD` are the consumer's choice
|
var *name* `INDEX_KEYSTORE_PASSWORD` is the consumer's choice too, not a
|
||||||
too, not a convention this repo imposes: `sign-apk.sh` and `fdroid-update.sh`
|
convention this repo imposes: `fdroid-update.sh` takes the name of the
|
||||||
each take the name of the environment variable to read a password from as an
|
environment variable to read the password from as an argument. Name it
|
||||||
argument, specifically so two different keystores in the same job (the APK
|
distinctly from whatever holds the APK key's password even if, today, both
|
||||||
key and the separate repo index key -- see below) never have to share one
|
happen to resolve to the same underlying keystore/secret (see "The repo
|
||||||
variable name.
|
index key is not the APK signing key" below) -- that way separating the two
|
||||||
|
keys later is a value change on the consumer's side, not a rename of this
|
||||||
|
convention.
|
||||||
|
|
||||||
|
### The exception: a consumer whose build does not already sign
|
||||||
|
|
||||||
|
`sign-apk.sh` is **not part of the flow above** and `action.yml` does not
|
||||||
|
call it. It exists only for a consumer whose build genuinely emits an
|
||||||
|
unsigned APK and has nowhere else to sign it -- an app signing key cannot be
|
||||||
|
rotated, so a build that produces an unsigned artifact and relies on this
|
||||||
|
step is a real (if narrow) case, not a shortcut around the settled model.
|
||||||
|
Insert it between the build and the `fdroid-update.sh` step above:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- env:
|
||||||
|
APK_KEYSTORE_PASSWORD: ${{ secrets.APK_KEYSTORE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
bash "${{ steps.ci.outputs.path }}/sign-apk.sh" \
|
||||||
|
app-release-unsigned.apk app-release.apk \
|
||||||
|
"$APK_KEYSTORE" release-key APK_KEYSTORE_PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
Using it at all means this repo's job **is** handed an app signing key for
|
||||||
|
that run, which is exactly the thing "This repository is never handed an
|
||||||
|
app signing key" (above) warns against as the default. Treat a workflow
|
||||||
|
that includes this step as the documented exception, and prefer fixing the
|
||||||
|
build to emit a signed APK over reaching for it.
|
||||||
|
|
||||||
## One `versionCode` rule, shared by CI and a manual build (fdroid-repo-ci#6)
|
## One `versionCode` rule, shared by CI and a manual build (fdroid-repo-ci#6)
|
||||||
|
|
||||||
@@ -277,6 +322,32 @@ takes its own keystore/alias/password arguments, deliberately unrelated to
|
|||||||
`sign-apk.sh`'s. Conflating the two would mean one compromised key
|
`sign-apk.sh`'s. Conflating the two would mean one compromised key
|
||||||
undermines both the apps and the index that vouches for them.
|
undermines both the apps and the index that vouches for them.
|
||||||
|
|
||||||
|
This is also, concretely, the boundary of what this repo ever holds: it
|
||||||
|
receives the **repo index key**, and nothing here is the *app* signing key
|
||||||
|
except through the narrow, opt-in `sign-apk.sh` exception documented above.
|
||||||
|
The index key identifies this repo to a client, which pins it via
|
||||||
|
`?fingerprint=` in the repo URL; the app key identifies each app to Android
|
||||||
|
and, unlike the index key, can never be rotated once a device has installed
|
||||||
|
against it.
|
||||||
|
|
||||||
|
As of 2026-09-09 (bit), the index key is *for now* the same keystore as the
|
||||||
|
APK key -- they are expected to be **split later**. That is exactly why
|
||||||
|
`fdroid-update.sh` and `sign-apk.sh` take independently-named keystore/alias/
|
||||||
|
password arguments rather than sharing one: a consumer's workflow should
|
||||||
|
already name the index key's secrets (e.g. `INDEX_KEYSTORE`,
|
||||||
|
`INDEX_KEYSTORE_PASSWORD`) distinctly from the APK key's (e.g.
|
||||||
|
`APK_KEYSTORE`, `APK_KEYSTORE_PASSWORD`), even while both names resolve to
|
||||||
|
the same underlying secret today. Splitting the keys later is then a value
|
||||||
|
change on the consumer's side -- a new keystore, a new secret -- not a
|
||||||
|
rename of any argument or config key here.
|
||||||
|
|
||||||
|
**Generate the index key as RSA 4096, never 2048.** F-Droid's own
|
||||||
|
documentation suggests 2048 for a repo signing key; that is not followed
|
||||||
|
here on purpose (bit, 2026-09-09) -- use 4096 regardless of what a `keytool
|
||||||
|
-genkeypair`/`fdroid init` example elsewhere shows. Recorded here so the
|
||||||
|
choice is not "corrected" back to F-Droid's default by someone going only
|
||||||
|
off those docs.
|
||||||
|
|
||||||
`fdroid-update.sh` writes `<repo-root>/config.yml` itself, on every run,
|
`fdroid-update.sh` writes `<repo-root>/config.yml` itself, on every run,
|
||||||
using fdroidserver's own `{env: VARNAME}` indirection for both passwords --
|
using fdroidserver's own `{env: VARNAME}` indirection for both passwords --
|
||||||
the exact mechanism its `common.py` implements *specifically so config.yml
|
the exact mechanism its `common.py` implements *specifically so config.yml
|
||||||
|
|||||||
+17
-6
@@ -10,17 +10,28 @@
|
|||||||
# - uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
# - id: ci
|
# - id: ci
|
||||||
# uses: https://git.chaosbit.de/weblib/fdroid-repo-ci@main
|
# uses: https://git.chaosbit.de/weblib/fdroid-repo-ci@main
|
||||||
# - run: bash "${{ steps.ci.outputs.path }}/sign-apk.sh" ...
|
# - run: bash "${{ steps.ci.outputs.path }}/fdroid-update.sh" ...
|
||||||
|
# - run: bash "${{ steps.ci.outputs.path }}/publish.sh" ...
|
||||||
|
#
|
||||||
|
# sign-apk.sh is deliberately not in that default flow: the builder signs
|
||||||
|
# the APK before it ever reaches this action (weblib-viewer#19), so this
|
||||||
|
# repo's normal job is to index and publish an already-signed artifact.
|
||||||
|
# sign-apk.sh stays available for the exceptional consumer whose build
|
||||||
|
# genuinely produces an unsigned APK -- see the README.
|
||||||
#
|
#
|
||||||
# Nothing here reads a secret or names a bucket, a keystore or a consumer
|
# Nothing here reads a secret or names a bucket, a keystore or a consumer
|
||||||
# repo -- those all stay on the caller's side of this action's output path.
|
# repo -- those all stay on the caller's side of this action's output path.
|
||||||
|
# This action is also never handed an *app* signing key -- only the repo
|
||||||
|
# index key that fdroid-update.sh signs the index with (see README).
|
||||||
name: F-Droid repo publishing tools
|
name: F-Droid repo publishing tools
|
||||||
description: >-
|
description: >-
|
||||||
Shared, key-free tooling to align-check and sign a prebuilt APK, run
|
Key-free tooling to index an already-signed F-Droid binary repo (`fdroid
|
||||||
`fdroid update` over a binary repo, compute the next monotonic versionCode
|
update`), compute the next monotonic versionCode for a commit's committer
|
||||||
for a commit's committer date, and publish the result to an S3-compatible
|
date, and publish the result to an S3-compatible bucket. Also includes
|
||||||
bucket. The caller supplies the keys, the bucket, the APK and the commit
|
sign-apk.sh for the exceptional consumer whose build does not already
|
||||||
being built; this action supplies the pinned toolchain and the scripts.
|
produce a signed APK. The caller supplies the keys, the bucket, the APK
|
||||||
|
and the commit being built; this action supplies the pinned toolchain and
|
||||||
|
the scripts.
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
path:
|
path:
|
||||||
|
|||||||
Reference in New Issue
Block a user