From c9f7c854fd1dc46ecc41e7896abd928d8a0f2d6e Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Mon, 2 Mar 2026 10:55:20 -0500 Subject: [PATCH 1/3] fix(installer): resolve core fingerprint from trust bulletin Avoid stale hardcoded installer fingerprints by loading and validating docs/trust/core-registry-fingerprint.txt at install time, with explicit override support for controlled/offline use. --- README.md | 2 +- docs/install-flow.md | 2 + docs/registry-bootstrap-runbook.md | 8 +++ scripts/install.ps1 | 85 +++++++++++++++++++++++++++++- scripts/install.sh | 78 ++++++++++++++++++++++++++- 5 files changed, 170 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c08826c..914117c 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Before first metadata use, verify the published fingerprint in both channels: - Matching GitHub Release note entry for the same `updated_at` and `key_id`. ```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 ``` diff --git a/docs/install-flow.md b/docs/install-flow.md index a2e41f9..b228106 100644 --- a/docs/install-flow.md +++ b/docs/install-flow.md @@ -200,6 +200,8 @@ 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 from `docs/trust/core-registry-fingerprint.txt` at runtime and fail closed on fetch/parse/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..5756bef 100644 --- a/docs/registry-bootstrap-runbook.md +++ b/docs/registry-bootstrap-runbook.md @@ -27,6 +27,14 @@ crosspack registry list Expected state: `core` appears with `snapshot=ready:`. +## Installer Behavior + +- `scripts/install.sh` and `scripts/install.ps1` fetch `docs/trust/core-registry-fingerprint.txt` at install time and use that value for `registry add`. +- Installers validate bulletin shape and fail closed on fetch/parse/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. diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 7c99418..f757625 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -6,7 +6,8 @@ param( [string]$CoreUrl = "https://github.com/spiritledsoftware/crosspack-registry.git", [string]$CoreKind = "git", [int]$CorePriority = 100, - [string]$CoreFingerprint = "65149d198a39db9ecfea6f63d098858ed3b06c118c1f455f84ab571106b830c2", + [string]$CoreFingerprint = "", + [string]$TrustBulletinUrl = "", [switch]$NoShellSetup ) @@ -15,6 +16,80 @@ $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 Parse-TrustBulletin { + param([string]$Content) + + $data = @{} + foreach ($line in ($Content -split "`r?`n")) { + if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$') { + $key = $matches[1] + $value = $matches[2].Trim() + if ($value.StartsWith('"') -and $value.EndsWith('"') -and $value.Length -ge 2) { + $value = $value.Substring(1, $value.Length - 2) + } + if ($data.ContainsKey($key)) { + throw "trust bulletin contains duplicate key '$key'" + } + $data[$key] = $value + } + } + + return $data +} + +function Resolve-CoreFingerprint { + param( + [string]$Override, + [string]$BulletinUrl, + [string]$ExpectedSource, + [string]$ExpectedKind, + [string]$ExpectedUrl + ) + + if (-not [string]::IsNullOrWhiteSpace($Override)) { + if (-not (Test-Hex64 -Value $Override)) { + throw "-CoreFingerprint must be exactly 64 hex characters" + } + return $Override + } + + if ([string]::IsNullOrWhiteSpace($BulletinUrl)) { + throw "Trust bulletin URL is empty" + } + if (-not $BulletinUrl.StartsWith('https://')) { + throw "Trust bulletin URL must use https: $BulletinUrl" + } + + $bulletinResponse = Invoke-WebRequest -Uri $BulletinUrl -UseBasicParsing + $data = Parse-TrustBulletin -Content $bulletinResponse.Content + + foreach ($requiredKey in @('source', 'kind', 'url', 'fingerprint_sha256')) { + if (-not $data.ContainsKey($requiredKey) -or [string]::IsNullOrWhiteSpace($data[$requiredKey])) { + throw "trust bulletin is missing required key '$requiredKey'" + } + } + + if ($data['source'] -ne $ExpectedSource) { + throw "trust bulletin source mismatch (expected '$ExpectedSource', got '$($data['source'])')" + } + if ($data['kind'] -ne $ExpectedKind) { + throw "trust bulletin kind mismatch (expected '$ExpectedKind', got '$($data['kind'])')" + } + if ($data['url'] -ne $ExpectedUrl) { + throw "trust bulletin url mismatch (expected '$ExpectedUrl', got '$($data['url'])')" + } + if (-not (Test-Hex64 -Value $data['fingerprint_sha256'])) { + throw "trust bulletin fingerprint_sha256 must be exactly 64 hex characters" + } + + return $data['fingerprint_sha256'] +} + function Update-CrosspackManagedProfileBlock { param( [string]$ProfilePath, @@ -119,6 +194,10 @@ $end } try { + if ([string]::IsNullOrWhiteSpace($TrustBulletinUrl)) { + $TrustBulletinUrl = "https://raw.githubusercontent.com/$Repo/main/docs/trust/core-registry-fingerprint.txt" + } + if ([string]::IsNullOrWhiteSpace($Version)) { $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" $Version = $release.tag_name @@ -163,8 +242,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 -BulletinUrl $TrustBulletinUrl -ExpectedSource $CoreName -ExpectedKind $CoreKind -ExpectedUrl $CoreUrl + 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..0bb4c35 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:-}" +TRUST_BULLETIN_URL="${CROSSPACK_TRUST_BULLETIN_URL:-https://raw.githubusercontent.com/${REPO}/main/docs/trust/core-registry-fingerprint.txt}" SHELL_SETUP_OPT_OUT="${CROSSPACK_NO_SHELL_SETUP:-0}" SHELL_SETUP_BEGIN="# >>> crosspack shell setup >>>" @@ -24,6 +25,78 @@ 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 "trust bulletin URL must use https: ${url}" ;; + esac +} + +read_bulletin_value() { + key="$1" + file="$2" + awk -F '=' -v key="$key" ' + { + raw_key = $1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", raw_key) + if (raw_key == key) { + value = $2 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + gsub(/^"|"$/, "", value) + print value + found += 1 + } + } + END { + if (found != 1) { + exit 1 + } + } + ' "$file" +} + +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 "$TRUST_BULLETIN_URL" + + bulletin_path="${tmp_dir}/core-registry-fingerprint.txt" + if ! download "$TRUST_BULLETIN_URL" "$bulletin_path"; then + err "failed fetching trust bulletin from ${TRUST_BULLETIN_URL}; set CROSSPACK_CORE_FINGERPRINT to override" + fi + + bulletin_source="$(read_bulletin_value source "$bulletin_path")" || err "trust bulletin is missing a unique 'source' key" + bulletin_kind="$(read_bulletin_value kind "$bulletin_path")" || err "trust bulletin is missing a unique 'kind' key" + bulletin_url="$(read_bulletin_value url "$bulletin_path")" || err "trust bulletin is missing a unique 'url' key" + bulletin_fingerprint="$(read_bulletin_value fingerprint_sha256 "$bulletin_path")" || err "trust bulletin is missing a unique 'fingerprint_sha256' key" + + [ "$bulletin_source" = "$CORE_NAME" ] || err "trust bulletin source mismatch (expected ${CORE_NAME}, got ${bulletin_source})" + [ "$bulletin_kind" = "$CORE_KIND" ] || err "trust bulletin kind mismatch (expected ${CORE_KIND}, got ${bulletin_kind})" + [ "$bulletin_url" = "$CORE_URL" ] || err "trust bulletin URL mismatch (expected ${CORE_URL}, got ${bulletin_url})" + if ! is_hex64 "$bulletin_fingerprint"; then + err "trust bulletin fingerprint must be 64 hex characters" + fi + + echo "$bulletin_fingerprint" +} + download() { url="$1" out="$2" @@ -264,7 +337,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 From fa58a26e489cf54351c20ea4c4a60e234013393c Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Mon, 2 Mar 2026 10:58:42 -0500 Subject: [PATCH 2/3] fix(installer): derive core fingerprint from registry key Fetch crosspack-registry registry.pub at install time and hash it to compute the pinned fingerprint, avoiding stale repository-side constants. --- docs/install-flow.md | 3 +- docs/registry-bootstrap-runbook.md | 4 +- scripts/install.ps1 | 71 +++++++----------------------- scripts/install.sh | 50 +++++---------------- 4 files changed, 31 insertions(+), 97 deletions(-) diff --git a/docs/install-flow.md b/docs/install-flow.md index b228106..848383c 100644 --- a/docs/install-flow.md +++ b/docs/install-flow.md @@ -200,7 +200,8 @@ 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 from `docs/trust/core-registry-fingerprint.txt` at runtime and fail closed on fetch/parse/validation errors. +- 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: diff --git a/docs/registry-bootstrap-runbook.md b/docs/registry-bootstrap-runbook.md index 5756bef..199d608 100644 --- a/docs/registry-bootstrap-runbook.md +++ b/docs/registry-bootstrap-runbook.md @@ -29,8 +29,8 @@ Expected state: `core` appears with `snapshot=ready:`. ## Installer Behavior -- `scripts/install.sh` and `scripts/install.ps1` fetch `docs/trust/core-registry-fingerprint.txt` at install time and use that value for `registry add`. -- Installers validate bulletin shape and fail closed on fetch/parse/validation errors. +- `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>` diff --git a/scripts/install.ps1 b/scripts/install.ps1 index f757625..0aea9e2 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -7,7 +7,8 @@ param( [string]$CoreKind = "git", [int]$CorePriority = 100, [string]$CoreFingerprint = "", - [string]$TrustBulletinUrl = "", + [Alias("TrustBulletinUrl")] + [string]$CoreRegistryPubUrl = "", [switch]$NoShellSetup ) @@ -21,34 +22,11 @@ function Test-Hex64 { return $Value -match '^[0-9a-fA-F]{64}$' } -function Parse-TrustBulletin { - param([string]$Content) - - $data = @{} - foreach ($line in ($Content -split "`r?`n")) { - if ($line -match '^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$') { - $key = $matches[1] - $value = $matches[2].Trim() - if ($value.StartsWith('"') -and $value.EndsWith('"') -and $value.Length -ge 2) { - $value = $value.Substring(1, $value.Length - 2) - } - if ($data.ContainsKey($key)) { - throw "trust bulletin contains duplicate key '$key'" - } - $data[$key] = $value - } - } - - return $data -} - function Resolve-CoreFingerprint { param( [string]$Override, - [string]$BulletinUrl, - [string]$ExpectedSource, - [string]$ExpectedKind, - [string]$ExpectedUrl + [string]$RegistryPubUrl, + [string]$TempDirectory ) if (-not [string]::IsNullOrWhiteSpace($Override)) { @@ -58,36 +36,21 @@ function Resolve-CoreFingerprint { return $Override } - if ([string]::IsNullOrWhiteSpace($BulletinUrl)) { - throw "Trust bulletin URL is empty" - } - if (-not $BulletinUrl.StartsWith('https://')) { - throw "Trust bulletin URL must use https: $BulletinUrl" + if ([string]::IsNullOrWhiteSpace($RegistryPubUrl)) { + throw "Core registry.pub URL is empty" } - - $bulletinResponse = Invoke-WebRequest -Uri $BulletinUrl -UseBasicParsing - $data = Parse-TrustBulletin -Content $bulletinResponse.Content - - foreach ($requiredKey in @('source', 'kind', 'url', 'fingerprint_sha256')) { - if (-not $data.ContainsKey($requiredKey) -or [string]::IsNullOrWhiteSpace($data[$requiredKey])) { - throw "trust bulletin is missing required key '$requiredKey'" - } + if (-not $RegistryPubUrl.StartsWith('https://')) { + throw "Core registry.pub URL must use https: $RegistryPubUrl" } - if ($data['source'] -ne $ExpectedSource) { - throw "trust bulletin source mismatch (expected '$ExpectedSource', got '$($data['source'])')" - } - if ($data['kind'] -ne $ExpectedKind) { - throw "trust bulletin kind mismatch (expected '$ExpectedKind', got '$($data['kind'])')" - } - if ($data['url'] -ne $ExpectedUrl) { - throw "trust bulletin url mismatch (expected '$ExpectedUrl', got '$($data['url'])')" - } - if (-not (Test-Hex64 -Value $data['fingerprint_sha256'])) { - throw "trust bulletin fingerprint_sha256 must be exactly 64 hex characters" + $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 $data['fingerprint_sha256'] + return $fingerprint } function Update-CrosspackManagedProfileBlock { @@ -194,8 +157,8 @@ $end } try { - if ([string]::IsNullOrWhiteSpace($TrustBulletinUrl)) { - $TrustBulletinUrl = "https://raw.githubusercontent.com/$Repo/main/docs/trust/core-registry-fingerprint.txt" + if ([string]::IsNullOrWhiteSpace($CoreRegistryPubUrl)) { + $CoreRegistryPubUrl = "https://raw.githubusercontent.com/spiritledsoftware/crosspack-registry/main/registry.pub" } if ([string]::IsNullOrWhiteSpace($Version)) { @@ -242,7 +205,7 @@ 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 -BulletinUrl $TrustBulletinUrl -ExpectedSource $CoreName -ExpectedKind $CoreKind -ExpectedUrl $CoreUrl + $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 $resolvedCoreFingerprint 2>&1 diff --git a/scripts/install.sh b/scripts/install.sh index 0bb4c35..40ce59c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -10,7 +10,7 @@ CORE_URL="${CROSSPACK_CORE_URL:-https://github.com/spiritledsoftware/crosspack-r CORE_KIND="${CROSSPACK_CORE_KIND:-git}" CORE_PRIORITY="${CROSSPACK_CORE_PRIORITY:-100}" CORE_FINGERPRINT="${CROSSPACK_CORE_FINGERPRINT:-}" -TRUST_BULLETIN_URL="${CROSSPACK_TRUST_BULLETIN_URL:-https://raw.githubusercontent.com/${REPO}/main/docs/trust/core-registry-fingerprint.txt}" +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 >>>" @@ -39,33 +39,10 @@ assert_https_url() { url="$1" case "$url" in https://*) return 0 ;; - *) err "trust bulletin URL must use https: ${url}" ;; + *) err "registry key URL must use https: ${url}" ;; esac } -read_bulletin_value() { - key="$1" - file="$2" - awk -F '=' -v key="$key" ' - { - raw_key = $1 - gsub(/^[[:space:]]+|[[:space:]]+$/, "", raw_key) - if (raw_key == key) { - value = $2 - gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) - gsub(/^"|"$/, "", value) - print value - found += 1 - } - } - END { - if (found != 1) { - exit 1 - } - } - ' "$file" -} - resolve_core_fingerprint() { if [ -n "$CORE_FINGERPRINT" ]; then if ! is_hex64 "$CORE_FINGERPRINT"; then @@ -75,26 +52,19 @@ resolve_core_fingerprint() { return 0 fi - assert_https_url "$TRUST_BULLETIN_URL" + assert_https_url "$CORE_REGISTRY_PUB_URL" - bulletin_path="${tmp_dir}/core-registry-fingerprint.txt" - if ! download "$TRUST_BULLETIN_URL" "$bulletin_path"; then - err "failed fetching trust bulletin from ${TRUST_BULLETIN_URL}; set CROSSPACK_CORE_FINGERPRINT to override" + 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 - bulletin_source="$(read_bulletin_value source "$bulletin_path")" || err "trust bulletin is missing a unique 'source' key" - bulletin_kind="$(read_bulletin_value kind "$bulletin_path")" || err "trust bulletin is missing a unique 'kind' key" - bulletin_url="$(read_bulletin_value url "$bulletin_path")" || err "trust bulletin is missing a unique 'url' key" - bulletin_fingerprint="$(read_bulletin_value fingerprint_sha256 "$bulletin_path")" || err "trust bulletin is missing a unique 'fingerprint_sha256' key" - - [ "$bulletin_source" = "$CORE_NAME" ] || err "trust bulletin source mismatch (expected ${CORE_NAME}, got ${bulletin_source})" - [ "$bulletin_kind" = "$CORE_KIND" ] || err "trust bulletin kind mismatch (expected ${CORE_KIND}, got ${bulletin_kind})" - [ "$bulletin_url" = "$CORE_URL" ] || err "trust bulletin URL mismatch (expected ${CORE_URL}, got ${bulletin_url})" - if ! is_hex64 "$bulletin_fingerprint"; then - err "trust bulletin fingerprint must be 64 hex characters" + computed_fingerprint="$(sha256_of "$registry_pub_path")" + if ! is_hex64 "$computed_fingerprint"; then + err "computed registry key fingerprint is invalid" fi - echo "$bulletin_fingerprint" + echo "$computed_fingerprint" } download() { From 34054823f140ac4605232a73a30a47c11043fada Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Mon, 2 Mar 2026 11:04:03 -0500 Subject: [PATCH 3/3] docs(trust): remove repository fingerprint bulletin Switch guidance to deriving the core source fingerprint directly from crosspack-registry registry.pub and drop stale in-repo fingerprint distribution references. --- README.md | 9 +++------ crates/crosspack-cli/src/main.rs | 2 +- docs/registry-bootstrap-runbook.md | 23 +++++++++-------------- docs/source-management-spec.md | 18 ++++++------------ docs/trust/core-registry-fingerprint.txt | 11 ----------- 5 files changed, 19 insertions(+), 44 deletions(-) delete mode 100644 docs/trust/core-registry-fingerprint.txt diff --git a/README.md b/README.md index 914117c..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 +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/registry-bootstrap-runbook.md b/docs/registry-bootstrap-runbook.md index 199d608..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 @@ -39,10 +36,8 @@ Expected state: `core` appears with `snapshot=ready:`. 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: @@ -69,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`) @@ -100,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`.