fix(auth): harden restore verification transaction

This commit is contained in:
2026-08-18 00:58:12 +02:00
parent 0651f3316f
commit dee17893b4
10 changed files with 698 additions and 115 deletions
+108 -10
View File
@@ -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'
}