diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index c6f27b6..60ad1ef 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -109,6 +109,82 @@ jobs: echo "pkg=$PKG_FILE" >> "$GITHUB_OUTPUT" + - name: Verify .pkg structural correctness + shell: bash + run: | + set -euo pipefail + PKG="${{ steps.macos-assets.outputs.pkg }}" + EXPAND_DIR="$(mktemp -d)/expanded" + PAYLOAD_DIR="$(mktemp -d)/payload" + fail=0 + + echo "==> Verifying $PKG" + + # 1) Flat-package expansion + if pkgutil --expand "$PKG" "$EXPAND_DIR"; then + echo "PASS: pkgutil --expand" + else + echo "FAIL: pkgutil --expand" + fail=1 + fi + + # Extract the cpio.gz Payload so we can inspect installed file layout + PAYLOAD_FILE="$(find "$EXPAND_DIR" -name Payload -type f | head -n 1)" + if [[ -z "$PAYLOAD_FILE" ]]; then + echo "FAIL: no Payload file inside expanded pkg" + fail=1 + else + mkdir -p "$PAYLOAD_DIR" + (cd "$PAYLOAD_DIR" && gzip -dc "$PAYLOAD_FILE" | cpio -i --quiet) + echo "PASS: extracted Payload to $PAYLOAD_DIR" + fi + + # 2) Binary at canonical install path (./usr/local/bin/fips inside payload) + BIN_PATH="$PAYLOAD_DIR/usr/local/bin/fips" + if [[ -f "$BIN_PATH" ]]; then + echo "PASS: binary present at usr/local/bin/fips" + else + echo "FAIL: binary missing at usr/local/bin/fips" + echo " fallback search:" + find "$PAYLOAD_DIR" -name fips -type f -print || true + fail=1 + fi + for extra in fipsctl fipstop; do + if [[ -f "$PAYLOAD_DIR/usr/local/bin/$extra" ]]; then + echo "PASS: binary present at usr/local/bin/$extra" + else + echo "FAIL: binary missing at usr/local/bin/$extra" + fail=1 + fi + done + + # 3) LaunchDaemon plist at canonical location + PLIST_PATH="$PAYLOAD_DIR/Library/LaunchDaemons/com.fips.daemon.plist" + if [[ -f "$PLIST_PATH" ]]; then + echo "PASS: plist present at Library/LaunchDaemons/com.fips.daemon.plist" + else + echo "FAIL: plist missing at Library/LaunchDaemons/com.fips.daemon.plist" + echo " fallback search:" + find "$PAYLOAD_DIR" -name '*.plist' -print || true + fail=1 + fi + + # 4) plutil -lint on the plist + if [[ -f "$PLIST_PATH" ]]; then + if plutil -lint "$PLIST_PATH"; then + echo "PASS: plutil -lint" + else + echo "FAIL: plutil -lint" + fail=1 + fi + fi + + if [[ "$fail" -ne 0 ]]; then + echo "==> .pkg verification FAILED" + exit 1 + fi + echo "==> .pkg verification PASSED" + - name: SHA-256 hash run: | echo "==> macOS release asset:" diff --git a/.github/workflows/package-openwrt.yml b/.github/workflows/package-openwrt.yml index 15fccaf..0529938 100644 --- a/.github/workflows/package-openwrt.yml +++ b/.github/workflows/package-openwrt.yml @@ -200,6 +200,188 @@ jobs: LLVM_STRIP: llvm-strip run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }} + - name: Install shellcheck (if missing) + shell: bash + run: | + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y --no-install-recommends shellcheck + fi + shellcheck --version + + - name: Lint shipped shell scripts + shell: bash + run: | + set -euo pipefail + FILES_DIR=packaging/openwrt-ipk/files + # Scripts shipped inside the .ipk. The init scripts use the OpenWrt + # `#!/bin/sh /etc/rc.common` shebang; tell shellcheck to treat them + # as POSIX sh and silence the unrecognized-shebang warning (SC1008). + # SC2317 (unreachable command) fires on rc.common's externally-invoked + # start_service/stop_service/reload_service hooks. + TARGETS=( + "$FILES_DIR/etc/init.d/fips" + "$FILES_DIR/etc/init.d/fips-gateway" + "$FILES_DIR/etc/fips/firewall.sh" + "$FILES_DIR/etc/hotplug.d/net/99-fips" + "$FILES_DIR/etc/uci-defaults/90-fips-setup" + ) + fail=0 + for f in "${TARGETS[@]}"; do + if [ ! -f "$f" ]; then + echo "FAIL: missing $f" + fail=1 + continue + fi + echo "==> shellcheck $f" + if shellcheck --shell=sh --exclude=SC1008,SC2317 "$f"; then + echo " PASS" + else + echo " FAIL" + fail=1 + fi + done + if [ "$fail" -ne 0 ]; then + echo "shellcheck FAILED" + exit 1 + fi + echo "shellcheck PASS (${#TARGETS[@]} scripts)" + + - name: Sysctl drop-in syntax check + shell: bash + run: | + set -euo pipefail + FILES_DIR=packaging/openwrt-ipk/files + TARGETS=( + "$FILES_DIR/etc/sysctl.d/fips-gateway.conf" + "$FILES_DIR/etc/sysctl.d/fips-bridge.conf" + ) + fail=0 + for conf in "${TARGETS[@]}"; do + if [ ! -f "$conf" ]; then + echo "FAIL: missing $conf" + fail=1 + continue + fi + echo "==> validating $conf" + lineno=0 + file_ok=1 + while IFS= read -r line || [ -n "$line" ]; do + lineno=$((lineno + 1)) + # Skip comments and blank lines. + case "$line" in + ''|\#*) continue ;; + esac + # Match: = where key is dotted lower-id and value is + # an integer (sysctl drop-ins shipped here are all numeric toggles). + if ! [[ "$line" =~ ^[a-z0-9_.-]+[[:space:]]*=[[:space:]]*-?[0-9]+[[:space:]]*$ ]]; then + echo " FAIL line $lineno: $line" + file_ok=0 + fi + done < "$conf" + if [ "$file_ok" -eq 1 ]; then + echo " PASS" + else + fail=1 + fi + done + if [ "$fail" -ne 0 ]; then + echo "sysctl drop-in syntax check FAILED" + exit 1 + fi + echo "sysctl drop-in syntax check PASS" + + - name: Verify ipk structural integrity + shell: bash + run: | + set -euo pipefail + IPK="dist/${{ env.PACKAGE_FILENAME }}" + if [ ! -f "$IPK" ]; then + echo "FAIL: produced ipk not found at $IPK" + exit 1 + fi + echo "==> file type:" + file "$IPK" + + # OpenWrt .ipk = tar.gz containing debian-binary + control.tar.gz + + # data.tar.gz (NOT an ar archive like Debian's .deb). + WORK=$(mktemp -d) + trap 'rm -rf "$WORK"' EXIT + + tar -xzf "$IPK" -C "$WORK" + echo "==> top-level entries:" + ls -la "$WORK" + + # Top-level structural assertions. + fail=0 + for entry in debian-binary control.tar.gz data.tar.gz; do + if [ ! -f "$WORK/$entry" ]; then + echo "FAIL: missing top-level $entry" + fail=1 + else + echo " PASS top-level: $entry" + fi + done + if [ "$fail" -ne 0 ]; then exit 1; fi + + # debian-binary content sanity. + dbin_content=$(cat "$WORK/debian-binary" | tr -d '[:space:]') + if [ "$dbin_content" != "2.0" ]; then + echo "FAIL: debian-binary content is '$dbin_content' (expected 2.0)" + exit 1 + fi + echo " PASS debian-binary content: 2.0" + + # Inspect data.tar.gz contents. + DATA_LIST="$WORK/data.list" + tar -tzf "$WORK/data.tar.gz" > "$DATA_LIST" + echo "==> data.tar.gz entry count: $(wc -l < "$DATA_LIST")" + + # Required filesystem entries inside data.tar.gz. Entries are + # produced with a leading "./" by build-ipk.sh. + REQUIRED=( + ./usr/bin/fips + ./usr/bin/fipsctl + ./usr/bin/fipstop + ./usr/bin/fips-gateway + ./etc/init.d/fips + ./etc/init.d/fips-gateway + ./etc/fips/fips.yaml + ./etc/fips/firewall.sh + ./etc/dnsmasq.d/fips.conf + ./etc/sysctl.d/fips-gateway.conf + ./etc/sysctl.d/fips-bridge.conf + ./etc/hotplug.d/net/99-fips + ./etc/uci-defaults/90-fips-setup + ./lib/upgrade/keep.d/fips + ) + for path in "${REQUIRED[@]}"; do + if grep -Fxq "$path" "$DATA_LIST"; then + echo " PASS data: $path" + else + echo " FAIL data: missing $path" + fail=1 + fi + done + + # Inspect control.tar.gz: must contain control file + maintainer scripts. + CTRL_LIST="$WORK/control.list" + tar -tzf "$WORK/control.tar.gz" > "$CTRL_LIST" + echo "==> control.tar.gz entry count: $(wc -l < "$CTRL_LIST")" + for path in ./control ./conffiles ./postinst ./prerm; do + if grep -Fxq "$path" "$CTRL_LIST"; then + echo " PASS control: $path" + else + echo " FAIL control: missing $path" + fail=1 + fi + done + + if [ "$fail" -ne 0 ]; then + echo "ipk structural verification FAILED" + exit 1 + fi + echo "ipk structural verification PASS" + - name: SHA-256 hashes run: | echo "==> Binaries:" diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml index 5cd742c..5b98792 100644 --- a/.github/workflows/package-windows.yml +++ b/.github/workflows/package-windows.yml @@ -86,6 +86,54 @@ jobs: -Version "${{ needs.determine-versioning.outputs.package_version }}" ` -NoBuild + - name: Verify ZIP structural correctness + shell: pwsh + run: | + $zip = Get-ChildItem deploy\fips-*-windows-*.zip | Select-Object -First 1 + if (-not $zip) { Write-Error "No ZIP artifact found in deploy\"; exit 1 } + Write-Host "Verifying: $($zip.Name)" + + $extractDir = "verify-extract" + if (Test-Path $extractDir) { Remove-Item -Recurse -Force $extractDir } + Expand-Archive -Path $zip.FullName -DestinationPath $extractDir + + # Expected top-level files (flat ZIP, no wrapper directory). + # Source of truth: packaging/windows/build-zip.ps1 + $expected = @( + "fips.exe", + "fipsctl.exe", + "fipstop.exe", + "fips.yaml", + "hosts", + "install-service.ps1", + "uninstall-service.ps1", + "README.txt" + ) + + $missing = @() + foreach ($f in $expected) { + $path = Join-Path $extractDir $f + if (Test-Path -LiteralPath $path -PathType Leaf) { + Write-Host "PASS: $f" + } else { + Write-Host "FAIL: $f" + $missing += $f + } + } + + Write-Host "" + Write-Host "ZIP contents:" + Get-ChildItem -Path $extractDir -Recurse | ForEach-Object { + Write-Host " $($_.FullName.Substring((Resolve-Path $extractDir).Path.Length + 1))" + } + + if ($missing.Count -gt 0) { + Write-Error "Missing expected files: $($missing -join ', ')" + exit 1 + } + Write-Host "" + Write-Host "All expected files present." + - name: SHA-256 hash shell: pwsh run: |