feat(native): add protected JD final residency control
This commit is contained in:
parent
11834c057a
commit
8b5328993e
8 changed files with 503 additions and 24 deletions
|
|
@ -0,0 +1,34 @@
|
||||||
|
schema: guanghu.jd-final-native-residency-control-gate/v1
|
||||||
|
gate_id: GH-OS-JD-FD-PRIMARY-001-FINAL-RESIDENCY-CONTROL
|
||||||
|
status: SOURCE_READY_QEMU_PASS_PHYSICAL_PENDING
|
||||||
|
node_id: JD-FD-PRIMARY
|
||||||
|
problem:
|
||||||
|
previous_native_default: PASS_100
|
||||||
|
previous_native_runtime_selected_recovery_itself: true
|
||||||
|
final_native_residency_proven: false
|
||||||
|
required_model:
|
||||||
|
normal_state: GUANGHU_OS_NATIVE_RESIDENT
|
||||||
|
native_runtime_autonomously_selects_recovery: false
|
||||||
|
outbound_relay_keeps_nat_path_alive: true
|
||||||
|
ordinary_reply: HLDP-NATIVE-ACK
|
||||||
|
recovery_reply: PER_DEPLOYMENT_128_BIT_CAPABILITY
|
||||||
|
recovery_request_authority: EXPLICIT_ROOT_ACTION_ON_BS-SG-003
|
||||||
|
token_repository_exposure: forbidden
|
||||||
|
wrong_capability: IGNORE_AND_REMAIN_NATIVE
|
||||||
|
qemu_acceptance:
|
||||||
|
native_initial_pipeline: PASS_100
|
||||||
|
native_remained_resident_before_authorized_recovery: PASS_100
|
||||||
|
wrong_capability_ignored: PASS_100
|
||||||
|
protected_recovery_capability_returned_to_linux: PASS_100
|
||||||
|
physical_acceptance:
|
||||||
|
source_published: pending
|
||||||
|
relay_installed: pending
|
||||||
|
one_time_final_candidate_booted: pending
|
||||||
|
sustained_native_residency_observed: pending
|
||||||
|
explicit_recovery_request_verified: pending
|
||||||
|
returned_linux_and_beacon_cleared: pending
|
||||||
|
final_native_residency_proven: false
|
||||||
|
boundary:
|
||||||
|
qemu_capability: 100
|
||||||
|
physical_server_capability: 0
|
||||||
|
do_not_claim_final_residency_until_physical_gate: true
|
||||||
|
|
@ -216,6 +216,27 @@ long_mode_start:
|
||||||
jc native_block_proof_error
|
jc native_block_proof_error
|
||||||
call ghal_block_persist_branch_receipt
|
call ghal_block_persist_branch_receipt
|
||||||
jc native_world_store_error
|
jc native_world_store_error
|
||||||
|
%ifdef GHOS_NATIVE_FINAL_RESIDENT
|
||||||
|
mov byte [rel physical_proof_flag], 0xa7
|
||||||
|
call ghal_block_write_proof
|
||||||
|
jc native_block_proof_error
|
||||||
|
.final_resident_loop:
|
||||||
|
; Keep the outbound NAT path alive. Recovery is selected by the protected
|
||||||
|
; relay capability, never autonomously by the native runtime.
|
||||||
|
mov byte [rel ghal_net_expected_sequence], 6
|
||||||
|
lea rax, [rel ghal_login_magic]
|
||||||
|
mov [rel ghal_net_expected_magic], rax
|
||||||
|
mov byte [rel ghal_net_command_kind], 0
|
||||||
|
call ghal_net_icmp_exchange_once
|
||||||
|
jc .final_resident_loop
|
||||||
|
cmp byte [rel ghal_net_matched_kind], 3
|
||||||
|
jne .final_resident_loop
|
||||||
|
call ghal_write_native_recovery_beacon
|
||||||
|
jc native_world_store_error
|
||||||
|
call ghal_block_write_proof
|
||||||
|
jc native_block_proof_error
|
||||||
|
jmp hardware_reset64
|
||||||
|
%else
|
||||||
%ifdef GHOS_NATIVE_RESIDENT
|
%ifdef GHOS_NATIVE_RESIDENT
|
||||||
mov ecx, 10
|
mov ecx, 10
|
||||||
.resident_login_loop:
|
.resident_login_loop:
|
||||||
|
|
@ -240,6 +261,7 @@ long_mode_start:
|
||||||
jc native_block_proof_error
|
jc native_block_proof_error
|
||||||
call ghal_write_native_recovery_beacon
|
call ghal_write_native_recovery_beacon
|
||||||
jc native_world_store_error
|
jc native_world_store_error
|
||||||
|
%endif
|
||||||
%endif
|
%endif
|
||||||
mov byte [rel physical_proof_flag], 0xa7
|
mov byte [rel physical_proof_flag], 0xa7
|
||||||
%endif
|
%endif
|
||||||
|
|
|
||||||
|
|
@ -1148,14 +1148,26 @@ ghal_net_icmp_exchange_once:
|
||||||
lea rdi, [rel ghal_native_ack_magic]
|
lea rdi, [rel ghal_native_ack_magic]
|
||||||
mov ecx, GHOS_LOGIN_MAGIC_SIZE
|
mov ecx, GHOS_LOGIN_MAGIC_SIZE
|
||||||
repe cmpsb
|
repe cmpsb
|
||||||
|
je .expected_magic_match
|
||||||
|
%ifdef GHOS_NATIVE_FINAL_RESIDENT
|
||||||
|
cmp byte [rel ghal_net_command_kind], 0
|
||||||
jne .ignore_packet
|
jne .ignore_packet
|
||||||
jmp .expected_magic_match
|
mov rsi, VIRTIO_NET_RX_BUFFER + GHOS_LOGIN_MAGIC_OFFSET + GHOS_LOGIN_MAGIC_SIZE
|
||||||
|
lea rdi, [rel ghal_recovery_capability]
|
||||||
|
mov ecx, GHOS_LOGIN_MAGIC_SIZE
|
||||||
|
repe cmpsb
|
||||||
|
jne .ignore_packet
|
||||||
|
mov byte [rel ghal_net_matched_kind], 3
|
||||||
|
jmp .matched_kind_ready
|
||||||
|
%else
|
||||||
|
jmp .ignore_packet
|
||||||
|
%endif
|
||||||
|
|
||||||
.expected_magic_match:
|
.expected_magic_match:
|
||||||
mov al, [rel ghal_net_command_kind]
|
mov al, [rel ghal_net_command_kind]
|
||||||
.store_matched_kind:
|
|
||||||
mov [rel ghal_net_matched_kind], al
|
mov [rel ghal_net_matched_kind], al
|
||||||
|
|
||||||
|
.matched_kind_ready:
|
||||||
cmp byte [rel ghal_net_matched_kind], 0
|
cmp byte [rel ghal_net_matched_kind], 0
|
||||||
jne .record_command
|
jne .record_command
|
||||||
mov byte [rel physical_proof_ipv4_rx_verified], 1
|
mov byte [rel physical_proof_ipv4_rx_verified], 1
|
||||||
|
|
@ -1299,6 +1311,9 @@ ghal_code_commit_magic: db "HLDP-CODE-COMMIT"
|
||||||
ghal_branch_move_magic: db "HLDP-BRANCH-MOVE"
|
ghal_branch_move_magic: db "HLDP-BRANCH-MOVE"
|
||||||
ghal_recovery_magic: db "HLDP-RECOVER-OS!"
|
ghal_recovery_magic: db "HLDP-RECOVER-OS!"
|
||||||
ghal_native_ack_magic: db "HLDP-NATIVE-ACK!"
|
ghal_native_ack_magic: db "HLDP-NATIVE-ACK!"
|
||||||
|
%ifdef GHOS_NATIVE_FINAL_RESIDENT
|
||||||
|
%include "recovery-token.inc"
|
||||||
|
%endif
|
||||||
ghal_branch_receipt_template:
|
ghal_branch_receipt_template:
|
||||||
db "GHOS_BRANCH_MAIN_V1", 10
|
db "GHOS_BRANCH_MAIN_V1", 10
|
||||||
db "channel=HLP-MOD-CODE-CHANNEL", 10
|
db "channel=HLP-MOD-CODE-CHANNEL", 10
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
[[ $# -eq 3 ]] || {
|
||||||
|
echo "usage: build-native-final-resident-candidate.sh <world-root> <output-directory> <recovery-token-file>" >&2
|
||||||
|
exit 64
|
||||||
|
}
|
||||||
|
|
||||||
|
world_root=$(readlink -f "$1")
|
||||||
|
output_root=$(readlink -m "$2")
|
||||||
|
token_file=$(readlink -f "$3")
|
||||||
|
source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
native_root=${source_root}/native/x86_64-bios
|
||||||
|
|
||||||
|
command -v nasm >/dev/null
|
||||||
|
token_hex=$(tr -d '\r\n ' <"${token_file}")
|
||||||
|
[[ ${token_hex} =~ ^[0-9a-fA-F]{32}$ ]] || {
|
||||||
|
echo "recovery token must be exactly 16 bytes encoded as 32 hexadecimal characters" >&2
|
||||||
|
exit 65
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "${output_root}"
|
||||||
|
chmod 0700 "${output_root}"
|
||||||
|
cargo run --quiet --manifest-path "${source_root}/Cargo.toml" \
|
||||||
|
-p hldp-native-compiler -- "${world_root}" "${output_root}/world.inc"
|
||||||
|
{
|
||||||
|
printf 'ghal_recovery_capability: db '
|
||||||
|
for index in $(seq 0 15); do
|
||||||
|
[[ ${index} -eq 0 ]] || printf ','
|
||||||
|
printf '0x%s' "${token_hex:$((index * 2)):2}"
|
||||||
|
done
|
||||||
|
printf '\n'
|
||||||
|
} >"${output_root}/recovery-token.inc"
|
||||||
|
chmod 0600 "${output_root}/recovery-token.inc"
|
||||||
|
|
||||||
|
layout_args=()
|
||||||
|
while IFS= read -r argument; do layout_args+=("${argument}"); done \
|
||||||
|
< <("${source_root}/scripts/native-layout-nasm-args.sh" "${world_root}")
|
||||||
|
(
|
||||||
|
cd "${output_root}"
|
||||||
|
nasm -f bin -I "${output_root}/" -I "${native_root}/" \
|
||||||
|
"${layout_args[@]}" \
|
||||||
|
-dGHOS_PHYSICAL_CANDIDATE=1 \
|
||||||
|
-dGHOS_NATIVE_RESIDENT=1 \
|
||||||
|
-dGHOS_NATIVE_FINAL_RESIDENT=1 \
|
||||||
|
"${native_root}/boot.asm" \
|
||||||
|
-o guanghu-os-x86_64-bios-final-resident.img
|
||||||
|
)
|
||||||
|
[[ $(stat -c %s "${output_root}/guanghu-os-x86_64-bios-final-resident.img") -eq 14848 ]]
|
||||||
|
sha256sum "${output_root}/guanghu-os-x86_64-bios-final-resident.img" \
|
||||||
|
>"${output_root}/guanghu-os-x86_64-bios-final-resident.img.sha256"
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
[[ $# -eq 5 ]] || {
|
||||||
|
echo "usage: install-jd-final-resident-relay.sh <peer-script> <recovery-token-file> <allowed-native-source> <request-file> <receipt-file>" >&2
|
||||||
|
exit 64
|
||||||
|
}
|
||||||
|
[[ ${EUID} -eq 0 ]] || exit 77
|
||||||
|
|
||||||
|
peer_script=$(readlink -f "$1")
|
||||||
|
token_file=$(readlink -f "$2")
|
||||||
|
allowed_source=$3
|
||||||
|
request_file=$(readlink -m "$4")
|
||||||
|
receipt_file=$(readlink -m "$5")
|
||||||
|
|
||||||
|
token_hex=$(tr -d '\r\n ' <"${token_file}")
|
||||||
|
[[ ${token_hex} =~ ^[0-9a-fA-F]{32}$ ]] || exit 65
|
||||||
|
[[ ${allowed_source} =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 65
|
||||||
|
[[ ${request_file} == /run/guanghu-jd-native-final-recovery.request ]] || exit 65
|
||||||
|
[[ ${receipt_file} == /var/lib/guanghu-os/jd-final-resident-relay.hldp ]] || exit 65
|
||||||
|
|
||||||
|
install -D -m 0755 "${peer_script}" \
|
||||||
|
/usr/local/libexec/guanghu-os/physical-native-icmp-peer.py
|
||||||
|
install -D -m 0600 "${token_file}" \
|
||||||
|
/etc/guanghu-os/jd-final-resident-recovery-token.hex
|
||||||
|
install -d -m 0700 /var/lib/guanghu-os
|
||||||
|
rm -f "${request_file}"
|
||||||
|
|
||||||
|
install -m 0644 /dev/stdin \
|
||||||
|
/etc/systemd/system/guanghu-jd-native-final-resident-relay.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Protected relay for JD Guanghu OS final native residency
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/libexec/guanghu-os/physical-native-icmp-peer.py --raw-socket --final-resident-relay --allowed-source ${allowed_source} --recovery-token-file /etc/guanghu-os/jd-final-resident-recovery-token.hex --recovery-request-file ${request_file} --receipt ${receipt_file} --timeout 604800
|
||||||
|
Restart=always
|
||||||
|
RestartSec=2
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/lib/guanghu-os /run
|
||||||
|
RestrictAddressFamilies=AF_INET
|
||||||
|
AmbientCapabilities=CAP_NET_RAW
|
||||||
|
CapabilityBoundingSet=CAP_NET_RAW
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -D -m 0755 /dev/stdin \
|
||||||
|
/usr/local/sbin/guanghu-request-jd-final-native-recovery <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
[[ ${EUID} -eq 0 ]] || exit 77
|
||||||
|
receipt=/var/lib/guanghu-os/jd-final-resident-relay.hldp
|
||||||
|
request=/run/guanghu-jd-native-final-recovery.request
|
||||||
|
grep -q '^status: READY_NATIVE_RESIDENT$' "${receipt}"
|
||||||
|
install -m 0600 /dev/null "${request}"
|
||||||
|
echo "authorized recovery request armed"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
install -D -m 0755 /dev/stdin \
|
||||||
|
/usr/local/sbin/guanghu-clear-jd-final-native-recovery-request <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
[[ ${EUID} -eq 0 ]] || exit 77
|
||||||
|
rm -f /run/guanghu-jd-native-final-recovery.request
|
||||||
|
echo "recovery request cleared"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable guanghu-jd-native-final-resident-relay.service >/dev/null
|
||||||
|
echo "installed but not started"
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
|
|
@ -72,12 +73,17 @@ def verify_native_request(packet: bytes, pipeline: list[bytes]) -> tuple[int, st
|
||||||
return sequence, source
|
return sequence, source
|
||||||
|
|
||||||
|
|
||||||
def native_ack_reply(packet: bytes) -> bytes:
|
def native_ack_reply(
|
||||||
|
packet: bytes,
|
||||||
|
response_capability: bytes = NATIVE_ACK_MAGIC,
|
||||||
|
) -> bytes:
|
||||||
|
if len(response_capability) != 16:
|
||||||
|
raise ValueError("response capability must be exactly 16 bytes")
|
||||||
header_length = (packet[0] & 0x0F) * 4
|
header_length = (packet[0] & 0x0F) * 4
|
||||||
icmp = bytearray(packet[header_length:])
|
icmp = bytearray(packet[header_length:])
|
||||||
icmp[0] = 0
|
icmp[0] = 0
|
||||||
icmp[2:4] = b"\0\0"
|
icmp[2:4] = b"\0\0"
|
||||||
icmp[32:48] = NATIVE_ACK_MAGIC
|
icmp[32:48] = response_capability
|
||||||
icmp[2:4] = struct.pack("!H", checksum(bytes(icmp)))
|
icmp[2:4] = struct.pack("!H", checksum(bytes(icmp)))
|
||||||
return bytes(icmp)
|
return bytes(icmp)
|
||||||
|
|
||||||
|
|
@ -122,6 +128,9 @@ def main() -> None:
|
||||||
parser.add_argument("--resume-resident-count", type=int)
|
parser.add_argument("--resume-resident-count", type=int)
|
||||||
parser.add_argument("--resident-pipeline", action="store_true")
|
parser.add_argument("--resident-pipeline", action="store_true")
|
||||||
parser.add_argument("--native-relay", action="store_true")
|
parser.add_argument("--native-relay", action="store_true")
|
||||||
|
parser.add_argument("--final-resident-relay", action="store_true")
|
||||||
|
parser.add_argument("--recovery-token-file")
|
||||||
|
parser.add_argument("--recovery-request-file")
|
||||||
parser.add_argument("--allowed-source")
|
parser.add_argument("--allowed-source")
|
||||||
parser.add_argument("--native-ack-delay", type=float, default=0.25)
|
parser.add_argument("--native-ack-delay", type=float, default=0.25)
|
||||||
parser.add_argument("--native-ack-repetitions", type=int, default=4)
|
parser.add_argument("--native-ack-repetitions", type=int, default=4)
|
||||||
|
|
@ -149,37 +158,66 @@ def main() -> None:
|
||||||
args.resume_resident_count is not None,
|
args.resume_resident_count is not None,
|
||||||
args.resident_pipeline,
|
args.resident_pipeline,
|
||||||
args.native_relay,
|
args.native_relay,
|
||||||
|
args.final_resident_relay,
|
||||||
)
|
)
|
||||||
) > 1:
|
) > 1:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"--resident, --login-only, --resume-after-login, and "
|
"--resident, --login-only, --resume-after-login, and "
|
||||||
"--resume-after-commit, --resume-after-branch, and "
|
"--resume-after-commit, --resume-after-branch, and "
|
||||||
"--resume-resident-count, --resident-pipeline, and --native-relay "
|
"--resume-resident-count, --resident-pipeline, --native-relay, "
|
||||||
|
"and --final-resident-relay "
|
||||||
"are mutually exclusive"
|
"are mutually exclusive"
|
||||||
)
|
)
|
||||||
|
|
||||||
socket_type = socket.SOCK_RAW if args.raw_socket else socket.SOCK_DGRAM
|
socket_type = socket.SOCK_RAW if args.raw_socket else socket.SOCK_DGRAM
|
||||||
peer = socket.socket(socket.AF_INET, socket_type, socket.IPPROTO_ICMP)
|
peer = socket.socket(socket.AF_INET, socket_type, socket.IPPROTO_ICMP)
|
||||||
deadline = time.monotonic() + args.timeout
|
deadline = time.monotonic() + args.timeout
|
||||||
if args.native_relay:
|
if args.native_relay or args.final_resident_relay:
|
||||||
if not args.raw_socket:
|
if not args.raw_socket:
|
||||||
raise SystemExit("--native-relay requires --raw-socket")
|
raise SystemExit("native relay modes require --raw-socket")
|
||||||
if not args.allowed_source:
|
if not args.allowed_source:
|
||||||
raise SystemExit("--native-relay requires --allowed-source")
|
raise SystemExit("native relay modes require --allowed-source")
|
||||||
|
recovery_capability = None
|
||||||
|
if args.final_resident_relay:
|
||||||
|
if not args.recovery_token_file or not args.recovery_request_file:
|
||||||
|
raise SystemExit(
|
||||||
|
"--final-resident-relay requires --recovery-token-file "
|
||||||
|
"and --recovery-request-file"
|
||||||
|
)
|
||||||
|
token_hex = (
|
||||||
|
pathlib.Path(args.recovery_token_file)
|
||||||
|
.read_text(encoding="ascii")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
recovery_capability = bytes.fromhex(token_hex)
|
||||||
|
if len(recovery_capability) != 16:
|
||||||
|
raise SystemExit("recovery token must be exactly 16 bytes")
|
||||||
allowed_source = socket.gethostbyname(args.allowed_source)
|
allowed_source = socket.gethostbyname(args.allowed_source)
|
||||||
pipeline = [
|
pipeline = (
|
||||||
LOGIN_MAGIC,
|
[
|
||||||
LOGIN_MAGIC,
|
LOGIN_MAGIC,
|
||||||
LOGIN_MAGIC,
|
LOGIN_MAGIC,
|
||||||
COMMIT_MAGIC,
|
LOGIN_MAGIC,
|
||||||
BRANCH_MAGIC,
|
COMMIT_MAGIC,
|
||||||
*([LOGIN_MAGIC] * 10),
|
BRANCH_MAGIC,
|
||||||
RECOVERY_MAGIC,
|
LOGIN_MAGIC,
|
||||||
]
|
]
|
||||||
|
if args.final_resident_relay
|
||||||
|
else [
|
||||||
|
LOGIN_MAGIC,
|
||||||
|
LOGIN_MAGIC,
|
||||||
|
LOGIN_MAGIC,
|
||||||
|
COMMIT_MAGIC,
|
||||||
|
BRANCH_MAGIC,
|
||||||
|
*([LOGIN_MAGIC] * 10),
|
||||||
|
RECOVERY_MAGIC,
|
||||||
|
]
|
||||||
|
)
|
||||||
peer.settimeout(0.5)
|
peer.settimeout(0.5)
|
||||||
acknowledged: set[int] = set()
|
acknowledged: set[int] = set()
|
||||||
sources: set[str] = set()
|
sources: set[str] = set()
|
||||||
completed_at: float | None = None
|
completed_at: float | None = None
|
||||||
|
recovery_capability_sent = False
|
||||||
while time.monotonic() < deadline and (
|
while time.monotonic() < deadline and (
|
||||||
completed_at is None or time.monotonic() < completed_at + 5.0
|
completed_at is None or time.monotonic() < completed_at + 5.0
|
||||||
):
|
):
|
||||||
|
|
@ -196,7 +234,15 @@ def main() -> None:
|
||||||
# Repeat the authenticated frame because the physical JD path can
|
# Repeat the authenticated frame because the physical JD path can
|
||||||
# lose a single raw-socket reply after NAT has been established.
|
# lose a single raw-socket reply after NAT has been established.
|
||||||
time.sleep(args.native_ack_delay)
|
time.sleep(args.native_ack_delay)
|
||||||
reply = native_ack_reply(packet)
|
response_capability = NATIVE_ACK_MAGIC
|
||||||
|
if (
|
||||||
|
args.final_resident_relay
|
||||||
|
and sequence == 6
|
||||||
|
and os.path.isfile(args.recovery_request_file)
|
||||||
|
):
|
||||||
|
response_capability = recovery_capability
|
||||||
|
recovery_capability_sent = True
|
||||||
|
reply = native_ack_reply(packet, response_capability)
|
||||||
for repetition in range(args.native_ack_repetitions):
|
for repetition in range(args.native_ack_repetitions):
|
||||||
peer.sendto(reply, address)
|
peer.sendto(reply, address)
|
||||||
if repetition + 1 < args.native_ack_repetitions:
|
if repetition + 1 < args.native_ack_repetitions:
|
||||||
|
|
@ -208,8 +254,39 @@ def main() -> None:
|
||||||
)
|
)
|
||||||
acknowledged.add(sequence)
|
acknowledged.add(sequence)
|
||||||
sources.add(source)
|
sources.add(source)
|
||||||
if len(acknowledged) == len(pipeline) and completed_at is None:
|
if args.final_resident_relay:
|
||||||
|
ready = all(sequence_id in acknowledged for sequence_id in range(1, 7))
|
||||||
|
receipt = pathlib.Path(args.receipt)
|
||||||
|
receipt.write_text(
|
||||||
|
"schema: guanghu.physical-native-final-resident-relay/v1\n"
|
||||||
|
f"status: {'RECOVERY_CAPABILITY_SENT' if recovery_capability_sent else 'READY_NATIVE_RESIDENT' if ready else 'STARTING'}\n"
|
||||||
|
"handshake_direction: NATIVE_INITIATED_OUTBOUND_ICMP\n"
|
||||||
|
"native_ack_marker: HLDP-NATIVE-ACK!\n"
|
||||||
|
"relay_cannot_select_command: true\n"
|
||||||
|
"recovery_requires_root_owned_request_file: true\n"
|
||||||
|
f"recovery_capability_sent: {str(recovery_capability_sent).lower()}\n"
|
||||||
|
f"acknowledged_sequences: {','.join(str(item) for item in sorted(acknowledged))}\n"
|
||||||
|
f"observed_nat_sources: {','.join(sorted(sources))}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
if ready and recovery_capability_sent and completed_at is None:
|
||||||
|
completed_at = time.monotonic()
|
||||||
|
elif len(acknowledged) == len(pipeline) and completed_at is None:
|
||||||
completed_at = time.monotonic()
|
completed_at = time.monotonic()
|
||||||
|
if args.final_resident_relay:
|
||||||
|
if not all(sequence_id in acknowledged for sequence_id in range(1, 7)):
|
||||||
|
raise TimeoutError(
|
||||||
|
"native final-resident relay did not reach ready state; "
|
||||||
|
"acknowledged sequences: "
|
||||||
|
+ ",".join(str(item) for item in sorted(acknowledged))
|
||||||
|
)
|
||||||
|
if not recovery_capability_sent:
|
||||||
|
raise TimeoutError(
|
||||||
|
"native final-resident relay remained ready but no "
|
||||||
|
"authorized recovery request was observed"
|
||||||
|
)
|
||||||
|
print(pathlib.Path(args.receipt).read_text(encoding="utf-8"), end="")
|
||||||
|
return
|
||||||
if len(acknowledged) != len(pipeline):
|
if len(acknowledged) != len(pipeline):
|
||||||
raise TimeoutError(
|
raise TimeoutError(
|
||||||
"native relay pipeline incomplete; acknowledged sequences: "
|
"native relay pipeline incomplete; acknowledged sequences: "
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,12 @@ def ordinary_reply(frame: bytes) -> bytes:
|
||||||
return bytes(reply)
|
return bytes(reply)
|
||||||
|
|
||||||
|
|
||||||
def authenticated_reply(frame: bytes, magic: bytes) -> bytes:
|
def authenticated_reply(frame: bytes, response_capability: bytes = NATIVE_ACK_MAGIC) -> bytes:
|
||||||
|
if len(response_capability) != 16:
|
||||||
|
raise ValueError("response capability must be exactly 16 bytes")
|
||||||
reply = bytearray(ordinary_reply(frame))
|
reply = bytearray(ordinary_reply(frame))
|
||||||
reply[36:38] = b"\0\0"
|
reply[36:38] = b"\0\0"
|
||||||
reply[66:82] = NATIVE_ACK_MAGIC
|
reply[66:82] = response_capability
|
||||||
reply[36:38] = struct.pack("!H", checksum(bytes(reply[34:82])))
|
reply[36:38] = struct.pack("!H", checksum(bytes(reply[34:82])))
|
||||||
return bytes(reply)
|
return bytes(reply)
|
||||||
|
|
||||||
|
|
@ -93,11 +95,23 @@ def main() -> None:
|
||||||
parser.add_argument("--qemu-port", type=int, required=True)
|
parser.add_argument("--qemu-port", type=int, required=True)
|
||||||
parser.add_argument("--receipt", required=True)
|
parser.add_argument("--receipt", required=True)
|
||||||
parser.add_argument("--resident", action="store_true")
|
parser.add_argument("--resident", action="store_true")
|
||||||
|
parser.add_argument("--final-resident", action="store_true")
|
||||||
|
parser.add_argument("--recovery-token-file")
|
||||||
parser.add_argument("--login-only", action="store_true")
|
parser.add_argument("--login-only", action="store_true")
|
||||||
parser.add_argument("--guest-ip", default="172.16.0.6")
|
parser.add_argument("--guest-ip", default="172.16.0.6")
|
||||||
parser.add_argument("--peer-ip", default="172.16.0.1")
|
parser.add_argument("--peer-ip", default="172.16.0.1")
|
||||||
parser.add_argument("--relay-ip", default="43.153.193.169")
|
parser.add_argument("--relay-ip", default="43.153.193.169")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
if sum((args.resident, args.final_resident, args.login_only)) > 1:
|
||||||
|
raise SystemExit("--resident, --final-resident and --login-only are mutually exclusive")
|
||||||
|
if args.final_resident and not args.recovery_token_file:
|
||||||
|
raise SystemExit("--final-resident requires --recovery-token-file")
|
||||||
|
recovery_capability = None
|
||||||
|
if args.recovery_token_file:
|
||||||
|
token_hex = open(args.recovery_token_file, encoding="ascii").read().strip()
|
||||||
|
recovery_capability = bytes.fromhex(token_hex)
|
||||||
|
if len(recovery_capability) != 16:
|
||||||
|
raise SystemExit("recovery token must be exactly 16 bytes")
|
||||||
GUEST_IP = socket.inet_aton(args.guest_ip)
|
GUEST_IP = socket.inet_aton(args.guest_ip)
|
||||||
GATEWAY_IP = socket.inet_aton(args.peer_ip)
|
GATEWAY_IP = socket.inet_aton(args.peer_ip)
|
||||||
RELAY_IP = socket.inet_aton(args.relay_ip)
|
RELAY_IP = socket.inet_aton(args.relay_ip)
|
||||||
|
|
@ -135,6 +149,9 @@ def main() -> None:
|
||||||
elif sequence == 5:
|
elif sequence == 5:
|
||||||
magic = BRANCH_MAGIC
|
magic = BRANCH_MAGIC
|
||||||
branch_verified = True
|
branch_verified = True
|
||||||
|
elif args.final_resident and sequence == 6:
|
||||||
|
magic = LOGIN_MAGIC
|
||||||
|
resident_login_count += 1
|
||||||
elif 6 <= sequence <= 15:
|
elif 6 <= sequence <= 15:
|
||||||
magic = LOGIN_MAGIC
|
magic = LOGIN_MAGIC
|
||||||
resident_login_count += 1
|
resident_login_count += 1
|
||||||
|
|
@ -146,14 +163,26 @@ def main() -> None:
|
||||||
verified_request(frame, sequence, magic)
|
verified_request(frame, sequence, magic)
|
||||||
peer.sendto(ordinary_reply(frame), qemu)
|
peer.sendto(ordinary_reply(frame), qemu)
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
reply = authenticated_reply(frame, magic)
|
response_capability = NATIVE_ACK_MAGIC
|
||||||
|
if args.final_resident and resident_login_count >= 3:
|
||||||
|
response_capability = recovery_capability
|
||||||
|
recovery_verified = True
|
||||||
|
reply = authenticated_reply(frame, response_capability)
|
||||||
for repetition in range(4):
|
for repetition in range(4):
|
||||||
peer.sendto(reply, qemu)
|
peer.sendto(reply, qemu)
|
||||||
if repetition < 3:
|
if repetition < 3:
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
terminal_sequence = 3 if args.login_only else (16 if args.resident else 5)
|
terminal = (
|
||||||
if sequence != terminal_sequence:
|
sequence == 3
|
||||||
|
if args.login_only
|
||||||
|
else recovery_verified
|
||||||
|
if args.final_resident
|
||||||
|
else sequence == 16
|
||||||
|
if args.resident
|
||||||
|
else sequence == 5
|
||||||
|
)
|
||||||
|
if not terminal:
|
||||||
continue
|
continue
|
||||||
with open(args.receipt, "w", encoding="utf-8") as output:
|
with open(args.receipt, "w", encoding="utf-8") as output:
|
||||||
output.write(
|
output.write(
|
||||||
|
|
@ -169,6 +198,8 @@ def main() -> None:
|
||||||
f"branch_move_reply_verified: {str(branch_verified).lower()}\n"
|
f"branch_move_reply_verified: {str(branch_verified).lower()}\n"
|
||||||
f"resident_login_reply_count: {resident_login_count}\n"
|
f"resident_login_reply_count: {resident_login_count}\n"
|
||||||
f"recovery_reply_verified: {str(recovery_verified).lower()}\n"
|
f"recovery_reply_verified: {str(recovery_verified).lower()}\n"
|
||||||
|
f"final_resident_control: {str(args.final_resident).lower()}\n"
|
||||||
|
"recovery_selected_by_native: false\n"
|
||||||
"login_magic: HLDP-GHOS-LOGIN!\n"
|
"login_magic: HLDP-GHOS-LOGIN!\n"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
[[ $# -eq 3 ]] || {
|
||||||
|
echo "usage: test-native-final-resident-candidate.sh <candidate-image> <recovery-token-file> <receipt-output>" >&2
|
||||||
|
exit 64
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate=$(readlink -f "$1")
|
||||||
|
token_file=$(readlink -f "$2")
|
||||||
|
receipt=$(readlink -m "$3")
|
||||||
|
source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
native_root=${source_root}/native/x86_64-bios
|
||||||
|
test_root=$(mktemp -d)
|
||||||
|
peer_pid=
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n ${peer_pid} ]]; then
|
||||||
|
kill "${peer_pid}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ ${GHOS_KEEP_TEST_ROOT:-0} != 1 ]]; then
|
||||||
|
rm -rf "${test_root}"
|
||||||
|
else
|
||||||
|
echo "GHOS_TEST_ROOT=${test_root}" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
[[ $(stat -c %s "${candidate}") -eq 14848 ]]
|
||||||
|
[[ $(tr -d '\r\n ' <"${token_file}" | wc -c) -eq 32 ]]
|
||||||
|
python3 - "${token_file}" "${test_root}/wrong-token.hex" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
token = bytearray.fromhex(pathlib.Path(sys.argv[1]).read_text().strip())
|
||||||
|
token[0] ^= 0x01
|
||||||
|
pathlib.Path(sys.argv[2]).write_text(token.hex() + "\n", encoding="ascii")
|
||||||
|
PY
|
||||||
|
truncate -s 700M "${test_root}/disk.img"
|
||||||
|
nasm -f bin -dCANDIDATE_LBA=105 -dPROOF_LBA=134 \
|
||||||
|
"${native_root}/physical-test-mbr.asm" -o "${test_root}/mbr.bin"
|
||||||
|
dd if="${test_root}/mbr.bin" of="${test_root}/disk.img" \
|
||||||
|
bs=512 seek=0 conv=notrunc status=none
|
||||||
|
dd if="${candidate}" of="${test_root}/disk.img" \
|
||||||
|
bs=512 seek=105 conv=notrunc status=none
|
||||||
|
|
||||||
|
peer_port=$((33000 + ${BASHPID:-$$} % 1000))
|
||||||
|
qemu_port=$((peer_port + 1))
|
||||||
|
python3 "${source_root}/scripts/qemu-native-net-peer.py" \
|
||||||
|
--listen-port "${peer_port}" \
|
||||||
|
--qemu-port "${qemu_port}" \
|
||||||
|
--receipt "${test_root}/peer.hldp" \
|
||||||
|
--final-resident \
|
||||||
|
--recovery-token-file "${token_file}" >"${test_root}/peer.log" 2>&1 &
|
||||||
|
peer_pid=$!
|
||||||
|
set +e
|
||||||
|
timeout 40 qemu-system-x86_64 \
|
||||||
|
-machine pc,accel=tcg \
|
||||||
|
-m 64M \
|
||||||
|
-drive "if=none,id=ghboot,format=raw,file=${test_root}/disk.img" \
|
||||||
|
-device virtio-blk-pci,drive=ghboot,disable-modern=on,bootindex=0 \
|
||||||
|
-netdev "dgram,id=ghnet,local.type=inet,local.host=127.0.0.1,local.port=${qemu_port},remote.type=inet,remote.host=127.0.0.1,remote.port=${peer_port}" \
|
||||||
|
-device virtio-net-pci,netdev=ghnet,disable-modern=on,mac=52:54:00:26:71:98 \
|
||||||
|
-display none \
|
||||||
|
-monitor none \
|
||||||
|
-serial "file:${test_root}/serial.log" \
|
||||||
|
-device isa-debug-exit,iobase=0xf4,iosize=0x04
|
||||||
|
qemu_status=$?
|
||||||
|
set -e
|
||||||
|
[[ ${qemu_status} -eq 33 ]]
|
||||||
|
wait "${peer_pid}"
|
||||||
|
peer_pid=
|
||||||
|
|
||||||
|
grep -q '^final_resident_control: true$' "${test_root}/peer.hldp"
|
||||||
|
grep -q '^resident_login_reply_count: 3$' "${test_root}/peer.hldp"
|
||||||
|
grep -q '^recovery_reply_verified: true$' "${test_root}/peer.hldp"
|
||||||
|
grep -q '^recovery_selected_by_native: false$' "${test_root}/peer.hldp"
|
||||||
|
grep -q '^GHOS_NATIVE_RECOVERY_BEACON=WRITE_READ_VERIFIED' "${test_root}/serial.log"
|
||||||
|
grep -q '^GHOS_DISK_PROOF_OBSERVED_AFTER_RESET=LBA134' "${test_root}/serial.log"
|
||||||
|
|
||||||
|
python3 - "${test_root}/disk.img" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with pathlib.Path(sys.argv[1]).open("rb") as disk:
|
||||||
|
def sector(lba: int, count: int = 1) -> bytes:
|
||||||
|
disk.seek(lba * 512)
|
||||||
|
return disk.read(count * 512)
|
||||||
|
|
||||||
|
proof = sector(134)
|
||||||
|
assert proof[0] == 0xA7
|
||||||
|
assert proof[1:].startswith(b"GHOS_NATIVE_LONG64_DISK_PROOF\0")
|
||||||
|
assert proof[99:102] == bytes([1, 1, 1])
|
||||||
|
assert proof[102:105] == bytes([1, 1, 1])
|
||||||
|
assert proof[105] == 6
|
||||||
|
assert sector(139, 2).startswith(b"# GRUB Environment Block\n")
|
||||||
|
assert sector(141).startswith(b"GHOS_GHCIP_INDEX_V1\n")
|
||||||
|
PY
|
||||||
|
|
||||||
|
truncate -s 700M "${test_root}/wrong-capability-disk.img"
|
||||||
|
dd if="${test_root}/mbr.bin" of="${test_root}/wrong-capability-disk.img" \
|
||||||
|
bs=512 seek=0 conv=notrunc status=none
|
||||||
|
dd if="${candidate}" of="${test_root}/wrong-capability-disk.img" \
|
||||||
|
bs=512 seek=105 conv=notrunc status=none
|
||||||
|
wrong_peer_port=$((peer_port + 2))
|
||||||
|
wrong_qemu_port=$((qemu_port + 2))
|
||||||
|
python3 "${source_root}/scripts/qemu-native-net-peer.py" \
|
||||||
|
--listen-port "${wrong_peer_port}" \
|
||||||
|
--qemu-port "${wrong_qemu_port}" \
|
||||||
|
--receipt "${test_root}/wrong-peer.hldp" \
|
||||||
|
--final-resident \
|
||||||
|
--recovery-token-file "${test_root}/wrong-token.hex" \
|
||||||
|
>"${test_root}/wrong-peer.log" 2>&1 &
|
||||||
|
peer_pid=$!
|
||||||
|
set +e
|
||||||
|
timeout 5 qemu-system-x86_64 \
|
||||||
|
-machine pc,accel=tcg \
|
||||||
|
-m 64M \
|
||||||
|
-drive "if=none,id=ghboot,format=raw,file=${test_root}/wrong-capability-disk.img" \
|
||||||
|
-device virtio-blk-pci,drive=ghboot,disable-modern=on,bootindex=0 \
|
||||||
|
-netdev "dgram,id=ghnet,local.type=inet,local.host=127.0.0.1,local.port=${wrong_qemu_port},remote.type=inet,remote.host=127.0.0.1,remote.port=${wrong_peer_port}" \
|
||||||
|
-device virtio-net-pci,netdev=ghnet,disable-modern=on,mac=52:54:00:26:71:98 \
|
||||||
|
-display none \
|
||||||
|
-monitor none \
|
||||||
|
-serial "file:${test_root}/wrong-capability-serial.log" \
|
||||||
|
-device isa-debug-exit,iobase=0xf4,iosize=0x04
|
||||||
|
wrong_qemu_status=$?
|
||||||
|
set -e
|
||||||
|
[[ ${wrong_qemu_status} -eq 124 ]]
|
||||||
|
wait "${peer_pid}"
|
||||||
|
peer_pid=
|
||||||
|
python3 - "${test_root}/wrong-capability-disk.img" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with pathlib.Path(sys.argv[1]).open("rb") as disk:
|
||||||
|
disk.seek(134 * 512)
|
||||||
|
proof = disk.read(512)
|
||||||
|
disk.seek(139 * 512)
|
||||||
|
recovery = disk.read(1024)
|
||||||
|
assert proof[0] == 0xA7
|
||||||
|
assert recovery == bytes(1024)
|
||||||
|
PY
|
||||||
|
|
||||||
|
observed_at=$(date --iso-8601=seconds)
|
||||||
|
image_sha=$(sha256sum "${candidate}" | awk '{print $1}')
|
||||||
|
cat >"${receipt}" <<EOF
|
||||||
|
schema: guanghu.jd-native-final-resident-qemu-test/v1
|
||||||
|
receipt_id: GH-OS-JD-FD-PRIMARY-001-NATIVE-FINAL-RESIDENT-CONTROL-QEMU-001
|
||||||
|
status: PASS_100
|
||||||
|
observed_at: ${observed_at}
|
||||||
|
node_id: JD-FD-PRIMARY
|
||||||
|
candidate:
|
||||||
|
lba_start: 105
|
||||||
|
sector_count: 29
|
||||||
|
sha256: ${image_sha}
|
||||||
|
resident_runtime:
|
||||||
|
initial_login_replies: 3
|
||||||
|
code_commit_reply_verified: true
|
||||||
|
branch_move_reply_verified: true
|
||||||
|
sustained_login_replies_before_recovery: 2
|
||||||
|
recovery_control:
|
||||||
|
selected_by_native_runtime: false
|
||||||
|
per_deployment_capability_required: true
|
||||||
|
protected_relay_capability_verified: true
|
||||||
|
wrong_capability_ignored_while_native_remained_resident: true
|
||||||
|
recovery_beacon_write_read: PASS_100
|
||||||
|
boundary:
|
||||||
|
qemu_capability: 100
|
||||||
|
physical_server_capability: 0
|
||||||
|
final_native_residency_proven: false
|
||||||
|
next_action: PHYSICAL_ONE_TIME_FINAL_RESIDENCY_AND_PROTECTED_RECOVERY_GATE
|
||||||
|
EOF
|
||||||
Loading…
Reference in a new issue