6598 lines
253 KiB
Diff
6598 lines
253 KiB
Diff
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>(¤t_path)?;
|
||
+ let wake_path = resolve_world_path(world_root, &manifest.continuity.wake)?;
|
||
+ let wake = read_yaml::<WakeDocument>(&wake_path)?;
|
||
+ validate_target_identity(
|
||
+ &manifest,
|
||
+ ¤t,
|
||
+ &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 A:LBA 34–62;
|
||
+- Slot B:LBA 73–101;
|
||
+- 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/3,HTTP 200,HTTPS 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)
|
||
|