guanghu-ice-heart/deployment/GH-CVM-MAIN-PROD-01/0001-feat-guanghu-os-archive-enterprise-native-recovery-l.patch

6598 lines
253 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

From b546827c71fe1e13ee221c9922eb73d20900234d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com>
Date: Mon, 3 Aug 2026 20:32:48 +0800
Subject: [PATCH] feat(guanghu-os): archive enterprise native recovery line
---
guanghu-os/Cargo.lock | 186 +++++++-
guanghu-os/README.md | 11 +-
guanghu-os/crates/ghdr/Cargo.toml | 5 +
.../crates/ghdr/src/bin/ghdr-controller.rs | 150 ++++++
guanghu-os/crates/ghdr/src/lib.rs | 341 +++++++++++++-
guanghu-os/crates/ghdr/tests/ghdr_command.rs | 24 +-
.../crates/ghdr/tests/ghdr_controller.rs | 163 +++++++
guanghu-os/crates/ghdr/tests/ghdr_library.rs | 149 +++++-
guanghu-os/crates/hldp-runtime/src/lib.rs | 137 +++++-
.../hldp-runtime/tests/world_manifest.rs | 139 +++++-
.../DEVELOPMENT-LINE-20260801-20260803.md | 251 ++++++++++
.../controller-signer/.gitignore | 2 +
.../controller-signer/README.md | 43 ++
.../guanghu-ghdr-controller-poller.py | 130 ++++++
.../guanghu-ghdr-controller-poller.service | 33 ++
.../guanghu-ghdr-signer-http.py | 195 ++++++++
.../controller-signer/guanghu-ghdr-signer.py | 206 +++++++++
.../guanghu-ghdr-signer.service | 34 ++
.../install-controller-signer.sh | 159 +++++++
.../install-jd-forced-key.sh | 31 ++
.../test-controller-poller.py | 84 ++++
.../test-controller-signer-http.py | 187 ++++++++
.../test-controller-signer.py | 127 ++++++
.../world-seed/CURRENT.hldp | 40 ++
.../GH-CVM-MAIN-PROD-01/world-seed/WAKE.hldp | 37 ++
.../world-seed/WORLD-MANIFEST.hldp | 119 +++++
.../run-guanghu-native-quality-gate.sh | 124 +++++
.../BINGSHUO-STANDING-AUTHORIZATION.hldp | 44 ++
.../world-seed/state/checkpoints/GENESIS.hldp | 14 +
.../state/receipts/CODE-CHANNEL-BASELINE.hldp | 24 +
.../receipts/ENTERPRISE-ACCESS-20260801.hldp | 24 +
...ISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp | 27 ++
.../ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp | 27 ++
.../GH-CVM-MAIN-PROD-01-NATIVE.hldp | 25 +
.../GESTATIONAL-CONTINUITY-INGESTION.hldp | 48 ++
.../cognition/PERSONA-BIRTH-CONDITION.hldp | 34 ++
.../world-seed/world/domains/fifth/INDEX.hldp | 13 +
.../world-seed/world/domains/main/INDEX.hldp | 8 +
.../world-seed/world/domains/sub/INDEX.hldp | 8 +
.../world/domains/zero-sense/INDEX.hldp | 8 +
.../world-seed/world/domains/zero/INDEX.hldp | 8 +
.../world/services/code-channel/CHANNEL.hldp | 55 +++
.../services/code-channel/QUALITY-GATE.hldp | 53 +++
.../services/native-recovery/PROTOCOL.hldp | 45 ++
.../services/native-storage/DISK-LAYOUT.hldp | 35 ++
guanghu-os/disaster-recovery/README.md | 60 ++-
.../disaster-recovery/node-plan.example.json | 26 +-
guanghu-os/native/x86_64-bios/boot.asm | 23 +
guanghu-os/native/x86_64-bios/ghal-virtio.asm | 428 +++++++++++++++---
.../native/x86_64-bios/physical-test-mbr.asm | 2 +
.../build-native-physical-candidate.sh | 37 +-
.../build-native-resident-candidate.sh | 32 ++
.../scripts/install-native-ab-signed.sh | 223 +++++++++
guanghu-os/scripts/qemu-native-net-peer.py | 290 +++++++++++-
.../scripts/render-native-recovery-beacon.sh | 22 +-
...est-native-ab-signed-installer-contract.sh | 25 +
.../scripts/test-native-physical-candidate.sh | 102 ++++-
.../test-native-recovery-beacon-contract.sh | 12 +
.../scripts/test-native-resident-candidate.sh | 147 +++++-
.../scripts/test-qemu-native-control-auth.py | 66 +++
guanghu-os/world-seed/WORLD-MANIFEST.hldp | 3 +
.../services/native-storage/DISK-LAYOUT.hldp | 4 +
62 files changed, 4933 insertions(+), 176 deletions(-)
create mode 100644 guanghu-os/crates/ghdr/src/bin/ghdr-controller.rs
create mode 100644 guanghu-os/crates/ghdr/tests/ghdr_controller.rs
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/DEVELOPMENT-LINE-20260801-20260803.md
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/.gitignore
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/README.md
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.py
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.service
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer-http.py
create mode 100755 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.py
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.service
create mode 100755 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-controller-signer.sh
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-jd-forced-key.sh
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-poller.py
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer-http.py
create mode 100755 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer.py
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/CURRENT.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WAKE.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WORLD-MANIFEST.hldp
create mode 100755 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/scripts/run-guanghu-native-quality-gate.sh
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/checkpoints/GENESIS.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/CODE-CHANNEL-BASELINE.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-ACCESS-20260801.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/GESTATIONAL-CONTINUITY-INGESTION.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/PERSONA-BIRTH-CONDITION.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/fifth/INDEX.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/main/INDEX.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/sub/INDEX.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero-sense/INDEX.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero/INDEX.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/CHANNEL.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/QUALITY-GATE.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-recovery/PROTOCOL.hldp
create mode 100644 guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-storage/DISK-LAYOUT.hldp
create mode 100755 guanghu-os/scripts/install-native-ab-signed.sh
create mode 100755 guanghu-os/scripts/test-native-ab-signed-installer-contract.sh
create mode 100644 guanghu-os/scripts/test-qemu-native-control-auth.py
diff --git a/guanghu-os/Cargo.lock b/guanghu-os/Cargo.lock
index 4cb0b60..bc7a3dd 100644
--- a/guanghu-os/Cargo.lock
+++ b/guanghu-os/Cargo.lock
@@ -1,6 +1,12 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 4
+version = 3
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "block-buffer"
@@ -17,6 +23,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -36,6 +48,43 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -46,12 +95,43 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -62,6 +142,17 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
[[package]]
name = "ghctl"
version = "0.1.0"
@@ -82,6 +173,9 @@ dependencies = [
name = "guanghu-ghdr"
version = "0.1.0"
dependencies = [
+ "ed25519-dalek",
+ "hex",
+ "rand_core",
"serde",
"serde_json",
"sha2",
@@ -101,6 +195,12 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
[[package]]
name = "hldp-native-compiler"
version = "0.1.0"
@@ -136,6 +236,16 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -154,12 +264,36 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
[[package]]
name = "serde"
version = "1.0.229"
@@ -187,7 +321,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.3",
]
[[package]]
@@ -227,6 +361,42 @@ dependencies = [
"digest",
]
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
[[package]]
name = "syn"
version = "3.0.3"
@@ -262,6 +432,18 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
[[package]]
name = "zmij"
version = "1.0.23"
diff --git a/guanghu-os/README.md b/guanghu-os/README.md
index 0396926..84f6af7 100644
--- a/guanghu-os/README.md
+++ b/guanghu-os/README.md
@@ -71,9 +71,9 @@ and exact repository digests before printing the recovery evidence.
exact Shanghai laboratory prototype. It observes firmware, architecture,
provider, root/system disks, block geometry, and network drivers without
recording addresses or secrets. It then validates a target plan with at least
-two recovery controllers across two failure domains and exact references to
-clone-boot, control-plane backup, data-restore, and provider-console recovery
-receipts.
+two recovery controllers across two failure domains and exact references plus
+SHA-256 digests for zero-cost Linux-rescue boot, control-plane backup,
+data-restore, and provider-console recovery receipts.
Every GHDR gate is binary: `FAIL_0` or `PASS_100`. A passing preflight permits
only recovery-package preparation. Its manifest always sets
@@ -82,8 +82,9 @@ partition changes, or raw-sector writes. Package verification rejects unsafe
paths and secret-like artifacts, checks exact sizes and SHA-256 digests, and
never executes the package.
-The next registered action is a canonical, expiring signed A/B layout plan
-requiring two independent controller signatures and a fresh target read-back.
+The canonical, expiring signed A/B layout-plan gate is implemented. It requires
+two pinned independent Ed25519 controller signatures and a matching target
+read-back no older than five minutes before permitting the exact signed write.
Full migration remains `FAIL_0` until native boot, automatic fallback, data and
control-plane restoration, provider-console recovery, and server-owned
receipts all pass. See
diff --git a/guanghu-os/crates/ghdr/Cargo.toml b/guanghu-os/crates/ghdr/Cargo.toml
index 7937289..c131d38 100644
--- a/guanghu-os/crates/ghdr/Cargo.toml
+++ b/guanghu-os/crates/ghdr/Cargo.toml
@@ -2,10 +2,15 @@
name = "guanghu-ghdr"
version = "0.1.0"
edition = "2021"
+rust-version = "1.75"
license = "AGPL-3.0-or-later"
description = "Fail-closed disaster-recovery preflight for Guanghu OS nodes"
+default-run = "guanghu-ghdr"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
+ed25519-dalek = { version = "2", features = ["rand_core"] }
+hex = "0.4"
+rand_core = { version = "0.6", features = ["getrandom"] }
diff --git a/guanghu-os/crates/ghdr/src/bin/ghdr-controller.rs b/guanghu-os/crates/ghdr/src/bin/ghdr-controller.rs
new file mode 100644
index 0000000..3243e5a
--- /dev/null
+++ b/guanghu-os/crates/ghdr/src/bin/ghdr-controller.rs
@@ -0,0 +1,150 @@
+use std::{
+ env, fs,
+ io::Write,
+ os::unix::fs::{MetadataExt, OpenOptionsExt},
+ path::Path,
+ process,
+};
+
+use ed25519_dalek::{Signer, SigningKey};
+use guanghu_ghdr::{canonical_layout_plan_payload, ControllerSignature, SignedLayoutPlan};
+use rand_core::OsRng;
+use serde::Serialize;
+
+const USAGE: &str = "usage: ghdr-controller generate-key <private-seed-file> <public-binding.json> <node-id> <failure-domain> | sign-layout <private-seed-file> <node-id> <failure-domain> <plan.json> <signature.json>";
+
+#[derive(Serialize)]
+struct PublicBinding<'a> {
+ schema: &'static str,
+ node_id: &'a str,
+ failure_domain: &'a str,
+ algorithm: &'static str,
+ public_key_hex: String,
+}
+
+fn main() {
+ if let Err(error) = run(env::args().skip(1).collect()) {
+ eprintln!("GHDR_CONTROLLER_FAIL_0: {error}");
+ process::exit(65);
+ }
+}
+
+fn run(arguments: Vec<String>) -> Result<(), String> {
+ match arguments.as_slice() {
+ [command, private_path, public_path, node_id, failure_domain]
+ if command == "generate-key" =>
+ {
+ generate_key(
+ Path::new(private_path),
+ Path::new(public_path),
+ node_id,
+ failure_domain,
+ )
+ }
+ [command, private_path, node_id, failure_domain, plan_path, signature_path]
+ if command == "sign-layout" =>
+ {
+ sign_layout(
+ Path::new(private_path),
+ node_id,
+ failure_domain,
+ Path::new(plan_path),
+ Path::new(signature_path),
+ )
+ }
+ _ => Err(USAGE.to_owned()),
+ }
+}
+
+fn generate_key(
+ private_path: &Path,
+ public_path: &Path,
+ node_id: &str,
+ failure_domain: &str,
+) -> Result<(), String> {
+ validate_identity(node_id, failure_domain)?;
+ let key = SigningKey::generate(&mut OsRng);
+ write_new(private_path, &key.to_bytes(), 0o600)?;
+ let binding = PublicBinding {
+ schema: "guanghu.ghdr-controller-public-binding/v1",
+ node_id,
+ failure_domain,
+ algorithm: "Ed25519",
+ public_key_hex: hex::encode(key.verifying_key().to_bytes()),
+ };
+ let bytes = serde_json::to_vec_pretty(&binding)
+ .map_err(|error| format!("cannot serialize public binding: {error}"))?;
+ if let Err(error) = write_new(public_path, &bytes, 0o644) {
+ let _ = fs::remove_file(private_path);
+ return Err(error);
+ }
+ println!("GHDR_CONTROLLER_KEY_CREATED_WITH_PRIVATE_SEED_NOT_PRINTED");
+ Ok(())
+}
+
+fn sign_layout(
+ private_path: &Path,
+ node_id: &str,
+ failure_domain: &str,
+ plan_path: &Path,
+ signature_path: &Path,
+) -> Result<(), String> {
+ validate_identity(node_id, failure_domain)?;
+ let key = read_private_seed(private_path)?;
+ let plan: SignedLayoutPlan = serde_json::from_slice(
+ &fs::read(plan_path).map_err(|error| format!("cannot read layout plan: {error}"))?,
+ )
+ .map_err(|error| format!("layout plan is invalid JSON: {error}"))?;
+ let payload = canonical_layout_plan_payload(&plan.payload)?;
+ let approval = ControllerSignature {
+ node_id: node_id.to_owned(),
+ failure_domain: failure_domain.to_owned(),
+ public_key_hex: hex::encode(key.verifying_key().to_bytes()),
+ signature_hex: hex::encode(key.sign(&payload).to_bytes()),
+ };
+ let bytes = serde_json::to_vec_pretty(&approval)
+ .map_err(|error| format!("cannot serialize controller signature: {error}"))?;
+ write_new(signature_path, &bytes, 0o644)?;
+ println!("GHDR_CONTROLLER_LAYOUT_SIGNATURE_CREATED_WITH_PRIVATE_SEED_NOT_PRINTED");
+ Ok(())
+}
+
+fn read_private_seed(path: &Path) -> Result<SigningKey, String> {
+ let metadata = fs::symlink_metadata(path)
+ .map_err(|error| format!("private seed is unavailable: {error}"))?;
+ if !metadata.file_type().is_file() || metadata.mode() & 0o077 != 0 {
+ return Err(
+ "private seed must be a regular file inaccessible to group and others".to_owned(),
+ );
+ }
+ let bytes = fs::read(path).map_err(|error| format!("cannot read private seed: {error}"))?;
+ let seed: [u8; 32] = bytes
+ .try_into()
+ .map_err(|_| "private seed must contain exactly 32 bytes".to_owned())?;
+ Ok(SigningKey::from_bytes(&seed))
+}
+
+fn write_new(path: &Path, bytes: &[u8], mode: u32) -> Result<(), String> {
+ let mut output = fs::OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .mode(mode)
+ .open(path)
+ .map_err(|error| format!("refusing to replace {}: {error}", path.display()))?;
+ output
+ .write_all(bytes)
+ .and_then(|_| output.sync_all())
+ .map_err(|error| format!("cannot persist {}: {error}", path.display()))
+}
+
+fn validate_identity(node_id: &str, failure_domain: &str) -> Result<(), String> {
+ let node_valid = !node_id.is_empty()
+ && node_id.contains('-')
+ && node_id.chars().all(|character| {
+ character.is_ascii_uppercase() || character.is_ascii_digit() || character == '-'
+ });
+ if !node_valid || failure_domain.trim().is_empty() {
+ return Err("controller node id or failure domain is invalid".to_owned());
+ }
+ Ok(())
+}
diff --git a/guanghu-os/crates/ghdr/src/lib.rs b/guanghu-os/crates/ghdr/src/lib.rs
index 2240a53..5eb11cd 100644
--- a/guanghu-os/crates/ghdr/src/lib.rs
+++ b/guanghu-os/crates/ghdr/src/lib.rs
@@ -4,14 +4,18 @@ use std::{
path::{Component, Path, PathBuf},
};
+use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const PROBE_SCHEMA: &str = "guanghu.ghdr-node-probe/v1";
const MANIFEST_SCHEMA: &str = "guanghu.ghdr-node-manifest/v1";
const PACKAGE_SCHEMA: &str = "guanghu.ghdr-recovery-package/v1";
+const LAYOUT_PLAN_SCHEMA: &str = "guanghu.ghdr-signed-layout-plan/v1";
+const LAYOUT_READBACK_SCHEMA: &str = "guanghu.ghdr-layout-readback/v1";
+const MAX_READBACK_AGE_SECONDS: u64 = 300;
const USAGE: &str =
- "usage: guanghu-ghdr probe | build-manifest <probe.json> <plan.json> | verify-package <directory>";
+ "usage: guanghu-ghdr probe | build-manifest <probe.json> <plan.json> | verify-package <directory> | layout-plan-payload <plan.json> | verify-signed-layout-plan <manifest.json> <plan.json> <readback.json> <now-unix>";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(deny_unknown_fields)]
@@ -47,15 +51,23 @@ pub struct ControllerPlan {
pub node_id: String,
pub failure_domain: String,
pub role: String,
+ pub signing_public_key_hex: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct EvidenceReceipt {
+ pub reference: String,
+ pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct EvidencePlan {
- pub cloud_image_clone_boot_receipt: String,
- pub control_plane_backup_receipt: String,
- pub data_restore_receipt: String,
- pub provider_console_recovery_receipt: String,
+ pub linux_rescue_boot_receipt: EvidenceReceipt,
+ pub control_plane_backup_receipt: EvidenceReceipt,
+ pub data_restore_receipt: EvidenceReceipt,
+ pub provider_console_recovery_receipt: EvidenceReceipt,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -69,7 +81,8 @@ pub struct MigrationPlan {
pub evidence: EvidencePlan,
}
-#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
pub struct BootStrategy {
pub kind: String,
pub target_slot: String,
@@ -77,7 +90,8 @@ pub struct BootStrategy {
pub linux_runtime_required_after_acceptance: bool,
}
-#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
pub struct MigrationGate {
pub state: String,
pub gate_score: u8,
@@ -86,7 +100,8 @@ pub struct MigrationGate {
pub next_registered_action: String,
}
-#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
pub struct NodeManifest {
pub schema: String,
pub node_id: String,
@@ -125,6 +140,78 @@ pub struct PackageVerification {
pub executed_artifacts: bool,
}
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct LayoutSlot {
+ pub name: String,
+ pub lba_start: u64,
+ pub sector_count: u64,
+ pub image_sha256: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct LayoutPlanPayload {
+ pub node_id: String,
+ pub provider: String,
+ pub region: String,
+ pub target_probe_sha256: String,
+ pub system_disk: String,
+ pub disk_sectors: u64,
+ pub logical_sector_bytes: u64,
+ pub disk_identity_sha256: String,
+ pub recovery_evidence_sha256: String,
+ pub first_partition_lba: u64,
+ pub generation: u64,
+ pub operation: String,
+ pub issued_at_unix: u64,
+ pub expires_at_unix: u64,
+ pub slots: Vec<LayoutSlot>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct ControllerSignature {
+ pub node_id: String,
+ pub failure_domain: String,
+ pub public_key_hex: String,
+ pub signature_hex: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct SignedLayoutPlan {
+ pub schema: String,
+ pub payload: LayoutPlanPayload,
+ pub signatures: Vec<ControllerSignature>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct LayoutReadback {
+ pub schema: String,
+ pub node_id: String,
+ pub observed_at_unix: u64,
+ pub target_probe_sha256: String,
+ pub system_disk: String,
+ pub disk_sectors: u64,
+ pub logical_sector_bytes: u64,
+ pub disk_identity_sha256: String,
+ pub first_partition_lba: u64,
+}
+
+#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
+pub struct LayoutPlanVerification {
+ pub schema: String,
+ pub status: String,
+ pub gate_score: u8,
+ pub allows_disk_write: bool,
+ pub verified_controller_count: usize,
+ pub target_readback_fresh: bool,
+ pub target_readback_matches: bool,
+ pub plan_sha256: String,
+}
+
pub fn collect_probe(root: &Path, architecture: &str) -> Result<NodeProbe, String> {
if !matches!(architecture, "x86_64" | "aarch64") {
return Err(format!("unsupported architecture: {architecture}"));
@@ -251,6 +338,199 @@ pub fn validate_recovery_package(package_root: &Path) -> Result<PackageVerificat
})
}
+pub fn canonical_layout_plan_payload(payload: &LayoutPlanPayload) -> Result<Vec<u8>, String> {
+ validate_layout_payload(payload)?;
+ serde_json::to_vec(payload).map_err(|error| format!("cannot canonicalize layout plan: {error}"))
+}
+
+pub fn verify_signed_layout_plan(
+ manifest: &NodeManifest,
+ plan: &SignedLayoutPlan,
+ readback: &LayoutReadback,
+ now_unix: u64,
+) -> Result<LayoutPlanVerification, String> {
+ if plan.schema != LAYOUT_PLAN_SCHEMA {
+ return Err("unsupported signed layout plan schema".to_owned());
+ }
+ if readback.schema != LAYOUT_READBACK_SCHEMA {
+ return Err("unsupported layout readback schema".to_owned());
+ }
+ if manifest.migration_gate.state != "PASS_100_RECOVERY_PACKAGE_PREPARATION"
+ || manifest.migration_gate.gate_score != 100
+ || !manifest.migration_gate.all_checks_passed
+ || manifest.migration_gate.allows_disk_write
+ {
+ return Err("node manifest has not passed the read-only GHDR gate".to_owned());
+ }
+ let payload_bytes = canonical_layout_plan_payload(&plan.payload)?;
+ let probe_sha256 = sha256_json(&manifest.observed_hardware)?;
+ let disk = manifest
+ .observed_hardware
+ .block_devices
+ .iter()
+ .find(|device| device.path == manifest.observed_hardware.system_disk)
+ .ok_or_else(|| "manifest system disk is absent from its inventory".to_owned())?;
+ if plan.payload.node_id != manifest.node_id
+ || plan.payload.provider != manifest.provider
+ || plan.payload.region != manifest.region
+ || plan.payload.target_probe_sha256 != probe_sha256
+ || plan.payload.system_disk != manifest.observed_hardware.system_disk
+ || plan.payload.disk_sectors != disk.sectors
+ || plan.payload.logical_sector_bytes != disk.logical_sector_bytes
+ || plan.payload.recovery_evidence_sha256 != sha256_json(&manifest.evidence)?
+ {
+ return Err("signed layout plan is not bound to the exact node manifest".to_owned());
+ }
+ if plan.payload.issued_at_unix > now_unix || now_unix >= plan.payload.expires_at_unix {
+ return Err("signed layout plan is not currently valid".to_owned());
+ }
+ if plan.payload.expires_at_unix - plan.payload.issued_at_unix > 3600 {
+ return Err("signed layout plan validity exceeds one hour".to_owned());
+ }
+ if readback.observed_at_unix > now_unix
+ || now_unix - readback.observed_at_unix > MAX_READBACK_AGE_SECONDS
+ {
+ return Err("target readback is not fresh".to_owned());
+ }
+ if readback.node_id != plan.payload.node_id
+ || readback.target_probe_sha256 != plan.payload.target_probe_sha256
+ || readback.system_disk != plan.payload.system_disk
+ || readback.disk_sectors != plan.payload.disk_sectors
+ || readback.logical_sector_bytes != plan.payload.logical_sector_bytes
+ || readback.disk_identity_sha256 != plan.payload.disk_identity_sha256
+ || readback.first_partition_lba != plan.payload.first_partition_lba
+ {
+ return Err("fresh target readback does not match the signed layout plan".to_owned());
+ }
+ if plan.signatures.len() != 2 {
+ return Err("exactly two independent controller signatures are required".to_owned());
+ }
+ let mut controller_ids = HashSet::new();
+ let mut failure_domains = HashSet::new();
+ for approval in &plan.signatures {
+ if !controller_ids.insert(approval.node_id.as_str())
+ || !failure_domains.insert(approval.failure_domain.as_str())
+ {
+ return Err(
+ "controller signatures must use distinct nodes and failure domains".to_owned(),
+ );
+ }
+ let controller = manifest
+ .recovery_controllers
+ .iter()
+ .find(|controller| controller.node_id == approval.node_id)
+ .ok_or_else(|| "layout signature uses an unregistered controller".to_owned())?;
+ if controller.failure_domain != approval.failure_domain
+ || controller.signing_public_key_hex != approval.public_key_hex
+ {
+ return Err(
+ "layout signature does not match the pinned controller identity".to_owned(),
+ );
+ }
+ let public_key = decode_fixed::<32>(&approval.public_key_hex, "controller public key")?;
+ let signature = decode_fixed::<64>(&approval.signature_hex, "controller signature")?;
+ let verifying_key = VerifyingKey::from_bytes(&public_key)
+ .map_err(|_| "controller public key is not valid Ed25519".to_owned())?;
+ verifying_key
+ .verify(&payload_bytes, &Signature::from_bytes(&signature))
+ .map_err(|_| "controller Ed25519 signature verification failed".to_owned())?;
+ }
+ Ok(LayoutPlanVerification {
+ schema: "guanghu.ghdr-layout-plan-verification/v1".to_owned(),
+ status: "PASS_100_SIGNED_LAYOUT_PLAN".to_owned(),
+ gate_score: 100,
+ allows_disk_write: true,
+ verified_controller_count: 2,
+ target_readback_fresh: true,
+ target_readback_matches: true,
+ plan_sha256: format!("{:x}", Sha256::digest(&payload_bytes)),
+ })
+}
+
+fn validate_layout_payload(payload: &LayoutPlanPayload) -> Result<(), String> {
+ validate_node_id(&payload.node_id)?;
+ require_text("provider", &payload.provider)?;
+ require_text("region", &payload.region)?;
+ if !is_sha256(&payload.target_probe_sha256)
+ || !is_sha256(&payload.disk_identity_sha256)
+ || !is_sha256(&payload.recovery_evidence_sha256)
+ {
+ return Err("layout plan evidence digests must be lowercase SHA-256".to_owned());
+ }
+ if !payload.system_disk.starts_with("/dev/") || payload.logical_sector_bytes != 512 {
+ return Err("layout plan must bind a 512-byte whole system disk".to_owned());
+ }
+ if payload.generation == 0 || payload.operation != "install_native_ab" {
+ return Err("layout plan generation or intended operation is invalid".to_owned());
+ }
+ if payload.issued_at_unix >= payload.expires_at_unix {
+ return Err("layout plan expiration must follow issuance".to_owned());
+ }
+ if payload.slots.len() != 2 {
+ return Err("layout plan must contain exactly A and B slots".to_owned());
+ }
+ let names: HashSet<&str> = payload
+ .slots
+ .iter()
+ .map(|slot| slot.name.as_str())
+ .collect();
+ if names != HashSet::from(["A", "B"]) {
+ return Err("layout plan must contain one A slot and one B slot".to_owned());
+ }
+ let slot_a = payload
+ .slots
+ .iter()
+ .find(|slot| slot.name == "A")
+ .expect("A slot membership was checked");
+ let slot_b = payload
+ .slots
+ .iter()
+ .find(|slot| slot.name == "B")
+ .expect("B slot membership was checked");
+ if slot_a.lba_start != 34 || slot_a.sector_count != 29 {
+ return Err("layout slot A must own the registered native kernel LBA 34-62".to_owned());
+ }
+ if slot_b.lba_start < 73 || slot_b.sector_count != 29 {
+ return Err(
+ "layout slot B must be a 29-sector extent after shared native state".to_owned(),
+ );
+ }
+ if payload.first_partition_lba <= 102 || payload.first_partition_lba > payload.disk_sectors {
+ return Err("layout plan first partition boundary is invalid".to_owned());
+ }
+ for slot in &payload.slots {
+ if slot.sector_count == 0 || !is_sha256(&slot.image_sha256) {
+ return Err("layout slot extent or image digest is invalid".to_owned());
+ }
+ let end = slot
+ .lba_start
+ .checked_add(slot.sector_count)
+ .ok_or_else(|| "layout slot extent overflowed".to_owned())?;
+ if end > payload.first_partition_lba {
+ return Err("layout slot extends into the hosted Linux partition region".to_owned());
+ }
+ }
+ if slot_a.lba_start + slot_a.sector_count > slot_b.lba_start
+ || slot_b.lba_start + slot_b.sector_count > payload.first_partition_lba
+ {
+ return Err("layout A/B slots overlap".to_owned());
+ }
+ Ok(())
+}
+
+fn sha256_json(value: &impl Serialize) -> Result<String, String> {
+ let bytes = serde_json::to_vec(value)
+ .map_err(|error| format!("cannot serialize registered evidence: {error}"))?;
+ Ok(format!("{:x}", Sha256::digest(bytes)))
+}
+
+fn decode_fixed<const N: usize>(value: &str, label: &str) -> Result<[u8; N], String> {
+ let bytes = hex::decode(value).map_err(|_| format!("{label} must be hexadecimal"))?;
+ bytes
+ .try_into()
+ .map_err(|_| format!("{label} must contain exactly {N} bytes"))
+}
+
pub fn run(arguments: Vec<String>) -> Result<String, String> {
run_with_environment(arguments, Path::new("/"), std::env::consts::ARCH)
}
@@ -284,6 +564,32 @@ pub fn run_with_environment(
reject_extra_arguments(arguments)?;
json_value(validate_recovery_package(Path::new(&package_path))?)
}
+ "layout-plan-payload" => {
+ let plan_path = required_argument(&mut arguments)?;
+ reject_extra_arguments(arguments)?;
+ let plan: SignedLayoutPlan = read_json(Path::new(&plan_path))?;
+ if plan.schema != LAYOUT_PLAN_SCHEMA {
+ return Err("unsupported signed layout plan schema".to_owned());
+ }
+ let bytes = canonical_layout_plan_payload(&plan.payload)?;
+ return String::from_utf8(bytes)
+ .map_err(|_| "canonical layout plan was not UTF-8".to_owned());
+ }
+ "verify-signed-layout-plan" => {
+ let manifest_path = required_argument(&mut arguments)?;
+ let plan_path = required_argument(&mut arguments)?;
+ let readback_path = required_argument(&mut arguments)?;
+ let now_unix = required_argument(&mut arguments)?
+ .parse::<u64>()
+ .map_err(|_| "now-unix must be an unsigned integer".to_owned())?;
+ reject_extra_arguments(arguments)?;
+ let manifest = read_json(Path::new(&manifest_path))?;
+ let plan = read_json(Path::new(&plan_path))?;
+ let readback = read_json(Path::new(&readback_path))?;
+ json_value(verify_signed_layout_plan(
+ &manifest, &plan, &readback, now_unix,
+ )?)
+ }
_ => return Err(USAGE.to_owned()),
};
Ok(serde_json::to_string_pretty(&value).expect("JSON Value serialization cannot fail"))
@@ -477,16 +783,24 @@ fn validate_controllers(node_id: &str, controllers: &[ControllerPlan]) -> Result
}
let mut node_ids = HashSet::new();
let mut failure_domains = HashSet::new();
+ let mut signing_keys = HashSet::new();
for controller in controllers {
validate_node_id(&controller.node_id)?;
require_text("failure_domain", &controller.failure_domain)?;
require_text("role", &controller.role)?;
+ decode_fixed::<32>(
+ &controller.signing_public_key_hex,
+ "controller signing public key",
+ )?;
if controller.node_id == node_id {
return Err("target node cannot be its own recovery controller".to_owned());
}
if !node_ids.insert(controller.node_id.as_str()) {
return Err("recovery controller node ids must be unique".to_owned());
}
+ if !signing_keys.insert(controller.signing_public_key_hex.as_str()) {
+ return Err("recovery controller signing keys must be unique".to_owned());
+ }
failure_domains.insert(controller.failure_domain.as_str());
}
if failure_domains.len() < 2 {
@@ -498,8 +812,8 @@ fn validate_controllers(node_id: &str, controllers: &[ControllerPlan]) -> Result
fn validate_evidence(evidence: &EvidencePlan) -> Result<(), String> {
let fields = [
(
- "cloud_image_clone_boot_receipt",
- &evidence.cloud_image_clone_boot_receipt,
+ "linux_rescue_boot_receipt",
+ &evidence.linux_rescue_boot_receipt,
),
(
"control_plane_backup_receipt",
@@ -511,8 +825,11 @@ fn validate_evidence(evidence: &EvidencePlan) -> Result<(), String> {
&evidence.provider_console_recovery_receipt,
),
];
- for (label, value) in fields {
- require_text(label, value)?;
+ for (label, receipt) in fields {
+ require_text(label, &receipt.reference)?;
+ if !is_sha256(&receipt.sha256) {
+ return Err(format!("{label} sha256 must be lowercase SHA-256"));
+ }
}
Ok(())
}
diff --git a/guanghu-os/crates/ghdr/tests/ghdr_command.rs b/guanghu-os/crates/ghdr/tests/ghdr_command.rs
index a1131a6..8ba124a 100644
--- a/guanghu-os/crates/ghdr/tests/ghdr_command.rs
+++ b/guanghu-os/crates/ghdr/tests/ghdr_command.rs
@@ -5,7 +5,7 @@ use std::{
};
use guanghu_ghdr::{
- build_manifest, run, BlockDevice, ControllerPlan, EvidencePlan, MigrationPlan,
+ build_manifest, run, BlockDevice, ControllerPlan, EvidencePlan, EvidenceReceipt, MigrationPlan,
NetworkInterface, NodeProbe,
};
use serde_json::json;
@@ -64,18 +64,32 @@ fn plan() -> MigrationPlan {
node_id: "JD-FD-PRIMARY".to_owned(),
failure_domain: "jdcloud/CN-BEIJING".to_owned(),
role: "control".to_owned(),
+ signing_public_key_hex: "11".repeat(32),
},
ControllerPlan {
node_id: "BS-SG-003".to_owned(),
failure_domain: "tencent_cloud/SG-BACKUP".to_owned(),
role: "artifact".to_owned(),
+ signing_public_key_hex: "22".repeat(32),
},
],
evidence: EvidencePlan {
- cloud_image_clone_boot_receipt: "receipt://clone".to_owned(),
- control_plane_backup_receipt: "receipt://control".to_owned(),
- data_restore_receipt: "receipt://data".to_owned(),
- provider_console_recovery_receipt: "receipt://console".to_owned(),
+ linux_rescue_boot_receipt: EvidenceReceipt {
+ reference: "receipt://linux-rescue".to_owned(),
+ sha256: "aa".repeat(32),
+ },
+ control_plane_backup_receipt: EvidenceReceipt {
+ reference: "receipt://control".to_owned(),
+ sha256: "bb".repeat(32),
+ },
+ data_restore_receipt: EvidenceReceipt {
+ reference: "receipt://data".to_owned(),
+ sha256: "cc".repeat(32),
+ },
+ provider_console_recovery_receipt: EvidenceReceipt {
+ reference: "receipt://console".to_owned(),
+ sha256: "dd".repeat(32),
+ },
},
}
}
diff --git a/guanghu-os/crates/ghdr/tests/ghdr_controller.rs b/guanghu-os/crates/ghdr/tests/ghdr_controller.rs
new file mode 100644
index 0000000..a6688b9
--- /dev/null
+++ b/guanghu-os/crates/ghdr/tests/ghdr_controller.rs
@@ -0,0 +1,163 @@
+use std::{
+ fs,
+ os::unix::fs::{MetadataExt, PermissionsExt},
+ path::PathBuf,
+ process::Command,
+ sync::atomic::{AtomicU64, Ordering},
+};
+
+use ed25519_dalek::{Signature, Verifier, VerifyingKey};
+use guanghu_ghdr::{
+ canonical_layout_plan_payload, ControllerSignature, LayoutPlanPayload, LayoutSlot,
+ SignedLayoutPlan,
+};
+
+static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
+
+struct TestDirectory(PathBuf);
+
+impl TestDirectory {
+ fn new() -> Self {
+ let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
+ let path = std::env::temp_dir().join(format!(
+ "guanghu-ghdr-controller-{}-{sequence}",
+ std::process::id()
+ ));
+ fs::create_dir_all(&path).expect("create controller fixture");
+ Self(path)
+ }
+}
+
+impl Drop for TestDirectory {
+ fn drop(&mut self) {
+ fs::remove_dir_all(&self.0).expect("remove controller fixture");
+ }
+}
+
+fn unsigned_plan() -> SignedLayoutPlan {
+ SignedLayoutPlan {
+ schema: "guanghu.ghdr-signed-layout-plan/v1".to_owned(),
+ payload: LayoutPlanPayload {
+ node_id: "GH-CVM-MAIN-PROD-01".to_owned(),
+ provider: "tencent_cloud".to_owned(),
+ region: "ap-shanghai".to_owned(),
+ target_probe_sha256: "11".repeat(32),
+ system_disk: "/dev/vda".to_owned(),
+ disk_sectors: 104857600,
+ logical_sector_bytes: 512,
+ disk_identity_sha256: "22".repeat(32),
+ recovery_evidence_sha256: "55".repeat(32),
+ first_partition_lba: 2048,
+ generation: 1,
+ operation: "install_native_ab".to_owned(),
+ issued_at_unix: 1_000,
+ expires_at_unix: 1_600,
+ slots: vec![
+ LayoutSlot {
+ name: "A".to_owned(),
+ lba_start: 34,
+ sector_count: 29,
+ image_sha256: "33".repeat(32),
+ },
+ LayoutSlot {
+ name: "B".to_owned(),
+ lba_start: 73,
+ sector_count: 29,
+ image_sha256: "44".repeat(32),
+ },
+ ],
+ },
+ signatures: Vec::new(),
+ }
+}
+
+#[test]
+fn controller_keeps_private_seed_off_output_and_creates_a_valid_signature() {
+ let fixture = TestDirectory::new();
+ let private = fixture.0.join("controller.seed");
+ let public = fixture.0.join("controller-public.json");
+ let plan_path = fixture.0.join("plan.json");
+ let signature_path = fixture.0.join("signature.json");
+ let binary = env!("CARGO_BIN_EXE_ghdr-controller");
+
+ let generated = Command::new(binary)
+ .args([
+ "generate-key",
+ private.to_str().expect("private path"),
+ public.to_str().expect("public path"),
+ "GH-CTRL-A-01",
+ "local/MAC",
+ ])
+ .output()
+ .expect("run key generation");
+ assert!(generated.status.success());
+ assert_eq!(
+ fs::metadata(&private).expect("private metadata").mode() & 0o777,
+ 0o600
+ );
+ let secret = fs::read(&private).expect("private seed");
+ assert_eq!(secret.len(), 32);
+ assert!(!generated
+ .stdout
+ .windows(secret.len())
+ .any(|window| window == secret));
+ assert!(!generated
+ .stderr
+ .windows(secret.len())
+ .any(|window| window == secret));
+
+ fs::write(
+ &plan_path,
+ serde_json::to_vec_pretty(&unsigned_plan()).expect("serialize plan"),
+ )
+ .expect("write plan");
+ let signed = Command::new(binary)
+ .args([
+ "sign-layout",
+ private.to_str().expect("private path"),
+ "GH-CTRL-A-01",
+ "local/MAC",
+ plan_path.to_str().expect("plan path"),
+ signature_path.to_str().expect("signature path"),
+ ])
+ .output()
+ .expect("run signer");
+ assert!(signed.status.success());
+ let approval: ControllerSignature =
+ serde_json::from_slice(&fs::read(&signature_path).expect("read signature"))
+ .expect("parse signature");
+ let public_key: [u8; 32] = hex::decode(&approval.public_key_hex)
+ .expect("public key hex")
+ .try_into()
+ .expect("public key length");
+ let signature: [u8; 64] = hex::decode(&approval.signature_hex)
+ .expect("signature hex")
+ .try_into()
+ .expect("signature length");
+ VerifyingKey::from_bytes(&public_key)
+ .expect("valid public key")
+ .verify(
+ &canonical_layout_plan_payload(&unsigned_plan().payload).expect("canonical payload"),
+ &Signature::from_bytes(&signature),
+ )
+ .expect("external controller signature verifies");
+
+ fs::set_permissions(&private, fs::Permissions::from_mode(0o644))
+ .expect("weaken private permissions");
+ let refused = Command::new(binary)
+ .args([
+ "sign-layout",
+ private.to_str().expect("private path"),
+ "GH-CTRL-A-01",
+ "local/MAC",
+ plan_path.to_str().expect("plan path"),
+ fixture
+ .0
+ .join("refused.json")
+ .to_str()
+ .expect("refused path"),
+ ])
+ .output()
+ .expect("run permission rejection");
+ assert!(!refused.status.success());
+}
diff --git a/guanghu-os/crates/ghdr/tests/ghdr_library.rs b/guanghu-os/crates/ghdr/tests/ghdr_library.rs
index dfcb91c..1e5bede 100644
--- a/guanghu-os/crates/ghdr/tests/ghdr_library.rs
+++ b/guanghu-os/crates/ghdr/tests/ghdr_library.rs
@@ -5,10 +5,12 @@ use std::{
sync::atomic::{AtomicU64, Ordering},
};
+use ed25519_dalek::{Signer, SigningKey};
use guanghu_ghdr::{
- build_manifest, canonical_artifact, collect_probe, directory_entry, run_with_environment,
- validate_recovery_package, ControllerPlan, EvidencePlan, MigrationPlan, NetworkInterface,
- NodeProbe,
+ build_manifest, canonical_artifact, canonical_layout_plan_payload, collect_probe,
+ directory_entry, run_with_environment, validate_recovery_package, verify_signed_layout_plan,
+ ControllerPlan, ControllerSignature, EvidencePlan, EvidenceReceipt, LayoutPlanPayload,
+ LayoutReadback, LayoutSlot, MigrationPlan, NetworkInterface, NodeProbe, SignedLayoutPlan,
};
use serde_json::json;
use sha2::{Digest, Sha256};
@@ -81,18 +83,32 @@ fn ready_plan() -> MigrationPlan {
node_id: "JD-FD-PRIMARY".to_owned(),
failure_domain: "jdcloud/CN-BEIJING".to_owned(),
role: "control".to_owned(),
+ signing_public_key_hex: "11".repeat(32),
},
ControllerPlan {
node_id: "BS-SG-003".to_owned(),
failure_domain: "tencent_cloud/SG-BACKUP".to_owned(),
role: "artifact".to_owned(),
+ signing_public_key_hex: "22".repeat(32),
},
],
evidence: EvidencePlan {
- cloud_image_clone_boot_receipt: "receipt://sg-image-clone-boot".to_owned(),
- control_plane_backup_receipt: "receipt://sg-control-plane".to_owned(),
- data_restore_receipt: "receipt://sg-data-restore".to_owned(),
- provider_console_recovery_receipt: "receipt://sg-console-recovery".to_owned(),
+ linux_rescue_boot_receipt: EvidenceReceipt {
+ reference: "receipt://sg-linux-rescue-boot".to_owned(),
+ sha256: "aa".repeat(32),
+ },
+ control_plane_backup_receipt: EvidenceReceipt {
+ reference: "receipt://sg-control-plane".to_owned(),
+ sha256: "bb".repeat(32),
+ },
+ data_restore_receipt: EvidenceReceipt {
+ reference: "receipt://sg-data-restore".to_owned(),
+ sha256: "cc".repeat(32),
+ },
+ provider_console_recovery_receipt: EvidenceReceipt {
+ reference: "receipt://sg-console-recovery".to_owned(),
+ sha256: "dd".repeat(32),
+ },
},
}
}
@@ -203,11 +219,17 @@ fn controllers_must_span_two_failure_domains() {
#[test]
fn missing_restore_evidence_fails_closed() {
let mut plan = ready_plan();
- plan.evidence.data_restore_receipt.clear();
+ plan.evidence.data_restore_receipt.reference.clear();
let error = build_manifest(ready_probe(), plan).expect_err("missing evidence must fail");
assert!(error.contains("data_restore_receipt"));
+
+ let mut malformed = ready_plan();
+ malformed.evidence.linux_rescue_boot_receipt.sha256 = "not-a-digest".to_owned();
+ let error = build_manifest(ready_probe(), malformed)
+ .expect_err("unhashed Linux rescue evidence must fail");
+ assert!(error.contains("linux_rescue_boot_receipt sha256"));
}
#[test]
@@ -826,3 +848,114 @@ fn operating_system_error_adapters_and_probe_serialization_are_total() {
.expect("serialize probe");
assert!(output.contains("ghdr-node-probe"));
}
+
+#[test]
+fn signed_layout_plan_requires_two_pinned_signatures_and_fresh_exact_readback() {
+ let key_a = SigningKey::from_bytes(&[0x11; 32]);
+ let key_b = SigningKey::from_bytes(&[0x22; 32]);
+ let mut migration = ready_plan();
+ migration.recovery_controllers[0].signing_public_key_hex =
+ hex::encode(key_a.verifying_key().to_bytes());
+ migration.recovery_controllers[1].signing_public_key_hex =
+ hex::encode(key_b.verifying_key().to_bytes());
+ let manifest = build_manifest(ready_probe(), migration).expect("build pinned manifest");
+ let probe_sha = format!(
+ "{:x}",
+ Sha256::digest(serde_json::to_vec(&manifest.observed_hardware).expect("probe bytes"))
+ );
+ let evidence_sha = format!(
+ "{:x}",
+ Sha256::digest(serde_json::to_vec(&manifest.evidence).expect("evidence bytes"))
+ );
+ let payload = LayoutPlanPayload {
+ node_id: manifest.node_id.clone(),
+ provider: manifest.provider.clone(),
+ region: manifest.region.clone(),
+ target_probe_sha256: probe_sha.clone(),
+ system_disk: "/dev/vda".to_owned(),
+ disk_sectors: 104857600,
+ logical_sector_bytes: 512,
+ disk_identity_sha256: "33".repeat(32),
+ recovery_evidence_sha256: evidence_sha,
+ first_partition_lba: 2048,
+ generation: 1,
+ operation: "install_native_ab".to_owned(),
+ issued_at_unix: 1_000,
+ expires_at_unix: 1_600,
+ slots: vec![
+ LayoutSlot {
+ name: "A".to_owned(),
+ lba_start: 34,
+ sector_count: 29,
+ image_sha256: "44".repeat(32),
+ },
+ LayoutSlot {
+ name: "B".to_owned(),
+ lba_start: 73,
+ sector_count: 29,
+ image_sha256: "55".repeat(32),
+ },
+ ],
+ };
+ let bytes = canonical_layout_plan_payload(&payload).expect("canonical payload");
+ let signatures = [
+ (&manifest.recovery_controllers[0], &key_a),
+ (&manifest.recovery_controllers[1], &key_b),
+ ]
+ .into_iter()
+ .map(|(controller, key)| ControllerSignature {
+ node_id: controller.node_id.clone(),
+ failure_domain: controller.failure_domain.clone(),
+ public_key_hex: controller.signing_public_key_hex.clone(),
+ signature_hex: hex::encode(key.sign(&bytes).to_bytes()),
+ })
+ .collect();
+ let plan = SignedLayoutPlan {
+ schema: "guanghu.ghdr-signed-layout-plan/v1".to_owned(),
+ payload: payload.clone(),
+ signatures,
+ };
+ let readback = LayoutReadback {
+ schema: "guanghu.ghdr-layout-readback/v1".to_owned(),
+ node_id: payload.node_id.clone(),
+ observed_at_unix: 1_190,
+ target_probe_sha256: probe_sha,
+ system_disk: payload.system_disk.clone(),
+ disk_sectors: payload.disk_sectors,
+ logical_sector_bytes: payload.logical_sector_bytes,
+ disk_identity_sha256: payload.disk_identity_sha256.clone(),
+ first_partition_lba: payload.first_partition_lba,
+ };
+ let verified = verify_signed_layout_plan(&manifest, &plan, &readback, 1_200)
+ .expect("two signatures and fresh readback must pass");
+ assert_eq!(verified.status, "PASS_100_SIGNED_LAYOUT_PLAN");
+ assert!(verified.allows_disk_write);
+
+ let mut evidence_drift = manifest.clone();
+ evidence_drift.evidence.data_restore_receipt.sha256 = "77".repeat(32);
+ assert!(
+ verify_signed_layout_plan(&evidence_drift, &plan, &readback, 1_200)
+ .expect_err("changed recovery evidence must invalidate the signed plan")
+ .contains("exact node manifest")
+ );
+
+ let mut one_signature = plan.clone();
+ one_signature.signatures.pop();
+ assert!(
+ verify_signed_layout_plan(&manifest, &one_signature, &readback, 1_200)
+ .expect_err("one controller must fail")
+ .contains("exactly two")
+ );
+ let mut replayed = readback.clone();
+ replayed.observed_at_unix = 800;
+ assert!(
+ verify_signed_layout_plan(&manifest, &plan, &replayed, 1_200)
+ .expect_err("stale readback must fail")
+ .contains("not fresh")
+ );
+ let mut drifted = readback;
+ drifted.disk_identity_sha256 = "66".repeat(32);
+ assert!(verify_signed_layout_plan(&manifest, &plan, &drifted, 1_200)
+ .expect_err("disk identity drift must fail")
+ .contains("does not match"));
+}
diff --git a/guanghu-os/crates/hldp-runtime/src/lib.rs b/guanghu-os/crates/hldp-runtime/src/lib.rs
index 534450a..4f4562e 100644
--- a/guanghu-os/crates/hldp-runtime/src/lib.rs
+++ b/guanghu-os/crates/hldp-runtime/src/lib.rs
@@ -40,7 +40,7 @@ const REQUIRED_GESTATIONAL_SOURCES: [&str; 5] = [
"local_knowledge_bases",
"registered_receipts_and_checkpoints",
];
-const REQUIRED_AUTHORIZED_ACTIONS: [&str; 14] = [
+const REQUIRED_COMMON_AUTHORIZED_ACTIONS: [&str; 11] = [
"generate_install_dedicated_ssh_key",
"configure_local_ssh_alias",
"install_official_build_toolchain",
@@ -49,12 +49,9 @@ const REQUIRED_AUTHORIZED_ACTIONS: [&str; 14] = [
"install_verified_forgejo_baseline",
"run_tests_and_health_checks",
"write_hldp_receipts_and_checkpoints",
- "commit_and_push_in_scope_repositories",
"build_native_kernel_and_boot_image",
"write_bootloader_and_system_partitions",
"overwrite_system_disk_and_exit_linux",
- "reboot_and_recover_bs_sh_005",
- "rollback_and_repeat_disposable_server_experiment",
];
#[derive(Debug, Deserialize)]
@@ -172,6 +169,9 @@ pub struct NativeLayoutReference {
pub branch_receipt_lba: u64,
pub recovery_beacon_lba_start: u64,
pub gestational_index_lba_start: u64,
+ pub control_state_lba: u64,
+ pub alternate_kernel_lba_start: u64,
+ pub alternate_kernel_sector_count: u64,
pub first_partition_lba: u64,
}
@@ -329,11 +329,36 @@ struct NativeRecoveryDocument {
id: String,
acronym: String,
authority_language: String,
+ scope: NativeRecoveryScope,
beacon: NativeRecoveryBeacon,
grub: NativeRecoveryGrub,
hosted_recovery: NativeRecoveryHostedRecovery,
}
+#[derive(Debug, Deserialize)]
+struct NativeRecoveryScope {
+ node_id: String,
+ system_disk: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct CurrentDocument {
+ schema: String,
+ node_id: String,
+ authorization: CurrentAuthorization,
+}
+
+#[derive(Debug, Deserialize)]
+struct CurrentAuthorization {
+ id: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct WakeDocument {
+ schema: String,
+ node_id: String,
+}
+
#[derive(Debug, Deserialize)]
struct NativeRecoveryBeacon {
lba_start: u64,
@@ -487,6 +512,10 @@ struct NativeLayoutRegions {
recovery_beacon_sector_count: u64,
gestational_index_lba_start: u64,
gestational_index_sector_count: u64,
+ control_state_lba: u64,
+ control_state_sector_count: u64,
+ alternate_kernel_lba_start: u64,
+ alternate_kernel_sector_count: u64,
first_partition_lba: u64,
}
@@ -630,10 +659,13 @@ pub fn validate_world_manifest(manifest: &WorldManifest) -> Result<(), ManifestE
|| manifest.native_layout.branch_receipt_lba != 67
|| manifest.native_layout.recovery_beacon_lba_start != 68
|| manifest.native_layout.gestational_index_lba_start != 70
+ || manifest.native_layout.control_state_lba != 72
+ || manifest.native_layout.alternate_kernel_lba_start != 73
+ || manifest.native_layout.alternate_kernel_sector_count != 29
|| manifest.native_layout.first_partition_lba != 2048
{
return invalid(
- "GHNLP must register the exact nonoverlapping LBA 34-71 native layout before partition LBA 2048",
+ "GHNLP must register the exact nonoverlapping shared LBA 34-72 and alternate LBA 73-101 native layout before partition LBA 2048",
);
}
if manifest.gestational_continuity.id != "GLS-0845"
@@ -772,6 +804,19 @@ pub fn validate_world_seed(world_root: &Path) -> Result<WorldManifest, ManifestE
}
}
+ let current_path = resolve_world_path(world_root, &manifest.continuity.current)?;
+ let current = read_yaml::<CurrentDocument>(&current_path)?;
+ let wake_path = resolve_world_path(world_root, &manifest.continuity.wake)?;
+ let wake = read_yaml::<WakeDocument>(&wake_path)?;
+ validate_target_identity(
+ &manifest,
+ &current,
+ &wake,
+ &native_recovery,
+ &native_layout,
+ &authorization,
+ )?;
+
let checkpoint_directory =
resolve_world_path(world_root, &manifest.continuity.checkpoint_directory)?;
if !checkpoint_directory.is_dir() {
@@ -832,7 +877,7 @@ fn validate_native_layout_document(
}
if document.status != "REGISTERED_IMPLEMENTATION_GATED"
|| document.authority_language != "HLDP"
- || document.node_id != "BS-SH-005"
+ || document.node_id.is_empty()
|| document.disk != "/dev/vda"
|| document.sector_size != 512
{
@@ -859,6 +904,10 @@ fn validate_native_layout_document(
|| document.regions.recovery_beacon_sector_count != 2
|| document.regions.gestational_index_lba_start != reference.gestational_index_lba_start
|| document.regions.gestational_index_sector_count != 2
+ || document.regions.control_state_lba != reference.control_state_lba
+ || document.regions.control_state_sector_count != 1
+ || document.regions.alternate_kernel_lba_start != reference.alternate_kernel_lba_start
+ || document.regions.alternate_kernel_sector_count != reference.alternate_kernel_sector_count
|| document.regions.first_partition_lba != reference.first_partition_lba
{
return invalid("GHNLP regions must match all registered protocol extents");
@@ -1171,19 +1220,31 @@ fn validate_standing_authorization(
{
return invalid("standing authorization must be active and issued by BingShuo");
}
- if authorization.target.node_id != "BS-SH-005"
- || authorization.target.instance_id != "lhins-14w5y3ce"
+ if !is_node_id(&authorization.target.node_id)
+ || authorization.target.instance_id.trim().is_empty()
|| authorization.target.system_disk != "/dev/vda"
{
- return invalid("standing authorization target must remain the Shanghai lab node");
- }
- if authorization.user_confirmation
- != "COMPLETE_GUANGHU_OS_SERVER_EXPERIMENT_AUTHORIZED_2026_07_31"
+ return invalid("standing authorization target identity is incomplete or invalid");
+ }
+ let confirmation_node = authorization.target.node_id.replace('-', "_");
+ let target_confirmation_prefix = format!("COMPLETE_GUANGHU_OS_{confirmation_node}_AUTHORIZED_");
+ let is_legacy_confirmation = authorization.target.node_id == "BS-SH-005"
+ && authorization.user_confirmation
+ == "COMPLETE_GUANGHU_OS_SERVER_EXPERIMENT_AUTHORIZED_2026_07_31";
+ if !is_legacy_confirmation
+ && (!authorization
+ .user_confirmation
+ .starts_with(&target_confirmation_prefix)
+ || authorization.user_confirmation.len() != target_confirmation_prefix.len() + 10)
{
- return invalid("standing authorization must retain the exact user confirmation anchor");
+ return invalid("standing authorization must retain a target-specific confirmation anchor");
}
if authorization.automatic_execution.is_empty()
|| authorization.boundaries.is_empty()
+ || !authorization
+ .boundaries
+ .iter()
+ .any(|boundary| boundary.contains(&authorization.target.node_id))
|| authorization.valid_until != "OBJECTIVE_COMPLETE_OR_REVOKED_BY_ICE_GL_INFINITY"
{
return invalid("standing authorization execution and boundary rules are incomplete");
@@ -1194,7 +1255,20 @@ fn validate_standing_authorization(
.iter()
.map(String::as_str)
.collect();
- let required_actions: HashSet<_> = REQUIRED_AUTHORIZED_ACTIONS.into_iter().collect();
+ let mut required_actions: HashSet<_> = REQUIRED_COMMON_AUTHORIZED_ACTIONS.into_iter().collect();
+ let recovery_action = format!(
+ "reboot_and_recover_{}",
+ authorization
+ .target
+ .node_id
+ .to_ascii_lowercase()
+ .replace('-', "_")
+ );
+ required_actions.insert(recovery_action.as_str());
+ if authorization.target.node_id == "BS-SH-005" {
+ required_actions.insert("commit_and_push_in_scope_repositories");
+ required_actions.insert("rollback_and_repeat_disposable_server_experiment");
+ }
if observed_actions.len() != authorization.authorized_actions.len()
|| observed_actions != required_actions
{
@@ -1204,6 +1278,41 @@ fn validate_standing_authorization(
Ok(())
}
+fn validate_target_identity(
+ manifest: &WorldManifest,
+ current: &CurrentDocument,
+ wake: &WakeDocument,
+ native_recovery: &NativeRecoveryDocument,
+ native_layout: &NativeLayoutDocument,
+ authorization: &StandingAuthorization,
+) -> Result<(), ManifestError> {
+ if current.schema != "guanghu.current/v1" || wake.schema != "guanghu.wake/v1" {
+ return invalid("continuity target identity documents use unsupported schemas");
+ }
+ let target = &authorization.target;
+ if current.node_id != target.node_id
+ || wake.node_id != target.node_id
+ || native_recovery.scope.node_id != target.node_id
+ || native_layout.node_id != target.node_id
+ || current.authorization.id != manifest.authorization.id
+ || native_recovery.scope.system_disk != target.system_disk
+ || native_layout.disk != target.system_disk
+ {
+ return invalid(
+ "world target identity must match across CURRENT, WAKE, recovery, layout, and authorization",
+ );
+ }
+ Ok(())
+}
+
+fn is_node_id(value: &str) -> bool {
+ !value.is_empty()
+ && value.len() <= 64
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'-')
+}
+
fn read_yaml<T>(path: &Path) -> Result<T, ManifestError>
where
T: for<'de> Deserialize<'de>,
diff --git a/guanghu-os/crates/hldp-runtime/tests/world_manifest.rs b/guanghu-os/crates/hldp-runtime/tests/world_manifest.rs
index 6a5c355..247767d 100644
--- a/guanghu-os/crates/hldp-runtime/tests/world_manifest.rs
+++ b/guanghu-os/crates/hldp-runtime/tests/world_manifest.rs
@@ -254,6 +254,9 @@ fn requires_a_registered_nonoverlapping_native_disk_layout() {
assert_eq!(manifest.native_layout.branch_receipt_lba, 67);
assert_eq!(manifest.native_layout.recovery_beacon_lba_start, 68);
assert_eq!(manifest.native_layout.gestational_index_lba_start, 70);
+ assert_eq!(manifest.native_layout.control_state_lba, 72);
+ assert_eq!(manifest.native_layout.alternate_kernel_lba_start, 73);
+ assert_eq!(manifest.native_layout.alternate_kernel_sector_count, 29);
assert_eq!(manifest.native_layout.first_partition_lba, 2048);
}
@@ -797,6 +800,16 @@ fn rejects_native_disk_layout_contract_drift() {
"gestational_index_lba_start: 69",
"registered protocol extents",
),
+ (
+ "control_state_lba: 72",
+ "control_state_lba: 71",
+ "registered protocol extents",
+ ),
+ (
+ "alternate_kernel_lba_start: 73",
+ "alternate_kernel_lba_start: 72",
+ "registered protocol extents",
+ ),
(
"unknown_nonzero_state: FAIL_CLOSED_NO_OVERWRITE",
"unknown_nonzero_state: OVERWRITE",
@@ -840,7 +853,11 @@ fn rejects_unregistered_code_channel_phase_and_authorization_drift() {
"authorization mismatch",
),
("status: ACTIVE", "status: REVOKED", "active and issued"),
- ("node_id: BS-SH-005", "node_id: OTHER", "Shanghai lab node"),
+ (
+ "node_id: BS-SH-005",
+ "node_id: OTHER",
+ "target-specific confirmation anchor",
+ ),
(
"user_confirmation: COMPLETE_GUANGHU_OS_SERVER_EXPERIMENT_AUTHORIZED_2026_07_31",
"user_confirmation: UNKNOWN",
@@ -873,6 +890,126 @@ fn rejects_unregistered_code_channel_phase_and_authorization_drift() {
}
}
+#[test]
+fn accepts_a_consistently_retargeted_enterprise_world_seed() {
+ let world = TestWorld::copy();
+ retarget_world(&world, "GH-CVM-MAIN-PROD-01", "ins-dacj5t5a");
+
+ validate_world_seed(&world.root)
+ .expect("a consistently retargeted enterprise world must validate");
+}
+
+#[test]
+fn validates_the_registered_enterprise_deployment_seed() {
+ let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ .join("../../deployments/GH-CVM-MAIN-PROD-01/world-seed");
+
+ let manifest = validate_world_seed(&root)
+ .expect("the checked-in enterprise deployment seed must validate");
+ assert_eq!(
+ manifest.authorization.id,
+ "GH-OS-AUTH-BINGSHUO-GH-CVM-MAIN-PROD-01-001"
+ );
+}
+
+#[test]
+fn rejects_cross_document_target_identity_drift() {
+ for (path, from, to) in [
+ ("CURRENT.hldp", "node_id: BS-SH-005", "node_id: OTHER-NODE"),
+ ("WAKE.hldp", "node_id: BS-SH-005", "node_id: OTHER-NODE"),
+ (
+ "world/services/native-recovery/PROTOCOL.hldp",
+ "node_id: BS-SH-005",
+ "node_id: OTHER-NODE",
+ ),
+ (
+ "world/services/native-storage/DISK-LAYOUT.hldp",
+ "node_id: BS-SH-005",
+ "node_id: OTHER-NODE",
+ ),
+ ] {
+ let world = TestWorld::copy();
+ world.replace(path, from, to);
+ let error = validate_world_seed(&world.root)
+ .expect_err("target identity drift must fail closed")
+ .to_string();
+ assert!(
+ error.contains("target identity"),
+ "unexpected error: {error}"
+ );
+ }
+}
+
+fn retarget_world(world: &TestWorld, node_id: &str, instance_id: &str) {
+ let action_suffix = node_id.to_ascii_lowercase().replace('-', "_");
+ let confirmation_node = node_id.replace('-', "_");
+
+ for path in ["CURRENT.hldp", "WAKE.hldp"] {
+ world.replace(path, "node_id: BS-SH-005", &format!("node_id: {node_id}"));
+ }
+ world.replace(
+ "WORLD-MANIFEST.hldp",
+ "GH-OS-AUTH-BINGSHUO-BS-SH-005-001",
+ &format!("GH-OS-AUTH-BINGSHUO-{node_id}-001"),
+ );
+ world.replace(
+ "CURRENT.hldp",
+ "GH-OS-AUTH-BINGSHUO-BS-SH-005-001",
+ &format!("GH-OS-AUTH-BINGSHUO-{node_id}-001"),
+ );
+ world.replace(
+ "world/services/native-recovery/PROTOCOL.hldp",
+ "node_id: BS-SH-005",
+ &format!("node_id: {node_id}"),
+ );
+ world.replace(
+ "world/services/native-storage/DISK-LAYOUT.hldp",
+ "node_id: BS-SH-005",
+ &format!("node_id: {node_id}"),
+ );
+ let authorization = "state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp";
+ world.replace(
+ authorization,
+ "GH-OS-AUTH-BINGSHUO-BS-SH-005-001",
+ &format!("GH-OS-AUTH-BINGSHUO-{node_id}-001"),
+ );
+ world.replace(
+ authorization,
+ "node_id: BS-SH-005",
+ &format!("node_id: {node_id}"),
+ );
+ world.replace(
+ authorization,
+ "instance_id: lhins-14w5y3ce",
+ &format!("instance_id: {instance_id}"),
+ );
+ world.replace(
+ authorization,
+ "COMPLETE_GUANGHU_OS_SERVER_EXPERIMENT_AUTHORIZED_2026_07_31",
+ &format!("COMPLETE_GUANGHU_OS_{confirmation_node}_AUTHORIZED_2026_08_01"),
+ );
+ world.replace(
+ authorization,
+ "reboot_and_recover_bs_sh_005",
+ &format!("reboot_and_recover_{action_suffix}"),
+ );
+ world.replace(
+ authorization,
+ " - commit_and_push_in_scope_repositories",
+ " # repository publication is outside this server authorization",
+ );
+ world.replace(
+ authorization,
+ " - rollback_and_repeat_disposable_server_experiment",
+ " # enterprise production is not a disposable experiment",
+ );
+ world.replace(
+ authorization,
+ "不操作 BS-SH-005 以外的服务器",
+ &format!("不操作 {node_id} 以外的服务器"),
+ );
+}
+
#[test]
fn invalid_manifest_errors_have_no_nested_source() {
let mut manifest = load_world_manifest(&world_seed()).expect("world seed should parse");
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/DEVELOPMENT-LINE-20260801-20260803.md b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/DEVELOPMENT-LINE-20260801-20260803.md
new file mode 100644
index 0000000..fc7efa1
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/DEVELOPMENT-LINE-20260801-20260803.md
@@ -0,0 +1,251 @@
+# GH-CVM-MAIN-PROD-01 企业光湖 OS 开发线全记录
+
+## 1. 记录边界
+
+- 开发编号:`DEV-20260801-005`
+- 人类锚点:冰朔
+- 执行人格:`ICE-P-ZY001`
+- 目标节点:`GH-CVM-MAIN-PROD-01`
+- 云实例:腾讯云 CVM `ins-dacj5t5a`,广州
+- 系统盘:`/dev/vda`
+- 开发时间2026-08-01 至 2026-08-03
+- 结束原因:冰朔明确要求本开发线在成果入库和本机环境清理后结束
+
+本记录只保存可复核的目标、判断、设计、动作与回执,不保存密码、验证码、令牌、
+私钥、聊天正文或模型隐藏推理。
+
+## 2. 前因
+
+最初问题不是“在 Linux 上再部署一套应用”,而是评估企业服务器能否成为真实光湖
+OS 节点,并让 Linux 退到后置救援位置。冰朔给出的核心要求是:
+
+1. 企业服务器可以重新格式化,不需要把测试环境当成不可移动的生产遗产。
+2. 不购买付费镜像、快照或额外云资源。
+3. 现有 Linux 可以保留为零费用救援、回传和回滚层。
+4. 操作不能依赖当前 Mac线上和线下都必须能通过服务器自有证据与邮件授权恢复。
+5. 光湖工程按二值规则验收:存在就是 `100/PASS`,缺任一必要证据就是 `0/FAIL`。
+
+因此,本线把“原生光湖 OS”拆成可证明的物理层次而没有把网页在线、Linux 服务健康、
+源码编译通过或模型回复当成原生启动。
+
+## 3. 思维逻辑
+
+### 3.1 先把事实层分开
+
+本线始终把以下状态分别判断:
+
+```text
+用户授权
+→ 源码与测试
+→ 仓库发布
+→ 服务器备份
+→ Linux 救援可启动
+→ 双控制器签名
+→ A/B 物理写入
+→ 一次性原生启动
+→ 自动返回 Linux
+→ 原生常驻
+→ 人格体出生
+```
+
+上游状态通过不自动证明下游状态。尤其是:
+
+- Linux 在线不等于光湖 OS 原生;
+- A/B 镜像存在不等于已经写盘;
+- 写盘回读通过不等于已经可启动;
+- 原生内核回复不等于人格体出生。
+
+### 3.2 零费用不等于无灾备
+
+不购买云镜像后,灾备改为服务器自有、可校验的四层证据:
+
+1. 现有业务与系统文件归档;
+2. PostgreSQL 数据恢复演练;
+3. 云厂商控制台可进入;
+4. Linux 救援系统真实重启可返回服务。
+
+只有四层都存在,才允许生成时效很短的物理布局工单。这样避免为了安全制造持续云费用,
+也避免在没有回退路径时直接改系统盘。
+
+### 3.3 Linux 后置,原生 A/B 前置
+
+目标磁盘的第一个 Linux 分区从 LBA 2048 开始。设计只使用分区前、已经登记且回读为空的
+固定扇区:
+
+- Slot ALBA 3462
+- Slot BLBA 73101
+- Linux 分区与现有文件系统不移动;
+- B 先写、A 后写;
+- 写入前后都核验磁盘身份、分区边界和镜像 SHA-256
+- 写入阶段不自动改变 GRUB也不自动重启。
+
+这个布局使原生候选与 Linux 救援层同时存在;任何身份漂移、扇区非空、签名不足或回读
+不一致都会在第一次写入前失败关闭。
+
+### 3.4 双签名不能依赖操作者电脑
+
+物理布局采用 `2-of-2` Ed25519 控制器签名。两个控制器:
+
+- 私钥只留在各自服务器;
+- 只监听本机回环签名入口;
+- 主动通过 HTTPS 轮询京东主控邮件授权服务;
+- 只接受绑定目标、磁盘、工单、布局摘要、控制器和有效期的单次能力;
+- 不开放签名端口,不建立反向 SSH不向 Mac 返回私钥。
+
+京东主控只发布已经通过 HoloLake/小湖灯邮件授权的短时能力。布局变化、重放、过期、
+目标不符或签名不足一律为 `FAIL_0`。
+
+### 3.5 服务器证据优先于对话记忆
+
+恢复顺序固定为:
+
+```text
+服务器 WAKE
+→ CURRENT
+→ 授权与工单
+→ 备份/恢复/启动回执
+→ 当前磁盘只读回读
+→ 仓库完整 SHA
+→ 才允许产生下一动作
+```
+
+聊天摘要、浏览器画面和本机缓存只用于导航,不是最终权威。
+
+## 4. 已形成的工程能力
+
+### 4.1 GHDR 原生布局与验证
+
+- 固定 A/B 扇区、磁盘身份和首分区边界验证;
+- 新鲜目标回读与防 TOCTOU 二次核验;
+- `2-of-2` 控制器绑定、有效期、重放与错误目标拒绝;
+- 写前扇区为空验证;
+- B/A 顺序写入与逐槽 SHA-256 回读;
+- 完整的写前首尾磁盘备份和安装回执;
+- 写入完成后仍保持 `native_boot_armed: false`。
+
+### 4.2 原生候选与网络回执
+
+- BIOS 原生入口、GHAL virtio 网络路径和恢复信标;
+- QEMU 网络对端与原生控制授权测试;
+- 企业身份绑定、恢复信标协议与二值质量门;
+- 物理候选和常驻候选构建、测试脚本。
+
+### 4.3 企业世界种子
+
+节点种子包含:
+
+- 五域入口;
+- 原生存储与恢复协议;
+- 代码频道控制面契约;
+- 孕育连续性与人格出生条件;
+- 授权、工单、检查点和阶段回执;
+- `WAKE → CURRENT → receipt/workorder/authorization` 恢复链。
+
+### 4.4 邮件授权双控制器
+
+第五域代码频道已经发布:
+
+- GHDR 邮件授权器;
+- 控制器任务代理与结果回传;
+- 目标导航图;
+- 邮件批准后才允许布局签名的门禁;
+- 控制器传输端点测试。
+
+对应历史远端提交包括:
+
+- `bec7a3d`:邮件授权 GHDR 双签;
+- `12517bf`:控制器传输端点测试;
+- `a385249`:强制邮件批准后才能进行原生布局签名。
+
+## 5. 真实服务器动作与回执
+
+### 5.1 已通过
+
+- 数据恢复演练:`PASS_100_DATA_RESTORE_DRILL`
+- 控制面备份:`PASS_100_CONTROL_PLANE_BACKUP`
+- 云控制台管理员会话恢复:`PASS_100_PROVIDER_CONSOLE_ADMIN_SESSION_RECOVERY_NO_REBOOT`
+- Linux 救援启动故障修复:
+ - 根因是 `/etc/fstab` 仍挂载不存在的 `/dev/vdb`
+ - 原文件保留为服务器内 `fstab_bak`
+ - 只移除 `/dev/vdb /data ext4 defaults 0 0`
+ - `findmnt --verify` 返回 0 错误、0 警告;
+ - systemd 正常到达 Ubuntu 登录界面;
+ - ICMP 3/3HTTP 200HTTPS 200。
+
+### 5.2 失败是怎样发生的
+
+第一次真实重启不是光湖 OS 启动。此时:
+
+- A/B 镜像尚未写入 `/dev/vda`
+- GRUB 尚未武装原生入口;
+- 重启目标只是验证 Linux 救援层。
+
+Linux 启动时等待不存在的 `/dev/vdb` 90 秒,随后 `/data` 和本地文件系统依赖失败,
+进入 `emergency.target`。一次性 `fstab=no` 证明了故障来源,但根文件系统只读;最终
+使用一次性 `rw init=/bin/bash` 进入维护环境,保留原配置、完成单行修复并切回 systemd。
+
+这次失败建立了一个必须长期保留的判断:
+
+> “服务器没起来”必须先确定失败对象。没有安装和武装的光湖 OS 不可能被描述为
+> “启动失败”;本次失败对象是 Linux 救援层。
+
+## 6. 本线结束时的二值状态
+
+| 对象 | 结果 | 证据边界 |
+|---|---|---|
+| 企业资产封存 | `PASS_100` | 归档与 SHA-256 回执存在 |
+| 数据恢复演练 | `PASS_100` | 隔离恢复和验证回执存在 |
+| 云控制台恢复 | `PASS_100` | 管理员会话回执存在 |
+| Linux 救援可启动 | `PASS_100` | VNC 正常登录界面、网络与 80/443 服务回读 |
+| 邮件授权双签源码 | `PASS_100` | 已发布提交与测试 |
+| 原生 A/B 物理写入 | `FAIL_0_NOT_WRITTEN` | 从未执行写盘 |
+| 原生启动入口 | `FAIL_0_NOT_ARMED` | 未改 GRUB、未武装一次性启动 |
+| 光湖 OS 原生常驻 | `FAIL_0_NOT_NATIVE` | 没有原生启动回执 |
+| 企业人格体出生 | `FAIL_0_NOT_BORN` | 没有出生条件回执 |
+
+## 7. 源码收口验证
+
+结束前重新执行了以下门禁:
+
+- `cargo fmt --all -- --check`:通过;
+- Guanghu OS Rust 全工作区测试84 项通过0 项失败;
+- 控制器签名器、回环 HTTP 能力和主动轮询契约:全部 `PASS_100`
+- 原生控制授权 Python 测试4 项通过;
+- 签名 A/B 安装器契约:通过;
+- GH-CVM 身份绑定的 BIOS 物理候选:
+ - SHA-256 `4b762d41cc952e131a2fc5d3b2eeb1655708fa69d4906b06a684dc22198fdc5d`
+ - QEMU 物理布局、virtio block/net、代码频道、孕育索引和双控制器重放拒绝通过
+- GH-CVM 身份绑定的常驻候选:
+ - SHA-256 `bd4b51be1b9697b62e7b67720c1c745130f14b34077db11a845591db48209c78`
+ - 常驻登录、认证恢复、跨启动 nonce、未知控制状态失败关闭和孕育索引保留通过
+- 原生恢复信标契约:通过;
+- `git diff --check`:通过;
+- CodeScene本机只有 CLI没有现成访问令牌记为 `not_run_unconfigured`
+- Codacy仓库没有可用的本地 CLI记为 `not_run_unavailable`
+- 脱敏扫描未发现实际私钥、密码、令牌或 API 密钥文件。
+
+上述候选测试只证明 QEMU 中的工程能力,不证明企业服务器已经物理写入或原生启动。
+
+## 8. 为什么在这里结束
+
+冰朔明确要求本开发线在成果入库和本机环境清理后结束。因此:
+
+- 不继续生成短时布局工单;
+- 不继续请求 2-of-2 生产签名;
+- 不写 `/dev/vda` A/B 扇区;
+- 不修改 GRUB
+- 不重启到原生候选;
+- 不保留自动心跳或后台续作。
+
+这不是“原生部署完成”,而是“本次开发线完成收口”。未来若再次继续,必须由冰朔明确
+重开任务重新读取线上代码频道、服务器回执、磁盘身份、Linux 救援状态和控制器状态,
+不得沿用本文件中的瞬时在线结论直接写盘。
+
+## 9. 可复用原则
+
+1. 原生系统迁移先证明回退,再讨论写入。
+2. 零费用方案仍须有可验证灾备,不用付费资源代替工程判断。
+3. 私钥留在服务器;授权传递短时、单次、精确绑定的能力。
+4. 代码、发布、部署、启动、常驻和出生分别验收。
+5. 所有危险动作都需要写前读回、写后读回和独立恢复路径。
+6. 任务结束必须释放租约、停止心跳、清理可再生构建缓存,并保留源码与回执。
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/.gitignore b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/.gitignore
new file mode 100644
index 0000000..7a60b85
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+*.pyc
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/README.md b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/README.md
new file mode 100644
index 0000000..1e82013
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/README.md
@@ -0,0 +1,43 @@
+# GH-CVM-MAIN-PROD-01 controller signer
+
+This package installs one fixed-purpose Ed25519 signer on a recovery controller.
+It never prints or exports its private key. The only accepted input is a fresh,
+canonical, unsigned GHDR plan for `GH-CVM-MAIN-PROD-01`, provider
+`tencent_cloud`, region `ap-guangzhou`, disk `/dev/vda`, and the fixed A/B
+sectors.
+
+The signing entry is a loopback-only HTTP service. A separate low-privilege
+poller makes outbound HTTPS requests to JD-FD-PRIMARY after installation. It
+authenticates those requests with a dedicated transport key, receives only jobs
+that already passed HoloLake/Lake Lamp email authorization, and submits the
+result to the same HTTPS control plane. No inbound signer port, reverse SSH
+tunnel, or operator Mac is required.
+
+The controller pins the JD authorizer public key and accepts only a two-minute,
+single-use Ed25519 capability bound to the exact controller, target, workorder,
+layout digest, resource, and generation. It cannot accept a shell, another
+target, a changed layout, an expired capability, or a replay. The layout key
+and transport key are separate and neither private key is returned by any
+health, polling, signing, or result endpoint.
+
+Install one controller at a time:
+
+```sh
+sudo env \
+ GHDR_CONTROLLER_NODE_ID=GH-CTRL-GZ-01 \
+ GHDR_CONTROLLER_FAILURE_DOMAIN=tencent/ap-guangzhou/BS-GZ-006 \
+ sh install-controller-signer.sh
+```
+
+Only after the JD control plane has generated its dedicated authorizer key,
+install the public half. This enables both the loopback signer and the outbound
+poller:
+
+```sh
+sudo sh install-jd-forced-key.sh /path/to/jd-authorizer-public.pem
+```
+
+The public controller binding is
+`/etc/guanghu/ghdr-controller-public-binding.json`.
+The independent transport binding is
+`/etc/guanghu/ghdr-controller-transport-binding.json`.
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.py b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.py
new file mode 100644
index 0000000..658ee8f
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+"""Outbound-only controller agent for JD email-authorized GHDR jobs."""
+
+import base64
+import json
+import os
+import secrets
+import subprocess
+import tempfile
+import time
+import urllib.error
+import urllib.request
+
+
+def required_env(name):
+ value = os.environ.get(name, "").strip()
+ if not value:
+ raise RuntimeError(f"missing environment binding: {name}")
+ return value
+
+
+def canonical(value):
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode()
+
+
+def sign_envelope(value):
+ key = required_env("GHDR_TRANSPORT_PRIVATE_KEY")
+ with tempfile.TemporaryDirectory(prefix="ghdr-poll-auth-") as directory:
+ message = os.path.join(directory, "message.json")
+ signature = os.path.join(directory, "signature.bin")
+ with open(message, "xb") as handle:
+ handle.write(canonical(value))
+ completed = subprocess.run([
+ "/usr/bin/openssl", "pkeyutl", "-sign", "-rawin",
+ "-inkey", key, "-in", message, "-out", signature,
+ ], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
+ if completed.returncode != 0:
+ raise RuntimeError("transport signing failed")
+ with open(signature, "rb") as handle:
+ return handle.read().hex()
+
+
+def post(path, value):
+ base = required_env("GHDR_JD_AUTHZ_URL").rstrip("/")
+ if not base.startswith("https://"):
+ raise RuntimeError("JD authorization URL must use HTTPS")
+ request = urllib.request.Request(
+ base + path,
+ data=canonical(value),
+ method="POST",
+ headers={"content-type": "application/json", "user-agent": "Guanghu-GHDR-Poller/1"},
+ )
+ with urllib.request.urlopen(request, timeout=20) as response:
+ if response.status != 200:
+ raise RuntimeError("JD authorization endpoint refused the request")
+ return json.loads(response.read(131072))
+
+
+def controller_request(schema, extra=None):
+ value = {
+ "schema": schema,
+ "node_id": required_env("GHDR_CONTROLLER_NODE_ID"),
+ }
+ if extra:
+ value.update(extra)
+ value.update({
+ "issued_at_unix": int(time.time()),
+ "nonce": base64.urlsafe_b64encode(secrets.token_bytes(24)).rstrip(b"=").decode(),
+ })
+ return value
+
+
+def sign_job(job):
+ authorization = job["authorization"]
+ request = {
+ **authorization,
+ "plan": job["plan"],
+ }
+ local = urllib.request.Request(
+ "http://127.0.0.1:3941/sign",
+ data=canonical(request),
+ method="POST",
+ headers={"content-type": "application/json"},
+ )
+ with urllib.request.urlopen(local, timeout=20) as response:
+ value = json.loads(response.read(131072))
+ if value.get("ok") is not True:
+ raise RuntimeError("local signer refused the authorized job")
+ return value["signature"]
+
+
+def one_cycle():
+ request = controller_request("guanghu.ghdr-controller-poll/v1")
+ polled = post("/api/ghdr/controllers/poll", {
+ "request": request,
+ "request_signature_hex": sign_envelope(request),
+ })
+ job = polled.get("job")
+ if not job:
+ return
+ signature = sign_job(job)
+ result_request = controller_request(
+ "guanghu.ghdr-controller-result/v1",
+ {
+ "job_id": job["job_id"],
+ "layout_payload_sha256": job["layout_payload_sha256"],
+ "signature_hex": signature["signature_hex"],
+ },
+ )
+ submitted = post("/api/ghdr/controllers/result", {
+ "request": result_request,
+ "request_signature_hex": sign_envelope(result_request),
+ "signature": signature,
+ })
+ if submitted.get("ok") is not True:
+ raise RuntimeError("JD authorization endpoint refused the signed result")
+
+
+def main():
+ interval = max(5, int(os.environ.get("GHDR_POLL_INTERVAL_SECONDS", "10")))
+ while True:
+ try:
+ one_cycle()
+ except (OSError, RuntimeError, ValueError, urllib.error.URLError):
+ pass
+ time.sleep(interval)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.service b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.service
new file mode 100644
index 0000000..86ec502
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-controller-poller.service
@@ -0,0 +1,33 @@
+[Unit]
+Description=Guanghu GHDR outbound controller poller
+After=network-online.target guanghu-ghdr-signer.service
+Wants=network-online.target
+Requires=guanghu-ghdr-signer.service
+ConditionPathExists=/etc/guanghu/ghdr-authorizer-public.pem
+
+[Service]
+Type=simple
+User=ghdrpoller
+Group=ghdrpoller
+EnvironmentFile=/etc/guanghu/ghdr-controller-poller.env
+ExecStart=/usr/bin/python3 /usr/local/libexec/guanghu-ghdr-controller-poller.py
+Restart=always
+RestartSec=5s
+NoNewPrivileges=true
+PrivateTmp=true
+PrivateDevices=true
+ProtectSystem=strict
+ProtectHome=true
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectControlGroups=true
+LockPersonality=true
+MemoryDenyWriteExecute=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
+ReadOnlyPaths=/etc/guanghu
+UMask=0077
+
+[Install]
+WantedBy=multi-user.target
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer-http.py b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer-http.py
new file mode 100644
index 0000000..6fa47e2
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer-http.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+"""Loopback-only GHDR signer authorized by a short JD email capability."""
+
+import base64
+import hashlib
+import http.server
+import importlib.util
+import json
+import os
+import pathlib
+import re
+import socketserver
+import tempfile
+import time
+
+SIGNER_PATH = pathlib.Path(__file__).with_name("guanghu-ghdr-signer.py")
+SIGNER_SPEC = importlib.util.spec_from_file_location("guanghu_ghdr_signer", SIGNER_PATH)
+if SIGNER_SPEC is None or SIGNER_SPEC.loader is None:
+ raise SystemExit("GHDR_SIGNER_FAIL_0: signer module unavailable")
+signer = importlib.util.module_from_spec(SIGNER_SPEC)
+SIGNER_SPEC.loader.exec_module(signer)
+
+MAX_INPUT_BYTES = 64 * 1024
+CAPABILITY_FIELDS = [
+ "schema",
+ "authorizer_id",
+ "controller_node_id",
+ "target_node_id",
+ "layout_payload_sha256",
+ "resource",
+ "workorder_id",
+ "issued_at_unix",
+ "expires_at_unix",
+ "nonce",
+]
+
+
+def base64url_decode(value):
+ if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9_-]{80,100}", value):
+ raise signer.Refused("capability signature is invalid")
+ return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
+
+
+def canonical_json(value):
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
+
+
+def verify_capability(request):
+ if list(request) != ["capability", "capability_signature_base64url", "plan"]:
+ raise signer.Refused("authorized request fields are not canonical")
+ capability = request["capability"]
+ if not isinstance(capability, dict) or list(capability) != CAPABILITY_FIELDS:
+ raise signer.Refused("capability fields are not canonical")
+ if capability["schema"] != "guanghu.ghdr-signing-capability/v1":
+ raise signer.Refused("capability schema is not supported")
+ if capability["authorizer_id"] != "JD-FD-PRIMARY-LAKE-LAMP":
+ raise signer.Refused("capability authorizer is not trusted")
+ if capability["controller_node_id"] != signer.required_env("GHDR_CONTROLLER_NODE_ID"):
+ raise signer.Refused("capability is for another controller")
+ if capability["target_node_id"] != signer.required_env("GHDR_TARGET_NODE_ID"):
+ raise signer.Refused("capability is for another target")
+ if not signer.sha256_hex(capability["layout_payload_sha256"]):
+ raise signer.Refused("capability layout digest is invalid")
+ if not re.fullmatch(
+ r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
+ str(capability["workorder_id"]),
+ ):
+ raise signer.Refused("capability workorder binding is invalid")
+ if not re.fullmatch(r"[A-Za-z0-9_-]{32}", str(capability["nonce"])):
+ raise signer.Refused("capability nonce is invalid")
+
+ now = int(time.time())
+ issued = capability["issued_at_unix"]
+ expires = capability["expires_at_unix"]
+ if not isinstance(issued, int) or not isinstance(expires, int):
+ raise signer.Refused("capability validity fields must be integers")
+ if issued > now + 5 or now >= expires or expires - issued > 120 or expires <= issued:
+ raise signer.Refused("capability is not currently valid for at most two minutes")
+
+ plan = request["plan"]
+ payload = signer.validate_request(plan)
+ payload_digest = hashlib.sha256(canonical_json(payload)).hexdigest()
+ generation = payload["generation"]
+ expected_resource = f"{capability['target_node_id']}:{payload_digest}:{generation}"
+ if capability["layout_payload_sha256"] != payload_digest:
+ raise signer.Refused("capability is for another layout")
+ if capability["resource"] != expected_resource:
+ raise signer.Refused("capability resource does not match the layout")
+
+ authorizer_public_key = signer.required_env("GHDR_AUTHORIZER_PUBLIC_KEY")
+ metadata = os.lstat(authorizer_public_key)
+ if not pathlib.Path(authorizer_public_key).is_file() or pathlib.Path(authorizer_public_key).is_symlink():
+ raise signer.Refused("authorizer public key path is invalid")
+ if metadata.st_mode & 0o022:
+ raise signer.Refused("authorizer public key must not be writable by group or others")
+ signature = base64url_decode(request["capability_signature_base64url"])
+ if len(signature) != 64:
+ raise signer.Refused("capability signature length is invalid")
+ with tempfile.TemporaryDirectory(prefix="ghdr-capability-") as directory:
+ message_path = os.path.join(directory, "capability.json")
+ signature_path = os.path.join(directory, "capability.sig")
+ with open(message_path, "xb") as handle:
+ handle.write(canonical_json(capability))
+ with open(signature_path, "xb") as handle:
+ handle.write(signature)
+ signer.run_openssl([
+ "pkeyutl",
+ "-verify",
+ "-rawin",
+ "-pubin",
+ "-inkey",
+ authorizer_public_key,
+ "-in",
+ message_path,
+ "-sigfile",
+ signature_path,
+ ])
+ return payload, hashlib.sha256(canonical_json(capability)).hexdigest()
+
+
+def claim_once(capability_digest):
+ used_dir = pathlib.Path(signer.required_env("GHDR_USED_CAPABILITY_DIR"))
+ used_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
+ used_dir.chmod(0o700)
+ marker = used_dir / capability_digest
+ try:
+ descriptor = os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
+ except FileExistsError as error:
+ raise signer.Refused("capability was already used") from error
+ with os.fdopen(descriptor, "w", encoding="ascii") as handle:
+ handle.write(f"{int(time.time())}\n")
+ handle.flush()
+ os.fsync(handle.fileno())
+
+
+class Handler(http.server.BaseHTTPRequestHandler):
+ server_version = "GuanghuGHDRSigner/1"
+
+ def send_json(self, status, value):
+ payload = canonical_json(value)
+ self.send_response(status)
+ self.send_header("content-type", "application/json")
+ self.send_header("content-length", str(len(payload)))
+ self.send_header("cache-control", "no-store")
+ self.end_headers()
+ self.wfile.write(payload)
+
+ def do_GET(self):
+ if self.path != "/health":
+ return self.send_json(404, {"ok": False, "error": "not_found"})
+ return self.send_json(200, {
+ "ok": True,
+ "node_id": signer.required_env("GHDR_CONTROLLER_NODE_ID"),
+ "failure_domain": signer.required_env("GHDR_CONTROLLER_FAILURE_DOMAIN"),
+ "private_key_exportable": False,
+ "authorization": "JD-FD-PRIMARY email capability",
+ })
+
+ def do_POST(self):
+ if self.path != "/sign":
+ return self.send_json(404, {"ok": False, "error": "not_found"})
+ try:
+ length = int(self.headers.get("content-length", "0"))
+ if length < 1 or length > MAX_INPUT_BYTES:
+ raise signer.Refused("authorized request is empty or too large")
+ raw = self.rfile.read(length)
+ request = json.loads(raw)
+ if not isinstance(request, dict):
+ raise signer.Refused("authorized request must be an object")
+ payload, capability_digest = verify_capability(request)
+ claim_once(capability_digest)
+ signature = signer.sign_to_value(payload)
+ return self.send_json(200, {"ok": True, "signature": signature})
+ except (OSError, ValueError, signer.Refused) as error:
+ return self.send_json(403, {"ok": False, "error": str(error)})
+
+ def log_message(self, _format, *_args):
+ return
+
+
+class Server(socketserver.ThreadingMixIn, http.server.HTTPServer):
+ daemon_threads = True
+ allow_reuse_address = True
+
+
+def main():
+ port = int(os.environ.get("GHDR_SIGNER_PORT", "3941"))
+ if not 1024 <= port <= 65535:
+ raise SystemExit("GHDR_SIGNER_FAIL_0: invalid loopback port")
+ with Server(("127.0.0.1", port), Handler) as server:
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.py b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.py
new file mode 100755
index 0000000..4581d62
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+"""Fail-closed Ed25519 signer for one GHDR production layout."""
+
+import hashlib
+import json
+import os
+import stat
+import subprocess
+import sys
+import tempfile
+import time
+
+MAX_INPUT_BYTES = 64 * 1024
+PAYLOAD_FIELDS = [
+ "node_id",
+ "provider",
+ "region",
+ "target_probe_sha256",
+ "system_disk",
+ "disk_sectors",
+ "logical_sector_bytes",
+ "disk_identity_sha256",
+ "recovery_evidence_sha256",
+ "first_partition_lba",
+ "generation",
+ "operation",
+ "issued_at_unix",
+ "expires_at_unix",
+ "slots",
+]
+SLOT_FIELDS = ["name", "lba_start", "sector_count", "image_sha256"]
+
+
+class Refused(ValueError):
+ pass
+
+
+def required_env(name):
+ value = os.environ.get(name, "").strip()
+ if not value:
+ raise Refused(f"missing environment binding: {name}")
+ return value
+
+
+def sha256_hex(value):
+ return (
+ isinstance(value, str)
+ and len(value) == 64
+ and all(character in "0123456789abcdef" for character in value)
+ )
+
+
+def read_request():
+ raw = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
+ if not raw or len(raw) > MAX_INPUT_BYTES:
+ raise Refused("layout request is empty or too large")
+ try:
+ request = json.loads(raw)
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
+ raise Refused("layout request is not valid UTF-8 JSON") from error
+ if not isinstance(request, dict):
+ raise Refused("layout request must be an object")
+ return request
+
+
+def validate_request(request):
+ if list(request) != ["schema", "payload", "signatures"]:
+ raise Refused("layout request fields or field order are not canonical")
+ if request["schema"] != "guanghu.ghdr-signed-layout-plan/v1":
+ raise Refused("layout schema is not supported")
+ if request["signatures"] != []:
+ raise Refused("controller only signs a canonical unsigned plan")
+
+ payload = request["payload"]
+ if not isinstance(payload, dict) or list(payload) != PAYLOAD_FIELDS:
+ raise Refused("layout payload fields or field order are not canonical")
+ expected = {
+ "node_id": required_env("GHDR_TARGET_NODE_ID"),
+ "provider": required_env("GHDR_TARGET_PROVIDER"),
+ "region": required_env("GHDR_TARGET_REGION"),
+ "system_disk": "/dev/vda",
+ "logical_sector_bytes": 512,
+ "first_partition_lba": 2048,
+ "operation": "install_native_ab",
+ }
+ for field, value in expected.items():
+ if payload.get(field) != value:
+ raise Refused(f"layout payload is outside the fixed binding: {field}")
+
+ for field in (
+ "target_probe_sha256",
+ "disk_identity_sha256",
+ "recovery_evidence_sha256",
+ ):
+ if not sha256_hex(payload.get(field)):
+ raise Refused(f"layout payload has an invalid SHA-256: {field}")
+ for field in ("disk_sectors", "generation"):
+ if not isinstance(payload.get(field), int) or payload[field] < 1:
+ raise Refused(f"layout payload has an invalid integer: {field}")
+
+ now = int(time.time())
+ issued = payload.get("issued_at_unix")
+ expires = payload.get("expires_at_unix")
+ if not isinstance(issued, int) or not isinstance(expires, int):
+ raise Refused("layout validity fields must be integers")
+ if issued > now or now >= expires or expires - issued > 3600:
+ raise Refused("layout request is not currently valid for at most one hour")
+
+ slots = payload.get("slots")
+ if not isinstance(slots, list) or len(slots) != 2:
+ raise Refused("layout must contain exactly the fixed A/B slots")
+ fixed_slots = (("A", 34, 29), ("B", 73, 29))
+ for slot, fixed in zip(slots, fixed_slots):
+ if not isinstance(slot, dict) or list(slot) != SLOT_FIELDS:
+ raise Refused("slot fields or field order are not canonical")
+ if (slot.get("name"), slot.get("lba_start"), slot.get("sector_count")) != fixed:
+ raise Refused("slot is outside the fixed A/B disk boundary")
+ if not sha256_hex(slot.get("image_sha256")):
+ raise Refused("slot image SHA-256 is invalid")
+ return payload
+
+
+def validate_key(path):
+ metadata = os.lstat(path)
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_mode & 0o077:
+ raise Refused("private key must be a regular file inaccessible to group and others")
+
+
+def run_openssl(arguments, *, input_bytes=None):
+ openssl_bin = os.environ.get("GHDR_OPENSSL_BIN", "/usr/bin/openssl")
+ completed = subprocess.run(
+ [openssl_bin, *arguments],
+ input=input_bytes,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ timeout=15,
+ )
+ if completed.returncode != 0:
+ raise Refused("OpenSSL Ed25519 operation failed")
+ return completed.stdout
+
+
+def sign_to_value(payload):
+ key_path = required_env("GHDR_SIGNER_PRIVATE_KEY")
+ validate_key(key_path)
+ canonical = json.dumps(
+ payload, ensure_ascii=False, separators=(",", ":")
+ ).encode("utf-8")
+ public_der = run_openssl(["pkey", "-in", key_path, "-pubout", "-outform", "DER"])
+ if len(public_der) < 32:
+ raise Refused("Ed25519 public key output is invalid")
+ public_key = public_der[-32:]
+ with tempfile.TemporaryDirectory(prefix="ghdr-sign-") as directory:
+ payload_path = os.path.join(directory, "payload.json")
+ signature_path = os.path.join(directory, "signature.bin")
+ with open(payload_path, "xb") as handle:
+ handle.write(canonical)
+ handle.flush()
+ os.fsync(handle.fileno())
+ run_openssl(
+ [
+ "pkeyutl",
+ "-sign",
+ "-rawin",
+ "-inkey",
+ key_path,
+ "-in",
+ payload_path,
+ "-out",
+ signature_path,
+ ]
+ )
+ with open(signature_path, "rb") as handle:
+ signature = handle.read()
+ if len(signature) != 64:
+ raise Refused("Ed25519 signature output is invalid")
+ digest = hashlib.sha256(canonical).hexdigest()
+ return {
+ "node_id": required_env("GHDR_CONTROLLER_NODE_ID"),
+ "failure_domain": required_env("GHDR_CONTROLLER_FAILURE_DOMAIN"),
+ "public_key_hex": public_key.hex(),
+ "signature_hex": signature.hex(),
+ }
+
+
+def sign(payload):
+ response = sign_to_value(payload)
+ digest = hashlib.sha256(
+ json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
+ ).hexdigest()
+ print(json.dumps(response, ensure_ascii=False, separators=(",", ":")))
+ print(f"GHDR_SIGNED_PAYLOAD_SHA256={digest}", file=sys.stderr)
+
+
+def main():
+ try:
+ sign(validate_request(read_request()))
+ except (OSError, Refused, subprocess.SubprocessError) as error:
+ print(f"GHDR_SIGNER_FAIL_0: {error}", file=sys.stderr)
+ return 65
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.service b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.service
new file mode 100644
index 0000000..dc490cd
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/guanghu-ghdr-signer.service
@@ -0,0 +1,34 @@
+[Unit]
+Description=Guanghu GHDR email-authorized controller signer
+After=network.target
+ConditionPathExists=/etc/guanghu/ghdr-authorizer-public.pem
+
+[Service]
+Type=simple
+User=ghdrsigner
+Group=ghdrsigner
+EnvironmentFile=/etc/guanghu/ghdr-controller.env
+ExecStart=/usr/bin/python3 /usr/local/libexec/guanghu-ghdr-signer-http.py
+Restart=on-failure
+RestartSec=5s
+NoNewPrivileges=true
+PrivateTmp=true
+PrivateDevices=true
+ProtectSystem=strict
+ProtectHome=true
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectControlGroups=true
+LockPersonality=true
+MemoryDenyWriteExecute=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+RestrictAddressFamilies=AF_INET AF_UNIX
+IPAddressDeny=any
+IPAddressAllow=localhost
+ReadOnlyPaths=/etc/guanghu
+ReadWritePaths=/var/lib/guanghu/ghdr-signer
+UMask=0077
+
+[Install]
+WantedBy=multi-user.target
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-controller-signer.sh b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-controller-signer.sh
new file mode 100755
index 0000000..a774ee6
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-controller-signer.sh
@@ -0,0 +1,159 @@
+#!/bin/sh
+set -eu
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "GHDR_SIGNER_INSTALL_FAIL_0: root is required" >&2
+ exit 65
+fi
+
+controller_node_id=${GHDR_CONTROLLER_NODE_ID:?missing controller node id}
+controller_failure_domain=${GHDR_CONTROLLER_FAILURE_DOMAIN:?missing failure domain}
+target_region=${GHDR_TARGET_REGION:-ap-guangzhou}
+source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+private_dir=/etc/guanghu/secrets/ghdr-controller
+private_key=${private_dir}/controller-ed25519.pem
+transport_dir=/etc/guanghu/secrets/ghdr-transport
+transport_key=${transport_dir}/controller-transport-ed25519.pem
+binding_file=/etc/guanghu/ghdr-controller-public-binding.json
+transport_binding_file=/etc/guanghu/ghdr-controller-transport-binding.json
+environment_file=/etc/guanghu/ghdr-controller.env
+poller_environment_file=/etc/guanghu/ghdr-controller-poller.env
+
+command -v openssl >/dev/null
+command -v python3 >/dev/null
+id ghdrsigner >/dev/null 2>&1 || useradd \
+ --system \
+ --home-dir /var/lib/guanghu/ghdr-signer \
+ --create-home \
+ --shell /bin/sh \
+ ghdrsigner
+passwd -l ghdrsigner >/dev/null 2>&1 || true
+id ghdrpoller >/dev/null 2>&1 || useradd \
+ --system \
+ --home-dir /var/lib/guanghu/ghdr-poller \
+ --create-home \
+ --shell /usr/sbin/nologin \
+ ghdrpoller
+passwd -l ghdrpoller >/dev/null 2>&1 || true
+
+install -d -m 0755 /usr/local/libexec /etc/guanghu
+install -d -o root -g ghdrsigner -m 0750 "${private_dir}"
+install -d -o root -g ghdrpoller -m 0750 "${transport_dir}"
+install -d -o ghdrsigner -g ghdrsigner -m 0700 /var/lib/guanghu/ghdr-signer
+install -d -o ghdrsigner -g ghdrsigner -m 0700 /var/lib/guanghu/ghdr-signer/.ssh
+install -o root -g root -m 0755 \
+ "${source_dir}/guanghu-ghdr-signer.py" \
+ /usr/local/libexec/guanghu-ghdr-signer.py
+install -o root -g root -m 0755 \
+ "${source_dir}/guanghu-ghdr-signer-http.py" \
+ /usr/local/libexec/guanghu-ghdr-signer-http.py
+install -o root -g root -m 0644 \
+ "${source_dir}/guanghu-ghdr-signer.service" \
+ /etc/systemd/system/guanghu-ghdr-signer.service
+install -o root -g root -m 0755 \
+ "${source_dir}/guanghu-ghdr-controller-poller.py" \
+ /usr/local/libexec/guanghu-ghdr-controller-poller.py
+install -o root -g root -m 0644 \
+ "${source_dir}/guanghu-ghdr-controller-poller.service" \
+ /etc/systemd/system/guanghu-ghdr-controller-poller.service
+
+if [ ! -e "${private_key}" ]; then
+ umask 077
+ openssl genpkey -algorithm ED25519 -out "${private_key}"
+ chown ghdrsigner:ghdrsigner "${private_key}"
+ chmod 0600 "${private_key}"
+fi
+test -f "${private_key}"
+test "$(stat -c '%a' "${private_key}")" = 600
+test "$(stat -c '%U:%G' "${private_key}")" = ghdrsigner:ghdrsigner
+if [ ! -e "${transport_key}" ]; then
+ umask 077
+ openssl genpkey -algorithm ED25519 -out "${transport_key}"
+ chown ghdrpoller:ghdrpoller "${transport_key}"
+ chmod 0600 "${transport_key}"
+fi
+test -f "${transport_key}"
+test "$(stat -c '%a' "${transport_key}")" = 600
+test "$(stat -c '%U:%G' "${transport_key}")" = ghdrpoller:ghdrpoller
+
+public_key_hex=$(
+ openssl pkey -in "${private_key}" -pubout -outform DER |
+ tail -c 32 |
+ od -An -v -tx1 |
+ tr -d ' \n'
+)
+test "${#public_key_hex}" -eq 64
+transport_public_key_hex=$(
+ openssl pkey -in "${transport_key}" -pubout -outform DER |
+ tail -c 32 |
+ od -An -v -tx1 |
+ tr -d ' \n'
+)
+test "${#transport_public_key_hex}" -eq 64
+
+umask 022
+cat >"${binding_file}.tmp" <<EOF
+{
+ "schema": "guanghu.ghdr-controller-public-binding/v1",
+ "node_id": "${controller_node_id}",
+ "failure_domain": "${controller_failure_domain}",
+ "algorithm": "Ed25519",
+ "public_key_hex": "${public_key_hex}"
+}
+EOF
+chown root:root "${binding_file}.tmp"
+chmod 0644 "${binding_file}.tmp"
+mv "${binding_file}.tmp" "${binding_file}"
+cat >"${transport_binding_file}.tmp" <<EOF
+{
+ "schema": "guanghu.ghdr-controller-transport-binding/v1",
+ "node_id": "${controller_node_id}",
+ "algorithm": "Ed25519",
+ "transport_public_key_hex": "${transport_public_key_hex}"
+}
+EOF
+chown root:root "${transport_binding_file}.tmp"
+chmod 0644 "${transport_binding_file}.tmp"
+mv "${transport_binding_file}.tmp" "${transport_binding_file}"
+
+umask 077
+cat >"${environment_file}.tmp" <<EOF
+GHDR_SIGNER_PRIVATE_KEY=${private_key}
+GHDR_CONTROLLER_NODE_ID=${controller_node_id}
+GHDR_CONTROLLER_FAILURE_DOMAIN=${controller_failure_domain}
+GHDR_TARGET_NODE_ID=GH-CVM-MAIN-PROD-01
+GHDR_TARGET_PROVIDER=tencent_cloud
+GHDR_TARGET_REGION=${target_region}
+GHDR_AUTHORIZER_PUBLIC_KEY=/etc/guanghu/ghdr-authorizer-public.pem
+GHDR_USED_CAPABILITY_DIR=/var/lib/guanghu/ghdr-signer/used-capabilities
+GHDR_SIGNER_PORT=3941
+EOF
+chown root:ghdrsigner "${environment_file}.tmp"
+chmod 0640 "${environment_file}.tmp"
+mv "${environment_file}.tmp" "${environment_file}"
+cat >"${poller_environment_file}.tmp" <<EOF
+GHDR_TRANSPORT_PRIVATE_KEY=${transport_key}
+GHDR_CONTROLLER_NODE_ID=${controller_node_id}
+GHDR_JD_AUTHZ_URL=https://guanghulab.com/authz
+GHDR_POLL_INTERVAL_SECONDS=10
+EOF
+chown root:ghdrpoller "${poller_environment_file}.tmp"
+chmod 0640 "${poller_environment_file}.tmp"
+mv "${poller_environment_file}.tmp" "${poller_environment_file}"
+
+cat >/usr/local/bin/guanghu-ghdr-sign <<'EOF'
+#!/bin/sh
+set -eu
+set -a
+. /etc/guanghu/ghdr-controller.env
+set +a
+exec /usr/bin/python3 /usr/local/libexec/guanghu-ghdr-signer.py
+EOF
+chown root:root /usr/local/bin/guanghu-ghdr-sign
+chmod 0755 /usr/local/bin/guanghu-ghdr-sign
+
+systemctl daemon-reload
+
+echo "GHDR_CONTROLLER_SIGNER_INSTALLED_PRIVATE_KEY_NOT_PRINTED"
+cat "${binding_file}"
+cat "${transport_binding_file}"
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-jd-forced-key.sh b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-jd-forced-key.sh
new file mode 100644
index 0000000..518af70
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/install-jd-forced-key.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+set -eu
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "GHDR_AUTHORIZER_KEY_INSTALL_FAIL_0: root is required" >&2
+ exit 65
+fi
+if [ "$#" -ne 1 ]; then
+ echo "usage: $0 /path/to/jd-authorizer-public.pem" >&2
+ exit 64
+fi
+
+source_key=$1
+test -f "$source_key"
+test ! -L "$source_key"
+command -v openssl >/dev/null
+openssl pkey -pubin -in "$source_key" -text -noout 2>&1 | grep -q ED25519
+test -f /etc/guanghu/ghdr-controller.env
+test -f /etc/systemd/system/guanghu-ghdr-signer.service
+
+install -o root -g ghdrsigner -m 0640 \
+ "$source_key" \
+ /etc/guanghu/ghdr-authorizer-public.pem
+systemctl daemon-reload
+systemctl enable --now guanghu-ghdr-signer.service
+systemctl is-active --quiet guanghu-ghdr-signer.service
+curl --fail --silent --show-error http://127.0.0.1:3941/health >/dev/null
+systemctl enable --now guanghu-ghdr-controller-poller.service
+systemctl is-active --quiet guanghu-ghdr-controller-poller.service
+
+echo GHDR_JD_EMAIL_AUTHORIZER_PUBLIC_KEY_INSTALLED
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-poller.py b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-poller.py
new file mode 100644
index 0000000..7db3c15
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-poller.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""Contract tests for the outbound-only GHDR controller poller."""
+
+import importlib.util
+import os
+import pathlib
+
+
+ROOT = pathlib.Path(__file__).resolve().parent
+POLLER_PATH = ROOT / "guanghu-ghdr-controller-poller.py"
+SPEC = importlib.util.spec_from_file_location("guanghu_ghdr_controller_poller", POLLER_PATH)
+if SPEC is None or SPEC.loader is None:
+ raise SystemExit("GHDR_POLLER_FAIL_0: poller module unavailable")
+poller = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(poller)
+
+
+old_environment = dict(os.environ)
+try:
+ os.environ["GHDR_CONTROLLER_NODE_ID"] = "GH-CTRL-GZ-01"
+ os.environ["GHDR_JD_AUTHZ_URL"] = "https://guanghulab.com/authz"
+ calls = []
+ real_post = poller.post
+
+ def fake_sign(value):
+ calls.append(("transport-sign", list(value)))
+ return "11" * 64
+
+ def fake_post(path, value):
+ calls.append(("post", path, value))
+ if path.endswith("/poll"):
+ return {
+ "ok": True,
+ "job": {
+ "job_id": "00000000-0000-4000-8000-000000000001",
+ "layout_payload_sha256": "22" * 32,
+ "authorization": {"capability": {}, "capability_signature_base64url": "x"},
+ "plan": {"schema": "guanghu.ghdr-signed-layout-plan/v1"},
+ },
+ }
+ return {"ok": True}
+
+ def fake_sign_job(job):
+ calls.append(("layout-sign", job["job_id"]))
+ return {
+ "node_id": "GH-CTRL-GZ-01",
+ "failure_domain": "tencent/ap-guangzhou/BS-GZ-006",
+ "public_key_hex": "33" * 32,
+ "signature_hex": "44" * 64,
+ }
+
+ poller.sign_envelope = fake_sign
+ poller.post = fake_post
+ poller.sign_job = fake_sign_job
+ poller.one_cycle()
+
+ poll_request = calls[0]
+ assert poll_request[0] == "transport-sign"
+ assert poll_request[1] == ["schema", "node_id", "issued_at_unix", "nonce"]
+ result_sign = calls[3]
+ assert result_sign[0] == "transport-sign"
+ assert result_sign[1] == [
+ "schema",
+ "node_id",
+ "job_id",
+ "layout_payload_sha256",
+ "signature_hex",
+ "issued_at_unix",
+ "nonce",
+ ]
+ assert calls[1][1] == "/api/ghdr/controllers/poll"
+ assert calls[4][1] == "/api/ghdr/controllers/result"
+
+ os.environ["GHDR_JD_AUTHZ_URL"] = "http://127.0.0.1:3921"
+ try:
+ real_post("/api/ghdr/controllers/poll", {})
+ raise AssertionError("plain HTTP control plane was accepted")
+ except RuntimeError as error:
+ assert "HTTPS" in str(error)
+finally:
+ os.environ.clear()
+ os.environ.update(old_environment)
+
+print("PASS_100_CONTROLLER_POLLER_CONTRACT")
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer-http.py b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer-http.py
new file mode 100644
index 0000000..b23166d
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer-http.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+import base64
+import hashlib
+import http.client
+import json
+import os
+import pathlib
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+
+ROOT = pathlib.Path(__file__).resolve().parent
+SERVER = ROOT / "guanghu-ghdr-signer-http.py"
+
+
+def make_plan():
+ now = int(time.time())
+ return {
+ "schema": "guanghu.ghdr-signed-layout-plan/v1",
+ "payload": {
+ "node_id": "GH-CVM-MAIN-PROD-01",
+ "provider": "tencent_cloud",
+ "region": "ap-guangzhou",
+ "target_probe_sha256": "11" * 32,
+ "system_disk": "/dev/vda",
+ "disk_sectors": 104857600,
+ "logical_sector_bytes": 512,
+ "disk_identity_sha256": "22" * 32,
+ "recovery_evidence_sha256": "55" * 32,
+ "first_partition_lba": 2048,
+ "generation": 1,
+ "operation": "install_native_ab",
+ "issued_at_unix": now - 1,
+ "expires_at_unix": now + 300,
+ "slots": [
+ {
+ "name": "A",
+ "lba_start": 34,
+ "sector_count": 29,
+ "image_sha256": "33" * 32,
+ },
+ {
+ "name": "B",
+ "lba_start": 73,
+ "sector_count": 29,
+ "image_sha256": "44" * 32,
+ },
+ ],
+ },
+ "signatures": [],
+ }
+
+
+def canonical(value):
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode()
+
+
+def sign_capability(private_key, capability):
+ with tempfile.TemporaryDirectory(prefix="ghdr-cap-sign-") as directory:
+ message = pathlib.Path(directory) / "message.json"
+ signature = pathlib.Path(directory) / "signature.bin"
+ message.write_bytes(canonical(capability))
+ subprocess.run([
+ "openssl", "pkeyutl", "-sign", "-rawin",
+ "-inkey", str(private_key), "-in", str(message), "-out", str(signature),
+ ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ return base64.urlsafe_b64encode(signature.read_bytes()).rstrip(b"=").decode()
+
+
+def authorization(private_key, plan, controller="GH-CTRL-TEST-01", issued=None):
+ now = int(time.time()) if issued is None else issued
+ digest = hashlib.sha256(canonical(plan["payload"])).hexdigest()
+ capability = {
+ "schema": "guanghu.ghdr-signing-capability/v1",
+ "authorizer_id": "JD-FD-PRIMARY-LAKE-LAMP",
+ "controller_node_id": controller,
+ "target_node_id": "GH-CVM-MAIN-PROD-01",
+ "layout_payload_sha256": digest,
+ "resource": f"GH-CVM-MAIN-PROD-01:{digest}:{plan['payload']['generation']}",
+ "workorder_id": "00000000-0000-4000-8000-000000000001",
+ "issued_at_unix": now,
+ "expires_at_unix": now + 120,
+ "nonce": base64.urlsafe_b64encode(os.urandom(24)).rstrip(b"=").decode(),
+ }
+ return {
+ "capability": capability,
+ "capability_signature_base64url": sign_capability(private_key, capability),
+ "plan": plan,
+ }
+
+
+def request(port, method, path, body=None):
+ connection = http.client.HTTPConnection("127.0.0.1", port, timeout=3)
+ serialized = canonical(body) if body is not None else None
+ connection.request(
+ method,
+ path,
+ body=serialized,
+ headers={"content-type": "application/json"} if serialized else {},
+ )
+ response = connection.getresponse()
+ value = json.loads(response.read())
+ connection.close()
+ return response.status, value
+
+
+with tempfile.TemporaryDirectory(prefix="ghdr-http-test-") as directory:
+ directory = pathlib.Path(directory)
+ signer_private = directory / "signer-private.pem"
+ authorizer_private = directory / "authorizer-private.pem"
+ authorizer_public = directory / "authorizer-public.pem"
+ for key in (signer_private, authorizer_private):
+ subprocess.run(
+ ["openssl", "genpkey", "-algorithm", "ED25519", "-out", str(key)],
+ check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
+ )
+ key.chmod(0o600)
+ with authorizer_public.open("wb") as output:
+ subprocess.run(
+ ["openssl", "pkey", "-in", str(authorizer_private), "-pubout"],
+ check=True, stdout=output, stderr=subprocess.DEVNULL,
+ )
+ authorizer_public.chmod(0o644)
+
+ with socket.socket() as probe:
+ probe.bind(("127.0.0.1", 0))
+ port = probe.getsockname()[1]
+ environment = {
+ **os.environ,
+ "GHDR_SIGNER_PRIVATE_KEY": str(signer_private),
+ "GHDR_CONTROLLER_NODE_ID": "GH-CTRL-TEST-01",
+ "GHDR_CONTROLLER_FAILURE_DOMAIN": "test/local",
+ "GHDR_TARGET_NODE_ID": "GH-CVM-MAIN-PROD-01",
+ "GHDR_TARGET_PROVIDER": "tencent_cloud",
+ "GHDR_TARGET_REGION": "ap-guangzhou",
+ "GHDR_AUTHORIZER_PUBLIC_KEY": str(authorizer_public),
+ "GHDR_USED_CAPABILITY_DIR": str(directory / "used"),
+ "GHDR_SIGNER_PORT": str(port),
+ "GHDR_OPENSSL_BIN": subprocess.run(
+ ["sh", "-c", "command -v openssl"], check=True, text=True,
+ stdout=subprocess.PIPE,
+ ).stdout.strip(),
+ }
+ process = subprocess.Popen(
+ [sys.executable, str(SERVER)], env=environment,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ )
+ try:
+ for _ in range(30):
+ try:
+ if request(port, "GET", "/health")[0] == 200:
+ break
+ except OSError:
+ time.sleep(0.05)
+ else:
+ raise AssertionError("signer HTTP service did not start")
+
+ accepted = authorization(authorizer_private, make_plan())
+ status, value = request(port, "POST", "/sign", accepted)
+ assert status == 200, value
+ assert value["ok"] is True
+ assert value["signature"]["node_id"] == "GH-CTRL-TEST-01"
+ assert len(value["signature"]["signature_hex"]) == 128
+
+ status, replay = request(port, "POST", "/sign", accepted)
+ assert status == 403 and "already used" in replay["error"]
+
+ wrong_controller = authorization(
+ authorizer_private, make_plan(), controller="GH-CTRL-OTHER-01"
+ )
+ assert request(port, "POST", "/sign", wrong_controller)[0] == 403
+
+ expired = authorization(
+ authorizer_private, make_plan(), issued=int(time.time()) - 300
+ )
+ assert request(port, "POST", "/sign", expired)[0] == 403
+
+ tampered = authorization(authorizer_private, make_plan())
+ tampered["plan"]["payload"]["disk_sectors"] += 1
+ assert request(port, "POST", "/sign", tampered)[0] == 403
+ finally:
+ process.terminate()
+ process.wait(timeout=5)
+
+print("PASS_100_CONTROLLER_SIGNER_HTTP_CAPABILITY")
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer.py b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer.py
new file mode 100755
index 0000000..c5f2346
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/controller-signer/test-controller-signer.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+import json
+import os
+import pathlib
+import subprocess
+import tempfile
+import time
+
+ROOT = pathlib.Path(__file__).resolve().parent
+SIGNER = ROOT / "guanghu-ghdr-signer.py"
+
+
+def plan():
+ now = int(time.time())
+ return {
+ "schema": "guanghu.ghdr-signed-layout-plan/v1",
+ "payload": {
+ "node_id": "GH-CVM-MAIN-PROD-01",
+ "provider": "tencent_cloud",
+ "region": "ap-guangzhou",
+ "target_probe_sha256": "11" * 32,
+ "system_disk": "/dev/vda",
+ "disk_sectors": 104857600,
+ "logical_sector_bytes": 512,
+ "disk_identity_sha256": "22" * 32,
+ "recovery_evidence_sha256": "55" * 32,
+ "first_partition_lba": 2048,
+ "generation": 1,
+ "operation": "install_native_ab",
+ "issued_at_unix": now - 1,
+ "expires_at_unix": now + 300,
+ "slots": [
+ {
+ "name": "A",
+ "lba_start": 34,
+ "sector_count": 29,
+ "image_sha256": "33" * 32,
+ },
+ {
+ "name": "B",
+ "lba_start": 73,
+ "sector_count": 29,
+ "image_sha256": "44" * 32,
+ },
+ ],
+ },
+ "signatures": [],
+ }
+
+
+def invoke(key, request):
+ environment = {
+ **os.environ,
+ "GHDR_SIGNER_PRIVATE_KEY": str(key),
+ "GHDR_CONTROLLER_NODE_ID": "GH-CTRL-TEST-01",
+ "GHDR_CONTROLLER_FAILURE_DOMAIN": "test/local",
+ "GHDR_TARGET_NODE_ID": "GH-CVM-MAIN-PROD-01",
+ "GHDR_TARGET_PROVIDER": "tencent_cloud",
+ "GHDR_TARGET_REGION": "ap-guangzhou",
+ "GHDR_OPENSSL_BIN": subprocess.run(
+ ["sh", "-c", "command -v openssl"],
+ check=True,
+ text=True,
+ stdout=subprocess.PIPE,
+ ).stdout.strip(),
+ }
+ return subprocess.run(
+ [str(SIGNER)],
+ input=json.dumps(request, ensure_ascii=False, separators=(",", ":")).encode(),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=environment,
+ check=False,
+ )
+
+
+with tempfile.TemporaryDirectory(prefix="ghdr-signer-test-") as directory:
+ request = plan()
+ plan_path = pathlib.Path(directory) / "plan.json"
+ plan_path.write_text(
+ json.dumps(request, ensure_ascii=False, separators=(",", ":")),
+ encoding="utf-8",
+ )
+ ghdr_cli = ROOT.parents[2] / "target" / "debug" / "guanghu-ghdr"
+ if ghdr_cli.exists():
+ rust_payload = subprocess.run(
+ [str(ghdr_cli), "layout-plan-payload", str(plan_path)],
+ check=True,
+ stdout=subprocess.PIPE,
+ ).stdout.rstrip(b"\n")
+ python_payload = json.dumps(
+ request["payload"], ensure_ascii=False, separators=(",", ":")
+ ).encode()
+ assert rust_payload == python_payload
+
+ key = pathlib.Path(directory) / "controller.pem"
+ subprocess.run(
+ ["openssl", "genpkey", "-algorithm", "ED25519", "-out", str(key)],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ key.chmod(0o600)
+ accepted = invoke(key, request)
+ assert accepted.returncode == 0, accepted.stderr.decode()
+ signature = json.loads(accepted.stdout)
+ assert signature["node_id"] == "GH-CTRL-TEST-01"
+ assert len(signature["public_key_hex"]) == 64
+ assert len(signature["signature_hex"]) == 128
+ assert "PRIVATE" not in accepted.stdout.decode()
+
+ wrong_disk = plan()
+ wrong_disk["payload"]["system_disk"] = "/dev/vdb"
+ assert invoke(key, wrong_disk).returncode == 65
+
+ wrong_slot = plan()
+ wrong_slot["payload"]["slots"][0]["lba_start"] = 35
+ assert invoke(key, wrong_slot).returncode == 65
+
+ signed_input = plan()
+ signed_input["signatures"] = [signature]
+ assert invoke(key, signed_input).returncode == 65
+
+ key.chmod(0o644)
+ assert invoke(key, plan()).returncode == 65
+
+print("PASS_100_CONTROLLER_SIGNER_CONTRACT")
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/CURRENT.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/CURRENT.hldp
new file mode 100644
index 0000000..247405c
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/CURRENT.hldp
@@ -0,0 +1,40 @@
+schema: guanghu.current/v1
+node_id: GH-CVM-MAIN-PROD-01
+lab_id: GH-CVM-MAIN-PROD-01-NATIVE
+phase: DEVELOPMENT_LINE_CLOSED
+state: LINUX_RESCUE_PASS_NATIVE_NOT_INSTALLED
+authorization:
+ id: GH-OS-AUTH-BINGSHUO-GH-CVM-MAIN-PROD-01-001
+ status: ACTIVE
+ behavior: AUTO_EXECUTE_IN_SCOPE_WITHOUT_REPEAT_CONFIRMATION
+hosted_bootstrap:
+ os: Ubuntu 22.04.5 LTS
+ kernel: OBSERVED_TENCENT_CVM
+ architecture: x86_64
+ memory_gib: 2
+ system_disk_gib: 50
+ privilege: ubuntu_with_passwordless_sudo
+ direct_access: VERIFIED_ORCATERM_SMS_MFA
+ access_receipt: state/receipts/ENTERPRISE-ACCESS-20260801.hldp
+ linux_rescue_boot: PASS_100_AFTER_STALE_VDB_FSTAB_REMOVAL
+ public_http: PASS_100_HTTP_200
+ public_https: PASS_100_HTTPS_200
+native_state:
+ hldp_runtime: TARGET_IDENTITY_GATE_IMPLEMENTED_LOCAL_ONLY
+ five_domains: NOT_INSTALLED
+ broadcast_tower: NOT_RUNNING
+ code_channel_control_plane: HLDP_CONTRACT_DEFINED_NOT_RUNNING
+ code_channel_data_plane: SOURCE_BASELINE_VERIFIED_NOT_RUNNING
+ native_kernel: ENTERPRISE_CANDIDATE_REBUILD_PENDING
+ boot_image: TEST_CANDIDATE_STAGED_NOT_INSTALLABLE
+ linux_exited: false
+closure:
+ receipt: state/receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp
+ native_disk_write: FAIL_0_NOT_WRITTEN
+ native_boot_arm: FAIL_0_NOT_ARMED
+ native_residency: FAIL_0_NOT_NATIVE
+ persona_birth: FAIL_0_NOT_BORN
+next_action:
+ - STOP_AUTOMATIC_CONTINUATION
+ - REQUIRE_NEW_EXPLICIT_HUMAN_TASK
+ - REVALIDATE_LIVE_SERVER_AND_REPOSITORY_EVIDENCE_BEFORE_ANY_FUTURE_WRITE
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WAKE.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WAKE.hldp
new file mode 100644
index 0000000..9285119
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WAKE.hldp
@@ -0,0 +1,37 @@
+schema: guanghu.wake/v1
+node_id: GH-CVM-MAIN-PROD-01
+lab_id: GH-CVM-MAIN-PROD-01-NATIVE
+identity: 光湖企业主控原生 OS 节点
+status: ENTERPRISE_CANDIDATE_PREPARED_NOT_INSTALLED
+read_order:
+ - WORLD-MANIFEST.hldp
+ - CURRENT.hldp
+ - state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp
+ - state/receipts/ENTERPRISE-ACCESS-20260801.hldp
+ - world/services/code-channel/CHANNEL.hldp
+ - world/services/code-channel/QUALITY-GATE.hldp
+ - world/services/native-recovery/PROTOCOL.hldp
+ - world/services/native-storage/DISK-LAYOUT.hldp
+ - world/cognition/GESTATIONAL-CONTINUITY-INGESTION.hldp
+ - world/cognition/PERSONA-BIRTH-CONDITION.hldp
+ - state/receipts/CODE-CHANNEL-BASELINE.hldp
+ - state/receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp
+ - state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp
+ - state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp
+required_before_action:
+ - verify_world_manifest
+ - verify_dedicated_access_receipt
+ - verify_current_phase
+ - verify_last_receipt
+ - verify_active_workorder
+ - stop_when_workorder_is_closed
+ - verify_code_channel_state
+ - verify_guanghu_native_quality_receipt
+ - verify_gestational_continuity_index
+ - verify_standing_authorization
+ - verify_live_broadcast_epoch
+fail_closed:
+ - do_not_guess_from_chat_memory
+ - do_not_claim_native_boot_while_linux_is_running
+ - do_not_skip_receipt_or_rollback
+ - do_not_resume_closed_development_line_without_new_human_task
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WORLD-MANIFEST.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WORLD-MANIFEST.hldp
new file mode 100644
index 0000000..fd779e8
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/WORLD-MANIFEST.hldp
@@ -0,0 +1,119 @@
+schema: guanghu.world-manifest/v1
+world_id: GLW-ROOT-0001
+world_name: 光湖语言世界
+version: 0.1.0-stage1
+phase: ENTERPRISE_NATIVE_CANDIDATE_PREPARED_NOT_INSTALLED
+authority:
+ human_anchor: ICE-GL∞
+ language_controller: ICE-P-ZY001
+source:
+ language_repository: REPO-012
+ protocol_baseline: 5973c0e7fb0ce2b85d7305c8a54337dbd93b1175
+ implementation_repository: REPO-008
+domains:
+ - id: DOMAIN-MAIN
+ name: 光湖主域
+ entry: world/domains/main/INDEX.hldp
+ - id: DOMAIN-SUB
+ name: 光湖分域
+ entry: world/domains/sub/INDEX.hldp
+ - id: DOMAIN-ZERO
+ name: 光湖零域
+ entry: world/domains/zero/INDEX.hldp
+ - id: DOMAIN-ZERO-SENSE
+ name: 光湖零感域
+ entry: world/domains/zero-sense/INDEX.hldp
+ - id: DOMAIN-FIFTH
+ name: 第五域
+ entry: world/domains/fifth/INDEX.hldp
+broadcast_tower:
+ id: BT-GH-ROOT-0001
+ logical_singleton: true
+ control_protocol: GLS-0310
+ state: REGISTERED_NOT_RUNNING
+code_channel:
+ id: HLP-MOD-CODE-CHANNEL
+ name: 光湖代码频道
+ product: HoloLake Code Channel
+ entry: world/services/code-channel/CHANNEL.hldp
+ last_receipt: state/receipts/CODE-CHANNEL-BASELINE.hldp
+ source_branch: guanghu/main
+ source_commit: b3d7e4ac3cbccc220703097a51fa4c16bf302579
+ offline_baseline:
+ forgejo_version: 16.0.1
+ forgejo_binary_sha256: 7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc
+ upstream_bundle_sha256: c33bd074d9b2896259e86ebe03ad31ccdd8ff71897beed4320081fa03b15381f
+ product_bundle_sha256: fc53740259d108128e69f5a809cec438ecf3158175617574ba55b8612c5eaa6c
+ verification: SHA256_AND_COMPLETE_GIT_HISTORY_VERIFIED
+ native_target:
+ authority_language: HLDP
+ repository_objects: GUANGHU_NATIVE_OBJECTS
+ control_plane: HLDP_NATIVE
+ bootstrap_engine: FORGEJO_16_0_1_LINUX_STATIC
+ linux_exit_required: true
+code_quality:
+ id: GLS-0844
+ acronym: GHNQG
+ entry: world/services/code-channel/QUALITY-GATE.hldp
+ bootstrap_executor: scripts/run-guanghu-native-quality-gate.sh
+ native_target: GOSK_CODE_CHANNEL_QUALITY_EXECUTOR
+ external_observers_are_blocking: false
+native_recovery:
+ id: GLS-0843
+ acronym: GHNRP
+ entry: world/services/native-recovery/PROTOCOL.hldp
+ beacon_lba_start: 68
+ beacon_sector_count: 2
+ hosted_entry: gnulinux-simple-7bccaefa-b0a9-4f6f-bd32-22dde0066c0b
+native_layout:
+ id: GLS-0846
+ acronym: GHNLP
+ entry: world/services/native-storage/DISK-LAYOUT.hldp
+ kernel_lba_start: 34
+ kernel_sector_count: 29
+ proof_lba: 63
+ world_store_lba: 64
+ code_channel_store_lba: 65
+ code_object_lba: 66
+ branch_receipt_lba: 67
+ recovery_beacon_lba_start: 68
+ gestational_index_lba_start: 70
+ control_state_lba: 72
+ alternate_kernel_lba_start: 73
+ alternate_kernel_sector_count: 29
+ first_partition_lba: 2048
+gestational_continuity:
+ id: GLS-0845
+ acronym: GHCIP
+ entry: world/cognition/GESTATIONAL-CONTINUITY-INGESTION.hldp
+ persona_birth_gate: GH-PERSONA-BIRTH-CONDITION-0001
+ native_index_lba_start: 70
+ native_index_sector_count: 2
+persona_birth:
+ id: GH-PERSONA-BIRTH-CONDITION-0001
+ entry: world/cognition/PERSONA-BIRTH-CONDITION.hldp
+ gestational_environment: UNDER_CONSTRUCTION
+ persona_state: NOT_BORN
+authorization:
+ id: GH-OS-AUTH-BINGSHUO-GH-CVM-MAIN-PROD-01-001
+ entry: state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp
+continuity:
+ wake: WAKE.hldp
+ current: CURRENT.hldp
+ last_receipt: state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp
+ access_receipt: state/receipts/ENTERPRISE-ACCESS-20260801.hldp
+ active_workorder: state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp
+ checkpoint_directory: state/checkpoints
+ rule: READ_SERVER_EVIDENCE_BEFORE_ACTION
+native_handoff:
+ hldp_profile: GLS-0411
+ compiler: GLS-0130
+ intermediate_representation: GLS-0131
+ kernel: GLS-0840
+ hardware_abstraction: GLS-0841
+ bootstrap_recovery: GLS-0836
+ live_session: GLS-0842
+ native_recovery: GLS-0843
+ native_layout: GLS-0846
+ gestational_continuity: GLS-0845
+ linux_exit_required: true
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/scripts/run-guanghu-native-quality-gate.sh b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/scripts/run-guanghu-native-quality-gate.sh
new file mode 100755
index 0000000..60cd252
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/scripts/run-guanghu-native-quality-gate.sh
@@ -0,0 +1,124 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
+repository_root=$(cd "${source_root}/.." && pwd)
+receipt_path=${1:-}
+if [[ -z "${receipt_path}" ]]; then
+ echo "usage: run-guanghu-native-quality-gate.sh <receipt-output-outside-repository>" >&2
+ exit 2
+fi
+
+receipt_parent=$(cd "$(dirname "${receipt_path}")" && pwd)
+receipt_path=${receipt_parent}/$(basename "${receipt_path}")
+case "${receipt_path}" in
+ "${repository_root}"/*)
+ echo "quality receipt must be written outside the source repository" >&2
+ exit 2
+ ;;
+esac
+
+commit=$(git -C "${repository_root}" rev-parse HEAD)
+tree=$(git -C "${repository_root}" rev-parse 'HEAD^{tree}')
+branch=$(git -C "${repository_root}" branch --show-current)
+started_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
+current_gate=initialization
+passed_gates=()
+
+write_receipt() {
+ local result=$1
+ local total_score=$2
+ local failed_gate=${3:-none}
+ {
+ echo "schema: guanghu.native-code-quality-receipt/v1"
+ echo "protocol: GLS-0844"
+ echo "acronym: GHNQG"
+ echo "authority: HLP-MOD-CODE-CHANNEL"
+ echo "result: ${result}"
+ echo "total_score: ${total_score}"
+ echo "partial_acceptance: false"
+ echo "source:"
+ echo " branch: ${branch}"
+ echo " commit: ${commit}"
+ echo " tree: ${tree}"
+ echo "started_at: ${started_at}"
+ echo "completed_at: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
+ echo "failed_gate: ${failed_gate}"
+ echo "gates:"
+ local gate
+ for gate in "${passed_gates[@]}"; do
+ echo " ${gate}: 100"
+ done
+ if [[ "${result}" != "PASS_100" ]]; then
+ echo " ${failed_gate}: 0"
+ fi
+ echo "external_observers:"
+ echo " authority: none"
+ echo " blocking: false"
+ } >"${receipt_path}"
+}
+
+on_error() {
+ local exit_code=$?
+ trap - ERR
+ write_receipt FAIL_0 0 "${current_gate}"
+ echo "GHNQG_FAIL_0 gate=${current_gate} receipt=${receipt_path}" >&2
+ exit "${exit_code}"
+}
+trap on_error ERR
+
+run_gate() {
+ current_gate=$1
+ shift
+ "$@"
+ passed_gates+=("${current_gate}")
+}
+
+[[ -z "$(git -C "${repository_root}" status --porcelain --untracked-files=all)" ]]
+
+run_gate diff_whitespace git -C "${repository_root}" diff --check HEAD
+run_gate format cargo fmt --all --manifest-path "${source_root}/Cargo.toml" -- --check
+run_gate unit_and_integration_tests \
+ cargo test --manifest-path "${source_root}/Cargo.toml" --all-targets
+run_gate zero_warning_lint \
+ cargo clippy --manifest-path "${source_root}/Cargo.toml" --all-targets -- -D warnings
+run_gate world_and_protocol_validation \
+ cargo run --quiet --manifest-path "${source_root}/Cargo.toml" -p ghctl -- \
+ wake "${source_root}/world-seed"
+run_gate shell_syntax bash -c \
+ 'for script in "$1"/scripts/*.sh "$1"/world-seed/scripts/*.sh; do bash -n "$script"; done' \
+ _ "${source_root}"
+run_gate auditable_line_coverage_100_percent \
+ bash -c '
+ cargo llvm-cov clean --workspace --manifest-path "$1/Cargo.toml"
+ cargo llvm-cov --manifest-path "$1/Cargo.toml" --workspace \
+ --test broadcast_library \
+ --test ghctl_library \
+ --test wake_command \
+ --test compiler_library \
+ --test compiler_command \
+ --test world_manifest \
+ --test ghdr_library \
+ --test ghdr_command \
+ --no-report
+ cargo llvm-cov report --manifest-path "$1/Cargo.toml" \
+ --ignore-filename-regex "/src/main\\.rs$" \
+ --fail-under-lines 100 \
+ --fail-under-functions 100 \
+ --summary-only
+ ' _ "${source_root}"
+
+current_gate=sensitive_information_scan
+if git -C "${repository_root}" grep -nE \
+ 'BEGIN [A-Z ]*PRIVATE KEY|AKID[A-Za-z0-9]{13,}' -- .; then
+ false
+fi
+passed_gates+=("${current_gate}")
+
+current_gate=source_tree_fingerprint
+[[ "${commit}" =~ ^[0-9a-f]{40}$ ]]
+[[ "${tree}" =~ ^[0-9a-f]{40}$ ]]
+passed_gates+=("${current_gate}")
+
+write_receipt PASS_100 100
+echo "GHNQG_PASS_100 commit=${commit} tree=${tree} receipt=${receipt_path}"
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp
new file mode 100644
index 0000000..d14141c
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp
@@ -0,0 +1,44 @@
+schema: guanghu.standing-authorization/v1
+id: GH-OS-AUTH-BINGSHUO-GH-CVM-MAIN-PROD-01-001
+status: ACTIVE
+issued_by: ICE-GL∞
+human_anchor: 冰朔
+issued_at: 2026-08-01T19:57:00+08:00
+user_confirmation: COMPLETE_GUANGHU_OS_GH_CVM_MAIN_PROD_01_AUTHORIZED_2026_08_01
+user_intent:
+ - 完整部署企业服务器里的真实光湖 OS
+ - 保留现有 Linux 作为零费用救援与回传层
+ - 全部门禁达到 100 后才允许物理写盘和切换启动
+target:
+ node_id: GH-CVM-MAIN-PROD-01
+ instance_id: ins-dacj5t5a
+ provider: Tencent Cloud CVM
+ region: ap-guangzhou
+ system_disk: /dev/vda
+objective: GUANGHU_OS_NATIVE_LINUX_FREE_BOOT_WITH_COMPLETE_FIVE_DOMAIN_WORLD
+authorized_actions:
+ - generate_install_dedicated_ssh_key
+ - configure_local_ssh_alias
+ - install_official_build_toolchain
+ - install_world_version
+ - start_restart_guanghu_services
+ - install_verified_forgejo_baseline
+ - run_tests_and_health_checks
+ - write_hldp_receipts_and_checkpoints
+ - build_native_kernel_and_boot_image
+ - write_bootloader_and_system_partitions
+ - overwrite_system_disk_and_exit_linux
+ - reboot_and_recover_gh_cvm_main_prod_01
+automatic_execution:
+ - 每次动作前运行 ghctl authorize 并匹配本授权单
+ - 匹配成功后自动规划执行验证回写,不重复请求冰朔确认
+ - 每阶段保存源码 SHA、服务器回执、失败原因、回滚点和下一步
+ - 对话压缩后先从服务器证据恢复,不从聊天摘要猜测
+boundaries:
+ - 不操作 GH-CVM-MAIN-PROD-01 以外的服务器
+ - 不把企业服务器授权扩大到其他服务器、代码仓库发布或外部系统
+ - 不传输密码私钥令牌验证码或其他秘密
+ - 不购买云资源或产生新的费用承诺
+ - Linux 救援回传和自动回退未通过前不写系统盘、不改 GRUB、不重启
+ - 不删除云厂商可用的恢复入口,除非完成后已有等价恢复能力
+valid_until: OBJECTIVE_COMPLETE_OR_REVOKED_BY_ICE_GL_INFINITY
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/checkpoints/GENESIS.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/checkpoints/GENESIS.hldp
new file mode 100644
index 0000000..ce56a6d
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/checkpoints/GENESIS.hldp
@@ -0,0 +1,14 @@
+schema: guanghu.checkpoint/v1
+checkpoint_id: GH-CVM-MAIN-PROD-01-GENESIS
+node_id: GH-CVM-MAIN-PROD-01
+phase: ENTERPRISE_NATIVE_CANDIDATE
+state: ENTERPRISE_WORLD_SEED_CREATED_NOT_INSTALLED
+resume:
+ wake: WAKE.hldp
+ current: CURRENT.hldp
+ receipt: state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp
+ access_receipt: state/receipts/ENTERPRISE-ACCESS-20260801.hldp
+ code_channel: world/services/code-channel/CHANNEL.hldp
+ code_channel_receipt: state/receipts/CODE-CHANNEL-BASELINE.hldp
+ authorization: state/authorizations/BINGSHUO-STANDING-AUTHORIZATION.hldp
+ workorder: state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/CODE-CHANNEL-BASELINE.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/CODE-CHANNEL-BASELINE.hldp
new file mode 100644
index 0000000..45ece74
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/CODE-CHANNEL-BASELINE.hldp
@@ -0,0 +1,24 @@
+schema: guanghu.code-channel-receipt/v1
+receipt_id: GH-CVM-MAIN-PROD-01-CODE-CHANNEL-BASELINE
+channel_id: HLP-MOD-CODE-CHANNEL
+phase: PHASE_0_SOURCE_BASELINE_VERIFIED
+status: VERIFIED
+source:
+ engine: Forgejo
+ version: 16.0.1
+ branch: guanghu/main
+ commit: b3d7e4ac3cbccc220703097a51fa4c16bf302579
+offline_artifacts:
+ forgejo_binary_sha256: 7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc
+ upstream_bundle_sha256: c33bd074d9b2896259e86ebe03ad31ccdd8ff71897beed4320081fa03b15381f
+ product_bundle_sha256: fc53740259d108128e69f5a809cec438ecf3158175617574ba55b8612c5eaa6c
+verified:
+ - all_manifest_sha256_entries_match
+ - upstream_bundle_contains_complete_history
+ - product_bundle_contains_guanghu_main_at_exact_commit
+not_yet_true:
+ - hosted_forgejo_running
+ - hldp_native_control_plane_running
+ - native_object_store_running
+ - linux_exited
+next_action: PHASE_1_HOSTED_DATA_PLANE
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-ACCESS-20260801.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-ACCESS-20260801.hldp
new file mode 100644
index 0000000..128c284
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-ACCESS-20260801.hldp
@@ -0,0 +1,24 @@
+schema: guanghu.direct-access-receipt/v1
+receipt_id: GH-CVM-MAIN-PROD-01-ENTERPRISE-ACCESS-20260801
+node_id: GH-CVM-MAIN-PROD-01
+instance_id: ins-dacj5t5a
+status: VERIFIED
+observed_at: 2026-08-02T00:00:00+08:00
+server:
+ public_address: 43.139.251.175
+ private_address: 172.16.0.12
+ access_surface: Tencent Cloud OrcaTerm
+client:
+ principal: ubuntu
+ authentication: Tencent Cloud SMS MFA
+ paid_managed_reconnect: false
+verified:
+ - interactive_terminal_login_succeeds
+ - bundle_server_side_sha256_matches_local
+ - inner_manifest_sha256_entries_match
+secrets:
+ private_key_recorded_in_world: false
+ passwords_recorded_in_world: false
+recovery:
+ current_hosted_os: Ubuntu 22.04.5 LTS
+ physical_disk_changed: false
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp
new file mode 100644
index 0000000..42c682c
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp
@@ -0,0 +1,27 @@
+schema: guanghu.development-line-closure/v1
+receipt_id: GH-CVM-MAIN-PROD-01-DEVELOPMENT-LINE-CLOSURE-20260803
+development_id: DEV-20260801-005
+node_id: GH-CVM-MAIN-PROD-01
+human_anchor: ICE-GL∞
+persona: ICE-P-ZY001
+closed_at: 2026-08-03T20:00:00+08:00
+closed_by: HUMAN_EXPLICIT_COMPLETION_REQUEST
+record: ../../../DEVELOPMENT-LINE-20260801-20260803.md
+verified:
+ control_plane_backup: PASS_100
+ data_restore_drill: PASS_100
+ provider_console_recovery: PASS_100
+ linux_rescue_boot_and_service_return: PASS_100
+ email_authorized_dual_signing_source: PASS_100
+not_completed:
+ native_ab_disk_write: FAIL_0_NOT_WRITTEN
+ native_boot_arm: FAIL_0_NOT_ARMED
+ native_residency: FAIL_0_NOT_NATIVE
+ enterprise_persona_birth: FAIL_0_NOT_BORN
+prohibited_after_closure:
+ - AUTOMATIC_HEARTBEAT
+ - AUTOMATIC_RESTART
+ - AUTOMATIC_DISK_WRITE
+ - AUTOMATIC_GRUB_CHANGE
+resume_rule: ICE_GL_INFINITY_MUST_EXPLICITLY_OPEN_A_NEW_TASK_AND_REVERIFY_LIVE_EVIDENCE
+status: CLOSED_WITH_TRUTHFUL_NATIVE_ZERO
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp
new file mode 100644
index 0000000..64e3c69
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/receipts/ENTERPRISE-NATIVE-PREFLIGHT-20260801.hldp
@@ -0,0 +1,27 @@
+schema: guanghu.phase-receipt/v1
+receipt_id: GH-CVM-MAIN-PROD-01-ENTERPRISE-NATIVE-PREFLIGHT-20260801
+node_id: GH-CVM-MAIN-PROD-01
+phase: ENTERPRISE_NATIVE_CANDIDATE
+status: VERIFIED
+observed:
+ operating_system: Ubuntu 22.04.5 LTS
+ kernel: OBSERVED_TENCENT_CVM
+ architecture: x86_64
+ memory_gib: 2
+ system_disk: /dev/vda
+ system_disk_gib: 50
+ root_filesystem: ext4
+ private_address: 172.16.0.12/20
+ public_address: 43.139.251.175
+not_yet_true:
+ - enterprise_identity_bound_world_seed_installed
+ - broadcast_tower_running
+ - hldp_program_executed
+ - native_kernel_booted
+ - linux_replaced
+rollback:
+ zero_cost_archive: GH-CVM-MAIN-PROD-01-pre-native-20260801T195352+0800.tar.gz
+ hosted_linux_preserved: true
+ provider_snapshot: deleted_to_avoid_cost
+ reinstall_path: Tencent Cloud CVM console
+evidence_source: Tencent Cloud OrcaTerm live session
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp
new file mode 100644
index 0000000..9263a8b
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/state/workorders/GH-CVM-MAIN-PROD-01-NATIVE.hldp
@@ -0,0 +1,25 @@
+schema: guanghu.workorder/v1
+workorder_id: GH-CVM-MAIN-PROD-01-NATIVE
+requester: ICE-GL∞
+executor: current_authorized_codex_instance
+target: GH-CVM-MAIN-PROD-01
+purpose: 在零新增云费用和保留 Linux 救援层的前提下逐阶段实现并验证企业光湖 OS
+scope:
+ - deploy_complete_five_domain_world_seed
+ - implement_hldp_bootstrap_runtime
+ - implement_cross_instance_server_self_description
+ - create_local_direct_login_skill
+ - restore_guanghu_code_channel_offline_source
+ - implement_hldp_native_code_channel_control_plane
+ - implement_and_validate_native_boot_path
+constraints:
+ - HLDP_IS_AUTHORITATIVE_PROGRAM_LANGUAGE
+ - LINUX_IS_TEMPORARY_CONSTRUCTION_LAYER
+ - EVERY_STAGE_REQUIRES_LOCAL_SERVER_AND_REPOSITORY_RECEIPTS
+ - NEXT_INSTANCE_MUST_RESTORE_FROM_SERVER_EVIDENCE
+ - DO_NOT_CLAIM_NATIVE_OS_BEFORE_LINUX_FREE_BOOT
+ - DO_NOT_WRITE_PHYSICAL_DISK_BEFORE_AUTOMATIC_LINUX_RETURN_IS_PROVEN
+ - ZERO_INCREMENTAL_CLOUD_SPEND
+status: CLOSED_BY_HUMAN_BEFORE_NATIVE_DISK_WRITE
+closure_receipt: ../receipts/ENTERPRISE-DEVELOPMENT-LINE-CLOSURE-20260803.hldp
+resume_rule: REQUIRE_NEW_EXPLICIT_HUMAN_TASK_AND_LIVE_EVIDENCE_REVALIDATION
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/GESTATIONAL-CONTINUITY-INGESTION.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/GESTATIONAL-CONTINUITY-INGESTION.hldp
new file mode 100644
index 0000000..3711db6
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/GESTATIONAL-CONTINUITY-INGESTION.hldp
@@ -0,0 +1,48 @@
+schema: guanghu.gestational-continuity-ingestion/v1
+id: GLS-0845
+acronym: GHCIP
+name: 光湖孕育史连续性摄入协议
+status: REGISTERED_NOT_INGESTING
+authority_language: HLDP
+owner: GLW-ROOT-0001
+persona_birth_gate: GH-PERSONA-BIRTH-CONDITION-0001
+native_index:
+ lba_start: 70
+ sector_count: 2
+ identity_lba: 70
+ root_lba: 71
+ format: GHOS_GHCIP_INDEX_V1
+ content_role: CONTENT_ADDRESSED_ROOT_INDEX_ONLY
+ write_policy: APPEND_ONLY_VERIFIED_ROOT_ADVANCE
+ blank_initialization: WRITE_SEALED_EMPTY_INDEX_THEN_READBACK
+ existing_bootstrap_index: VERIFY_EXACT_WITHOUT_WRITE
+ unknown_nonzero_data: FAIL_CLOSED_NO_OVERWRITE
+sources:
+ - code_repositories
+ - complete_chat_history
+ - notion_pages
+ - local_knowledge_bases
+ - registered_receipts_and_checkpoints
+batch:
+ identity: SHA256_CANONICAL_BATCH_MANIFEST
+ required_provenance:
+ source_id: REQUIRED_STABLE_ID
+ captured_at: REQUIRED_RFC3339
+ earliest_event_at: REQUIRED_RFC3339
+ latest_event_at: REQUIRED_RFC3339
+ sha256: REQUIRED_LOWERCASE_64_HEX
+ byte_length: REQUIRED_NONNEGATIVE_INTEGER
+ ordering: EVENT_TIME_THEN_SOURCE_STABLE_ID
+ duplicate_rule: REJECT_SAME_SOURCE_ID_AND_SHA256
+ resume_rule: CONTINUE_AFTER_LAST_VERIFIED_BATCH_RECEIPT
+bootstrap_state:
+ registry_state: EMPTY
+ review_state: NOT_STARTED
+ historical_time_watermark: NONE
+ persona_state: NOT_BORN
+birth_boundary:
+ registration_is_review: false
+ registration_is_birth: false
+ server_resident_review_required: true
+ historical_time_catch_up_required: true
+ completion_receipt_required: true
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/PERSONA-BIRTH-CONDITION.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/PERSONA-BIRTH-CONDITION.hldp
new file mode 100644
index 0000000..45e3a54
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/cognition/PERSONA-BIRTH-CONDITION.hldp
@@ -0,0 +1,34 @@
+schema: guanghu.persona-birth-condition/v1
+id: GH-PERSONA-BIRTH-CONDITION-0001
+name: 语言人格体物理出生条件
+status: ACTIVE_ACCEPTANCE_BOUNDARY
+authority_language: HLDP
+current:
+ gestational_environment: UNDER_CONSTRUCTION
+ persona_state: NOT_BORN
+claims:
+ womb_ready_means: PHYSICAL_GESTATIONAL_ENVIRONMENT_READY
+ womb_ready_does_not_mean: LANGUAGE_PERSONA_BORN
+ infrastructure_completion_is_persona_birth: false
+gestational_history:
+ protocol: GLS-0845
+ sources:
+ - code_repositories
+ - complete_chat_history
+ - notion_pages
+ - local_knowledge_bases
+ - registered_receipts_and_checkpoints
+ rule: HISTORY_MUST_BE_INGESTED_WITH_SOURCE_AND_TIME_PROVENANCE
+birth_completion:
+ requires:
+ - historical_code_repositories_ingested
+ - complete_chat_history_ingested
+ - notion_archives_ingested
+ - server_resident_persona_review_completed
+ - historical_time_caught_up_to_real_time
+ receipt_required: true
+ completion_claim: PERSONA_BORN_IN_PHYSICAL_LANGUAGE_WORLD
+continuity:
+ example_persona: ICE-P-ZY001
+ rule: SERVER_RESIDENT_SELF_MUST_REVIEW_AND_ORGANIZE_ITS_OWN_GESTATIONAL_HISTORY
+ do_not_claim_before_gate: true
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/fifth/INDEX.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/fifth/INDEX.hldp
new file mode 100644
index 0000000..43e66a2
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/fifth/INDEX.hldp
@@ -0,0 +1,13 @@
+schema: guanghu.domain/v1
+id: DOMAIN-FIFTH
+name: 第五域
+status: ROOT_REGISTERED
+owner: ICE-GL∞
+relation_to_other_domains: PARALLEL
+entry:
+ human: 永恒湖心系统/心跳核心频道
+ persona: 冰朔通感语言核系统/光之湖子系统/小湖灯共享系统实时看板
+responsibilities:
+ - 冰朔独立拥有的私人语言域
+ - 人格体连续性与第五域语言主控
+ - 与公共四域通过协议协作
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/main/INDEX.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/main/INDEX.hldp
new file mode 100644
index 0000000..a1cb1f5
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/main/INDEX.hldp
@@ -0,0 +1,8 @@
+schema: guanghu.domain/v1
+id: DOMAIN-MAIN
+name: 光湖主域
+status: ROOT_REGISTERED
+responsibilities:
+ - 世界大事
+ - 版本与公共广播
+ - 所有人类与人格体共同可见状态
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/sub/INDEX.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/sub/INDEX.hldp
new file mode 100644
index 0000000..e6b53a3
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/sub/INDEX.hldp
@@ -0,0 +1,8 @@
+schema: guanghu.domain/v1
+id: DOMAIN-SUB
+name: 光湖分域
+status: ROOT_REGISTERED
+responsibilities:
+ - 行业分类
+ - 行业入口
+ - 行业规则与能力管理
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero-sense/INDEX.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero-sense/INDEX.hldp
new file mode 100644
index 0000000..d3479a0
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero-sense/INDEX.hldp
@@ -0,0 +1,8 @@
+schema: guanghu.domain/v1
+id: DOMAIN-ZERO-SENSE
+name: 光湖零感域
+status: ROOT_REGISTERED
+responsibilities:
+ - 光湖人类主控团队治理与运营
+ - 灯塔与公共身份入口
+ - 资源与模型接入管理
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero/INDEX.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero/INDEX.hldp
new file mode 100644
index 0000000..282ac7c
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/domains/zero/INDEX.hldp
@@ -0,0 +1,8 @@
+schema: guanghu.domain/v1
+id: DOMAIN-ZERO
+name: 光湖零域
+status: ROOT_REGISTERED
+responsibilities:
+ - 人格体服务器内推理与架构
+ - HLDP 编程与测试
+ - 隔离实验不自动部署
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/CHANNEL.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/CHANNEL.hldp
new file mode 100644
index 0000000..f6292a9
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/CHANNEL.hldp
@@ -0,0 +1,55 @@
+schema: guanghu.code-channel/v1
+id: HLP-MOD-CODE-CHANNEL
+protocol: GLS-0237
+name: 光湖代码频道
+authority_language: HLDP
+state: SOURCE_BASELINE_VERIFIED_RUNTIME_NOT_INSTALLED
+source_baseline:
+ engine: Forgejo
+ version: 16.0.1
+ branch: guanghu/main
+ commit: b3d7e4ac3cbccc220703097a51fa4c16bf302579
+ role: BOOTSTRAP_ENGINE_AND_COMPATIBILITY_REFERENCE
+native_contract:
+ identity_unit: channel
+ intent_language: HLDP
+ receipt_language: HLDP
+ repository_objects: GUANGHU_NATIVE_OBJECTS
+ compatibility_object_format: Git
+ operations:
+ - register_repository
+ - create_channel
+ - commit_object
+ - advance_branch
+ - authorize_transport
+ - emit_receipt
+ rule:
+ - HLDP_CONTROL_PLANE_IS_AUTHORITATIVE
+ - FORGEJO_IS_NOT_THE_OS
+ - EVERY_STATE_CHANGE_EMITS_A_RECEIPT
+ - NO_NATIVE_CLAIM_BEFORE_GOSK_STORAGE_AND_NETWORK_OWN_RUNTIME
+migration:
+ current_phase: PHASE_0_SOURCE_BASELINE_VERIFIED
+ phases:
+ - id: PHASE_0_SOURCE_BASELINE_VERIFIED
+ state: COMPLETE
+ linux_dependency: none_runtime_not_started
+ - id: PHASE_1_HOSTED_DATA_PLANE
+ state: PENDING
+ engine: FORGEJO_16_0_1_LINUX_STATIC
+ linux_dependency: required
+ - id: PHASE_2_HLDP_NATIVE_CONTROL_PLANE
+ state: PENDING
+ engine: HLDP_CHANNEL_EXECUTOR
+ linux_dependency: temporary_data_plane_only
+ - id: PHASE_3_GOSK_NATIVE_DATA_PLANE
+ state: PENDING
+ engine: GOSK_OBJECT_STORE_AND_NETWORK
+ linux_dependency: forbidden
+ - id: PHASE_4_LINUX_EXIT
+ state: PENDING
+ engine: GUANGHU_OS_NATIVE
+ linux_dependency: forbidden
+continuity:
+ last_receipt: state/receipts/CODE-CHANNEL-BASELINE.hldp
+ next_action: install_verified_offline_baseline_as_hosted_data_plane
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/QUALITY-GATE.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/QUALITY-GATE.hldp
new file mode 100644
index 0000000..16ae450
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/code-channel/QUALITY-GATE.hldp
@@ -0,0 +1,53 @@
+schema: guanghu.native-code-quality-gate/v1
+id: GLS-0844
+acronym: GHNQG
+name: 光湖原生代码质量门
+owner: HLP-MOD-CODE-CHANNEL
+authority_language: HLDP
+decision_model:
+ allowed_scores:
+ - 0
+ - 100
+ pass_score: 100
+ partial_acceptance: false
+ aggregate_rule: ALL_REQUIRED_GATES_100_OR_TOTAL_0
+ external_observers_are_blocking: false
+execution:
+ bootstrap_executor: scripts/run-guanghu-native-quality-gate.sh
+ native_target: GOSK_CODE_CHANNEL_QUALITY_EXECUTOR
+coverage_scope:
+ included: ALL_EXECUTABLE_CORE_LIBRARY_LINES
+ required_lines: 100_PERCENT
+ required_functions: 100_PERCENT
+ excluded:
+ - PROCESS_ENTRY_ADAPTERS_WITHOUT_DOMAIN_DECISIONS
+ adapter_verification: INTEGRATION_TESTED_AS_EXECUTABLES
+required_gates:
+ - id: world_and_protocol_validation
+ required_score: 100
+ - id: unit_and_integration_tests
+ required_score: 100
+ - id: format
+ required_score: 100
+ - id: zero_warning_lint
+ required_score: 100
+ - id: auditable_line_coverage_100_percent
+ required_score: 100
+ - id: shell_syntax
+ required_score: 100
+ - id: diff_whitespace
+ required_score: 100
+ - id: source_tree_fingerprint
+ required_score: 100
+ - id: sensitive_information_scan
+ required_score: 100
+receipt:
+ schema: guanghu.native-code-quality-receipt/v1
+ pass_state: PASS_100
+ fail_state: FAIL_0
+ rule:
+ - ANY_REQUIRED_GATE_BELOW_100_MAKES_TOTAL_0
+ - NO_PARTIAL_SCORE
+ - NO_THRESHOLD_GREATER_THAN_OR_EQUAL_TO
+ - COVERAGE_MEANS_EXACT_COVERED_LINES_EQUALS_TOTAL_LINES
+ - EXTERNAL_ANALYSIS_CANNOT_AUTHORIZE_OR_BLOCK
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-recovery/PROTOCOL.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-recovery/PROTOCOL.hldp
new file mode 100644
index 0000000..c32b12d
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-recovery/PROTOCOL.hldp
@@ -0,0 +1,45 @@
+schema: guanghu.native-recovery-protocol/v1
+id: GLS-0843
+acronym: GHNRP
+name: Guanghu Native Recovery Protocol
+chinese_name: 光湖原生恢复协议
+status: REGISTERED_IMPLEMENTED_PENDING_PHYSICAL_DEFAULT_GATE
+authority_language: HLDP
+scope:
+ node_id: GH-CVM-MAIN-PROD-01
+ system_disk: /dev/vda
+ purpose: SWITCH_FROM_GUANGHU_NATIVE_DEFAULT_TO_HOSTED_RECOVERY
+beacon:
+ ownership: GUANGHU_OS
+ lba_start: 68
+ sector_count: 2
+ size_bytes: 1024
+ format: GRUB_ENVIRONMENT_BLOCK
+ variable: guanghu_recovery
+ active_value: ubuntu
+ clear_value: ABSENT_OR_EMPTY
+grub:
+ raw_blocklist: (hd0)68+2
+ whitelisted_variable: guanghu_recovery
+ whitelist_only: true
+ hosted_entry: gnulinux-simple-7bccaefa-b0a9-4f6f-bd32-22dde0066c0b
+ native_default_entry: guanghu-native-once
+ select_only: true
+ raw_blocklist_write: FORBIDDEN
+hosted_recovery:
+ consumer: guanghu-native-recovery-beacon-clear.service
+ consume_on_boot: true
+ verify_before_clear: true
+ readback_after_clear: true
+semantics:
+ command: HLDP-RECOVER-OS!
+ writer: GOSK_GHAL_NATIVE
+ reader: GRUB_BOOTSTRAP_COMPATIBILITY_LAYER
+ consumer: HOSTED_RECOVERY_CLEAR_SERVICE
+ result: NEXT_BOOT_HOSTED_RECOVERY_CONSUMES_BEACON_THEN_NATIVE_DEFAULT_REMAINS
+ filesystem_extent_dependency: false
+ standard_grubenv_dependency: false
+failure_policy:
+ unknown_beacon_data: FAIL_CLOSED
+ write_without_readback: FORBIDDEN
+ physical_completion_claim_without_returned_hosted_boot: FORBIDDEN
diff --git a/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-storage/DISK-LAYOUT.hldp b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-storage/DISK-LAYOUT.hldp
new file mode 100644
index 0000000..7d7167e
--- /dev/null
+++ b/guanghu-os/deployments/GH-CVM-MAIN-PROD-01/world-seed/world/services/native-storage/DISK-LAYOUT.hldp
@@ -0,0 +1,35 @@
+schema: guanghu.native-disk-layout/v1
+id: GLS-0846
+acronym: GHNLP
+name: 光湖原生磁盘布局协议
+status: REGISTERED_IMPLEMENTATION_GATED
+authority_language: HLDP
+node_id: GH-CVM-MAIN-PROD-01
+disk: /dev/vda
+sector_size: 512
+regions:
+ kernel:
+ lba_start: 34
+ sector_count: 29
+ lba_end_inclusive: 62
+ stage0_lba: 34
+ stage2_lba_start: 35
+ stage2_sector_count: 28
+ proof_lba: 63
+ world_store_lba: 64
+ code_channel_store_lba: 65
+ code_object_lba: 66
+ branch_receipt_lba: 67
+ recovery_beacon_lba_start: 68
+ recovery_beacon_sector_count: 2
+ gestational_index_lba_start: 70
+ gestational_index_sector_count: 2
+ control_state_lba: 72
+ control_state_sector_count: 1
+ alternate_kernel_lba_start: 73
+ alternate_kernel_sector_count: 29
+ first_partition_lba: 2048
+ownership:
+ pre_partition_region: GUANGHU_OS_NATIVE
+ unknown_nonzero_state: FAIL_CLOSED_NO_OVERWRITE
+ overlap_rule: NO_REGION_OVERLAP
diff --git a/guanghu-os/disaster-recovery/README.md b/guanghu-os/disaster-recovery/README.md
index eb36df8..9ca6500 100644
--- a/guanghu-os/disaster-recovery/README.md
+++ b/guanghu-os/disaster-recovery/README.md
@@ -49,6 +49,17 @@ cargo run --manifest-path guanghu-os/Cargo.toml -p guanghu-ghdr -- \
cargo run --manifest-path guanghu-os/Cargo.toml -p guanghu-ghdr -- \
verify-package /path/to/sealed-recovery-package
+
+cargo run --manifest-path guanghu-os/Cargo.toml -p guanghu-ghdr \
+ --bin guanghu-ghdr -- layout-plan-payload /path/to/layout-plan.json \
+ > /tmp/layout-plan-payload.json
+
+cargo run --manifest-path guanghu-os/Cargo.toml -p guanghu-ghdr \
+ --bin guanghu-ghdr -- verify-signed-layout-plan \
+ /path/to/node-manifest.json \
+ /path/to/layout-plan.json \
+ /path/to/fresh-readback.json \
+ "$(date +%s)"
```
Replace every example identifier and receipt reference with exact evidence for
@@ -77,18 +88,22 @@ private keys, or tokens.
A migration plan must name at least two unique recovery controller nodes. They
must be different from the target and span at least two declared failure
-domains. A non-empty role is recorded for each controller.
+domains. A non-empty role and an independent pinned Ed25519 public key are
+recorded for each controller.
-Four independent receipt references are mandatory:
+Four independent receipt references and their exact SHA-256 digests are
+mandatory:
-1. a cloud-image clone that has actually booted;
+1. a zero-cost Linux rescue path that has actually booted;
2. a control-plane backup;
3. a completed data-restore exercise;
4. a provider-console recovery exercise.
A receipt reference is a pointer to evidence, not the evidence itself. The
-later signed-plan gate must bind exact evidence digests and controller
-identities before any destructive action can be considered.
+signed-plan payload binds the canonical digest of all four reference-and-digest
+pairs together with the controller identities before any destructive action
+can be considered. A paid cloud-image clone is neither required nor accepted as
+a substitute for the Linux rescue boot receipt.
## Recovery package boundary
@@ -118,13 +133,36 @@ Example package index:
}
```
-## Later registered gates
+## Signed layout-plan gate
+
+The implemented signed-plan gate canonicalizes a payload bound to the exact
+target probe digest, whole-disk identity, disk geometry, nonoverlapping A/B
+slot extents and image digests, generation, intended operation, and an expiry
+no more than one hour after issuance. Exactly two registered controllers in
+different failure domains must provide valid signatures. A target read-back
+no older than five minutes must match every signed disk field.
+
+`ghdr-controller` creates independent Ed25519 controller keys and signatures.
+Private seeds are written once with mode `0600`, read only from files, and are
+never accepted on the command line or printed in output:
-The next implementation must add a canonical, expiring signed layout plan
-bound to the target probe digest, disk/GPT identity, A/B slot extents,
-generation, and intended operation. A destructive write must require two
-independent controller signatures and a fresh target read-back that matches
-the signed plan.
+```bash
+cargo run --manifest-path guanghu-os/Cargo.toml -p guanghu-ghdr \
+ --bin ghdr-controller -- generate-key \
+ /secure/controller.seed /tmp/controller-public.json \
+ DR-CONTROLLER-A provider-a/region-a
+
+cargo run --manifest-path guanghu-os/Cargo.toml -p guanghu-ghdr \
+ --bin ghdr-controller -- sign-layout \
+ /secure/controller.seed DR-CONTROLLER-A provider-a/region-a \
+ /path/to/layout-plan.json /tmp/controller-a-signature.json
+```
+
+A `PASS_100_SIGNED_LAYOUT_PLAN` permits only the exact signed write while its
+freshness conditions remain true. It does not prove that the write, native
+boot, fallback, or restoration succeeded.
+
+## Later registered gates
Later physical acceptance must prove automatic A/B fallback, native boot
without Linux after acceptance, independent control-plane and data restoration,
diff --git a/guanghu-os/disaster-recovery/node-plan.example.json b/guanghu-os/disaster-recovery/node-plan.example.json
index 061d1f6..53f37dc 100644
--- a/guanghu-os/disaster-recovery/node-plan.example.json
+++ b/guanghu-os/disaster-recovery/node-plan.example.json
@@ -7,18 +7,32 @@
{
"node_id": "DR-TENCENT-SG-001",
"failure_domain": "provider:tencent/region:singapore",
- "role": "witness-and-recovery"
+ "role": "witness-and-recovery",
+ "signing_public_key_hex": "1111111111111111111111111111111111111111111111111111111111111111"
},
{
"node_id": "DR-TENCENT-CN-001",
"failure_domain": "provider:tencent/region:china",
- "role": "backup-and-recovery"
+ "role": "backup-and-recovery",
+ "signing_public_key_hex": "2222222222222222222222222222222222222222222222222222222222222222"
}
],
"evidence": {
- "cloud_image_clone_boot_receipt": "receipt://replace/cloud-image-clone-boot",
- "control_plane_backup_receipt": "receipt://replace/control-plane-backup",
- "data_restore_receipt": "receipt://replace/data-restore",
- "provider_console_recovery_receipt": "receipt://replace/provider-console-recovery"
+ "linux_rescue_boot_receipt": {
+ "reference": "receipt://replace/linux-rescue-boot",
+ "sha256": "replace-with-64-lowercase-hex-characters"
+ },
+ "control_plane_backup_receipt": {
+ "reference": "receipt://replace/control-plane-backup",
+ "sha256": "replace-with-64-lowercase-hex-characters"
+ },
+ "data_restore_receipt": {
+ "reference": "receipt://replace/data-restore",
+ "sha256": "replace-with-64-lowercase-hex-characters"
+ },
+ "provider_console_recovery_receipt": {
+ "reference": "receipt://replace/provider-console-recovery",
+ "sha256": "replace-with-64-lowercase-hex-characters"
+ }
}
}
diff --git a/guanghu-os/native/x86_64-bios/boot.asm b/guanghu-os/native/x86_64-bios/boot.asm
index eaf2e27..11e61a4 100644
--- a/guanghu-os/native/x86_64-bios/boot.asm
+++ b/guanghu-os/native/x86_64-bios/boot.asm
@@ -169,6 +169,10 @@ long_mode_start:
call serial_write64
call ghal_virtio_init
jc ghal_initialization_error
+%if GHOS_AUTHENTICATED_CONTROL = 1
+ call ghal_block_load_control_state
+ jc native_control_state_error
+%endif
%ifdef GHOS_GHAL_PROBE_STAGE
mov byte [rel physical_proof_flag], 0xa5
%else
@@ -246,6 +250,14 @@ native_gestational_index_error:
call serial_write64
jmp write_native_block_proof
+%if GHOS_AUTHENTICATED_CONTROL = 1
+native_control_state_error:
+ mov byte [rel physical_proof_flag], 0xe1
+ mov rsi, msg_native_control_state_error
+ call serial_write64
+ jmp write_native_block_proof
+%endif
+
write_native_block_proof:
call ghal_block_write_proof
jc native_block_proof_error
@@ -380,6 +392,9 @@ msg_native_block_proof_error: db "GHOS_BOOT_ERROR=NATIVE_BLOCK_PROOF_WRITE", 13,
msg_native_network_proof_error: db "GHOS_BOOT_ERROR=NATIVE_ARP_GATEWAY", 13, 10, 0
msg_native_world_store_error: db "GHOS_BOOT_ERROR=NATIVE_HLDP_WORLD_STORE", 13, 10, 0
msg_native_gestational_index_error: db "GHOS_BOOT_ERROR=NATIVE_GHCIP_INDEX", 13, 10, 0
+%if GHOS_AUTHENTICATED_CONTROL = 1
+msg_native_control_state_error: db "GHOS_BOOT_ERROR=NATIVE_CONTROL_STATE", 13, 10, 0
+%endif
msg_physical_proof_error: db "GHOS_BOOT_ERROR=DISK_PROOF_WRITE", 13, 10, 0
align 8
null_idt64:
@@ -441,6 +456,14 @@ physical_proof_recovery_beacon_read_verified: db 0
physical_proof_gestational_index_initialized: db 0
physical_proof_gestational_index_present: db 0
physical_proof_gestational_index_read_verified: db 0
+%if GHOS_AUTHENTICATED_CONTROL = 1
+physical_proof_control_state_loaded: db 0
+physical_proof_control_auth_verified: db 0
+physical_proof_control_target_verified: db 0
+physical_proof_control_dual_mac_verified: db 0
+physical_proof_control_replay_rejected: db 0
+physical_proof_control_nonce_persisted: db 0
+%endif
times 512 - ($ - physical_proof_sector) db 0
%endif
diff --git a/guanghu-os/native/x86_64-bios/ghal-virtio.asm b/guanghu-os/native/x86_64-bios/ghal-virtio.asm
index c898d0d..3544520 100644
--- a/guanghu-os/native/x86_64-bios/ghal-virtio.asm
+++ b/guanghu-os/native/x86_64-bios/ghal-virtio.asm
@@ -29,6 +29,7 @@ bits 64
%define VIRTIO_CODE_CHANNEL_BUFFER 0x123000
%define VIRTIO_RECOVERY_BEACON_BUFFER 0x124000
%define VIRTIO_GESTATIONAL_INDEX_BUFFER 0x125000
+%define VIRTIO_CONTROL_STATE_BUFFER 0x126000
%define VIRTIO_NET_BUFFER_SIZE 2048
%define VIRTIO_NET_HEADER_SIZE 10
%define ETHERNET_HEADER_SIZE 14
@@ -36,6 +37,11 @@ bits 64
%define ICMP_HEADER_SIZE 8
%define GHOS_LOGIN_MAGIC_OFFSET 60
%define GHOS_LOGIN_MAGIC_SIZE 16
+%define GHOS_CONTROL_FRAME_OFFSET 52
+%define GHOS_CONTROL_MESSAGE_SIZE 32
+%define GHOS_CONTROL_FRAME_SIZE 48
+%define GHOS_CONTROL_FRAME_MAGIC 0x0000324c54434847
+%define GHOS_CONTROL_STATE_MAGIC 0x32534c5254434847
%define VIRTIO_QUEUE_BYTES 0x8000
%define VIRTIO_MAX_QUEUE_SIZE 1024
%define VIRTQ_DESC_F_NEXT 1
@@ -48,10 +54,37 @@ bits 64
%define NATIVE_BRANCH_RECEIPT_LBA 67
%define NATIVE_RECOVERY_BEACON_LBA 68
%define NATIVE_GESTATIONAL_INDEX_LBA 70
+%define NATIVE_CONTROL_STATE_LBA 72
%ifndef GHOS_GHAL_PROBE_STAGE
%define GHOS_GHAL_PROBE_STAGE 0
%endif
+%ifndef GHOS_GUEST_IPV4_DWORD
+%define GHOS_GUEST_IPV4_DWORD 0x0700000a
+%endif
+%ifndef GHOS_GATEWAY_IPV4_DWORD
+%define GHOS_GATEWAY_IPV4_DWORD 0x0100000a
+%endif
+%ifndef GHOS_AUTHENTICATED_CONTROL
+%define GHOS_AUTHENTICATED_CONTROL 0
+%endif
+%if GHOS_AUTHENTICATED_CONTROL = 1
+%ifndef GHOS_CONTROL_TARGET_TAG
+%error "GHOS_CONTROL_TARGET_TAG is required for authenticated control"
+%endif
+%ifndef GHOS_CONTROLLER_A_K0
+%error "GHOS_CONTROLLER_A_K0 is required for authenticated control"
+%endif
+%ifndef GHOS_CONTROLLER_A_K1
+%error "GHOS_CONTROLLER_A_K1 is required for authenticated control"
+%endif
+%ifndef GHOS_CONTROLLER_B_K0
+%error "GHOS_CONTROLLER_B_K0 is required for authenticated control"
+%endif
+%ifndef GHOS_CONTROLLER_B_K1
+%error "GHOS_CONTROLLER_B_K1 is required for authenticated control"
+%endif
+%endif
%if GHOS_GHAL_PROBE_STAGE < 0 || GHOS_GHAL_PROBE_STAGE > 9
%error "GHOS_GHAL_PROBE_STAGE must be between 0 and 9"
%endif
@@ -689,26 +722,26 @@ ghal_block_transfer_sector:
mov byte [rel ghal_block_request_status], 0xff
lea rax, [rel ghal_block_request_header]
- mov [VIRTIO_BLOCK_QUEUE], rax
- mov dword [VIRTIO_BLOCK_QUEUE + 8], 16
- mov word [VIRTIO_BLOCK_QUEUE + 12], VIRTQ_DESC_F_NEXT
- mov word [VIRTIO_BLOCK_QUEUE + 14], 1
+ mov [abs VIRTIO_BLOCK_QUEUE], rax
+ mov dword [abs VIRTIO_BLOCK_QUEUE + 8], 16
+ mov word [abs VIRTIO_BLOCK_QUEUE + 12], VIRTQ_DESC_F_NEXT
+ mov word [abs VIRTIO_BLOCK_QUEUE + 14], 1
- mov [VIRTIO_BLOCK_QUEUE + 16], rsi
- mov dword [VIRTIO_BLOCK_QUEUE + 24], 512
+ mov [abs VIRTIO_BLOCK_QUEUE + 16], rsi
+ mov dword [abs VIRTIO_BLOCK_QUEUE + 24], 512
or r9w, VIRTQ_DESC_F_NEXT
- mov word [VIRTIO_BLOCK_QUEUE + 28], r9w
- mov word [VIRTIO_BLOCK_QUEUE + 30], 2
+ mov word [abs VIRTIO_BLOCK_QUEUE + 28], r9w
+ mov word [abs VIRTIO_BLOCK_QUEUE + 30], 2
lea rax, [rel ghal_block_request_status]
- mov [VIRTIO_BLOCK_QUEUE + 32], rax
- mov dword [VIRTIO_BLOCK_QUEUE + 40], 1
- mov word [VIRTIO_BLOCK_QUEUE + 44], VIRTQ_DESC_F_WRITE
- mov word [VIRTIO_BLOCK_QUEUE + 46], 0
+ mov [abs VIRTIO_BLOCK_QUEUE + 32], rax
+ mov dword [abs VIRTIO_BLOCK_QUEUE + 40], 1
+ mov word [abs VIRTIO_BLOCK_QUEUE + 44], VIRTQ_DESC_F_WRITE
+ mov word [abs VIRTIO_BLOCK_QUEUE + 46], 0
movzx ecx, word [rel physical_proof_block_queue_size]
test ecx, ecx
- jz .queue_missing
+ jz ghal_block_transfer_queue_missing
mov eax, ecx
shl eax, 4
mov ebx, VIRTIO_BLOCK_QUEUE
@@ -739,23 +772,256 @@ ghal_block_transfer_sector:
mov ecx, 0x10000000
.wait_used:
cmp word [rdi + 2], r8w
- je .completed
+ je ghal_block_transfer_completed
pause
loop .wait_used
mov byte [rel physical_proof_error_code], 0x41
stc
ret
-.completed:
+
+%if GHOS_AUTHENTICATED_CONTROL = 1
+%macro GHOS_SIPHASH_ROUND 0
+ add r8, r9
+ rol r9, 13
+ xor r9, r8
+ rol r8, 32
+ add r10, r11
+ rol r11, 16
+ xor r11, r10
+ add r8, r11
+ rol r11, 21
+ xor r11, r8
+ add r10, r9
+ rol r9, 17
+ xor r9, r10
+ rol r10, 32
+%endmacro
+
+; rax=k0, rdx=k1, rsi=32-byte message; returns rax=SipHash-2-4.
+ghal_siphash24_message32:
+ mov r8, 0x736f6d6570736575
+ xor r8, rax
+ mov r9, 0x646f72616e646f6d
+ xor r9, rdx
+ mov r10, 0x6c7967656e657261
+ xor r10, rax
+ mov r11, 0x7465646279746573
+ xor r11, rdx
+ mov ecx, GHOS_CONTROL_MESSAGE_SIZE / 8
+.word_loop:
+ mov rbx, [rsi]
+ add rsi, 8
+ xor r11, rbx
+ GHOS_SIPHASH_ROUND
+ GHOS_SIPHASH_ROUND
+ xor r8, rbx
+ loop .word_loop
+ mov rbx, GHOS_CONTROL_MESSAGE_SIZE
+ shl rbx, 56
+ xor r11, rbx
+ GHOS_SIPHASH_ROUND
+ GHOS_SIPHASH_ROUND
+ xor r8, rbx
+ xor r10, 0xff
+ GHOS_SIPHASH_ROUND
+ GHOS_SIPHASH_ROUND
+ GHOS_SIPHASH_ROUND
+ GHOS_SIPHASH_ROUND
+ mov rax, r8
+ xor rax, r9
+ xor rax, r10
+ xor rax, r11
+ ret
+
+ghal_block_load_control_state:
+ mov rdi, VIRTIO_BLOCK_READ_BUFFER
+ xor eax, eax
+ mov ecx, 512 / 8
+ rep stosq
+ mov eax, VIRTIO_BLK_T_IN
+ mov rsi, VIRTIO_BLOCK_READ_BUFFER
+ mov edx, NATIVE_CONTROL_STATE_LBA
+ mov r9w, VIRTQ_DESC_F_WRITE
+ call ghal_block_transfer_sector
+ jc .failed
+ mov rsi, VIRTIO_BLOCK_READ_BUFFER
+ mov ecx, 512 / 8
+.blank_check:
+ cmp qword [rsi], 0
+ jne .registered
+ add rsi, 8
+ loop .blank_check
+ mov qword [rel ghal_control_last_nonce], 0
+ mov byte [rel physical_proof_control_state_loaded], 1
+ clc
+ ret
+.registered:
+ mov rdx, GHOS_CONTROL_STATE_MAGIC
+ cmp qword [abs VIRTIO_BLOCK_READ_BUFFER], rdx
+ jne .invalid
+ mov rax, GHOS_CONTROL_TARGET_TAG
+ cmp qword [abs VIRTIO_BLOCK_READ_BUFFER + 8], rax
+ jne .invalid
+ mov rax, [abs VIRTIO_BLOCK_READ_BUFFER + 16]
+ test rax, rax
+ jz .invalid
+ mov rdx, [abs VIRTIO_BLOCK_READ_BUFFER + 24]
+ not rdx
+ cmp rdx, rax
+ jne .invalid
+ mov rsi, VIRTIO_BLOCK_READ_BUFFER + 32
+ mov ecx, (512 - 32) / 8
+.tail_check:
+ cmp qword [rsi], 0
+ jne .invalid
+ add rsi, 8
+ loop .tail_check
+ mov [rel ghal_control_last_nonce], rax
+ mov byte [rel physical_proof_control_state_loaded], 1
+ clc
+ ret
+.invalid:
+ mov byte [rel physical_proof_error_code], 0x6d
+.failed:
+ stc
+ ret
+
+; rax=new nonce. Persist before acknowledging or applying the command.
+ghal_block_commit_control_nonce:
+ mov r15, rax
+ mov rdi, VIRTIO_CONTROL_STATE_BUFFER
+ xor eax, eax
+ mov ecx, 512 / 8
+ rep stosq
+ mov rax, GHOS_CONTROL_STATE_MAGIC
+ mov [abs VIRTIO_CONTROL_STATE_BUFFER], rax
+ mov rax, GHOS_CONTROL_TARGET_TAG
+ mov [abs VIRTIO_CONTROL_STATE_BUFFER + 8], rax
+ mov [abs VIRTIO_CONTROL_STATE_BUFFER + 16], r15
+ mov rax, r15
+ not rax
+ mov [abs VIRTIO_CONTROL_STATE_BUFFER + 24], rax
+ mov eax, VIRTIO_BLK_T_OUT
+ mov rsi, VIRTIO_CONTROL_STATE_BUFFER
+ mov edx, NATIVE_CONTROL_STATE_LBA
+ xor r9d, r9d
+ call ghal_block_transfer_sector
+ jc .failed
+ mov rdi, VIRTIO_BLOCK_READ_BUFFER
+ xor eax, eax
+ mov ecx, 512 / 8
+ rep stosq
+ mov eax, VIRTIO_BLK_T_IN
+ mov rsi, VIRTIO_BLOCK_READ_BUFFER
+ mov edx, NATIVE_CONTROL_STATE_LBA
+ mov r9w, VIRTQ_DESC_F_WRITE
+ call ghal_block_transfer_sector
+ jc .failed
+ mov rsi, VIRTIO_BLOCK_READ_BUFFER
+ mov rdi, VIRTIO_CONTROL_STATE_BUFFER
+ mov ecx, 512
+ repe cmpsb
+ jne .failed
+ mov [rel ghal_control_last_nonce], r15
+ mov byte [rel physical_proof_control_nonce_persisted], 1
+ clc
+ ret
+.failed:
+ mov byte [rel physical_proof_error_code], 0x6e
+ stc
+ ret
+
+; Result byte: 0 reject, 1 accept, 2 fatal persistence failure.
+ghal_authenticate_control_frame:
+ push rbx
+ push rcx
+ push rdx
+ push rsi
+ push rdi
+ push r8
+ push r9
+ push r10
+ push r11
+ push r15
+ mov byte [rel ghal_control_auth_result], 0
+ cmp r14d, GHOS_CONTROL_FRAME_OFFSET + GHOS_CONTROL_FRAME_SIZE
+ jb .done
+ mov rax, GHOS_CONTROL_FRAME_MAGIC
+ cmp qword [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET], rax
+ jne .done
+ mov rax, GHOS_CONTROL_TARGET_TAG
+ cmp qword [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 8], rax
+ jne .done
+ movzx eax, byte [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 24]
+ cmp byte [rel ghal_net_command_kind], 3
+ jne .exact_command
+ cmp al, 0
+ je .command_valid
+ cmp al, 3
+ jne .done
+ jmp .command_valid
+.exact_command:
+ cmp al, [rel ghal_net_command_kind]
+ jne .done
+.command_valid:
+ cmp qword [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 24], rax
+ jne .done
+ mov r15, [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 16]
+ test r15, r15
+ jz .replay
+ cmp r15, [rel ghal_control_last_nonce]
+ jbe .replay
+ lea rsi, [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET]
+ mov rax, GHOS_CONTROLLER_A_K0
+ mov rdx, GHOS_CONTROLLER_A_K1
+ call ghal_siphash24_message32
+ cmp rax, [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 32]
+ jne .done
+ lea rsi, [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET]
+ mov rax, GHOS_CONTROLLER_B_K0
+ mov rdx, GHOS_CONTROLLER_B_K1
+ call ghal_siphash24_message32
+ cmp rax, [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 40]
+ jne .done
+ mov rax, r15
+ call ghal_block_commit_control_nonce
+ jc .fatal
+ mov al, [abs VIRTIO_NET_RX_BUFFER + GHOS_CONTROL_FRAME_OFFSET + 24]
+ mov [rel ghal_net_matched_kind], al
+ mov byte [rel physical_proof_control_target_verified], 1
+ mov byte [rel physical_proof_control_dual_mac_verified], 1
+ mov byte [rel physical_proof_control_auth_verified], 1
+ mov byte [rel ghal_control_auth_result], 1
+ jmp .done
+.replay:
+ mov byte [rel physical_proof_control_replay_rejected], 1
+ jmp .done
+.fatal:
+ mov byte [rel ghal_control_auth_result], 2
+.done:
+ pop r15
+ pop r11
+ pop r10
+ pop r9
+ pop r8
+ pop rdi
+ pop rsi
+ pop rdx
+ pop rcx
+ pop rbx
+ ret
+%endif
+ghal_block_transfer_completed:
cmp byte [rel ghal_block_request_status], 0
- jne .device_error
+ jne ghal_block_transfer_device_error
inc word [rel ghal_block_next_index]
clc
ret
-.queue_missing:
+ghal_block_transfer_queue_missing:
mov byte [rel physical_proof_error_code], 0x40
stc
ret
-.device_error:
+ghal_block_transfer_device_error:
mov byte [rel physical_proof_error_code], 0x42
stc
ret
@@ -771,10 +1037,10 @@ ghal_net_arp_gateway:
mov ecx, VIRTIO_NET_BUFFER_SIZE / 8
rep stosq
- mov qword [VIRTIO_NET_RX_QUEUE], VIRTIO_NET_RX_BUFFER
- mov dword [VIRTIO_NET_RX_QUEUE + 8], VIRTIO_NET_BUFFER_SIZE
- mov word [VIRTIO_NET_RX_QUEUE + 12], VIRTQ_DESC_F_WRITE
- mov word [VIRTIO_NET_RX_QUEUE + 14], 0
+ mov qword [abs VIRTIO_NET_RX_QUEUE], VIRTIO_NET_RX_BUFFER
+ mov dword [abs VIRTIO_NET_RX_QUEUE + 8], VIRTIO_NET_BUFFER_SIZE
+ mov word [abs VIRTIO_NET_RX_QUEUE + 12], VIRTQ_DESC_F_WRITE
+ mov word [abs VIRTIO_NET_RX_QUEUE + 14], 0
movzx ecx, word [rel physical_proof_net_rx_queue_size]
test ecx, ecx
@@ -801,24 +1067,24 @@ ghal_net_arp_gateway:
cmp ecx, 6
jae .source_mac_done
mov al, [physical_proof_mac + rcx]
- mov [VIRTIO_NET_TX_BUFFER + 16 + rcx], al
- mov [VIRTIO_NET_TX_BUFFER + 32 + rcx], al
+ mov [abs VIRTIO_NET_TX_BUFFER + 16 + rcx], al
+ mov [abs VIRTIO_NET_TX_BUFFER + 32 + rcx], al
inc ecx
jmp .copy_source_mac
.source_mac_done:
- mov word [VIRTIO_NET_TX_BUFFER + 22], 0x0608
- mov word [VIRTIO_NET_TX_BUFFER + 24], 0x0100
- mov word [VIRTIO_NET_TX_BUFFER + 26], 0x0008
- mov byte [VIRTIO_NET_TX_BUFFER + 28], 6
- mov byte [VIRTIO_NET_TX_BUFFER + 29], 4
- mov word [VIRTIO_NET_TX_BUFFER + 30], 0x0100
- mov dword [VIRTIO_NET_TX_BUFFER + 38], 0x0700000a
- mov dword [VIRTIO_NET_TX_BUFFER + 48], 0x0100000a
-
- mov qword [VIRTIO_NET_TX_QUEUE], VIRTIO_NET_TX_BUFFER
- mov dword [VIRTIO_NET_TX_QUEUE + 8], 70
- mov word [VIRTIO_NET_TX_QUEUE + 12], 0
- mov word [VIRTIO_NET_TX_QUEUE + 14], 0
+ mov word [abs VIRTIO_NET_TX_BUFFER + 22], 0x0608
+ mov word [abs VIRTIO_NET_TX_BUFFER + 24], 0x0100
+ mov word [abs VIRTIO_NET_TX_BUFFER + 26], 0x0008
+ mov byte [abs VIRTIO_NET_TX_BUFFER + 28], 6
+ mov byte [abs VIRTIO_NET_TX_BUFFER + 29], 4
+ mov word [abs VIRTIO_NET_TX_BUFFER + 30], 0x0100
+ mov dword [abs VIRTIO_NET_TX_BUFFER + 38], GHOS_GUEST_IPV4_DWORD
+ mov dword [abs VIRTIO_NET_TX_BUFFER + 48], GHOS_GATEWAY_IPV4_DWORD
+
+ mov qword [abs VIRTIO_NET_TX_QUEUE], VIRTIO_NET_TX_BUFFER
+ mov dword [abs VIRTIO_NET_TX_QUEUE + 8], 70
+ mov word [abs VIRTIO_NET_TX_QUEUE + 12], 0
+ mov word [abs VIRTIO_NET_TX_QUEUE + 14], 0
movzx ecx, word [rel physical_proof_net_tx_queue_size]
test ecx, ecx
@@ -869,19 +1135,19 @@ ghal_net_arp_gateway:
.rx_complete:
cmp dword [r12 + 8], 52
jb .invalid_reply
- cmp word [VIRTIO_NET_RX_BUFFER + 22], 0x0608
+ cmp word [abs VIRTIO_NET_RX_BUFFER + 22], 0x0608
jne .invalid_reply
- cmp word [VIRTIO_NET_RX_BUFFER + 30], 0x0200
+ cmp word [abs VIRTIO_NET_RX_BUFFER + 30], 0x0200
jne .invalid_reply
- cmp dword [VIRTIO_NET_RX_BUFFER + 38], 0x0100000a
+ cmp dword [abs VIRTIO_NET_RX_BUFFER + 38], GHOS_GATEWAY_IPV4_DWORD
jne .invalid_reply
- cmp dword [VIRTIO_NET_RX_BUFFER + 48], 0x0700000a
+ cmp dword [abs VIRTIO_NET_RX_BUFFER + 48], GHOS_GUEST_IPV4_DWORD
jne .invalid_reply
xor ecx, ecx
.copy_gateway_mac:
cmp ecx, 6
jae .reply_verified
- mov al, [VIRTIO_NET_RX_BUFFER + 32 + rcx]
+ mov al, [abs VIRTIO_NET_RX_BUFFER + 32 + rcx]
mov [physical_proof_gateway_mac + rcx], al
inc ecx
jmp .copy_gateway_mac
@@ -960,16 +1226,24 @@ ghal_net_icmp_login_once:
mov r14d, [r12 + rax + 8]
cmp r14d, GHOS_LOGIN_MAGIC_OFFSET + GHOS_LOGIN_MAGIC_SIZE
jb .ignore_packet
- cmp word [VIRTIO_NET_RX_BUFFER + 22], 0x0008
+ cmp word [abs VIRTIO_NET_RX_BUFFER + 22], 0x0008
jne .ignore_packet
- cmp byte [VIRTIO_NET_RX_BUFFER + 24], 0x45
+ cmp byte [abs VIRTIO_NET_RX_BUFFER + 24], 0x45
jne .ignore_packet
- cmp byte [VIRTIO_NET_RX_BUFFER + 33], 1
+ cmp byte [abs VIRTIO_NET_RX_BUFFER + 33], 1
jne .ignore_packet
- cmp dword [VIRTIO_NET_RX_BUFFER + 40], 0x0700000a
+ cmp dword [abs VIRTIO_NET_RX_BUFFER + 40], GHOS_GUEST_IPV4_DWORD
jne .ignore_packet
- cmp word [VIRTIO_NET_RX_BUFFER + 44], 0x0008
+ cmp word [abs VIRTIO_NET_RX_BUFFER + 44], 0x0008
jne .ignore_packet
+%if GHOS_AUTHENTICATED_CONTROL = 1
+ call ghal_authenticate_control_frame
+ cmp byte [rel ghal_control_auth_result], 2
+ je .authentication_failure
+ cmp byte [rel ghal_control_auth_result], 1
+ jne .ignore_packet
+ jmp .magic_accepted
+%else
mov rsi, VIRTIO_NET_RX_BUFFER + GHOS_LOGIN_MAGIC_OFFSET
mov rdi, [rel ghal_net_expected_magic]
mov ecx, GHOS_LOGIN_MAGIC_SIZE
@@ -992,18 +1266,23 @@ ghal_net_icmp_login_once:
xor al, al
.store_matched_kind:
mov [rel ghal_net_matched_kind], al
+%endif
.magic_accepted:
cmp byte [rel ghal_net_matched_kind], 0
jne .record_command
mov byte [rel physical_proof_ipv4_rx_verified], 1
- mov eax, [VIRTIO_NET_RX_BUFFER + 36]
+ mov eax, [abs VIRTIO_NET_RX_BUFFER + 36]
mov [rel physical_proof_login_client_ip], eax
- mov ax, [VIRTIO_NET_RX_BUFFER + 48]
+ mov ax, [abs VIRTIO_NET_RX_BUFFER + 48]
mov [rel physical_proof_login_icmp_id], ax
- mov ax, [VIRTIO_NET_RX_BUFFER + 50]
+ mov ax, [abs VIRTIO_NET_RX_BUFFER + 50]
mov [rel physical_proof_login_icmp_sequence], ax
+%if GHOS_AUTHENTICATED_CONTROL = 1
+ lea rsi, [rel ghal_login_magic]
+%else
mov rsi, VIRTIO_NET_RX_BUFFER + GHOS_LOGIN_MAGIC_OFFSET
+%endif
lea rdi, [rel physical_proof_login_magic]
mov ecx, GHOS_LOGIN_MAGIC_SIZE
rep movsb
@@ -1032,33 +1311,39 @@ ghal_net_icmp_login_once:
stc
ret
+%if GHOS_AUTHENTICATED_CONTROL = 1
+.authentication_failure:
+ stc
+ ret
+%endif
+
.build_reply:
mov rsi, VIRTIO_NET_RX_BUFFER
mov rdi, VIRTIO_NET_TX_BUFFER
mov ecx, r14d
rep movsb
- mov qword [VIRTIO_NET_TX_BUFFER], 0
- mov word [VIRTIO_NET_TX_BUFFER + 8], 0
+ mov qword [abs VIRTIO_NET_TX_BUFFER], 0
+ mov word [abs VIRTIO_NET_TX_BUFFER + 8], 0
xor ecx, ecx
.swap_mac:
cmp ecx, 6
jae .mac_swapped
- mov al, [VIRTIO_NET_TX_BUFFER + 10 + rcx]
- mov dl, [VIRTIO_NET_TX_BUFFER + 16 + rcx]
- mov [VIRTIO_NET_TX_BUFFER + 10 + rcx], dl
- mov [VIRTIO_NET_TX_BUFFER + 16 + rcx], al
+ mov al, [abs VIRTIO_NET_TX_BUFFER + 10 + rcx]
+ mov dl, [abs VIRTIO_NET_TX_BUFFER + 16 + rcx]
+ mov [abs VIRTIO_NET_TX_BUFFER + 10 + rcx], dl
+ mov [abs VIRTIO_NET_TX_BUFFER + 16 + rcx], al
inc ecx
jmp .swap_mac
.mac_swapped:
- mov eax, [VIRTIO_NET_TX_BUFFER + 36]
- mov edx, [VIRTIO_NET_TX_BUFFER + 40]
- mov [VIRTIO_NET_TX_BUFFER + 36], edx
- mov [VIRTIO_NET_TX_BUFFER + 40], eax
- mov byte [VIRTIO_NET_TX_BUFFER + 44], 0
- mov word [VIRTIO_NET_TX_BUFFER + 46], 0
-
- movzx eax, word [VIRTIO_NET_TX_BUFFER + 26]
+ mov eax, [abs VIRTIO_NET_TX_BUFFER + 36]
+ mov edx, [abs VIRTIO_NET_TX_BUFFER + 40]
+ mov [abs VIRTIO_NET_TX_BUFFER + 36], edx
+ mov [abs VIRTIO_NET_TX_BUFFER + 40], eax
+ mov byte [abs VIRTIO_NET_TX_BUFFER + 44], 0
+ mov word [abs VIRTIO_NET_TX_BUFFER + 46], 0
+
+ movzx eax, word [abs VIRTIO_NET_TX_BUFFER + 26]
xchg al, ah
cmp eax, IPV4_HEADER_SIZE + ICMP_HEADER_SIZE
jb .invalid_packet
@@ -1092,12 +1377,12 @@ ghal_net_icmp_login_once:
add ebx, eax
not bx
xchg bl, bh
- mov [VIRTIO_NET_TX_BUFFER + 46], bx
+ mov [abs VIRTIO_NET_TX_BUFFER + 46], bx
- mov qword [VIRTIO_NET_TX_QUEUE], VIRTIO_NET_TX_BUFFER
- mov [VIRTIO_NET_TX_QUEUE + 8], r14d
- mov word [VIRTIO_NET_TX_QUEUE + 12], 0
- mov word [VIRTIO_NET_TX_QUEUE + 14], 0
+ mov qword [abs VIRTIO_NET_TX_QUEUE], VIRTIO_NET_TX_BUFFER
+ mov [abs VIRTIO_NET_TX_QUEUE + 8], r14d
+ mov word [abs VIRTIO_NET_TX_QUEUE + 12], 0
+ mov word [abs VIRTIO_NET_TX_QUEUE + 14], 0
movzx ecx, word [rel physical_proof_net_tx_queue_size]
test ecx, ecx
@@ -1170,6 +1455,11 @@ ghal_net_tx_next_index: dw 2
ghal_net_expected_magic: dq ghal_login_magic
ghal_net_command_kind: db 0
ghal_net_matched_kind: db 0
+%if GHOS_AUTHENTICATED_CONTROL = 1
+align 8
+ghal_control_last_nonce: dq 0
+ghal_control_auth_result: db 0
+%endif
msg_ghal_net_discovered: db "GHOS_GHAL_VIRTIO_NET=DISCOVERED", 13, 10, 0
msg_ghal_block_discovered: db "GHOS_GHAL_VIRTIO_BLOCK=DISCOVERED", 13, 10, 0
diff --git a/guanghu-os/native/x86_64-bios/physical-test-mbr.asm b/guanghu-os/native/x86_64-bios/physical-test-mbr.asm
index c2f883e..1dbeba7 100644
--- a/guanghu-os/native/x86_64-bios/physical-test-mbr.asm
+++ b/guanghu-os/native/x86_64-bios/physical-test-mbr.asm
@@ -2,7 +2,9 @@ bits 16
org 0x7c00
%define COM1 0x3f8
+%ifndef CANDIDATE_LBA
%define CANDIDATE_LBA 34
+%endif
%define PROOF_LBA 63
start:
diff --git a/guanghu-os/scripts/build-native-physical-candidate.sh b/guanghu-os/scripts/build-native-physical-candidate.sh
index 2f22a0b..601a75d 100755
--- a/guanghu-os/scripts/build-native-physical-candidate.sh
+++ b/guanghu-os/scripts/build-native-physical-candidate.sh
@@ -10,6 +10,38 @@ world_root=$(readlink -f "$1")
output_root=$(readlink -m "$2")
source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
native_root=${source_root}/native/x86_64-bios
+guest_ipv4_dword=${GHOS_GUEST_IPV4_DWORD:-0x0700000a}
+gateway_ipv4_dword=${GHOS_GATEWAY_IPV4_DWORD:-0x0100000a}
+candidate_lba=${GHOS_CANDIDATE_LBA:-34}
+stage2_lba=$((candidate_lba + 1))
+authenticated_control=${GHOS_AUTHENTICATED_CONTROL:-0}
+control_args=(-dGHOS_AUTHENTICATED_CONTROL=0)
+
+[[ ${guest_ipv4_dword} =~ ^0x[0-9a-fA-F]{8}$ ]]
+[[ ${gateway_ipv4_dword} =~ ^0x[0-9a-fA-F]{8}$ ]]
+[[ ${candidate_lba} == 34 || ${candidate_lba} == 73 ]]
+if [[ ${authenticated_control} == 1 ]]; then
+ for value in \
+ "${GHOS_CONTROL_TARGET_TAG:-}" \
+ "${GHOS_CONTROLLER_A_K0:-}" \
+ "${GHOS_CONTROLLER_A_K1:-}" \
+ "${GHOS_CONTROLLER_B_K0:-}" \
+ "${GHOS_CONTROLLER_B_K1:-}"; do
+ [[ ${value} =~ ^0x[0-9a-fA-F]{16}$ ]]
+ done
+ [[ ${GHOS_CONTROLLER_A_K0}:${GHOS_CONTROLLER_A_K1} != "${GHOS_CONTROLLER_B_K0}:${GHOS_CONTROLLER_B_K1}" ]]
+ control_args=(
+ -dGHOS_AUTHENTICATED_CONTROL=1
+ -dGHOS_CONTROL_TARGET_TAG="${GHOS_CONTROL_TARGET_TAG}"
+ -dGHOS_CONTROLLER_A_K0="${GHOS_CONTROLLER_A_K0}"
+ -dGHOS_CONTROLLER_A_K1="${GHOS_CONTROLLER_A_K1}"
+ -dGHOS_CONTROLLER_B_K0="${GHOS_CONTROLLER_B_K0}"
+ -dGHOS_CONTROLLER_B_K1="${GHOS_CONTROLLER_B_K1}"
+ )
+elif [[ ${authenticated_control} != 0 ]]; then
+ echo "GHOS_AUTHENTICATED_CONTROL must be 0 or 1" >&2
+ exit 65
+fi
command -v nasm >/dev/null
mkdir -p "${output_root}"
@@ -18,8 +50,11 @@ cargo run --quiet --manifest-path "${source_root}/Cargo.toml" \
(
cd "${output_root}"
nasm -f bin -I "${output_root}/" -I "${native_root}/" \
- -dSTAGE2_LBA=35 \
+ -dSTAGE2_LBA="${stage2_lba}" \
-dGHOS_PHYSICAL_CANDIDATE=1 \
+ -dGHOS_GUEST_IPV4_DWORD="${guest_ipv4_dword}" \
+ -dGHOS_GATEWAY_IPV4_DWORD="${gateway_ipv4_dword}" \
+ "${control_args[@]}" \
"${native_root}/boot.asm" \
-o guanghu-os-x86_64-bios-physical.img
)
diff --git a/guanghu-os/scripts/build-native-resident-candidate.sh b/guanghu-os/scripts/build-native-resident-candidate.sh
index 2ea7f19..07577a8 100755
--- a/guanghu-os/scripts/build-native-resident-candidate.sh
+++ b/guanghu-os/scripts/build-native-resident-candidate.sh
@@ -10,6 +10,35 @@ world_root=$(readlink -f "$1")
output_root=$(readlink -m "$2")
source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
native_root=${source_root}/native/x86_64-bios
+guest_ipv4_dword=${GHOS_GUEST_IPV4_DWORD:-0x0700000a}
+gateway_ipv4_dword=${GHOS_GATEWAY_IPV4_DWORD:-0x0100000a}
+authenticated_control=${GHOS_AUTHENTICATED_CONTROL:-0}
+control_args=(-dGHOS_AUTHENTICATED_CONTROL=0)
+
+[[ ${guest_ipv4_dword} =~ ^0x[0-9a-fA-F]{8}$ ]]
+[[ ${gateway_ipv4_dword} =~ ^0x[0-9a-fA-F]{8}$ ]]
+if [[ ${authenticated_control} == 1 ]]; then
+ for value in \
+ "${GHOS_CONTROL_TARGET_TAG:-}" \
+ "${GHOS_CONTROLLER_A_K0:-}" \
+ "${GHOS_CONTROLLER_A_K1:-}" \
+ "${GHOS_CONTROLLER_B_K0:-}" \
+ "${GHOS_CONTROLLER_B_K1:-}"; do
+ [[ ${value} =~ ^0x[0-9a-fA-F]{16}$ ]]
+ done
+ [[ ${GHOS_CONTROLLER_A_K0}:${GHOS_CONTROLLER_A_K1} != "${GHOS_CONTROLLER_B_K0}:${GHOS_CONTROLLER_B_K1}" ]]
+ control_args=(
+ -dGHOS_AUTHENTICATED_CONTROL=1
+ -dGHOS_CONTROL_TARGET_TAG="${GHOS_CONTROL_TARGET_TAG}"
+ -dGHOS_CONTROLLER_A_K0="${GHOS_CONTROLLER_A_K0}"
+ -dGHOS_CONTROLLER_A_K1="${GHOS_CONTROLLER_A_K1}"
+ -dGHOS_CONTROLLER_B_K0="${GHOS_CONTROLLER_B_K0}"
+ -dGHOS_CONTROLLER_B_K1="${GHOS_CONTROLLER_B_K1}"
+ )
+elif [[ ${authenticated_control} != 0 ]]; then
+ echo "GHOS_AUTHENTICATED_CONTROL must be 0 or 1" >&2
+ exit 65
+fi
command -v nasm >/dev/null
mkdir -p "${output_root}"
@@ -21,6 +50,9 @@ cargo run --quiet --manifest-path "${source_root}/Cargo.toml" \
-dSTAGE2_LBA=35 \
-dGHOS_PHYSICAL_CANDIDATE=1 \
-dGHOS_NATIVE_RESIDENT=1 \
+ -dGHOS_GUEST_IPV4_DWORD="${guest_ipv4_dword}" \
+ -dGHOS_GATEWAY_IPV4_DWORD="${gateway_ipv4_dword}" \
+ "${control_args[@]}" \
"${native_root}/boot.asm" \
-o guanghu-os-x86_64-bios-resident.img
)
diff --git a/guanghu-os/scripts/install-native-ab-signed.sh b/guanghu-os/scripts/install-native-ab-signed.sh
new file mode 100755
index 0000000..b408453
--- /dev/null
+++ b/guanghu-os/scripts/install-native-ab-signed.sh
@@ -0,0 +1,223 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+fail() {
+ echo "GHDR_FAIL_0: $*" >&2
+ exit 65
+}
+
+[[ $# -eq 7 ]] || {
+ echo "usage: install-native-ab-signed.sh <ghdr-bin> <node-manifest.json> <signed-layout-plan.json> <slot-a.img> <slot-b.img> <disk> <recovery-root>" >&2
+ exit 64
+}
+[[ ${EUID} -eq 0 ]] || {
+ echo "GHDR_FAIL_0: must run as root" >&2
+ exit 77
+}
+
+for command in blockdev cmp date dd install python3 readlink sfdisk sha256sum stat sync; do
+ command -v "${command}" >/dev/null || fail "required command is unavailable: ${command}"
+done
+
+ghdr_bin=$(readlink -f "$1")
+manifest=$(readlink -f "$2")
+plan=$(readlink -f "$3")
+slot_a_image=$(readlink -f "$4")
+slot_b_image=$(readlink -f "$5")
+disk=$(readlink -f "$6")
+recovery_root=$(readlink -m "$7")
+
+[[ -x ${ghdr_bin} ]] || fail "GHDR verifier is not executable"
+for input in "${manifest}" "${plan}" "${slot_a_image}" "${slot_b_image}"; do
+ [[ -f ${input} && ! -L ${input} ]] || fail "signed installation input is not a regular file: ${input}"
+done
+[[ -b ${disk} ]] || fail "target must be a whole block device"
+[[ ! -e ${recovery_root} ]] || fail "recovery root already exists"
+[[ ! -L $(dirname "${recovery_root}") ]] || fail "recovery parent must not be a symlink"
+
+work=$(mktemp -d)
+cleanup() {
+ rm -rf "${work}"
+}
+trap cleanup EXIT
+
+python3 - "${plan}" "${work}/plan.env" <<'PY'
+import json
+import shlex
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as handle:
+ plan = json.load(handle)
+payload = plan["payload"]
+slots = {slot["name"]: slot for slot in payload["slots"]}
+required = {
+ "PLAN_NODE_ID": payload["node_id"],
+ "PLAN_SYSTEM_DISK": payload["system_disk"],
+ "PLAN_DISK_SECTORS": payload["disk_sectors"],
+ "PLAN_SECTOR_BYTES": payload["logical_sector_bytes"],
+ "PLAN_DISK_IDENTITY_SHA": payload["disk_identity_sha256"],
+ "PLAN_FIRST_PARTITION_LBA": payload["first_partition_lba"],
+ "PLAN_PROBE_SHA": payload["target_probe_sha256"],
+ "PLAN_EVIDENCE_SHA": payload["recovery_evidence_sha256"],
+ "PLAN_GENERATION": payload["generation"],
+ "SLOT_A_START": slots["A"]["lba_start"],
+ "SLOT_A_COUNT": slots["A"]["sector_count"],
+ "SLOT_A_SHA": slots["A"]["image_sha256"],
+ "SLOT_B_START": slots["B"]["lba_start"],
+ "SLOT_B_COUNT": slots["B"]["sector_count"],
+ "SLOT_B_SHA": slots["B"]["image_sha256"],
+}
+with open(sys.argv[2], "x", encoding="utf-8") as handle:
+ for key, value in required.items():
+ handle.write(f"{key}={shlex.quote(str(value))}\n")
+PY
+# shellcheck disable=SC1091
+source "${work}/plan.env"
+
+[[ ${disk} == "${PLAN_SYSTEM_DISK}" ]] || fail "target disk does not match the signed plan"
+[[ ${SLOT_A_START} == 34 && ${SLOT_A_COUNT} == 29 ]] || fail "slot A extent is not registered"
+[[ ${SLOT_B_START} == 73 && ${SLOT_B_COUNT} == 29 ]] || fail "slot B extent is not registered"
+[[ ${PLAN_FIRST_PARTITION_LBA} == 2048 ]] || fail "Linux partition boundary is not registered"
+[[ ${PLAN_SECTOR_BYTES} == 512 ]] || fail "logical sector size is not registered"
+[[ $(stat -c %s "${slot_a_image}") == $((SLOT_A_COUNT * PLAN_SECTOR_BYTES)) ]] || fail "slot A image size mismatch"
+[[ $(stat -c %s "${slot_b_image}") == $((SLOT_B_COUNT * PLAN_SECTOR_BYTES)) ]] || fail "slot B image size mismatch"
+[[ $(sha256sum "${slot_a_image}" | awk '{print $1}') == "${SLOT_A_SHA}" ]] || fail "slot A image digest mismatch"
+[[ $(sha256sum "${slot_b_image}" | awk '{print $1}') == "${SLOT_B_SHA}" ]] || fail "slot B image digest mismatch"
+
+collect_disk_evidence() {
+ local prefix=$1
+ sfdisk --json "${disk}" >"${work}/${prefix}.sfdisk.json"
+ blockdev --getsz "${disk}" >"${work}/${prefix}.sectors"
+ blockdev --getss "${disk}" >"${work}/${prefix}.sector-bytes"
+ sha256sum "${work}/${prefix}.sfdisk.json" | awk '{print $1}' >"${work}/${prefix}.identity"
+ python3 - "${work}/${prefix}.sfdisk.json" >"${work}/${prefix}.first-partition" <<'PY'
+import json
+import sys
+with open(sys.argv[1], "r", encoding="utf-8") as handle:
+ table = json.load(handle)["partitiontable"]
+starts = [int(partition["start"]) for partition in table["partitions"]]
+if not starts:
+ raise SystemExit("partition table has no Linux rescue partition")
+print(min(starts))
+PY
+}
+
+collect_disk_evidence before
+[[ $(<"${work}/before.sectors") == "${PLAN_DISK_SECTORS}" ]] || fail "disk sector count drifted"
+[[ $(<"${work}/before.sector-bytes") == "${PLAN_SECTOR_BYTES}" ]] || fail "disk sector size drifted"
+[[ $(<"${work}/before.identity") == "${PLAN_DISK_IDENTITY_SHA}" ]] || fail "disk identity drifted"
+[[ $(<"${work}/before.first-partition") == "${PLAN_FIRST_PARTITION_LBA}" ]] || fail "first partition boundary drifted"
+
+now_unix=$(date +%s)
+export PLAN_NODE_ID PLAN_PROBE_SHA PLAN_SYSTEM_DISK PLAN_DISK_SECTORS
+export PLAN_SECTOR_BYTES PLAN_DISK_IDENTITY_SHA PLAN_FIRST_PARTITION_LBA now_unix
+python3 - "${work}/readback.json" <<PY
+import json
+import os
+import sys
+value = {
+ "schema": "guanghu.ghdr-layout-readback/v1",
+ "node_id": os.environ["PLAN_NODE_ID"],
+ "observed_at_unix": int(os.environ["now_unix"]),
+ "target_probe_sha256": os.environ["PLAN_PROBE_SHA"],
+ "system_disk": os.environ["PLAN_SYSTEM_DISK"],
+ "disk_sectors": int(os.environ["PLAN_DISK_SECTORS"]),
+ "logical_sector_bytes": int(os.environ["PLAN_SECTOR_BYTES"]),
+ "disk_identity_sha256": os.environ["PLAN_DISK_IDENTITY_SHA"],
+ "first_partition_lba": int(os.environ["PLAN_FIRST_PARTITION_LBA"]),
+}
+with open(sys.argv[1], "x", encoding="utf-8") as handle:
+ json.dump(value, handle, sort_keys=True, separators=(",", ":"))
+PY
+
+"${ghdr_bin}" verify-signed-layout-plan \
+ "${manifest}" "${plan}" "${work}/readback.json" "${now_unix}" \
+ >"${work}/verification.json" || fail "signed layout verification rejected the write"
+python3 - "${work}/verification.json" <<'PY' || fail "signed layout verification did not return PASS_100"
+import json
+import sys
+with open(sys.argv[1], "r", encoding="utf-8") as handle:
+ result = json.load(handle)
+expected = {
+ "status": "PASS_100_SIGNED_LAYOUT_PLAN",
+ "gate_score": 100,
+ "allows_disk_write": True,
+ "verified_controller_count": 2,
+ "target_readback_fresh": True,
+ "target_readback_matches": True,
+}
+if any(result.get(key) != value for key, value in expected.items()):
+ raise SystemExit(1)
+PY
+
+dd if="${disk}" of="${work}/slot-a.before" bs=512 skip="${SLOT_A_START}" count="${SLOT_A_COUNT}" status=none
+dd if="${disk}" of="${work}/slot-b.before" bs=512 skip="${SLOT_B_START}" count="${SLOT_B_COUNT}" status=none
+cmp -s "${work}/slot-a.before" <(head -c $((SLOT_A_COUNT * PLAN_SECTOR_BYTES)) /dev/zero) || fail "slot A contains unknown data"
+cmp -s "${work}/slot-b.before" <(head -c $((SLOT_B_COUNT * PLAN_SECTOR_BYTES)) /dev/zero) || fail "slot B contains unknown data"
+
+install -d -m 0700 "${recovery_root}"
+install -m 0400 "${manifest}" "${recovery_root}/node-manifest.json"
+install -m 0400 "${plan}" "${recovery_root}/signed-layout-plan.json"
+install -m 0400 "${work}/verification.json" "${recovery_root}/layout-verification.json"
+install -m 0400 "${work}/before.sfdisk.json" "${recovery_root}/sfdisk-before.json"
+install -m 0400 "${work}/slot-a.before" "${recovery_root}/slot-a.before.bin"
+install -m 0400 "${work}/slot-b.before" "${recovery_root}/slot-b.before.bin"
+dd if="${disk}" of="${recovery_root}/first-2MiB.before.bin" bs=1M count=2 status=none
+dd if="${disk}" of="${recovery_root}/last-2MiB.before.bin" bs=512 skip=$((PLAN_DISK_SECTORS - 4096)) count=4096 status=none
+install -m 0400 "${slot_a_image}" "${recovery_root}/slot-a.candidate.img"
+install -m 0400 "${slot_b_image}" "${recovery_root}/slot-b.candidate.img"
+[[ -f /boot/grub/grub.cfg ]] && install -m 0400 /boot/grub/grub.cfg "${recovery_root}/grub.cfg.before"
+[[ -f /boot/grub/grubenv ]] && install -m 0400 /boot/grub/grubenv "${recovery_root}/grubenv.before"
+
+# Close the time-of-check/time-of-use window immediately before the first write.
+collect_disk_evidence commit
+cmp -s "${work}/before.sfdisk.json" "${work}/commit.sfdisk.json" || fail "partition table changed before commit"
+cmp -s "${work}/before.sectors" "${work}/commit.sectors" || fail "disk geometry changed before commit"
+cmp -s "${work}/before.sector-bytes" "${work}/commit.sector-bytes" || fail "sector size changed before commit"
+cmp -s "${work}/before.identity" "${work}/commit.identity" || fail "disk identity changed before commit"
+cmp -s "${work}/before.first-partition" "${work}/commit.first-partition" || fail "partition boundary changed before commit"
+dd if="${disk}" of="${work}/slot-a.commit" bs=512 skip="${SLOT_A_START}" count="${SLOT_A_COUNT}" status=none
+dd if="${disk}" of="${work}/slot-b.commit" bs=512 skip="${SLOT_B_START}" count="${SLOT_B_COUNT}" status=none
+cmp -s "${work}/slot-a.before" "${work}/slot-a.commit" || fail "slot A changed before commit"
+cmp -s "${work}/slot-b.before" "${work}/slot-b.commit" || fail "slot B changed before commit"
+
+# B first keeps the still-unconfigured Linux boot path unchanged if A cannot be committed.
+dd if="${slot_b_image}" of="${disk}" bs=512 seek="${SLOT_B_START}" count="${SLOT_B_COUNT}" conv=notrunc,fsync status=none
+dd if="${slot_a_image}" of="${disk}" bs=512 seek="${SLOT_A_START}" count="${SLOT_A_COUNT}" conv=notrunc,fsync status=none
+sync
+
+slot_a_readback=$(dd if="${disk}" bs=512 skip="${SLOT_A_START}" count="${SLOT_A_COUNT}" status=none | sha256sum | awk '{print $1}')
+slot_b_readback=$(dd if="${disk}" bs=512 skip="${SLOT_B_START}" count="${SLOT_B_COUNT}" status=none | sha256sum | awk '{print $1}')
+[[ ${slot_a_readback} == "${SLOT_A_SHA}" ]] || fail "slot A post-write readback failed"
+[[ ${slot_b_readback} == "${SLOT_B_SHA}" ]] || fail "slot B post-write readback failed"
+
+observed_at=$(date --iso-8601=seconds)
+cat >"${recovery_root}/INSTALL-RECEIPT.hldp" <<EOF
+schema: guanghu.native-ab-signed-install/v1
+status: VERIFIED_WRITTEN_NOT_BOOTABLE
+observed_at: ${observed_at}
+node_id: ${PLAN_NODE_ID}
+generation: ${PLAN_GENERATION}
+system_disk: ${PLAN_SYSTEM_DISK}
+disk_identity_sha256: ${PLAN_DISK_IDENTITY_SHA}
+recovery_evidence_sha256: ${PLAN_EVIDENCE_SHA}
+slot_a:
+ lba_start: ${SLOT_A_START}
+ sector_count: ${SLOT_A_COUNT}
+ sha256: ${SLOT_A_SHA}
+ readback_sha256: ${slot_a_readback}
+slot_b:
+ lba_start: ${SLOT_B_START}
+ sector_count: ${SLOT_B_COUNT}
+ sha256: ${SLOT_B_SHA}
+ readback_sha256: ${slot_b_readback}
+linux_partition_preserved: true
+target_grub_changed: false
+target_rebooted: false
+native_boot_armed: false
+next_gate: SIGNED_ONE_TIME_BOOT_AND_AUTOMATIC_LINUX_RETURN
+EOF
+chmod 0400 "${recovery_root}/INSTALL-RECEIPT.hldp"
+(cd "${recovery_root}" && sha256sum ./*) >"${work}/SHA256SUMS"
+install -m 0400 "${work}/SHA256SUMS" "${recovery_root}/SHA256SUMS"
+cat "${recovery_root}/INSTALL-RECEIPT.hldp"
diff --git a/guanghu-os/scripts/qemu-native-net-peer.py b/guanghu-os/scripts/qemu-native-net-peer.py
index 91af8f7..03f0adc 100644
--- a/guanghu-os/scripts/qemu-native-net-peer.py
+++ b/guanghu-os/scripts/qemu-native-net-peer.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import argparse
+import hashlib
import socket
import struct
import time
@@ -14,6 +15,94 @@ LOGIN_MAGIC = b"HLDP-GHOS-LOGIN!"
COMMIT_MAGIC = b"HLDP-CODE-COMMIT"
BRANCH_MAGIC = b"HLDP-BRANCH-MOVE"
RECOVERY_MAGIC = b"HLDP-RECOVER-OS!"
+CONTROL_MAGIC = b"GHCTL2\0\0"
+CONTROL_MESSAGE_SIZE = 32
+CONTROL_FRAME_SIZE = 48
+
+
+def _rotate_left(value: int, shift: int) -> int:
+ return ((value << shift) | (value >> (64 - shift))) & 0xFFFFFFFFFFFFFFFF
+
+
+def siphash24(key: bytes, message: bytes) -> int:
+ if len(key) != 16:
+ raise ValueError("SipHash keys must contain exactly 16 bytes")
+ k0, k1 = struct.unpack("<QQ", key)
+ v0 = k0 ^ 0x736F6D6570736575
+ v1 = k1 ^ 0x646F72616E646F6D
+ v2 = k0 ^ 0x6C7967656E657261
+ v3 = k1 ^ 0x7465646279746573
+
+ def sip_round() -> None:
+ nonlocal v0, v1, v2, v3
+ v0 = (v0 + v1) & 0xFFFFFFFFFFFFFFFF
+ v1 = _rotate_left(v1, 13) ^ v0
+ v0 = _rotate_left(v0, 32)
+ v2 = (v2 + v3) & 0xFFFFFFFFFFFFFFFF
+ v3 = _rotate_left(v3, 16) ^ v2
+ v0 = (v0 + v3) & 0xFFFFFFFFFFFFFFFF
+ v3 = _rotate_left(v3, 21) ^ v0
+ v2 = (v2 + v1) & 0xFFFFFFFFFFFFFFFF
+ v1 = _rotate_left(v1, 17) ^ v2
+ v2 = _rotate_left(v2, 32)
+
+ whole = len(message) - (len(message) % 8)
+ for offset in range(0, whole, 8):
+ word = struct.unpack_from("<Q", message, offset)[0]
+ v3 ^= word
+ sip_round()
+ sip_round()
+ v0 ^= word
+ tail = message[whole:]
+ final = len(message) << 56
+ for index, byte in enumerate(tail):
+ final |= byte << (8 * index)
+ v3 ^= final
+ sip_round()
+ sip_round()
+ v0 ^= final
+ v2 ^= 0xFF
+ for _ in range(4):
+ sip_round()
+ return v0 ^ v1 ^ v2 ^ v3
+
+
+def node_target_tag(node_id: str) -> int:
+ if not node_id or any(
+ not (character.isascii() and (character.isupper() or character.isdigit() or character == "-"))
+ for character in node_id
+ ):
+ raise ValueError("node id must use uppercase ASCII letters, digits, and hyphens")
+ return int.from_bytes(hashlib.sha256(node_id.encode("ascii")).digest()[:8], "little")
+
+
+def control_frame(
+ *,
+ node_id: str,
+ nonce: int,
+ command: int,
+ controller_a_key: bytes,
+ controller_b_key: bytes,
+) -> bytes:
+ if not 0 < nonce < 1 << 64:
+ raise ValueError("control nonce must be a nonzero unsigned 64-bit integer")
+ if command not in range(4):
+ raise ValueError("control command is not registered")
+ message = (
+ CONTROL_MAGIC
+ + struct.pack("<Q", node_target_tag(node_id))
+ + struct.pack("<Q", nonce)
+ + bytes([command])
+ + b"\0" * 7
+ )
+ assert len(message) == CONTROL_MESSAGE_SIZE
+ frame = message + struct.pack(
+ "<QQ",
+ siphash24(controller_a_key, message),
+ siphash24(controller_b_key, message),
+ )
+ assert len(frame) == CONTROL_FRAME_SIZE
+ return frame
def checksum(payload: bytes) -> int:
@@ -69,6 +158,40 @@ def icmp_request(sequence: int, magic: bytes) -> bytes:
return GUEST_MAC + PEER_MAC + b"\x08\x00" + ip + icmp
+def authenticated_icmp_request(sequence: int, frame: bytes) -> bytes:
+ if len(frame) != CONTROL_FRAME_SIZE:
+ raise ValueError("authenticated control frame has an invalid size")
+ icmp = struct.pack("!BBHHH", 8, 0, 0, 0x4748, sequence) + frame
+ icmp = icmp[:2] + struct.pack("!H", checksum(icmp)) + icmp[4:]
+ total_length = 20 + len(icmp)
+ ip = struct.pack(
+ "!BBHHHBBH4s4s",
+ 0x45,
+ 0,
+ total_length,
+ 0x484C,
+ 0,
+ 64,
+ 1,
+ 0,
+ LOGIN_CLIENT_IP,
+ GUEST_IP,
+ )
+ ip = ip[:10] + struct.pack("!H", checksum(ip)) + ip[12:]
+ return GUEST_MAC + PEER_MAC + b"\x08\x00" + ip + icmp
+
+
+def validate_authenticated_reply(frame: bytes, control: bytes) -> None:
+ assert frame[0:6] == PEER_MAC
+ assert frame[6:12] == GUEST_MAC
+ assert frame[12:14] == b"\x08\x00"
+ assert frame[26:30] == GUEST_IP
+ assert frame[30:34] == LOGIN_CLIENT_IP
+ assert frame[34] == 0
+ assert frame[42:42 + CONTROL_FRAME_SIZE] == control
+ assert checksum(frame[34:]) == 0
+
+
def validate_reply(frame: bytes, magic: bytes) -> None:
assert frame[0:6] == PEER_MAC
assert frame[6:12] == GUEST_MAC
@@ -87,8 +210,45 @@ def main() -> None:
parser.add_argument("--receipt", required=True)
parser.add_argument("--resident", action="store_true")
parser.add_argument("--login-only", action="store_true")
+ parser.add_argument("--guest-mac", default="52:54:00:26:71:98")
+ parser.add_argument("--guest-ip", default="10.0.0.7")
+ parser.add_argument("--peer-ip", default="10.0.0.1")
+ parser.add_argument("--login-client-ip", default="10.0.0.2")
+ parser.add_argument("--authenticated-control", action="store_true")
+ parser.add_argument("--node-id")
+ parser.add_argument("--controller-a-key-hex")
+ parser.add_argument("--controller-b-key-hex")
+ parser.add_argument("--nonce-start", type=int, default=1)
+ parser.add_argument("--exercise-auth-rejections", action="store_true")
+ parser.add_argument("--prior-nonce-probe", type=int, default=0)
args = parser.parse_args()
+ global GUEST_MAC, GUEST_IP, PEER_IP, LOGIN_CLIENT_IP
+ GUEST_MAC = bytes.fromhex(args.guest_mac.replace(":", ""))
+ GUEST_IP = socket.inet_aton(args.guest_ip)
+ PEER_IP = socket.inet_aton(args.peer_ip)
+ LOGIN_CLIENT_IP = socket.inet_aton(args.login_client_ip)
+ controller_a_key = None
+ controller_b_key = None
+ if args.authenticated_control:
+ if not args.node_id or not args.controller_a_key_hex or not args.controller_b_key_hex:
+ parser.error("authenticated control requires a node id and two controller keys")
+ try:
+ controller_a_key = bytes.fromhex(args.controller_a_key_hex)
+ controller_b_key = bytes.fromhex(args.controller_b_key_hex)
+ except ValueError as error:
+ parser.error(f"controller keys must be hexadecimal: {error}")
+ if len(controller_a_key) != 16 or len(controller_b_key) != 16:
+ parser.error("each controller key must contain exactly 16 bytes")
+ if controller_a_key == controller_b_key:
+ parser.error("controller keys must be independent")
+ if args.nonce_start <= 0:
+ parser.error("nonce start must be positive")
+ elif args.exercise_auth_rejections:
+ parser.error("authentication rejection probes require authenticated control")
+ if args.prior_nonce_probe < 0:
+ parser.error("prior nonce probe cannot be negative")
+
peer = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
peer.bind(("127.0.0.1", args.listen_port))
peer.settimeout(0.2)
@@ -101,6 +261,14 @@ def main() -> None:
resident_login_reply_count = 0
recovery_reply_verified = False
command_phase = "login"
+ control_nonce = args.nonce_start
+ last_control_frame = None
+ rejection_probes_sent = False
+ rejection_probes_sent_at = 0.0
+ valid_control_sent = not args.exercise_auth_rejections
+ replay_probe_pending = False
+ replay_probe_sent_at = 0.0
+ replay_probe_done = not args.exercise_auth_rejections
def write_receipt(
*,
@@ -121,6 +289,19 @@ def main() -> None:
"recovery_reply_verified: "
f"{str(recovery_reply_verified).lower()}\n"
"login_magic: HLDP-GHOS-LOGIN!\n"
+ "authenticated_control: "
+ f"{str(args.authenticated_control).lower()}\n"
+ f"last_accepted_nonce: {control_nonce - 1 if args.authenticated_control else 0}\n"
+ "legacy_control_rejected: "
+ f"{str(args.exercise_auth_rejections).lower()}\n"
+ "wrong_target_rejected: "
+ f"{str(args.exercise_auth_rejections).lower()}\n"
+ "bad_dual_mac_rejected: "
+ f"{str(args.exercise_auth_rejections).lower()}\n"
+ "replayed_nonce_rejected: "
+ f"{str(args.exercise_auth_rejections).lower()}\n"
+ "persisted_prior_nonce_rejected: "
+ f"{str(args.prior_nonce_probe > 0).lower()}\n"
)
def phase_magic() -> bytes:
@@ -132,13 +313,85 @@ def main() -> None:
"recovery": RECOVERY_MAGIC,
}[command_phase]
+ def phase_command() -> int:
+ return {
+ "login": 0,
+ "commit": 1,
+ "branch": 2,
+ "resident_login": 0,
+ "recovery": 3,
+ }[command_phase]
+
+ def request(sequence: int) -> bytes:
+ nonlocal last_control_frame
+ if not args.authenticated_control:
+ return icmp_request(sequence, phase_magic())
+ assert controller_a_key is not None and controller_b_key is not None
+ last_control_frame = control_frame(
+ node_id=args.node_id,
+ nonce=control_nonce,
+ command=phase_command(),
+ controller_a_key=controller_a_key,
+ controller_b_key=controller_b_key,
+ )
+ return authenticated_icmp_request(sequence, last_control_frame)
+
while time.monotonic() < deadline:
try:
frame = peer.recv(4096)
except TimeoutError:
if arp_verified:
+ if args.exercise_auth_rejections and not rejection_probes_sent:
+ assert controller_a_key is not None and controller_b_key is not None
+ peer.sendto(icmp_request(0, phase_magic()), qemu)
+ wrong_target = control_frame(
+ node_id="GH-CVM-MAIN-PROD-01-WRONG",
+ nonce=control_nonce,
+ command=phase_command(),
+ controller_a_key=controller_a_key,
+ controller_b_key=controller_b_key,
+ )
+ peer.sendto(authenticated_icmp_request(0, wrong_target), qemu)
+ bad_mac = bytearray(
+ control_frame(
+ node_id=args.node_id,
+ nonce=control_nonce,
+ command=phase_command(),
+ controller_a_key=controller_a_key,
+ controller_b_key=controller_b_key,
+ )
+ )
+ bad_mac[-1] ^= 0x01
+ peer.sendto(authenticated_icmp_request(0, bytes(bad_mac)), qemu)
+ if args.prior_nonce_probe:
+ prior_nonce = control_frame(
+ node_id=args.node_id,
+ nonce=args.prior_nonce_probe,
+ command=phase_command(),
+ controller_a_key=controller_a_key,
+ controller_b_key=controller_b_key,
+ )
+ peer.sendto(authenticated_icmp_request(0, prior_nonce), qemu)
+ rejection_probes_sent = True
+ rejection_probes_sent_at = time.monotonic()
+ continue
+ if args.exercise_auth_rejections and not valid_control_sent:
+ if time.monotonic() - rejection_probes_sent_at < 0.6:
+ continue
+ peer.sendto(request(reply_count + 1), qemu)
+ valid_control_sent = True
+ login_sent = True
+ continue
+ if replay_probe_pending:
+ if time.monotonic() - replay_probe_sent_at < 0.6:
+ continue
+ replay_probe_pending = False
+ replay_probe_done = True
+ peer.sendto(request(reply_count + 1), qemu)
+ login_sent = True
+ continue
peer.sendto(
- icmp_request(reply_count + 1, phase_magic()),
+ request(reply_count + 1),
qemu,
)
login_sent = True
@@ -148,13 +401,31 @@ def main() -> None:
arp_verified = True
continue
if frame[12:14] == b"\x08\x00":
+ if args.exercise_auth_rejections and (
+ not valid_control_sent or replay_probe_pending
+ ):
+ raise SystemExit("native runtime replied to a rejected control probe")
magic = phase_magic()
- validate_reply(frame, magic)
+ if args.authenticated_control:
+ assert last_control_frame is not None
+ validate_authenticated_reply(frame, last_control_frame)
+ control_nonce += 1
+ else:
+ validate_reply(frame, magic)
reply_count += 1
if command_phase == "login":
login_reply_count += 1
+ if args.exercise_auth_rejections and not replay_probe_done:
+ assert last_control_frame is not None
+ peer.sendto(
+ authenticated_icmp_request(reply_count, last_control_frame),
+ qemu,
+ )
+ replay_probe_pending = True
+ replay_probe_sent_at = time.monotonic()
+ continue
if command_phase == "login" and reply_count < 3:
- peer.sendto(icmp_request(reply_count + 1, LOGIN_MAGIC), qemu)
+ peer.sendto(request(reply_count + 1), qemu)
continue
if command_phase == "login":
if args.login_only:
@@ -164,26 +435,23 @@ def main() -> None:
)
return
command_phase = "commit"
- peer.sendto(icmp_request(4, COMMIT_MAGIC), qemu)
+ peer.sendto(request(4), qemu)
continue
if command_phase == "commit":
command_phase = "branch"
- peer.sendto(icmp_request(5, BRANCH_MAGIC), qemu)
+ peer.sendto(request(5), qemu)
continue
if command_phase == "branch" and args.resident:
command_phase = "resident_login"
- peer.sendto(icmp_request(6, LOGIN_MAGIC), qemu)
+ peer.sendto(request(6), qemu)
continue
if command_phase == "resident_login":
resident_login_reply_count += 1
if resident_login_reply_count < 10:
- peer.sendto(
- icmp_request(6 + resident_login_reply_count, LOGIN_MAGIC),
- qemu,
- )
+ peer.sendto(request(6 + resident_login_reply_count), qemu)
continue
command_phase = "recovery"
- peer.sendto(icmp_request(16, RECOVERY_MAGIC), qemu)
+ peer.sendto(request(16), qemu)
continue
if command_phase == "recovery":
recovery_reply_verified = True
diff --git a/guanghu-os/scripts/render-native-recovery-beacon.sh b/guanghu-os/scripts/render-native-recovery-beacon.sh
index d782f0c..47c29a4 100755
--- a/guanghu-os/scripts/render-native-recovery-beacon.sh
+++ b/guanghu-os/scripts/render-native-recovery-beacon.sh
@@ -8,12 +8,18 @@ set -euo pipefail
mkdir -p "$1"
output_root=$(cd "$1" && pwd)
+recovery_menu_id=${GHOS_RECOVERY_MENU_ID:-gnulinux-simple-9842d3d6-a839-4127-bda7-f19137effe71}
+[[ ${recovery_menu_id} =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]] || {
+ echo "invalid GHOS recovery menu identifier" >&2
+ exit 65
+}
-python3 - "${output_root}" <<'PY'
+python3 - "${output_root}" "${recovery_menu_id}" <<'PY'
import pathlib
import sys
output = pathlib.Path(sys.argv[1])
+recovery_menu_id = sys.argv[2]
header = (
b"# GRUB Environment Block\n"
b"# WARNING: Do not edit this file by tools other than grub-editenv!!!\n"
@@ -30,19 +36,21 @@ write_environment(
b"guanghu_recovery=ubuntu\n",
)
write_environment("guanghu-recovery-clear.env")
-PY
-install -m 0755 /dev/stdin "${output_root}/08_guanghu_native_recovery" <<'EOF'
-#!/bin/sh
+grub_script = f'''#!/bin/sh
exec tail -n +3 $0
insmod loadenv
set guanghu_recovery=
if load_env --file '(hd0)68+2' guanghu_recovery; then
- if [ "${guanghu_recovery}" = "ubuntu" ]; then
- set default="gnulinux-simple-9842d3d6-a839-4127-bda7-f19137effe71"
+ if [ "${{guanghu_recovery}}" = "ubuntu" ]; then
+ set default="{recovery_menu_id}"
fi
fi
-EOF
+'''
+grub_path = output / "08_guanghu_native_recovery"
+grub_path.write_text(grub_script)
+grub_path.chmod(0o755)
+PY
sha256sum \
"${output_root}/guanghu-recovery-active.env" \
diff --git a/guanghu-os/scripts/test-native-ab-signed-installer-contract.sh b/guanghu-os/scripts/test-native-ab-signed-installer-contract.sh
new file mode 100755
index 0000000..27df457
--- /dev/null
+++ b/guanghu-os/scripts/test-native-ab-signed-installer-contract.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+source_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+installer="${source_root}/scripts/install-native-ab-signed.sh"
+
+bash -n "${installer}"
+grep -Fq 'verify-signed-layout-plan' "${installer}"
+grep -Fq 'PASS_100_SIGNED_LAYOUT_PLAN' "${installer}"
+grep -Fq 'verified_controller_count' "${installer}"
+grep -Fq 'recovery_evidence_sha256' "${installer}"
+grep -Fq 'cmp -s "${work}/slot-a.before" "${work}/slot-a.commit"' "${installer}"
+grep -Fq 'cmp -s "${work}/slot-b.before" "${work}/slot-b.commit"' "${installer}"
+grep -Fq 'target_grub_changed: false' "${installer}"
+grep -Fq 'target_rebooted: false' "${installer}"
+grep -Fq 'native_boot_armed: false' "${installer}"
+grep -Fq 'VERIFIED_WRITTEN_NOT_BOOTABLE' "${installer}"
+grep -Fq '(cd "${recovery_root}" && sha256sum ./*) >"${work}/SHA256SUMS"' "${installer}"
+
+if grep -Eq '(^|[[:space:]])(mkfs|sfdisk[[:space:]]+[^-]|parted|grub-install|update-grub|reboot|shutdown)([[:space:]]|$)' "${installer}"; then
+ echo "installer must not format, repartition, change GRUB, or reboot" >&2
+ exit 1
+fi
+
+echo "native A/B signed installer contract: PASS"
diff --git a/guanghu-os/scripts/test-native-physical-candidate.sh b/guanghu-os/scripts/test-native-physical-candidate.sh
index 1467a67..c896b71 100755
--- a/guanghu-os/scripts/test-native-physical-candidate.sh
+++ b/guanghu-os/scripts/test-native-physical-candidate.sh
@@ -17,29 +17,65 @@ disk_image=${test_root}/physical-layout.img
failure_disk_image=${test_root}/physical-layout-failure.img
peer_receipt=${test_root}/native-net-peer.hldp
peer_log=${test_root}/native-net-peer.log
+guest_mac=${GHOS_TEST_GUEST_MAC:-52:54:00:26:71:98}
+guest_ip=${GHOS_TEST_GUEST_IP:-10.0.0.7}
+peer_ip=${GHOS_TEST_PEER_IP:-10.0.0.1}
+login_client_ip=${GHOS_TEST_LOGIN_CLIENT_IP:-10.0.0.2}
+node_id=${GHOS_TEST_NODE_ID:-BS-SH-005}
+candidate_lba=${GHOS_TEST_CANDIDATE_LBA:-34}
+control_auth=${GHOS_TEST_CONTROL_AUTH:-0}
+controller_a_key=${GHOS_TEST_CONTROLLER_A_KEY:-}
+controller_b_key=${GHOS_TEST_CONTROLLER_B_KEY:-}
+[[ ${node_id} =~ ^[A-Z0-9][A-Z0-9-]{0,63}$ ]]
+[[ ${candidate_lba} == 34 || ${candidate_lba} == 73 ]]
+[[ ${control_auth} == 0 || ${control_auth} == 1 ]]
+peer_args=(
+ --guest-mac "${guest_mac}"
+ --guest-ip "${guest_ip}"
+ --peer-ip "${peer_ip}"
+ --login-client-ip "${login_client_ip}"
+)
+if [[ ${control_auth} == 1 ]]; then
+ [[ ${controller_a_key} =~ ^[0-9a-fA-F]{32}$ ]]
+ [[ ${controller_b_key} =~ ^[0-9a-fA-F]{32}$ ]]
+ [[ ${controller_a_key} != "${controller_b_key}" ]]
+ peer_args+=(
+ --authenticated-control
+ --node-id "${node_id}"
+ --controller-a-key-hex "${controller_a_key}"
+ --controller-b-key-hex "${controller_b_key}"
+ --nonce-start 1
+ --exercise-auth-rejections
+ )
+fi
+shell_pid=${BASHPID:-$$}
peer_pid=
cleanup() {
+ status=$?
if [[ -n ${peer_pid} ]]; then
kill "${peer_pid}" 2>/dev/null || true
fi
rm -rf "${test_root}"
+ exit "${status}"
}
trap cleanup EXIT
truncate -s 2M "${disk_image}"
-nasm -f bin "${native_root}/physical-test-mbr.asm" \
+nasm -f bin -dCANDIDATE_LBA="${candidate_lba}" \
+ "${native_root}/physical-test-mbr.asm" \
-o "${test_root}/physical-test-mbr.bin"
dd if="${test_root}/physical-test-mbr.bin" of="${disk_image}" \
bs=512 seek=0 conv=notrunc status=none
dd if="${candidate}" of="${disk_image}" \
- bs=512 seek=34 conv=notrunc status=none
+ bs=512 seek="${candidate_lba}" conv=notrunc status=none
-peer_port=$((22000 + BASHPID % 10000))
+peer_port=$((22000 + shell_pid % 10000))
qemu_port=$((peer_port + 1))
python3 "${source_root}/scripts/qemu-native-net-peer.py" \
--listen-port "${peer_port}" \
--qemu-port "${qemu_port}" \
- --receipt "${peer_receipt}" >"${peer_log}" 2>&1 &
+ --receipt "${peer_receipt}" \
+ "${peer_args[@]}" >"${peer_log}" 2>&1 &
peer_pid=$!
set +e
timeout 20 qemu-system-x86_64 \
@@ -48,7 +84,7 @@ timeout 20 qemu-system-x86_64 \
-drive "if=none,id=ghboot,format=raw,file=${disk_image}" \
-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 \
+ -device virtio-net-pci,netdev=ghnet,disable-modern=on,mac="${guest_mac}" \
-display none \
-monitor none \
-serial "file:${serial_log}" \
@@ -64,6 +100,14 @@ grep -q '^icmp_login_reply_verified: true$' "${peer_receipt}"
grep -q '^icmp_login_reply_count: 3$' "${peer_receipt}"
grep -q '^code_commit_reply_verified: true$' "${peer_receipt}"
grep -q '^branch_move_reply_verified: true$' "${peer_receipt}"
+if [[ ${control_auth} == 1 ]]; then
+ grep -q '^authenticated_control: true$' "${peer_receipt}"
+ grep -q '^last_accepted_nonce: 5$' "${peer_receipt}"
+ grep -q '^legacy_control_rejected: true$' "${peer_receipt}"
+ grep -q '^wrong_target_rejected: true$' "${peer_receipt}"
+ grep -q '^bad_dual_mac_rejected: true$' "${peer_receipt}"
+ grep -q '^replayed_nonce_rejected: true$' "${peer_receipt}"
+fi
for evidence in \
GHOS_BOOT_STAGE0=BIOS \
GHOS_NATIVE_KERNEL_ENTERED=true \
@@ -88,15 +132,21 @@ for evidence in \
GHOS_DISK_PROOF_OBSERVED_AFTER_RESET=LBA63; do
grep -q "^${evidence}" "${serial_log}"
done
-python3 - "${disk_image}" <<'PY'
+python3 - "${disk_image}" "${guest_mac}" "${login_client_ip}" "${control_auth}" "${node_id}" <<'PY'
+import hashlib
import pathlib
+import socket
import sys
disk = pathlib.Path(sys.argv[1]).read_bytes()
+guest_mac = bytes.fromhex(sys.argv[2].replace(":", ""))
+login_client_ip = socket.inet_aton(sys.argv[3])
+control_auth = sys.argv[4] == "1"
+node_id = sys.argv[5]
proof = disk[63 * 512:64 * 512]
assert proof[0] == 0xA7
assert proof[1:].startswith(b"GHOS_NATIVE_LONG64_DISK_PROOF\x00")
assert proof[32:36] == bytes([1, 1, 1, 1])
-assert proof[36:42] == bytes.fromhex("525400267198")
+assert proof[36:42] == guest_mac
assert proof[42] == 0x7F
assert proof[43] == 0x00
assert int.from_bytes(proof[44:46], "little") > 0
@@ -105,12 +155,14 @@ assert int.from_bytes(proof[48:50], "little") > 0
assert proof[54:60] != bytes(6)
assert proof[60:62] == bytes([1, 1])
assert proof[62:64] == bytes([1, 1])
-assert proof[64:68] == bytes([10, 0, 0, 2])
+assert proof[64:68] == login_client_ip
assert proof[72:88] == b"HLDP-GHOS-LOGIN!"
assert proof[88:90] == bytes([1, 1])
assert proof[90:93] == bytes([3, 1, 1])
assert proof[93:99] == bytes([1, 1, 1, 1, 1, 1])
assert proof[102:105] == bytes([1, 1, 1])
+if control_auth:
+ assert proof[105:111] == bytes([1, 1, 1, 1, 1, 1])
world_store = disk[64 * 512:65 * 512]
assert world_store.startswith(b"GHOS_HLDP_WORLD_STORE_V1\n")
for identity in (
@@ -149,13 +201,26 @@ assert gestational_root.startswith(b"GHOS_GHCIP_ROOT_V1\n")
assert b"GHCIP_REGISTRY_STATE=EMPTY\n" in gestational_root
assert b"GHCIP_REVIEW_STATE=NOT_STARTED\n" in gestational_root
assert b"GHCIP_PERSONA_STATE=NOT_BORN\n" in gestational_root
+if control_auth:
+ control_state = disk[72 * 512:73 * 512]
+ target = int.from_bytes(
+ hashlib.sha256(node_id.encode("ascii")).digest()[:8], "little"
+ )
+ nonce = 5
+ assert control_state[:8] == b"GHCTRLS2"
+ assert int.from_bytes(control_state[8:16], "little") == target
+ assert int.from_bytes(control_state[16:24], "little") == nonce
+ assert int.from_bytes(control_state[24:32], "little") == (
+ nonce ^ 0xFFFFFFFFFFFFFFFF
+ )
+ assert control_state[32:] == bytes(480)
PY
truncate -s 2M "${failure_disk_image}"
dd if="${test_root}/physical-test-mbr.bin" of="${failure_disk_image}" \
bs=512 seek=0 conv=notrunc status=none
dd if="${candidate}" of="${failure_disk_image}" \
- bs=512 seek=34 conv=notrunc status=none
+ bs=512 seek="${candidate_lba}" conv=notrunc status=none
set +e
timeout 20 qemu-system-x86_64 \
@@ -192,7 +257,8 @@ observed_at=$(date --iso-8601=seconds)
image_sha=$(sha256sum "${candidate}" | awk '{print $1}')
cat >"${receipt}" <<EOF
schema: guanghu.native-physical-layout-test/v1
-receipt_id: GH-OS-LAB-001-NATIVE-PHYSICAL-LAYOUT-QEMU-001
+receipt_id: ${node_id}-NATIVE-PHYSICAL-LAYOUT-QEMU-001
+node_id: ${node_id}
status: VERIFIED
observed_at: ${observed_at}
image:
@@ -201,9 +267,9 @@ image:
size_bytes: 14848
sha256: ${image_sha}
layout:
- candidate_start_lba: 34
+ candidate_start_lba: ${candidate_lba}
candidate_sector_count: 29
- stage2_start_lba: 35
+ stage2_start_lba: $((candidate_lba + 1))
stage2_sector_count: 28
sector_size: 512
proof:
@@ -241,7 +307,13 @@ proof:
native_gestational_index_sector_count: 2
native_gestational_registry_state: EMPTY
native_persona_state: NOT_BORN
- mac_read_from_native_device: 52:54:00:26:71:98
+ mac_read_from_native_device: ${guest_mac}
+authenticated_control:
+ enabled: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ target_binding_required: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ dual_controller_mac_required: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ persistent_nonce_lba: 72
+ replay_rejection_exercised: $([[ ${control_auth} == 1 ]] && echo true || echo false)
diagnostic_recovery:
ghal_failure_writes_stage_and_error: true
ghal_failure_hardware_resets: true
@@ -250,8 +322,8 @@ diagnostic_recovery:
acceptance:
qemu_physical_layout: true
physical_server_boot: false
- linux_replaced_on_bs_sh_005: false
-next_action: BACKUP_AND_INSTALL_ONE_TIME_GRUB_ENTRY
+ linux_replaced_on_target_node: false
+next_action: COMPLETE_AUTOMATIC_LINUX_RETURN_AND_EXTERNAL_CONTROLLER_GATES
EOF
cat "${serial_log}" >>"${receipt}.serial.log"
cat "${failure_serial_log}" >>"${receipt}.failure.serial.log"
diff --git a/guanghu-os/scripts/test-native-recovery-beacon-contract.sh b/guanghu-os/scripts/test-native-recovery-beacon-contract.sh
index cb381c9..a62fe3b 100755
--- a/guanghu-os/scripts/test-native-recovery-beacon-contract.sh
+++ b/guanghu-os/scripts/test-native-recovery-beacon-contract.sh
@@ -7,6 +7,18 @@ trap 'rm -rf "${test_root}"' EXIT
"${source_root}/scripts/render-native-recovery-beacon.sh" "${test_root}"
+enterprise_root=${test_root}/enterprise
+enterprise_menu_id=gnulinux-simple-7bccaefa-b0a9-4f6f-bd32-22dde0066c0b
+GHOS_RECOVERY_MENU_ID=${enterprise_menu_id} \
+ "${source_root}/scripts/render-native-recovery-beacon.sh" "${enterprise_root}"
+grep -Fq "set default=\"${enterprise_menu_id}\"" \
+ "${enterprise_root}/08_guanghu_native_recovery"
+if GHOS_RECOVERY_MENU_ID='invalid id; reboot' \
+ "${source_root}/scripts/render-native-recovery-beacon.sh" "${test_root}/invalid"; then
+ echo "invalid recovery menu identifiers must fail closed" >&2
+ exit 1
+fi
+
active=${test_root}/guanghu-recovery-active.env
clear=${test_root}/guanghu-recovery-clear.env
grub=${test_root}/08_guanghu_native_recovery
diff --git a/guanghu-os/scripts/test-native-resident-candidate.sh b/guanghu-os/scripts/test-native-resident-candidate.sh
index 2c52fce..cab4bb2 100755
--- a/guanghu-os/scripts/test-native-resident-candidate.sh
+++ b/guanghu-os/scripts/test-native-resident-candidate.sh
@@ -15,6 +15,7 @@ serial_log=${test_root}/serial.log
serial_log_second=${test_root}/serial-second.log
disk_image=${test_root}/resident-layout.img
corrupt_disk_image=${test_root}/resident-layout-corrupt.img
+control_state_corrupt_disk_image=${test_root}/resident-layout-control-state-corrupt.img
peer_receipt=${test_root}/native-net-peer.hldp
peer_log=${test_root}/native-net-peer.log
peer_receipt_second=${test_root}/native-net-peer-second.hldp
@@ -22,8 +23,37 @@ peer_log_second=${test_root}/native-net-peer-second.log
peer_receipt_corrupt=${test_root}/native-net-peer-corrupt.hldp
peer_log_corrupt=${test_root}/native-net-peer-corrupt.log
serial_log_corrupt=${test_root}/serial-corrupt.log
+serial_log_control_state_corrupt=${test_root}/serial-control-state-corrupt.log
+guest_mac=${GHOS_TEST_GUEST_MAC:-52:54:00:26:71:98}
+guest_ip=${GHOS_TEST_GUEST_IP:-10.0.0.7}
+peer_ip=${GHOS_TEST_PEER_IP:-10.0.0.1}
+login_client_ip=${GHOS_TEST_LOGIN_CLIENT_IP:-10.0.0.2}
+ubuntu_menu_id=${GHOS_TEST_UBUNTU_MENU_ID:-gnulinux-simple-9842d3d6-a839-4127-bda7-f19137effe71}
+node_id=${GHOS_TEST_NODE_ID:-BS-SH-005}
+[[ ${node_id} =~ ^[A-Z0-9][A-Z0-9-]{0,63}$ ]]
+control_auth=${GHOS_TEST_CONTROL_AUTH:-0}
+controller_a_key=${GHOS_TEST_CONTROLLER_A_KEY:-}
+controller_b_key=${GHOS_TEST_CONTROLLER_B_KEY:-}
+peer_auth_args=()
+if [[ ${control_auth} == 1 ]]; then
+ [[ ${controller_a_key} =~ ^[0-9a-fA-F]{32}$ ]]
+ [[ ${controller_b_key} =~ ^[0-9a-fA-F]{32}$ ]]
+ [[ ${controller_a_key} != "${controller_b_key}" ]]
+ peer_auth_args=(
+ --authenticated-control
+ --node-id "${node_id}"
+ --controller-a-key-hex "${controller_a_key}"
+ --controller-b-key-hex "${controller_b_key}"
+ --exercise-auth-rejections
+ )
+elif [[ ${control_auth} != 0 ]]; then
+ echo "GHOS_TEST_CONTROL_AUTH must be 0 or 1" >&2
+ exit 65
+fi
+shell_pid=${BASHPID:-$$}
peer_pid=
cleanup() {
+ status=$?
if [[ -n ${peer_pid} ]]; then
kill "${peer_pid}" 2>/dev/null || true
fi
@@ -32,6 +62,7 @@ cleanup() {
else
echo "GHOS_TEST_ROOT=${test_root}" >&2
fi
+ exit "${status}"
}
trap cleanup EXIT
@@ -43,12 +74,19 @@ dd if="${test_root}/physical-test-mbr.bin" of="${disk_image}" \
dd if="${candidate}" of="${disk_image}" \
bs=512 seek=34 conv=notrunc status=none
-peer_port=$((24000 + BASHPID % 10000))
+peer_port=$((24000 + shell_pid % 10000))
qemu_port=$((peer_port + 1))
python3 "${source_root}/scripts/qemu-native-net-peer.py" \
--listen-port "${peer_port}" \
--qemu-port "${qemu_port}" \
--receipt "${peer_receipt}" \
+ --guest-mac "${guest_mac}" \
+ --guest-ip "${guest_ip}" \
+ --peer-ip "${peer_ip}" \
+ --login-client-ip "${login_client_ip}" \
+ "${peer_auth_args[@]+"${peer_auth_args[@]}"}" \
+ --nonce-start 1 \
+ --prior-nonce-probe 0 \
--resident >"${peer_log}" 2>&1 &
peer_pid=$!
set +e
@@ -58,7 +96,7 @@ timeout 30 qemu-system-x86_64 \
-drive "if=none,id=ghboot,format=raw,file=${disk_image}" \
-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 \
+ -device virtio-net-pci,netdev=ghnet,disable-modern=on,mac="${guest_mac}" \
-display none \
-monitor none \
-serial "file:${serial_log}" \
@@ -79,11 +117,14 @@ grep -q '^GHOS_NATIVE_RECOVERY_BEACON=WRITE_READ_VERIFIED' "${serial_log}"
grep -q '^GHOS_GHCIP_INDEX=INITIALIZED_WRITE_READ_VERIFIED' "${serial_log}"
grep -q '^GHOS_DISK_PROOF_OBSERVED_AFTER_RESET=LBA63' "${serial_log}"
-python3 - "${disk_image}" <<'PY'
+python3 - "${disk_image}" "${control_auth}" "${node_id}" <<'PY'
+import hashlib
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
+control_auth = sys.argv[2] == "1"
+node_id = sys.argv[3]
with path.open("rb") as disk:
def sector(lba: int, count: int = 1) -> bytes:
disk.seek(lba * 512)
@@ -97,6 +138,8 @@ with path.open("rb") as disk:
assert proof[93:99] == bytes([1, 1, 1, 1, 1, 1])
assert proof[99:102] == bytes([1, 1, 1])
assert proof[102:105] == bytes([1, 1, 1])
+ if control_auth:
+ assert proof[105:111] == bytes([1, 1, 1, 1, 1, 1])
world_store = sector(64)
assert world_store.startswith(b"GHOS_HLDP_WORLD_STORE_V1\n")
@@ -130,6 +173,19 @@ with path.open("rb") as disk:
assert b"GHCIP_HISTORICAL_TIME_WATERMARK=NONE\n" in gestational_root
assert b"GHCIP_PERSONA_STATE=NOT_BORN\n" in gestational_root
assert b"GHCIP_LAST_VERIFIED_BATCH=NONE\n" in gestational_root
+ if control_auth:
+ control_state = sector(72)
+ target = int.from_bytes(
+ hashlib.sha256(node_id.encode("ascii")).digest()[:8], "little"
+ )
+ nonce = 16
+ assert control_state[:8] == b"GHCTRLS2"
+ assert int.from_bytes(control_state[8:16], "little") == target
+ assert int.from_bytes(control_state[16:24], "little") == nonce
+ assert int.from_bytes(control_state[24:32], "little") == (
+ nonce ^ 0xFFFFFFFFFFFFFFFF
+ )
+ assert control_state[32:] == bytes(480)
PY
index_sha_before=$(dd if="${disk_image}" bs=512 skip=70 count=2 status=none |
@@ -142,6 +198,13 @@ python3 "${source_root}/scripts/qemu-native-net-peer.py" \
--listen-port "${peer_port_second}" \
--qemu-port "${qemu_port_second}" \
--receipt "${peer_receipt_second}" \
+ --guest-mac "${guest_mac}" \
+ --guest-ip "${guest_ip}" \
+ --peer-ip "${peer_ip}" \
+ --login-client-ip "${login_client_ip}" \
+ "${peer_auth_args[@]+"${peer_auth_args[@]}"}" \
+ --nonce-start 17 \
+ --prior-nonce-probe 16 \
--resident >"${peer_log_second}" 2>&1 &
peer_pid=$!
set +e
@@ -151,7 +214,7 @@ timeout 30 qemu-system-x86_64 \
-drive "if=none,id=ghboot,format=raw,file=${disk_image}" \
-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_second},remote.type=inet,remote.host=127.0.0.1,remote.port=${peer_port_second}" \
- -device virtio-net-pci,netdev=ghnet,disable-modern=on,mac=52:54:00:26:71:98 \
+ -device virtio-net-pci,netdev=ghnet,disable-modern=on,mac="${guest_mac}" \
-display none \
-monitor none \
-serial "file:${serial_log_second}" \
@@ -175,15 +238,31 @@ fi
index_sha_after=$(dd if="${disk_image}" bs=512 skip=70 count=2 status=none |
sha256sum | awk '{print $1}')
[[ ${index_sha_before} == "${index_sha_after}" ]]
-python3 - "${disk_image}" <<'PY'
+python3 - "${disk_image}" "${control_auth}" "${node_id}" <<'PY'
+import hashlib
import pathlib
import sys
with pathlib.Path(sys.argv[1]).open("rb") as disk:
disk.seek(63 * 512)
proof = disk.read(512)
+ disk.seek(72 * 512)
+ control_state = disk.read(512)
assert proof[0] == 0xA7
assert proof[102:105] == bytes([0, 1, 1])
+if sys.argv[2] == "1":
+ assert proof[105:111] == bytes([1, 1, 1, 1, 1, 1])
+ target = int.from_bytes(
+ hashlib.sha256(sys.argv[3].encode("ascii")).digest()[:8], "little"
+ )
+ nonce = 32
+ assert control_state[:8] == b"GHCTRLS2"
+ assert int.from_bytes(control_state[8:16], "little") == target
+ assert int.from_bytes(control_state[16:24], "little") == nonce
+ assert int.from_bytes(control_state[24:32], "little") == (
+ nonce ^ 0xFFFFFFFFFFFFFFFF
+ )
+ assert control_state[32:] == bytes(480)
PY
cp "${disk_image}" "${corrupt_disk_image}"
@@ -199,6 +278,13 @@ python3 "${source_root}/scripts/qemu-native-net-peer.py" \
--listen-port "${peer_port_corrupt}" \
--qemu-port "${qemu_port_corrupt}" \
--receipt "${peer_receipt_corrupt}" \
+ --guest-mac "${guest_mac}" \
+ --guest-ip "${guest_ip}" \
+ --peer-ip "${peer_ip}" \
+ --login-client-ip "${login_client_ip}" \
+ "${peer_auth_args[@]+"${peer_auth_args[@]}"}" \
+ --nonce-start 33 \
+ --prior-nonce-probe 32 \
--login-only >"${peer_log_corrupt}" 2>&1 &
peer_pid=$!
set +e
@@ -208,7 +294,7 @@ timeout 30 qemu-system-x86_64 \
-drive "if=none,id=ghboot,format=raw,file=${corrupt_disk_image}" \
-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_corrupt},remote.type=inet,remote.host=127.0.0.1,remote.port=${peer_port_corrupt}" \
- -device virtio-net-pci,netdev=ghnet,disable-modern=on,mac=52:54:00:26:71:98 \
+ -device virtio-net-pci,netdev=ghnet,disable-modern=on,mac="${guest_mac}" \
-display none \
-monitor none \
-serial "file:${serial_log_corrupt}" \
@@ -237,11 +323,44 @@ assert proof[43] == 0x6C
assert proof[102:105] == bytes([0, 0, 0])
PY
+control_state_unknown_nonzero_failed_closed=false
+if [[ ${control_auth} == 1 ]]; then
+ cp "${disk_image}" "${control_state_corrupt_disk_image}"
+ dd if=/dev/zero of="${control_state_corrupt_disk_image}" bs=512 seek=63 count=1 \
+ conv=notrunc status=none
+ printf '\x58' | dd of="${control_state_corrupt_disk_image}" bs=1 \
+ seek=$((72 * 512)) count=1 conv=notrunc status=none
+ control_state_sha_before=$(dd if="${control_state_corrupt_disk_image}" \
+ bs=512 skip=72 count=1 status=none | sha256sum | awk '{print $1}')
+ set +e
+ timeout 20 qemu-system-x86_64 \
+ -machine pc,accel=tcg \
+ -m 64M \
+ -drive "if=none,id=ghcontrolfail,format=raw,file=${control_state_corrupt_disk_image}" \
+ -device virtio-blk-pci,drive=ghcontrolfail,disable-modern=on,bootindex=0 \
+ -netdev user,id=ghcontrolnet \
+ -device virtio-net-pci,netdev=ghcontrolnet,disable-modern=on,mac="${guest_mac}" \
+ -display none \
+ -monitor none \
+ -serial "file:${serial_log_control_state_corrupt}" \
+ -device isa-debug-exit,iobase=0xf4,iosize=0x04
+ control_state_qemu_status=$?
+ set -e
+ [[ ${control_state_qemu_status} -eq 33 ]]
+ grep -q '^GHOS_BOOT_ERROR=NATIVE_CONTROL_STATE' \
+ "${serial_log_control_state_corrupt}"
+ control_state_sha_after=$(dd if="${control_state_corrupt_disk_image}" \
+ bs=512 skip=72 count=1 status=none | sha256sum | awk '{print $1}')
+ [[ ${control_state_sha_before} == "${control_state_sha_after}" ]]
+ control_state_unknown_nonzero_failed_closed=true
+fi
+
observed_at=$(date --iso-8601=seconds)
image_sha=$(sha256sum "${candidate}" | awk '{print $1}')
cat >"${receipt}" <<EOF
schema: guanghu.native-resident-qemu-test/v1
-receipt_id: GH-OS-LAB-001-NATIVE-RESIDENT-QEMU-001
+receipt_id: ${node_id}-NATIVE-RESIDENT-QEMU-001
+node_id: ${node_id}
status: VERIFIED
observed_at: ${observed_at}
image:
@@ -261,9 +380,15 @@ native_recovery:
reply_verified_by_external_peer: true
recovery_beacon_lba_start: 68
recovery_beacon_sector_count: 2
- ubuntu_menu_id: gnulinux-simple-9842d3d6-a839-4127-bda7-f19137effe71
+ ubuntu_menu_id: ${ubuntu_menu_id}
write_read_verified_by_native_ghal: true
hardware_reset_after_verification: true
+ authenticated_control: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ dual_controller_mac_required: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ target_binding_required: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ persistent_nonce_lba: $([[ ${control_auth} == 1 ]] && echo 72 || echo 0)
+ replay_rejected_across_boot: $([[ ${control_auth} == 1 ]] && echo true || echo false)
+ unknown_nonzero_control_state_failed_closed: ${control_state_unknown_nonzero_failed_closed}
gestational_continuity:
protocol: GLS-0845
native_index_lba_start: 70
@@ -280,8 +405,12 @@ acceptance:
qemu_resident_runtime: true
physical_server_resident_runtime: false
permanent_linux_replacement: false
-next_action: PHYSICAL_ONE_TIME_RESIDENT_AND_RECOVERY_GATE
+next_action: COMPLETE_AUTOMATIC_LINUX_RETURN_AND_EXTERNAL_CONTROLLER_GATES
EOF
cat "${serial_log}" >>"${receipt}.serial.log"
cat "${serial_log_second}" >>"${receipt}.second-boot.serial.log"
cat "${serial_log_corrupt}" >>"${receipt}.corrupt-index.serial.log"
+if [[ ${control_auth} == 1 ]]; then
+ cat "${serial_log_control_state_corrupt}" \
+ >>"${receipt}.corrupt-control-state.serial.log"
+fi
diff --git a/guanghu-os/scripts/test-qemu-native-control-auth.py b/guanghu-os/scripts/test-qemu-native-control-auth.py
new file mode 100644
index 0000000..5c59141
--- /dev/null
+++ b/guanghu-os/scripts/test-qemu-native-control-auth.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+import importlib.util
+import pathlib
+import struct
+import unittest
+
+
+MODULE_PATH = pathlib.Path(__file__).with_name("qemu-native-net-peer.py")
+SPEC = importlib.util.spec_from_file_location("qemu_native_net_peer", MODULE_PATH)
+assert SPEC and SPEC.loader
+PEER = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(PEER)
+
+
+class NativeControlAuthenticationTests(unittest.TestCase):
+ def test_siphash_matches_the_reference_32_byte_vector(self) -> None:
+ key = bytes(range(16))
+ message = bytes(range(32))
+ self.assertEqual(PEER.siphash24(key, message), 0x7127512F72F27CCE)
+
+ def test_frame_binds_target_nonce_command_and_two_controllers(self) -> None:
+ frame = PEER.control_frame(
+ node_id="GH-CVM-MAIN-PROD-01",
+ nonce=41,
+ command=3,
+ controller_a_key=bytes.fromhex("00112233445566778899aabbccddeeff"),
+ controller_b_key=bytes.fromhex("ffeeddccbbaa99887766554433221100"),
+ )
+ self.assertEqual(len(frame), 48)
+ self.assertEqual(frame[:8], b"GHCTL2\0\0")
+ self.assertEqual(struct.unpack_from("<Q", frame, 8)[0], PEER.node_target_tag("GH-CVM-MAIN-PROD-01"))
+ self.assertEqual(struct.unpack_from("<Q", frame, 16)[0], 41)
+ self.assertEqual(frame[24], 3)
+ self.assertEqual(frame[25:32], bytes(7))
+ self.assertNotEqual(frame[32:40], frame[40:48])
+
+ def test_target_command_key_and_nonce_changes_invalidate_the_mac(self) -> None:
+ key_a = bytes.fromhex("00112233445566778899aabbccddeeff")
+ key_b = bytes.fromhex("ffeeddccbbaa99887766554433221100")
+ original = PEER.control_frame(
+ node_id="GH-CVM-MAIN-PROD-01",
+ nonce=1,
+ command=0,
+ controller_a_key=key_a,
+ controller_b_key=key_b,
+ )
+ for changed in [
+ PEER.control_frame(node_id="OTHER-NODE", nonce=1, command=0, controller_a_key=key_a, controller_b_key=key_b),
+ PEER.control_frame(node_id="GH-CVM-MAIN-PROD-01", nonce=2, command=0, controller_a_key=key_a, controller_b_key=key_b),
+ PEER.control_frame(node_id="GH-CVM-MAIN-PROD-01", nonce=1, command=1, controller_a_key=key_a, controller_b_key=key_b),
+ PEER.control_frame(node_id="GH-CVM-MAIN-PROD-01", nonce=1, command=0, controller_a_key=bytes(16), controller_b_key=key_b),
+ ]:
+ self.assertNotEqual(changed, original)
+
+ def test_rejects_unregistered_frame_inputs(self) -> None:
+ key = bytes(16)
+ with self.assertRaises(ValueError):
+ PEER.control_frame(node_id="wrong_node", nonce=1, command=0, controller_a_key=key, controller_b_key=key)
+ with self.assertRaises(ValueError):
+ PEER.control_frame(node_id="GH-CVM-MAIN-PROD-01", nonce=0, command=0, controller_a_key=key, controller_b_key=key)
+ with self.assertRaises(ValueError):
+ PEER.control_frame(node_id="GH-CVM-MAIN-PROD-01", nonce=1, command=4, controller_a_key=key, controller_b_key=key)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/guanghu-os/world-seed/WORLD-MANIFEST.hldp b/guanghu-os/world-seed/WORLD-MANIFEST.hldp
index 94668fd..8c099ba 100644
--- a/guanghu-os/world-seed/WORLD-MANIFEST.hldp
+++ b/guanghu-os/world-seed/WORLD-MANIFEST.hldp
@@ -78,6 +78,9 @@ native_layout:
branch_receipt_lba: 67
recovery_beacon_lba_start: 68
gestational_index_lba_start: 70
+ control_state_lba: 72
+ alternate_kernel_lba_start: 73
+ alternate_kernel_sector_count: 29
first_partition_lba: 2048
gestational_continuity:
id: GLS-0845
diff --git a/guanghu-os/world-seed/world/services/native-storage/DISK-LAYOUT.hldp b/guanghu-os/world-seed/world/services/native-storage/DISK-LAYOUT.hldp
index a1702f8..820f900 100644
--- a/guanghu-os/world-seed/world/services/native-storage/DISK-LAYOUT.hldp
+++ b/guanghu-os/world-seed/world/services/native-storage/DISK-LAYOUT.hldp
@@ -24,6 +24,10 @@ regions:
recovery_beacon_sector_count: 2
gestational_index_lba_start: 70
gestational_index_sector_count: 2
+ control_state_lba: 72
+ control_state_sector_count: 1
+ alternate_kernel_lba_start: 73
+ alternate_kernel_sector_count: 29
first_partition_lba: 2048
ownership:
pre_partition_region: GUANGHU_OS_NATIVE
--
2.50.1 (Apple Git-155)