Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

SHELL := /bin/bash
.PHONY: all iso iso-netinst iso-offline package sbom clean test help
.PHONY: all iso iso-netinst iso-offline package sbom clean test test-integrity help

# Build configuration
CODENAME := trixie
Expand Down Expand Up @@ -37,6 +37,7 @@ help:
@echo " package PKG=x Build specific package (cx-core, cx-full, cx-archive-keyring)"
@echo " sbom Generate Software Bill of Materials"
@echo " test Run build verification tests"
@echo " test-integrity Run APT repository integrity verifier tests"
@echo " clean Remove build artifacts"
@echo " deps Install build dependencies"
@echo ""
Expand Down Expand Up @@ -162,6 +163,11 @@ test:
./tests/verify-preseed.sh || true
@echo -e "$(GREEN)Tests complete$(NC)"

test-integrity:
@echo -e "$(GREEN)Running package integrity verifier tests...$(NC)"
bash ./tests/verify-integrity-test.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with other test invocations in this Makefile (lines 161-163), the test script should be called directly rather than via bash, assuming it has the appropriate shebang and execution permissions.

	./tests/verify-integrity-test.sh

@echo -e "$(GREEN)Integrity verifier tests complete$(NC)"

# Clean build artifacts
clean:
@echo -e "$(YELLOW)Cleaning build artifacts...$(NC)"
Expand Down
25 changes: 25 additions & 0 deletions apt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,31 @@ gpg --armor --export-secret-keys YOUR_KEY_ID
./scripts/sign-release.sh YOUR_KEY_ID
```

## Integrity Verification

Use `scripts/verify-integrity.sh` to validate repository metadata and package
artifacts before publishing or after downloading a repository snapshot.

```bash
# Verify package checksums from Packages indexes
./scripts/verify-integrity.sh .

# Also verify Release.gpg and InRelease signatures with the public key
./scripts/verify-integrity.sh --keyring deploy/pub.gpg .
```

The verifier reports:

- SHA256 checksum mismatches for `.deb` files listed in `Packages` or
`Packages.gz`
- Missing package files referenced by package indexes
- Unsafe package paths in repository metadata
- Invalid or missing `Release.gpg` / `InRelease` signatures when a keyring is
provided

This check is read-only. It does not install packages or execute package
maintainer scripts.

## GitHub Pages Setup

1. Go to Settings → Pages
Expand Down
218 changes: 218 additions & 0 deletions apt/scripts/verify-integrity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#!/bin/bash
# verify-integrity.sh - Verify CX Linux APT repository package integrity
#
# Checks repository metadata signatures and validates package checksums from
# Packages indexes without installing or executing package contents.
#
# Usage:
# ./apt/scripts/verify-integrity.sh [repo-root]
# ./apt/scripts/verify-integrity.sh --keyring /path/to/pub.gpg [repo-root]
#
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

KEYRING=""
REPO_ROOT=""

usage() {
cat <<'EOF'
Usage: verify-integrity.sh [--keyring KEYRING] [REPO_ROOT]

Verify a CX Linux APT repository:
- Release.gpg detached signatures when a keyring is supplied
- InRelease clearsigned metadata when a keyring is supplied
- SHA256 checksums listed in Packages and Packages.gz indexes
- Missing or tampered .deb files referenced by package indexes

Arguments:
REPO_ROOT Repository root containing dists/ and pool/.
Defaults to the parent of this script's directory.

Options:
--keyring PATH Public GPG keyring or armored key used for signature checks.
-h, --help Show this help text.
EOF
}

while [[ $# -gt 0 ]]; do
case "$1" in
--keyring)
if [[ $# -lt 2 ]]; then
echo "Error: --keyring requires a path" >&2
exit 2
fi
KEYRING="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Error: unknown option: $1" >&2
usage >&2
exit 2
;;
*)
if [[ -n "$REPO_ROOT" ]]; then
echo "Error: multiple repository roots provided" >&2
exit 2
fi
REPO_ROOT="$1"
shift
;;
esac
done

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"

if [[ ! -d "$REPO_ROOT" ]]; then
echo "Error: repository root does not exist: $REPO_ROOT" >&2
exit 2
fi

DISTS_DIR="$REPO_ROOT/dists"
POOL_DIR="$REPO_ROOT/pool"

if [[ ! -d "$DISTS_DIR" ]]; then
echo "Error: missing dists directory: $DISTS_DIR" >&2
exit 2
fi

PASS=0
FAIL=0
WARN=0

pass() {
printf 'PASS %s\n' "$1"
PASS=$((PASS + 1))
}

fail() {
printf 'FAIL %s\n' "$1" >&2
FAIL=$((FAIL + 1))
}

warn() {
printf 'WARN %s\n' "$1" >&2
WARN=$((WARN + 1))
}

verify_signature() {
local release_file="$1"
local dist_dir
dist_dir="$(dirname "$release_file")"

if [[ -z "$KEYRING" ]]; then
warn "signature checks skipped for ${release_file#$REPO_ROOT/}; no --keyring supplied"
return
fi

if [[ ! -f "$KEYRING" ]]; then
fail "keyring not found: $KEYRING"
return
fi

if [[ -f "$dist_dir/Release.gpg" ]]; then
if gpgv --keyring "$KEYRING" "$dist_dir/Release.gpg" "$release_file" >/dev/null 2>&1; then
pass "detached signature valid: ${dist_dir#$REPO_ROOT/}/Release.gpg"
else
fail "detached signature invalid: ${dist_dir#$REPO_ROOT/}/Release.gpg"
fi
else
warn "missing detached signature: ${dist_dir#$REPO_ROOT/}/Release.gpg"
fi

if [[ -f "$dist_dir/InRelease" ]]; then
if gpgv --keyring "$KEYRING" "$dist_dir/InRelease" >/dev/null 2>&1; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The gpgv utility does not support armored GPG keyrings (ASCII text starting with -----BEGIN PGP PUBLIC KEY BLOCK-----). However, the apt/README.md (line 122) instructs users to export the public key using the --armor flag. This will cause gpgv to fail with an error like unknown type of key resource when verifying signatures.

Consider using gpg --no-default-keyring --keyring "$KEYRING" --verify which supports both binary and armored formats, or add a step to dearmor the keyring using gpg --dearmor if an armored file is detected.

pass "clearsigned metadata valid: ${dist_dir#$REPO_ROOT/}/InRelease"
else
fail "clearsigned metadata invalid: ${dist_dir#$REPO_ROOT/}/InRelease"
fi
else
warn "missing clearsigned metadata: ${dist_dir#$REPO_ROOT/}/InRelease"
fi
}

packages_stream() {
local package_index="$1"

case "$package_index" in
*.gz) gzip -cd "$package_index" ;;
*) cat "$package_index" ;;
esac
}

verify_packages_index() {
local package_index="$1"
local index_dir filename sha256 package_path actual

index_dir="$(dirname "$package_index")"

while IFS=$'\t' read -r filename sha256; do
if [[ -z "$filename" || -z "$sha256" ]]; then
continue
fi

if [[ "$filename" = /* || "$filename" == *".."* ]]; then
fail "unsafe package path in ${package_index#$REPO_ROOT/}: $filename"
continue
fi

package_path="$REPO_ROOT/$filename"
if [[ ! -f "$package_path" ]]; then
fail "missing package referenced by ${package_index#$REPO_ROOT/}: $filename"
continue
fi

actual="$(sha256sum "$package_path" | awk '{print $1}')"
if [[ "$actual" == "$sha256" ]]; then
pass "checksum valid: $filename"
else
fail "checksum mismatch: $filename expected $sha256 got $actual"
fi
done < <(packages_stream "$package_index" | awk '
/^Filename: / { filename=$2 }
/^SHA256: / { sha256=$2 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using awk '{print $2}' is fragile as it only captures the first word after the field name. While rare in APT repositories, if a filename or path contains spaces, this will truncate the value. A more robust approach is to use sub() to remove the field prefix and capture the remainder of the line.

Suggested change
/^Filename: / { filename=$2 }
/^SHA256: / { sha256=$2 }
/^Filename: / { sub(/^Filename: [[:space:]]*/, ""); filename=$0 }
/^SHA256: / { sub(/^SHA256: [[:space:]]*/, ""); sha256=$0 }

/^$/ {
if (filename != "" && sha256 != "") {
print filename "\t" sha256
}
filename=""
sha256=""
}
END {
if (filename != "" && sha256 != "") {
print filename "\t" sha256
}
}
')

pass "parsed package index: ${package_index#$REPO_ROOT/}"

if [[ ! -d "$POOL_DIR" ]]; then
warn "pool directory missing while package index exists: ${index_dir#$REPO_ROOT/}"
fi
}

while IFS= read -r release_file; do
verify_signature "$release_file"
done < <(find "$DISTS_DIR" -type f -name Release | sort)

package_indexes_found=0
while IFS= read -r package_index; do
package_indexes_found=$((package_indexes_found + 1))
verify_packages_index "$package_index"
done < <(find "$DISTS_DIR" -type f \( -name Packages -o -name Packages.gz \) | sort)

if [[ "$package_indexes_found" -eq 0 ]]; then
warn "no package indexes found under ${DISTS_DIR#$REPO_ROOT/}"
fi

printf '\nIntegrity report: %d passed, %d warnings, %d failed\n' "$PASS" "$WARN" "$FAIL"

if [[ "$FAIL" -gt 0 ]]; then
exit 1
fi
85 changes: 85 additions & 0 deletions tests/verify-integrity-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash
# Tests for apt/scripts/verify-integrity.sh.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$REPO_ROOT/apt/scripts/verify-integrity.sh"
TMP_DIR="$(mktemp -d)"

cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT

assert_success() {
local name="$1"
shift

if "$@" >"$TMP_DIR/${name}.out" 2>"$TMP_DIR/${name}.err"; then
printf 'PASS %s\n' "$name"
else
printf 'FAIL %s\n' "$name" >&2
cat "$TMP_DIR/${name}.out" >&2 || true
cat "$TMP_DIR/${name}.err" >&2 || true
exit 1
fi
}

assert_failure() {
local name="$1"
shift

if "$@" >"$TMP_DIR/${name}.out" 2>"$TMP_DIR/${name}.err"; then
printf 'FAIL %s unexpectedly succeeded\n' "$name" >&2
cat "$TMP_DIR/${name}.out" >&2 || true
exit 1
else
printf 'PASS %s\n' "$name"
fi
}

build_fixture() {
local fixture="$1"

mkdir -p "$fixture/dists/stable/main/binary-amd64"
mkdir -p "$fixture/pool/main/c/cx"

printf 'package payload\n' > "$fixture/pool/main/c/cx/cx-test_1.0.0_all.deb"
local checksum
checksum="$(sha256sum "$fixture/pool/main/c/cx/cx-test_1.0.0_all.deb" | awk '{print $1}')"

cat > "$fixture/dists/stable/main/binary-amd64/Packages" <<EOF
Package: cx-test
Version: 1.0.0
Architecture: all
Filename: pool/main/c/cx/cx-test_1.0.0_all.deb
SHA256: $checksum

EOF

cat > "$fixture/dists/stable/Release" <<'EOF'
Origin: repo.cxlinux.com
Label: CX Linux
Suite: stable
Codename: stable
Architectures: amd64
Components: main
EOF
}

GOOD_REPO="$TMP_DIR/good"
BAD_REPO="$TMP_DIR/bad"
MISSING_REPO="$TMP_DIR/missing"

build_fixture "$GOOD_REPO"
cp -R "$GOOD_REPO" "$BAD_REPO"
cp -R "$GOOD_REPO" "$MISSING_REPO"

printf 'tampered payload\n' > "$BAD_REPO/pool/main/c/cx/cx-test_1.0.0_all.deb"
rm "$MISSING_REPO/pool/main/c/cx/cx-test_1.0.0_all.deb"

assert_success "valid-checksum" "$SCRIPT" "$GOOD_REPO"
assert_failure "tampered-package" "$SCRIPT" "$BAD_REPO"
assert_failure "missing-package" "$SCRIPT" "$MISSING_REPO"