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
|
||||
call ghal_block_persist_branch_receipt
|
||||
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
|
||||
mov ecx, 10
|
||||
.resident_login_loop:
|
||||
|
|
@ -240,6 +261,7 @@ long_mode_start:
|
|||
jc native_block_proof_error
|
||||
call ghal_write_native_recovery_beacon
|
||||
jc native_world_store_error
|
||||
%endif
|
||||
%endif
|
||||
mov byte [rel physical_proof_flag], 0xa7
|
||||
%endif
|
||||
|
|
|
|||
|
|
@ -1148,14 +1148,26 @@ ghal_net_icmp_exchange_once:
|
|||
lea rdi, [rel ghal_native_ack_magic]
|
||||
mov ecx, GHOS_LOGIN_MAGIC_SIZE
|
||||
repe cmpsb
|
||||
je .expected_magic_match
|
||||
%ifdef GHOS_NATIVE_FINAL_RESIDENT
|
||||
cmp byte [rel ghal_net_command_kind], 0
|
||||
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:
|
||||
mov al, [rel ghal_net_command_kind]
|
||||
.store_matched_kind:
|
||||
mov [rel ghal_net_matched_kind], al
|
||||
|
||||
.matched_kind_ready:
|
||||
cmp byte [rel ghal_net_matched_kind], 0
|
||||
jne .record_command
|
||||
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_recovery_magic: db "HLDP-RECOVER-OS!"
|
||||
ghal_native_ack_magic: db "HLDP-NATIVE-ACK!"
|
||||
%ifdef GHOS_NATIVE_FINAL_RESIDENT
|
||||
%include "recovery-token.inc"
|
||||
%endif
|
||||
ghal_branch_receipt_template:
|
||||
db "GHOS_BRANCH_MAIN_V1", 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
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import socket
|
||||
import struct
|
||||
|
|
@ -72,12 +73,17 @@ def verify_native_request(packet: bytes, pipeline: list[bytes]) -> tuple[int, st
|
|||
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
|
||||
icmp = bytearray(packet[header_length:])
|
||||
icmp[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)))
|
||||
return bytes(icmp)
|
||||
|
||||
|
|
@ -122,6 +128,9 @@ def main() -> None:
|
|||
parser.add_argument("--resume-resident-count", type=int)
|
||||
parser.add_argument("--resident-pipeline", 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("--native-ack-delay", type=float, default=0.25)
|
||||
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.resident_pipeline,
|
||||
args.native_relay,
|
||||
args.final_resident_relay,
|
||||
)
|
||||
) > 1:
|
||||
raise SystemExit(
|
||||
"--resident, --login-only, --resume-after-login, 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"
|
||||
)
|
||||
|
||||
socket_type = socket.SOCK_RAW if args.raw_socket else socket.SOCK_DGRAM
|
||||
peer = socket.socket(socket.AF_INET, socket_type, socket.IPPROTO_ICMP)
|
||||
deadline = time.monotonic() + args.timeout
|
||||
if args.native_relay:
|
||||
if args.native_relay or args.final_resident_relay:
|
||||
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:
|
||||
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)
|
||||
pipeline = [
|
||||
LOGIN_MAGIC,
|
||||
LOGIN_MAGIC,
|
||||
LOGIN_MAGIC,
|
||||
COMMIT_MAGIC,
|
||||
BRANCH_MAGIC,
|
||||
*([LOGIN_MAGIC] * 10),
|
||||
RECOVERY_MAGIC,
|
||||
]
|
||||
pipeline = (
|
||||
[
|
||||
LOGIN_MAGIC,
|
||||
LOGIN_MAGIC,
|
||||
LOGIN_MAGIC,
|
||||
COMMIT_MAGIC,
|
||||
BRANCH_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)
|
||||
acknowledged: set[int] = set()
|
||||
sources: set[str] = set()
|
||||
completed_at: float | None = None
|
||||
recovery_capability_sent = False
|
||||
while time.monotonic() < deadline and (
|
||||
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
|
||||
# lose a single raw-socket reply after NAT has been established.
|
||||
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):
|
||||
peer.sendto(reply, address)
|
||||
if repetition + 1 < args.native_ack_repetitions:
|
||||
|
|
@ -208,8 +254,39 @@ def main() -> None:
|
|||
)
|
||||
acknowledged.add(sequence)
|
||||
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()
|
||||
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):
|
||||
raise TimeoutError(
|
||||
"native relay pipeline incomplete; acknowledged sequences: "
|
||||
|
|
|
|||
|
|
@ -78,10 +78,12 @@ def ordinary_reply(frame: bytes) -> bytes:
|
|||
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[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])))
|
||||
return bytes(reply)
|
||||
|
||||
|
|
@ -93,11 +95,23 @@ def main() -> None:
|
|||
parser.add_argument("--qemu-port", type=int, required=True)
|
||||
parser.add_argument("--receipt", required=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("--guest-ip", default="172.16.0.6")
|
||||
parser.add_argument("--peer-ip", default="172.16.0.1")
|
||||
parser.add_argument("--relay-ip", default="43.153.193.169")
|
||||
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)
|
||||
GATEWAY_IP = socket.inet_aton(args.peer_ip)
|
||||
RELAY_IP = socket.inet_aton(args.relay_ip)
|
||||
|
|
@ -135,6 +149,9 @@ def main() -> None:
|
|||
elif sequence == 5:
|
||||
magic = BRANCH_MAGIC
|
||||
branch_verified = True
|
||||
elif args.final_resident and sequence == 6:
|
||||
magic = LOGIN_MAGIC
|
||||
resident_login_count += 1
|
||||
elif 6 <= sequence <= 15:
|
||||
magic = LOGIN_MAGIC
|
||||
resident_login_count += 1
|
||||
|
|
@ -146,14 +163,26 @@ def main() -> None:
|
|||
verified_request(frame, sequence, magic)
|
||||
peer.sendto(ordinary_reply(frame), qemu)
|
||||
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):
|
||||
peer.sendto(reply, qemu)
|
||||
if repetition < 3:
|
||||
time.sleep(0.01)
|
||||
|
||||
terminal_sequence = 3 if args.login_only else (16 if args.resident else 5)
|
||||
if sequence != terminal_sequence:
|
||||
terminal = (
|
||||
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
|
||||
with open(args.receipt, "w", encoding="utf-8") as output:
|
||||
output.write(
|
||||
|
|
@ -169,6 +198,8 @@ def main() -> None:
|
|||
f"branch_move_reply_verified: {str(branch_verified).lower()}\n"
|
||||
f"resident_login_reply_count: {resident_login_count}\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"
|
||||
)
|
||||
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