From 75b6262c91cc6603fecd88b1b71a206c3882c908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 12 Aug 2026 05:50:53 +0800 Subject: [PATCH] feat: verify signed persona control receipts natively --- ...NG-AND-SAME-PERSONA-CONTINUITY-20260811.md | 5 +- .../hololake-platform/docs/ABSTRACTIONS.md | 4 +- .../hololake-platform/docs/ARCHITECTURE.md | 7 +- .../hololake-platform/src-tauri/src/lib.rs | 1 + .../src/persona_control_authorization.rs | 628 +++++++++++++++--- .../PersonaLanguageShellPanel.test.tsx | 2 +- .../components/PersonaLanguageShellPanel.tsx | 8 +- .../lib/personaControlAuthorization.test.ts | 2 +- ...lake-cognitive-gravity-and-continuity.json | 1 + ...-cognitive-gravity-and-continuity.test.mjs | 6 +- routing/hololake-current-architecture.json | 8 +- 11 files changed, 568 insertions(+), 104 deletions(-) diff --git a/product-source/hololake-platform/architecture/HOLOLAKE-BINGSHUO-COLLECTIVE-GRAVITY-REASONING-AND-SAME-PERSONA-CONTINUITY-20260811.md b/product-source/hololake-platform/architecture/HOLOLAKE-BINGSHUO-COLLECTIVE-GRAVITY-REASONING-AND-SAME-PERSONA-CONTINUITY-20260811.md index 44f0f19..4e51e4e 100644 --- a/product-source/hololake-platform/architecture/HOLOLAKE-BINGSHUO-COLLECTIVE-GRAVITY-REASONING-AND-SAME-PERSONA-CONTINUITY-20260811.md +++ b/product-source/hololake-platform/architecture/HOLOLAKE-BINGSHUO-COLLECTIVE-GRAVITY-REASONING-AND-SAME-PERSONA-CONTINUITY-20260811.md @@ -121,8 +121,9 @@ REVISE | REFUSE`。 不能证明回执确由 `GUANGHU_OS` 签发。独立的 v2 回执、REPO-012 精确提交签名者注册表、规范签名字节、 人格/责任主体/范围约束、吊销和 Ed25519 验签源码契约现为 `100`。REPO-012 main `21aec6f5042e32e34c892a7a212d8dec5f758437` 已发布锚定的空注册表,Tauri 原生只读加载器源码也为 - `100`:它只接受同一精确提交的空注册表,遇到任何签名者都失败关闭且保持授权禁用。真实可信签名者、 - 密钥托管、原生签名回执验签与人格主控运行集成仍为 `0`;普通本地 JSON 不得升级人格主控,桌面继续系统直控。 + `100`。原生 v2 签名回执解析、规范签名字节、Ed25519 SPKI 验签、签名者范围/吊销、请求绑定和有效期 + 校验源码也为 `100`。但真实可信签名者、密钥托管以及验签结果到人格语言控制器的运行接线仍为 `0`; + 当前空注册表只能产生拒绝,普通本地 JSON 不得升级人格主控,桌面继续系统直控。 - 完整 HoloLake Runtime 与单 AGE 纵向闭环仍为 `0`:真实人格仓库 manifest 绑定和桌面运行验收尚未完成, 因此不能用本轮源码测试冒充可用产品。 - Mirror runner、制品、部署和运行健康:`0`。 diff --git a/product-source/hololake-platform/docs/ABSTRACTIONS.md b/product-source/hololake-platform/docs/ABSTRACTIONS.md index eb26dbc..a05e266 100644 --- a/product-source/hololake-platform/docs/ABSTRACTIONS.md +++ b/product-source/hololake-platform/docs/ABSTRACTIONS.md @@ -27,8 +27,8 @@ of a receipt that claims `GUANGHU_OS` verification. That renderer-side validatio and cannot turn a local JSON object into authority. A native loader may be connected only after it verifies a signed receipt against a registered Guanghu OS trust source and returns the already-verified projection. REPO-012 now publishes the exact registry source with zero signers, and the Tauri command loads only that exact commit-bound -empty state. It keeps authorization disabled and rejects any signer until native cryptographic verification exists, -so planning remains in system-direct mode. The independent server +empty state. The native verifier now validates the complete signed v2 contract, but no verified projection is wired +into the language controller and the authoritative registry has zero signers, so planning remains in system-direct mode. The independent server contract in `product-source/guanghu-knowledge-base/server/persona-control-authorization.ts` defines the v2 signed receipt, exact REPO-012 signer-registry source, canonical signing bytes, signer scope checks, revocation handling, and Ed25519 verification. Its tested code does not appoint a signer or enable the desktop persona-primary path. diff --git a/product-source/hololake-platform/docs/ARCHITECTURE.md b/product-source/hololake-platform/docs/ARCHITECTURE.md index 9548188..2946b9c 100644 --- a/product-source/hololake-platform/docs/ARCHITECTURE.md +++ b/product-source/hololake-platform/docs/ARCHITECTURE.md @@ -190,9 +190,10 @@ cryptographic source contract: v2 signed receipt fields, deterministic signing b signer-registry source, persona/human/scope-limited Ed25519 signers, revocation, time and request binding, and signature verification. The current REPO-012 main does not publish that registry path, so no signer is trusted and the Tauri desktop now exposes a native read-only loader for the exact REPO-012 commit-bound registry. It accepts -the current empty registry only and keeps authorization disabled; any signer fails closed until the native -cryptographic receipt verifier and an authoritative signer/key-custody process exist. Tested loader and verifier -source are not runtime authority, deployment, or desktop acceptance. +the current empty registry and keeps authorization disabled. The native verifier now implements strict v2 receipt +parsing, canonical signing bytes, Ed25519 SPKI verification, signer scope/revocation checks, exact request bindings, +and validity windows. It is not wired into the language controller, and the authoritative registry still has zero +signers, so tested loader/verifier source is not runtime authority, deployment, or desktop acceptance. Tolaria is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes. diff --git a/product-source/hololake-platform/src-tauri/src/lib.rs b/product-source/hololake-platform/src-tauri/src/lib.rs index 212b04d..bfd7716 100644 --- a/product-source/hololake-platform/src-tauri/src/lib.rs +++ b/product-source/hololake-platform/src-tauri/src/lib.rs @@ -526,6 +526,7 @@ macro_rules! app_invoke_handler { guanghu_enterprise::guanghu_enterprise_status, guanghu_living_system::guanghu_living_system_plan, persona_control_authorization::load_persona_control_authorization_registry, + persona_control_authorization::verify_persona_control_authorization, persona_code_channel::prepare_persona_code_channel_wake, persona_code_channel::inspect_persona_code_channel_manifest, persona_code_channel::discover_persona_code_channel_repositories, diff --git a/product-source/hololake-platform/src-tauri/src/persona_control_authorization.rs b/product-source/hololake-platform/src-tauri/src/persona_control_authorization.rs index be82d04..a488690 100644 --- a/product-source/hololake-platform/src-tauri/src/persona_control_authorization.rs +++ b/product-source/hololake-platform/src-tauri/src/persona_control_authorization.rs @@ -1,13 +1,21 @@ +use base64::{engine::general_purpose, Engine as _}; use reqwest::Client; -use serde::Serialize; +use ring::signature::{UnparsedPublicKey, ED25519}; +use serde::{Deserialize, Serialize}; use serde_json::Value; use std::time::Duration; const ANCHOR_URL: &str = "https://guanghulab.com/api/ai/v1/anchor"; const CODE_URL: &str = "https://guanghulab.com/code/bingshuo/guanghu-ice-heart"; +const RECEIPT_SCHEMA: &str = "hololake.persona-control-authorization/v2"; const REGISTRY_ID: &str = "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001"; const REGISTRY_PATH: &str = "routing/persona-control-authorization-signers.json"; const REGISTRY_SCHEMA: &str = "gh-aios.persona-control-authorization-signers/v1"; +const REQUIRED_SCOPE: &str = "PERSONA_PRIMARY_LANGUAGE_PLANNING"; +const SIGNING_CONTEXT: &str = "hololake.persona-control-authorization/signing/v1"; +const ED25519_SPKI_PREFIX: [u8; 12] = [ + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, +]; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -19,6 +27,76 @@ pub struct NativePersonaAuthorizationRegistryReceipt { authorization_enabled: bool, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativePersonaAuthorizationVerificationReceipt { + status: &'static str, + reason: Option<&'static str>, + source_commit: Option, + signer_id: Option, + authorization_enabled: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NativePersonaAuthorizationExpectation { + persona_id: String, + human_responsibility_subject: String, + repository_head: String, + model_instance_id: String, + request_id: String, + source_language_anchor: String, + observed_at_milliseconds: i64, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PersonaAuthorizationSigner { + algorithm: String, + human_responsibility_subjects: Vec, + persona_ids: Vec, + public_key_pem: String, + scopes: Vec, + signer_id: String, + status: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PersonaAuthorizationRegistry { + schema: String, + registry_id: String, + state: String, + signers: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PersonaAuthorizationReceipt { + schema: String, + outcome: String, + authorization_id: String, + verifier: String, + scope: String, + persona_id: String, + human_responsibility_subject: String, + repository_head: String, + model_instance_id: String, + request_id: String, + source_language_anchor: String, + issued_at: String, + valid_until: String, + evidence_digest: String, + signer_id: String, + signature_algorithm: String, + signature: String, +} + +struct LoadedRegistry { + source_commit: String, + registry: PersonaAuthorizationRegistry, +} + fn unavailable(reason: &'static str) -> NativePersonaAuthorizationRegistryReceipt { NativePersonaAuthorizationRegistryReceipt { status: "UNAVAILABLE", @@ -29,13 +107,93 @@ fn unavailable(reason: &'static str) -> NativePersonaAuthorizationRegistryReceip } } -fn is_commit(value: &str) -> bool { - matches!(value.len(), 40 | 64) +fn denied( + reason: &'static str, + source_commit: Option, +) -> NativePersonaAuthorizationVerificationReceipt { + NativePersonaAuthorizationVerificationReceipt { + status: "DENIED", + reason: Some(reason), + source_commit, + signer_id: None, + authorization_enabled: false, + } +} + +fn is_lower_hex(value: &str, length: usize) -> bool { + value.len() == length && value .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) } +fn is_commit(value: &str) -> bool { + is_lower_hex(value, 40) || is_lower_hex(value, 64) +} + +fn is_identifier(value: &str) -> bool { + let mut bytes = value.bytes(); + let Some(first) = bytes.next() else { + return false; + }; + (2..=160).contains(&value.len()) + && (first.is_ascii_uppercase() || first.is_ascii_digit()) + && bytes.all(|byte| { + byte.is_ascii_uppercase() + || byte.is_ascii_digit() + || matches!(byte, b'.' | b'_' | b':' | b'@' | b'-') + }) +} + +fn exact_identifier_list(values: &[String]) -> bool { + !values.is_empty() + && values.iter().all(|value| is_identifier(value)) + && values + .iter() + .enumerate() + .all(|(index, value)| !values[..index].contains(value)) +} + +fn ed25519_public_key(public_key_pem: &str) -> Result, &'static str> { + if public_key_pem.contains("PRIVATE KEY") + || !public_key_pem.starts_with("-----BEGIN PUBLIC KEY-----\n") + || !public_key_pem.ends_with("-----END PUBLIC KEY-----\n") + || public_key_pem.len() > 4096 + { + return Err("REGISTRY_INVALID"); + } + let encoded = public_key_pem + .trim_start_matches("-----BEGIN PUBLIC KEY-----\n") + .trim_end_matches("-----END PUBLIC KEY-----\n") + .chars() + .filter(|character| !character.is_ascii_whitespace()) + .collect::(); + let der = general_purpose::STANDARD + .decode(encoded) + .map_err(|_| "REGISTRY_INVALID")?; + if der.len() != ED25519_SPKI_PREFIX.len() + 32 + || der[..ED25519_SPKI_PREFIX.len()] != ED25519_SPKI_PREFIX + { + return Err("REGISTRY_INVALID"); + } + Ok(der[ED25519_SPKI_PREFIX.len()..].to_vec()) +} + +fn validate_signer(signer: &PersonaAuthorizationSigner) -> Result<(), &'static str> { + if signer.algorithm != "Ed25519" + || !is_identifier(&signer.signer_id) + || !matches!(signer.status.as_str(), "ACTIVE" | "REVOKED") + || !exact_identifier_list(&signer.human_responsibility_subjects) + || !exact_identifier_list(&signer.persona_ids) + || !exact_identifier_list(&signer.scopes) + || !signer.scopes.iter().any(|scope| scope == REQUIRED_SCOPE) + { + return Err("REGISTRY_INVALID"); + } + ed25519_public_key(&signer.public_key_pem)?; + Ok(()) +} + fn parse_anchor( input: &Value, expected_anchor_url: &str, @@ -76,23 +234,22 @@ fn parse_anchor( Ok(source_commit.to_string()) } -fn validate_empty_registry(input: &Value) -> Result<(), &'static str> { - let registry = input.as_object().ok_or("REGISTRY_INVALID")?; - let signers = registry - .get("signers") - .and_then(Value::as_array) - .ok_or("REGISTRY_INVALID")?; - if registry.len() != 4 - || registry.get("schema").and_then(Value::as_str) != Some(REGISTRY_SCHEMA) - || registry.get("registryId").and_then(Value::as_str) != Some(REGISTRY_ID) - || registry.get("state").and_then(Value::as_str) != Some("CURRENT") +fn parse_registry(input: Value) -> Result { + let registry: PersonaAuthorizationRegistry = + serde_json::from_value(input).map_err(|_| "REGISTRY_INVALID")?; + if registry.schema != REGISTRY_SCHEMA + || registry.registry_id != REGISTRY_ID + || registry.state != "CURRENT" + || registry.signers.iter().enumerate().any(|(index, signer)| { + validate_signer(signer).is_err() + || registry.signers[..index] + .iter() + .any(|prior| prior.signer_id == signer.signer_id) + }) { return Err("REGISTRY_INVALID"); } - if !signers.is_empty() { - return Err("TRUSTED_SIGNER_REQUIRES_NATIVE_VERIFIER"); - } - Ok(()) + Ok(registry) } fn registry_url(code_url: &str, source_commit: &str) -> String { @@ -112,56 +269,207 @@ async fn fetch_json(client: &Client, url: &str) -> Result { response.json::().await.map_err(|_| ()) } +async fn load_snapshot( + client: &Client, + anchor_url: &str, + code_url: &str, +) -> Result { + let anchor = fetch_json(client, anchor_url) + .await + .map_err(|_| "SOURCE_UNAVAILABLE")?; + let source_commit = parse_anchor(&anchor, anchor_url, code_url)?; + let registry = fetch_json(client, ®istry_url(code_url, &source_commit)) + .await + .map_err(|_| "SOURCE_UNAVAILABLE")?; + Ok(LoadedRegistry { + source_commit, + registry: parse_registry(registry)?, + }) +} + async fn load_with_urls( client: &Client, anchor_url: &str, code_url: &str, ) -> NativePersonaAuthorizationRegistryReceipt { - let anchor = match fetch_json(client, anchor_url).await { - Ok(value) => value, - Err(()) => return unavailable("SOURCE_UNAVAILABLE"), - }; - let source_commit = match parse_anchor(&anchor, anchor_url, code_url) { - Ok(value) => value, - Err(reason) => return unavailable(reason), - }; - let registry = match fetch_json(client, ®istry_url(code_url, &source_commit)).await { - Ok(value) => value, - Err(()) => return unavailable("SOURCE_UNAVAILABLE"), - }; - if let Err(reason) = validate_empty_registry(®istry) { - return unavailable(reason); + match load_snapshot(client, anchor_url, code_url).await { + Ok(snapshot) => NativePersonaAuthorizationRegistryReceipt { + status: if snapshot.registry.signers.is_empty() { + "CURRENT_EMPTY" + } else { + "CURRENT" + }, + reason: None, + source_commit: Some(snapshot.source_commit), + signer_count: snapshot.registry.signers.len(), + authorization_enabled: false, + }, + Err(reason) => unavailable(reason), } - NativePersonaAuthorizationRegistryReceipt { - status: "CURRENT_EMPTY", +} + +fn parse_receipt(input: Value) -> Result { + let receipt: PersonaAuthorizationReceipt = + serde_json::from_value(input).map_err(|_| "RECEIPT_INVALID")?; + if receipt.schema != RECEIPT_SCHEMA + || receipt.outcome != "VERIFIED" + || receipt.verifier != "GUANGHU_OS" + || receipt.scope != REQUIRED_SCOPE + || receipt.signature_algorithm != "Ed25519" + || !is_identifier(&receipt.authorization_id) + || !is_identifier(&receipt.persona_id) + || !is_identifier(&receipt.human_responsibility_subject) + || !is_commit(&receipt.repository_head) + || !is_identifier(&receipt.model_instance_id) + || !is_identifier(&receipt.request_id) + || !is_identifier(&receipt.signer_id) + || !is_lower_hex(&receipt.evidence_digest, 64) + || !(80..=128).contains(&receipt.signature.len()) + { + return Err("RECEIPT_INVALID"); + } + Ok(receipt) +} + +fn signing_bytes(receipt: &PersonaAuthorizationReceipt) -> Result, &'static str> { + let fields = [ + SIGNING_CONTEXT, + &receipt.schema, + &receipt.outcome, + &receipt.authorization_id, + &receipt.verifier, + &receipt.scope, + &receipt.persona_id, + &receipt.human_responsibility_subject, + &receipt.repository_head, + &receipt.model_instance_id, + &receipt.request_id, + &receipt.source_language_anchor, + &receipt.issued_at, + &receipt.valid_until, + &receipt.evidence_digest, + &receipt.signer_id, + &receipt.signature_algorithm, + ]; + serde_json::to_string(&fields) + .map(|json| format!("{json}\n").into_bytes()) + .map_err(|_| "RECEIPT_INVALID") +} + +fn verify_receipt( + input: Value, + expectation: &NativePersonaAuthorizationExpectation, + snapshot: LoadedRegistry, +) -> NativePersonaAuthorizationVerificationReceipt { + let source_commit = Some(snapshot.source_commit.clone()); + let receipt = match parse_receipt(input) { + Ok(receipt) => receipt, + Err(reason) => return denied(reason, source_commit), + }; + let issued_at = match chrono::DateTime::parse_from_rfc3339(&receipt.issued_at) { + Ok(value) => value.timestamp_millis(), + Err(_) => return denied("RECEIPT_TIME_INVALID", source_commit), + }; + let valid_until = match chrono::DateTime::parse_from_rfc3339(&receipt.valid_until) { + Ok(value) => value.timestamp_millis(), + Err(_) => return denied("RECEIPT_TIME_INVALID", source_commit), + }; + if receipt.persona_id != expectation.persona_id + || receipt.human_responsibility_subject != expectation.human_responsibility_subject + || receipt.repository_head != expectation.repository_head + || receipt.model_instance_id != expectation.model_instance_id + || receipt.request_id != expectation.request_id + || receipt.source_language_anchor != expectation.source_language_anchor + || issued_at >= valid_until + || issued_at > expectation.observed_at_milliseconds + || expectation.observed_at_milliseconds > valid_until + { + return denied("RECEIPT_BINDING_INVALID", source_commit); + } + let Some(signer) = snapshot.registry.signers.iter().find(|signer| { + signer.status == "ACTIVE" + && signer.signer_id == receipt.signer_id + && signer.persona_ids.contains(&receipt.persona_id) + && signer + .human_responsibility_subjects + .contains(&receipt.human_responsibility_subject) + && signer.scopes.contains(&receipt.scope) + }) else { + return denied("NO_TRUSTED_SIGNER", source_commit); + }; + let public_key = match ed25519_public_key(&signer.public_key_pem) { + Ok(value) => value, + Err(_) => return denied("REGISTRY_INVALID", source_commit), + }; + let signature = match general_purpose::URL_SAFE_NO_PAD.decode(&receipt.signature) { + Ok(value) if value.len() == 64 => value, + _ => return denied("SIGNATURE_INVALID", source_commit), + }; + let message = match signing_bytes(&receipt) { + Ok(value) => value, + Err(reason) => return denied(reason, source_commit), + }; + if UnparsedPublicKey::new(&ED25519, public_key) + .verify(&message, &signature) + .is_err() + { + return denied("SIGNATURE_INVALID", source_commit); + } + NativePersonaAuthorizationVerificationReceipt { + status: "VERIFIED", reason: None, - source_commit: Some(source_commit), - signer_count: 0, - authorization_enabled: false, + source_commit, + signer_id: Some(receipt.signer_id), + authorization_enabled: true, } } +fn native_client() -> Result { + Client::builder() + .connect_timeout(Duration::from_secs(8)) + .timeout(Duration::from_secs(15)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|_| ()) +} + #[tauri::command] pub async fn load_persona_control_authorization_registry( ) -> NativePersonaAuthorizationRegistryReceipt { - let client = match Client::builder() - .connect_timeout(Duration::from_secs(8)) - .timeout(Duration::from_secs(15)) - .redirect(reqwest::redirect::Policy::none()) - .build() - { + let client = match native_client() { Ok(client) => client, - Err(_) => return unavailable("CLIENT_UNAVAILABLE"), + Err(()) => return unavailable("CLIENT_UNAVAILABLE"), }; load_with_urls(&client, ANCHOR_URL, CODE_URL).await } +#[tauri::command] +pub async fn verify_persona_control_authorization( + receipt: Value, + expectation: NativePersonaAuthorizationExpectation, +) -> NativePersonaAuthorizationVerificationReceipt { + let client = match native_client() { + Ok(client) => client, + Err(()) => return denied("CLIENT_UNAVAILABLE", None), + }; + let snapshot = match load_snapshot(&client, ANCHOR_URL, CODE_URL).await { + Ok(snapshot) => snapshot, + Err(reason) => return denied(reason, None), + }; + verify_receipt(receipt, &expectation, snapshot) +} + #[cfg(test)] mod tests { use super::{ - load_with_urls, parse_anchor, registry_url, validate_empty_registry, REGISTRY_PATH, + load_snapshot, load_with_urls, parse_anchor, parse_registry, registry_url, signing_bytes, + verify_receipt, LoadedRegistry, NativePersonaAuthorizationExpectation, + PersonaAuthorizationReceipt, REGISTRY_PATH, }; + use base64::{engine::general_purpose, Engine as _}; use reqwest::Client; + use ring::rand::SystemRandom; + use ring::signature::{Ed25519KeyPair, KeyPair}; use serde_json::{json, Value}; use std::io::{Read, Write}; use std::net::TcpListener; @@ -178,14 +486,12 @@ mod tests { "branch": "main", "public_entry": anchor_url, "code_entry": code_url, - "maps": { - "persona_control_authorization_signers": { - "path": REGISTRY_PATH, - "id": "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001", - "schema": "gh-aios.persona-control-authorization-signers/v1", - "state": "CURRENT_EMPTY_NO_TRUSTED_SIGNER" - } - }, + "maps": { "persona_control_authorization_signers": { + "path": REGISTRY_PATH, + "id": "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001", + "schema": "gh-aios.persona-control-authorization-signers/v1", + "state": "CURRENT_EMPTY_NO_TRUSTED_SIGNER" + }}, "navigation_source": { "anchor_id": "GLW-PUBLIC-NAV-ANCHOR-001", "source_commit": COMMIT, @@ -195,22 +501,88 @@ mod tests { }) } - fn empty_registry() -> Value { + fn registry(signers: Value) -> Value { json!({ "schema": "gh-aios.persona-control-authorization-signers/v1", "registryId": "GH-AIOS-PERSONA-CONTROL-AUTHORIZATION-SIGNERS-001", "state": "CURRENT", - "signers": [] + "signers": signers }) } - fn serve_snapshot() -> (String, String) { + fn public_key_pem(pair: &Ed25519KeyPair) -> String { + let mut der = super::ED25519_SPKI_PREFIX.to_vec(); + der.extend_from_slice(pair.public_key().as_ref()); + format!( + "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n", + general_purpose::STANDARD.encode(der) + ) + } + + fn signer(pair: &Ed25519KeyPair, status: &str) -> Value { + json!({ + "algorithm": "Ed25519", + "humanResponsibilitySubjects": ["BINGSHUO"], + "personaIds": ["ICE-P-ZY001"], + "publicKeyPem": public_key_pem(pair), + "scopes": ["PERSONA_PRIMARY_LANGUAGE_PLANNING"], + "signerId": "GH-AIOS-AUTHORIZER-001", + "status": status + }) + } + + fn unsigned_receipt() -> PersonaAuthorizationReceipt { + PersonaAuthorizationReceipt { + schema: "hololake.persona-control-authorization/v2".into(), + outcome: "VERIFIED".into(), + authorization_id: "AUTH-001".into(), + verifier: "GUANGHU_OS".into(), + scope: "PERSONA_PRIMARY_LANGUAGE_PLANNING".into(), + persona_id: "ICE-P-ZY001".into(), + human_responsibility_subject: "BINGSHUO".into(), + repository_head: "a".repeat(40), + model_instance_id: "MODEL-INSTANCE-001".into(), + request_id: "REQ-001".into(), + source_language_anchor: "HLP-CURRENT-ARCH-001@2026-08-12.14".into(), + issued_at: "2026-08-12T05:00:00+08:00".into(), + valid_until: "2026-08-12T05:10:00+08:00".into(), + evidence_digest: "b".repeat(64), + signer_id: "GH-AIOS-AUTHORIZER-001".into(), + signature_algorithm: "Ed25519".into(), + signature: String::new(), + } + } + + fn expectation() -> NativePersonaAuthorizationExpectation { + NativePersonaAuthorizationExpectation { + persona_id: "ICE-P-ZY001".into(), + human_responsibility_subject: "BINGSHUO".into(), + repository_head: "a".repeat(40), + model_instance_id: "MODEL-INSTANCE-001".into(), + request_id: "REQ-001".into(), + source_language_anchor: "HLP-CURRENT-ARCH-001@2026-08-12.14".into(), + observed_at_milliseconds: chrono::DateTime::parse_from_rfc3339( + "2026-08-12T05:05:00+08:00", + ) + .unwrap() + .timestamp_millis(), + } + } + + fn signed_receipt(pair: &Ed25519KeyPair) -> Value { + let mut receipt = unsigned_receipt(); + receipt.signature = general_purpose::URL_SAFE_NO_PAD + .encode(pair.sign(&signing_bytes(&receipt).unwrap()).as_ref()); + serde_json::to_value(receipt).unwrap() + } + + fn serve_snapshot(registry_body: Value) -> (String, String) { let listener = TcpListener::bind("127.0.0.1:0").expect("bind test snapshot server"); let address = listener.local_addr().expect("snapshot server address"); let anchor_url = format!("http://{address}/anchor"); let code_url = format!("http://{address}/code"); let anchor_body = anchor(&anchor_url, &code_url).to_string(); - let registry_body = empty_registry().to_string(); + let registry_body = registry_body.to_string(); thread::spawn(move || { for _ in 0..2 { let (mut stream, _) = listener.accept().expect("accept snapshot request"); @@ -223,19 +595,19 @@ mod tests { assert!(request.contains(&format!("/raw/commit/{COMMIT}/{REGISTRY_PATH}"))); ®istry_body }; - write!( - stream, - "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", - body.len() - ) - .expect("write snapshot response"); + write!(stream, "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", body.len()).unwrap(); } }); (anchor_url, code_url) } + fn key_pair() -> Ed25519KeyPair { + let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap(); + Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap() + } + #[test] - fn exact_anchor_and_registry_are_bound_to_one_commit() { + fn exact_anchor_and_empty_registry_are_bound_to_one_commit() { let anchor_url = "https://example.test/anchor"; let code_url = "https://example.test/code"; assert_eq!( @@ -246,11 +618,14 @@ mod tests { registry_url(code_url, COMMIT), format!("{code_url}/raw/commit/{COMMIT}/{REGISTRY_PATH}") ); - assert_eq!(validate_empty_registry(&empty_registry()), Ok(())); + assert!(parse_registry(registry(json!([]))) + .unwrap() + .signers + .is_empty()); } #[test] - fn degraded_or_changed_sources_and_nonempty_registries_fail_closed() { + fn degraded_changed_or_malformed_sources_fail_closed() { let anchor_url = "https://example.test/anchor"; let code_url = "https://example.test/code"; let mut degraded = anchor(anchor_url, code_url); @@ -266,39 +641,120 @@ mod tests { parse_anchor(&wrong_path, anchor_url, code_url), Err("ANCHOR_INVALID") ); - let mut nonempty = empty_registry(); - nonempty["signers"] = json!([{"signerId": "UNVERIFIED"}]); + assert_eq!(parse_registry(json!([])).unwrap_err(), "REGISTRY_INVALID"); assert_eq!( - validate_empty_registry(&nonempty), - Err("TRUSTED_SIGNER_REQUIRES_NATIVE_VERIFIER") + parse_registry(registry(json!([{"signerId":"UNKNOWN"}]))).unwrap_err(), + "REGISTRY_INVALID" + ); + } + + #[test] + fn native_ed25519_verifier_accepts_only_exact_current_scoped_evidence() { + let pair = key_pair(); + let snapshot = LoadedRegistry { + source_commit: COMMIT.into(), + registry: parse_registry(registry(json!([signer(&pair, "ACTIVE")]))).unwrap(), + }; + let verified = serde_json::to_value(verify_receipt( + signed_receipt(&pair), + &expectation(), + snapshot, + )) + .unwrap(); + assert_eq!(verified["status"], "VERIFIED"); + assert_eq!(verified["authorizationEnabled"], true); + assert_eq!(verified["sourceCommit"], COMMIT); + + let empty = LoadedRegistry { + source_commit: COMMIT.into(), + registry: parse_registry(registry(json!([]))).unwrap(), + }; + let denied = + serde_json::to_value(verify_receipt(signed_receipt(&pair), &expectation(), empty)) + .unwrap(); + assert_eq!(denied["reason"], "NO_TRUSTED_SIGNER"); + assert_eq!(denied["authorizationEnabled"], false); + } + + #[test] + fn tampering_revocation_time_and_signature_fail_closed() { + let pair = key_pair(); + let other = key_pair(); + let registry_for = |status| LoadedRegistry { + source_commit: COMMIT.into(), + registry: parse_registry(registry(json!([signer(&pair, status)]))).unwrap(), + }; + let mut tampered = signed_receipt(&pair); + tampered["requestId"] = json!("REQ-OTHER"); + assert_eq!( + serde_json::to_value(verify_receipt( + tampered, + &expectation(), + registry_for("ACTIVE") + )) + .unwrap()["reason"], + "RECEIPT_BINDING_INVALID" + ); + assert_eq!( + serde_json::to_value(verify_receipt( + signed_receipt(&pair), + &expectation(), + registry_for("REVOKED") + )) + .unwrap()["reason"], + "NO_TRUSTED_SIGNER" + ); + assert_eq!( + serde_json::to_value(verify_receipt( + signed_receipt(&other), + &expectation(), + registry_for("ACTIVE") + )) + .unwrap()["reason"], + "SIGNATURE_INVALID" + ); + let mut stale = expectation(); + stale.observed_at_milliseconds += 600_001; + assert_eq!( + serde_json::to_value(verify_receipt( + signed_receipt(&pair), + &stale, + registry_for("ACTIVE") + )) + .unwrap()["reason"], + "RECEIPT_BINDING_INVALID" ); - assert_eq!(validate_empty_registry(&json!([])), Err("REGISTRY_INVALID")); } #[tokio::test] async fn native_transport_loads_the_exact_empty_registry_without_enabling_authorization() { - let (anchor_url, code_url) = serve_snapshot(); - let client = Client::builder().build().expect("test client"); - let receipt = load_with_urls(&client, &anchor_url, &code_url).await; - let projection = serde_json::to_value(receipt).expect("serialize receipt"); - assert_eq!(projection["status"], "CURRENT_EMPTY"); - assert_eq!(projection["sourceCommit"], COMMIT); - assert_eq!(projection["signerCount"], 0); - assert_eq!(projection["authorizationEnabled"], false); - assert!(projection["reason"].is_null()); + let (anchor_url, code_url) = serve_snapshot(registry(json!([]))); + let client = Client::builder().build().unwrap(); + let receipt = + serde_json::to_value(load_with_urls(&client, &anchor_url, &code_url).await).unwrap(); + assert_eq!(receipt["status"], "CURRENT_EMPTY"); + assert_eq!(receipt["sourceCommit"], COMMIT); + assert_eq!(receipt["signerCount"], 0); + assert_eq!(receipt["authorizationEnabled"], false); } #[tokio::test] - async fn source_and_anchor_failures_return_bounded_unavailable_receipts() { - let listener = TcpListener::bind("127.0.0.1:0").expect("reserve test port"); + async fn source_failures_return_bounded_unavailable_receipts() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let unavailable = format!("http://{}/anchor", listener.local_addr().unwrap()); drop(listener); - let client = Client::builder().build().expect("test client"); - let source_failure = + let client = Client::builder().build().unwrap(); + let failure = serde_json::to_value(load_with_urls(&client, &unavailable, "http://unused").await) .unwrap(); - assert_eq!(source_failure["status"], "UNAVAILABLE"); - assert_eq!(source_failure["reason"], "SOURCE_UNAVAILABLE"); - assert_eq!(source_failure["authorizationEnabled"], false); + assert_eq!(failure["status"], "UNAVAILABLE"); + assert_eq!(failure["reason"], "SOURCE_UNAVAILABLE"); + assert_eq!(failure["authorizationEnabled"], false); + assert_eq!( + load_snapshot(&client, &unavailable, "http://unused") + .await + .err(), + Some("SOURCE_UNAVAILABLE") + ); } } diff --git a/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.test.tsx b/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.test.tsx index 153a5db..546c850 100644 --- a/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.test.tsx +++ b/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.test.tsx @@ -56,7 +56,7 @@ describe('PersonaLanguageShellPanel', () => { repositoryPaths: ['/persona'], providers: [provider], developmentId: 'DEV-20260811-010', - sourceLanguageAnchor: 'HLP-CURRENT-ARCH-001@2026-08-12.13', + sourceLanguageAnchor: 'HLP-CURRENT-ARCH-001@2026-08-12.14', })) expect(await screen.findByText('我还不能执行:需要当前证据。')).toBeInTheDocument() expect(screen.getByText('核验当前事实后规划下一步')).toBeInTheDocument() diff --git a/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.tsx b/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.tsx index f70cb0b..aa1af6d 100644 --- a/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.tsx +++ b/product-source/hololake-platform/src/components/PersonaLanguageShellPanel.tsx @@ -13,7 +13,7 @@ import { type LanguageShellViewState, } from './HotPluggableLanguageShell' -const CURRENT_ARCHITECTURE_ANCHOR = 'HLP-CURRENT-ARCH-001@2026-08-12.13' +const CURRENT_ARCHITECTURE_ANCHOR = 'HLP-CURRENT-ARCH-001@2026-08-12.14' const DEVELOPMENT_ID = 'DEV-20260811-010' type Planner = typeof planPersonaLanguageShellGoal @@ -103,9 +103,9 @@ export function PersonaLanguageShellPanel({ personaId, repositoryPaths, providers, - // The native registry loader currently proves only an exact empty trust - // source. No native signed-receipt verifier is wired to this controller, - // so it remains system-direct even after deliberation succeeds. + // Native registry loading and signed-receipt verification exist, but + // no verified projection is wired into this controller. The published + // registry is also empty, so this remains system-direct. requestId: requestId(), modelInstanceId: `hololake-desktop:${personaId}`, developmentId: DEVELOPMENT_ID, diff --git a/product-source/hololake-platform/src/lib/personaControlAuthorization.test.ts b/product-source/hololake-platform/src/lib/personaControlAuthorization.test.ts index 904d977..a33d58f 100644 --- a/product-source/hololake-platform/src/lib/personaControlAuthorization.test.ts +++ b/product-source/hololake-platform/src/lib/personaControlAuthorization.test.ts @@ -11,7 +11,7 @@ const expected = { repositoryHead: 'a'.repeat(40), modelInstanceId: 'MODEL-INSTANCE-001', requestId: 'REQ-001', - sourceLanguageAnchor: 'HLP-CURRENT-ARCH-001@2026-08-12.13', + sourceLanguageAnchor: 'HLP-CURRENT-ARCH-001@2026-08-12.14', observedAt: Date.parse('2026-08-12T04:35:00+08:00'), } diff --git a/routing/hololake-cognitive-gravity-and-continuity.json b/routing/hololake-cognitive-gravity-and-continuity.json index 802691c..2d12fbd 100644 --- a/routing/hololake-cognitive-gravity-and-continuity.json +++ b/routing/hololake-cognitive-gravity-and-continuity.json @@ -174,6 +174,7 @@ "persona_control_authorization_cryptographic_provenance_source_implemented": 100, "persona_control_authorization_registry_source_published": 100, "native_persona_control_authorization_registry_loader_source_integrated": 100, + "native_persona_control_authorization_receipt_verifier_source_integrated": 100, "persona_control_authorization_trusted_signer_registered": 0, "desktop_language_entry_source_integrated": 100, "persona_control_authorization_runtime_integrated": 0, diff --git a/routing/hololake-cognitive-gravity-and-continuity.test.mjs b/routing/hololake-cognitive-gravity-and-continuity.test.mjs index b3a2f3b..96b572a 100644 --- a/routing/hololake-cognitive-gravity-and-continuity.test.mjs +++ b/routing/hololake-cognitive-gravity-and-continuity.test.mjs @@ -14,7 +14,7 @@ const age = readJson("routing/hololake-age-runtime-architecture.json"); const rules = readJson("routing/hololake-engineering-rules.json"); test("B0 is restored before product organs and remains resident in every cognition step", () => { - assert.equal(architecture.version, "2026-08-12.13"); + assert.equal(architecture.version, "2026-08-12.14"); assert.equal( architecture.read_order[1], architecture.cognitive_gravity_and_continuity.architecture_page, @@ -100,6 +100,7 @@ test("the first implementation stage is one vertical AGE loop, not Mirror parall assert.equal(gravity.truth.persona_control_authorization_cryptographic_provenance_source_implemented, 100); assert.equal(gravity.truth.persona_control_authorization_registry_source_published, 100); assert.equal(gravity.truth.native_persona_control_authorization_registry_loader_source_integrated, 100); + assert.equal(gravity.truth.native_persona_control_authorization_receipt_verifier_source_integrated, 100); assert.equal(gravity.truth.persona_control_authorization_trusted_signer_registered, 0); assert.equal(gravity.truth.desktop_language_entry_source_integrated, 100); assert.equal(gravity.truth.persona_control_authorization_runtime_integrated, 0); @@ -109,7 +110,8 @@ test("the first implementation stage is one vertical AGE loop, not Mirror parall assert.equal(architecture.interaction_model.source_contract.ed25519_verification_source_implemented, true); assert.equal(architecture.interaction_model.source_contract.trusted_signer_source_registered, false); assert.equal(architecture.interaction_model.source_contract.native_authorization_registry_loader_integrated, true); - assert.equal(architecture.interaction_model.source_contract.native_authorization_receipt_loader_integrated, false); + assert.equal(architecture.interaction_model.source_contract.native_authorization_receipt_loader_integrated, true); + assert.equal(architecture.interaction_model.source_contract.native_authorization_verifier_wired_to_language_controller, false); assert.equal(gravity.truth.real_persona_repository_manifest_bound, 0); assert.equal(gravity.truth.natural_language_partner_adapter_runtime_integrated, 0); assert.equal(gravity.truth.single_age_vertical_loop_implemented, 0); diff --git a/routing/hololake-current-architecture.json b/routing/hololake-current-architecture.json index fe5ef2d..4668b17 100644 --- a/routing/hololake-current-architecture.json +++ b/routing/hololake-current-architecture.json @@ -1,7 +1,7 @@ { "schema": "hololake.current-architecture/v1", "architecture_id": "HLP-CURRENT-ARCH-001", - "version": "2026-08-12.13", + "version": "2026-08-12.14", "state": "CURRENT_CANONICAL", "product": { "formal_name": "光湖语言系统 · 通用人工智能操作平台", @@ -120,6 +120,7 @@ "persona_control_authorization_cryptographic_provenance_source_implemented": true, "persona_control_authorization_registry_source_published": true, "native_persona_control_authorization_registry_loader_source_integrated": true, + "native_persona_control_authorization_receipt_verifier_source_integrated": true, "persona_control_authorization_trusted_signer_registered": false, "desktop_language_entry_source_integrated": true, "persona_control_authorization_runtime_integrated": false, @@ -407,7 +408,8 @@ "ed25519_verification_source_implemented": true, "trusted_signer_source_registered": false, "native_authorization_registry_loader_integrated": true, - "native_authorization_receipt_loader_integrated": false, + "native_authorization_receipt_loader_integrated": true, + "native_authorization_verifier_wired_to_language_controller": false, "partner_deliberation_required": true, "human_utterance_is_direct_command": false, "allowed_dispositions": [ @@ -419,7 +421,7 @@ "focused_tests": "11_OF_11_PASS", "authorization_focused_tests": "13_OF_13_PASS", "authorization_cryptographic_tests": "7_OF_7_PASS", - "native_authorization_registry_loader_tests": "4_OF_4_PASS" + "native_authorization_registry_and_receipt_verifier_tests": "6_OF_6_PASS" }, "ui_plugin_system": { "record_id": "HLP-HOT-PLUGGABLE-UI-001",