mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
wait_for_peers_exact fell back to 0 when it could not read a container's peer count. Two of its six call sites are the ACL denial checks, which expect exactly 0 connected peers — so a failed docker exec satisfied them on the first iteration and the isolation property the suite exists to prove was never observed. Confirmed against a container that does not exist: the old reader returns "0" and the expect-zero comparison passes. The reader now returns the empty string when the daemon does not answer, which no numeric comparison can satisfy, and the timeout message says which of the two happened — never answered, or answered the wrong number. Same shape as admission-cap-test.sh's read_peer_count.
184 lines
7.6 KiB
Bash
Executable File
184 lines
7.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Integration test for the ACL allowlist harness.
|
|
#
|
|
# Usage: ./test.sh [--skip-build] [--keep-up]
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
TESTING_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
|
|
GENERATE_CONFIGS="$SCRIPT_DIR/generate-configs.sh"
|
|
|
|
SKIP_BUILD=false
|
|
KEEP_UP=false
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--skip-build) SKIP_BUILD=true; shift ;;
|
|
--keep-up) KEEP_UP=true; shift ;;
|
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
cleanup() {
|
|
if [ "$KEEP_UP" = false ]; then
|
|
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
log() {
|
|
echo "=== $*"
|
|
}
|
|
|
|
peer_npubs() {
|
|
local container="$1"
|
|
docker exec "$container" fipsctl show peers \
|
|
| python3 -c 'import json,sys; data=json.load(sys.stdin); print(" ".join(sorted(p["npub"] for p in data.get("peers", []) if p.get("connectivity") == "connected")))'
|
|
}
|
|
|
|
acl_field() {
|
|
local container="$1"
|
|
local field="$2"
|
|
docker exec "$container" fipsctl acl show \
|
|
| python3 -c 'import json,sys; data=json.load(sys.stdin); field=sys.argv[1]; value=data.get(field); print(" ".join(sorted(value)) if isinstance(value, list) else ("" if value is None else value))' "$field"
|
|
}
|
|
|
|
assert_peer_set() {
|
|
local container="$1"
|
|
local expected="$2"
|
|
local actual
|
|
actual="$(peer_npubs "$container")"
|
|
if [ "$actual" != "$expected" ]; then
|
|
echo "FAIL: $container peers mismatch" >&2
|
|
echo " expected: $expected" >&2
|
|
echo " actual: $actual" >&2
|
|
exit 1
|
|
fi
|
|
echo "PASS: $container peers match expected set"
|
|
}
|
|
|
|
assert_acl_field() {
|
|
local container="$1"
|
|
local field="$2"
|
|
local expected="$3"
|
|
local actual
|
|
actual="$(acl_field "$container" "$field")"
|
|
if [ "$actual" != "$expected" ]; then
|
|
echo "FAIL: $container ACL field $field mismatch" >&2
|
|
echo " expected: $expected" >&2
|
|
echo " actual: $actual" >&2
|
|
exit 1
|
|
fi
|
|
echo "PASS: $container ACL field $field matches expected value"
|
|
}
|
|
|
|
# Connected-peer count for a container, or the empty string if it did not
|
|
# answer.
|
|
#
|
|
# Empty is deliberately distinct from a real 0, and here the distinction is
|
|
# the whole point: the ACL denial checks below expect exactly 0, so an
|
|
# `|| echo 0` fallback lets an unreachable container satisfy them on the first
|
|
# iteration and the security property is never observed. Same shape as
|
|
# admission-cap-test.sh's read_peer_count.
|
|
read_connected_peers() {
|
|
local container="$1"
|
|
docker exec "$container" fipsctl show peers 2>/dev/null \
|
|
| python3 -c 'import json,sys; data=json.load(sys.stdin); print(sum(1 for p in data.get("peers", []) if p.get("connectivity") == "connected"))' 2>/dev/null \
|
|
|| true
|
|
}
|
|
|
|
wait_for_peers_exact() {
|
|
local container="$1"
|
|
local expected_count="$2"
|
|
local timeout="${3:-30}"
|
|
|
|
local count="" answered=false
|
|
for _ in $(seq 1 "$timeout"); do
|
|
count=$(read_connected_peers "$container")
|
|
if [ -n "$count" ]; then
|
|
answered=true
|
|
if [ "$count" -eq "$expected_count" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [ "$answered" = false ]; then
|
|
echo "FAIL: $container never answered a peer query in ${timeout}s, so a count of $expected_count was never actually observed" >&2
|
|
else
|
|
echo "FAIL: $container did not reach $expected_count connected peers in ${timeout}s (last answer: $count)" >&2
|
|
fi
|
|
docker exec "$container" fipsctl show peers >&2 || true
|
|
exit 1
|
|
}
|
|
|
|
assert_log_contains() {
|
|
local container="$1"
|
|
local pattern="$2"
|
|
local timeout="${3:-15}"
|
|
local logs
|
|
|
|
# Poll docker logs instead of one-shot reading: under XX handshake,
|
|
# the cross-connection tie-breaker determines which side reaches
|
|
# its ACL-check point first, so the inbound-handshake-context
|
|
# rejection may not emit until a later retry. Same wait-with-timeout
|
|
# shape as wait_for_peers_exact above.
|
|
for _ in $(seq 1 "$timeout"); do
|
|
logs="$(docker logs "$container" 2>&1 | python3 -c 'import re,sys; print(re.sub(r"\x1b\[[0-9;]*m", "", sys.stdin.read()), end="")' || true)"
|
|
if printf '%s' "$logs" | grep -F "$pattern" >/dev/null; then
|
|
echo "PASS: $container logs contain expected ACL rejection"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
echo "FAIL: missing log pattern in $container: $pattern (waited ${timeout}s)" >&2
|
|
exit 1
|
|
}
|
|
|
|
if [ "$SKIP_BUILD" = false ]; then
|
|
log "Building Linux test binaries"
|
|
"$TESTING_DIR/scripts/build.sh" --no-docker
|
|
fi
|
|
|
|
log "Generating ACL allowlist fixtures"
|
|
"$GENERATE_CONFIGS"
|
|
|
|
log "Starting ACL allowlist harness"
|
|
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
|
|
|
log "Waiting for expected peer convergence"
|
|
wait_for_peers_exact fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} 3 40
|
|
wait_for_peers_exact fips-acl-container-b${FIPS_CI_NAME_SUFFIX:-} 1 40
|
|
wait_for_peers_exact fips-acl-container-c${FIPS_CI_NAME_SUFFIX:-} 0 5
|
|
wait_for_peers_exact fips-acl-container-d${FIPS_CI_NAME_SUFFIX:-} 0 5
|
|
wait_for_peers_exact fips-acl-container-e${FIPS_CI_NAME_SUFFIX:-} 1 40
|
|
wait_for_peers_exact fips-acl-container-f${FIPS_CI_NAME_SUFFIX:-} 1 40
|
|
|
|
log "Verifying peer sets"
|
|
assert_peer_set fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le npub1x5z9rwzzm26q9verutx4aajhf2zw2pyp34c6whhde2zduxqav40qgq36l6 npub1ytrut7gjncn2zfnhn56c0zgftf0w6p99gf6fu8j73hzw5603zglqc9av6c"
|
|
assert_peer_set fips-acl-container-b${FIPS_CI_NAME_SUFFIX:-} "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
|
assert_peer_set fips-acl-container-c${FIPS_CI_NAME_SUFFIX:-} ""
|
|
assert_peer_set fips-acl-container-d${FIPS_CI_NAME_SUFFIX:-} ""
|
|
assert_peer_set fips-acl-container-e${FIPS_CI_NAME_SUFFIX:-} "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
|
assert_peer_set fips-acl-container-f${FIPS_CI_NAME_SUFFIX:-} "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
|
|
|
log "Checking alias-based ACL resolution"
|
|
assert_acl_field fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} allow_file_entries "node-a node-b node-e node-f"
|
|
assert_acl_field fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} allow_entries "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le npub1x5z9rwzzm26q9verutx4aajhf2zw2pyp34c6whhde2zduxqav40qgq36l6 npub1ytrut7gjncn2zfnhn56c0zgftf0w6p99gf6fu8j73hzw5603zglqc9av6c"
|
|
assert_acl_field fips-acl-container-c${FIPS_CI_NAME_SUFFIX:-} allow_file_entries "node-a node-b node-c node-d node-e node-f"
|
|
assert_acl_field fips-acl-container-c${FIPS_CI_NAME_SUFFIX:-} allow_entries "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6 npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le npub1x5z9rwzzm26q9verutx4aajhf2zw2pyp34c6whhde2zduxqav40qgq36l6 npub1ytrut7gjncn2zfnhn56c0zgftf0w6p99gf6fu8j73hzw5603zglqc9av6c"
|
|
|
|
log "Checking ACL rejection logs"
|
|
assert_log_contains fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6"
|
|
assert_log_contains fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl"
|
|
assert_log_contains fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} "context=inbound_handshake"
|
|
assert_log_contains fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} "decision=denylist match"
|
|
|
|
log "ACL allowlist integration test passed"
|