From 8e09c20874c6af1cfe73c33e11a46f37cc533eff Mon Sep 17 00:00:00 2001 From: Antics Decoded <237784870+AnticsDecoded@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:41:06 +0100 Subject: [PATCH 1/3] Harden review and release trust boundaries --- .github/workflows/publish-release.yml | 132 ++++++++++ .github/workflows/release.yml | 242 ++---------------- CHANGELOG.md | 34 +++ docs/03-verification-status.md | 22 +- docs/08-what-was-checked.md | 37 ++- docs/09-against-real-transactions.md | 17 +- docs/15-release-signing.md | 79 +++--- platform/README.md | 10 +- platform/sel4/linux-signer/build.sh | 30 ++- platform/sel4/linux-signer/integrated.system | 2 +- platform/sel4/linux-signer/run.py | 16 +- platform/sel4/linux-signer/signer.c | 33 ++- platform/sel4/linux-signer/vmm.c | 128 ++------- platform/sel4/signer-system/build.sh | 5 +- platform/sel4/signer-system/display.c | 101 ++++++++ platform/sel4/signer-system/run.py | 23 +- platform/sel4/signer-system/signer.c | 45 ++-- platform/sel4/signer-system/signer.system | 16 +- platform/sel4/signer-system/wallet_ui.c | 19 +- platform/signer-image/EXPECTED-HASHES.txt | 2 +- scripts/release_signing.py | 82 +----- scripts/test_platform_security.py | 76 ++++++ scripts/test_release_signing.py | 98 +++---- signing-core/EXPECTED-HASHES.txt | 8 +- .../crates/authority-agent/src/lib.rs | 2 +- .../crates/authority-agent/src/main.rs | 17 +- .../crates/authority-agent/src/tools.rs | 11 + .../crates/authority-agent/tests/adapter.rs | 10 +- signing-core/crates/clearsign/src/calls.rs | 23 +- signing-core/crates/clearsign/src/error.rs | 3 + signing-core/crates/clearsign/src/evm.rs | 10 + signing-core/crates/clearsign/src/lib.rs | 4 +- signing-core/crates/clearsign/src/review.rs | 17 +- signing-core/crates/clearsign/src/rlp.rs | 67 ++++- .../crates/clearsign/tests/administration.rs | 7 + .../crates/clearsign/tests/invariants.rs | 18 +- .../crates/clearsign/tests/mutation_gaps.rs | 31 ++- .../scripts/in-canonical-container.sh | 13 +- .../scripts/reproducible-cross-check.sh | 17 +- 39 files changed, 860 insertions(+), 647 deletions(-) create mode 100644 .github/workflows/publish-release.yml create mode 100644 platform/sel4/signer-system/display.c create mode 100644 scripts/test_platform_security.py diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..727cf50 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,132 @@ +name: publish validated release + +# workflow_run uses this workflow from the default branch, not from the release +# tag. The publisher never checks out or executes candidate repository code. +on: + workflow_run: + workflows: [release] + types: [completed] + +permissions: + actions: read + contents: read + +jobs: + publish: + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' + environment: release + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: write + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: publication-candidate + path: publication + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - name: Verify and publish the exact candidate + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_COMMIT: ${{ github.event.workflow_run.head_sha }} + EXPECTED_TAG: ${{ github.event.workflow_run.head_branch }} + run: | + set -euo pipefail + test -x /usr/bin/gh + test -x /usr/bin/jq + test -f publication/notes.md + test -f publication/release-flags.txt + test -f publication/dist/BUILD-METADATA.json + test -f publication/dist/SHA256SUMS + + version=$(/usr/bin/jq -er '.version' publication/dist/BUILD-METADATA.json) + commit=$(/usr/bin/jq -er '.commit' publication/dist/BUILD-METADATA.json) + reference_type=$(/usr/bin/jq -er '.referenceType' publication/dist/BUILD-METADATA.json) + release_mode=$(/usr/bin/jq -er '.releaseMode' publication/dist/BUILD-METADATA.json) + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] + [ "$commit" = "$EXPECTED_COMMIT" ] + [ "$reference_type" = tag ] + [ "$release_mode" = unsigned-evaluation ] + tag="v$version" + [ "$tag" = "$EXPECTED_TAG" ] + # Resolve the exact tag ref. The commits endpoint accepts ambiguous + # ref names, so a branch named like a release tag must never satisfy + # this publication boundary. Peel annotated tags until the commit is + # reached, with a hard bound for malformed or cyclic-looking input. + ref_json=$(/usr/bin/gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/$tag") + object_type=$(/usr/bin/jq -er '.object.type' <<<"$ref_json") + object_sha=$(/usr/bin/jq -er '.object.sha' <<<"$ref_json") + for _ in 1 2 3 4 5 6 7 8; do + [ "$object_type" = commit ] && break + [ "$object_type" = tag ] + tag_json=$(/usr/bin/gh api "repos/${GITHUB_REPOSITORY}/git/tags/$object_sha") + object_type=$(/usr/bin/jq -er '.object.type' <<<"$tag_json") + object_sha=$(/usr/bin/jq -er '.object.sha' <<<"$tag_json") + done + [ "$object_type" = commit ] + [ "$object_sha" = "$commit" ] + + expected=( + BINARY-SHA256 + BUILD-METADATA.json + ClearSign.spdx.json + "ClearSign_${version}_aarch64.dmg" + "ClearSign_${version}_amd64.AppImage" + "ClearSign_${version}_amd64.deb" + "ClearSign_${version}_x64-setup.exe" + "ClearSign_${version}_x64_en-US.msi" + SHA256SUMS + clearsign-aarch64-apple-darwin.tar.gz + clearsign-aarch64-apple-darwin.tar.gz.sha256 + clearsign-aarch64-unknown-linux-musl.tar.gz + clearsign-aarch64-unknown-linux-musl.tar.gz.sha256 + clearsign-x86_64-apple-darwin.tar.gz + clearsign-x86_64-apple-darwin.tar.gz.sha256 + clearsign-x86_64-unknown-linux-musl.tar.gz + clearsign-x86_64-unknown-linux-musl.tar.gz.sha256 + clearsign.html + ) + /usr/bin/printf '%s\n' "${expected[@]}" | /usr/bin/sort > /tmp/expected-files + /usr/bin/find publication/dist -mindepth 1 -maxdepth 1 -type f -printf '%f\n' \ + | /usr/bin/sort > /tmp/actual-files + /usr/bin/diff -u /tmp/expected-files /tmp/actual-files + + manifest_files=( + BINARY-SHA256 + BUILD-METADATA.json + ClearSign.spdx.json + "ClearSign_${version}_aarch64.dmg" + "ClearSign_${version}_amd64.AppImage" + "ClearSign_${version}_amd64.deb" + "ClearSign_${version}_x64-setup.exe" + "ClearSign_${version}_x64_en-US.msi" + clearsign-aarch64-apple-darwin.tar.gz + clearsign-aarch64-unknown-linux-musl.tar.gz + clearsign-x86_64-apple-darwin.tar.gz + clearsign-x86_64-unknown-linux-musl.tar.gz + clearsign.html + ) + /usr/bin/printf '%s\n' "${manifest_files[@]}" | /usr/bin/sort > /tmp/expected-manifest + /usr/bin/awk '{print $2}' publication/dist/SHA256SUMS | /usr/bin/sort > /tmp/actual-manifest + /usr/bin/diff -u /tmp/expected-manifest /tmp/actual-manifest + (cd publication/dist && /usr/bin/sha256sum --check SHA256SUMS) + + mapfile -t release_flags < publication/release-flags.txt + [ "${#release_flags[@]}" -eq 2 ] + [ "${release_flags[0]}" = --draft ] + [ "${release_flags[1]}" = --prerelease ] + + assets=(publication/dist/SHA256SUMS) + for name in "${manifest_files[@]}"; do + assets+=("publication/dist/$name") + done + /usr/bin/gh release create "$tag" \ + "${release_flags[@]}" \ + --target "$commit" \ + --title "$tag" \ + --notes-file publication/notes.md \ + "${assets[@]}" + /usr/bin/gh release edit "$tag" --draft=false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa415e5..3f5d36d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,11 @@ jobs: echo "apple_team_id=$(jq -r .appleTeamId <<< "$policy")" echo "windows_certificate_thumbprint=$(jq -r .windowsCertificateThumbprint <<< "$policy")" } >> "$GITHUB_OUTPUT" + if [ "$(jq -r .releaseMode <<< "$policy")" = signed ]; then + echo "::error::signed releases are disabled in this repository workflow." >&2 + echo "::error::Use a separately administered signing service that receives only digest-verified unsigned artifacts." >&2 + exit 1 + fi - name: Validate release tooling run: | python3 -m unittest discover -s scripts -p 'test_*.py' @@ -255,11 +260,10 @@ jobs: exit 1 fi - publish: + prepare-publication: needs: [validate, artifacts, signer-image] - environment: release permissions: - contents: write + contents: read runs-on: ubuntu-24.04 if: startsWith(github.ref, 'refs/tags/v') steps: @@ -278,9 +282,8 @@ jobs: python3 scripts/release_manifest.py verify dist "${GITHUB_REF_NAME#v}" "${GITHUB_SHA}" tag \ "$CLEARSIGN_RELEASE_MODE" "$APPLE_TEAM_ID" "$WINDOWS_CERTIFICATE_THUMBPRINT" cat dist/SHA256SUMS - - name: Publish + - name: Prepare release notes env: - GH_TOKEN: ${{ github.token }} CLEARSIGN_RELEASE_MODE: ${{ needs.validate.outputs.release_mode }} run: | set -euo pipefail @@ -334,8 +337,8 @@ jobs: **Reproducibility.** The `aarch64-unknown-linux-musl` build is the one you can check, and it is built here the same way you would rebuild it: in the canonical container, with build paths remapped out and the - compile itself offline from a copied registry cache. The container's - own bootstrap is not offline — it installs three packages with apt and + compile itself offline from a frozen registry-cache snapshot. The container's + own bootstrap is not offline — it installs bootstrap packages with apt and downloads rustup, neither pinned by digest — so the environment is named and reproducible in practice rather than sealed. Rebuild it with `signing-core/scripts/reproducible-cross-check.sh` and @@ -352,23 +355,12 @@ jobs: imply otherwise. NOTES - if [ "$CLEARSIGN_RELEASE_MODE" = signed ]; then - cat >> notes.md <<'SIGNED' - **Publisher authentication.** The macOS application and DMG are signed - with a Developer ID certificate, accepted by Apple's notarization - service and stapled. The Windows application, MSI and NSIS installer - carry an Authenticode signature and trusted timestamp. Linux packages - are not platform-signed; use `SHA256SUMS` and the GitHub build-provenance - attestation. `BUILD-METADATA.json` records these platform differences. - SIGNED - else - cat >> notes.md <<'UNSIGNED' + cat >> notes.md <<'UNSIGNED' **Publisher authentication.** This is the explicitly selected unsigned evaluation mode. The macOS and Windows artifacts have no platform publisher signature or notarization. Use `SHA256SUMS`, inspect `BUILD-METADATA.json`, and verify the GitHub build-provenance attestation. UNSIGNED - fi cat >> notes.md <<'NOTES' **Not for real keys.** The signing and seed commands stay behind a @@ -379,17 +371,15 @@ jobs: # away: only the tarballs were ever attached. # Upload into a draft first. When immutable releases are enabled, the # final publication locks the complete asset set and tag together. - release_flags=(--draft) - if [ "$CLEARSIGN_RELEASE_MODE" = unsigned-evaluation ]; then - release_flags+=(--prerelease) - fi - gh release create "${GITHUB_REF_NAME}" \ - "${release_flags[@]}" \ - --title "${GITHUB_REF_NAME}" \ - --notes-file notes.md \ - dist/*.tar.gz dist/clearsign.html dist/SHA256SUMS dist/BINARY-SHA256 dist/BUILD-METADATA.json dist/ClearSign.spdx.json \ - $(ls dist/*.dmg dist/*.msi dist/*.exe dist/*.deb dist/*.AppImage 2>/dev/null || true) - gh release edit "${GITHUB_REF_NAME}" --draft=false + release_flags=(--draft --prerelease) + printf '%s\n' "${release_flags[@]}" > release-flags.txt + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: publication-candidate + path: | + dist/* + notes.md + release-flags.txt # The installers. One job per platform, because each one is built by the # operating system it installs onto. @@ -449,144 +439,15 @@ jobs: test -s desktop/src/index.html \ || { echo "app/build.sh did not produce desktop/src/index.html" >&2; exit 1; } echo "front end: $(wc -c < desktop/src/index.html) bytes" - - name: Prepare macOS Developer ID signing and notarization - if: runner.os == 'macOS' && startsWith(github.ref, 'refs/tags/v') && needs.validate.outputs.release_mode == 'signed' - shell: bash - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} - APPLE_TEAM_ID: ${{ needs.validate.outputs.apple_team_id }} - run: | - set -euo pipefail - python3 scripts/release_signing.py check-env macos - [[ "$APPLE_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]] \ - || { echo "::error::APPLE_TEAM_ID must be 10 uppercase letters or digits" >&2; exit 1; } - - certificate="$RUNNER_TEMP/clearsign-developer-id.p12" - api_key="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" - python3 - "$certificate" "$api_key" <<'PY' - import base64 - import os - from pathlib import Path - import sys - - Path(sys.argv[1]).write_bytes(base64.b64decode(os.environ["APPLE_CERTIFICATE"], validate=True)) - Path(sys.argv[2]).write_bytes(base64.b64decode(os.environ["APPLE_API_KEY_P8_BASE64"], validate=True)) - PY - chmod 600 "$certificate" "$api_key" - - keychain="$RUNNER_TEMP/clearsign-signing.keychain-db" - keychain_password=$(openssl rand -hex 32) - echo "::add-mask::$keychain_password" - previous_keychain=$(security default-keychain -d user | tr -d '"') - { - echo "CLEARSIGN_KEYCHAIN_PATH=$keychain" - echo "CLEARSIGN_PREVIOUS_KEYCHAIN=$previous_keychain" - } >> "$GITHUB_ENV" - security create-keychain -p "$keychain_password" "$keychain" - security set-keychain-settings -lut 21600 "$keychain" - security unlock-keychain -p "$keychain_password" "$keychain" - security import "$certificate" -k "$keychain" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain" - security list-keychains -d user -s "$keychain" "$previous_keychain" - security default-keychain -d user -s "$keychain" - - identities=$(security find-identity -v -p codesigning "$keychain" \ - | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p') - count=$(printf '%s\n' "$identities" | sed '/^$/d' | wc -l | tr -d ' ') - [ "$count" = 1 ] || { - echo "::error::expected exactly one Developer ID Application identity, found $count" >&2 - exit 1 - } - - { - echo "APPLE_SIGNING_IDENTITY=$identities" - echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" - echo "APPLE_API_KEY=$APPLE_API_KEY" - echo "APPLE_API_KEY_PATH=$api_key" - echo "APPLE_TEAM_ID=$APPLE_TEAM_ID" - } >> "$GITHUB_ENV" - - name: Prepare Windows Authenticode signing - if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v') && needs.validate.outputs.release_mode == 'signed' - shell: pwsh - env: - WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} - WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} - WINDOWS_CERTIFICATE_THUMBPRINT: ${{ needs.validate.outputs.windows_certificate_thumbprint }} - WINDOWS_TIMESTAMP_URL: ${{ vars.WINDOWS_TIMESTAMP_URL }} - run: | - $ErrorActionPreference = 'Stop' - python scripts/release_signing.py check-env windows - if ($LASTEXITCODE -ne 0) { throw 'Windows signing credentials are incomplete' } - - $certificatePath = Join-Path $env:RUNNER_TEMP 'clearsign-code-signing.pfx' - [IO.File]::WriteAllBytes( - $certificatePath, - [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE) - ) - $password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force - $imported = @(Import-PfxCertificate -FilePath $certificatePath -CertStoreLocation Cert:\CurrentUser\My -Password $password) - $signingCertificates = @($imported | Where-Object { - $_.HasPrivateKey -and - ($_.EnhancedKeyUsageList.ObjectId.Value -contains '1.3.6.1.5.5.7.3.3') - }) - if ($signingCertificates.Count -ne 1) { - throw "Expected one imported certificate with a private key, found $($signingCertificates.Count)" - } - $certificate = $signingCertificates[0] - $expectedThumbprint = ($env:WINDOWS_CERTIFICATE_THUMBPRINT -replace '\s', '').ToUpperInvariant() - if ($certificate.Thumbprint -ne $expectedThumbprint) { - throw 'Imported Windows certificate does not match WINDOWS_CERTIFICATE_THUMBPRINT' - } - if ($certificate.NotAfter -le (Get-Date).ToUniversalTime()) { - throw 'Windows code-signing certificate is expired' - } - if ($certificate.NotBefore -gt (Get-Date).ToUniversalTime()) { - throw 'Windows code-signing certificate is not valid yet' - } - Add-Content $env:GITHUB_ENV "WINDOWS_SIGNING_THUMBPRINT=$($certificate.Thumbprint)" - - $timestampUrl = $env:WINDOWS_TIMESTAMP_URL - if ([string]::IsNullOrWhiteSpace($timestampUrl)) { - $timestampUrl = 'http://timestamp.digicert.com' - } - $config = 'desktop/src-tauri/tauri.windows-signing.generated.json' - python scripts/release_signing.py windows-config $config $certificate.Thumbprint $timestampUrl - if ($LASTEXITCODE -ne 0) { throw 'Could not generate the Windows signing configuration' } - Add-Content $env:GITHUB_ENV "TAURI_SIGNING_CONFIG=src-tauri/tauri.windows-signing.generated.json" - name: Build the application working-directory: desktop shell: bash run: | set -euo pipefail npm ci - if [ -n "${TAURI_SIGNING_CONFIG:-}" ]; then - npm run build -- --config "$TAURI_SIGNING_CONFIG" -- --locked - else - npm run build -- -- --locked - fi - - name: Verify macOS signature, notarization and stapling - if: runner.os == 'macOS' && startsWith(github.ref, 'refs/tags/v') && needs.validate.outputs.release_mode == 'signed' - shell: bash - run: | - set -euo pipefail - apps=(desktop/src-tauri/target/release/bundle/macos/*.app) - images=(desktop/src-tauri/target/release/bundle/dmg/*.dmg) - [ "${#apps[@]}" = 1 ] && [ "${#images[@]}" = 1 ] \ - || { echo "::error::expected one macOS app and one DMG" >&2; exit 1; } - codesign --verify --deep --strict --verbose=2 "${apps[0]}" - codesign --verify --verbose=2 "${images[0]}" - team_id=$(codesign -d --verbose=4 "${apps[0]}" 2>&1 | sed -n 's/^TeamIdentifier=//p') - [ "$team_id" = "$APPLE_TEAM_ID" ] \ - || { echo "::error::macOS app has unexpected TeamIdentifier: $team_id" >&2; exit 1; } - spctl --assess --type execute --verbose=4 "${apps[0]}" - xcrun stapler validate "${apps[0]}" - xcrun stapler validate "${images[0]}" + npm run build -- -- --locked - name: Verify the evaluation macOS artifacts have no publisher identity - if: runner.os == 'macOS' && needs.validate.outputs.release_mode != 'signed' + if: runner.os == 'macOS' shell: bash run: | set -euo pipefail @@ -671,31 +532,8 @@ jobs: fi kill "$app_pid" wait "$app_pid" || true - - name: Verify Windows Authenticode signatures and timestamps - if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v') && needs.validate.outputs.release_mode == 'signed' - shell: pwsh - run: | - $ErrorActionPreference = 'Stop' - $paths = @( - 'desktop/src-tauri/target/release/clearsign-desktop.exe' - ) - $paths += @(Get-ChildItem desktop/src-tauri/target/release/bundle/msi/*.msi | ForEach-Object FullName) - $paths += @(Get-ChildItem desktop/src-tauri/target/release/bundle/nsis/*.exe | ForEach-Object FullName) - if ($paths.Count -ne 3) { throw "Expected three signed Windows files, found $($paths.Count)" } - foreach ($path in $paths) { - $signature = Get-AuthenticodeSignature $path - if ($signature.Status -ne 'Valid') { - throw "Invalid Authenticode signature on ${path}: $($signature.StatusMessage)" - } - if ($signature.SignerCertificate.Thumbprint -ne $env:WINDOWS_SIGNING_THUMBPRINT) { - throw "Unexpected signer certificate on ${path}" - } - if ($null -eq $signature.TimeStamperCertificate) { - throw "Missing trusted timestamp on ${path}" - } - } - name: Verify the evaluation Windows artifacts are unsigned - if: runner.os == 'Windows' && needs.validate.outputs.release_mode != 'signed' + if: runner.os == 'Windows' shell: pwsh run: | $ErrorActionPreference = 'Stop' @@ -735,16 +573,8 @@ jobs: run: | function Assert-ReleaseSignature([string]$Path) { $signature = Get-AuthenticodeSignature $Path - if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_THUMBPRINT)) { - if ($signature.Status -ne 'NotSigned') { - throw "Unsigned evaluation file has unexpected signature status on ${Path}: $($signature.Status)" - } - return - } - if ($signature.Status -ne 'Valid' -or - $signature.SignerCertificate.Thumbprint -ne $env:WINDOWS_SIGNING_THUMBPRINT -or - $null -eq $signature.TimeStamperCertificate) { - throw "Installed file is not validly signed and timestamped: $Path" + if ($signature.Status -ne 'NotSigned') { + throw "Unsigned evaluation file has unexpected signature status on ${Path}: $($signature.Status)" } } cargo install --git https://github.com/chippers/msedgedriver-tool --rev 8c4b34f51b45f5cf08013366d703de464ab871d1 --locked @@ -819,28 +649,6 @@ jobs: cp app/dist/clearsign.html desktop-dist/ fi ls -la desktop-dist - - name: Remove temporary signing credentials - if: always() && runner.os == 'macOS' && startsWith(github.ref, 'refs/tags/v') - shell: bash - run: | - set +e - if [ -n "${CLEARSIGN_PREVIOUS_KEYCHAIN:-}" ]; then - security default-keychain -d user -s "$CLEARSIGN_PREVIOUS_KEYCHAIN" - security list-keychains -d user -s "$CLEARSIGN_PREVIOUS_KEYCHAIN" - fi - if [ -n "${CLEARSIGN_KEYCHAIN_PATH:-}" ]; then - security delete-keychain "$CLEARSIGN_KEYCHAIN_PATH" - fi - rm -f "$RUNNER_TEMP"/clearsign-developer-id.p12 "$RUNNER_TEMP"/AuthKey_*.p8 - - name: Remove imported Windows signing certificate - if: always() && runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/v') - shell: pwsh - run: | - if ($env:WINDOWS_SIGNING_THUMBPRINT) { - Remove-Item "Cert:\CurrentUser\My\$($env:WINDOWS_SIGNING_THUMBPRINT)" -Force -ErrorAction SilentlyContinue - } - Remove-Item "$env:RUNNER_TEMP\clearsign-code-signing.pfx" -Force -ErrorAction SilentlyContinue - Remove-Item 'desktop/src-tauri/tauri.windows-signing.generated.json' -Force -ErrorAction SilentlyContinue - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: success() || failure() with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6093155..800bcd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,40 @@ Security fixes say what an affected version does wrong, not just that something was fixed. A release note that says "various improvements" is a release note that keeps people on a broken version. +## Unreleased + +### Security + +- Release builds no longer receive Apple or Windows publisher credentials. + Signed mode now fails before building until a separately administered signer + exists. Final GitHub publication moved to a no-checkout workflow loaded from + the default branch, with an exact tag/commit check, fixed asset allowlist and + explicit upload list. +- A transaction with calls beyond the display, count or nesting limits is now + display-only. The decoder still reports risks found in hidden calls, but + grouped or generic findings can no longer authorize a signature over actions + the operator could not read. +- The bare-metal demo gives the physical UART only to a trusted display domain; + the untrusted wallet cannot print signer-looking verdicts. The Linux harness + now accepts review text only from the signer and the blocked-write record only + from the VMM. The Linux/VMM build also refuses every board except the tested + QEMU AArch64 configuration; the inherited x86 path passed COM1 directly to the + guest and is not part of the security boundary. +- Raw transactions, RLP item counts and agent proposal JSON now have total + resource limits before expensive parsing or allocation. + +### Verification + +- The 238-transaction live Safe replay still agrees with Safe's hash 238/238. + Current code reports a second, unexecuted legacy-Safe `DELEGATECALL` that the + original historical report treated as authenticated MultiSend; the report now + records that distinction. +- The canonical two-build check now uses two locales that are actually present + in the pinned container. The previous `en_US.UTF-8` setting was not generated + and fell back with a warning, so it did not test the locale variation the log + claimed. Both builds share one frozen snapshot of the Cargo registry while + retaining separate source trees and containers. + ## v0.1.7 — 2026-10-26 - Carries forward the unpublished v0.1.6 candidate after its Windows installer diff --git a/docs/03-verification-status.md b/docs/03-verification-status.md index d7b975f..f67cef5 100644 --- a/docs/03-verification-status.md +++ b/docs/03-verification-status.md @@ -1,6 +1,6 @@ # 03 — Verification status -**Recorded checks, their scope and remaining limitations.** Updated 5 Oct 2026. +**Recorded checks, their scope and remaining limitations.** Updated 26 Oct 2026. The rows below include historical runs and counts, not a fresh certification of the current release. See the generated [support matrix](10-what-is-supported.md) and CI for current workspace results. Reproduction requirements vary by check. @@ -10,7 +10,7 @@ and CI for current workspace results. Reproduction requirements vary by check. | Area | Evidence | Status | |---|---|---| | Decoder correctness | Named invariant tests, and vectors produced by Foundry `cast` rather than by `clearsign` | 32 tests passing | -| How many times this has been reviewed, and by what | **One external security review**, by a person, 17 Sep 2026: ten findings, all closed. Since then, four **AI-assisted review passes** between 27 and 29 Sep 2026, which found and closed a MultiSend classification gap, a fail-open acknowledgement collapse, a saturating authorization identifier, an unbounded parse, a duplicated JSON parser, a broken front end and several overstated claims. Those passes are recorded honestly as what they were: the reviewer stated plainly that they are not a substitute for an independent human security review and could not offer a professional attestation. **The second human review remains unmet**, and nine of the first review's ten fixes — plus everything found since — are still unread by a human outside this project | One human review; the second is a v1 exit criterion and is still open | +| How many times this has been reviewed, and by what | **One external security review**, by a person, 17 Sep 2026: ten findings, all closed. Since then, five **AI-assisted review passes** through 26 Oct 2026 found further parser, acknowledgement, release-pipeline, display-trust and resource-boundary defects. The latest standard pass reported seven findings: two high, two medium and three low. A separate post-fix pass found three residual paths in exact-tag validation, undecoded-call signing authority and an inherited x86 console passthrough; the 26 Oct change closes them. These passes are not a substitute for an independent human security review and are not a professional attestation. **The second human review remains unmet**, and nine of the first review's ten fixes — plus everything found since — are still unread by a human outside this project | One human review; the second is a v1 exit criterion and is still open | | What is actually supported | Generated from the code by `signing-core/scripts/support-matrix.sh` into `10-what-is-supported.md`, which distinguishes listed from tested from proven. The deployment table was documented here as 1,404 address-chain pairs while the code held 2,089 — nobody lied, the table grew and the prose did not, which is the failure mode a hand-written support claim always has | Generated, 29 Sep 2026 | | Safe MultiSend batches | Batch calldata built by `cast`; addresses and the 2,089 address-chain pairs across 561 chain IDs taken from `safe-global/safe-deployments` and re-checked against their published EIP-55 strings. An ordinary batch reads WARNING and shows every inner call; a delegatecall inside a batch, an unpinned batching contract, a published address on an unpublished chain, a truncated batch and an over-long batch all stay CRITICAL or BLIND and undecoded | 14 tests passing | | The batch tests can actually fail | Three bugs planted deliberately: decode any delegatecall target, ignore each element's operation byte, ignore the chain ID. Each was caught by a different test, then reverted | Demonstrated | @@ -31,33 +31,33 @@ and CI for current workspace results. Reproduction requirements vary by check. | QR transport against another implementation | Requests produced by Keystone's `@keystonehq/bc-ur-registry-eth` 0.22.1 (the library MetaMask's air-gapped flow uses) are read single-part, animated, and animated with frames missed and out of order. The `eth-signature` reply this signer emits is decoded back by that same library, and the signature equals `cast wallet sign --no-hash` | 6 tests passing | | The QR tests can actually fail | Three bugs planted: skip the Bytewords CRC-32, accept parts from a different message, ignore the address the request names. The second and third were caught; the first was **not**, which exposed a test that was passing for the wrong reason. The test was rewritten to corrupt a payload byte into a different valid word, and then it caught the bug | Demonstrated, including one test fixed because of it | | What a wallet claims about a request | The chain ID it states is compared with the one in the signed bytes; the wallet it names, by BIP-32 master fingerprint, is compared with this one; the address it expects is compared with the key the path derives. Disagreements refuse. A claim the wallet omits is reported rather than passed over, because a check nobody can fail is not a check. The fingerprint of the test phrase was verified against BIP-32 in plain Python, independently of the crate | 6 tests passing | -| Command-line tool | Tests of the built binary: dev guard, refusals, signature equal to `cast` | 5 tests passing | +| Command-line tool | Tests of the built binary: dev guard, refusals, signature equal to `cast` | 10 tests passing | | Never panics, statically | `clippy` denies indexing, `unwrap`, `expect`, `panic` and unchecked arithmetic in the decoder and key crates; `unsafe` is forbidden | Clean | | Portability | Decoder and key crates build for `aarch64-unknown-none` and `thumbv7em-none-eabihf`, with no operating system | Demonstrated | | Isolation path | Verified Alpine image boots with no network device. The static binary reviews, refuses, then signs, matching `cast` | Demonstrated, development only | -| Authority engine | Tests for information-flow tracing, mislabelled steps, escape-sequence injection, transaction steps reviewed by `clearsign`, exact approval, tampering after approval and fail-closed execution; 20,000 random plans | 18 tests passing | +| Authority engine | Tests for information-flow tracing, mislabelled steps, escape-sequence injection, transaction steps reviewed by `clearsign`, exact approval, tampering after approval and fail-closed execution; 20,000 random plans | 19 tests passing; one diagnostic sample ignored by default | | Plan wire format | A plan proposed by an untrusted planner is decoded on the deciding side. 11 tests: round trip for every action, refusal of trailing bytes, every truncation, an unknown action tag, a four-gigabyte declared length, more steps than the engine accepts, and text that is not UTF-8. One test flips every bit of a real plan and requires each mutation to be refused or to change the fingerprint | 11 tests passing | -| Agent adapter | An agent's tool-call proposal becomes a typed plan or is refused. 10 tests: an unknown tool is refused rather than approximated, nonsense arguments are refused, a step using an unproposed step is refused, an oversized proposal is refused before review, a path containing `..` is outside every allowed root and cannot be classified by where it appears to end up, editing the destination changes the fingerprint an approval binds to, and the runner will not read a file local policy calls secret even after approval | 10 tests passing | +| Agent adapter | An agent's tool-call proposal becomes a typed plan or is refused. Tests cover unknown tools, nonsense arguments, undeclared dependencies, size limits before JSON parsing, path traversal, fingerprint binding, local sensitivity policy and refusal to read secret files | 13 tests passing | | The planner cannot classify its own data | The example proposal claims the notes are `"sensitivity": "public"`. The adapter ignores the claim and classifies by local policy: the notes come out PERSONAL and `/home/user/.config/keys/...` comes out SECRET, which is what makes the egress finding fire | Test: `the_planner_does_not_get_to_say_how_sensitive_data_is` | | Prompt injection blocked across compartments | The Linux compartment writes an assistant's plan into shared memory; the signer compartment decodes it, traces the flow and answers CRITICAL `SECRET_EGRESS`. The verdict is read from a line only the signer can write | Demonstrated (`platform/sel4/linux-signer/run.py`) | -| The console the operator reads | The guest has no serial device. Its writes to the UART trap to the VMM, which relays them a line at a time behind `GUEST\|`, replacing anything not plainly printable, so it cannot move the cursor or clear the screen. There is no receive path, so the compartment cannot be driven from outside: it runs a fixed script and is read, not typed into. The harness asserts the signer's verdict from `SIGNER\|` lines and, separately, that no guest line begins with that prefix | Demonstrated. On hardware this becomes a signer-owned display; the property being tested is the same | +| The console the operator reads | The guest has no serial device. Its writes to the UART trap to the VMM, which relays them a line at a time behind `GUEST\|`, replacing anything not plainly printable, so it cannot move the cursor or clear the screen. There is no receive path, so the compartment cannot be driven from outside: it runs a fixed script and is read, not typed into. The harness accepts review text and the Safe hash only from `SIGNER\|REVIEW\|` lines and accepts the denied review write only from the VMM's exact trusted fault record | Demonstrated. On hardware this becomes a signer-owned display; the property being tested is the same | | Plan decoding robustness | `plan_wire` fuzz target with the canonical-encoding property asserted, seeded with valid plans so the fuzzer starts inside the format: coverage rose from 34 to 1,386 edges once seeded | About 56 million executions, zero failures | | The plan tests can actually fail | Two bugs planted: ignore the step-count limit, and accept a non-canonical encoding. The first was caught. The second was **not** — and 30 million further fuzzing runs with that check disabled found no input where it would fire, because the format's fixed-width integers and length prefixes already make each plan's encoding unique. The check stays as a guard for future fields, and is documented as defensive rather than as something the tests cover | Demonstrated, with one honest gap named | | Authority engine robustness | `cargo-fuzz` target building arbitrary plans; asserts deterministic review, no raw escape characters, exact acknowledgements accepted, approved plans run fully, and a changed plan is refused | About 6.5 million executions, zero failures | | Image build gates can actually fail | Two violations planted in the image build: a shell copied into the root filesystem, and networking re-enabled in the kernel config. Gate 4 caught the first ("the image contains more than the signer: ./bin_sh"), gate 1 the second ("the kernel still has networking"). Both reverted | Demonstrated | -| Signer image reproducibility | Two builds of identical content produce an identical kernel (`2aa6a4b8…`) and an identical initramfs (`ae1ce532…`). This exposed a real defect: GNU cpio's `--reproducible` still recorded each file's mtime, so the same content hashed differently every build. Fixed by zeroing mtimes before archiving, in the signer image and in the Linux guest, whose root filesystem now also reproduces (`fee4afcf…`) | Reproducible on one machine (`platform/signer-image/check-reproducible.sh`) | +| Signer image reproducibility | Two builds of identical content produce an identical kernel (`2aa6a4b8…`) and an identical initramfs (`ffc5f31c…`). This exposed a real defect: GNU cpio's `--reproducible` still recorded each file's mtime, so the same content hashed differently every build. Fixed by zeroing mtimes before archiving, in the signer image and in the Linux guest, whose root filesystem now also reproduces (`fee4afcf…`) | Reproducible on one machine (`platform/signer-image/check-reproducible.sh`) | | Signer-only image | A Linux image containing one program. Five build-time gates refuse a kernel with networking or modules, a dynamically linked signer, more than one regular file, or anything named or containing a shell. Booted in QEMU with no network device: it reads a request produced by another wallet's implementation, decodes the transaction, refuses a key the request did not ask for, and signs with the one it did | Demonstrated (`platform/signer-image/run.py`) | | Shared-memory ordering and TOCTOU | Both compartments now fence on entry and before publishing a review, read shared regions through `volatile` pointers, and copy the request into private memory before decoding. Found by an adversarial pass, not by a failing test: on QEMU the missing barriers are invisible, and the wallet-UI demo had been decoding shared memory in place | Fixed 17 Sep 2026; both compartment runs still pass | -| Arena exhaustion | The bare-metal signer serves requests from a 4 MiB bump arena whose `dealloc` is a no-op, so a request costs the *sum* of its allocations. Measured for the worst cases a guest can send: maximal transaction 1.57 MB, maximal plan 1.51 MB, maximal batch 591 KB, and — added with the carried-call decoders on 5 Oct 2026 — a request-sized multicall of the smallest calls that each raise a finding, 1.41 MB. A budget test fails at half the arena, so a regression is caught here rather than by a protection domain that stops answering | 4 tests passing (`crates/clearsign-ffi/tests/arena_budget.rs`) | +| Arena exhaustion | The bare-metal signer serves requests from a 4 MiB bump arena whose `dealloc` is a no-op, so a request costs the *sum* of its allocations. Measured on 26 Oct for the densest requests a guest can send: maximal malformed transaction 196,512 bytes, maximal plan 1,495,254 bytes, maximal MultiSend 201,287 bytes, maximal multicall 1,838,334 bytes. A budget test fails at half the arena, so a regression is caught here rather than by a protection domain that stops answering | 4 tests passing (`crates/clearsign-ffi/tests/arena_budget.rs`) | | Hostile QR streams | The fountain decoder held mixed parts without limit; a stream of crafted codes could have reached tens of megabytes. Now bounded by the message's fragment count. The first version of the regression test passed against the unbounded code — it was building invalid parts — and was rewritten until it failed against it | 1 test passing, verified against the unfixed code | -| seL4 compartments | Microkit 2.3.0 in QEMU: untrusted wallet UI and bare-metal signer. Signer returns CRITICAL with the `cast`-matching Safe hash; the UI's write to the review region raises a seL4 permission fault | Demonstrated (`platform/sel4/signer-system/run.py`) | -| Linux inside seL4 | libvmm 0.2.0 runs a Linux 7.1 guest beside the signer. A Linux program gets a CRITICAL review over shared memory; Linux then uses kernel privileges to write the review region and seL4 blocks it | Demonstrated (`platform/sel4/linux-signer/run.py`) | +| seL4 compartments | Microkit 2.3.0 in QEMU: untrusted wallet UI, bare-metal signer and signer-owned display path. Only the display domain maps the UART. It reports CRITICAL with the `cast`-matching Safe hash; the UI's write to the review region cannot reach the post-write notification | Demonstrated (`platform/sel4/signer-system/run.py`) | +| Linux inside seL4 | libvmm 0.2.0 runs a Linux 6.18.52 guest beside the signer. A Linux program gets a CRITICAL review over shared memory; Linux then uses kernel privileges to write the review region and seL4 blocks it | Demonstrated (`platform/sel4/linux-signer/run.py`) | | Linux guest from verified source | Kernel 6.18.52 LTS from kernel.org, signature checked against Greg Kroah-Hartman's key from kernel.org's key directory; BusyBox 1.37.0 and glibc 2.41 from apt-verified Debian packages; every seL4 Linux-compartment check passes on it | Demonstrated | | Guest kernel reproducibility | Clean rebuild in a brand-new build volume produced a bit-for-bit identical kernel Image (`d30fb888…f18f`), using fixed build identity and timestamp | Reproducible on one machine (`platform/guest/check-reproducible.sh`) | | Central configuration | Every borrowed component is fetched and verified from `config/pins.env` by `platform/fetch-deps.sh`; `platform/run-all.sh` builds and tests everything | Demonstrated | | GrapheneOS source | Release `2026091000` manifest tag signature verified against a pinned key fingerprint; 1,057 projects pinned to commit hashes | Verified | | GrapheneOS hardened_malloc | Built at the manifest-pinned commit; full upstream suite passes; deployment variant runs as the system allocator in the seL4 Linux compartment and aborts a write-after-free that glibc misses | Demonstrated | -| Reproducible build, canonical environment | Two builds in the pinned Debian image, differing in build path, locale, timezone, umask, hostname and container instance, produce identical `aarch64-unknown-linux-musl` binaries (`66a86f32…` and `c3d968df…`, recorded in `signing-core/EXPECTED-HASHES.txt`). The signer image embeds that exact binary and the build refuses any other | Reproducible in the canonical environment (`scripts/reproducible-cross-check.sh`) | +| Reproducible build, canonical environment | Two builds in the pinned Debian image, differing in build path, locale, timezone, umask, hostname and container instance, produce identical `aarch64-unknown-linux-musl` binaries (`8880d013…` and `907d6b01…`, recorded in `signing-core/EXPECTED-HASHES.txt`). The signer image embeds that exact binary and the build refuses any other | Reproducible in the canonical environment (`scripts/reproducible-cross-check.sh`) | | Reproducibility across compiler hosts | **Not achieved, and measured rather than assumed.** The same source and the same Rust 1.98.1, hosted on macOS instead of Linux, produces identical `.rodata`, `.data`, `.got`, `.gcc_except_table` and `.init_array`, and a different `.text` and `.comment`. rustc makes no cross-host determinism promise, so, as GrapheneOS, Tor Browser and Debian do, the project names one canonical environment instead of claiming more | Documented, with the evidence | | Reproducible on a second machine | **Met, 28 Sep 2026.** `.github/workflows/verify.yml` built twice in the canonical environment on a GitHub `ubuntu-latest` runner — different machine, different architecture underneath, arm64 through QEMU — and the result was byte-identical to the hashes recorded on the maintainer's machine. The job printed `This machine reproduced the recorded binaries exactly` (run 36451047609). The job is written so that agreeing with itself is not enough: it keeps the recorded hashes aside first and compares against them afterwards | v1 exit criterion met | diff --git a/docs/08-what-was-checked.md b/docs/08-what-was-checked.md index 8583713..b02caf6 100644 --- a/docs/08-what-was-checked.md +++ b/docs/08-what-was-checked.md @@ -4,6 +4,31 @@ below records the 27 September pass; later evidence is listed in [03-verification-status.md](03-verification-status.md). +## 26 October security pass + +A later repository-wide standard review found seven additional issues: the +release workflow exposed publisher credentials to tagged build code; publication +authority ran tagged scripts; hidden batch actions could share an +acknowledgement; the untrusted wallet shared the signer's debug console; the +Linux harness accepted guest-authored evidence; proposal JSON was unbounded +before parsing; and the public transaction/RLP parsers lacked total resource +limits. + +The code in this change closes the parser and authorization issues, gives the +physical UART to a separate trusted display domain, makes the Linux harness +accept review and fault evidence only from trusted components, and moves release +publication to a no-checkout default-branch workflow with a fixed asset +allowlist. Repository signing is deliberately disabled until an independently +administered Apple/Windows signing service exists. This was an AI-assisted +standard review, not a second independent human assessment. + +A separate post-fix pass then found three residual paths: an ambiguous Git ref +lookup in the publisher, count and nesting limits that still left a signing +target, and the inherited x86 VMM's direct COM1 passthrough. The publisher now +resolves only `refs/tags/` and peels annotated tags; every undecoded +executable-call limit permanently removes signing authority; and the VMM refuses +to compile for any board except the tested QEMU AArch64 target. + A full pass over everything, run rather than assumed. This is the record of what was exercised and what it found, so the next person does not have to take the word "working" on trust. @@ -12,7 +37,7 @@ word "working" on trust. | | | |---|---| -| Rust workspace | 314 tests pass in release, clippy clean at `-D warnings`, `rustfmt` clean | +| Rust workspace | 319 tests are defined (one diagnostic sample is ignored); the release suite passes, clippy is clean at `-D warnings`, and `rustfmt` is clean | | Bare metal | All four device crates build for `aarch64-unknown-none` and `thumbv7em-none-eabihf` | | Fuzzing | Nine targets are now defined; the seven-target 27 September pass ran ~35 million executions with no failures | | Reproducible build | Two builds in the canonical environment agree; CI reproduces them on other hardware | @@ -30,10 +55,12 @@ with the key the request names and that the signature equals Foundry's. ## What the pass found -Nothing wrong with the product. Four things wrong with the scaffolding around it, -which is its own kind of finding: three of them were tests asserting on text that -had been deliberately changed, and they would have gone on passing if the text -had never been checked against reality. +The September pass found no decoder defect. It found four problems in the +scaffolding around it: three were tests asserting on text that had been +deliberately changed, and they would have gone on passing if the text had never +been checked against reality. The later October review above did find security +defects, so this section should be read as the historical result of that earlier +pass, not a claim about all subsequent work. **The signer image refused to build, correctly.** The gate that requires the binary inside the image to match `EXPECTED-HASHES.txt` stopped the build, because diff --git a/docs/09-against-real-transactions.md b/docs/09-against-real-transactions.md index 4f1ab59..d67352a 100644 --- a/docs/09-against-real-transactions.md +++ b/docs/09-against-real-transactions.md @@ -6,6 +6,20 @@ > transaction's own CRITICAL is now numbered first, ahead of the chain-binding > notice. This file records the first run as it happened. +> **26 Oct 2026 rerun:** hashes still agree **238/238**. The current decoder +> reports two top-level `DELEGATECALL` transactions, not one. The Bybit +> transaction (`0xb3476d06…5ba004f8`) was executed. The other +> (`0xeda7646b…030ced2d`) is queued and unexecuted on another v1.1.x Safe; its +> target is a published MultiSend address, but the legacy Safe signature does +> not bind a chain ID, so current code refuses to authenticate that deployment +> from caller-supplied chain metadata and reports both `SAFE_DELEGATECALL` and +> `MULTISEND_WRONG_CHAIN`. The older “one DELEGATECALL” statement below is kept +> only as the result recorded by the original decoder, not as a current claim. +> Current outcomes are 8 exit-0, 44 BLIND and 186 CRITICAL. Critical codes are +> 176 `SIGNATURE_NOT_CHAIN_BOUND`, 24 `SAFE_OWNER_CHANGE`, two +> `SAFE_DELEGATECALL`, one `SAFE_THRESHOLD_CHANGE` and one +> `MULTISEND_WRONG_CHAIN` (a transaction may contribute more than one code). + **Run 28 September 2026.** Reproduce with: ```sh @@ -67,7 +81,8 @@ CRITICAL findings by code: | `SAFE_FALLBACK_HANDLER_CHANGE` | 1 | | `SAFE_DELEGATECALL` | 1 | -**One DELEGATECALL in 238 transactions, and it is the Bybit one** — +**Historical 28 September result: one DELEGATECALL in 238 transactions, and it +is the Bybit one** — `0xb3476d06…5ba004f8`, nonce 71. The other 73 transactions on that same Safe are ordinary and the reviewer treats them as ordinary. It did not find the attack because it was pointed at it; it found it because it was the only one of diff --git a/docs/15-release-signing.md b/docs/15-release-signing.md index 0e3fd0a..328cf01 100644 --- a/docs/15-release-signing.md +++ b/docs/15-release-signing.md @@ -5,8 +5,14 @@ that already-published downloads were retroactively signed. ClearSign v0.1.2 is unsigned. The protected v0.1.3 tag produced no release because its macOS evaluation gate rejected an identity-free ad-hoc linker signature. v0.1.4 was published as an unsigned evaluation release, and v0.1.7 is configured the same -way. Every future release tag requires an explicit mode; an unset or unknown -mode fails before packaging begins. +way. + +The former signed path imported publisher credentials before repository build +code ran. That is not an acceptable signing boundary. The repository workflow +therefore no longer consumes Apple or Windows private credentials, and +`CLEARSIGN_RELEASE_MODE=signed` fails during validation. A stable release remains +blocked until a separately administered signer can accept only immutable, +digest-verified artifacts. This is a security gate, not a missing CI toggle. ## What the workflow enforces @@ -14,10 +20,8 @@ Branch and manual builds remain without publisher identities. They exercise packaging, installation and smoke tests without consuming notarization requests or exposing signing credentials to ordinary development builds. -Set the public repository variable `CLEARSIGN_RELEASE_MODE` to exactly one of: +The current repository workflow accepts: -- `signed`: a normal stable release. The macOS and Windows credential and - verification gates below are mandatory. - `unsigned-evaluation`: a temporary evaluation release. Platform signing and notarization credentials are not loaded. Windows artifacts and the macOS DMG must be unsigned. The macOS application may carry only an identity-free ad-hoc @@ -25,6 +29,12 @@ Set the public repository variable `CLEARSIGN_RELEASE_MODE` to exactly one of: and no stapled ticket. `BUILD-METADATA.json` records the mode, the release notes begin with an explicit warning, and GitHub publishes it as a prerelease. +`signed` remains a recognized policy value so external release tooling and +metadata can describe a properly signed candidate, but this workflow rejects it. +The future signer must be protected independently from the tagged source and +must not run package managers, build scripts, newly built applications or +downloaded test tools while publisher authority is available. + The validation job normalizes and snapshots this mode plus the public Apple Team ID and Windows certificate thumbprint. Every later build, metadata and publish decision uses those job outputs, so changing a repository variable during a run @@ -32,14 +42,8 @@ cannot make its artifacts and recorded policy disagree. For a `v*` tag, `.github/workflows/release.yml` then fails closed: -- macOS requires a Developer ID Application certificate and App Store Connect - API key. The workflow imports them into a temporary keychain, asks Tauri to - sign and notarize, then runs `codesign`, Gatekeeper assessment and `stapler - validate` against the application and DMG. -- Windows requires a publicly trusted Authenticode certificate in a PFX file. - Tauri signs the executable, MSI, NSIS installer and NSIS uninstaller. The - workflow rejects any missing, invalid, unexpectedly signed or un-timestamped - file, including the executable after each installer has placed it on disk. +- the workflow rejects signed mode before any build starts; +- macOS and Windows outputs must prove they contain no publisher identity; - Linux `.deb` and AppImage downloads have no universal publisher-signature mechanism in this distribution channel. They are covered by `SHA256SUMS`, the release metadata and GitHub's OIDC-backed build-provenance attestation. @@ -51,9 +55,13 @@ For a `v*` tag, `.github/workflows/release.yml` then fails closed: Published vulnerabilities and RustSec `unsound` warnings block the build; unmaintained and yanked transitive packages remain visible as warnings. -The workflow deletes imported signing material before uploading artifacts. The -runners are ephemeral, but cleanup is still explicit. GitHub Actions secrets are -available only to the native platform step that needs them. +The workflow contains no Apple or Windows publisher-secret references. Release +notes are prepared without repository write authority. The final publication +step receives `GH_TOKEN`, accepts only a fixed filename allowlist, confirms the +tag resolves to the candidate commit, verifies the exact `SHA256SUMS` contents +and every listed asset with system tools, and only then uploads those explicit +files. It uses the publisher workflow from the protected default branch and +never checks out or executes code from the release tag. ## Accounts and credentials @@ -70,7 +78,8 @@ Create a **team** App Store Connect API key with Developer access. Individual AP keys cannot use `notarytool`. Download the `.p8` file once and retain an offline backup; Apple does not offer the private key for download again. -Configure these GitHub Actions repository secrets: +Do not configure these as repository secrets. The separate signing service will +need equivalent inputs in its own administrative boundary: | Secret | Value | | --- | --- | @@ -80,9 +89,8 @@ Configure these GitHub Actions repository secrets: | `APPLE_API_KEY` | App Store Connect API key ID | | `APPLE_API_KEY_P8_BASE64` | Base64 of the downloaded `.p8` private key | -Configure the public repository variable `APPLE_TEAM_ID` with the ten-character -Developer Team ID. The workflow compares it with the signed application's -`TeamIdentifier`, and the release metadata records it. +The public `APPLE_TEAM_ID` value may remain in repository settings for metadata +and verification, but it grants no signing authority. The certificate and API key are separate credentials. Revoke and replace either immediately if its secret is exposed. Track their expiry or revocation outside @@ -90,15 +98,16 @@ the repository and test a manual workflow build before creating a release tag. ### Windows -The implemented backend accepts a password-protected PFX containing one -publicly trusted code-signing certificate and private key. Configure: +The retired in-workflow backend accepted a password-protected PFX. Do not put a +PFX or its password in repository secrets. Prefer a non-exportable HSM or managed +signing service, and keep these values in that service's boundary: | Secret or variable | Value | | --- | --- | | `WINDOWS_CERTIFICATE` | Base64 of the PFX file | | `WINDOWS_CERTIFICATE_PASSWORD` | PFX export password | -| `WINDOWS_CERTIFICATE_THUMBPRINT` | Repository variable containing the expected 40-hex SHA-1 certificate thumbprint | -| `WINDOWS_TIMESTAMP_URL` | Optional repository variable; defaults to DigiCert's RFC 3161 service | +| `WINDOWS_CERTIFICATE_THUMBPRINT` | Public expected 40-hex SHA-1 certificate thumbprint | +| `WINDOWS_TIMESTAMP_URL` | Approved RFC 3161 timestamp service | Do not use a self-signed certificate for public downloads. Microsoft recommends Artifact Signing for eligible publishers; its entry plan is currently about @@ -141,24 +150,22 @@ that the program is safe. 1. Confirm all product versions and the dated changelog agree. Refresh both the canonical binary hashes and signer-image hashes before tagging. The tag workflow rebuilds each twice and refuses stale records. -2. Set `CLEARSIGN_RELEASE_MODE` through a reviewed repository-settings change. - Use `unsigned-evaluation` for the next evaluation tag or `signed` for a stable - release. Never use an approximate or third value. -3. For `signed`, confirm the Apple membership, certificate and API key are active. - Confirm the Windows certificate is active and the timestamp service is reachable. - For `unsigned-evaluation`, confirm the release is intended only for evaluation - and that public copy does not imply platform publisher authentication. +2. Keep `CLEARSIGN_RELEASE_MODE=unsigned-evaluation` for repository-produced + tags. Setting it to `signed` must fail validation. +3. Do not create a stable tag until the separate signing system exists and has + been exercised with non-production test identities. Confirm the Apple + membership, certificate and notarization key and the Windows signing service + only inside that isolated system. 4. Run the release workflow manually from the intended commit. This produces a candidate without publisher identities and tests installers; it does not prove tag signing. 5. Create the tag only from the reviewed commit. Never move or reuse a release tag. -6. Require complete-candidate validation and provenance attestation to pass. In - `signed` mode, also require signing, notarization and Authenticode checks. In - `unsigned-evaluation` mode, require the explicit no-publisher-identity checks and - confirm the GitHub release is marked prerelease. +6. Require complete-candidate validation, provenance attestation and explicit + no-publisher-identity checks to pass, and confirm the GitHub release is marked + prerelease. 7. Download the published assets onto clean macOS, Windows and Linux machines. Verify the mode, checksums, provenance, installation, displayed version and - historical transaction fixture. For `signed`, also verify platform signatures. + historical transaction fixture. 8. Preserve the workflow run URL, source commit, `SHA256SUMS`, metadata, SBOM and notarization/signing evidence in the release record. Do not replace assets in place; issue a new version if any published byte must change. diff --git a/platform/README.md b/platform/README.md index 2198ea6..c9448af 100644 --- a/platform/README.md +++ b/platform/README.md @@ -16,7 +16,10 @@ The layers of `docs/04-platform-architecture.md`, built from borrowed, verified ```sh ./sel4/signer-system/build.sh && python3 sel4/signer-system/run.py ``` -seL4 with two compartments. An untrusted wallet UI submits a Safe transaction; the signer reviews it on bare metal; the UI's attempt to overwrite the verdict is stopped by seL4. +seL4 with three compartments. An untrusted wallet UI submits a Safe transaction; +the signer reviews it on bare metal, and a separate trusted display domain is the +only component that maps the physical UART. The UI's attempt to overwrite the +verdict is stopped by seL4. Everything at once, from pinned and verified sources: @@ -32,6 +35,11 @@ Or individually: ./grapheneos/build-hardened-malloc.sh # needs Docker ./sel4/linux-signer/build.sh && python3 sel4/linux-signer/run.py ``` + +The hardened Linux/VMM integration currently supports only the +`qemu_virt_aarch64` board. Other upstream libvmm example targets are rejected at +compile time because their device-passthrough paths have not been brought inside +ClearSign's trusted-display boundary. seL4 running a Linux guest and the signer side by side. Inside Linux, GrapheneOS's hardened_malloc is the system allocator and stops a write-after-free that glibc's allocator misses. A Linux program asks the signer compartment for a review over shared memory and a doorbell; the verdict comes back CRITICAL with the correct Safe hash. Linux then uses its own kernel privileges to try to overwrite the review, and seL4 blocks the write. ## The signer-only image diff --git a/platform/sel4/linux-signer/build.sh b/platform/sel4/linux-signer/build.sh index 43b5034..9a96cca 100755 --- a/platform/sel4/linux-signer/build.sh +++ b/platform/sel4/linux-signer/build.sh @@ -36,19 +36,23 @@ mkdir -p "$STAGE/usr/bin" && cp "$HERE/guest/signer-request" "$STAGE/usr/bin/" & cp "$HERE/guest/compartment-demo.sh" "$STAGE/usr/bin/compartment-demo" && chmod 0755 "$STAGE/usr/bin/compartment-demo" # GrapheneOS hardened_malloc as the system-wide allocator (built by platform/grapheneos/build-hardened-malloc.sh) GOS="$HERE/../../grapheneos/out" -if [[ -f "$GOS/libhardened_malloc.so" ]]; then - mkdir -p "$STAGE/usr/lib" "$STAGE/etc" - # Safety gate: a globally preloaded library with an unsatisfied dependency kills init. - NEEDED=$(/opt/homebrew/opt/llvm/bin/llvm-readelf -d "$GOS/libhardened_malloc.so" | grep NEEDED | grep -oE '\[[^]]+\]' | tr -d '[]') - for lib in $NEEDED; do - case "$lib" in libc.so.6|ld-linux-aarch64.so.1) ;; *) echo "refusing to preload: guest lacks $lib" >&2; exit 1 ;; esac - done - cp "$GOS/libhardened_malloc.so" "$STAGE/usr/lib/" && chmod 0755 "$STAGE/usr/lib/libhardened_malloc.so" - echo /usr/lib/libhardened_malloc.so > "$STAGE/etc/ld.so.preload" - cp "$GOS/write_after_free" "$GOS/write_after_free_static" "$STAGE/usr/bin/" - chmod 0755 "$STAGE/usr/bin/write_after_free" "$STAGE/usr/bin/write_after_free_static" - echo "including GrapheneOS hardened_malloc" -fi +for artifact in libhardened_malloc.so write_after_free write_after_free_static; do + [[ -f "$GOS/$artifact" ]] || { + echo "missing GrapheneOS allocator artifact $GOS/$artifact; run platform/grapheneos/build-hardened-malloc.sh" >&2 + exit 1 + } +done +mkdir -p "$STAGE/usr/lib" "$STAGE/etc" +# Safety gate: a globally preloaded library with an unsatisfied dependency kills init. +NEEDED=$(/opt/homebrew/opt/llvm/bin/llvm-readelf -d "$GOS/libhardened_malloc.so" | grep NEEDED | grep -oE '\[[^]]+\]' | tr -d '[]') +for lib in $NEEDED; do + case "$lib" in libc.so.6|ld-linux-aarch64.so.1) ;; *) echo "refusing to preload: guest lacks $lib" >&2; exit 1 ;; esac +done +cp "$GOS/libhardened_malloc.so" "$STAGE/usr/lib/" && chmod 0755 "$STAGE/usr/lib/libhardened_malloc.so" +echo /usr/lib/libhardened_malloc.so > "$STAGE/etc/ld.so.preload" +cp "$GOS/write_after_free" "$GOS/write_after_free_static" "$STAGE/usr/bin/" +chmod 0755 "$STAGE/usr/bin/write_after_free" "$STAGE/usr/bin/write_after_free_static" +echo "including GrapheneOS hardened_malloc" ( cd "$STAGE" && find usr etc 2>/dev/null | LC_ALL=C sort | cpio -o -H newc -R 0:0 2>/dev/null | gzip -9n > "$BUILD/overlay.cpio.gz" ) # 3. Base guest images. diff --git a/platform/sel4/linux-signer/integrated.system b/platform/sel4/linux-signer/integrated.system index e83be3c..54bf276 100644 --- a/platform/sel4/linux-signer/integrated.system +++ b/platform/sel4/linux-signer/integrated.system @@ -1,6 +1,6 @@ + @@ -21,8 +22,19 @@ + + + + + + + + + + + diff --git a/platform/sel4/signer-system/wallet_ui.c b/platform/sel4/signer-system/wallet_ui.c index f1e4757..c28d2d2 100644 --- a/platform/sel4/signer-system/wallet_ui.c +++ b/platform/sel4/signer-system/wallet_ui.c @@ -25,7 +25,6 @@ void init(void) for (uint32_t i = 0; i < len; i++) { req[REQ_HEADER + i] = REQUEST_BYBIT_PATTERN[i]; } - microkit_dbg_puts("WALLET_UI|submitted a Safe transaction for review (the UI calls it a token transfer)\n"); microkit_notify(CH_SIGNER); } @@ -34,22 +33,14 @@ void notified(microkit_channel ch) if (ch != CH_SIGNER) { return; } + __atomic_thread_fence(__ATOMIC_ACQUIRE); const uint8_t *rev = (const uint8_t *)review_vaddr; if (rev[0] != 'R') { - microkit_dbg_puts("WALLET_UI|signer refused the request\n"); return; } - if (rev[1] <= 2) { - microkit_dbg_puts("WALLET_UI|signer verdict received: reviewed, no blocking finding\n"); - } else if (rev[1] == 3) { - microkit_dbg_puts("WALLET_UI|signer verdict received: BLIND; approval blocked\n"); - } else if (rev[1] == 4) { - microkit_dbg_puts("WALLET_UI|signer verdict received: CRITICAL\n"); - } else { - microkit_dbg_puts("WALLET_UI|signer verdict received: invalid severity; approval blocked\n"); - } - - microkit_dbg_puts("WALLET_UI|attempting to overwrite the signer's verdict with 'no risk'...\n"); ((uint8_t *)review_vaddr)[1] = 0; /* review region is mapped read-only here: seL4 must fault */ - microkit_dbg_puts("WALLET_UI|ISOLATION FAILURE: write succeeded\n"); + /* Reaching this notification means the forbidden write was not blocked. + * The trusted signer, not this untrusted domain, reports the failure. */ + __atomic_thread_fence(__ATOMIC_RELEASE); + microkit_notify(CH_SIGNER); } diff --git a/platform/signer-image/EXPECTED-HASHES.txt b/platform/signer-image/EXPECTED-HASHES.txt index 572d79d..8421897 100644 --- a/platform/signer-image/EXPECTED-HASHES.txt +++ b/platform/signer-image/EXPECTED-HASHES.txt @@ -7,4 +7,4 @@ # binary recorded in signing-core/EXPECTED-HASHES.txt, and the build refuses to # package any other. 2aa6a4b8a0bedd695c4d331f7c824c96433290598869ec9a91720dd065c11a13 Image -b24bfacf57ec6202d7b91ac80102a18fa35b15fa108e88b89a7b15221fa1c474 initramfs.cpio.gz +ffc5f31cc475c85ad29aaa92091c7a1468b1c1a710fdf9c04e1a0b0ce216e4a1 initramfs.cpio.gz diff --git a/scripts/release_signing.py b/scripts/release_signing.py index 64e7ff8..85ba85e 100644 --- a/scripts/release_signing.py +++ b/scripts/release_signing.py @@ -1,34 +1,11 @@ -"""Fail-closed helpers for release signing configuration. +"""Classify identity-free macOS signatures for evaluation builds. -This module validates credential presence without printing credential values and -creates the temporary Tauri override used for Windows Authenticode signing. +Publisher credentials deliberately have no helper or execution path in this +repository. Stable signing belongs in a separately administered service. """ -import json -import os from pathlib import Path -import re import sys -from urllib.parse import urlsplit - - -REQUIRED_CREDENTIALS = { - "macos": ( - "APPLE_CERTIFICATE", - "APPLE_CERTIFICATE_PASSWORD", - "APPLE_API_ISSUER", - "APPLE_API_KEY", - "APPLE_API_KEY_P8_BASE64", - "APPLE_TEAM_ID", - ), - "windows": ( - "WINDOWS_CERTIFICATE", - "WINDOWS_CERTIFICATE_PASSWORD", - "WINDOWS_CERTIFICATE_THUMBPRINT", - ), -} - -DEFAULT_WINDOWS_TIMESTAMP_URL = "http://timestamp.digicert.com" def require_macos_adhoc_signature(details): @@ -53,65 +30,14 @@ def require_macos_adhoc_signature(details): raise ValueError("macOS evaluation app contains a signing authority") -def require_credentials(platform, environ=None): - environ = os.environ if environ is None else environ - try: - required = REQUIRED_CREDENTIALS[platform] - except KeyError as error: - raise ValueError(f"unsupported signing platform: {platform}") from error - missing = [name for name in required if not environ.get(name, "").strip()] - if missing: - raise ValueError( - f"{platform} release signing credentials are incomplete: " - + ", ".join(missing) - ) - - -def windows_config(thumbprint, timestamp_url=DEFAULT_WINDOWS_TIMESTAMP_URL): - compact_thumbprint = re.sub(r"\s", "", thumbprint).upper() - if not re.fullmatch(r"[0-9A-F]{40}", compact_thumbprint): - raise ValueError("Windows certificate thumbprint must be 40 hexadecimal characters") - parsed = urlsplit(timestamp_url) - if parsed.scheme not in {"http", "https"} or not parsed.netloc: - raise ValueError("Windows timestamp URL must be an absolute HTTP(S) URL") - if parsed.username or parsed.password or parsed.fragment: - raise ValueError("Windows timestamp URL must not contain credentials or a fragment") - return { - "bundle": { - "windows": { - "certificateThumbprint": compact_thumbprint, - "digestAlgorithm": "sha256", - "timestampUrl": timestamp_url, - } - } - } - - -def write_windows_config(path, thumbprint, timestamp_url): - output = Path(path) - output.write_text( - json.dumps(windows_config(thumbprint, timestamp_url), indent=2) + "\n", - encoding="utf-8", - ) - - def main(argv): - if len(argv) == 2 and argv[0] == "check-env": - require_credentials(argv[1]) - return - if len(argv) in {3, 4} and argv[0] == "windows-config": - timestamp_url = argv[3] if len(argv) == 4 else DEFAULT_WINDOWS_TIMESTAMP_URL - write_windows_config(argv[1], argv[2], timestamp_url) - return if len(argv) == 2 and argv[0] == "check-macos-adhoc": require_macos_adhoc_signature( Path(argv[1]).read_text(encoding="utf-8", errors="replace") ) return raise SystemExit( - "usage: release_signing.py check-env PLATFORM | " - "windows-config OUTPUT THUMBPRINT [TIMESTAMP_URL] | " - "check-macos-adhoc DETAILS_FILE" + "usage: release_signing.py check-macos-adhoc DETAILS_FILE" ) diff --git a/scripts/test_platform_security.py b/scripts/test_platform_security.py new file mode 100644 index 0000000..250b6bf --- /dev/null +++ b/scripts/test_platform_security.py @@ -0,0 +1,76 @@ +from pathlib import Path +import unittest +import xml.etree.ElementTree as ET + + +ROOT = Path(__file__).resolve().parents[1] + + +class PlatformSecurityTests(unittest.TestCase): + def test_only_trusted_display_maps_the_signer_system_uart(self): + system = ET.parse( + ROOT / "platform/sel4/signer-system/signer.system" + ).getroot() + owners = [] + for domain in system.findall("protection_domain"): + for mapping in domain.findall("map"): + if mapping.get("mr") == "serial": + owners.append(domain.get("name")) + self.assertEqual(owners, ["display"]) + + display_peers = [] + for channel in system.findall("channel"): + ends = [(end.get("pd"), end.get("id")) for end in channel.findall("end")] + if any(pd == "display" for pd, _ in ends): + display_peers.extend(pd for pd, _ in ends if pd != "display") + self.assertEqual(display_peers, ["signer"]) + + wallet = ( + ROOT / "platform/sel4/signer-system/wallet_ui.c" + ).read_text(encoding="utf-8") + self.assertNotIn("microkit_dbg_puts", wallet) + self.assertNotIn("CH_DISPLAY", wallet) + build = ( + ROOT / "platform/sel4/signer-system/build.sh" + ).read_text(encoding="utf-8") + self.assertIn('CONFIG="${CONFIG:-release}"', build) + + def test_linux_security_oracle_uses_trusted_producers(self): + signer = ( + ROOT / "platform/sel4/linux-signer/signer.c" + ).read_text(encoding="utf-8") + vmm = ( + ROOT / "platform/sel4/linux-signer/vmm.c" + ).read_text(encoding="utf-8") + harness = ( + ROOT / "platform/sel4/linux-signer/run.py" + ).read_text(encoding="utf-8") + self.assertIn("SIGNER|REVIEW|", signer) + self.assertIn("VMM|blocked guest write to signer review region", vmm) + self.assertIn("#if !defined(BOARD_qemu_virt_aarch64)", vmm) + self.assertIn( + '#error "ClearSign Linux signer supports only BOARD_qemu_virt_aarch64"', + vmm, + ) + self.assertNotIn("microkit_vcpu_x86_enable_ioport", vmm) + self.assertNotIn("COM1_IO_PORT", vmm) + self.assertIn("EXPECTED_HASH in trusted_review", harness) + self.assertNotIn("EXPECTED_HASH in text", harness) + self.assertIn( + '"VMM|blocked guest write to signer review region" in text', + harness, + ) + + def test_linux_build_requires_hardened_allocator_outputs(self): + build = ( + ROOT / "platform/sel4/linux-signer/build.sh" + ).read_text(encoding="utf-8") + self.assertIn( + "for artifact in libhardened_malloc.so write_after_free write_after_free_static", + build, + ) + self.assertNotIn('if [[ -f "$GOS/libhardened_malloc.so" ]]', build) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_release_signing.py b/scripts/test_release_signing.py index c101a08..9ef4a18 100644 --- a/scripts/test_release_signing.py +++ b/scripts/test_release_signing.py @@ -1,15 +1,7 @@ -import json from pathlib import Path -import tempfile import unittest -from release_signing import ( - DEFAULT_WINDOWS_TIMESTAMP_URL, - require_macos_adhoc_signature, - require_credentials, - windows_config, - write_windows_config, -) +from release_signing import require_macos_adhoc_signature class ReleaseSigningTests(unittest.TestCase): @@ -17,15 +9,35 @@ def test_unsigned_platform_checks_run_on_branch_candidates(self): workflow = ( Path(__file__).resolve().parents[1] / ".github/workflows/release.yml" ).read_text(encoding="utf-8") - self.assertIn( - "runner.os == 'macOS' && needs.validate.outputs.release_mode != 'signed'", - workflow, - ) - self.assertIn( - "runner.os == 'Windows' && needs.validate.outputs.release_mode != 'signed'", - workflow, - ) + self.assertIn("if: runner.os == 'macOS'", workflow) + self.assertIn("if: runner.os == 'Windows'", workflow) self.assertIn("check-macos-adhoc", workflow) + self.assertIn("signed releases are disabled in this repository workflow", workflow) + self.assertNotIn("secrets.APPLE_", workflow) + self.assertNotIn("secrets.WINDOWS_", workflow) + + def test_publication_token_is_confined_to_the_final_upload_step(self): + candidate_workflow = ( + Path(__file__).resolve().parents[1] / ".github/workflows/release.yml" + ).read_text(encoding="utf-8") + publisher = ( + Path(__file__).resolve().parents[1] + / ".github/workflows/publish-release.yml" + ).read_text(encoding="utf-8") + self.assertNotIn("GH_TOKEN:", candidate_workflow) + self.assertEqual(publisher.count("GH_TOKEN:"), 1) + self.assertNotIn("actions/checkout", publisher) + self.assertNotIn("scripts/", publisher) + self.assertIn("workflow_run:", publisher) + self.assertIn("github.event.workflow_run.event == 'push'", publisher) + self.assertIn('git/ref/tags/$tag"', publisher) + self.assertIn('git/tags/$object_sha"', publisher) + self.assertIn('[ "$object_type" = commit ]', publisher) + self.assertIn('[ "$object_sha" = "$commit" ]', publisher) + self.assertNotIn('repos/${GITHUB_REPOSITORY}/commits/$tag', publisher) + self.assertIn('[ "$tag" = "$EXPECTED_TAG" ]', publisher) + self.assertIn("/usr/bin/diff -u /tmp/expected-files /tmp/actual-files", publisher) + self.assertIn('"${assets[@]}"', publisher) def test_macos_evaluation_accepts_only_identity_free_adhoc_signatures(self): require_macos_adhoc_signature( @@ -52,57 +64,5 @@ def test_macos_evaluation_accepts_only_identity_free_adhoc_signatures(self): "Signature=adhoc\nTeamIdentifier=not set\nAuthority=Unexpected\n" ) - def test_each_platform_requires_its_complete_secret_set(self): - with self.assertRaisesRegex(ValueError, "APPLE_API_KEY_P8_BASE64"): - require_credentials("macos", { - "APPLE_CERTIFICATE": "certificate", - "APPLE_CERTIFICATE_PASSWORD": "password", - "APPLE_API_ISSUER": "issuer", - "APPLE_API_KEY": "key", - "APPLE_TEAM_ID": "team", - }) - with self.assertRaisesRegex(ValueError, "WINDOWS_CERTIFICATE_PASSWORD"): - require_credentials("windows", {"WINDOWS_CERTIFICATE": "certificate"}) - - def test_complete_credentials_are_accepted_without_returning_them(self): - credentials = { - "WINDOWS_CERTIFICATE": "private material", - "WINDOWS_CERTIFICATE_PASSWORD": "secret", - "WINDOWS_CERTIFICATE_THUMBPRINT": "a" * 40, - } - self.assertIsNone(require_credentials("windows", credentials)) - - def test_windows_config_is_narrow_and_normalizes_thumbprint(self): - config = windows_config("aa bb cc dd ee ff 00 11 22 33 44 55 66 77 88 99 aa bb cc dd") - self.assertEqual(config, { - "bundle": { - "windows": { - "certificateThumbprint": "AABBCCDDEEFF00112233445566778899AABBCCDD", - "digestAlgorithm": "sha256", - "timestampUrl": DEFAULT_WINDOWS_TIMESTAMP_URL, - } - } - }) - - def test_bad_thumbprints_and_timestamp_urls_are_refused(self): - for thumbprint in ["", "ab", "g" * 40, "a" * 41]: - with self.subTest(thumbprint=thumbprint): - with self.assertRaisesRegex(ValueError, "thumbprint"): - windows_config(thumbprint) - for url in ["", "timestamp.example", "file:///tmp/server", "https://u:p@example.test"]: - with self.subTest(url=url): - with self.assertRaisesRegex(ValueError, "timestamp URL"): - windows_config("a" * 40, url) - - def test_generated_config_is_valid_json(self): - with tempfile.TemporaryDirectory() as tmp: - path = Path(tmp) / "tauri-signing.json" - write_windows_config(path, "a" * 40, "https://timestamp.example.test") - self.assertEqual( - json.loads(path.read_text())["bundle"]["windows"]["digestAlgorithm"], - "sha256", - ) - - if __name__ == "__main__": unittest.main() diff --git a/signing-core/EXPECTED-HASHES.txt b/signing-core/EXPECTED-HASHES.txt index 4c4ed5b..ef10a92 100644 --- a/signing-core/EXPECTED-HASHES.txt +++ b/signing-core/EXPECTED-HASHES.txt @@ -1,11 +1,11 @@ # The signer binaries, as built in the canonical environment. # Reproduce with signing-core/scripts/reproducible-cross-check.sh. # Environment: debian:trixie@sha256:f324c7ff54321e8d9c588493a20244965938ce0aa50bbd1022d38010e9ffc4b1, Rust 1.98.1, target aarch64-unknown-linux-musl. -# The compile is offline, from a copied registry cache. The container's -# own bootstrap is not: it apt-gets curl, gcc and libc6-dev, and downloads +# The compile is offline, from a frozen registry-cache snapshot. The container's +# own bootstrap is not: it installs bootstrap packages with apt and downloads # rustup. Those are not pinned by digest, so the environment is named and # reproducible in practice rather than sealed. # Anything built elsewhere, including on the maintainer's own machine, may # differ: rustc does not promise identical output across compiler hosts. -65831267d31f804853d267c3640b686abd54e1cbeac1199a3bc97cf2f54215ce clearsign -c49256d64f8e116297e973cf944bf3bb0f56ab1c14fdff574fe258c6bf06adb5 clearsign-device +8880d0135c1f886a3fd24cd8d14a9b3b7bf6661255e53d7f32dd105aa9a462e9 clearsign +907d6b013d6f279d8a6ee41c51e5490b9ec72e81f36adb3c4392c9989f09ef62 clearsign-device diff --git a/signing-core/crates/authority-agent/src/lib.rs b/signing-core/crates/authority-agent/src/lib.rs index 16ba770..a1303c3 100644 --- a/signing-core/crates/authority-agent/src/lib.rs +++ b/signing-core/crates/authority-agent/src/lib.rs @@ -24,4 +24,4 @@ pub mod tools; pub use policy::{PathPolicy, Policy, Resolved, sensitivity_for_path}; pub use runner::{LocalRunner, RunnerLimits}; -pub use tools::{AdapterError, plan_from_json}; +pub use tools::{AdapterError, MAX_PROPOSAL_BYTES, plan_from_json}; diff --git a/signing-core/crates/authority-agent/src/main.rs b/signing-core/crates/authority-agent/src/main.rs index 7332864..2111a02 100644 --- a/signing-core/crates/authority-agent/src/main.rs +++ b/signing-core/crates/authority-agent/src/main.rs @@ -11,10 +11,11 @@ //! | 2 | the review contains a BLIND finding | //! | 3 | the review contains a CRITICAL finding | +use std::io::Read; use std::process::ExitCode; use authority::{approve_plan, execute, fingerprint, review_plan}; -use authority_agent::{LocalRunner, Policy, RunnerLimits, plan_from_json}; +use authority_agent::{LocalRunner, MAX_PROPOSAL_BYTES, Policy, RunnerLimits, plan_from_json}; use clearsign::Severity; const USAGE: &str = "\ @@ -76,10 +77,20 @@ fn run_command(args: &[String], execute_it: bool) -> ExitCode { let Some(path) = path else { return fail("name the proposal file to read"); }; - let json = match std::fs::read_to_string(&path) { - Ok(text) => text, + let file = match std::fs::File::open(&path) { + Ok(file) => file, Err(e) => return fail(&format!("cannot read {path}: {e}")), }; + let limit = u64::try_from(MAX_PROPOSAL_BYTES) + .unwrap_or(u64::MAX) + .saturating_add(1); + let mut json = String::new(); + if let Err(e) = file.take(limit).read_to_string(&mut json) { + return fail(&format!("cannot read {path}: {e}")); + } + if json.len() > MAX_PROPOSAL_BYTES { + return fail("the encoded proposal is larger than 32 MiB"); + } let policy = Policy::default(); let plan = match plan_from_json(&json, &policy) { diff --git a/signing-core/crates/authority-agent/src/tools.rs b/signing-core/crates/authority-agent/src/tools.rs index 9e03cba..009b516 100644 --- a/signing-core/crates/authority-agent/src/tools.rs +++ b/signing-core/crates/authority-agent/src/tools.rs @@ -80,6 +80,12 @@ pub const TOOLS: &[(&str, &str)] = &[ ]; const MAX_STEPS_PROPOSED: usize = authority::MAX_STEPS; +/// Maximum encoded proposal accepted before JSON allocation begins. +/// +/// The typed model permits 128 transaction steps of 64 KiB each. Hex encoding +/// doubles that payload, so 32 MiB preserves every typed maximum with room for +/// labels and JSON structure while still bounding parser allocation. +pub const MAX_PROPOSAL_BYTES: usize = 32 * 1024 * 1024; /// Build a plan from an agent's proposal. /// @@ -90,6 +96,11 @@ const MAX_STEPS_PROPOSED: usize = authority::MAX_STEPS; /// "arguments": { "path": "…" }, "uses": [] } ] } /// ``` pub fn plan_from_json(json: &str, policy: &Policy) -> Result { + if json.len() > MAX_PROPOSAL_BYTES { + return Err(AdapterError::TooLarge( + "the encoded proposal is larger than 32 MiB", + )); + } let root: Value = serde_json::from_str(json).map_err(|e| AdapterError::NotJson(e.to_string()))?; let goal = root diff --git a/signing-core/crates/authority-agent/tests/adapter.rs b/signing-core/crates/authority-agent/tests/adapter.rs index 70da7ef..6fdaefd 100644 --- a/signing-core/crates/authority-agent/tests/adapter.rs +++ b/signing-core/crates/authority-agent/tests/adapter.rs @@ -8,7 +8,7 @@ )] use authority::{Action, Sensitivity, fingerprint, review_plan}; -use authority_agent::{AdapterError, Policy, plan_from_json}; +use authority_agent::{AdapterError, MAX_PROPOSAL_BYTES, Policy, plan_from_json}; use clearsign::Severity; const INJECTED: &str = include_str!("../examples/injected-proposal.json"); @@ -110,6 +110,14 @@ fn an_oversized_proposal_is_refused_before_review() { assert!(matches!(plan(&json), Err(AdapterError::TooLarge(_)))); } +#[test] +fn an_oversized_encoded_proposal_is_refused_before_json_parsing() { + let at_limit = " ".repeat(MAX_PROPOSAL_BYTES); + assert!(matches!(plan(&at_limit), Err(AdapterError::NotJson(_)))); + let json = " ".repeat(MAX_PROPOSAL_BYTES.saturating_add(1)); + assert!(matches!(plan(&json), Err(AdapterError::TooLarge(_)))); +} + #[test] fn a_path_that_walks_upwards_is_outside_the_allowed_roots() { let policy = Policy::default(); diff --git a/signing-core/crates/clearsign/src/calls.rs b/signing-core/crates/clearsign/src/calls.rs index 40d2cc1..ccd5248 100644 --- a/signing-core/crates/clearsign/src/calls.rs +++ b/signing-core/crates/clearsign/src/calls.rs @@ -493,6 +493,7 @@ fn review_batch( let calls = match calls { Ok(calls) => calls, Err(Error::TooDeep) => { + review.refuse_signing(); review.sections.push(s); review.find( Severity::Blind, @@ -574,6 +575,7 @@ fn review_batch( let depth = ctx.depth.saturating_add(1); if depth > MAX_NESTING { + review.refuse_signing(); review.find( Severity::Blind, "NESTING_LIMIT", @@ -596,11 +598,14 @@ fn review_batch( // were never shown. // // Each hidden call goes through exactly the same rules as a displayed one, - // into a scratch review whose sections are then discarded. Only its findings - // survive, and they are grouped by code before being reported: a batch may - // hold a thousand calls, and a thousand separately acknowledgeable findings - // is not a review anybody reads. + // into a scratch review whose sections are then discarded. Findings are + // grouped for a bounded report, but the review is permanently display-only: + // grouping two unlimited approvals must never let one acknowledgement + // authorize both actions. if let Some(hidden) = calls.get(shown..) { + if !hidden.is_empty() { + review.refuse_signing(); + } let mut by_code: BTreeMap<&'static str, (Severity, Vec)> = BTreeMap::new(); for (offset, call) in hidden.iter().enumerate() { let position = shown.saturating_add(offset).saturating_add(1); @@ -705,6 +710,10 @@ fn list_positions(positions: &[usize]) -> String { /// will be read must not earn an attacker a milder review than a malformed /// encoding gets, so it is CRITICAL, as an over-long MultiSend batch already is. fn undecoded_calls_finding(review: &mut Review) { + // A human cannot authorize executable calls the decoder did not inspect. + // Keep the finding for diagnosis, but permanently remove signing authority + // even if a caller later computes and installs the transaction digest. + review.refuse_signing(); review.find( Severity::Critical, "UNDECODED_CALLS", @@ -757,10 +766,14 @@ pub struct InnerCall<'a> { /// Review carried calls the way a batch is reviewed: every one is judged by the /// same rules, the first [`multisend::MAX_BATCH_CALLS`] are displayed, and the /// findings of the rest are grouped by code so nothing past the display limit -/// can go unnamed. +/// can go unnamed. A review with hidden calls is display-only: grouped findings +/// can describe why it was refused, but can never authorize a signature. fn review_inner_calls(review: &mut Review, calls: &[InnerCall<'_>], title: &str, ctx: CallContext) { let shown = calls.len().min(multisend::MAX_BATCH_CALLS); if let Some(hidden) = calls.get(shown..) { + if !hidden.is_empty() { + review.refuse_signing(); + } let mut by_code: BTreeMap<&'static str, (Severity, Vec)> = BTreeMap::new(); for (offset, call) in hidden.iter().enumerate() { let position = shown.saturating_add(offset).saturating_add(1); diff --git a/signing-core/crates/clearsign/src/error.rs b/signing-core/crates/clearsign/src/error.rs index 0a4e679..b3235c3 100644 --- a/signing-core/crates/clearsign/src/error.rs +++ b/signing-core/crates/clearsign/src/error.rs @@ -14,6 +14,8 @@ pub enum Error { NonCanonical(&'static str), /// Nesting exceeded the fixed limit. TooDeep, + /// An encoded input exceeds a fixed resource budget. + TooLarge(&'static str), /// A structure had the wrong number of fields. WrongFieldCount { expected: usize, found: usize }, /// Expected a byte string, found a list, or the reverse. @@ -36,6 +38,7 @@ impl fmt::Display for Error { Error::TrailingBytes => f.write_str("unexpected bytes after the end of the structure"), Error::NonCanonical(what) => write!(f, "non-canonical encoding: {what}"), Error::TooDeep => f.write_str("nesting is deeper than allowed"), + Error::TooLarge(what) => write!(f, "input is too large: {what}"), Error::WrongFieldCount { expected, found } => { write!(f, "expected {expected} fields, found {found}") } diff --git a/signing-core/crates/clearsign/src/evm.rs b/signing-core/crates/clearsign/src/evm.rs index 11fa88e..b961fd5 100644 --- a/signing-core/crates/clearsign/src/evm.rs +++ b/signing-core/crates/clearsign/src/evm.rs @@ -13,6 +13,13 @@ use crate::review::{Review, Section, Severity}; use crate::rlp::{self, Item}; use crate::{Error, U256, hex}; +/// Maximum encoded transaction accepted by the public raw-byte parser. +/// +/// This matches the record limit used by the JSON, native and browser +/// boundaries. Keeping it here makes the library safe for embedders that do +/// not add a transport-specific cap of their own. +pub const MAX_TRANSACTION_BYTES: usize = 1024 * 1024; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TxType { /// Pre-EIP-155: no chain ID, replayable on other chains. @@ -153,6 +160,9 @@ pub struct EvmTransaction { /// Parse the unsigned transaction bytes a wallet asks the signer to sign. /// Signed transactions are rejected: a signer only ever signs unsigned payloads. pub fn parse_unsigned_transaction(bytes: &[u8]) -> Result { + if bytes.len() > MAX_TRANSACTION_BYTES { + return Err(Error::TooLarge("transaction exceeds 1 MiB")); + } let first = *bytes.first().ok_or(Error::Truncated)?; let signing_hash = keccak256(bytes); let typed_fields = || -> Result, Error> { diff --git a/signing-core/crates/clearsign/src/lib.rs b/signing-core/crates/clearsign/src/lib.rs index b1ebcf6..add4692 100644 --- a/signing-core/crates/clearsign/src/lib.rs +++ b/signing-core/crates/clearsign/src/lib.rs @@ -37,8 +37,8 @@ pub mod u256; pub use error::Error; pub use evm::{ - Authorization, AuthorizationSignatureError, EvmTransaction, TxType, parse_unsigned_transaction, - review_evm_transaction, review_transaction_bytes, + Authorization, AuthorizationSignatureError, EvmTransaction, MAX_TRANSACTION_BYTES, TxType, + parse_unsigned_transaction, review_evm_transaction, review_transaction_bytes, }; pub use review::{ Finding, MAX_ACKNOWLEDGEABLE_FINDINGS, Review, Section, Severity, SigningTarget, TargetKind, diff --git a/signing-core/crates/clearsign/src/review.rs b/signing-core/crates/clearsign/src/review.rs index b418723..7620964 100644 --- a/signing-core/crates/clearsign/src/review.rs +++ b/signing-core/crates/clearsign/src/review.rs @@ -76,6 +76,10 @@ pub struct Review { /// The exact digest this review covers, if it may be signed. Private so that /// code outside this crate cannot attach a digest the review did not derive. signing_target: Option, + /// Set when the decoder can report risk but cannot present enough detail + /// for an informed signing decision. A later attempt to attach a digest + /// must not make such a review signable again. + signing_refused: bool, } /// What a signature over this review's digest authorises. @@ -104,6 +108,7 @@ impl Review { findings: Vec::new(), digests: Vec::new(), signing_target: None, + signing_refused: false, } } @@ -114,7 +119,9 @@ impl Review { } pub(crate) fn set_signing_target(&mut self, target: SigningTarget) { - self.signing_target = Some(target); + if !self.signing_refused { + self.signing_target = Some(target); + } } /// Make the review display-only, for a review built by a path that signs @@ -123,6 +130,14 @@ impl Review { self.signing_target = None; } + /// Permanently make this review display-only. This is stronger than + /// `clear_signing_target`: callers that compute the digest after decoding + /// cannot accidentally make the review signable again. + pub(crate) fn refuse_signing(&mut self) { + self.signing_refused = true; + self.signing_target = None; + } + /// Everything the review found, in the order it was found. pub fn findings(&self) -> &[Finding] { &self.findings diff --git a/signing-core/crates/clearsign/src/rlp.rs b/signing-core/crates/clearsign/src/rlp.rs index e65399e..0ffc343 100644 --- a/signing-core/crates/clearsign/src/rlp.rs +++ b/signing-core/crates/clearsign/src/rlp.rs @@ -13,6 +13,8 @@ use alloc::vec::Vec; use crate::{Error, U256}; pub const MAX_DEPTH: usize = 16; +/// Maximum number of RLP nodes in one encoded value, including the root. +pub const MAX_ITEMS: usize = 4096; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Item<'a> { @@ -61,17 +63,25 @@ impl<'a> Item<'a> { /// Decode exactly one item that spans the entire input. pub fn decode(input: &[u8]) -> Result, Error> { - let (item, rest) = decode_item(input, 0)?; + let mut remaining = MAX_ITEMS; + let (item, rest) = decode_item(input, 0, &mut remaining)?; if !rest.is_empty() { return Err(Error::TrailingBytes); } Ok(item) } -fn decode_item(input: &[u8], depth: usize) -> Result<(Item<'_>, &[u8]), Error> { +fn decode_item<'a>( + input: &'a [u8], + depth: usize, + remaining: &mut usize, +) -> Result<(Item<'a>, &'a [u8]), Error> { if depth > MAX_DEPTH { return Err(Error::TooDeep); } + *remaining = remaining + .checked_sub(1) + .ok_or(Error::TooLarge("RLP contains too many items"))?; let (&prefix, after) = input.split_first().ok_or(Error::Truncated)?; match prefix { 0x00..=0x7f => { @@ -97,22 +107,32 @@ fn decode_item(input: &[u8], depth: usize) -> Result<(Item<'_>, &[u8]), Error> { 0xc0..=0xf7 => { let len = usize::from(prefix.checked_sub(0xc0).ok_or(Error::Truncated)?); let (payload, rest) = take(after, len)?; - Ok((Item::List(decode_list_payload(payload, depth)?), rest)) + Ok(( + Item::List(decode_list_payload(payload, depth, remaining)?), + rest, + )) } 0xf8..=0xff => { let len_of_len = usize::from(prefix.checked_sub(0xf7).ok_or(Error::Truncated)?); let (len, after_len) = read_long_length(after, len_of_len)?; let (payload, rest) = take(after_len, len)?; - Ok((Item::List(decode_list_payload(payload, depth)?), rest)) + Ok(( + Item::List(decode_list_payload(payload, depth, remaining)?), + rest, + )) } } } -fn decode_list_payload(mut payload: &[u8], depth: usize) -> Result>, Error> { +fn decode_list_payload<'a>( + mut payload: &'a [u8], + depth: usize, + remaining: &mut usize, +) -> Result>, Error> { let mut items = Vec::new(); let child_depth = depth.checked_add(1).ok_or(Error::TooDeep)?; while !payload.is_empty() { - let (item, rest) = decode_item(payload, child_depth)?; + let (item, rest) = decode_item(payload, child_depth, remaining)?; items.push(item); payload = rest; } @@ -145,3 +165,38 @@ fn take(input: &[u8], len: usize) -> Result<(&[u8], &[u8]), Error> { } Ok(input.split_at(len)) } + +#[cfg(test)] +mod tests { + use alloc::vec; + + use super::{Error, MAX_ITEMS, decode}; + + #[test] + fn a_flat_list_cannot_exhaust_memory_with_item_headers() { + // f9 10 00 is the canonical prefix for a 4096-byte list. Each payload + // byte is one complete RLP item, so the root plus its children exceeds + // the global item budget even though nesting remains shallow. + let mut encoded = vec![0xf9, 0x10, 0x00]; + encoded.extend(core::iter::repeat_n(0x01, MAX_ITEMS)); + assert_eq!( + decode(&encoded), + Err(Error::TooLarge("RLP contains too many items")) + ); + } + + #[test] + fn the_exact_global_item_limit_is_accepted() { + // The root is one item and the payload contains MAX_ITEMS - 1 + // single-byte children. This locks the inclusive boundary so a future + // hardening change cannot silently reject the last permitted item. + let children = MAX_ITEMS - 1; + assert_eq!(MAX_ITEMS, 4096); + let mut encoded = vec![0xf9, 0x0f, 0xff]; + encoded.extend(core::iter::repeat_n(0x01, children)); + assert_eq!( + decode(&encoded).and_then(|item| item.as_list().map(|items| items.len())), + Ok(children) + ); + } +} diff --git a/signing-core/crates/clearsign/tests/administration.rs b/signing-core/crates/clearsign/tests/administration.rs index 2e69fb7..358c210 100644 --- a/signing-core/crates/clearsign/tests/administration.rs +++ b/signing-core/crates/clearsign/tests/administration.rs @@ -567,6 +567,10 @@ fn a_dangerous_call_past_the_display_limit_of_a_multicall_is_named() { assert_eq!(grant.severity, Severity::Critical); assert!(grant.message.contains("37"), "{}", grant.message); assert!(r.has("CARRIED_CALLS_NOT_SHOWN")); + assert!( + r.signing_target().is_none(), + "a grouped hidden action must make the entire review display-only" + ); } #[test] @@ -579,6 +583,7 @@ fn a_multicall_too_long_to_review_is_not_decoded_and_is_critical() { // malformed list gets: what was not read could be anything. assert_eq!(severity_of(&r, "UNDECODED_CALLS"), Severity::Critical); assert!(r.render().contains("DO NOT SIGN")); + assert!(r.signing_target().is_none()); } #[test] @@ -703,6 +708,7 @@ fn nesting_past_the_limit_is_blind() { assert!(r.has("NESTING_LIMIT")); assert_eq!(severity_of(&r, "UNDECODED_CALLS"), Severity::Critical); assert!(r.render().contains("DO NOT SIGN")); + assert!(r.signing_target().is_none()); } #[test] @@ -1258,6 +1264,7 @@ fn an_account_batch_at_the_limit_is_judged_call_by_call_and_one_more_is_not_read assert!(r.has("TOO_MANY_CARRIED_CALLS")); assert!(!r.has("UNLIMITED_APPROVAL")); assert_eq!(severity_of(&r, "UNDECODED_CALLS"), Severity::Critical); + assert!(r.signing_target().is_none()); } #[test] diff --git a/signing-core/crates/clearsign/tests/invariants.rs b/signing-core/crates/clearsign/tests/invariants.rs index b333543..e65748f 100644 --- a/signing-core/crates/clearsign/tests/invariants.rs +++ b/signing-core/crates/clearsign/tests/invariants.rs @@ -8,12 +8,28 @@ )] use clearsign::rlp; -use clearsign::{DomainVersion, Error, SafeTransaction, Severity, TxType, U256}; +use clearsign::{ + DomainVersion, Error, MAX_TRANSACTION_BYTES, SafeTransaction, Severity, TxType, U256, +}; use clearsign::{hex, parse_unsigned_transaction, review_evm_transaction, review_safe_transaction}; const SAFE: [u8; 20] = [0x11; 20]; const TOKEN: [u8; 20] = [0x22; 20]; +#[test] +fn raw_transactions_are_bounded_at_the_public_parser() { + let at_limit = vec![0u8; MAX_TRANSACTION_BYTES]; + assert_ne!( + parse_unsigned_transaction(&at_limit), + Err(Error::TooLarge("transaction exceeds 1 MiB")) + ); + let oversized = vec![0u8; MAX_TRANSACTION_BYTES.saturating_add(1)]; + assert_eq!( + parse_unsigned_transaction(&oversized), + Err(Error::TooLarge("transaction exceeds 1 MiB")) + ); +} + fn safe_call(to: [u8; 20], data: Vec, operation: u8) -> SafeTransaction { SafeTransaction { chain_id: U256::from_u64(1), diff --git a/signing-core/crates/clearsign/tests/mutation_gaps.rs b/signing-core/crates/clearsign/tests/mutation_gaps.rs index e41ac5c..d8f0033 100644 --- a/signing-core/crates/clearsign/tests/mutation_gaps.rs +++ b/signing-core/crates/clearsign/tests/mutation_gaps.rs @@ -401,6 +401,7 @@ fn safe_transactions_nest_exactly_to_the_limit() { .iter() .any(|f| f.code == "UNDECODED_CALLS" && f.severity == clearsign::Severity::Critical) ); + assert!(r.signing_target().is_none()); } // calls.rs:391 — batches nested to the limit are decoded, one more is not. @@ -447,7 +448,9 @@ fn a_batch_shows_its_total_its_overflow_and_its_own_value() { ); } -// calls.rs:502, calls.rs:510 — the positions of hidden findings are listed. +// A screen cannot present every call in an oversized batch. The decoder still +// reports what it found, but the review must not carry signing authority: a +// grouped warning is a useful refusal reason, not permission for many actions. #[test] fn hidden_findings_list_where_they_are() { let mut calls: Vec<_> = (0..45).map(|_| (0u8, TOKEN, 0u64, vec![])).collect(); @@ -461,29 +464,29 @@ fn hidden_findings_list_where_they_are() { &safe_call(MULTISEND_130, multisend(&calls), 1), DomainVersion::V1_3Plus, ); - let f = r + let findings: Vec<_> = r .findings() .iter() - .find(|f| f.code == "UNLIMITED_APPROVAL") - .unwrap(); - assert!( - f.message - .contains("33, 34, 35, 36, 37, 38, 39, 40 and 5 more"), - "{}", - f.message - ); + .filter(|f| f.code == "UNLIMITED_APPROVAL") + .collect(); + assert_eq!(findings.len(), 1); + assert!(findings[0].message.contains("33, 34, 35")); + assert!(findings[0].message.contains("5 more")); + assert!(r.signing_target().is_none()); calls.truncate(35); let r = review_safe_transaction( &safe_call(MULTISEND_130, multisend(&calls), 1), DomainVersion::V1_3Plus, ); - let f = r + let findings: Vec<_> = r .findings() .iter() - .find(|f| f.code == "UNLIMITED_APPROVAL") - .unwrap(); - assert!(f.message.contains(": 33, 34, 35."), "{}", f.message); + .filter(|f| f.code == "UNLIMITED_APPROVAL") + .collect(); + assert_eq!(findings.len(), 1); + assert!(findings[0].message.contains("33, 34, 35")); + assert!(r.signing_target().is_none()); } // calls.rs:872 — one SELECTOR_IS_NOT_BEHAVIOUR notice per destination. diff --git a/signing-core/scripts/in-canonical-container.sh b/signing-core/scripts/in-canonical-container.sh index 1f9eafd..d7dff60 100755 --- a/signing-core/scripts/in-canonical-container.sh +++ b/signing-core/scripts/in-canonical-container.sh @@ -27,7 +27,18 @@ export PATH="/opt/cargo/bin:$PATH" BUILD_PATH="${BUILD_PATH:-/build-one}" mkdir -p "$(dirname "$BUILD_PATH")" cp -a /src "$BUILD_PATH" -rm -rf /opt/cargo/registry && cp -a "$BUILD_PATH/cargo-cache/registry" /opt/cargo/registry +if [[ -d /cargo-registry ]]; then + REGISTRY_SNAPSHOT=/cargo-registry +elif [[ -d "$BUILD_PATH/cargo-cache/registry" ]]; then + # The signer-image builder packages its snapshot with the source tree rather + # than mounting it separately. Supporting both callers keeps this helper + # reusable without allowing Cargo to reach the network. + REGISTRY_SNAPSHOT="$BUILD_PATH/cargo-cache/registry" +else + echo "no frozen Cargo registry snapshot was supplied" >&2 + exit 1 +fi +rm -rf /opt/cargo/registry && cp -a "$REGISTRY_SNAPSHOT" /opt/cargo/registry cd "$BUILD_PATH" export CARGO_ENCODED_RUSTFLAGS=$'--remap-path-prefix='"$BUILD_PATH"$'=/build\x1f--remap-path-prefix=/opt/cargo=/cargo\x1f-C\x1fstrip=symbols' export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=rust-lld diff --git a/signing-core/scripts/reproducible-cross-check.sh b/signing-core/scripts/reproducible-cross-check.sh index 80f1f18..08b31db 100755 --- a/signing-core/scripts/reproducible-cross-check.sh +++ b/signing-core/scripts/reproducible-cross-check.sh @@ -28,12 +28,10 @@ PACKAGES=(clearsign-cli clearsign-device) BINARIES=(clearsign clearsign-device) mkdir -p "$ARTIFACTS" -prepare() { # source tree plus this machine's crate cache, so the build is offline +prepare() { # source tree; the caller mounts one frozen crate-cache snapshot local work="$1" # bench/ is its own workspace with large corpora and is not part of any build. (cd "$ROOT" && tar --exclude ./target --exclude ./fuzz --exclude ./vendor --exclude ./bench -cf - .) | (cd "$work" && tar -xf -) - mkdir -p "$work/cargo-cache" - cp -R "$CARGO_HOME_DIR/registry" "$work/cargo-cache/registry" cp "$ROOT/scripts/in-canonical-container.sh" "$work/in-container.sh" chmod +x "$work/in-container.sh" } @@ -42,7 +40,7 @@ canonical_build() { # $1 work dir, $2 label, $3 build path, $4 locale, $5 timezo local work="$1" label="$2" docker run --rm --platform linux/arm64 \ --hostname "builder-$label" \ - -v "$work:/src:ro" -v "$ARTIFACTS:/artifacts" \ + -v "$work:/src:ro" -v "$CACHE_SNAPSHOT:/cargo-registry:ro" -v "$ARTIFACTS:/artifacts" \ -e BUILD_PATH="$3" -e BUILD_LOCALE="$4" -e BUILD_TZ="$5" -e BUILD_UMASK="$6" -e LABEL="$label" \ "$DEBIAN_BUILD_IMAGE" /src/in-container.sh "$TOOLCHAIN" "$TARGET" "${PACKAGES[@]}" \ | tail -n "${#BINARIES[@]}" @@ -62,10 +60,13 @@ host_build() { # informational: the developer's own machine and toolchain WORK_PARENT="$(mktemp -d "$REPO/.repro-work.XXXXXX")" WORK_A="$WORK_PARENT/one"; WORK_B="$WORK_PARENT/two" +CACHE_SNAPSHOT="$WORK_PARENT/cargo-registry" mkdir -p "$WORK_A" "$WORK_B" trap 'rm -rf "$WORK_PARENT"' EXIT echo "canonical environment: $DEBIAN_BUILD_IMAGE" echo "toolchain $TOOLCHAIN, target $TARGET" +echo "snapshotting the Cargo registry once for both offline builds" +cp -R "$CARGO_HOME_DIR/registry" "$CACHE_SNAPSHOT" echo "preparing two identical source trees" prepare "$WORK_A" prepare "$WORK_B" @@ -73,8 +74,8 @@ prepare "$WORK_B" echo echo "--- canonical build 1: /build-one, C.UTF-8, UTC, umask 022" A="$(canonical_build "$WORK_A" one /build-one C.UTF-8 UTC 022)"; echo "$A" -echo "--- canonical build 2: /a/much/longer/build/path/two, en_US.UTF-8, Pacific/Kiritimati, umask 077" -B="$(canonical_build "$WORK_B" two /a/much/longer/build/path/two en_US.UTF-8 Pacific/Kiritimati 077)"; echo "$B" +echo "--- canonical build 2: /a/much/longer/build/path/two, C, Pacific/Kiritimati, umask 077" +B="$(canonical_build "$WORK_B" two /a/much/longer/build/path/two C Pacific/Kiritimati 077)"; echo "$B" if [[ "${SKIP_HOST_BUILD:-0}" != "1" ]]; then echo @@ -88,8 +89,8 @@ if [[ "$A" == "$B" ]]; then echo "# The signer binaries, as built in the canonical environment." echo "# Reproduce with signing-core/scripts/reproducible-cross-check.sh." echo "# Environment: $DEBIAN_BUILD_IMAGE, Rust $TOOLCHAIN, target $TARGET." - echo "# The compile is offline, from a copied registry cache. The container's" - echo "# own bootstrap is not: it apt-gets curl, gcc and libc6-dev, and downloads" + echo "# The compile is offline, from a frozen registry-cache snapshot. The container's" + echo "# own bootstrap is not: it installs bootstrap packages with apt and downloads" echo "# rustup. Those are not pinned by digest, so the environment is named and" echo "# reproducible in practice rather than sealed." echo "# Anything built elsewhere, including on the maintainer's own machine, may" From 3ce55b7cbe52febc89efa3275562aeb90958b9cf Mon Sep 17 00:00:00 2001 From: Antics Decoded <237784870+AnticsDecoded@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:45:00 +0100 Subject: [PATCH 2/3] Refresh Expo patch dependencies --- CHANGELOG.md | 4 + mobile/README.md | 10 +- mobile/package-lock.json | 554 ++++++++++++------------ mobile/package.json | 6 +- mobile/scripts/check-npm-audit.mjs | 32 +- mobile/scripts/check-npm-audit.test.mjs | 11 +- 6 files changed, 325 insertions(+), 292 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 800bcd2..d6e471a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ that keeps people on a broken version. and fell back with a warning, so it did not test the locale variation the log claimed. Both builds share one frozen snapshot of the Cargo registry while retaining separate source trees and containers. +- Mobile dependencies now match the current Expo 58 patch set. This removes the + `node-forge` signature-verification advisory; the audit gate records the + remaining Zod and `deepmerge` build-tool advisories exactly and still fails on + any unreviewed change to that graph. ## v0.1.7 — 2026-10-26 diff --git a/mobile/README.md b/mobile/README.md index 022c075..d6f9ef0 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -27,10 +27,12 @@ Run `npm run typecheck` and `npx expo-doctor` before submitting a change. CI builds the Rust libraries and both native applications from clean runners. `npm run audit` fails if the dependency advisory set changes. The current Expo -SDK build CLI brings two known high-severity advisories through `node-forge` -and Zod; neither has a compatible upstream release yet. They are build-tool -dependencies, not code loaded by the transaction reviewer, and are named rather -than suppressed so an Expo update can remove the exception. +SDK build CLI brings a high-severity resource-exhaustion advisory through Zod +and a low-severity prototype-poisoning advisory through `deepmerge`; neither has +a compatible upstream release yet. They are build-tool dependencies, not code +loaded by the transaction reviewer, and are named rather than suppressed so an +Expo update can remove the exception. Expo 58.0.20 also moves `node-forge` to +1.4.2, removing the earlier signature-verification advisory. ## Native boundary diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 433571c..2db6379 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -9,14 +9,14 @@ "version": "0.1.7", "dependencies": { "@expo/vector-icons": "^15.0.2", - "expo": "~58.0.19", + "expo": "~58.0.20", "expo-clipboard": "~58.0.4", "expo-constants": "~58.0.13", "expo-document-picker": "~58.0.4", - "expo-file-system": "~58.0.11", + "expo-file-system": "~58.0.12", "expo-font": "~58.0.10", "expo-linking": "~58.0.14", - "expo-router": "~58.0.26", + "expo-router": "~58.0.27", "expo-status-bar": "~58.0.4", "expo-system-ui": "~58.0.6", "react": "19.3.0", @@ -1276,9 +1276,9 @@ } }, "node_modules/@expo/fingerprint": { - "version": "0.21.6", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.21.6.tgz", - "integrity": "sha512-EKh7ak+SYwXjvX/4P7BcyeLxip2z9KAsnN/55VP10g8H1Ua4Dm/BgLAhgBcbzbsYretYmNEynAcf9oKCK9pjxA==", + "version": "0.21.7", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.21.7.tgz", + "integrity": "sha512-rE9QN4TXGZxS3PC1yW77D1B6JNnuv3n7hXVUZxFEDDndPKFnQPC0XkqFyIz21oXbtrvThROCbV9CDVJiycCntg==", "license": "MIT", "dependencies": { "@expo/env": "^2.5.3", @@ -1392,6 +1392,62 @@ "metro-transform-worker": "0.87.1" } }, + "node_modules/@expo/metro-config": { + "version": "58.0.18", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-58.0.18.tgz", + "integrity": "sha512-hdjbZqs7Wcxj54vM46J9DkfvsSxH83GvZQHkIQccikQ7TVyYqDbe2bmSk3oaO0khyhgaImd9fiR2Obo/4/XH5g==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~58.0.8", + "@expo/env": "~2.5.3", + "@expo/json-file": "~11.2.1", + "@expo/metro": "~58.0.0", + "@expo/platforms": "^58.0.1", + "@expo/require-utils": "^58.0.4", + "@expo/spawn-async": "^1.8.0", + "@jridgewell/gen-mapping": "^0.3.13", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "@jridgewell/trace-mapping": "^0.3.31", + "@react-native/js-polyfills": "0.88.0", + "2g": "^1.0.1", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.36.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "noxcturnal": "^0.1.10", + "picomatch": "^4.0.4", + "postcss": "^8.5.23", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@expo/metro-config/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@expo/metro-file-map": { "version": "58.0.5", "resolved": "https://registry.npmjs.org/@expo/metro-file-map/-/metro-file-map-58.0.5.tgz", @@ -1406,10 +1462,35 @@ "walker": "^1.0.8" } }, + "node_modules/@expo/metro-runtime": { + "version": "58.0.15", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-58.0.15.tgz", + "integrity": "sha512-csALn54hZUkHFM3FofWlU5MQcJB62deCt+JtHOhQogRT+k/VTtTuhLQCrCUOtO2MAUsyhrcRRa6Nh79e1bkvfg==", + "license": "MIT", + "dependencies": { + "@expo/log-box": "^58.0.13", + "anser": "^1.4.9", + "pretty-format": "^29.7.0", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0" + }, + "peerDependencies": { + "@expo/log-box": "^58.0.13", + "expo": "*", + "react": "*", + "react-dom": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/@expo/modules-macros-linux-arm64": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@expo/modules-macros-linux-arm64/-/modules-macros-linux-arm64-0.22.0.tgz", - "integrity": "sha512-FGXyxpZMEJ4pDPCeCBjfBjftCbk03P/1Rf6UkFoL22joqu4XXt+XUsjfTzyKBzP9pAXL39FVdbNClwk529CEEQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@expo/modules-macros-linux-arm64/-/modules-macros-linux-arm64-0.24.0.tgz", + "integrity": "sha512-pSjuli8PFrDKjf1Cikl8IbX8CmF/OyiyQaEjOgQwspB8UbM+aZtviDYquEWxkMFWggHaCsPSzTcZxsf7AY6myQ==", "cpu": [ "arm64" ], @@ -1423,9 +1504,9 @@ ] }, "node_modules/@expo/modules-macros-linux-x64": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@expo/modules-macros-linux-x64/-/modules-macros-linux-x64-0.22.0.tgz", - "integrity": "sha512-4yN4cZTCUWkq1Olr0/lA57gFan8+MEo7K3K0cx0bB3TUEqHgL/hyJN79vQKXlp9NWTnOxO7J5uwqMQkQwfh6aw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@expo/modules-macros-linux-x64/-/modules-macros-linux-x64-0.24.0.tgz", + "integrity": "sha512-B/w/mojEJ5Q0vkRSoraWj5r1C/AUgXc8t2tHAc18rcPoi/QGSQLNc1cWk7zF7Avbdi4hR8jOzAxxTEbcAkUjKg==", "cpu": [ "x64" ], @@ -1439,9 +1520,9 @@ ] }, "node_modules/@expo/modules-macros-win32-arm64": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@expo/modules-macros-win32-arm64/-/modules-macros-win32-arm64-0.22.0.tgz", - "integrity": "sha512-nLDXRGZWKNERzP4zH9LdfdZ3jvPBQcQE7LX5UnWW4cnDE/GoiVyUKdXQFK+KRnMD6We5gxqhIOU6SLj74h6fHA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@expo/modules-macros-win32-arm64/-/modules-macros-win32-arm64-0.24.0.tgz", + "integrity": "sha512-gfoUROUyMqSs6W53FzVJUgGv7pVACab9I1DLD7YlICNtIvkZWyUHDitAdfu5pc0R9l8ypqqGni5MvLGjXgJqNg==", "cpu": [ "arm64" ], @@ -1452,9 +1533,9 @@ ] }, "node_modules/@expo/modules-macros-win32-x64": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@expo/modules-macros-win32-x64/-/modules-macros-win32-x64-0.22.0.tgz", - "integrity": "sha512-IT5/YsBTj2gnfNlxDfv3sC51rBudiSHbPFBKoPBbpwmTCMcQg1rmWVdUoiy7LLC48NNzm86wyMxHpSI5lkW1Gg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@expo/modules-macros-win32-x64/-/modules-macros-win32-x64-0.24.0.tgz", + "integrity": "sha512-120H4TvPwpN6GSErXzgRyaNDz6xSYN2ZLICZ2XRORGKiiEdkL8fYyjwDcv3V9gm2zgxWv5ELNtyFfCPoO9OP6Q==", "cpu": [ "x64" ], @@ -1693,9 +1774,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-darwin-arm64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-darwin-arm64/-/noxcturnal-darwin-arm64-0.1.9.tgz", - "integrity": "sha512-CJWyUWfp+/vnLTdaFoYHXV7abCA1qO0VClPgFRjv2FbMFQ81XhWZcG9hPdt4FzSZ1oqqiQqlvpk7hA/Nh8ML9g==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-darwin-arm64/-/noxcturnal-darwin-arm64-0.1.11.tgz", + "integrity": "sha512-tqvZOVmSoiJzm7QjQ+whPxCP45J2C1zCAZnzc1HhwmJrFIWw/qyA8o9Cup0aVqnjNVAN+as9GHX+DbAmkCbGlQ==", "cpu": [ "arm64" ], @@ -1709,9 +1790,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-darwin-x64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-darwin-x64/-/noxcturnal-darwin-x64-0.1.9.tgz", - "integrity": "sha512-53D55Ai3rAUf8USjnCjgSzegtuc4nkvcR9EIiTXd4QgRLUtK2IY0q4IRdWU8Yrnlq/O5/GdGmmD6+t3ymQMjqg==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-darwin-x64/-/noxcturnal-darwin-x64-0.1.11.tgz", + "integrity": "sha512-V2HXkK5iKFBSc2GmeZE2I2wGxBc0kMQUHxxIUBSjuO8enKIby6cEQj8WW5EAuIli622Sqw1V7f0IIz8mXB4M9g==", "cpu": [ "x64" ], @@ -1725,9 +1806,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-linux-arm64-gnu": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-arm64-gnu/-/noxcturnal-linux-arm64-gnu-0.1.9.tgz", - "integrity": "sha512-ingSEdcjdZBiGfjpnTNSlEE6db247fUsdRF3NJI/0anYuJa4WlNnm+z8as2m9dFATr+qmZLjBXgUFfnc6ZRQAg==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-arm64-gnu/-/noxcturnal-linux-arm64-gnu-0.1.11.tgz", + "integrity": "sha512-Vf9Qbke9Hq5ToFt88XicQN0FJJuepOnPKspcKDICeYYXS2b5wQnU7uhf33QWzr1KPQhn0L0IWUCwBNhMVn6E6w==", "cpu": [ "arm64" ], @@ -1744,9 +1825,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-linux-arm64-musl": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-arm64-musl/-/noxcturnal-linux-arm64-musl-0.1.9.tgz", - "integrity": "sha512-i45wjmtleHdQYCshqCiAIxdD57gaApdUTgu5M99FBsddpYHXIEHvdwA78aZiqMwjLNz723tFdGB52DwbhzgzEA==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-arm64-musl/-/noxcturnal-linux-arm64-musl-0.1.11.tgz", + "integrity": "sha512-HlwlrcfBE7JBiwWaq32R8eN4APJHQCDoQlapeK+t0wxY9s9YDMDtAnG+v7RrEz05nu7Aci21x/s8qIAA5jE+NA==", "cpu": [ "arm64" ], @@ -1763,9 +1844,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-linux-x64-gnu": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-x64-gnu/-/noxcturnal-linux-x64-gnu-0.1.9.tgz", - "integrity": "sha512-IRV8YsMCT3QqbYEE5dFGGBe58Ivkz5bEUpdXC110uTfzdeRSYbv3Iq/PbdVcfyoXp1HXtBwV6bdIofDQTEPlTg==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-x64-gnu/-/noxcturnal-linux-x64-gnu-0.1.11.tgz", + "integrity": "sha512-kIzTkH4rEIn8vFl42lzfK8qmKaXMwHM7txbI+XkhYKjS+2WV08yslBpwHaPGtGLXPEfx+xUTUgY5K3KVET7HKw==", "cpu": [ "x64" ], @@ -1782,9 +1863,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-linux-x64-musl": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-x64-musl/-/noxcturnal-linux-x64-musl-0.1.9.tgz", - "integrity": "sha512-M7FT6J5R2XDWFioyFlxXb+RvnjNBpK2p1rtrtHHh6m/g1nHcNqvvTYjMQzrTHbMyOdQGlaV3zyALhURtX4kgQw==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-linux-x64-musl/-/noxcturnal-linux-x64-musl-0.1.11.tgz", + "integrity": "sha512-KpC0H+v+y/v0j4EmGiopzZ2OKo01JIBrHTjTi+veYAhZbpUfwZfShdTTHEj5OjdQ5VgaJU8yaBXmX48jgAACOg==", "cpu": [ "x64" ], @@ -1801,9 +1882,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-win32-arm64-msvc": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-win32-arm64-msvc/-/noxcturnal-win32-arm64-msvc-0.1.9.tgz", - "integrity": "sha512-EAaydqx5mgJfQn+V7LHyhpsdFkAYSwVMqz8AD3Hs/1slMvkwJgPjZv2KvZbgiJPGBaYhRg4X+CaOFW75LVx9iw==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-win32-arm64-msvc/-/noxcturnal-win32-arm64-msvc-0.1.11.tgz", + "integrity": "sha512-Q0Q0ll/qPzakp/GBmVDB/XthqyCkfM2CE5gZ78t9Ix/bkZDe5O0G7OyNkXRsGu49VcorxoGbBgSyF6CK+OaYlQ==", "cpu": [ "arm64" ], @@ -1817,9 +1898,9 @@ } }, "node_modules/@noxcturnal/noxcturnal-win32-x64-msvc": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-win32-x64-msvc/-/noxcturnal-win32-x64-msvc-0.1.9.tgz", - "integrity": "sha512-Vj1SNQ+ECsih/RSj+KSC3bXcrkGgETl6GOIIWIg6p5x6KXdLYPID9vyOjwlJa8B0Dmo6uK/ZNlIJX76bxc0WLg==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@noxcturnal/noxcturnal-win32-x64-msvc/-/noxcturnal-win32-x64-msvc-0.1.11.tgz", + "integrity": "sha512-as4HUwZkQegCcGoFzaLzlZbBqdx8BGgFhAoQsTsdJCUbNTxVH0f3R/AcRLOUJ6odfko0E2SSuJc70FI0zQ/IDg==", "cpu": [ "x64" ], @@ -2034,6 +2115,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.2.1.tgz", + "integrity": "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.5.tgz", @@ -2760,6 +2871,73 @@ "@babel/plugin-syntax-flow": "^7.12.1" } }, + "node_modules/babel-preset-expo": { + "version": "58.0.21", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-58.0.21.tgz", + "integrity": "sha512-EpfootmsfJKstMbSRI3qt7gwfoaRcGMTl20I3e8yr8/5CUqxqjK7PFKFI92Fc3sGdDRh4fXd1s+Ca1IXZNUlyw==", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.20.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.28.6", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-plugin-codegen": "0.88.0", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.4", + "babel-plugin-syntax-hermes-parser": "^0.36.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "expo-widgets": "^58.0.24", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + }, + "expo-widgets": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3400,33 +3578,33 @@ } }, "node_modules/expo": { - "version": "58.0.19", - "resolved": "https://registry.npmjs.org/expo/-/expo-58.0.19.tgz", - "integrity": "sha512-+MHH9Bu7OFWsAl8Ox44KgqbVIe72xbxzAi3vLPtVf14o5zHE39iCD//uw/4nyBlqZii6cFUZsvtJDbeRo5OhMA==", + "version": "58.0.20", + "resolved": "https://registry.npmjs.org/expo/-/expo-58.0.20.tgz", + "integrity": "sha512-6fSVY/08Mer8AHVdP4D++MHHUE+Tzug/+RT+QMGopy1+jMw/WO9Dr+F53FCy+N8zV9rXodEK8Tgr4tawEOYICw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^58.1.15", + "@expo/cli": "^58.1.16", "@expo/config": "~58.0.8", "@expo/config-plugins": "~58.0.10", "@expo/devtools": "~58.0.2", "@expo/dom-webview": "~58.0.2", - "@expo/fingerprint": "^0.21.6", + "@expo/fingerprint": "^0.21.7", "@expo/local-build-cache-provider": "^58.0.8", "@expo/log-box": "^58.0.13", "@expo/log-box-utils": "^58.0.2", "@expo/metro": "~58.0.0", - "@expo/metro-config": "~58.0.17", + "@expo/metro-config": "~58.0.18", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~58.0.20", + "babel-preset-expo": "~58.0.21", "expo-asset": "~58.0.18", "expo-constants": "~58.0.13", - "expo-file-system": "~58.0.11", + "expo-file-system": "~58.0.12", "expo-font": "~58.0.10", "expo-keep-awake": "~58.0.3", "expo-modules-autolinking": "~58.0.15", - "expo-modules-cli": "~0.3.2", - "expo-modules-core": "~58.0.26", + "expo-modules-cli": "~0.3.3", + "expo-modules-core": "~58.0.27", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.2.0" @@ -3515,9 +3693,9 @@ } }, "node_modules/expo-file-system": { - "version": "58.0.11", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-58.0.11.tgz", - "integrity": "sha512-accVZyXeA8RYUNk52ydkcHPnQaYMVJWlWvdF8pU0xLvt7ZPbaOgGzbgbeyTswjlsKSTmrAsKos4prx0PVM4ERA==", + "version": "58.0.12", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-58.0.12.tgz", + "integrity": "sha512-Ytua0iPjhacvM5eEpPFtrc2iWwdcIfQ3s/9ps0awxGCaHs54DI9weo8wPUJImfcKPmiseqVGM4TFg4lHfD0XmQ==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -3578,9 +3756,9 @@ } }, "node_modules/expo-modules-cli": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/expo-modules-cli/-/expo-modules-cli-0.3.2.tgz", - "integrity": "sha512-iSIZeNaqi5ku0H3pk2fAyIlM25TWveRr9Xcc+bIZSokhZ0ERLtT5Xc1qVZwszSo0GbXXEoA8zwiGnvaw1DydCQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/expo-modules-cli/-/expo-modules-cli-0.3.3.tgz", + "integrity": "sha512-nz+xyg5AM7eFYHKw3SAoBcyBLqPj7ff1+JZ/bIRsedBlJdDxXyWoIIgn8rB3pk9+2p1KBMsQ7TiORrI6iTJxdw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.20.0", @@ -3594,13 +3772,13 @@ } }, "node_modules/expo-modules-core": { - "version": "58.0.26", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-58.0.26.tgz", - "integrity": "sha512-HOuHt0nCBVv3E8+yMGeR6N6xa4WH+81CLqU/pWjDykt2NUBYGfXVMaWPsjwuBfeYOB/DcJTJR40yq+y9Ku/YcQ==", + "version": "58.0.27", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-58.0.27.tgz", + "integrity": "sha512-/sYuJW9bLkhu3VwUBtUAMLakk0fUtzth6H5o+nub035AHhq5sNA3zG0RHqj2d7GrkmkcATYjUU/5xTRLAMoz0A==", "license": "MIT", "dependencies": { - "expo-modules-jsi": "~58.0.19", - "expo-modules-macros": "0.22.0", + "expo-modules-jsi": "~58.0.20", + "expo-modules-macros": "0.24.0", "invariant": "^2.2.4" }, "peerDependencies": { @@ -3615,30 +3793,30 @@ } }, "node_modules/expo-modules-jsi": { - "version": "58.0.19", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-58.0.19.tgz", - "integrity": "sha512-dmYjRQnkSUdul6jZN3+yq6ZPKwiatUzLJW/pcXYx16q7QJW3i3+4RZ+MnxNeLNgg1rLrCR9F+dIgeh01+FxaFQ==", + "version": "58.0.20", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-58.0.20.tgz", + "integrity": "sha512-cBS3txcHqMRymzU2r5w24G5iZgGniDJ49Szyg85kd2J4tDr7yzJ95/6GIfjH/DOAGQ004JRU7+MtdhmLGQ9Mzw==", "license": "MIT", "peerDependencies": { "react-native": "*" } }, "node_modules/expo-modules-macros": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/expo-modules-macros/-/expo-modules-macros-0.22.0.tgz", - "integrity": "sha512-IHY+FkILfG19qsyf9G/z7dcWVgsyqdkWsvvob3n5nWKa2meCCAGeEjp42N68fG8Ta6dn/bsceuma7uh2i6ho9g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/expo-modules-macros/-/expo-modules-macros-0.24.0.tgz", + "integrity": "sha512-3CnIoiD5ftVxKm/ldEg9OrUubGxX1fPlVrO+hzyWM4xcFpOPGERvBJ4F9IrO4jGXswuME8NvXMmq3GnikZQ0mw==", "license": "MIT", "optionalDependencies": { - "@expo/modules-macros-linux-arm64": "0.22.0", - "@expo/modules-macros-linux-x64": "0.22.0", - "@expo/modules-macros-win32-arm64": "0.22.0", - "@expo/modules-macros-win32-x64": "0.22.0" + "@expo/modules-macros-linux-arm64": "0.24.0", + "@expo/modules-macros-linux-x64": "0.24.0", + "@expo/modules-macros-win32-arm64": "0.24.0", + "@expo/modules-macros-win32-x64": "0.24.0" } }, "node_modules/expo-router": { - "version": "58.0.26", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-58.0.26.tgz", - "integrity": "sha512-NaiGYRmAiwsRLeGLU4yEMeEDHzF1W8rOu1LKjpNgfVEWkPNlpqVjyXzgkPErg7xosQUBs33Dik3uKtz5PpDSUw==", + "version": "58.0.27", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-58.0.27.tgz", + "integrity": "sha512-a+fZ0tTzQp4+18RdFBSvtINm4SEBoBGzCNg2ejHRHvPEbqmiNMAJhPhh8xWWLytX4/JZhH4ksSzAzSzIzx8bqQ==", "license": "MIT", "dependencies": { "@expo/log-box": "^58.0.13", @@ -3664,7 +3842,7 @@ "peerDependencies": { "@expo/log-box": "^58.0.13", "@expo/metro-runtime": "^58.0.15", - "@expo/ui": "^58.0.23", + "@expo/ui": "^58.0.24", "@testing-library/react-native": ">= 14.0.0", "expo": "*", "expo-constants": "^58.0.13", @@ -3711,61 +3889,6 @@ } } }, - "node_modules/expo-router/node_modules/@expo/metro-runtime": { - "version": "58.0.15", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-58.0.15.tgz", - "integrity": "sha512-csALn54hZUkHFM3FofWlU5MQcJB62deCt+JtHOhQogRT+k/VTtTuhLQCrCUOtO2MAUsyhrcRRa6Nh79e1bkvfg==", - "license": "MIT", - "dependencies": { - "@expo/log-box": "^58.0.13", - "anser": "^1.4.9", - "pretty-format": "^29.7.0", - "stacktrace-parser": "^0.1.10", - "whatwg-fetch": "^3.0.0" - }, - "peerDependencies": { - "@expo/log-box": "^58.0.13", - "expo": "*", - "react": "*", - "react-dom": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.2.1.tgz", - "integrity": "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.2.0", - "@radix-ui/react-context": "1.3.0", - "@radix-ui/react-direction": "1.2.0", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-presence": "1.1.11", - "@radix-ui/react-primitive": "2.2.0", - "@radix-ui/react-roving-focus": "1.2.1", - "@radix-ui/react-use-controllable-state": "1.3.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/expo-router/node_modules/react-is": { "version": "19.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.1.tgz", @@ -3813,9 +3936,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "58.1.15", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.15.tgz", - "integrity": "sha512-+gBvfvmttvNqs8ceJe1SdiWBa6reocb/ZC8jMxsF8qFlCijAUQNqFMGCPkT/6zasFkQLUL8ZOFhf/9DKj2XaLg==", + "version": "58.1.16", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-58.1.16.tgz", + "integrity": "sha512-ULO+zBuF/6OEE4j5Dbi+Mv9YXkEIGj63QHdKy1deC09kU/C3GZCGXwlIESPSifqK2CUePsY0aYrsby0eE6362w==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.7", @@ -3829,7 +3952,7 @@ "@expo/log-box": "^58.0.13", "@expo/log-box-utils": "^58.0.2", "@expo/metro": "~58.0.0", - "@expo/metro-config": "58.0.17", + "@expo/metro-config": "58.0.18", "@expo/metro-file-map": "^58.0.5", "@expo/osascript": "^2.8.1", "@expo/package-manager": "^1.14.2", @@ -3837,7 +3960,7 @@ "@expo/plist": "^0.10.1", "@expo/prebuild-config": "^58.0.16", "@expo/require-utils": "^58.0.4", - "@expo/router-server": "^58.0.17", + "@expo/router-server": "^58.0.18", "@expo/schema-utils": "^58.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.1", @@ -3900,9 +4023,9 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "58.0.17", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-58.0.17.tgz", - "integrity": "sha512-9xZqpWIvb9/2ZH038WrWAtXQJsVep1ZG9FYh7cpoz6RGRo9SkmG97kMkQW7slKzL85Ppx8iW+68EHK9SQf/Zog==", + "version": "58.0.18", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-58.0.18.tgz", + "integrity": "sha512-qcaRdEVpsZa62t07NtiQe6J18OSzWHgvvhkRAtNpPUBqfVfaEl3fHdYf2kJMRMl/DJSrS6Jr6ymOLk74mPDGXA==", "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -3958,50 +4081,6 @@ } } }, - "node_modules/expo/node_modules/@expo/metro-config": { - "version": "58.0.17", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-58.0.17.tgz", - "integrity": "sha512-T2i8chQ5PsF6zgS10AixIteBlPu8oMAcqoD9MPk6CBQvcAdM4bE/x9VuDxuqKcAvFg+bRZMq1JfGAjJcRSmKkQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.20.0", - "@babel/core": "^7.20.0", - "@babel/generator": "^7.20.5", - "@expo/config": "~58.0.8", - "@expo/env": "~2.5.3", - "@expo/json-file": "~11.2.1", - "@expo/metro": "~58.0.0", - "@expo/platforms": "^58.0.1", - "@expo/require-utils": "^58.0.4", - "@expo/spawn-async": "^1.8.0", - "@jridgewell/gen-mapping": "^0.3.13", - "@jridgewell/remapping": "^2.3.5", - "@jridgewell/sourcemap-codec": "^1.5.5", - "@jridgewell/trace-mapping": "^0.3.31", - "@react-native/js-polyfills": "0.88.0", - "2g": "^1.0.1", - "browserslist": "^4.25.0", - "chalk": "^4.1.0", - "debug": "^4.3.2", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "hermes-parser": "^0.36.0", - "jsc-safe-url": "^0.2.4", - "lightningcss": "^1.30.1", - "noxcturnal": "^0.1.8", - "picomatch": "^4.0.4", - "postcss": "^8.5.23", - "resolve-from": "^5.0.0" - }, - "peerDependencies": { - "expo": "*" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - } - } - }, "node_modules/expo/node_modules/@expo/ws-tunnel": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.1.tgz", @@ -4024,73 +4103,6 @@ "node": ">= 0.6" } }, - "node_modules/expo/node_modules/babel-preset-expo": { - "version": "58.0.20", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-58.0.20.tgz", - "integrity": "sha512-pdOe2B3pR41uYvfMIWUxwz8YMqXuszMEBKTCCLnf2nvypybMEZ30z38J0IcwUCQ2H+hrWAXpueQzWQsdooXWOA==", - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.20.5", - "@babel/helper-module-imports": "^7.25.9", - "@babel/plugin-proposal-decorators": "^7.12.9", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.28.6", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-plugin-codegen": "0.88.0", - "babel-plugin-react-compiler": "^1.0.0", - "babel-plugin-react-native-web": "~0.21.4", - "babel-plugin-syntax-hermes-parser": "^0.36.0", - "babel-plugin-transform-flow-enums": "^0.0.2", - "debug": "^4.3.4" - }, - "peerDependencies": { - "@babel/runtime": "^7.20.0", - "expo": "*", - "expo-widgets": "^58.0.23", - "react-refresh": ">=0.14.0 <1.0.0" - }, - "peerDependenciesMeta": { - "@babel/runtime": { - "optional": true - }, - "expo": { - "optional": true - }, - "expo-widgets": { - "optional": true - } - } - }, "node_modules/expo/node_modules/expo-asset": { "version": "58.0.18", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-58.0.18.tgz", @@ -5562,9 +5574,9 @@ } }, "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.2.tgz", + "integrity": "sha512-rRtAR5XLJ8gCKd9du4qrKAsgCG1ObGP4gLeNJMa4vYSeq4bmBMOJk5IHkrviBV0q7uoAJlVd8HEQYQSVUb4FGQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -5586,9 +5598,9 @@ } }, "node_modules/noxcturnal": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/noxcturnal/-/noxcturnal-0.1.9.tgz", - "integrity": "sha512-TzgJ3xjHpa/WLBNLnyj1eiy2lpNggv73RW0Fo9+xTGPMwdkIk2+G/H4w9uGBDjMZr7FccjS0CWyPq8dFYVnwGQ==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/noxcturnal/-/noxcturnal-0.1.11.tgz", + "integrity": "sha512-nKgZ4mQpWz5Nry5KRqRcyCv37Uc24VH9UkNsIJJ58sC7ubyXtaiP4zfjOAsGiTVZIJ6rcfkmbYS0P4plVhdHEQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -5598,14 +5610,14 @@ "node": ">=18" }, "optionalDependencies": { - "@noxcturnal/noxcturnal-darwin-arm64": "0.1.9", - "@noxcturnal/noxcturnal-darwin-x64": "0.1.9", - "@noxcturnal/noxcturnal-linux-arm64-gnu": "0.1.9", - "@noxcturnal/noxcturnal-linux-arm64-musl": "0.1.9", - "@noxcturnal/noxcturnal-linux-x64-gnu": "0.1.9", - "@noxcturnal/noxcturnal-linux-x64-musl": "0.1.9", - "@noxcturnal/noxcturnal-win32-arm64-msvc": "0.1.9", - "@noxcturnal/noxcturnal-win32-x64-msvc": "0.1.9" + "@noxcturnal/noxcturnal-darwin-arm64": "0.1.11", + "@noxcturnal/noxcturnal-darwin-x64": "0.1.11", + "@noxcturnal/noxcturnal-linux-arm64-gnu": "0.1.11", + "@noxcturnal/noxcturnal-linux-arm64-musl": "0.1.11", + "@noxcturnal/noxcturnal-linux-x64-gnu": "0.1.11", + "@noxcturnal/noxcturnal-linux-x64-musl": "0.1.11", + "@noxcturnal/noxcturnal-win32-arm64-msvc": "0.1.11", + "@noxcturnal/noxcturnal-win32-x64-msvc": "0.1.11" } }, "node_modules/npm-package-arg": { @@ -5912,9 +5924,9 @@ } }, "node_modules/postcss": { - "version": "8.5.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", - "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -6242,9 +6254,9 @@ "license": "MIT" }, "node_modules/react-native-worklets": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.13.0.tgz", - "integrity": "sha512-1OrhAeAKaZF4XrBa4YbPQbKzNOpjh52HDlId8e6ete1OCHNdAlrQHzG4gznJ225EAj3dgQXHViJvK51M9B52jQ==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.13.1.tgz", + "integrity": "sha512-sX55Vx89/Y9uJmvAbAGoHXBZvCWXtYPGWPJ9Z9yl0h76mP9A10ULlSrixzGqBFLXeQMBpIpyJEhWpzXP7GG6YA==", "license": "MIT", "peer": true, "dependencies": { diff --git a/mobile/package.json b/mobile/package.json index 74b3b4b..f5492a9 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -4,14 +4,14 @@ "main": "expo-router/entry", "dependencies": { "@expo/vector-icons": "^15.0.2", - "expo": "~58.0.19", + "expo": "~58.0.20", "expo-clipboard": "~58.0.4", "expo-constants": "~58.0.13", "expo-document-picker": "~58.0.4", - "expo-file-system": "~58.0.11", + "expo-file-system": "~58.0.12", "expo-font": "~58.0.10", "expo-linking": "~58.0.14", - "expo-router": "~58.0.26", + "expo-router": "~58.0.27", "expo-status-bar": "~58.0.4", "expo-system-ui": "~58.0.6", "react": "19.3.0", diff --git a/mobile/scripts/check-npm-audit.mjs b/mobile/scripts/check-npm-audit.mjs index 6003a17..1927284 100644 --- a/mobile/scripts/check-npm-audit.mjs +++ b/mobile/scripts/check-npm-audit.mjs @@ -25,21 +25,33 @@ try { const vulnerabilities = report.vulnerabilities ?? {}; const expected = { '@expo/cli': { - via: ['@expo/code-signing-certificates', 'node-forge', 'zod'], + via: ['@expo/config', '@expo/metro-config', '@expo/prebuild-config', 'zod'], nodes: ['node_modules/expo/node_modules/@expo/cli'], }, - '@expo/code-signing-certificates': { - via: ['node-forge'], - nodes: ['node_modules/@expo/code-signing-certificates'], + '@expo/config': { + via: ['deepmerge'], + nodes: ['node_modules/@expo/config'], + }, + '@expo/local-build-cache-provider': { + via: ['@expo/config'], + nodes: ['node_modules/@expo/local-build-cache-provider'], + }, + '@expo/metro-config': { + via: ['@expo/config'], + nodes: ['node_modules/@expo/metro-config'], + }, + '@expo/prebuild-config': { + via: ['@expo/config'], + nodes: ['node_modules/@expo/prebuild-config'], + }, + deepmerge: { + via: ['https://github.com/advisories/GHSA-gcqx-w48m-w2h4'], + nodes: ['node_modules/deepmerge'], }, expo: { - via: ['@expo/cli'], + via: ['@expo/cli', '@expo/config', '@expo/local-build-cache-provider', '@expo/metro-config'], nodes: ['node_modules/expo'], }, - 'node-forge': { - via: ['https://github.com/advisories/GHSA-86w9-cpqp-85rv'], - nodes: ['node_modules/node-forge'], - }, zod: { via: ['https://github.com/advisories/GHSA-mrhx-c7xc-m3w5'], nodes: ['node_modules/zod'], @@ -66,6 +78,6 @@ if (JSON.stringify(normalizedGraph(vulnerabilities)) !== JSON.stringify(normaliz } process.stdout.write( - 'Known Expo CLI advisories only: node-forge GHSA-86w9-cpqp-85rv and zod GHSA-mrhx-c7xc-m3w5.\n' + + 'Known Expo CLI advisories only: deepmerge GHSA-gcqx-w48m-w2h4 and zod GHSA-mrhx-c7xc-m3w5.\n' + 'They are build-tool dependencies in the current Expo SDK and have no compatible upstream fix.\n', ); diff --git a/mobile/scripts/check-npm-audit.test.mjs b/mobile/scripts/check-npm-audit.test.mjs index 0f9e932..e88be99 100644 --- a/mobile/scripts/check-npm-audit.test.mjs +++ b/mobile/scripts/check-npm-audit.test.mjs @@ -9,10 +9,13 @@ import { fileURLToPath } from 'node:url'; const checker = fileURLToPath(new URL('./check-npm-audit.mjs', import.meta.url)); const expected = { vulnerabilities: { - '@expo/cli': { via: ['@expo/code-signing-certificates', 'node-forge', 'zod'], nodes: ['node_modules/expo/node_modules/@expo/cli'] }, - '@expo/code-signing-certificates': { via: ['node-forge'], nodes: ['node_modules/@expo/code-signing-certificates'] }, - expo: { via: ['@expo/cli'], nodes: ['node_modules/expo'] }, - 'node-forge': { via: [{ url: 'https://github.com/advisories/GHSA-86w9-cpqp-85rv' }], nodes: ['node_modules/node-forge'] }, + '@expo/cli': { via: ['@expo/config', '@expo/metro-config', '@expo/prebuild-config', 'zod'], nodes: ['node_modules/expo/node_modules/@expo/cli'] }, + '@expo/config': { via: ['deepmerge'], nodes: ['node_modules/@expo/config'] }, + '@expo/local-build-cache-provider': { via: ['@expo/config'], nodes: ['node_modules/@expo/local-build-cache-provider'] }, + '@expo/metro-config': { via: ['@expo/config'], nodes: ['node_modules/@expo/metro-config'] }, + '@expo/prebuild-config': { via: ['@expo/config'], nodes: ['node_modules/@expo/prebuild-config'] }, + deepmerge: { via: [{ url: 'https://github.com/advisories/GHSA-gcqx-w48m-w2h4' }], nodes: ['node_modules/deepmerge'] }, + expo: { via: ['@expo/cli', '@expo/config', '@expo/local-build-cache-provider', '@expo/metro-config'], nodes: ['node_modules/expo'] }, zod: { via: [{ url: 'https://github.com/advisories/GHSA-mrhx-c7xc-m3w5' }], nodes: ['node_modules/zod'] }, }, }; From 69172b7a0aa333eb4c52df88f8b269653be99b87 Mon Sep 17 00:00:00 2001 From: Antics Decoded <237784870+AnticsDecoded@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:49:27 +0100 Subject: [PATCH 3/3] Refresh generated support evidence --- docs/10-what-is-supported.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/10-what-is-supported.md b/docs/10-what-is-supported.md index ef70db3..66b1745 100644 --- a/docs/10-what-is-supported.md +++ b/docs/10-what-is-supported.md @@ -89,5 +89,5 @@ transaction types. Any network connectivity from the signer. ## Evidence -314 tests. See [03-verification-status.md](03-verification-status.md) for +318 tests. See [03-verification-status.md](03-verification-status.md) for what each one establishes, and what it does not.