#!/usr/bin/env bash # Read-only, loopless validation of a composed Turing RK1 image. set -Eeuo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) IMAGE=${1:-} BASE_RAW=${2:-"$PROJECT_DIR/downloads/Armbian_community_26.11.0-trunk.1_Turing-rk1_trixie_vendor_6.1.115_minimal.img"} [[ -n "$IMAGE" && -f "$IMAGE" ]] || { echo "Usage: $0 IMAGE.img[.xz] [BASE.img]" >&2 exit 2 } for tool in cmp debugfs e2fsck jq sfdisk sgdisk sha256sum xz; do command -v "$tool" >/dev/null || { echo "Missing command: $tool" >&2; exit 1; } done TEST_TMP=$(mktemp -d /tmp/rk1-image-validation.XXXXXXXX) trap 'rm -rf -- "$TEST_TMP"' EXIT RAW_IMAGE=$IMAGE if [[ "$IMAGE" == *.xz ]]; then xz --test "$IMAGE" RAW_IMAGE="$TEST_TMP/image.img" xz --decompress --stdout "$IMAGE" >"$RAW_IMAGE" fi sgdisk --verify "$RAW_IMAGE" >/dev/null partition_json=$(sfdisk --json "$RAW_IMAGE") [[ "$(jq '.partitiontable.partitions | length' <<<"$partition_json")" == 1 ]] || { echo 'Image must contain exactly one root partition' >&2 exit 1 } partition_start=$(jq -r '.partitiontable.partitions[0].start' <<<"$partition_json") partition_size=$(jq -r '.partitiontable.partitions[0].size' <<<"$partition_json") partition_type=$(jq -r '.partitiontable.partitions[0].type' <<<"$partition_json") [[ "$partition_start" == 32768 ]] || { echo "Unexpected partition start: $partition_start" >&2; exit 1; } [[ "$partition_type" == B921B045-1DF0-41C3-AF44-4C6F280D3FAE ]] || { echo "Unexpected ARM64 root partition type: $partition_type" >&2 exit 1 } if [[ -f "$BASE_RAW" ]]; then cmp --silent --bytes=440 "$BASE_RAW" "$RAW_IMAGE" || { echo 'MBR bootstrap differs from the locked base image' >&2 exit 1 } loader_offset=$((64 * 512)) loader_length=$(((32768 - 64) * 512)) cmp --silent --ignore-initial="$loader_offset:$loader_offset" \ --bytes="$loader_length" "$BASE_RAW" "$RAW_IMAGE" || { echo 'Rockchip loader region differs from the locked base image' >&2 exit 1 } fi ROOTFS="$TEST_TMP/rootfs.ext4" dd if="$RAW_IMAGE" of="$ROOTFS" bs=512 skip="$partition_start" \ count="$partition_size" status=none e2fsck -fn "$ROOTFS" >"$TEST_TMP/e2fsck.log" 2>&1 || { cat "$TEST_TMP/e2fsck.log" >&2 exit 1 } ext4_stat() { debugfs -R "stat $1" "$ROOTFS" 2>/dev/null; } ext4_exists() { ext4_stat "$1" | grep -q '^Inode:'; } assert_contains() { local path=$1 pattern=$2 debugfs -R "cat $path" "$ROOTFS" 2>/dev/null | grep -Eq "$pattern" || { echo "Missing $pattern in $path" >&2 exit 1 } } assert_mode() { local path=$1 mode=$2 ext4_stat "$path" | grep -Eq "Mode:[[:space:]]+$mode" || { echo "Unexpected mode for $path (wanted $mode)" >&2 exit 1 } } assert_contains /etc/os-release '^VERSION_ID="?13"?$' assert_contains /etc/armbian-release '^BOARD=turing-rk1$' assert_contains /boot/armbianEnv.txt '^fdtfile=rockchip/rk3588-turing-rk1\.dtb$' assert_contains /boot/armbianEnv.txt '^overlays=.*panthor-gpu' assert_contains /etc/shadow '^root:!:' assert_contains /etc/default/armbian-firstrun '^OPENSSHD_REGENERATE_HOST_KEYS=false$' assert_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^PermitRootLogin no$' assert_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^PasswordAuthentication no$' assert_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^AuthenticationMethods publickey$' assert_contains /etc/systemd/system/rk1-media-provision.service '^Before=.*kodi-rk\.service$' assert_contains /usr/local/sbin/rk1-media-provision \ '^seed_archives=/opt/rk1-seed/debs$' assert_contains /usr/local/sbin/rk1-media-provision \ 'Dir::Cache::archives=.*seed_archives' assert_contains /usr/local/sbin/rk1-media-provision \ '^systemctl --no-block start .*kodi-rk\.service$' assert_contains /usr/local/sbin/rk1-media-provision \ '^udevadm trigger --action=add --subsystem-match=misc' assert_contains /etc/systemd/system/rk1-media-identity.service '^RuntimeDirectory=sshd$' assert_contains /usr/local/sbin/rk1-media-identity \ '^install -d -o root -g root -m 0755 /run/sshd$' assert_contains /usr/local/sbin/rk1-media-identity '^/usr/sbin/sshd -t$' assert_contains /opt/rk1-seed/debs.sha256 \ '^dd0ca68696aa35b1998f9c9e131fdf8f0d1ef4ba6f58a36d0d99f0625d47f430 rk1-media-stack_' for required_path in \ /boot/dtb/rockchip/rk3588-turing-rk1.dtb \ /boot/dtb/rockchip/overlay/rockchip-rk3588-panthor-gpu.dtbo \ /lib/modules/6.1.115-vendor-rk35xx/kernel/drivers/gpu/drm/panthor/panthor.ko \ /opt/rknn/2.3.2/lib/librknnrt.so \ /opt/rknn/2.3.2/share/models/rk3588/mobilenet_v1.rknn \ /usr/local/bin/rknn-inference-test \ /usr/local/bin/rk1-media-selftest \ /usr/local/sbin/rk1-media-migrate-root-to-nvme \ /usr/local/sbin/rk1-media-diagnostics; do ext4_exists "$required_path" || { echo "Missing image payload: $required_path" >&2; exit 1; } done for critical_dir in / /etc /etc/ssh /etc/systemd /usr /opt; do assert_mode "$critical_dir" 0755 done assert_mode /etc/shadow 0640 ext4_stat /etc/shadow | grep -Eq 'Group:[[:space:]]+42' || { echo '/etc/shadow is not owned by the shadow group' >&2 exit 1 } assert_mode /usr/local/sbin/rk1-media-identity 0755 assert_mode /usr/local/sbin/rk1-media-provision 0755 assert_mode /usr/local/sbin/rk1-media-migrate-root-to-nvme 0755 ext4_stat /etc/systemd/system/sysinit.target.wants/rk1-media-identity.service \ | grep -Fq 'Fast link dest: "../rk1-media-identity.service"' || exit 1 ext4_stat /etc/systemd/system/multi-user.target.wants/rk1-media-provision.service \ | grep -Fq 'Fast link dest: "../rk1-media-provision.service"' || exit 1 for forbidden_path in /root/.not_logged_in_yet /root/.ssh/authorized_keys \ /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_ecdsa_key \ /etc/ssh/ssh_host_ed25519_key; do if ext4_exists "$forbidden_path"; then echo "Image contains forbidden credential: $forbidden_path" >&2 exit 1 fi done deb_count=$(debugfs -R 'ls -p /opt/rk1-seed/debs' "$ROOTFS" 2>/dev/null \ | grep -c '/.*\.deb/' || true) [[ "$deb_count" == 141 ]] || { echo "Expected 141 offline packages, found $deb_count" >&2; exit 1; } echo "PASS: loopless RK1 image validation ($IMAGE)"