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
10 changes: 10 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ 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-finalize.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-publish.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'
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
100 changes: 100 additions & 0 deletions .github/workflows/android-finalize.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Verify and attest signed Android release
on:
workflow_call:
inputs:
source_run:
type: number
required: true
source_attempt:
type: number
required: true
outputs:
artifact_id:
description: Immutable verified publishing handoff
value: ${{ jobs.attest.outputs.artifact_id }}
manifest_sha256:
description: SHA-256 of the attested release manifest
value: ${{ jobs.attest.outputs.manifest_sha256 }}
permissions:
contents: read
concurrency:
group: signer-finalize-${{ inputs.source_run }}-${{ inputs.source_attempt }}
cancel-in-progress: false
jobs:
attest:
if: ${{ github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
runs-on: ubuntu-latest
timeout-minutes: 60
outputs:
artifact_id: ${{ steps.handoff.outputs.artifact-id }}
manifest_sha256: ${{ steps.seal.outputs.manifest_sha256 }}
permissions:
contents: write
actions: read
id-token: write
attestations: write
steps:
- name: Load approved organization policy
uses: $/android-signer
with:
command: policy
out: ${{ runner.temp }}/signer-policy.json
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: zulu
java-version: '25'
- name: Verify both signing transactions
uses: $/android-signer
env:
GH_TOKEN: ${{ github.token }}
SIGNER_JOB_WORKFLOW_REF: ${{ job.workflow_ref }}
SIGNER_JOB_WORKFLOW_SHA: ${{ job.workflow_sha }}
with:
command: prepare
source-run: ${{ inputs.source_run }}
source-attempt: ${{ inputs.source_attempt }}
policy: ${{ runner.temp }}/signer-policy.json
out: ${{ runner.temp }}/verified-release
tools: ${{ runner.temp }}/reference-tools
- name: Attest final signed files
id: provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: |
${{ runner.temp }}/verified-release/oss/app-signed.apk
${{ runner.temp }}/verified-release/oss/app-signed.aab
${{ runner.temp }}/verified-release/google/app-signed.apk
${{ runner.temp }}/verified-release/google/app-signed.aab
${{ runner.temp }}/verified-release/publish/*
predicate-type: https://slsa.dev/provenance/v1
predicate-path: ${{ runner.temp }}/verified-release/predicate.json
create-storage-record: false
- name: Prepare verified draft
id: seal
uses: $/android-signer
env:
GH_TOKEN: ${{ github.token }}
SIGNER_JOB_WORKFLOW_REF: ${{ job.workflow_ref }}
SIGNER_JOB_WORKFLOW_SHA: ${{ job.workflow_sha }}
with:
command: seal
policy: ${{ runner.temp }}/signer-policy.json
out: ${{ runner.temp }}/verified-release
bundle: ${{ steps.provenance.outputs.bundle-path }}
- name: Preserve immutable publishing handoff
id: handoff
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: verified-publishing-${{ inputs.source_run }}-${{ inputs.source_attempt }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/verified-release/publish/
if-no-files-found: error
- name: Preserve verification evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: signed-release-verification-${{ inputs.source_run }}-${{ inputs.source_attempt }}
path: |
${{ runner.temp }}/verified-release/verification.json
${{ runner.temp }}/verified-release/predicate.json
${{ runner.temp }}/verified-release/final.sigstore.json
if-no-files-found: warn
116 changes: 116 additions & 0 deletions .github/workflows/android-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Publish verified Android release
on:
workflow_call:
inputs:
artifact_id:
type: string
required: true
manifest_sha256:
type: string
required: true
permissions:
contents: read
jobs:
github:
if: ${{ github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: publish-github-${{ github.repository_id }}-${{ inputs.manifest_sha256 }}
cancel-in-progress: false
permissions:
contents: write
actions: read
steps:
- name: Load approved organization policy
uses: $/android-signer
with:
command: policy
out: ${{ runner.temp }}/signer-policy.json
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ inputs.artifact_id }}
path: ${{ runner.temp }}/publish
merge-multiple: true
- name: Verify and publish GitHub release
uses: $/android-signer
env:
GH_TOKEN: ${{ github.token }}
SIGNER_JOB_WORKFLOW_REF: ${{ job.workflow_ref }}
SIGNER_JOB_WORKFLOW_SHA: ${{ job.workflow_sha }}
with:
command: github
policy: ${{ runner.temp }}/signer-policy.json
out: ${{ runner.temp }}/publish
manifest-sha256: ${{ inputs.manifest_sha256 }}
report: ${{ runner.temp }}/github-publication.json
- name: Preserve publication result
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: github-publication-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/github-publication.json
if-no-files-found: error
play:
if: ${{ vars.PLAY_ALPHA_ENABLED == 'true' && github.repository == 'connectbot/connectbot' && github.repository_id == '123235' && github.event_name == 'workflow_dispatch' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
environment: play-alpha
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: publish-play-org-connectbot-alpha
cancel-in-progress: false
permissions:
contents: read
actions: read
id-token: write
steps:
- name: Load approved organization policy
uses: $/android-signer
with:
command: policy
out: ${{ runner.temp }}/signer-policy.json
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ inputs.artifact_id }}
path: ${{ runner.temp }}/publish
merge-multiple: true
- name: Verify publishing handoff before Google authentication
uses: $/android-signer
env:
GH_TOKEN: ${{ github.token }}
SIGNER_JOB_WORKFLOW_REF: ${{ job.workflow_ref }}
SIGNER_JOB_WORKFLOW_SHA: ${{ job.workflow_sha }}
with:
command: verify
policy: ${{ runner.temp }}/signer-policy.json
out: ${{ runner.temp }}/publish
manifest-sha256: ${{ inputs.manifest_sha256 }}
- name: Obtain short-lived Play credentials
id: google
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
with:
project_id: api-4664730880327298161-873945
workload_identity_provider: ${{ vars.GOOGLE_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ vars.GOOGLE_PLAY_SERVICE_ACCOUNT }}
token_format: access_token
access_token_scopes: https://www.googleapis.com/auth/androidpublisher
create_credentials_file: false
export_environment_variables: false
- name: Publish verified Google bundle to alpha
uses: $/android-signer
env:
GH_TOKEN: ${{ github.token }}
SIGNER_JOB_WORKFLOW_REF: ${{ job.workflow_ref }}
SIGNER_JOB_WORKFLOW_SHA: ${{ job.workflow_sha }}
GOOGLE_PLAY_ACCESS_TOKEN: ${{ steps.google.outputs.access_token }}
with:
command: play
policy: ${{ runner.temp }}/signer-policy.json
out: ${{ runner.temp }}/publish
manifest-sha256: ${{ inputs.manifest_sha256 }}
report: ${{ runner.temp }}/play-publication.json
- name: Preserve publication result
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: play-publication-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/play-publication.json
if-no-files-found: error
13 changes: 4 additions & 9 deletions .github/workflows/android-signer-ci.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
name: Android signer tests
on:
push:
branches: [main]
paths:
- 'android-signer/**'
- '.github/workflows/android-signer-ci.yml'
branches: [main, android-signer-v1]
pull_request:
branches: [main]
paths:
- 'android-signer/**'
- '.github/workflows/android-signer-ci.yml'
branches: [main, android-signer-v1]
permissions:
contents: read
jobs:
test:
name: Android signer contract tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand All @@ -26,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
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
Loading
Loading