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
231 changes: 231 additions & 0 deletions .github/workflows/package-repository.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
name: NexaWrt signed APK package repository

on:
pull_request:
paths:
- 'configs/ax9000-single-ubi*.config'
- 'manifests/package-repository*'
- 'packages/nexawrt-repository/**'
- 'scripts/lock-file-policy.sh'
- 'scripts/package-repository-key.sh'
- 'scripts/prepare.sh'
- 'scripts/stage-package-repository.py'
- 'scripts/validate.sh'
- 'tests/test_package_repository*'
- '.github/workflows/package-repository.yml'
- '.github/workflows/pages.yml'
push:
branches: [main]
paths:
- 'configs/ax9000-single-ubi*.config'
- 'manifests/package-repository*'
- 'packages/nexawrt-repository/**'
- 'scripts/lock-file-policy.sh'
- 'scripts/package-repository-key.sh'
- 'scripts/prepare.sh'
- 'scripts/stage-package-repository.py'
- 'scripts/validate.sh'
- 'tests/test_package_repository*'
- '.github/workflows/package-repository.yml'
- '.github/workflows/pages.yml'
workflow_dispatch:

permissions: {}

concurrency:
group: package-repository-${{ github.ref }}
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
policy:
name: Validate repository policy
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout reviewed source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Install policy dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends openssl shellcheck
- name: Validate signed APK repository policy
run: |
set -euo pipefail
shellcheck -S warning \
scripts/lock-file-policy.sh \
scripts/package-repository-key.sh \
scripts/prepare.sh \
scripts/validate.sh \
tests/test_package_repository_policy.sh
bash tests/test_package_repository_policy.sh
NEXAWRT_FLAVOR=official ./scripts/validate.sh
NEXAWRT_FLAVOR=nss ./scripts/validate.sh

publish:
name: Build, sign, and publish testing repository
needs: policy
if: >-
github.repository == 'tifycloud/NexaWrt' &&
github.event_name != 'pull_request' &&
github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 180
environment: package-repository
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
env:
WORK_DIR: .work/package-repository
steps:
- name: Checkout trusted main source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Reject mutable or duplicate repository release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
source scripts/lock-file-policy.sh
nexawrt_validate_lock_file manifests/package-repository.lock package-repository
source manifests/package-repository.lock
test "$GITHUB_SHA" = "$(git rev-parse HEAD)"
if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$NEXAWRT_REPOSITORY_RELEASE_TAG" >/dev/null 2>&1; then
echo "Repository release tag already exists; increment NEXAWRT_REPOSITORY_RELEASE instead of overwriting it." >&2
exit 1
fi
if gh api "repos/$GITHUB_REPOSITORY/releases/tags/$NEXAWRT_REPOSITORY_RELEASE_TAG" >/dev/null 2>&1; then
echo "Repository release already exists; immutable assets will not be replaced." >&2
exit 1
fi
- name: Install OpenWrt build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential clang flex bison g++ gawk gcc-multilib g++-multilib \
gettext git libncurses-dev libssl-dev python3 python3-setuptools \
python3-pyelftools python3-packaging rsync swig unzip zlib1g-dev \
file wget libelf-dev shellcheck subversion time xsltproc zstd
- name: Prepare pinned OpenWrt source and feeds
run: |
set -euo pipefail
NEXAWRT_FLAVOR=official WORK_DIR="$WORK_DIR" ./scripts/prepare.sh --clean
NEXAWRT_FLAVOR=official ./scripts/validate.sh --source "$WORK_DIR"
- name: Build the locked APK package set without a private key
run: |
set -euo pipefail
public_key="$GITHUB_WORKSPACE/manifests/package-repository-public.pem"
make -C "$WORK_DIR" -j"$(nproc)" V=s \
NEXAWRT_APK_PUBLIC_ONLY=1 \
BUILD_KEY_APK_PUB="$public_key" \
package/nexawrt-repository/compile
mkdir -p release-staging/package-input
while IFS= read -r package; do
[[ -n "$package" && "${package:0:1}" != '#' ]] || continue
mapfile -t matches < <(find "$WORK_DIR/bin/packages" -type f -name "${package}-*.apk" -print | LC_ALL=C sort)
((${#matches[@]} == 1)) || {
printf 'Expected exactly one APK for %s, found %s\n' "$package" "${#matches[@]}" >&2
printf '%s\n' "${matches[@]}" >&2
exit 1
}
install -m 0644 "${matches[0]}" release-staging/package-input/
done < manifests/package-repository-packages.txt
find release-staging/package-input -type f -name '*.apk' -print -quit | grep -q .
if find release-staging/package-input \( -type l -o -type f -links +1 \) -print -quit | grep -q .; then
echo 'Package input contains a symbolic or hard link.' >&2
exit 1
fi
test -x "$WORK_DIR/staging_dir/host/bin/apk"
- name: Materialize and validate signing key
id: signing_key
env:
REPOSITORY_PRIVATE_KEY: ${{ secrets.NEXAWRT_REPOSITORY_SIGNING_PRIVATE_KEY }}
run: |
set -euo pipefail
umask 077
key="$RUNNER_TEMP/nexawrt-repository-signing.pem"
test -n "$REPOSITORY_PRIVATE_KEY"
printf '%s\n' "$REPOSITORY_PRIVATE_KEY" > "$key"
chmod 0600 "$key"
./scripts/package-repository-key.sh private "$key"
printf 'path=%s\n' "$key" >> "$GITHUB_OUTPUT"
- name: Build and verify signed repository archive
env:
PRIVATE_KEY_PATH: ${{ steps.signing_key.outputs.path }}
run: |
set -euo pipefail
source scripts/lock-file-policy.sh
nexawrt_validate_lock_file manifests/package-repository.lock package-repository
source manifests/package-repository.lock
mkdir -p release-staging/repository-parent release-staging/assets
python3 scripts/stage-package-repository.py build \
--input-dir release-staging/package-input \
--output-dir release-staging/repository-parent/repository \
--archive "release-staging/assets/$NEXAWRT_REPOSITORY_ASSET" \
--private-key "$PRIVATE_KEY_PATH" \
--public-key "$GITHUB_WORKSPACE/manifests/package-repository-public.pem" \
--apk-executable "$WORK_DIR/staging_dir/host/bin/apk"
archive="release-staging/assets/$NEXAWRT_REPOSITORY_ASSET"
(cd release-staging/assets && sha256sum "$NEXAWRT_REPOSITORY_ASSET" > "$NEXAWRT_REPOSITORY_ASSET.sha256")
"$WORK_DIR/staging_dir/host/bin/apk" adbdump --format json \
release-staging/repository-parent/repository/packages.adb >/dev/null
test -s "$archive"
- name: Destroy signing key
if: always()
env:
PRIVATE_KEY_PATH: ${{ steps.signing_key.outputs.path }}
run: |
set -euo pipefail
if [[ -n "${PRIVATE_KEY_PATH:-}" && -f "$PRIVATE_KEY_PATH" && ! -L "$PRIVATE_KEY_PATH" ]]; then
chmod 0600 "$PRIVATE_KEY_PATH"
rm -f -- "$PRIVATE_KEY_PATH"
fi
- name: Upload signed repository workflow artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: signed-apk-repository-${{ github.run_id }}-${{ github.run_attempt }}
path: release-staging/assets/
if-no-files-found: error
retention-days: 30
- name: Attest signed repository archive
uses: actions/attest-build-provenance@96278af6caaf10aea03fd8d33a09a777ca52d62f # v3.2.0
with:
subject-path: release-staging/assets/*.tar.gz
- name: Publish immutable prerelease assets
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
source scripts/lock-file-policy.sh
nexawrt_validate_lock_file manifests/package-repository.lock package-repository
source manifests/package-repository.lock
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$NEXAWRT_REPOSITORY_RELEASE_TAG" \
-f sha="$GITHUB_SHA" >/dev/null
cat > "$RUNNER_TEMP/package-repository-notes.md" <<NOTES
NexaWrt signed APK testing repository ${NEXAWRT_REPOSITORY_RELEASE}.

Repository index: ${NEXAWRT_REPOSITORY_INDEX_URL}
Architecture: ${NEXAWRT_REPOSITORY_ARCH}
Channel: ${NEXAWRT_REPOSITORY_CHANNEL}

This is an APK repository for OpenWrt 25.12, not an OPKG/IPK feed.
NOTES
gh release create "$NEXAWRT_REPOSITORY_RELEASE_TAG" \
--verify-tag --prerelease --latest=false \
--title "NexaWrt APK repository $NEXAWRT_REPOSITORY_RELEASE" \
--notes-file "$RUNNER_TEMP/package-repository-notes.md" \
"release-staging/assets/$NEXAWRT_REPOSITORY_ASSET" \
"release-staging/assets/$NEXAWRT_REPOSITORY_ASSET.sha256"
68 changes: 67 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
- 'site/**'
- 'components/**'
- 'manifests/community-feeds.lock'
- 'manifests/package-repository.lock'
- 'manifests/package-repository-public.pem'
- 'scripts/stage-package-repository.py'
- 'tests/test_package_repository.py'
- 'tests/test_pages_ui.js'
- 'tests/test_package_catalog.py'
- 'tests/test_pages_policy.sh'
Expand All @@ -24,7 +28,7 @@ on:
- 'evidence/vm-esxi/**'
- '.github/workflows/pages.yml'
workflow_run:
workflows: ['NexaWrt AX9000 reproducible RAM-test release', 'NexaWrt x86_64 VM release', 'Promote ESXi-accepted VM RC']
workflows: ['NexaWrt AX9000 reproducible RAM-test release', 'NexaWrt x86_64 VM release', 'Promote ESXi-accepted VM RC', 'NexaWrt signed APK package repository']
types: [completed]
schedule:
- cron: '17 */6 * * *'
Expand Down Expand Up @@ -93,6 +97,68 @@ jobs:
echo 'The staged component catalogs must not contain symbolic or hard links.' >&2
exit 1
fi
- name: Download, verify, and stage the signed APK repository
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
source scripts/lock-file-policy.sh
nexawrt_validate_lock_file manifests/package-repository.lock package-repository
source manifests/package-repository.lock
release_json=.pages-index/package-repository-release.json
release_error=.pages-index/package-repository-release.error
if ! gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"repos/$GITHUB_REPOSITORY/releases/tags/$NEXAWRT_REPOSITORY_RELEASE_TAG" \
> "$release_json" 2> "$release_error"; then
if grep -Fq 'HTTP 404' "$release_error"; then
echo "Signed APK repository $NEXAWRT_REPOSITORY_RELEASE_TAG is not published; refusing to deploy Pages without the locked repository." >&2
rm -f "$release_json" "$release_error"
exit 1
fi
cat "$release_error" >&2
exit 1
fi
rm -f "$release_error"
read -r asset_id asset_digest < <(python3 - "$release_json" "$NEXAWRT_REPOSITORY_ASSET" <<'PYDATA'
import json
import re
import sys

release = json.load(open(sys.argv[1], encoding='utf-8'))
if release.get('draft') is not False:
raise SystemExit('package repository release must be published')
assets = [item for item in release.get('assets', []) if item.get('name') == sys.argv[2]]
if len(assets) != 1:
raise SystemExit('package repository release must contain exactly one locked archive')
asset = assets[0]
digest = asset.get('digest')
if not isinstance(asset.get('id'), int) or not isinstance(digest, str) or not re.fullmatch(r'sha256:[0-9a-f]{64}', digest):
raise SystemExit('package repository release asset lacks a valid GitHub digest')
print(asset['id'], digest.removeprefix('sha256:'))
PYDATA
)
mkdir -p .pages-index/package-repository
archive=.pages-index/package-repository/$NEXAWRT_REPOSITORY_ASSET
gh api \
-H 'Accept: application/octet-stream' \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"repos/$GITHUB_REPOSITORY/releases/assets/$asset_id" > "$archive"
archive_sha="$(sha256sum "$archive" | awk '{print $1}')"
[[ "$archive_sha" == "$asset_digest" ]] || {
echo 'Downloaded package repository archive differs from the GitHub asset digest.' >&2
exit 1
}
python3 scripts/stage-package-repository.py stage-pages \
--archive "$archive" \
--archive-sha256 "$archive_sha" \
--github-asset-json "$release_json" \
--site-dir site
if find site/packages \( -type l -o -type f -links +1 \) -print -quit | grep -q .; then
echo 'The staged APK repository must not contain symbolic or hard links.' >&2
exit 1
fi
- name: Test Pages UI and policy
run: |
set -euo pipefail
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ NEXAWRT_FLAVOR=nss ./scripts/build.sh
构建不会再让 OpenWrt 在源码顶层随机生成 APK 私钥,也不会把长期私钥交给联网的第三方构建机。
当前 initramfs 构建采用**仅公钥信任身份**:仓库提交 `manifests/apk-signing-public.pem`,其规范化
SubjectPublicKeyInfo DER SHA-256 由 `manifests/apk-signing.lock` 锁定;OpenWrt 只嵌入该受信公钥,构建期
临时 package index 保持未签名并以显式 `--allow-untrusted` 安装。未来若发布可更新 APK 仓库,必须在受控、
离线的签名环境中使用仓库外私钥单独签署 index,不能把私钥注入固件构建工作流。
临时 package index 保持未签名并以显式 `--allow-untrusted` 安装。对外软件库使用另一套独立 P-256 身份:
固件只内置仓库公钥和 `packages.adb` URL,受保护的 `package-repository` GitHub Environment 只在受信任的
`main` 发布 job 中临时提供仓库外私钥,用它单独签署 index,随后删除;私钥不会进入固件构建、artifact、日志或 Pages。

生产构建使用已提交的公钥:

Expand Down Expand Up @@ -213,6 +214,12 @@ macOS 自带的 Bash、Make 和默认大小写不敏感文件系统通常不满
**Artifacts** 区域下载 `NexaWrt-AX9000-<flavor>-verified-dist-<commit>`。该下载仍是 RAM-only
真机测试候选,不是可直接写入闪存的生产刷机包。

### 自建签名 APK 软件库

OpenWrt 25.12 已使用 APK,不再使用 OPKG/IPK。NexaWrt 会把 testing 仓库 URL 和独立公钥直接编译进 AX9000 rootfs;启动后可直接执行 `apk update`、`apk search` 和 `apk add`。GitHub Actions 从固定源码构建审核包,签署 `packages.adb`,创建不可覆盖的版本 Release;Pages 再根据 GitHub asset digest 和归档内摘要安全发布到 `https://tifycloud.github.io/NexaWrt/packages/25.12/testing/aarch64_cortex-a53/`。

当前仓库基础设施已经按 `manifests/package-repository.lock` 和 `manifests/package-repository-packages.txt` 锁定,但频道仍是 `testing`。社区组件目录不会自动进入受信软件库;每个软件必须完成源码固定、许可证和构建脚本审查、APK 构建及安装测试。AX9000 当前仍是 RAM-only,运行时安装的软件重启后不会持久保存。设计、命令和 stable 门禁见 [docs/PACKAGE-REPOSITORY.md](docs/PACKAGE-REPOSITORY.md)。

### 浏览器自选组件云编译

GitHub Pages 的 **组件 / Components** 区域读取仓库审核过的 `components/catalog.json`,支持 x86_64 和 Xiaomi AX9000、组件搜索、依赖自动补齐与冲突阻止。网页只生成规范化请求和 request hash,不保存 GitHub token,也不接受任意软件包、脚本、路径或 UCI 输入。
Expand Down
3 changes: 3 additions & 0 deletions configs/ax9000-single-ubi-nss.config
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ CONFIG_EXPERIMENTAL=y
CONFIG_TOOLCHAINOPTS=y
CONFIG_TARGET_OPTIONS=y
CONFIG_TARGET_OPTIMIZATION="-O2 -pipe -mcpu=cortex-a53+crc+crypto"

# Build the NexaWrt repository helper as an APK artifact without installing it in the RAM-test image.
CONFIG_PACKAGE_nexawrt-repository=m
3 changes: 3 additions & 0 deletions configs/ax9000-single-ubi.config
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ CONFIG_PACKAGE_tcpdump-mini=y
# Keep the first release candidate close to official OpenWrt.
# Third-party NSS acceleration/ECM/firmware, proxy suites, containers, storage servers and third-party feeds
# are intentionally excluded until the base image passes hardware testing.

# Build the NexaWrt repository helper as an APK artifact without installing it in the RAM-test image.
CONFIG_PACKAGE_nexawrt-repository=m
Loading
Loading