#!/bin/sh # PQ Crypta Discovery Agent — offline verification # # Turns "it makes no outbound connections" from a claim you have to take on faith # into a property you can observe on your own host, with your own tools, before # you let the binary near anything that matters. # # sh verify-offline.sh ./pqcrypta-discovery # # The strongest check is the last one: the agent is run inside a network # namespace that has no interfaces at all except loopback. Nothing in that # namespace can reach anything. If a complete assessment still comes out, the # assessment demonstrably did not need the network — not because we say so, but # because there was none to use. # # Every check prints PASS, FAIL or SKIP. SKIP means this host could not run that # check (no unshare, not root, no strace); it never means the check passed. Read # the SKIPs — a run that is all SKIP and PASS has proven less than it looks. # # Exit status is 0 only if no check FAILed. # # POSIX sh on purpose: this has to run on whatever is inside the enclave. set -u AGENT="${1:-./pqcrypta-discovery}" WORK="$(mktemp -d "${TMPDIR:-/tmp}/pqcrypta-verify.XXXXXX")" FAILED=0 SKIPPED=0 pass() { printf 'PASS %s\n' "$1"; } fail() { printf 'FAIL %s\n' "$1"; FAILED=$((FAILED + 1)); } skip() { printf 'SKIP %s\n' "$1"; SKIPPED=$((SKIPPED + 1)); } info() { printf ' %s\n' "$1"; } head_() { printf '\n%s\n' "$1"; } # `grep -c` prints 0 AND exits 1 when nothing matches, so the usual # `$(grep -c ... || echo 0)` yields the two-line string "0\n0" and every later # arithmetic test blows up on it. Count through this instead. count_matches() { _c=$(grep -cE "$1" "$2" 2>/dev/null | head -1) [ -n "${_c:-}" ] || _c=0 printf '%s' "$_c" } cleanup() { rm -rf "$WORK"; } trap cleanup EXIT INT TERM printf 'PQ Crypta Discovery Agent — offline verification\n' printf 'Host: %s Date: %s\n' "$(uname -sr 2>/dev/null || echo unknown)" "$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo unknown)" # --------------------------------------------------------------------------- head_ '1. The binary' # --------------------------------------------------------------------------- if [ ! -x "$AGENT" ]; then fail "Agent not found or not executable: $AGENT" printf '\nUsage: sh verify-offline.sh /path/to/pqcrypta-discovery\n' exit 1 fi pass "Agent is executable: $AGENT" info "$("$AGENT" --version 2>/dev/null || echo 'version unavailable')" # A statically linked binary cannot be made to load a shared library that talks # to the network, which is one less thing to reason about inside an enclave. if command -v ldd >/dev/null 2>&1; then if ldd "$AGENT" 2>&1 | grep -q 'not a dynamic executable\|statically linked'; then pass 'Statically linked — no shared libraries loaded at runtime' else info 'Dynamically linked; libraries it loads:' ldd "$AGENT" 2>/dev/null | sed 's/^/ /' | head -20 fi else skip 'ldd unavailable — cannot inspect linkage' fi # --------------------------------------------------------------------------- head_ '2. No API endpoint is configured' # --------------------------------------------------------------------------- # Offline mode accepts neither, but an operator can still have them in the # environment from another run, and it is worth saying so out loud. if [ -n "${PQCRYPTA_API_URL:-}" ]; then fail "PQCRYPTA_API_URL is set in this environment: $PQCRYPTA_API_URL" else pass 'PQCRYPTA_API_URL is not set' fi if [ -n "${PQCRYPTA_API_KEY:-}" ]; then fail 'PQCRYPTA_API_KEY is set in this environment' else pass 'PQCRYPTA_API_KEY is not set' fi # --------------------------------------------------------------------------- head_ '3. What this host can currently reach' # --------------------------------------------------------------------------- # Context, not a verdict: a connected host is a perfectly normal place to run # offline mode. These lines are here so the report says what the conditions were. if command -v ip >/dev/null 2>&1; then IFACES=$(ip -o link show 2>/dev/null | grep -vc ' lo:' || echo '?') ROUTE=$(ip route show default 2>/dev/null | head -1) info "Non-loopback interfaces: ${IFACES}" info "Default route: ${ROUTE:-none}" elif command -v ifconfig >/dev/null 2>&1; then info "Interfaces: $(ifconfig -a 2>/dev/null | grep -c '^[a-z]')" else skip 'Neither ip nor ifconfig available — cannot describe interfaces' fi # --------------------------------------------------------------------------- head_ '4. A scan with no network namespace at all' # --------------------------------------------------------------------------- # The real proof. unshare -n gives the process a fresh network namespace whose # only interface is a down loopback: no addresses, no routes, no DNS, nothing to # connect to even if it wanted to. OUT="$WORK/assessment" run_scan() { # A tiny, fast target so the check is quick. Any readable directory works. "$@" "$AGENT" --offline-report "$OUT" --targets /etc/ssl/certs >"$WORK/scan.log" 2>&1 } NAMESPACED=0 if command -v unshare >/dev/null 2>&1; then if unshare -rn true 2>/dev/null; then # -r maps the current user to root inside the namespace, so this works # unprivileged on kernels that allow user namespaces. if run_scan unshare -rn; then NAMESPACED=1 pass 'Assessment completed inside a network namespace with no interfaces' else fail 'Agent failed inside an isolated network namespace' sed 's/^/ /' "$WORK/scan.log" | tail -15 fi elif [ "$(id -u)" = "0" ] && run_scan unshare -n; then NAMESPACED=1 pass 'Assessment completed inside a network namespace with no interfaces' else skip 'unshare present but this kernel/user cannot create a network namespace' fi else skip 'unshare unavailable — falling back to an ordinary run' fi if [ "$NAMESPACED" = "0" ] && [ ! -d "$OUT" ]; then if run_scan; then info 'Ran normally (not isolated) — this proves less. Prefer a host with unshare.' else fail 'Agent did not complete a scan' sed 's/^/ /' "$WORK/scan.log" | tail -15 fi fi # --------------------------------------------------------------------------- head_ '5. Sockets it actually opened' # --------------------------------------------------------------------------- # Direct observation rather than inference. If strace is present we count the # syscalls that could reach a network; a connect() to anything other than a # unix socket would show up here. if command -v strace >/dev/null 2>&1; then rm -rf "$WORK/traced" strace -f -qq -e trace=connect,sendto,sendmsg \ -o "$WORK/strace.log" \ "$AGENT" --offline-report "$WORK/traced" --targets /etc/ssl/certs \ >/dev/null 2>&1 NETCALLS=$(count_matches 'connect\(|sendto\(|sendmsg\(' "$WORK/strace.log") grep 'connect(' "$WORK/strace.log" 2>/dev/null > "$WORK/connects.log" || true INETCALLS=$(count_matches 'AF_INET' "$WORK/connects.log") info "connect/sendto/sendmsg calls (incl. local unix sockets): ${NETCALLS}" if [ "$INETCALLS" -eq 0 ]; then pass 'Zero AF_INET / AF_INET6 connections attempted' else fail "Agent attempted ${INETCALLS} IP connection(s) — see $WORK/strace.log" grep 'connect(' "$WORK/strace.log" | grep 'AF_INET' | sed 's/^/ /' | head -10 fi else skip 'strace unavailable — cannot count syscalls directly' fi # --------------------------------------------------------------------------- head_ '6. What came out' # --------------------------------------------------------------------------- for f in report.html cbom.json inventory.json; do if [ -s "$OUT/$f" ]; then pass "$f produced ($(wc -c <"$OUT/$f" | tr -d ' ') bytes)" else fail "$f missing or empty" fi done # The report has to be openable on a machine with no network, which means no # external references of any kind. if [ -s "$OUT/report.html" ]; then EXT=$(count_matches 'src="https?://|href="https?://|@import|url\(https?://' "$OUT/report.html") if [ "$EXT" -eq 0 ]; then pass 'report.html references nothing external — it opens with no network' else fail "report.html contains ${EXT} external reference(s)" fi fi if [ -s "$OUT/cbom.json" ]; then if grep -q '"bomFormat"[[:space:]]*:[[:space:]]*"CycloneDX"' "$OUT/cbom.json" && grep -q '"specVersion"[[:space:]]*:[[:space:]]*"1.6"' "$OUT/cbom.json"; then pass 'cbom.json declares CycloneDX 1.6' else fail 'cbom.json is not a CycloneDX 1.6 document' fi fi # --------------------------------------------------------------------------- head_ '7. The bundle describes itself' # --------------------------------------------------------------------------- # The evidence files (SHA256SUMS, manifest.json) arrived after the first offline # releases. An older agent that does not write them is not broken — it simply # predates them — so their absence is a SKIP with the reason stated, not a FAIL. # Reporting FAIL here would tell an operator their good bundle was bad. if [ ! -s "$OUT/SHA256SUMS" ] && [ ! -s "$OUT/manifest.json" ]; then skip 'No SHA256SUMS/manifest.json — this agent predates the evidence bundle' info 'Upgrade to get digests, provenance and optional signatures with every assessment.' elif [ -s "$OUT/SHA256SUMS" ]; then if command -v sha256sum >/dev/null 2>&1; then if (cd "$OUT" && sha256sum -c SHA256SUMS >/dev/null 2>&1); then pass 'SHA256SUMS verifies against every file it lists' else fail 'SHA256SUMS does not match the bundle' fi elif command -v shasum >/dev/null 2>&1; then if (cd "$OUT" && shasum -a 256 -c SHA256SUMS >/dev/null 2>&1); then pass 'SHA256SUMS verifies against every file it lists' else fail 'SHA256SUMS does not match the bundle' fi else skip 'No sha256sum/shasum — cannot verify digests' fi if grep -q 'manifest.json' "$OUT/SHA256SUMS"; then pass 'SHA256SUMS covers manifest.json, so provenance is sealed too' else fail 'SHA256SUMS omits manifest.json — the provenance record is unprotected' fi else fail 'manifest.json was written but SHA256SUMS was not — the bundle is incomplete' fi if [ -s "$OUT/manifest.json" ]; then if grep -q '"network_access"[[:space:]]*:[[:space:]]*false' "$OUT/manifest.json"; then pass 'manifest.json records the run as having made no network access' else fail 'manifest.json does not record network_access: false' fi fi # --------------------------------------------------------------------------- head_ 'Result' # --------------------------------------------------------------------------- if [ "$FAILED" -eq 0 ]; then printf 'PASS — %s\n' "no check failed${SKIPPED:+ (${SKIPPED} skipped)}" [ "$SKIPPED" -gt 0 ] && printf ' Skipped checks proved nothing. Re-run on a host with unshare and strace for the full result.\n' exit 0 else printf 'FAIL — %s check(s) failed\n' "$FAILED" exit 1 fi