Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
328546e
web: poll power LEDs quickly only while the power popover is open
dormancygrace Oct 7, 2026
3aaff58
web: stop rewriting the stream type when an admin opens the desktop
dormancygrace Oct 7, 2026
d684584
i18n: move RustDesk labels into the locale files
dormancygrace Oct 7, 2026
a63477c
web: semantic colour tokens, themed static messages, settings buildin…
dormancygrace Oct 7, 2026
cac2d22
web: point dashboard links at existing settings tabs
dormancygrace Oct 7, 2026
7aaaeab
web: import only the AES module from crypto-js
dormancygrace Oct 7, 2026
48b8f06
server, web: persist MJPEG frame detect on the device
dormancygrace Oct 7, 2026
93b7b47
web: apply the device time format where device times are shown
dormancygrace Oct 7, 2026
fc8d29d
web: use the native WebSocket instead of the websocket package
dormancygrace Oct 7, 2026
3410844
web: publish polled input region geometry only when it changes
dormancygrace Oct 7, 2026
b8270ab
web: load the PicoClaw gateway client with the PicoClaw sidebar only
dormancygrace Oct 7, 2026
d7b272d
web: translate remaining hard-coded interface strings
dormancygrace Oct 7, 2026
af580d8
i18n(ru): translate the Users feature and leftover English values
dormancygrace Oct 7, 2026
c89a342
web: load only the video player used by the session
dormancygrace Oct 7, 2026
35b20e9
i18n: drop orphan keys and mark partial languages
dormancygrace Oct 7, 2026
41d4708
web: report failed requests instead of swallowing them
dormancygrace Oct 7, 2026
b493719
web: check locale keys in the test suite
dormancygrace Oct 7, 2026
03f10bc
web: explain and complete the forced password change flow
dormancygrace Oct 7, 2026
cf60cdd
web: stop reporting Wi-Fi setup failures as success
dormancygrace Oct 7, 2026
06fd16b
web: show terminal disconnects and offer a reconnect
dormancygrace Oct 7, 2026
c8d7eec
i18n: make English interface text consistent
dormancygrace Oct 7, 2026
da97c4c
web: remove the unreachable change-password notification
dormancygrace Oct 7, 2026
96495dd
web: load the settings modal body on first open
dormancygrace Oct 7, 2026
41fa43a
web: load admin-only toolbar items lazily
dormancygrace Oct 7, 2026
2b9ea20
web: keep VPN status fetches independent of the language
dormancygrace Oct 7, 2026
7778366
Merge branch 'claude/ui-fixes' into claude/settings-ia
dormancygrace Oct 7, 2026
1ad6e8c
Merge branch 'claude/i18n' into claude/settings-ia
dormancygrace Oct 7, 2026
579aa85
web: ship the MSW service worker only in mocked mode
dormancygrace Oct 7, 2026
d061927
web: remove unused NanoKVM OS SVGs from public/
dormancygrace Oct 7, 2026
19be029
web: restore Vite's default chunk size warning limit
dormancygrace Oct 7, 2026
477e697
web: settings in seven sections with one page header
dormancygrace Oct 7, 2026
e59bab6
Merge branch claude/web-bundle into claude/settings-ia
dormancygrace Oct 7, 2026
99d431e
web: settings pages drop their own titles; Device joins System
dormancygrace Oct 7, 2026
ce042f4
web: lucide icons on login, password, Wi-Fi and input-region pages
dormancygrace Oct 7, 2026
6e4dd8f
web: delete unreferenced settings and HID mode components
dormancygrace Oct 7, 2026
e677ea2
web: remove unused API helpers and library exports
dormancygrace Oct 7, 2026
2aa938c
web: stop writing video settings nobody reads to localStorage
dormancygrace Oct 7, 2026
9066a40
web: tolerate a corrupt menu disabled-items value in localStorage
dormancygrace Oct 7, 2026
aba451e
web: settings pages use lucide icons instead of @ant-design/icons
dormancygrace Oct 7, 2026
c970ea3
web: one loading and one help glyph across settings pages
dormancygrace Oct 7, 2026
b5de33b
web: settings icon sizes follow one scale
dormancygrace Oct 7, 2026
963cec9
web: name settings switches and make help icons keyboard accessible
dormancygrace Oct 7, 2026
7012a2b
web: give icon-only buttons in settings and PicoClaw accessible names
dormancygrace Oct 7, 2026
6ef2ea3
web: turn clickable divs and spans into real buttons
dormancygrace Oct 7, 2026
7995ce1
web: raise contrast of informational text in settings
dormancygrace Oct 7, 2026
99f5530
web: drop translation keys no source file references
dormancygrace Oct 7, 2026
c61acb3
web: confirm changes that cut the connection or risk the device
dormancygrace Oct 7, 2026
88e776c
Merge branch 'claude/icons-lucide' into claude/settings-ia
dormancygrace Oct 7, 2026
8dd6ce7
Merge branch 'claude/a11y-dead' into claude/settings-ia
dormancygrace Oct 7, 2026
c2fe39b
web: unify the appearance settings layout
dormancygrace Oct 7, 2026
ac4cb4b
web: unify the system and device settings rows
dormancygrace Oct 7, 2026
f66a295
web: unify the USB settings layout
dormancygrace Oct 7, 2026
7b955a7
web: unify the memory settings layout
dormancygrace Oct 7, 2026
30e306b
web: unify the account and users settings layout
dormancygrace Oct 7, 2026
bf63f9d
web: unify the network settings layout
dormancygrace Oct 7, 2026
0ae9118
web: stop the HTTPS switch spinning after a cancelled confirmation
dormancygrace Oct 7, 2026
ece0062
web: unify date/time, MCP, diagnostics, dashboard, video and about
dormancygrace Oct 7, 2026
98bc4aa
web: unify the WireGuard, OpenVPN, Tailscale and NetBird settings
dormancygrace Oct 7, 2026
ef066ea
web: unify the software, updates, add-on and RustDesk settings
dormancygrace Oct 7, 2026
5b59036
web: drop the global mobile wrap rules for settings rows
dormancygrace Oct 7, 2026
372ef49
web: provide the memory page's new imports to its effect test
dormancygrace Oct 7, 2026
d9f403c
web: restore heading and paragraph spacing in settings pages
dormancygrace Oct 7, 2026
d8d040b
web: show no matching packages only after a search
dormancygrace Oct 7, 2026
6d3bd14
web: warn about ending Ethernet connections only when turning it off
dormancygrace Oct 7, 2026
21cc40b
web: show the overclock warning only while overclocked
dormancygrace Oct 7, 2026
53a3d2f
web: lay out reboot, sign-out and user rows like other settings
dormancygrace Oct 7, 2026
a3c74a1
web: show a spinner while RustDesk settings load
dormancygrace Oct 7, 2026
e64e219
rustdesk: stop turning USB back on at daemon start
dormancygrace Oct 7, 2026
d6fbd2e
web: open memory settings from the dashboard RAM tile
dormancygrace Oct 7, 2026
f5e1663
cpufreq: optionally apply the selected frequency at startup
dormancygrace Oct 7, 2026
e2a6a2b
web: list VPN providers on one VPN page instead of menu entries
dormancygrace Oct 7, 2026
9d0381d
web: show only installed VPNs on the dashboard
dormancygrace Oct 7, 2026
dbb5a4a
server, web: poll the remote desktop state with one request
dormancygrace Oct 7, 2026
0734511
rustdesk: answer status polls from cached package facts
dormancygrace Oct 7, 2026
8566406
web: merge small build chunks to cut first-load requests
dormancygrace Oct 7, 2026
4b411d1
Merge branch claude/rustdesk-status-speed into claude/settings-ia
dormancygrace Oct 7, 2026
e9f1767
Merge branch claude/web-chunks into claude/settings-ia
dormancygrace Oct 7, 2026
62e1358
server, web: send precompressed, long-cached build assets
dormancygrace Oct 7, 2026
efc419f
rustdesk: keep status fast while the repository indexes load
dormancygrace Oct 7, 2026
90806cf
Merge branch claude/settings-ia into claude/rustdesk-status-speed
dormancygrace Oct 7, 2026
3455790
server: generate ECDSA P-256 device certificates
dormancygrace Oct 7, 2026
5be3416
network: answer Wi-Fi status polls without wpa_cli and iw
dormancygrace Oct 7, 2026
f2e88a5
network: read the default gateways from /proc/net/route
dormancygrace Oct 7, 2026
58d66f9
wireguard: read each tunnel's status with one wg run
dormancygrace Oct 7, 2026
5e0df92
openvpn: stop running ip on every status poll
dormancygrace Oct 7, 2026
3963f39
server: add a shared apk runner with lock and memory guard
dormancygrace Oct 7, 2026
a70a181
server: run every apk through the shared runner
dormancygrace Oct 7, 2026
3e9cee5
vpn: keep tool versions until the program file changes
dormancygrace Oct 7, 2026
2843c88
netbird: answer status polls for a stopped daemon without commands
dormancygrace Oct 7, 2026
a176e3a
tailscale: read the status from tailscaled instead of the client
dormancygrace Oct 7, 2026
d1c6716
server: apply GOMEMLIMIT at start, default to a quarter of RAM
dormancygrace Oct 7, 2026
8ef0347
Merge branch claude/settings-ia into claude/hot-path-exec
dormancygrace Oct 7, 2026
34c1f84
server, kvmapp: let the OOM killer take helpers before the server
dormancygrace Oct 7, 2026
1a44ea4
Merge branch claude/settings-ia into claude/apk-memory-guard
dormancygrace Oct 7, 2026
45ee9b0
Merge branch claude/apk-memory-guard into claude/settings-ia
dormancygrace Oct 7, 2026
074e1dd
server: start daemons without the server OOM protection
dormancygrace Oct 7, 2026
1016cd0
platform: require a chosen APK signing key
dormancygrace Oct 7, 2026
0961b9e
platform: keep -d inside the user namespace
dormancygrace Oct 7, 2026
220e41c
packages: add nanokvm-keys, the owner of the release keys
dormancygrace Oct 7, 2026
f33f232
packages: sign APKs and indexes with RSA-SHA256
dormancygrace Oct 7, 2026
3d842bb
keys: add the ECDSA P-256 release key nkos-release-ec-b8e89b66
dormancygrace Oct 7, 2026
67ca082
platform: write a dual-signed v3 index and use it in the image
dormancygrace Oct 7, 2026
98b3e1f
nanokvm-keys: move devices to the v3 index after installing the keys
dormancygrace Oct 7, 2026
93080db
c906: dual-sign the overlay's Packages.adb with an ECDSA key
dormancygrace Oct 7, 2026
46e25c6
attended: read NanoKVM repositories through the v3 index
dormancygrace Oct 7, 2026
3e452cb
docs: describe the release keys, both indexes and key changes
dormancygrace Oct 7, 2026
bad616b
Merge branch claude/apk-keys into claude/settings-ia
dormancygrace Oct 7, 2026
01b745e
platform: expect the new web and server outputs
dormancygrace Oct 7, 2026
c6c429e
scripts: restore the executable modes lost in this branch
dormancygrace Oct 7, 2026
6ca7ada
server: cache only assets that exist
dormancygrace Oct 7, 2026
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
2 changes: 1 addition & 1 deletion docs/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- the toolchain, Linux kernel and modules, U-Boot, `fip.bin` and the boot images;
- the SOPHGO media libraries, the board service, the server and the web UI;
- the six `nanokvm-*` APK packages, the Alpine root file system and the image.
- the seven `nanokvm-*` APK packages, the Alpine root file system and the image.

Host requirements, the steps, the outputs and how they are checked are described in [platform/README.md](../platform/README.md). Buildroot (`firmware/buildroot/configs/nanokvm_platform_defconfig`) builds only the toolchain, host tools and initramfs utilities; the installed OS and optional software are Alpine APKs.

Expand Down
94 changes: 92 additions & 2 deletions firmware/alpine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ NanoKVM still supplies its SG2002 kernel, drivers, board firmware and applicatio

The target repository list is:

1. the signed NanoKVM repository (`https://nkos.pesin.pro/repos/nanokvm`);
1. the signed NanoKVM repository, read through its v3 index
(`https://nkos.pesin.pro/repos/nanokvm/riscv64/Packages.adb`; see
[signing keys](#signing-keys));
2. Alpine v3.24 `main`;
3. Alpine v3.24 `community`;
4. Alpine `edge/community`, tagged as `@edgecommunity`.
Expand Down Expand Up @@ -107,6 +109,93 @@ Full recovery images remain necessary for partition layout, filesystem, FIP or
bootloader changes. Moving to another Alpine stable branch is an explicit system
upgrade rather than an incidental package update.

## Signing keys

apk trusts every public key in `/etc/apk/keys` for every repository; it does
not tie a key to a repository. The NanoKVM release keys are in `keys/`:

| Key | Type | Signs |
|---|---|---|
| `dgrace-6aaddbb6.rsa.pub` | RSA 4096 | the packages and `APKINDEX.tar.gz` (RSA256, RSA with SHA-256); `Packages.adb` during the transition |
| `nkos-release-ec-b8e89b66.pub` | ECDSA P-256 | `Packages.adb` |

The package `nanokvm-keys` installs them in `/etc/apk/keys` and owns them, as
`alpine-keys` does for Alpine's keys; `nanokvm-base` depends on it. Because a
package owns the files, a release can add a key, and a key removed from `keys/`
is removed from devices when they update. A key copied by hand stays.

The image build must trust the keys before it can install from the repository,
so the rootfs step copies `keys/` into the root first. When apk then installs
`nanokvm-keys`, it takes over these identical files: `/etc/apk` is not a
protected path, so there is neither a conflict nor an `.apk-new`. Devices
installed before `nanokvm-keys` have the RSA key as a file without an owner;
their update takes it over the same way. (Tested with apk-tools 3.0.8 in a
scratch root on the device.)

### Two indexes

The repository has two indexes of the same abuild packages in `riscv64/`:

- `APKINDEX.tar.gz`, the v2 index, signed with the RSA key. Devices up to 2.0
use the repository line `https://nkos.pesin.pro/repos/nanokvm`, read this
index and trust only the RSA key.
- `Packages.adb`, the apk-tools v3 index, signed with the ECDSA key and, during
the transition, the RSA key; apk accepts it if it trusts either key. Images
from 2.5 on use the line
`https://nkos.pesin.pro/repos/nanokvm/riscv64/Packages.adb`. When an older
device installs 2.5, the post-install script of `nanokvm-keys` changes its
NanoKVM line to that URL, after the keys are in place. Alpine's lines and any
other line stay as they are.

apk checks a package from an index against the package hash in that index, so
the packages need no ECDSA signature.

### Publishing a release

1. Build with both release keys, each with its public key next to it:

```sh
platform/build.sh -k /secure/dgrace-6aaddbb6.rsa -e /secure/nkos-release-ec-b8e89b66.key
```

2. Upload the contents of `OUTPUT/release/apk/recipes/riscv64/` (the `*.apk`
files, `APKINDEX.tar.gz` and `Packages.adb`) to
`https://nkos.pesin.pro/repos/nanokvm/riscv64/`, replacing the previous
release. Upload the packages first and the two indexes last, so that no
index names a package that is not there yet.
3. Check the published indexes from a scratch root, for example
`apk --keys-dir DIR verify Packages.adb` with only one of the keys in `DIR`.

### Moving to the ECDSA key alone

- While devices on 2.0 or earlier may still update, keep publishing
`APKINDEX.tar.gz` signed with the RSA key, and keep the RSA signature on
`Packages.adb`. Such a device can update from the v2 index; that update
installs `nanokvm-keys` and moves it to the v3 index.
- Once those devices have updated, a later release may sign `Packages.adb` with
the ECDSA key alone (remove the RSA `--sign-key` from the packages step of
`platform/build.sh`). A device that still reads only the v2 index then keeps
working as long as `APKINDEX.tar.gz` is published; after that it needs the
ECDSA key and the new repository line by hand.
- The packages themselves stay signed with the RSA key, which `apk verify` and
`apk add ./file.apk` check. Keep `dgrace-6aaddbb6.rsa.pub` in `keys/` while
packages are signed with it.

### Replacing a key

1. Create the new key on the signing machine, add its public key to `keys/`,
and publish a release signed with the old key. `nanokvm-keys` ships the new
key; devices that install the release trust both keys. `platform/build.sh`
refuses `-k` or `-e` keys whose public key is not in `keys/`, so a key
cannot be used before it is shipped.
2. Sign the following releases with the new key. During a transition, sign each
index with both keys, so that a device that skipped step 1 can still update:
`Packages.adb` with two `--sign-key` options, and `APKINDEX.tar.gz` with a
second signature in front, `abuild-sign -t RSA256 -k old.rsa APKINDEX.tar.gz`.
apk-tools 3.0.8 accepts an index with two signatures if it trusts either key.
3. In a later release, remove the old key from `keys/`; updating `nanokvm-keys`
removes it from devices.

## Existing C906 installations

Removing the tuned repository alone does not replace its higher `pkgrel`
Expand Down Expand Up @@ -207,4 +296,5 @@ PAYLOAD_ROOT=work/alpine/payloads \
The repository consumed by APK is
`work/alpine/repo/stock/recipes/riscv64`; its parent
`work/alpine/repo/stock/recipes` is the repository URL because APK appends
the target architecture.
the target architecture. This helper writes only the v2 index; the packages
step of `platform/build.sh` also writes the signed v3 index `Packages.adb`.
21 changes: 14 additions & 7 deletions firmware/alpine/attended/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,20 @@ Repository order in the generated root is:
3. Alpine `main`;
4. Alpine `community`.

The NanoKVM and C906 repositories are read through their apk-tools v3 index:
a repository base URL becomes `URL/riscv64/Packages.adb`, and a URL ending in
`.adb` is used as it is. Pass the ECDSA key that signs the v3 index
(`nkos-release-ec-b8e89b66.pub`) as a `--repo-key` together with the RSA key
(see [signing keys](../README.md#signing-keys)).

The NanoKVM packages have unique names. Tuned replacements retain their Alpine
package names and use the tested higher `pkgrel`; packages missing from the two
local repositories resolve normally from Alpine. A changing Alpine branch URL
does not provide byte-for-byte reproducibility by itself. Production should
point `--alpine-main` and `--alpine-community` at immutable mirrored snapshots.
The output records the exact effective repository file and every installed
package version. The HTTP service also hashes the current remote Alpine
`APKINDEX.tar.gz` files, both local repository indexes, the public keys and all
`APKINDEX.tar.gz` files, both local `Packages.adb` indexes, the public keys and all
image-building inputs into the build ID. A repeated request is cached only while
those inputs are unchanged, so a new Alpine index produces a new personal image.

Expand Down Expand Up @@ -104,7 +110,8 @@ signing key:
--base-rootfs work/alpine/alpine-minirootfs-3.24.2-riscv64.tar.gz \
--boot-fit work/alpine/final-vm-good/boot-alpine.sd \
--nanokvm-repo work/alpine/repo-r4/stock/recipes \
--repo-key work/alpine/signing/dgrace-6aaddbb6.rsa.pub \
--repo-key firmware/alpine/keys/dgrace-6aaddbb6.rsa.pub \
--repo-key firmware/alpine/keys/nkos-release-ec-b8e89b66.pub \
--qemu-static work/alpine/tuned-builder-full/rootfs/usr/bin/qemu-riscv64-static
```

Expand All @@ -122,10 +129,9 @@ sudo apt-get install python3 qemu-user-static binfmt-support device-tree-compile
`qemu-user-static` and `binfmt-support` are required for APK maintainer scripts;
the bundled Buildroot `mkimage` invokes the host `dtc` when it creates the
recovery FIT. Run one build as root in a fresh output directory. The public key
filename must match the key name used by the APK signatures. The current
development NanoKVM and C906 repositories use the same signing key. If
production separates those keys, repeat the repo-key option once for each
public key.
filename of an RSA key must match the key name used by the APK signatures. The
NanoKVM repository needs both release keys; the C906 repository uses the same
RSA key. Repeat the repo-key option once for each public key.

```sh
sudo ./scripts/build-alpine-personal-image.sh \
Expand All @@ -135,7 +141,8 @@ sudo ./scripts/build-alpine-personal-image.sh \
--boot-fit /srv/nanokvm/input/boot-alpine.sd \
--boot-sha256 <verified-boot-sha256> \
--nanokvm-repo https://builder.example/nanokvm/stock/recipes \
--repo-key /srv/nanokvm/keys/nanokvm-packages.rsa.pub \
--repo-key /srv/nanokvm/keys/dgrace-6aaddbb6.rsa.pub \
--repo-key /srv/nanokvm/keys/nkos-release-ec-b8e89b66.pub \
--packages-file request.packages \
--output /srv/nanokvm/output/request-001
```
Expand Down
3 changes: 2 additions & 1 deletion firmware/alpine/attended/builder-config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"boot_sha256": "6421359320b60b6b03528f553290f266279f83cdacb03200af378d16254a5c12",
"nanokvm_repo": "repos/nanokvm",
"repo_keys": [
"keys/dgrace-6aaddbb6.rsa.pub"
"keys/dgrace-6aaddbb6.rsa.pub",
"keys/nkos-release-ec-b8e89b66.pub"
],
"qemu_static": "qemu/qemu-riscv64-static",
"output_root": "output",
Expand Down
4 changes: 4 additions & 0 deletions firmware/alpine/keys/nkos-release-ec-b8e89b66.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+4mJDPL3dBzWA5n3rem3qAECqfmW
Htic+nbyTgyVr6FyBp+KCv/kAcuy2fSD4kbd2KtIdcJsU/A2SXKqYblppQ==
-----END PUBLIC KEY-----
9 changes: 7 additions & 2 deletions firmware/alpine/packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and invokes `abuild` in dependency order.
Set `PAYLOAD_ROOT` to a directory containing these subdirectories:

```
keys/ etc/apk/keys/*.pub (default: firmware/alpine/keys)
base/ files installed below /
kernel-sg2002/ FIT, DTB and kernel payload below /usr/lib/nanokvm/boot
kmod-sg2002/ lib/modules/<kernel-release>/
Expand All @@ -24,7 +25,10 @@ release/ etc/nanokvm-release and build profile metadata
The helper also accepts `PAYLOAD_ROOT/<profile>/...` (for example
`PAYLOAD_ROOT/stock/app` and `PAYLOAD_ROOT/c906-scalar/app`) so stock and tuned
repositories can be generated from the same checkout. Files containing device
identity, keys, `/data`, or user settings must not be put in these payloads.
identity, private keys, `/data`, or user settings must not be put in these
payloads; `nanokvm-keys` refuses anything but public keys in `/etc/apk/keys`.
`nanokvm-base` depends on `nanokvm-keys`, which owns the repository keys (see
[signing keys](../README.md#signing-keys)).

## Sophgo `devmem` dependency

Expand Down Expand Up @@ -72,7 +76,8 @@ The helper copies each recipe to a temporary build directory and runs
`abuild checksum` there before `abuild -r`. This keeps checksums out of the
working tree while retaining normal APKBUILD checksum verification. The final
repository check requires a valid `APKINDEX.tar.gz` and one APK per recipe;
package and index signing are performed by the configured abuild key.
package and index signing are performed by the configured abuild key, with
RSA256 signatures (RSA with SHA-256) instead of abuild's default RSA with SHA-1.

The kernel APK stages all board FITs and their hashes below
`/usr/lib/nanokvm/boot` and has an exact dependency on the matching module APK.
Expand Down
2 changes: 1 addition & 1 deletion firmware/alpine/packages/nanokvm-base/APKBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pkgdesc="NanoKVM Alpine integration and OpenRC services"
url="https://github.com/sipeed/NanoKVM"
arch="riscv64"
license="GPL-3.0-only AND GPL-2.0-only"
depends="bash chrony-openrc avahi-openrc dbus-openrc busybox-openrc openssh-server openssh-server-common nftables>=1.1.6-r0 iproute2-tc util-linux openrc eudev kmod exfatprogs f2fs-tools"
depends="nanokvm-keys bash chrony-openrc avahi-openrc dbus-openrc busybox-openrc openssh-server openssh-server-common nftables>=1.1.6-r0 iproute2-tc util-linux openrc eudev kmod exfatprogs f2fs-tools"
source="$pkgname-$pkgver.tar.gz
nanokvm-banner.sh"
options="!check !strip"
Expand Down
36 changes: 36 additions & 0 deletions firmware/alpine/packages/nanokvm-keys/APKBUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# The public keys that NanoKVM OS packages and indexes are signed with, as
# alpine-keys holds Alpine's. Owning them lets an update add or remove a key.
pkgname=nanokvm-keys
pkgver=${PKGVER:-0.1.0}
pkgrel=0
pkgdesc="Public keys for NanoKVM OS packages"
url="https://github.com/sipeed/NanoKVM"
arch="noarch"
license="GPL-3.0-only"
source="$pkgname-$pkgver.tar.gz"
options="!check !strip !tracedeps"
# Moves the NanoKVM repository line to the v3 index once the keys are in place.
install="$pkgname.post-install"

build() {
:
}

package() {
mkdir -p "$pkgdir"
cp -a "$srcdir/$pkgname-$pkgver/." "$pkgdir/"
# Public keys in /etc/apk/keys and nothing else, never a private key.
local f count=0
for f in $(cd "$pkgdir" && find . ! -type d | LC_ALL=C sort); do
case $f in
./etc/apk/keys/*.pub) ;;
*) die "unexpected file $f" ;;
esac
[ -f "$pkgdir/$f" ] && [ "$(head -n 1 "$pkgdir/$f")" = "-----BEGIN PUBLIC KEY-----" ] &&
! grep -q PRIVATE "$pkgdir/$f" || die "$f is not a public key"
count=$((count + 1))
done
[ "$count" -gt 0 ] || die "no keys in the payload"
}

sha256sums=""
23 changes: 23 additions & 0 deletions firmware/alpine/packages/nanokvm-keys/nanokvm-keys.post-install
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh
# Devices from before nanokvm-keys read the v2 index (APKINDEX.tar.gz) of the
# NanoKVM repository, which only the RSA key signs. Now that this package has
# installed the release keys, including the ECDSA key, read the v3 index
# (Packages.adb) instead; it is signed with the ECDSA key. Only that one
# exact line changes: Alpine's repositories and any other line stay as they
# are, and a failure leaves the working v2 line in place.
repositories=/etc/apk/repositories
old=https://nkos.pesin.pro/repos/nanokvm
new=https://nkos.pesin.pro/repos/nanokvm/riscv64/Packages.adb

[ -f "$repositories" ] || exit 0
awk -v old="$old" '{ sub(/^[ \t]+/, ""); sub(/[ \t]+$/, ""); sub(/\/$/, "") }
$0 == old { found = 1 } END { exit !found }' "$repositories" || exit 0
tmp=$(mktemp "$repositories.XXXXXX") || exit 0
if awk -v old="$old" -v new="$new" '{ line = $0; sub(/^[ \t]+/, "", line); sub(/[ \t]+$/, "", line)
sub(/\/$/, "", line); print (line == old ? new : $0) }' "$repositories" > "$tmp" &&
chmod 0644 "$tmp" && mv "$tmp" "$repositories"; then
echo "NanoKVM repository: now $new" >&2
else
rm -f "$tmp"
fi
exit 0
2 changes: 1 addition & 1 deletion firmware/alpine/packages/nanokvm-release/APKBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pkgdesc="Tested NanoKVM Alpine package set"
url="https://github.com/sipeed/NanoKVM"
arch="riscv64"
license="GPL-3.0-only"
depends="alpine-base busybox-extras tzdata busybox-openrc !openvpn3 !openssl4-libs avahi avahi-openrc bash chrony chrony-openrc curl dbus dbus-openrc dnsmasq iproute2-tc nftables openresolv util-linux wireguard-tools ca-certificates coreutils eudev exfatprogs f2fs-tools hostapd i2c-tools iproute2 iw kmod libgcc libstdc++ lz4 openssh openssl util-linux-misc wpa_supplicant xz zstd nanokvm-base>=$pkgver-r0 nanokvm-kernel-sg2002>=$pkgver-r0 nanokvm-kmod-sg2002>=$pkgver-r0 nanokvm-firmware-sg2002>=$pkgver-r0 nanokvm-app>=$pkgver-r0"
depends="alpine-base busybox-extras tzdata busybox-openrc !openvpn3 !openssl4-libs avahi avahi-openrc bash chrony chrony-openrc curl dbus dbus-openrc dnsmasq iproute2-tc nftables openresolv util-linux wireguard-tools ca-certificates coreutils eudev exfatprogs f2fs-tools hostapd i2c-tools iproute2 iw kmod libgcc libstdc++ lz4 openssh openssl util-linux-misc wpa_supplicant xz zstd nanokvm-keys>=$pkgver-r0 nanokvm-base>=$pkgver-r0 nanokvm-kernel-sg2002>=$pkgver-r0 nanokvm-kmod-sg2002>=$pkgver-r0 nanokvm-firmware-sg2002>=$pkgver-r0 nanokvm-app>=$pkgver-r0"
source="$pkgname-$pkgver.tar.gz"
options="!check !strip !tracedeps"

Expand Down
57 changes: 57 additions & 0 deletions firmware/alpine/tests/test-keys-repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""nanokvm-keys post-install moves only the NanoKVM line to the v3 index."""
from pathlib import Path
import subprocess
import tempfile
import unittest

ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "firmware/alpine/packages/nanokvm-keys/nanokvm-keys.post-install"
V3 = "https://nkos.pesin.pro/repos/nanokvm/riscv64/Packages.adb"
ALPINE = ("https://dl-cdn.alpinelinux.org/alpine/v3.24/main\n"
"https://dl-cdn.alpinelinux.org/alpine/v3.24/community\n"
"@edgecommunity https://dl-cdn.alpinelinux.org/alpine/edge/community\n")


class RepositorySwitchTests(unittest.TestCase):
def run_script(self, content):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "repositories"
if content is not None:
path.write_text(content)
# Redirect the file only in the test copy; no host writes.
source = SCRIPT.read_text().replace("/etc/apk/repositories", str(path))
result = subprocess.run(["sh", "-s"], input=source, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
left = sorted(p.name for p in Path(directory).iterdir())
return (path.read_text() if path.exists() else None), left

def test_v2_line_moves_to_v3(self):
text, files = self.run_script("https://nkos.pesin.pro/repos/nanokvm\n" + ALPINE)
self.assertEqual(text, V3 + "\n" + ALPINE)
self.assertEqual(files, ["repositories"])

def test_trailing_slash_and_spaces(self):
text, _ = self.run_script(" https://nkos.pesin.pro/repos/nanokvm/ \n" + ALPINE)
self.assertEqual(text, V3 + "\n" + ALPINE)

def test_other_lines_unchanged(self):
original = ("https://nkos.pesin.pro/repos/c906-qualified\n"
"@nk https://nkos.pesin.pro/repos/nanokvm\n"
"# https://nkos.pesin.pro/repos/nanokvm\n"
"https://nkos.pesin.pro/repos/nanokvm-test\n" + ALPINE)
text, _ = self.run_script(original)
self.assertEqual(text, original)

def test_already_v3_unchanged(self):
text, _ = self.run_script(V3 + "\n" + ALPINE)
self.assertEqual(text, V3 + "\n" + ALPINE)

def test_missing_file(self):
text, files = self.run_script(None)
self.assertIsNone(text)
self.assertEqual(files, [])


if __name__ == "__main__":
unittest.main()
Loading