diff --git a/README.md b/README.md index c08826c..5c4183d 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,10 @@ cargo run -p crosspack-cli -- --help ### 2) Bootstrap the trusted default source (`core`) -Before first metadata use, verify the published fingerprint in both channels: - -- `docs/trust/core-registry-fingerprint.txt` in this repository. -- Matching GitHub Release note entry for the same `updated_at` and `key_id`. +Before first metadata use, derive the source fingerprint from trusted `registry.pub` bytes in the official registry repository. ```bash -cargo run -p crosspack-cli -- registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint 65149d198a39db9ecfea6f63d098858ed3b06c118c1f455f84ab571106b830c2 +cargo run -p crosspack-cli -- registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint cargo run -p crosspack-cli -- update cargo run -p crosspack-cli -- registry list ``` @@ -228,7 +225,7 @@ Crosspack verifies both metadata and artifacts: - Official default source name: `core`. - Official source kind and URL: `git` at `https://github.com/spiritledsoftware/crosspack-registry.git`. -- Official fingerprint distribution channel: `docs/trust/core-registry-fingerprint.txt` plus a matching GitHub Release note entry. +- Official fingerprint source: SHA-256 digest of `registry.pub` from the official registry repository. - Bootstrap and rotation troubleshooting: `docs/registry-bootstrap-runbook.md`. Trust boundary note: diff --git a/crates/crosspack-cli/src/main.rs b/crates/crosspack-cli/src/main.rs index 23dd380..61fd448 100644 --- a/crates/crosspack-cli/src/main.rs +++ b/crates/crosspack-cli/src/main.rs @@ -60,7 +60,7 @@ struct Cli { const NO_ROOT_PACKAGES_TO_UPGRADE: &str = "No root packages installed"; const METADATA_CONFIG_GUIDANCE: &str = - "no configured registry snapshots available; bootstrap trusted source `core` with `crosspack registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint <64-hex>` then run `crosspack update` (see https://github.com/spiritledsoftware/crosspack/blob/main/docs/registry-bootstrap-runbook.md and https://github.com/spiritledsoftware/crosspack/blob/main/docs/trust/core-registry-fingerprint.txt)"; + "no configured registry snapshots available; bootstrap trusted source `core` with `crosspack registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint <64-hex>` then run `crosspack update` (see https://github.com/spiritledsoftware/crosspack/blob/main/docs/registry-bootstrap-runbook.md)"; const SNAPSHOT_ID_MISMATCH_ERROR_CODE: &str = "snapshot-id-mismatch"; const SEARCH_METADATA_GUIDANCE: &str = "search metadata unavailable; run `crosspack update` to refresh local snapshots and `crosspack registry list` to inspect source status"; diff --git a/docs/install-flow.md b/docs/install-flow.md index a2e41f9..848383c 100644 --- a/docs/install-flow.md +++ b/docs/install-flow.md @@ -200,6 +200,9 @@ The following install-flow extensions are planned in `docs/dependency-policy-spe - creates or updates a single managed profile block in `~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`, - ensures PATH setup and completion sourcing are idempotent. - Windows installer (`scripts/install.ps1`) writes PowerShell completion script to `\share\completions\crosspack.ps1` and updates `$PROFILE.CurrentUserCurrentHost` with one managed block for PATH + completion sourcing. +- Installers resolve the default `core` fingerprint at runtime by downloading `registry.pub` from `https://github.com/spiritledsoftware/crosspack-registry` and hashing it (SHA-256). +- Installers fail closed on fetch/hash/validation errors. +- Installer fingerprint overrides remain available for controlled/offline scenarios (`CROSSPACK_CORE_FINGERPRINT` on Unix, `-CoreFingerprint` on Windows). - Installer shell setup is best-effort: unsupported shells or profile write failures print warnings and manual commands, but installation still succeeds. - Opt out of installer shell setup with: - Unix: `CROSSPACK_NO_SHELL_SETUP=1` diff --git a/docs/registry-bootstrap-runbook.md b/docs/registry-bootstrap-runbook.md index 724cd69..ea946ae 100644 --- a/docs/registry-bootstrap-runbook.md +++ b/docs/registry-bootstrap-runbook.md @@ -7,17 +7,14 @@ This runbook defines support and operator procedures for first-run trust bootstr - Source name: `core` - Source kind: `git` - Source URL: `https://github.com/spiritledsoftware/crosspack-registry.git` -- Fingerprint channel: - - `docs/trust/core-registry-fingerprint.txt` - - Matching GitHub Release note entry +- Fingerprint source: SHA-256 digest of `registry.pub` from `https://github.com/spiritledsoftware/crosspack-registry` -Always verify both channels match on `fingerprint_sha256`, `updated_at`, and `key_id` before bootstrap. +Always verify fingerprint derivation from trusted `registry.pub` bytes before bootstrap. ## First-Run Bootstrap (User) -1. Read `docs/trust/core-registry-fingerprint.txt`. -2. Confirm the same values appear in the latest corresponding GitHub Release note. -3. Add the trusted source and update snapshots: +1. Derive SHA-256 from trusted `registry.pub` bytes. +2. Add the trusted source and update snapshots: ```bash crosspack registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint @@ -27,14 +24,20 @@ crosspack registry list Expected state: `core` appears with `snapshot=ready:`. +## Installer Behavior + +- `scripts/install.sh` and `scripts/install.ps1` fetch `registry.pub` from `https://github.com/spiritledsoftware/crosspack-registry` at install time and compute its SHA-256 fingerprint for `registry add`. +- Installers fail closed on fetch/hash/validation errors. +- Override only when needed for controlled/offline operations: + - Unix: `CROSSPACK_CORE_FINGERPRINT=<64-hex>` + - Windows: `-CoreFingerprint <64-hex>` + ## Fingerprint and Key Rotation (Operator Procedure) 1. Prepare new signing keypair and stage new `registry.pub` at planned cutover revision. 2. Compute the new fingerprint from raw `registry.pub` bytes. -3. Update `docs/trust/core-registry-fingerprint.txt` with new `fingerprint_sha256`, `updated_at`, and `key_id`. -4. Publish a GitHub Release note entry with exactly matching values. -5. Announce cutover with user recovery commands. -6. Keep rollback window for the previous key; remove old key after successful migration. +3. Publish cutover communication and user recovery commands. +4. Keep rollback window for the previous key; remove old key after successful migration. User-facing recovery commands during rotation: @@ -61,8 +64,8 @@ Symptoms: - Error includes mismatch/fingerprint wording. Actions: -1. Re-check `docs/trust/core-registry-fingerprint.txt` against GitHub Release note. -2. Remove and re-add `core` with the published fingerprint. +1. Fetch trusted `registry.pub` bytes from the official registry repository. +2. Recompute fingerprint from trusted `registry.pub` bytes and compare with local `sources.toml` value. 3. Retry `crosspack update`. ### source sync failed (`source-sync-failed`) @@ -92,6 +95,6 @@ Symptoms: - Update or metadata read fails with signature/metadata validation context. Actions: -1. Confirm registry key and fingerprint channels still match. +1. Confirm `registry.pub` and configured `fingerprint_sha256` still match. 2. Retry after fresh sync (`crosspack update`). 3. Escalate to registry operators with failing package path and error text. diff --git a/docs/source-management-spec.md b/docs/source-management-spec.md index 236484a..0c6a5b8 100644 --- a/docs/source-management-spec.md +++ b/docs/source-management-spec.md @@ -46,19 +46,17 @@ Crosspack publishes one official default source for first-run bootstrap: - Source name: `core` - Source kind: `git` - Source URL: `https://github.com/spiritledsoftware/crosspack-registry.git` -- Fingerprint publication channel: - - `docs/trust/core-registry-fingerprint.txt` in this repository - - Matching GitHub Release note entry for the same `updated_at` and `key_id` +- Fingerprint source: SHA-256 digest of `registry.pub` from `https://github.com/spiritledsoftware/crosspack-registry` Bootstrap sequence: ```text -crosspack registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint +crosspack registry add core https://github.com/spiritledsoftware/crosspack-registry.git --kind git --priority 100 --fingerprint crosspack update crosspack registry list ``` -Users must verify both fingerprint channels match before adding or updating trusted source records. +Users must derive or verify the fingerprint from trusted `registry.pub` bytes before adding or updating trusted source records. ## CLI Contract @@ -264,13 +262,9 @@ Rotation is explicit and fail-closed. Operators must complete all steps in order 1. Generate and publish new `registry.pub` in the source root at the target cutover revision. 2. Compute new SHA-256 fingerprint from raw `registry.pub` bytes. -3. Update `docs/trust/core-registry-fingerprint.txt` with: - - `fingerprint_sha256` - - `updated_at` - - `key_id` -4. Publish a matching GitHub Release note entry with the same three values. -5. Announce cutover window and required user action. -6. Keep old key material available only for rollback interval; remove once cutover is complete. +3. Publish rotation notice and required user action in release communications. +4. Announce cutover window and required user action. +5. Keep old key material available only for rollback interval; remove once cutover is complete. User recovery commands after announced rotation: diff --git a/docs/trust/core-registry-fingerprint.txt b/docs/trust/core-registry-fingerprint.txt deleted file mode 100644 index f33f103..0000000 --- a/docs/trust/core-registry-fingerprint.txt +++ /dev/null @@ -1,11 +0,0 @@ -source = core -kind = git -url = https://github.com/spiritledsoftware/crosspack-registry.git -fingerprint_sha256 = 87085672ad174b59ec6e8cfac8cfffebf84568ba08917426fd9e82b310780a52 -updated_at = 2026-03-02T12:00:00Z -key_id = core-registry-ed25519-2026-03-02 -release = https://github.com/spiritledsoftware/crosspack-registry/releases/tag/trust-core-2026-03-02 - -Verification: -- Cross-check these exact values against the matching GitHub Release note entry. -- If values do not match, do not run `crosspack registry add` for `core`. diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 7c99418..0aea9e2 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -6,7 +6,9 @@ param( [string]$CoreUrl = "https://github.com/spiritledsoftware/crosspack-registry.git", [string]$CoreKind = "git", [int]$CorePriority = 100, - [string]$CoreFingerprint = "65149d198a39db9ecfea6f63d098858ed3b06c118c1f455f84ab571106b830c2", + [string]$CoreFingerprint = "", + [Alias("TrustBulletinUrl")] + [string]$CoreRegistryPubUrl = "", [switch]$NoShellSetup ) @@ -15,6 +17,42 @@ $ErrorActionPreference = "Stop" $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("crosspack-install-" + [guid]::NewGuid().ToString("N")) +function Test-Hex64 { + param([string]$Value) + return $Value -match '^[0-9a-fA-F]{64}$' +} + +function Resolve-CoreFingerprint { + param( + [string]$Override, + [string]$RegistryPubUrl, + [string]$TempDirectory + ) + + if (-not [string]::IsNullOrWhiteSpace($Override)) { + if (-not (Test-Hex64 -Value $Override)) { + throw "-CoreFingerprint must be exactly 64 hex characters" + } + return $Override + } + + if ([string]::IsNullOrWhiteSpace($RegistryPubUrl)) { + throw "Core registry.pub URL is empty" + } + if (-not $RegistryPubUrl.StartsWith('https://')) { + throw "Core registry.pub URL must use https: $RegistryPubUrl" + } + + $registryPubPath = Join-Path $TempDirectory "registry.pub" + Invoke-WebRequest -Uri $RegistryPubUrl -OutFile $registryPubPath -UseBasicParsing + $fingerprint = (Get-FileHash -Algorithm SHA256 -Path $registryPubPath).Hash.ToLowerInvariant() + if (-not (Test-Hex64 -Value $fingerprint)) { + throw "computed registry key fingerprint is invalid" + } + + return $fingerprint +} + function Update-CrosspackManagedProfileBlock { param( [string]$ProfilePath, @@ -119,6 +157,10 @@ $end } try { + if ([string]::IsNullOrWhiteSpace($CoreRegistryPubUrl)) { + $CoreRegistryPubUrl = "https://raw.githubusercontent.com/spiritledsoftware/crosspack-registry/main/registry.pub" + } + if ([string]::IsNullOrWhiteSpace($Version)) { $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" $Version = $release.tag_name @@ -163,8 +205,10 @@ try { Copy-Item (Join-Path $tmpDir "crosspack.exe") (Join-Path $BinDir "cpk.exe") -Force $crosspackExe = Join-Path $BinDir "crosspack.exe" + $resolvedCoreFingerprint = Resolve-CoreFingerprint -Override $CoreFingerprint -RegistryPubUrl $CoreRegistryPubUrl -TempDirectory $tmpDir + Write-Host "==> Configuring default registry source ($CoreName)" - $addOutput = & $crosspackExe registry add $CoreName $CoreUrl --kind $CoreKind --priority $CorePriority --fingerprint $CoreFingerprint 2>&1 + $addOutput = & $crosspackExe registry add $CoreName $CoreUrl --kind $CoreKind --priority $CorePriority --fingerprint $resolvedCoreFingerprint 2>&1 if ($LASTEXITCODE -ne 0) { $listOutput = & $crosspackExe registry list 2>&1 if ($LASTEXITCODE -ne 0 -or ($listOutput -notmatch [regex]::Escape($CoreName))) { diff --git a/scripts/install.sh b/scripts/install.sh index cc8357b..40ce59c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -9,7 +9,8 @@ CORE_NAME="${CROSSPACK_CORE_NAME:-core}" CORE_URL="${CROSSPACK_CORE_URL:-https://github.com/spiritledsoftware/crosspack-registry.git}" CORE_KIND="${CROSSPACK_CORE_KIND:-git}" CORE_PRIORITY="${CROSSPACK_CORE_PRIORITY:-100}" -CORE_FINGERPRINT="${CROSSPACK_CORE_FINGERPRINT:-65149d198a39db9ecfea6f63d098858ed3b06c118c1f455f84ab571106b830c2}" +CORE_FINGERPRINT="${CROSSPACK_CORE_FINGERPRINT:-}" +CORE_REGISTRY_PUB_URL="${CROSSPACK_CORE_REGISTRY_PUB_URL:-${CROSSPACK_TRUST_BULLETIN_URL:-https://raw.githubusercontent.com/spiritledsoftware/crosspack-registry/main/registry.pub}}" SHELL_SETUP_OPT_OUT="${CROSSPACK_NO_SHELL_SETUP:-0}" SHELL_SETUP_BEGIN="# >>> crosspack shell setup >>>" @@ -24,6 +25,48 @@ warn() { echo "warning: $*" >&2 } +is_hex64() { + value="$1" + [ "${#value}" -eq 64 ] || return 1 + normalized="$(printf "%s" "$value" | tr 'A-F' 'a-f')" + case "$normalized" in + *[!0-9a-f]*|'') return 1 ;; + esac + return 0 +} + +assert_https_url() { + url="$1" + case "$url" in + https://*) return 0 ;; + *) err "registry key URL must use https: ${url}" ;; + esac +} + +resolve_core_fingerprint() { + if [ -n "$CORE_FINGERPRINT" ]; then + if ! is_hex64 "$CORE_FINGERPRINT"; then + err "CROSSPACK_CORE_FINGERPRINT must be 64 hex characters" + fi + echo "$CORE_FINGERPRINT" + return 0 + fi + + assert_https_url "$CORE_REGISTRY_PUB_URL" + + registry_pub_path="${tmp_dir}/registry.pub" + if ! download "$CORE_REGISTRY_PUB_URL" "$registry_pub_path"; then + err "failed fetching registry key from ${CORE_REGISTRY_PUB_URL}; set CROSSPACK_CORE_FINGERPRINT to override" + fi + + computed_fingerprint="$(sha256_of "$registry_pub_path")" + if ! is_hex64 "$computed_fingerprint"; then + err "computed registry key fingerprint is invalid" + fi + + echo "$computed_fingerprint" +} + download() { url="$1" out="$2" @@ -264,7 +307,8 @@ else fi echo "==> Configuring default registry source (${CORE_NAME})" -if "${BIN_DIR}/crosspack" registry add "${CORE_NAME}" "${CORE_URL}" --kind "${CORE_KIND}" --priority "${CORE_PRIORITY}" --fingerprint "${CORE_FINGERPRINT}" >/dev/null 2>&1; then +resolved_core_fingerprint="$(resolve_core_fingerprint)" +if "${BIN_DIR}/crosspack" registry add "${CORE_NAME}" "${CORE_URL}" --kind "${CORE_KIND}" --priority "${CORE_PRIORITY}" --fingerprint "${resolved_core_fingerprint}" >/dev/null 2>&1; then echo "Added registry source '${CORE_NAME}'" else if "${BIN_DIR}/crosspack" registry list 2>/dev/null | grep -q "${CORE_NAME}"; then