Skip to content

Commit ca433c7

Browse files
authored
Streamline GitHub presentation and releases (#29)
## Summary - redesign the repository README as a clear application landing page with status badges and a direct APK download path - make all public project documentation and historical release notes English-only while retaining Russian runtime support - add direct install links and file guidance to release notes - reduce future GitHub Releases from 15 loose assets to the production APK, its checksum, and one verification bundle - preserve exact source, SBOM, vulnerability, license, signer, digest, and immutable-release verification inside the bundle ## Verification - `python -m unittest discover -s scripts/tests -p 'test_*.py'` (32/32) - `python scripts/security_source_scan.py .` - `python scripts/kotlin_style_check.py .` - workflow YAML parsing - Bash and PowerShell release-script syntax checks - `:app:lintRelease :crypto-core:lintRelease :pairing:lintRelease :secure-storage:lintRelease` - `:app:testDebugUnitTest :crypto-core:testDebugUnitTest :pairing:testDebugUnitTest :secure-storage:testDebugUnitTest` - public-prose Cyrillic scan ## Release Asset Compatibility Published releases remain immutable and keep their original evidence assets. Their live notes now point users directly to the installable APK. The next release produced from this branch will publish exactly three assets; the published-release workflow safely extracts and verifies the bundled evidence.
1 parent 008ff01 commit ca433c7

25 files changed

Lines changed: 1119 additions & 1175 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,5 +328,5 @@ jobs:
328328
echo "Tag: \`$RELEASE_TAG\`"
329329
echo "Commit: \`$RELEASE_COMMIT\`"
330330
echo
331-
echo 'GitHub did not receive or use the production keystore. From a clean checkout of this exact tag, run the local release script, review dist, enable immutable GitHub Releases, and publish only RELEASE_ARTIFACTS.sha256 plus every file named by that manifest. The Verify published release workflow will independently check the published snapshot.'
331+
echo 'GitHub did not receive or use the production keystore. From a clean checkout of this exact tag, run the local release script, review dist, enable immutable GitHub Releases, and publish exactly the production APK, its .sha256 file, and the verification bundle ZIP from dist. The Verify published release workflow will independently check the published snapshot.'
332332
} >>"$GITHUB_STEP_SUMMARY"

.github/workflows/verify-published-release.yml

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108
--repo "$GITHUB_REPOSITORY" \
109109
--dir "$assets"
110110
111-
- name: Verify artifact inventory and hashes
111+
- name: Verify public assets and extract evidence
112112
env:
113113
ARTIFACT_NAME: ${{ steps.source.outputs.artifact }}
114114
VERSION_NAME: ${{ steps.source.outputs.version }}
@@ -118,84 +118,74 @@ jobs:
118118
import hashlib
119119
import json
120120
import pathlib
121-
import re
122121
import sys
123122
124123
assets = pathlib.Path(sys.argv[1])
125124
release_api = pathlib.Path(sys.argv[2])
126125
artifact = sys.argv[3]
127126
version = sys.argv[4]
128-
manifest_name = "RELEASE_ARTIFACTS.sha256"
129-
manifest = assets / manifest_name
130-
if not manifest.is_file():
131-
raise SystemExit("release hash manifest is missing")
132-
133-
entries: dict[str, str] = {}
134-
line_pattern = re.compile(r"([0-9a-f]{64}) ([A-Za-z0-9][A-Za-z0-9._-]*)")
135-
for line_number, line in enumerate(manifest.read_text(encoding="ascii").splitlines(), 1):
136-
match = line_pattern.fullmatch(line)
137-
if not match:
138-
raise SystemExit(f"invalid release manifest line {line_number}")
139-
digest, name = match.groups()
140-
if name == manifest_name or name in entries:
141-
raise SystemExit(f"invalid or duplicate release asset: {name}")
142-
entries[name] = digest
143-
144127
apk_name = f"{artifact}-{version}-release.apk"
145-
required = {
128+
bundle_name = f"{artifact}-{version}-verification.zip"
129+
read_chunk_bytes = 1024 * 1024
130+
expected = {
146131
apk_name,
147132
f"{apk_name}.sha256",
148-
f"{artifact}-{version}-source.tar.gz",
149-
"SBOM.json",
150-
"VULNERABILITY_SCAN.json",
151-
"BUILD_INFO.txt",
152-
"THIRD_PARTY_NOTICES.txt",
153-
"LICENSE",
154-
"LICENSE-Apache-2.0",
155-
"LICENSE-BSD-3-Clause-NOTICES",
156-
"LICENSE-BlueOak-1.0.0",
157-
"LICENSE-CC-BY-SA-4.0",
158-
"LICENSE-MIT",
159-
"LICENSES.md",
133+
bundle_name,
160134
}
161-
missing = sorted(required - entries.keys())
162-
if missing:
163-
raise SystemExit("required release assets are missing: " + ", ".join(missing))
164-
165-
downloaded = {path.name for path in assets.iterdir() if path.is_file()}
166-
expected = set(entries) | {manifest_name}
135+
size_limits = {
136+
apk_name: 512 * 1024 * 1024,
137+
f"{apk_name}.sha256": 1024,
138+
bundle_name: 768 * 1024 * 1024,
139+
}
140+
downloaded = {path.name for path in assets.iterdir()}
167141
if downloaded != expected:
168142
raise SystemExit(
169-
"release asset inventory mismatch; missing="
143+
"public release asset inventory mismatch; missing="
170144
+ repr(sorted(expected - downloaded))
171145
+ ", unexpected="
172146
+ repr(sorted(downloaded - expected))
173147
)
174148
175-
for name, expected_digest in entries.items():
176-
path = assets / name
177-
actual_digest = hashlib.sha256(path.read_bytes()).hexdigest()
178-
if actual_digest != expected_digest:
179-
raise SystemExit(f"release asset hash mismatch: {name}")
180-
181-
apk_digest = entries[apk_name]
182-
apk_hash_line = (assets / f"{apk_name}.sha256").read_text(encoding="ascii").strip()
183-
if apk_hash_line != f"{apk_digest} {apk_name}":
184-
raise SystemExit("standalone APK hash file is inconsistent")
185-
186149
api = json.loads(release_api.read_text(encoding="utf-8"))
187150
if api.get("draft") is not False or api.get("tag_name") != f"v{version}":
188151
raise SystemExit("release API metadata does not describe the expected published tag")
189152
if api.get("immutable") is not True:
190153
raise SystemExit("GitHub Release immutability must be enabled before publication")
191-
api_assets = {item.get("name"): item for item in api.get("assets", [])}
154+
api_items = api.get("assets")
155+
if not isinstance(api_items, list):
156+
raise SystemExit("release API asset inventory is invalid")
157+
api_assets: dict[str, dict[str, object]] = {}
158+
for item in api_items:
159+
if not isinstance(item, dict):
160+
raise SystemExit("release API contains an invalid asset")
161+
name = item.get("name")
162+
if not isinstance(name, str) or name in api_assets:
163+
raise SystemExit("release API contains an invalid or duplicate asset")
164+
api_assets[name] = item
192165
if set(api_assets) != downloaded:
193166
raise SystemExit("release API and downloaded asset inventories differ")
194167
for name, item in api_assets.items():
195-
api_digest = item.get("digest")
196-
if api_digest and api_digest != "sha256:" + hashlib.sha256((assets / name).read_bytes()).hexdigest():
168+
path = assets / name
169+
if not path.is_file() or path.is_symlink():
170+
raise SystemExit(f"downloaded release asset is not a regular file: {name}")
171+
size = path.stat().st_size
172+
if size <= 0 or size > size_limits[name]:
173+
raise SystemExit(f"public release asset size limit exceeded: {name}")
174+
if item.get("size") != size:
175+
raise SystemExit(f"GitHub asset size mismatch: {name}")
176+
digest = hashlib.sha256()
177+
with path.open("rb") as handle:
178+
while chunk := handle.read(read_chunk_bytes):
179+
digest.update(chunk)
180+
api_digest = "sha256:" + digest.hexdigest()
181+
if item.get("digest") != api_digest:
197182
raise SystemExit(f"GitHub asset digest mismatch: {name}")
198183
PY
184+
python scripts/release_bundle.py extract \
185+
--assets-dir "$RUNNER_TEMP/release-assets" \
186+
--output-dir "$RUNNER_TEMP/release-bundle" \
187+
--artifact "$ARTIFACT_NAME" \
188+
--version "$VERSION_NAME"
199189
200190
- name: Verify signed APK and native hardening
201191
env:
@@ -237,9 +227,10 @@ jobs:
237227
run: |
238228
set -euo pipefail
239229
assets="$RUNNER_TEMP/release-assets"
230+
evidence="$RUNNER_TEMP/release-bundle"
240231
apk="$assets/$ARTIFACT_NAME-$VERSION_NAME-release.apk"
241232
expected_source="$RUNNER_TEMP/$ARTIFACT_NAME-$VERSION_NAME-source.tar"
242-
released_source="$assets/$ARTIFACT_NAME-$VERSION_NAME-source.tar.gz"
233+
released_source="$evidence/$ARTIFACT_NAME-$VERSION_NAME-source.tar.gz"
243234
# Reproduce the Windows release host's checkout transform and archive modes.
244235
git -c core.autocrlf=false -c core.eol=crlf -c tar.umask=0002 archive --format=tar \
245236
--prefix="$ARTIFACT_NAME-$VERSION_NAME-source/" \
@@ -329,7 +320,7 @@ jobs:
329320
raise SystemExit("released source archive inventory length mismatch")
330321
PY
331322
332-
python - "$assets" "$apk" "$SOURCE_COMMIT" "$VERSION_NAME" <<'PY'
323+
python - "$evidence" "$apk" "$SOURCE_COMMIT" "$VERSION_NAME" <<'PY'
333324
import hashlib
334325
import json
335326
import pathlib
@@ -396,7 +387,7 @@ jobs:
396387
printf '%s %s\n' "$OSV_SCANNER_LINUX_SHA256" "$scanner" | sha256sum --check --strict
397388
chmod 500 "$scanner"
398389
export XDG_CACHE_HOME="$cache"
399-
cp -- "$RUNNER_TEMP/release-assets/SBOM.json" "$input"
390+
cp -- "$RUNNER_TEMP/release-bundle/SBOM.json" "$input"
400391
"$scanner" scan source \
401392
--offline-vulnerabilities \
402393
--download-offline-databases \
@@ -406,7 +397,7 @@ jobs:
406397
python scripts/osv_offline_scan.py \
407398
--scanner "$scanner" \
408399
--database-root "$cache/osv-scanner" \
409-
--sbom "$RUNNER_TEMP/release-assets/SBOM.json" \
400+
--sbom "$RUNNER_TEMP/release-bundle/SBOM.json" \
410401
--output "$RUNNER_TEMP/verification-evidence/osv-independent.json"
411402
412403
- name: Upload public verification evidence

BUILD.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,16 @@ PowerShell 7:
154154
`dist/`, and applies the APK policy verifier. Two inherited HeliBoard regression
155155
tests are explicitly `@Ignore`d with issue-specific reasons; no build type
156156
conditionally bypasses these two tests. `build-release` additionally runs all
157-
module release lint tasks, Rust format/Clippy/test/audit with locked graphs, requires
158-
external signing material, signs with `apksigner`, and writes the APK SHA-256,
159-
CycloneDX `SBOM.json`, offline `VULNERABILITY_SCAN.json`,
160-
`RELEASE_ARTIFACTS.sha256`, `BUILD_INFO.txt`, notices, and an exact-commit GPL
161-
source archive. It accepts only an official OSV-Scanner v2.4.0 binary with a
162-
pinned SHA-256, requires fresh local Maven/crates.io databases, scans without
163-
network access, and fails on a finding or package-count mismatch. It rechecks
164-
the same clean Git HEAD before signing and publication.
157+
module release lint tasks, Rust format/Clippy/test/audit with locked graphs,
158+
requires external signing material, signs with `apksigner`, and writes exactly
159+
three public files to `dist/`: the production APK, its SHA-256 file, and a
160+
verification ZIP. The ZIP contains CycloneDX `SBOM.json`, offline
161+
`VULNERABILITY_SCAN.json`, `RELEASE_ARTIFACTS.sha256`, `BUILD_INFO.txt`, notices,
162+
licenses, and an exact-commit GPL source archive. The release process accepts
163+
only an official OSV-Scanner v2.4.0 binary with a pinned SHA-256, requires fresh
164+
local Maven/crates.io databases, scans without network access, and fails on a
165+
finding or package-count mismatch. It rechecks the same clean Git HEAD before
166+
signing and publication.
165167
Neither release script creates or overwrites a keystore.
166168

167169
The APK includes complete local license/provenance texts as generated assets;

CODE_OF_CONDUCT.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,3 @@ security, or incident details.
5151

5252
Security vulnerabilities must follow [`SECURITY.md`](SECURITY.md), not the
5353
conduct-reporting path.
54-
55-
## Russian Summary / Кратко по-русски
56-
57-
Участники проекта должны общаться уважительно, обсуждать технические решения на
58-
основе проверяемых фактов и не публиковать чужие персональные данные, секреты или
59-
детали неисправленной уязвимости. Оскорбления, угрозы, дискриминация, травля,
60-
выдача себя за представителей проекта и преследование добросовестных
61-
исследователей недопустимы. Для сообщения об уязвимости используйте
62-
[`SECURITY.md`](SECURITY.md).

LICENSES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ under the same content license; the complete text is
4040
[`LICENSE-CC-BY-SA-4.0`](LICENSE-CC-BY-SA-4.0).
4141

4242
The generation process extracts the word column, keeps only already-lowercase
43-
alphabetic English `a-z` or Russian `а-я` words of 4--10 letters,
43+
alphabetic English ASCII or Russian Cyrillic words of 4--10 letters,
4444
deduplicates entries, removes profanity and alarming words/fragments, then
4545
takes a deterministic 4096-entry list in source-frequency order. These
4646
transformations produce codebook tokens, not natural phrases or a language

PROJECT_CONTEXT.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ release.
214214

215215
The final user-facing report must state:
216216

217-
> Сборка реализует проверенные криптографические примитивы и прошла
218-
> автоматические тесты, но весь продукт не следует считать независимо
219-
> аудированным до проверки внешним специалистом по прикладной криптографии и
220-
> Android security.
217+
> The build uses reviewed cryptographic primitives and has passed automated
218+
> tests, but the complete product should not be considered independently
219+
> audited until it has been reviewed by an external applied-cryptography and
220+
> Android security specialist.

0 commit comments

Comments
 (0)