diff --git a/product-source/hololake-native-desktop/audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json b/product-source/hololake-native-desktop/audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json new file mode 100644 index 000000000..959e9f036 --- /dev/null +++ b/product-source/hololake-native-desktop/audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json @@ -0,0 +1,66 @@ +{ + "schema": "hololake.installed-numbered-root-acceptance/v1", + "record_id": "HLP-HOLOLAKE-0.5.0-NUMBERED-ROOT-INSTALLED-20260818", + "state": "LOCAL_DEVELOPER_ID_SIGNED_INSTALLED_ACCEPTANCE_PASS_PUBLIC_NOTARIZATION_PENDING", + "observed_at": "2026-08-18T15:18:00Z", + "source": { + "branch": "codex/hololake-clean-reassembly-20260818", + "source_commit": "5d01607459043d0713bc562ef6d04dec198930a9", + "numbered_root_commit": "64abf969bfbc1c576d4fa84ae3282d32efbfcc38" + }, + "installed_application": { + "path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app", + "version": "0.5.0", + "bundle_identifier": "world.guanghu.hololake", + "architecture": "arm64", + "binary_sha256": "2e162ff077187310f816f58df2250e3ee939eb489fe3c7b00e94557ff8f3370d", + "process_path_verified": true + }, + "developer_id": { + "identity": "Developer ID Application: bei sun (825A9L3G7Q)", + "team_identifier": "825A9L3G7Q", + "cdhash": "7a2372ed85fc3b775e9a352217b123a4bdee0d64", + "strict_signature_verification": "PASS", + "designated_requirement": "PASS", + "gatekeeper": "REJECTED_UNNOTARIZED_DEVELOPER_ID", + "apple_notarization_and_stapling": "PENDING" + }, + "runtime": { + "real_webview_loaded": true, + "visible_version": "V0.5.0", + "visible_domain": "第五域 · 光湖本源域", + "entered_surface": "永恒湖心系统", + "numbered_ipc_receipt_rows": 50, + "grant_and_execution_rows_present": true, + "empty_authority_binding_digest_rows": 0, + "recalculated_receipt_chain_failures": 0, + "last_sequence": 50, + "last_receipt_hash": "a294463ad9e6959d0f12ce5c2b59e11d97581e067b9480df9ae598dfafc067df" + }, + "old_application": { + "version": "0.4.1", + "role": "READ_ONLY_PRE_NUMBERED_ROOT_MODULE_DONOR", + "archive_receipt": "/Volumes/JZAO/HoloLake/artifacts/hololake-release/0.4.1/macos-arm64/pre-numbered-root-donor/archive-receipt.json", + "repair_in_place": false + }, + "release_boundary": { + "local_signed_install_complete": true, + "public_signed_notarized_release_complete": false, + "updater_public_key_continuity": "PASS_EXISTING_0.4.1_TRUST_ROOT_REUSED", + "updater_private_key_available_to_current_pipeline": true, + "updater_signature_generated_and_verified": true, + "pre_notarization_updater_artifact": { + "path": "src-tauri/target/release/bundle/macos/HoloLake.app.tar.gz", + "sha256": "4176bb7ea83c820744239c228671840289f87675329182bc055938e03bba9693", + "signature_path": "src-tauri/target/release/bundle/macos/HoloLake.app.tar.gz.sig", + "signature_sha256": "ae02c2918c26b6e3cc2389fec8d9f98884ae639d448edc78a4ec80ee30a8ae92", + "publication_allowed": false + }, + "apple_notarization_credentials_available_to_current_pipeline": false, + "public_broadcast_activated": false + }, + "persona_boundary": { + "current_codex_carrier_binding_claimed": false, + "runtime_persona_binding_created_by_numbered_ipc": false + } +} diff --git a/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json new file mode 100644 index 000000000..ffe8bbcf2 --- /dev/null +++ b/product-source/hololake-native-desktop/contracts/module-donor-admission-registry.json @@ -0,0 +1,176 @@ +{ + "schema": "hololake.module-donor-admission-registry/v1", + "record_id": "HLP-MODULE-DONOR-ADMISSION-001", + "state": "DONORS_QUARANTINED_CANDIDATES_NUMBERED_NOT_ADMITTED", + "root_rule": { + "official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT", + "repair_old_application_in_place": false, + "bulk_merge_or_wholesale_copy_allowed": false, + "one_candidate_per_admission_cycle": true, + "candidate_number_is_runtime_module_number": false, + "permanent_module_number_assignment_before_acceptance": false, + "all_frontend_backend_calls_must_cross_numbered_ipc": true, + "shared_file_merge_is_admission_evidence": false + }, + "donors": [ + { + "donor_id": "HLP-DONOR-COMPILED-DESKTOP-0.4.1", + "kind": "COMPILED_MACOS_APPLICATION", + "path": "/Volumes/JZAO/HoloLake/artifacts/hololake-release/0.4.1/macos-arm64/pre-numbered-root-donor/HoloLake.app", + "state": "READ_ONLY", + "source_commit": null, + "source_commit_state": "UNKNOWN_NOT_INFERRED_FROM_COMPILED_BUNDLE", + "binary_sha256": "adc0d8b028a8b874c39909265ed7c41e7c24e4fe5b38adba04e8f72b64900c15", + "use": "BEHAVIOR_AND_VISIBLE_PRODUCT_REFERENCE_ONLY" + }, + { + "donor_id": "HLP-DONOR-CHAOTIC-WORKTREE-20260818", + "kind": "DIRTY_SOURCE_WORKTREE", + "path": "/Users/bingshuolingdianyuanhe/Documents/Codex/2026-08-15/new-chat-2/work/jd-guanghu-supervisor/product-source/hololake-native-desktop", + "observed_head": "a8fe571b5d5c0a45207b64ac538d729b3d719d21", + "observed_dirty_path_count": 30, + "state": "READ_ONLY_UNTRUSTED_AS_A_WHOLE", + "use": "INDIVIDUAL_MODULE_SOURCE_CANDIDATES_ONLY" + } + ], + "rejected_inputs": [ + { + "candidate_number": "HLP-DONOR-CAND-0000", + "name": "legacy_numbered_operation_runtime", + "state": "REJECTED_SUPERSEDED", + "why": "It predates the single numbered IPC root and must not become a second numbering authority.", + "paths": [ + "contracts/numbered-operation-runtime.json", + "src/modules/numbered-runtime.ts", + "src-tauri/src/numbered_operation_runtime.rs" + ] + }, + { + "candidate_number": "HLP-DONOR-CAND-SHARED-MUTATIONS", + "name": "shared_file_mutation_set", + "state": "REJECTED_AS_MERGE_UNIT", + "why": "These files mix unrelated modules and divergent frontend/backend routes; each needed behavior must be reconstructed behind its owning numbered module.", + "examples": [ + "src/main.tsx", + "src/styles.css", + "src-tauri/src/lib.rs", + "src-tauri/src/knowledge_base.rs", + "src-tauri/src/gls_protocol_runtime.rs" + ] + } + ], + "candidates": [ + { + "admission_order": 1, + "candidate_number": "HLP-DONOR-CAND-0001", + "name": "native_composition_module_runtime", + "state": "QUARANTINED_ASSESS_FIRST", + "paths": [ + "contracts/native-composition-runtime.json", + "src/modules/native-composition", + "src-tauri/src/native_composition.rs" + ], + "why_first": "A module needs an isolated mount, self-test, unmount and rollback boundary before content modules can be admitted safely." + }, + { + "admission_order": 2, + "candidate_number": "HLP-DONOR-CAND-0002", + "name": "channel_document_and_spreadsheet_workbench", + "state": "QUARANTINED_PENDING_ADMISSION", + "paths": [ + "src/modules/channel-workbench" + ] + }, + { + "admission_order": 3, + "candidate_number": "HLP-DONOR-CAND-0003", + "name": "persona_channel_body", + "state": "QUARANTINED_PENDING_ADMISSION", + "paths": [ + "contracts/persona-channel-body.json", + "src/PersonaChannelBody.tsx", + "src-tauri/src/persona_channel_body.rs", + "src-tauri/src/channel_growth.rs" + ], + "boundary": "UI_AND_PERSISTENCE_BODY_ONLY; DOES_NOT_CREATE_PERSONA_BINDING" + }, + { + "admission_order": 4, + "candidate_number": "HLP-DONOR-CAND-0004", + "name": "education_workbench", + "state": "QUARANTINED_PENDING_ADMISSION", + "paths": [ + "contracts/education-workspace.json", + "src/modules/education-adaptive-rendering.ts", + "src/modules/education-data.ts", + "src/modules/education-document-engine.tsx", + "src-tauri/src/education_translation.rs", + "src-tauri/src/education_workspace.rs" + ] + }, + { + "admission_order": 5, + "candidate_number": "HLP-DONOR-CAND-0005", + "name": "web_novel_workbench_and_author_modules", + "state": "QUARANTINED_PENDING_ADMISSION", + "paths": [ + "contracts/web-novel-workspace.json", + "contracts/web-novel-module-marketplace-plan.json", + "src/WebNovelWorkspace.tsx", + "src/AuthorModuleCenter.tsx", + "src/AuthorWritingSidecar.tsx", + "src-tauri/src/web_novel_workspace.rs", + "src-tauri/src/web_novel_import.rs", + "src-tauri/src/web_novel_author.rs", + "src-tauri/src/web_novel_modules.rs" + ] + }, + { + "admission_order": 6, + "candidate_number": "HLP-DONOR-CAND-0006", + "name": "mobile_sync", + "state": "QUARANTINED_PENDING_ADMISSION", + "paths": [ + "contracts/mobile-sync-v1.json", + "src/MobileSyncPanel.tsx", + "src-tauri/src/mobile_sync.rs" + ] + }, + { + "admission_order": 7, + "candidate_number": "HLP-DONOR-CAND-0007", + "name": "dynamic_language_world_visual_surface", + "state": "QUARANTINED_PENDING_ADMISSION", + "paths": [ + "contracts/dynamic-language-world-visual-system.json", + "src/StarlakeSurface.tsx", + "src/starlake-surface.css", + "src/traditional-surface.tsx", + "src/traditional-surface.css", + "src-tauri/src/world_climate.rs" + ] + } + ], + "admission_gate": [ + "EXTRACT_ONLY_DECLARED_CANDIDATE_PATHS", + "REVIEW_SOURCE_PROVENANCE_AND_LICENSE", + "DEFINE_DATA_PERMISSION_AND_RESOURCE_BOUNDARY", + "ALLOCATE_NUMBERED_IPC_MODULE_TARGET_AND_OPERATION_COORDINATES", + "IMPLEMENT_ADAPTER_WITHOUT_RAW_TAURI_INVOKE", + "ADD_UNIT_INTEGRATION_AND_NEGATIVE_ROUTE_TESTS", + "BUILD_FROM_CLEAN_OFFICIAL_BASE", + "INSTALL_IN_ISOLATED_RUNTIME", + "VERIFY_MOUNT_SELF_TEST_RESTART_UNMOUNT_AND_ROLLBACK", + "WRITE_HASH_CHAINED_RUNTIME_RECEIPT", + "ONLY_THEN_ASSIGN_PERMANENT_MODULE_NUMBER" + ], + "hot_install_boundary": { + "source_repository_is_directly_executable": false, + "immutable_signed_artifact_required": true, + "compatibility_manifest_required": true, + "permissions_declared_before_mount": true, + "human_confirmation_required_when_boundary_expands": true, + "rollback_on_self_test_failure": true, + "user_data_survives_unmount": true + } +} diff --git a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md index 0035bb5f0..a91da9aab 100644 --- a/product-source/hololake-native-desktop/docs/ARCHITECTURE.md +++ b/product-source/hololake-native-desktop/docs/ARCHITECTURE.md @@ -90,10 +90,16 @@ HoloLake 0.3.0 includes a live, read-only projection of the PNCC resident runtim ## Release pipeline -`npm run release:macos -- release/inputs/.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v` tag equals the clean `main` head, and the Developer ID, Tauri updater-signing and Apple notarization credential sets are supplied at runtime. The pipeline runs all product and Rust gates, creates updater artifacts through a temporary Tauri override, then requires strict code-signature verification, Gatekeeper acceptance and stapled Apple notarization before writing the HoloLake broadcast and receipts. +`npm run release:macos -- release/inputs/.json` is the only product-owned macOS release entry. It fails before building unless the embedded trust contains the exact registered HoloLake HTTPS endpoint and updater public key, the immutable `v` tag equals the clean `main` head, and the Developer ID, Tauri updater-signing and Apple notarization credential sets are supplied at runtime. A protected updater-key path is materialized only into the child build environment; the key is never printed or copied into source. The pipeline runs all product and Rust gates, creates updater artifacts through a temporary Tauri override, verifies the updater signature against the embedded product trust, then requires strict code-signature verification, Gatekeeper acceptance and stapled Apple notarization before writing the HoloLake broadcast and receipts. Generated packages, private release inputs and credentials are not committed. The pipeline never uploads or activates a release; its terminal artifact is a bounded folder ready for a separately authorized JD-controller upload and server-owned readback receipt. +## Numbered-root module admission + +HoloLake 0.5.0 is the clean numbered-root base. The compiled 0.4.1 desktop application and the divergent dirty source worktree are read-only donors, not merge bases. Their old numbered-operation runtime is explicitly superseded, and mutations to shared files such as `src/main.tsx` or `src-tauri/src/lib.rs` are never accepted as a unit. + +Each donor capability receives a candidate coordinate, but no permanent runtime module number, until one isolated admission cycle has reviewed provenance and permissions, allocated numbered IPC module/target/operation coordinates, implemented an adapter without raw Tauri invoke, passed negative-route and data tests, and produced installed mount, restart, unmount and rollback receipts. The admission order and candidate inventory are recorded in `contracts/module-donor-admission-registry.json`. + ## Stage-one convergence verdict The Tauri source in this directory is the only future HoloLake desktop mainline. An installed build of it is an acceptance candidate, not a separate product line and not proof that stage one exists. The Electron 0.8.0 product and the legacy Tauri/platform sources remain read-only UX, behavior, engineering and protected-data donors until inventory, backup, readback, reversible migration rehearsal and signed installed-runtime acceptance all pass. diff --git a/product-source/hololake-native-desktop/foundation.json b/product-source/hololake-native-desktop/foundation.json index 28718d387..91dac9bf5 100644 --- a/product-source/hololake-native-desktop/foundation.json +++ b/product-source/hololake-native-desktop/foundation.json @@ -56,9 +56,11 @@ "local_runtime_acceptance_receipt": "audit/local-runtime-acceptance-20260815.json", "domain_membrane_pncc_installed_acceptance_receipt": "audit/hololake-0.4.0-domain-membrane-pncc-installed-acceptance-20260816.json", "updater_bootstrap_installed_acceptance_receipt": "audit/hololake-0.4.1-updater-bootstrap-installed-acceptance-20260817.json", - "installed_desktop_path": "/Applications/HoloLake.app", + "installed_desktop_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake.app", "old_desktop_apps_recoverably_archived": true, "public_developer_id_and_notarized_acceptance": false, + "local_developer_id_signed_0_5_0_installed_acceptance": true, + "numbered_root_installed_acceptance_receipt": "audit/hololake-0.5.0-numbered-root-installed-acceptance-20260818.json", "stage_one_implementation_order": [ "PERSONAL_CHANNEL_IDENTITY_TASK_EVENT_RECEIPT_KERNEL", "KNOWLEDGE_TREE_PAGE_SEARCH_AND_LOCAL_PERSISTENCE", @@ -99,8 +101,11 @@ "automatic_restart_allowed": false, "private_signing_material_allowed_in_source": false, "donor_audit_record": "audit/donor-audit.json", - "donor_audit_complete": false, + "donor_audit_complete": true, "donor_source_copying_allowed": false, + "module_donor_admission_registry": "contracts/module-donor-admission-registry.json", + "module_donor_bulk_merge_allowed": false, + "module_donor_one_candidate_per_admission_cycle": true, "stage_one_product_contract": "contracts/stage-one-platform.json", "local_development_bridge_contract": "contracts/local-development-bridge.json", "account_single_writer_kernel_implemented": true, @@ -128,7 +133,7 @@ "pncc_jd_live_server_projection_repository_content_exposed": false, "pncc_jd_live_server_projection_write_authority": false, "pncc_jd_live_server_projection_carrier_state": "UNBOUND_EVIDENCE_REQUIRED", - "installed_local_product_version": "0.4.1", + "installed_local_product_version": "0.5.0", "pncc_authenticated_direct_broker_integration_implemented": true, "pncc_human_mount_registration_implemented": true, "pncc_human_mount_registration_gate": "SATISFIED_NATIVE_FILE_PICKER_EXACT_CONFIRMATION", diff --git a/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs new file mode 100644 index 000000000..9b4f20153 --- /dev/null +++ b/product-source/hololake-native-desktop/scripts/module-donor-admission.test.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' + +const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admission-registry.json', import.meta.url), 'utf8')) +const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8')) + +test('chaotic donors are read-only candidates and never a bulk merge source', () => { + assert.equal(catalog.state, 'DONORS_QUARANTINED_CANDIDATES_NUMBERED_NOT_ADMITTED') + assert.equal(catalog.root_rule.repair_old_application_in_place, false) + assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false) + assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true) + assert.ok(catalog.donors.every((donor) => donor.state.startsWith('READ_ONLY'))) +}) + +test('candidate coordinates are unique but are not permanent runtime module numbers', () => { + const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number) + assert.equal(new Set(coordinates).size, coordinates.length) + assert.ok(catalog.candidates.every((candidate) => candidate.state.startsWith('QUARANTINED'))) + assert.equal(catalog.root_rule.candidate_number_is_runtime_module_number, false) + assert.equal(catalog.root_rule.permanent_module_number_assignment_before_acceptance, false) +}) + +test('legacy numbering donor is rejected and the numbered IPC root remains singular', () => { + const legacy = catalog.rejected_inputs.find((candidate) => candidate.name === 'legacy_numbered_operation_runtime') + assert.equal(legacy.state, 'REJECTED_SUPERSEDED') + assert.equal(numbered.runtime.public_tauri_command, 'numbered_ipc') + assert.equal(numbered.runtime.legacy_direct_commands_allowed, false) + assert.ok(catalog.admission_gate.includes('ALLOCATE_NUMBERED_IPC_MODULE_TARGET_AND_OPERATION_COORDINATES')) + assert.ok(catalog.admission_gate.includes('IMPLEMENT_ADAPTER_WITHOUT_RAW_TAURI_INVOKE')) +}) + +test('hot installation is artifact, permission, self-test and rollback backed', () => { + assert.equal(catalog.hot_install_boundary.source_repository_is_directly_executable, false) + assert.equal(catalog.hot_install_boundary.immutable_signed_artifact_required, true) + assert.equal(catalog.hot_install_boundary.compatibility_manifest_required, true) + assert.equal(catalog.hot_install_boundary.rollback_on_self_test_failure, true) + assert.equal(catalog.hot_install_boundary.user_data_survives_unmount, true) +}) diff --git a/product-source/hololake-native-desktop/scripts/release-pipeline.mjs b/product-source/hololake-native-desktop/scripts/release-pipeline.mjs index db42b1f31..af53c89e0 100644 --- a/product-source/hololake-native-desktop/scripts/release-pipeline.mjs +++ b/product-source/hololake-native-desktop/scripts/release-pipeline.mjs @@ -106,12 +106,30 @@ export function validateCredentialEnvironment(env) { if (missing.length) fail(`HOLOLAKE_RELEASE_PIPELINE_CREDENTIALS_MISSING:${[...new Set(missing)].sort().join(',')}`) } +export function materializeUpdaterPrivateKey(env, readText = (file) => fs.readFileSync(file, 'utf8')) { + if (typeof env.TAURI_SIGNING_PRIVATE_KEY === 'string' && env.TAURI_SIGNING_PRIVATE_KEY.trim()) { + return { ...env } + } + const privateKeyPath = requireText( + env.TAURI_SIGNING_PRIVATE_KEY_PATH, + 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_PRIVATE_KEY_PATH_REQUIRED', + ) + const privateKey = requireText( + readText(privateKeyPath), + 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_PRIVATE_KEY_EMPTY', + ) + return { + ...env, + TAURI_SIGNING_PRIVATE_KEY: privateKey, + } +} + function run(program, args, options = {}) { return execFileSync(program, args, { cwd: root, encoding: 'utf8', stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit', - env: process.env, + env: options.env || process.env, }) } @@ -194,6 +212,7 @@ export async function main(argv = process.argv.slice(2)) { const trustFacts = validateReleaseTrust(trust) const input = validateReleaseInput(readJson(inputPath), trustFacts) validateCredentialEnvironment(process.env) + const credentialEnvironment = materializeUpdaterPrivateKey(process.env) requireCleanImmutableSource(input) if (process.platform !== 'darwin' || process.arch !== 'arm64') fail('HOLOLAKE_RELEASE_PIPELINE_BUILD_HOST_MISMATCH') if (!process.env.APPLE_SIGNING_IDENTITY.includes(input.appleTeamIdentifier)) fail('HOLOLAKE_RELEASE_PIPELINE_SIGNING_TEAM_MISMATCH') @@ -205,7 +224,9 @@ export async function main(argv = process.argv.slice(2)) { const buildConfig = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-config-')), 'tauri.release.json') fs.writeFileSync(buildConfig, `${JSON.stringify({ bundle: { createUpdaterArtifacts: true } })}\n`, { mode: 0o600 }) try { - run('npm', ['run', 'tauri', '--', 'build', '--ci', '--config', buildConfig]) + run('npm', ['run', 'tauri', '--', 'build', '--ci', '--config', buildConfig], { + env: credentialEnvironment, + }) } finally { fs.rmSync(path.dirname(buildConfig), { recursive: true, force: true }) } @@ -217,6 +238,18 @@ export async function main(argv = process.argv.slice(2)) { const updater = findOne(bundleRoot, (file) => file.endsWith('.app.tar.gz'), 'HOLOLAKE_RELEASE_PIPELINE_UPDATER_NOT_UNIQUE') const updaterSignature = `${updater}.sig` if (!fs.statSync(updaterSignature, { throwIfNoEntry: false })?.isFile()) fail('HOLOLAKE_RELEASE_PIPELINE_UPDATER_SIGNATURE_MISSING') + run('cargo', [ + 'run', + '--quiet', + '--manifest-path', + 'src-tauri/Cargo.toml', + '--example', + 'verify_updater_signature', + '--', + 'src-tauri/release-trust.json', + updater, + updaterSignature, + ]) if (decodeURIComponent(input.packageUrl.pathname.split('/').pop()) !== path.basename(updater)) { fail('HOLOLAKE_RELEASE_PIPELINE_PACKAGE_URL_FILENAME_MISMATCH') } diff --git a/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs b/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs index 2a6e61298..f2b386a03 100644 --- a/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs +++ b/product-source/hololake-native-desktop/scripts/release-pipeline.test.mjs @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs' import test from 'node:test' import { + materializeUpdaterPrivateKey, validateCredentialEnvironment, validateReleaseInput, validateReleaseTrust, @@ -76,6 +77,25 @@ test('release pipeline requires updater signing, Developer ID and Apple notariza })) }) +test('release pipeline materializes a protected updater key path only inside the build environment', () => { + const source = { + TAURI_SIGNING_PRIVATE_KEY_PATH: '/protected/hololake-updater.key', + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: 'provided-at-runtime', + } + const hydrated = materializeUpdaterPrivateKey(source, (file) => { + assert.equal(file, source.TAURI_SIGNING_PRIVATE_KEY_PATH) + return 'encrypted-private-key-material' + }) + assert.equal(hydrated.TAURI_SIGNING_PRIVATE_KEY, 'encrypted-private-key-material') + assert.equal(source.TAURI_SIGNING_PRIVATE_KEY, undefined) +}) + +test('release pipeline verifies the updater signature against embedded product trust before broadcast', () => { + const source = readFileSync(new URL('./release-pipeline.mjs', import.meta.url), 'utf8') + assert.match(source, /--example',\s*'verify_updater_signature'/) + assert.match(source, /src-tauri\/release-trust\.json/) +}) + test('Windows updater keeps signed installation but never claims the macOS rollback boundary', () => { const source = readFileSync(new URL('../src-tauri/src/release_update.rs', import.meta.url), 'utf8') assert.match(source, /SIGNED_PACKAGE_VERIFIED_INSTALLING_NO_LOCAL_ROLLBACK/) diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.lock b/product-source/hololake-native-desktop/src-tauri/Cargo.lock index 308fe2d94..85ae99eb7 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.lock +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.lock @@ -1499,6 +1499,7 @@ dependencies = [ "fs2", "futures-util", "interprocess", + "minisign-verify", "reqwest", "ring", "rusqlite", diff --git a/product-source/hololake-native-desktop/src-tauri/Cargo.toml b/product-source/hololake-native-desktop/src-tauri/Cargo.toml index e51e4310f..56d30e107 100644 --- a/product-source/hololake-native-desktop/src-tauri/Cargo.toml +++ b/product-source/hololake-native-desktop/src-tauri/Cargo.toml @@ -38,4 +38,5 @@ futures-util = "0.3" widestring = "1" [dev-dependencies] +minisign-verify = "0.2.5" tempfile = "3" diff --git a/product-source/hololake-native-desktop/src-tauri/examples/verify_updater_signature.rs b/product-source/hololake-native-desktop/src-tauri/examples/verify_updater_signature.rs new file mode 100644 index 000000000..751aff370 --- /dev/null +++ b/product-source/hololake-native-desktop/src-tauri/examples/verify_updater_signature.rs @@ -0,0 +1,48 @@ +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use minisign_verify::{PublicKey, Signature}; +use serde_json::Value; +use std::{env, fs, path::Path}; + +fn decode_outer_base64(value: &str, label: &str) -> Result { + let bytes = STANDARD + .decode(value.trim()) + .map_err(|error| format!("{label}_BASE64_INVALID: {error}"))?; + String::from_utf8(bytes).map_err(|error| format!("{label}_UTF8_INVALID: {error}")) +} + +fn main() -> Result<(), String> { + let arguments: Vec = env::args().collect(); + let [_, trust_path, updater_path, signature_path] = arguments.as_slice() else { + return Err( + "USAGE: verify_updater_signature " + .to_string(), + ); + }; + let trust: Value = serde_json::from_str( + &fs::read_to_string(trust_path) + .map_err(|error| format!("RELEASE_TRUST_READ_FAILED: {error}"))?, + ) + .map_err(|error| format!("RELEASE_TRUST_JSON_INVALID: {error}"))?; + let encoded_public_key = trust + .get("publicKey") + .and_then(Value::as_str) + .ok_or_else(|| "RELEASE_TRUST_PUBLIC_KEY_MISSING".to_string())?; + let public_key = PublicKey::decode(&decode_outer_base64( + encoded_public_key, + "RELEASE_TRUST_PUBLIC_KEY", + )?) + .map_err(|error| format!("RELEASE_TRUST_PUBLIC_KEY_INVALID: {error}"))?; + let signature = Signature::decode(&decode_outer_base64( + &fs::read_to_string(signature_path) + .map_err(|error| format!("UPDATER_SIGNATURE_READ_FAILED: {error}"))?, + "UPDATER_SIGNATURE", + )?) + .map_err(|error| format!("UPDATER_SIGNATURE_INVALID: {error}"))?; + let updater = fs::read(Path::new(updater_path)) + .map_err(|error| format!("UPDATER_ARTIFACT_READ_FAILED: {error}"))?; + public_key + .verify(&updater, &signature, true) + .map_err(|error| format!("UPDATER_SIGNATURE_VERIFICATION_FAILED: {error}"))?; + println!("HOLOLAKE_UPDATER_SIGNATURE_VERIFIED"); + Ok(()) +}