Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ paths:
# GitHub.com documents job.workflow_sha, but actionlint 1.7.12 predates it.
# https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#job-context
- 'property "workflow_sha" is not defined in object type'
.github/workflows/android-build.yml:
ignore:
# Documented GitHub.com features newer than actionlint 1.7.12.
- 'property "workflow_(ref|sha)" is not defined in object type'
- 'specifying action "\$/android-signer" in invalid format'
.github/workflows/android-finalize.yml:
ignore:
# Documented GitHub.com features newer than actionlint 1.7.12.
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/android-attest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Trusted Android artifact provenance
on:
workflow_call:
inputs:
profile:
description: Profile used in this run's immutable artifact names
required: true
type: string
permissions:
contents: read
jobs:
attest-unsigned:
name: Attest unsigned ${{ inputs.profile }}
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
id-token: write
attestations: write
steps:
- name: Validate artifact profile
env:
PROFILE: ${{ inputs.profile }}
run: |
python3 - <<'PYTHON'
import os, re
if not re.fullmatch(r"[a-z][a-z0-9-]{0,31}", os.environ["PROFILE"]):
raise SystemExit("Invalid artifact profile")
PYTHON
- name: Download this attempt's immutable inputs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unsigned-${{ inputs.profile }}-${{ github.run_attempt }}
path: unsigned/
- name: Download this attempt's release support files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-support-${{ inputs.profile }}-${{ github.run_attempt }}
path: support/
- name: Validate artifact handoff
run: |
python3 - <<'PYTHON'
from pathlib import Path
for directory, expected in (
("unsigned", {"app.apk", "app.aab"}),
("support", {"mapping.txt", "native-debug-symbols.zip", "build-info.json"}),
):
entries = list(Path(directory).rglob("*"))
files = {str(p.relative_to(directory)) for p in entries if p.is_file()}
if any(p.is_symlink() for p in entries) or files != expected:
raise SystemExit("Unexpected artifact handoff: " + directory)
PYTHON
- name: Attest original APK, AAB, and release support files
id: provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: |
unsigned/app.apk
unsigned/app.aab
support/mapping.txt
support/native-debug-symbols.zip
support/build-info.json
create-storage-record: false
- name: Preserve provenance bundle
env:
BUNDLE: ${{ steps.provenance.outputs.bundle-path }}
FLAVOR: ${{ inputs.profile }}
run: cp "$BUNDLE" "provenance-${FLAVOR}.json"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: provenance-${{ inputs.profile }}-${{ github.run_attempt }}
path: provenance-${{ inputs.profile }}.json
if-no-files-found: error
157 changes: 0 additions & 157 deletions .github/workflows/android-build.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/android-signer-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
- name: Verify action contracts and adversarial publishing cases
run: python3 -m unittest discover -s android-signer/tests -v
- name: Validate workflow
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 .github/workflows/android-signer-ci.yml .github/workflows/android-build.yml .github/workflows/android-finalize.yml .github/workflows/android-publish.yml
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 .github/workflows/android-signer-ci.yml .github/workflows/android-attest.yml .github/workflows/android-finalize.yml .github/workflows/android-publish.yml
46 changes: 41 additions & 5 deletions android-signer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ signer. Private Android keys stay on the signing appliance. This action runs
Python standard-library code and pinned Android reference tools; it does not
build Android applications or hold their signing keys.

For direct composite use, use `connectbot/actions/android-signer@FULL_COMMIT_SHA`. Keep the thin build,
For legacy content-pin configurations, use `connectbot/actions/android-signer@FULL_COMMIT_SHA`. Keep the thin build,
attestation, finalization, and publishing workflows in the consuming repository,
with separate hosted jobs and minimal permissions. Keep its public signing
policy there too. Moving this composite action does not change reusable workflow
Expand Down Expand Up @@ -55,10 +55,46 @@ Android payload preservation, publishing recovery, and argument injection.

## Trusted reusable workflows

Call `connectbot/actions/.github/workflows/android-build.yml@android-signer-v1`
for the fixed Android release build and separate provenance jobs. It accepts no
caller commands, source overrides, or signing digests. Build and attest jobs are
fresh hosted jobs; Gradle has no provenance credentials.
Keep application builds in the consuming repository: Java/SDK setup, Gradle tasks,
flavor matrices, output paths, and release metadata belong beside the application.
Build jobs must have only `contents: read` permission and no signing secrets or
provenance credentials. Use fresh hosted runners and disable shared build caches.
The `build-info` composite command can create release metadata alongside the build.

After the build succeeds, call
`connectbot/actions/.github/workflows/android-attest.yml@android-signer-v1` once
per profile:

```yaml
attest-unsigned:
needs: release
if: ${{ github.event_name == 'push' }}
strategy:
matrix:
flavor: [oss, google]
uses: connectbot/actions/.github/workflows/android-attest.yml@android-signer-v1
permissions:
contents: read
actions: read
id-token: write
attestations: write
with:
profile: ${{ matrix.flavor }}
```

The build must upload `unsigned-PROFILE-ATTEMPT` containing `app.apk` and `app.aab`,
and `release-support-PROFILE-ATTEMPT` containing `mapping.txt`,
`native-debug-symbols.zip`, and `build-info.json`, with flat paths. `ATTEMPT` is
`${{ github.run_attempt }}`. This is a stable handoff contract, independent of
Gradle task names, project layout, Java versions, and original output filenames.
A new profile additionally needs independently provisioned signing policy/keys.

The shared attestor accepts only a profile label. It derives repository, run and
attempt from GitHub, downloads that run's immutable artifacts, checks their file
sets, and computes the subject digests on a fresh hosted runner. It accepts no
caller commands, artifact IDs, source overrides, or signing digests and never
checks out or executes application code. Provenance authenticates the source run
and output bytes; it does not certify the application's build logic as safe.

The finalizer and publishers are `android-finalize.yml` and `android-publish.yml`
at the same protected branch. They load the public policy from immutable
Expand Down
2 changes: 1 addition & 1 deletion android-signer/tests/test_trusted_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
p = fixtures.p


def identity(workflow="android-build.yml"):
def identity(workflow="android-attest.yml"):
return {"repository": "connectbot/actions", "repository_id": 1369420724,
"workflow": ".github/workflows/" + workflow, "refs": ["refs/heads/android-signer-v1"],
"authorization": "workflow-identity", "sha256": [], "helpers": {}}
Expand Down
Loading
Loading