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
4 changes: 3 additions & 1 deletion .github/workflows/installer-hash-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
# SPDX-License-Identifier: Apache-2.0
#
# Verifies pinned installer SHA-256 hashes still match upstream scripts.
# Checked: OpenShell v0.0.72 installer and Brev release assets.
# Checked: allowlisted OpenShell installer and Brev release assets.
# Reports the required network-backed drift check on every PR, every push to
# main, and weekly. Pull requests execute checker code from their base commit;
# the immutable bootstrap is used only for the PR that first adds that action.
# A new release-manifest allowlist entry must therefore land on main in a
# prerequisite PR before a later PR changes runtime selectors to that release.

name: Security / Installer Hash Check

Expand Down
15 changes: 14 additions & 1 deletion scripts/brev-launchable-ci-cpu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# bash scripts/brev-launchable-ci-cpu.sh --print-openshell-version # resolve only
#
# Environment overrides:
# OPENSHELL_VERSION — OpenShell CLI release tag (default: v0.0.72)
# OPENSHELL_VERSION — OpenShell CLI release tag (default: stable selector below)
# NEMOCLAW_OPENSHELL_CHANNEL — Release channel (stable/dev/auto)
# NEMOCLAW_ACCEPT_DEV_UNVERIFIED_INSTALL — Required opt-in for the unverified dev channel
# NEMOCLAW_REF — NemoClaw git ref to clone (default: main)
Expand Down Expand Up @@ -165,6 +165,18 @@ openshell_checksum_line() {
awk -v asset="$asset" '$2 == asset { print; found=1; exit } END { if (!found) exit 1 }' "$checksum_file"
}

validate_openshell_archive() {
local archive="$1" expected_member="$2" members verbose
members="$(LC_ALL=C tar -tzf "$archive")" \
|| fail "Unable to list OpenShell archive $(basename "$archive")"
[ "$members" = "$expected_member" ] \
|| fail "Unsafe OpenShell archive $(basename "$archive"): expected exactly one member named $expected_member"
verbose="$(LC_ALL=C tar -tvzf "$archive")" \
|| fail "Unable to inspect OpenShell archive $(basename "$archive")"
[[ "$verbose" != *$'\n'* && "${verbose:0:1}" = "-" && "${verbose##* }" = "$expected_member" ]] \
|| fail "Unsafe OpenShell archive $(basename "$archive"): $expected_member must be one regular file"
}

verify_openshell_cli_asset() {
local tmpdir="$1" asset="$2" checksum_file="openshell-checksums-sha256.txt"
local checksum_line expected_sha release_sha
Expand Down Expand Up @@ -201,6 +213,7 @@ install_openshell_cli_release() {
if [[ "$OPENSHELL_VERSION" != "dev" ]]; then
verify_openshell_cli_asset "$tmpdir" "$asset"
fi
validate_openshell_archive "$tmpdir/$asset" openshell
tar xzf "$tmpdir/$asset" -C "$tmpdir"
sudo install -m 755 "$tmpdir/openshell" /usr/local/bin/openshell
rm -rf "$tmpdir"
Expand Down
193 changes: 130 additions & 63 deletions scripts/check-installer-hash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# still match the immutable upstream checksum manifests.
#
# Checked artifacts:
# 1. OpenShell v0.0.72 — scripts/install-openshell.sh release-asset table
# 2. Brev OpenShell CLI — scripts/brev-launchable-ci-cpu.sh release-asset table
# 1. OpenShell archives — scripts/install-openshell.sh release-asset table
# 2. Brev OpenShell CLI — scripts/brev-launchable-ci-cpu.sh release-asset table
#
# Usage:
# scripts/check-installer-hash.sh # exit 0 if current, 1 if stale
Expand All @@ -23,7 +23,17 @@ else
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
fi
CHECKER_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OPENSHELL_RELEASE_VERSION="0.0.72"

# Trust-anchor rollout is intentionally two-step. First land a prerequisite PR
# that adds the reviewed release-manifest digests here while runtime selectors
# still name the current release. Only after that commit is on the target branch
# may a separate pin PR select the new release. Pull-request verification runs
# this file from the base SHA, so a pin PR can never authorize its own digests.
readonly -a OPENSHELL_RELEASE_MANIFEST_ALLOWLIST=(
"0.0.72|openshell-checksums-sha256.txt|0049181983eaf925ef9510382f75348229a9511d02e27196107782e7c3259ae1"
"0.0.72|openshell-gateway-checksums-sha256.txt|3c454dc15154b8c700ec820628559ea8964c6e552d9c5f8af78b6ee19cf34547"
"0.0.72|openshell-sandbox-checksums-sha256.txt|d38507501338576437cf3e554df71fefe927dc0d72758f88e260069527ed9ccc"
)

case "${1:-}" in
"") ;;
Expand Down Expand Up @@ -57,7 +67,7 @@ sha256_file() {
}

# invalidState: CI reports trusted OpenShell pins without comparing every
# consumed archive with the immutable v0.0.72 checksum release assets.
# consumed archive with the selected immutable checksum release assets.
# sourceBoundary: NVIDIA/OpenShell owns the release assets and their published
# digests; NemoClaw owns this independent verification of its local pin table.
# In pull-request CI, this checker and its pin parser execute only from the
Expand All @@ -72,47 +82,20 @@ sha256_file() {
check_openshell_release_assets() {
local installer="${REPO_ROOT}/scripts/install-openshell.sh"
local brev_installer="${REPO_ROOT}/scripts/brev-launchable-ci-cpu.sh"
local release_base="https://github.com/NVIDIA/OpenShell/releases/download/v${OPENSHELL_RELEASE_VERSION}"
local workspace manifests spec manifest expected actual source asset pinned upstream matches
local pin_records parser_error parser_errors
local release_base workspace manifests spec manifest expected actual source asset pinned upstream
local matches required_manifest required_matches
local pin_records parser_error parser_errors parsed_version release_version="" record_extra
local allowlist_entry allowlist_version allowlist_extra
local count=0 brev_count=0 published_count=0 failures=0
local -a manifest_specs=(
"openshell-checksums-sha256.txt:0049181983eaf925ef9510382f75348229a9511d02e27196107782e7c3259ae1"
"openshell-gateway-checksums-sha256.txt:3c454dc15154b8c700ec820628559ea8964c6e552d9c5f8af78b6ee19cf34547"
"openshell-sandbox-checksums-sha256.txt:d38507501338576437cf3e554df71fefe927dc0d72758f88e260069527ed9ccc"
)
local -a manifest_specs=()
workspace=$(mktemp -d)
manifests="${workspace}/published-sha256.txt"
: >"$manifests"
trap 'rm -rf "$workspace"' RETURN

echo "Checking OpenShell v${OPENSHELL_RELEASE_VERSION} release assets..."
for spec in "${manifest_specs[@]}"; do
manifest="${spec%%:*}"
expected="${spec#*:}"
if ! fetch_file "${release_base}/${manifest}" "${workspace}/${manifest}"; then
echo " STALE: unable to download ${manifest}."
failures=$((failures + 1))
continue
fi
if ! actual=$(sha256_file "${workspace}/${manifest}"); then
echo " STALE: unable to hash ${manifest}."
failures=$((failures + 1))
continue
fi
if [[ "$actual" != "$expected" ]]; then
echo " STALE: ${manifest} digest does not match the pinned v${OPENSHELL_RELEASE_VERSION} release asset."
echo " pinned: ${expected}"
echo " upstream: ${actual}"
failures=$((failures + 1))
continue
fi
echo " OK: ${manifest} (${actual})"
cat "${workspace}/${manifest}" >>"$manifests"
done

# invalidState: target-controlled shell formatting hides, duplicates, or
# changes a pin while the trusted release-asset check still reports success.
# mixes a release version while the trusted release-asset check still reports
# success.
# sourceBoundary: this parser executes beside the checker only from the
# base-trusted checkout or immutable bootstrap, never from the PR head. It
# defines the accepted static shell subset; PR-head installers are input data
Expand All @@ -126,47 +109,131 @@ check_openshell_release_assets() {
parser_errors="${workspace}/pin-parser-errors.txt"
if ! pin_records=$(node --experimental-strip-types \
"${CHECKER_ROOT}/checks/extract-installer-pins.mts" \
--release-version "$OPENSHELL_RELEASE_VERSION" \
--blueprint "${REPO_ROOT}/nemoclaw-blueprint/blueprint.yaml" \
--installer "$installer" \
--brev-installer "$brev_installer" \
--format tsv 2>"$parser_errors"); then
echo " STALE: unable to extract the OpenShell installer pin tables with trusted parser code."
while IFS= read -r parser_error; do
echo " ${parser_error}"
done <"$parser_errors"
failures=$((failures + 1))
else
while IFS=$'\t' read -r source asset pinned; do
if [[ "$source" == "installer" ]]; then
count=$((count + 1))
else
brev_count=$((brev_count + 1))
fi
matches=$(awk -v asset="$asset" '$2 == asset { count++ } END { print count + 0 }' "$manifests")
upstream=$(awk -v asset="$asset" '$2 == asset { print $1; exit }' "$manifests")
if [[ "$matches" -eq 1 && "$pinned" == "$upstream" ]]; then
published_count=$((published_count + 1))
echo " OK: ${source} ${asset} (${pinned})"
else
echo " STALE: ${source} ${asset} does not match exactly one v${OPENSHELL_RELEASE_VERSION} checksum entry."
echo " pinned: ${pinned}"
echo " upstream: ${upstream:-missing}"
echo " matches: ${matches}"
failures=$((failures + 1))
fi
done <<<"$pin_records"
return 1
fi

while IFS=$'\t' read -r parsed_version source asset pinned record_extra; do
if [[ ! "$parsed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ || -z "$source" || -z "$asset" || -z "$pinned" || -n "$record_extra" ]]; then
echo " STALE: trusted parser returned an invalid installer pin record."
return 1
fi
if [[ -z "$release_version" ]]; then
release_version="$parsed_version"
elif [[ "$parsed_version" != "$release_version" ]]; then
echo " STALE: trusted parser returned multiple OpenShell release versions."
return 1
fi
case "$source" in
installer) count=$((count + 1)) ;;
"Brev launchable") brev_count=$((brev_count + 1)) ;;
*)
echo " STALE: trusted parser returned an unknown pin source."
return 1
;;
esac
done <<<"$pin_records"

if [[ "$count" -ne 8 ]]; then
echo " STALE: expected 8 pinned OpenShell v${OPENSHELL_RELEASE_VERSION} assets, found ${count}."
echo " STALE: expected 8 pinned OpenShell v${release_version:-unknown} assets, found ${count}."
failures=$((failures + 1))
fi
if [[ "$brev_count" -ne 2 ]]; then
echo " STALE: expected 2 pinned Brev OpenShell v${OPENSHELL_RELEASE_VERSION} CLI assets, found ${brev_count}."
echo " STALE: expected 2 pinned Brev OpenShell v${release_version:-unknown} CLI assets, found ${brev_count}."
failures=$((failures + 1))
fi
if [[ "$failures" -ne 0 ]]; then
return "$failures"
fi

for allowlist_entry in "${OPENSHELL_RELEASE_MANIFEST_ALLOWLIST[@]}"; do
IFS='|' read -r allowlist_version manifest expected allowlist_extra <<<"$allowlist_entry"
if [[ ! "$allowlist_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ || ! "$expected" =~ ^[a-f0-9]{64}$ || -z "$manifest" || -n "$allowlist_extra" ]]; then
echo " STALE: trusted OpenShell release-manifest allowlist is invalid."
return 1
fi
if [[ "$allowlist_version" == "$release_version" ]]; then
manifest_specs+=("${manifest}:${expected}")
fi
done

if [[ "${#manifest_specs[@]}" -eq 0 ]]; then
echo " STALE: OpenShell v${release_version} is not in the trusted release-manifest allowlist."
return 1
fi
if [[ "${#manifest_specs[@]}" -ne 3 ]]; then
echo " STALE: OpenShell v${release_version} does not have exactly three trusted release-manifest digests."
return 1
fi
for required_manifest in \
openshell-checksums-sha256.txt \
openshell-gateway-checksums-sha256.txt \
openshell-sandbox-checksums-sha256.txt; do
required_matches=0
for spec in "${manifest_specs[@]}"; do
if [[ "${spec%%:*}" == "$required_manifest" ]]; then
required_matches=$((required_matches + 1))
fi
done
if [[ "$required_matches" -ne 1 ]]; then
echo " STALE: OpenShell v${release_version} does not have exactly one trusted ${required_manifest} digest."
failures=$((failures + 1))
fi
done
if [[ "$failures" -ne 0 ]]; then
return "$failures"
fi

release_base="https://github.com/NVIDIA/OpenShell/releases/download/v${release_version}"
echo "Checking OpenShell v${release_version} release assets..."
for spec in "${manifest_specs[@]}"; do
manifest="${spec%%:*}"
expected="${spec#*:}"
if ! fetch_file "${release_base}/${manifest}" "${workspace}/${manifest}"; then
echo " STALE: unable to download ${manifest}."
failures=$((failures + 1))
continue
fi
if ! actual=$(sha256_file "${workspace}/${manifest}"); then
echo " STALE: unable to hash ${manifest}."
failures=$((failures + 1))
continue
fi
if [[ "$actual" != "$expected" ]]; then
echo " STALE: ${manifest} digest does not match the pinned v${release_version} release asset."
echo " pinned: ${expected}"
echo " upstream: ${actual}"
failures=$((failures + 1))
continue
fi
echo " OK: ${manifest} (${actual})"
cat "${workspace}/${manifest}" >>"$manifests"
done

while IFS=$'\t' read -r parsed_version source asset pinned record_extra; do
matches=$(awk -v asset="$asset" '$2 == asset { count++ } END { print count + 0 }' "$manifests")
upstream=$(awk -v asset="$asset" '$2 == asset { print $1; exit }' "$manifests")
if [[ "$matches" -eq 1 && "$pinned" == "$upstream" ]]; then
published_count=$((published_count + 1))
echo " OK: ${source} ${asset} (${pinned})"
else
echo " STALE: ${source} ${asset} does not match exactly one v${release_version} checksum entry."
echo " pinned: ${pinned}"
echo " upstream: ${upstream:-missing}"
echo " matches: ${matches}"
failures=$((failures + 1))
fi
done <<<"$pin_records"

if [[ "$published_count" -ne 10 ]]; then
echo " STALE: expected all 10 pinned asset references in the v${OPENSHELL_RELEASE_VERSION} checksum manifests, matched ${published_count}."
echo " STALE: expected all 10 pinned asset references in the v${release_version} checksum manifests, matched ${published_count}."
failures=$((failures + 1))
fi
return "$failures"
Expand Down
40 changes: 29 additions & 11 deletions scripts/checks/dependency-pins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type DependencyPins = Readonly<{
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const OPENCLAW_VERSION_ARG_SUFFIX_RE = /[.-]/g;
const NUMERIC_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
const OPENSHELL_RELEASE_MANIFESTS = [
"openshell-checksums-sha256.txt",
"openshell-gateway-checksums-sha256.txt",
"openshell-sandbox-checksums-sha256.txt",
] as const;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
Expand Down Expand Up @@ -263,6 +268,29 @@ function requireVersionReference(
}
}

function requireOpenShellReleaseManifestAllowlist(
source: string,
expectedVersion: string,
failures: string[],
): void {
const entries = [
...source.matchAll(/^\s*"([0-9]+\.[0-9]+\.[0-9]+)\|([^|"\s]+)\|([a-f0-9]{64})"\s*$/gm),
]
.filter((match) => match[1] === expectedVersion)
.map((match) => match[2])
.filter((manifest): manifest is string => manifest !== undefined);
const complete =
entries.length === OPENSHELL_RELEASE_MANIFESTS.length &&
OPENSHELL_RELEASE_MANIFESTS.every(
(manifest) => entries.filter((entry) => entry === manifest).length === 1,
);
if (!complete) {
failures.push(
`OpenShell release-manifest allowlist: expected one complete entry for ${expectedVersion}`,
);
}
}

function verifyOpenShellPins(
pins: OpenShellPins,
sources: {
Expand Down Expand Up @@ -309,17 +337,7 @@ function verifyOpenShellPins(
"OpenShell installer PIN_VERSION",
failures,
);
compare(
extractSingle(
sources.installerHashCheck,
/^OPENSHELL_RELEASE_VERSION="([^"]+)"\s*$/gm,
"OpenShell installer hash release",
failures,
),
pins.maxVersion,
"OpenShell installer hash release",
failures,
);
requireOpenShellReleaseManifestAllowlist(sources.installerHashCheck, pins.maxVersion, failures);
compare(
extractSingle(
sources.openshellVersion,
Expand Down
Loading
Loading