fix(auth): harden restore verification transaction
This commit is contained in:
@@ -29,6 +29,71 @@ task13_sha256_text() {
|
||||
fi
|
||||
}
|
||||
|
||||
task13_registry_filesystem_fingerprint() {
|
||||
python3 - "$1" <<'PY'
|
||||
import hashlib
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
||||
root = os.path.abspath(sys.argv[1])
|
||||
records = []
|
||||
entries = 0
|
||||
total_bytes = 0
|
||||
|
||||
def snapshot(value):
|
||||
return (
|
||||
value.st_dev, value.st_ino, value.st_mode, value.st_uid, value.st_gid,
|
||||
value.st_size, value.st_mtime_ns, value.st_ctime_ns,
|
||||
)
|
||||
|
||||
def visit(path, relative):
|
||||
global entries, total_bytes
|
||||
before = os.lstat(path)
|
||||
entries += 1
|
||||
if entries > 65536:
|
||||
raise SystemExit("registry fingerprint entry bound exceeded")
|
||||
metadata = snapshot(before)
|
||||
if stat.S_ISLNK(before.st_mode):
|
||||
raise SystemExit("registry fingerprint rejected a symbolic link")
|
||||
if stat.S_ISDIR(before.st_mode):
|
||||
records.append(("directory", relative, metadata))
|
||||
with os.scandir(path) as listing:
|
||||
children = sorted((item.name for item in listing))
|
||||
for name in children:
|
||||
visit(os.path.join(path, name), name if relative == "." else relative + "/" + name)
|
||||
elif stat.S_ISREG(before.st_mode):
|
||||
if before.st_size > 256 * 1024 * 1024:
|
||||
raise SystemExit("registry fingerprint file bound exceeded")
|
||||
total_bytes += before.st_size
|
||||
if total_bytes > 2 * 1024 * 1024 * 1024:
|
||||
raise SystemExit("registry fingerprint total bound exceeded")
|
||||
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
||||
descriptor = os.open(path, flags)
|
||||
try:
|
||||
opened = os.fstat(descriptor)
|
||||
if snapshot(opened) != metadata:
|
||||
raise SystemExit("registry changed while fingerprinting")
|
||||
digest = hashlib.sha256()
|
||||
while True:
|
||||
chunk = os.read(descriptor, 1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
digest.update(chunk)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
records.append(("file", relative, metadata, digest.hexdigest()))
|
||||
else:
|
||||
raise SystemExit("registry fingerprint rejected a special file")
|
||||
if snapshot(os.lstat(path)) != metadata:
|
||||
raise SystemExit("registry changed while fingerprinting")
|
||||
|
||||
visit(root, ".")
|
||||
encoded = repr(records).encode("utf-8")
|
||||
print("sha256:" + hashlib.sha256(encoded).hexdigest())
|
||||
PY
|
||||
}
|
||||
|
||||
task13_sanitize() {
|
||||
local line
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
@@ -983,8 +1048,9 @@ PY
|
||||
}
|
||||
|
||||
task13_assert_server_oidc_restore_verification() {
|
||||
local archive frontend status diagnostics active_state_before restore_output restore_rc restore_cause
|
||||
local archive frontend status diagnostics restore_output restore_rc restore_cause
|
||||
local rollback_output rollback_rc rollback_sentinel provider_label checkpoint_leftover
|
||||
local registry_before registry_after integrity_output
|
||||
archive="$TASK13_TMP/server-oidc-restore-source.zip"
|
||||
frontend="$(task13_frontend_address)"
|
||||
task13_compose_logged "seed valid server authentication runtime excluded from restore" exec -T core node --input-type=module -e '
|
||||
@@ -1004,12 +1070,6 @@ task13_assert_server_oidc_restore_verification() {
|
||||
browserTransactionDigest: "d".repeat(64), browserTransactionTransport: "https",
|
||||
});
|
||||
'
|
||||
active_state_before="$(task13_compose exec -T core node -e '
|
||||
const fs = require("node:fs");
|
||||
process.stdout.write(fs.existsSync("/data/workspace-registry/state/active.json") ? "present" : "absent");
|
||||
')"
|
||||
[[ "$active_state_before" == present || "$active_state_before" == absent ]] \
|
||||
|| task13_fail "server restore fixture returned an invalid registry state diagnostic"
|
||||
task13_compose_logged "stop server stack for OIDC restore" stop
|
||||
rollback_sentinel="$TASK13_SERVER_DATA/task13-restore-rollback"
|
||||
printf 'backup-state\n' >"$rollback_sentinel"
|
||||
@@ -1059,6 +1119,22 @@ task13_assert_server_oidc_restore_verification() {
|
||||
>>"$TASK13_LOG" 2>&1 || task13_log_failure "restored scoped fake OIDC provider readiness"
|
||||
printf 'Task 13 server rollback injection passed: exit=%s; recovery checkpoint removed.\n' "$rollback_rc"
|
||||
|
||||
registry_before="$(task13_registry_filesystem_fingerprint "$TASK13_SERVER_REGISTRY")"
|
||||
integrity_output="$TASK13_TMP/server-workspace-integrity.json"
|
||||
if ! task13_compose run --rm --no-deps --no-TTY core \
|
||||
node /app/backend/dist/operator-command.js workspace-integrity \
|
||||
>"$integrity_output" 2>>"$TASK13_LOG"; then
|
||||
task13_log_failure "stopped read-only workspace registry verification"
|
||||
fi
|
||||
node -e '
|
||||
const value=JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
|
||||
const keys=Object.keys(value).sort().join(",");
|
||||
if(keys!=="fingerprint,ready,state,workspaces"||value.ready!==true||value.state!=="uninitialized"||value.workspaces!==0||!/^sha256:[0-9a-f]{64}$/.test(value.fingerprint)) process.exit(1);
|
||||
' "$integrity_output" || task13_fail "stopped registry inspector returned malformed or additional output"
|
||||
registry_after="$(task13_registry_filesystem_fingerprint "$TASK13_SERVER_REGISTRY")"
|
||||
[[ "$registry_after" == "$registry_before" ]] \
|
||||
|| task13_fail "stopped restore verification mutated the workspace registry filesystem"
|
||||
|
||||
restore_output="$TASK13_TMP/server-oidc-restore.out"
|
||||
set +e
|
||||
"$TASK13_THT" --installation "$TASK13_INSTALLATION" restore "$archive" --yes \
|
||||
@@ -1070,8 +1146,8 @@ task13_assert_server_oidc_restore_verification() {
|
||||
if grep -Fq 'restore verification workspace: validate restored workspace registry' "$restore_output"; then
|
||||
restore_cause=workspace-validator-rejected
|
||||
fi
|
||||
printf 'Task 13 stopped restore diagnostic: exit=%s active-state-before=%s cause=%s\n' \
|
||||
"$restore_rc" "$active_state_before" "$restore_cause" >&2
|
||||
printf 'Task 13 stopped restore diagnostic: exit=%s cause=%s\n' \
|
||||
"$restore_rc" "$restore_cause" >&2
|
||||
task13_sanitize <"$restore_output" | tail -n 8 >&2
|
||||
return "$restore_rc"
|
||||
fi
|
||||
@@ -1086,7 +1162,8 @@ task13_assert_server_oidc_restore_verification() {
|
||||
[[ "$status" == 401 ]] && break
|
||||
sleep 1
|
||||
done
|
||||
[[ "$status" == 401 ]] || task13_fail "OIDC restore did not require browser reauthentication"
|
||||
[[ "$status" == 401 ]] \
|
||||
|| task13_fail "OIDC restore did not require browser reauthentication (HTTP ${status:-unavailable})"
|
||||
task13_compose_logged "verify private empty server authentication state" exec -T core sh -ceu '
|
||||
test "$(stat -c %a /data/auth)" = 700
|
||||
test "$(stat -c %a /data/auth/sessions)" = 700
|
||||
@@ -2030,6 +2107,26 @@ task13_self_test_server_release_contract() {
|
||||
|| task13_fail "workflow lacks an outer timeout for the Linux server smoke"
|
||||
}
|
||||
|
||||
task13_self_test_registry_fingerprint() {
|
||||
local fixture fixture_escaped before after linked
|
||||
fixture="$(mktemp -d "${TMPDIR:-/tmp}/thothii-task13-registry-fingerprint.XXXXXX")"
|
||||
printf -v fixture_escaped '%q' "$fixture"
|
||||
trap "rm -rf -- $fixture_escaped; trap - RETURN" RETURN
|
||||
before="$(task13_registry_filesystem_fingerprint "$fixture")"
|
||||
[[ "$before" =~ ^sha256:[0-9a-f]{64}$ ]] \
|
||||
|| task13_fail "registry fingerprint did not return a bounded digest"
|
||||
[[ "$(task13_registry_filesystem_fingerprint "$fixture")" == "$before" ]] \
|
||||
|| task13_fail "registry fingerprint changed without a filesystem mutation"
|
||||
mkdir "$fixture/locks"
|
||||
after="$(task13_registry_filesystem_fingerprint "$fixture")"
|
||||
[[ "$after" != "$before" ]] || task13_fail "registry fingerprint ignored a new directory"
|
||||
linked="$fixture/linked"
|
||||
ln -s "$fixture/locks" "$linked" 2>/dev/null || return 0
|
||||
if task13_registry_filesystem_fingerprint "$fixture" >/dev/null 2>&1; then
|
||||
task13_fail "registry fingerprint accepted a symbolic link"
|
||||
fi
|
||||
}
|
||||
|
||||
task13_self_test_source_contract() {
|
||||
local root host_network push_command registry_function workflow uses_count pinned_uses_count
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
@@ -2094,6 +2191,7 @@ task13_self_test() {
|
||||
task13_self_test_internal_semantic_offline_contract
|
||||
task13_self_test_windows_release_contract
|
||||
task13_self_test_server_release_contract
|
||||
task13_self_test_registry_fingerprint
|
||||
task13_self_test_source_contract
|
||||
printf 'Task 13 smoke safety contracts passed.\n'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user