diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index bec50c3..a40b0ad 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -30,8 +30,8 @@ jobs: cache-provider: basic - name: Test, lint, and assemble debug and release run: ./gradlew --no-daemon test lint assembleDebug assembleRelease - - name: Verify release JNI field names - run: ./scripts/verify-release-jni-contract.sh + - name: Verify release shrinker contracts + run: ./scripts/verify-release-shrinker-contracts.sh - name: Install emulator system image run: sdkmanager "emulator" "system-images;android-35;google_apis;x86_64" - name: Create emulator diff --git a/PROJECT_SPEC.md b/PROJECT_SPEC.md index 4fee763..a23f526 100644 --- a/PROJECT_SPEC.md +++ b/PROJECT_SPEC.md @@ -1082,8 +1082,14 @@ authentication and default diagnostic checks subsequently passed on the physical release artifact after force-stop/reopen. Preserving that encrypted key through the next signed version update was then proven when permanent-key alpha.4 installed over alpha.3 with local state intact and authenticated with -the retained key without re-import. Alpha distribution is direct invited -sharing while Phase 5 is open. Because the repository is public, any published +the retained key without re-import. Permanent-key alpha.5 then proved the +resumable signing and in-place installation path, but a release-only R8 +relocation broke sshlib 0.4.2's bundled Ed25519 provider before authentication. +Alpha.5 is rejected. The alpha.6 source applies the narrow provider keep rule, +passes an isolated minified SSH probe, and adds a mapping/DEX release regression +gate. It still needs independent CI, permanent signing, and physical update +acceptance. Alpha distribution is direct invited sharing while Phase 5 is open. +Because the repository is public, any published GitHub prerelease would be public. Sufficient invited technical-alpha use is the remaining Phase 5 boundary. diff --git a/README.md b/README.md index c6ef164..cef5dd8 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,22 @@ like responses, while a real terminal remains underneath for interactive work. ## Alpha status **[Phase 5: Alpha polish](docs/STATUS.md) is in progress.** Its feature slices -and owner-device acceptance through alpha.4 are complete. The remaining product -boundary is enough invited technical-alpha use to evaluate whether ten real -users can complete small remote tasks for two weeks and provide useful feedback. - -The current accepted signed build is `0.1.0-alpha.4`; the next source candidate -is `0.1.0-alpha.5`. Signed builds are shared directly with invited testers. +and owner-device acceptance through alpha.4 are complete. Alpha.5 installed in +place but is rejected because a new release-shrinker failure prevents SSH +connection. The alpha.6 source correction now passes the minified SSH proof and +release regression gate; it still needs independent CI, permanent signing, and +physical update acceptance. The remaining product boundary is enough invited +technical-alpha use to evaluate whether ten real users can complete small +remote tasks for two weeks and provide useful feedback. + +The current accepted signed build is `0.1.0-alpha.4`; `0.1.0-alpha.5` is an +immutable rejected artifact, and `0.1.0-alpha.6` is the current corrected source +candidate. Signed builds are shared directly with invited testers. There is no public signed APK or GitHub prerelease while Phase 5 is open. Testers should obtain the APK and its checksum from the owner through the agreed private channel, then follow the [technical-alpha guide](docs/alpha-testing.md). -Alpha.5 must pass the signed update and physical-device regression path before -it replaces alpha.4 for testers. +Do not distribute alpha.5. Threadline has no supported production release. Do not use the alpha for privileged or sensitive systems. @@ -287,8 +291,16 @@ artifact. Public CI now produces source-identified unsigned candidates while the permanent signing key remains local. The permanent-key `0.1.0-alpha.4` artifact installed over alpha.3 in place, retained onboarding, profiles, trusted hosts, transcripts, settings, and the encrypted saved key, then authenticated with that -key without re-import. Alpha.5 signed-update acceptance is the next operational -check; invited technical-alpha use remains the product-validation boundary. +key without re-import. The permanent-key alpha.5 artifact was selected through +the resumable local signing path, verified against the established certificate, +and installed over the existing physical release in place. Its first connection +attempt exposed a release-only cbssh Ed25519 provider failure caused by R8 +relocation. Alpha.5 is rejected; an isolated minified probe reproduces the +failure and proves the narrow keep-rule correction intended for alpha.6. +The alpha.6 source applies that narrow rule and makes the release gate verify +the exact provider class names in both the R8 mapping and assembled DEX. Its +isolated minified password SSH proof passes. Independent CI, permanent signing, +and physical acceptance remain before invited technical-alpha use resumes. Use these records according to their purpose: diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9db3874..92314a0 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -12,3 +12,11 @@ -keepclassmembers class org.connectbot.terminal.ScreenCell { ; } + +# cbssh 0.4.2 registers these bundled JCA implementations by a class name +# derived from Ed25519Provider's runtime package. R8 must preserve the provider +# package and both name-loaded implementations or host-key verification fails +# before authentication. +-keep class org.connectbot.sshlib.crypto.ed25519.Ed25519Provider { *; } +-keep class org.connectbot.sshlib.crypto.ed25519.Ed25519KeyFactory { *; } +-keep class org.connectbot.sshlib.crypto.ed25519.Ed25519KeyPairGenerator { *; } diff --git a/docs/HISTORY.md b/docs/HISTORY.md index ce1a8da..d03f0eb 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -75,6 +75,12 @@ onboarding state, profile, trusted host, transcript history, settings, and encry survived, and the retained key authenticated and completed `pwd` without re-import. This closed the installed-update preservation boundary. Direct invited sharing was selected for the open Phase 5 alpha, with no public prerelease until the remaining technical-alpha use boundary is evaluated. +Alpha.5 then proved the resumable signing and in-place installation path but is +rejected because R8 relocation broke sshlib 0.4.2's bundled Ed25519 provider +before authentication. An isolated minified probe reproduced the failure and +proved the narrow keep-rule correction. The alpha.6 source now applies that rule +and adds a release mapping/DEX gate for the provider class names; permanent-key +and physical acceptance remain. Additional device and OEM coverage is opportunistic alpha evidence rather than a separate Pixel gate. See [STATUS.md](STATUS.md) rather than this chronology for the active boundary. diff --git a/docs/STATUS.md b/docs/STATUS.md index 9a888d8..45a1369 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -69,19 +69,39 @@ authenticated without re-import and completed `pwd`. This closes the installed update-preservation boundary. See the [alpha.4 update-preservation investigation](investigations/2026-08-09-alpha4-update-preservation.md). -The next source candidate is `0.1.0-alpha.5` (`10005`). Its pre-invite +The alpha.5 source candidate is `0.1.0-alpha.5` (`10005`). Its pre-invite hardening pins GitHub Actions to immutable commits, validates the Gradle wrapper and distribution checksum, enforces dependency checksums, verifies 16 KiB APK alignment after signing, upgrades ConnectBot `sshlib` to 0.4.2, and adds the -core API 35 instrumented suite to CI. Alpha.4 remains the latest accepted signed -artifact until alpha.5 passes the signed update and physical-device regression -path. +core API 35 instrumented suite to CI. The new local wrapper selected the exact +successful `main` candidate, signed it with the permanent key, and produced an +alpha.5 artifact whose checksum, identity, certificate, v2/v3 signatures, and +16 KiB alignment verify. Android accepted it as an in-place update over the +existing Galaxy S25 Ultra installation, but the installed release could not +connect. An isolated minified API 35 probe reproduced a pre-authentication +`NullPointerException`: sshlib 0.4.2 constructs its bundled Ed25519 provider, +while R8 had moved that provider into the default package and broken its runtime +package lookup. A narrow provider keep rule made the same minified probe pass +authentication, PTY creation, and shell startup. Alpha.5 is rejected and alpha.4 +remains the latest accepted signed artifact. See the +[alpha.5 signing and release-shrinker investigation](investigations/2026-08-10-alpha5-signing-update-progress.md). + +The current corrected source is `0.1.0-alpha.6` (`10006`). It preserves the +three cbssh Ed25519 JCA classes whose binary names are part of the provider +contract, and the renamed release verifier now requires those exact names in +both the R8 mapping and assembled DEX alongside the existing termlib JNI field +checks. The gate rejects the pre-fix alpha.5 output and accepts alpha.6. An +isolated minified API 35 probe with the exact production rule completed password +authentication, PTY creation, and shell startup. The full local JVM, lint, +debug, release, connected Android, password fixture, and encrypted-key fixture +paths pass. See the +[alpha.6 Ed25519 shrinker correction](investigations/2026-08-10-alpha6-ed25519-shrinker-correction.md). ## Remaining Phase 5 boundaries -- Alpha.5 signed-update acceptance on the physical device: retained local - state, password and imported-key authentication, and the structured/raw - same-session path. +- Merge alpha.6 and pass independent CI, then sign its exact `main` candidate + and repeat physical update, retained-state, authentication, Diagnostics, and + same-session structured/raw acceptance. - Technical-alpha use sufficient to evaluate the Phase 5 exit criterion. ## Alpha distribution: direct invited sharing, decided 2026-08-09 @@ -106,12 +126,12 @@ position. Two things worth recording so this isn't relitigated from memory: - **The signing question is separate and already settled.** The permanent update - lineage was established at alpha.1 and verified again at alpha.4 against + lineage was established at alpha.1 and verified again at alpha.5 against certificate SHA-256 `102893bc…`, with the release key never entering CI. That holds under either distribution choice, so it argues for neither. - **The transparency half of the public option is already done.** Checksums, the certificate fingerprint, signature schemes and 16 KiB alignment are published - in `investigations/2026-08-09-alpha4-update-preservation.md`, in a public + in `investigations/2026-08-10-alpha5-signing-update-progress.md`, in a public repository. Only the signed tester APK stays private. The asymmetry closes it: private can become public later, published can't become diff --git a/docs/development/release-signing.md b/docs/development/release-signing.md index 308cc04..fe983c2 100644 --- a/docs/development/release-signing.md +++ b/docs/development/release-signing.md @@ -60,6 +60,26 @@ The update retained the release app's onboarding state, profile, trusted host, transcript history, settings, and encrypted saved key. That retained key then authenticated without re-import and completed a structured command. +Permanent-key alpha.5 uses the same certificate. Its verified APK SHA-256 is: + +```text +2c238b48c14c3933ab3dcd468352370b0fb1e0d5a20476c10e6fe39b7d38d8bf +``` + +The artifact identifies as `io.github.r055le.threadline`, version +`0.1.0-alpha.5` (`10005`), verifies with APK signature schemes v2 and v3, and +is 16 KiB page aligned. It was selected from the exact successful `main` build +by `scripts/sign-latest-alpha.sh` and Android accepted it as an in-place update +over the existing physical release. Its first connection regression exposed a +release-only cbssh Ed25519 provider failure after R8 relocation, so alpha.5 is +rejected and must not be distributed or overwritten. Preserve it as immutable +evidence and advance the corrected build to alpha.6. + +The current corrected source is `0.1.0-alpha.6` (`10006`). Its release rules +preserve cbssh's name-loaded Ed25519 provider classes, and the release gate +checks their exact binary names in both the R8 mapping and assembled DEX. Its +permanent-key artifact and physical update evidence do not exist yet. + ## Signing-key boundary The long-lived release keystore and its passwords must never enter this diff --git a/docs/investigations/2026-08-10-alpha5-signing-update-progress.md b/docs/investigations/2026-08-10-alpha5-signing-update-progress.md new file mode 100644 index 0000000..3408085 --- /dev/null +++ b/docs/investigations/2026-08-10-alpha5-signing-update-progress.md @@ -0,0 +1,85 @@ +# Alpha.5 signing and release-shrinker rejection + +**Date:** 2026-08-10 +**Phase:** 5, alpha polish +**Status:** Rejected + +## Boundary + +Alpha.5 is the first permanent-key candidate produced through the resumable +local signing path. The public candidate matched current `main`, the signed APK +belonged to the established update lineage, and Android accepted it over +alpha.4. Its first connection regression then exposed a release-only shrinker +failure introduced by the sshlib 0.4.2 path. Alpha.5 is rejected. + +## Candidate and signing record + +GitHub Actions produced the unsigned alpha.5 candidate from source commit +`66a743f6a89c4a8dce7846d6411e0febf77c1caa`. The local wrapper selected the +successful Android push run for that exact commit, downloaded its uniquely +named candidate, and passed it to the existing signing verifier. The permanent +release key and passwords remained local. + +The locally signed artifact independently verifies as: + +- package: `io.github.r055le.threadline` +- version: `0.1.0-alpha.5` (`10005`) +- APK SHA-256: `2c238b48c14c3933ab3dcd468352370b0fb1e0d5a20476c10e6fe39b7d38d8bf` +- signing certificate SHA-256: `102893bcc2fa4b70fb451661579c717c6c2b917296a99baefa6d9e9d1d13e7fc` +- signer subject: `CN=Threadline` +- signature schemes: APK v2 and v3 verified +- alignment: 16 KiB page-aware zip alignment verified + +The certificate matches the permanent update lineage established by alpha.1. + +## Physical evidence so far + +The owner installed alpha.5 over the existing Threadline release on the Galaxy +S25 Ultra. Android accepted it as an in-place update rather than a second app. +This proves the application ID, version progression, and signing lineage are +compatible on the physical device. + +Google Play Protect offered to scan this APK. That prompt had not appeared for +the earlier Threadline alpha installs. This is recorded as a platform behavior +observation only. It is not evidence that Google approved, rejected, or +independently established the safety of the APK. + +The owner then found that the installed alpha.5 could not connect. This stopped +the acceptance run before authentication, structured commands, or raw-terminal +behavior could be evaluated. + +## Release-only reproduction and root cause + +The production JVM adapter and the API 35 debug Android fixture suite continued +to pass with sshlib 0.4.2. An isolated, manifest-declared probe inside the exact +minified release path reproduced the physical failure on API 35 before +authentication. The adapter reported a generic connection failure caused by a +`NullPointerException` during Ed25519 host-signature verification. + +Retracing the optimized stack located the failure in cbssh's bundled +`Ed25519Provider.setup()`. sshlib 0.4.2 now constructs this fallback provider +while converting the Ed25519 host key. The provider derives its implementation +package through `Ed25519Provider::class.java.package.name`; R8 had moved the +class into the default package, so that lookup returned null. Alpha.4 used +sshlib 0.4.1 and did not exercise this new path. + +A narrow temporary keep rule for cbssh's Ed25519 provider package was tested in +the same minified probe. The corrected build reached authentication, created a +PTY, started the shell, and reported success. This proves the correction shape, +and the follow-on source correction advances as alpha.6. It is recorded in the +[alpha.6 Ed25519 shrinker investigation](2026-08-10-alpha6-ed25519-shrinker-correction.md). + +## Required correction + +Do not distribute alpha.5 or overwrite its immutable artifact. Alpha.6 now +preserves the cbssh Ed25519 provider classes needed by their JCA name-based +registration and adds a release gate that fails the broken mapping and DEX. +After merge and permanent signing, repeat: + +- a minified password and imported-key fixture proof; +- permanent-key signing and installation over alpha.5; +- retained-state and Diagnostics checks; and +- the structured/raw same-session regression on the physical device. + +Alpha.4 remains the latest accepted tester build. No password, passphrase, +private key, private endpoint, or fixture identity is recorded here. diff --git a/docs/investigations/2026-08-10-alpha6-ed25519-shrinker-correction.md b/docs/investigations/2026-08-10-alpha6-ed25519-shrinker-correction.md new file mode 100644 index 0000000..cb8922b --- /dev/null +++ b/docs/investigations/2026-08-10-alpha6-ed25519-shrinker-correction.md @@ -0,0 +1,70 @@ +# Alpha.6 Ed25519 shrinker correction + +**Date:** 2026-08-10 +**Phase:** 5, alpha polish +**Status:** Source correction accepted; permanent artifact pending + +## Boundary + +Alpha.5 was rejected after its installed release failed before authentication. +The [alpha.5 investigation](2026-08-10-alpha5-signing-update-progress.md) +records the physical report, isolated minified reproduction, retraced stack, +and cbssh Ed25519 provider root cause. Alpha.6 must preserve that provider's +name-loaded JCA classes and make the release gate reject the broken R8 output. + +## Correction + +The source advances to `0.1.0-alpha.6` (`10006`). Its R8 rules preserve only +the three cbssh classes required by this contract: + +- `Ed25519Provider`, whose runtime package supplies the registered class-name + prefix; +- `Ed25519KeyFactory`, which JCA loads by that registered name; and +- `Ed25519KeyPairGenerator`, which JCA also loads by name. + +The release verifier is now named `verify-release-shrinker-contracts.sh` +because it owns both the existing termlib JNI field-name checks and this JCA +class-identity check. For all three provider classes, it requires the R8 +mapping to retain the exact binary name and the assembled release DEX to contain +that exact class. + +The new verifier rejected the pre-fix alpha.5 release output because +`Ed25519Provider` was renamed. After the narrow keep rule and version bump, the +alpha.6 release mapping and DEX passed the same gate. + +## Release-shaped evidence + +An isolated diagnostic clone added a manifest-declared probe directly to a +separately identified, debug-key-signed minified release. The probe used the +production adapter and the local OpenSSH fixture. No diagnostic activity or +alternate application ID enters the product source. + +With the exact three-class production rule, the minified probe completed +Ed25519 host-signature verification, password authentication, PTY creation, and +shell startup on API 35. The temporary emulator was stopped without saving a +snapshot, and the fixture password was passed only through process memory. + +Repository verification then passed: + +- JVM tests, Android lint, debug assembly, and minified release assembly; +- all 69 connected Android tests, with the three credential-gated profiles + skipped by the ordinary runner as designed; +- the production JVM fixture adapter; +- both explicit Android password and encrypted imported-key fixture tests; and +- the new release shrinker-contract verifier. + +The first explicit Android fixture run immediately after the heavier connected +suite reached SSH but timed out on its later Ctrl-C completion case. An +immediate isolated rerun passed both credential-gated tests. No product change +was made for that one load-sensitive timeout. + +## Remaining acceptance + +Alpha.6 still needs to merge and pass the independent GitHub gate. The resulting +exact `main` candidate must then be signed with the permanent key and installed +over alpha.5. On the physical device, repeat retained-state, password and +imported-key authentication, Diagnostics, structured command, and same-session +raw-terminal checks before sharing it with invited testers. + +Alpha.4 remains the latest accepted tester build. No password, passphrase, +private key, private endpoint, or fixture identity is recorded here. diff --git a/docs/investigations/README.md b/docs/investigations/README.md index 56c1ae8..e6bf70b 100644 --- a/docs/investigations/README.md +++ b/docs/investigations/README.md @@ -32,3 +32,5 @@ technical claim. | 2026-08-03 | 5 | [Permanent alpha.3 physical acceptance](2026-08-03-alpha3-permanent-physical-acceptance.md) | permanent artifact verification, physical same-session stress, release/JNI closure, and landscape UX evidence | | 2026-08-08 | 5 | [Alpha.3 imported-key and diagnostic acceptance](2026-08-08-alpha3-imported-key-diagnostics-acceptance.md) | permanent-release imported-key auth after force-stop/reopen and exact default diagnostic privacy proof | | 2026-08-09 | 5 | [Alpha.4 installed-update preservation](2026-08-09-alpha4-update-preservation.md) | public-CI candidate provenance, permanent-key update, retained local state, and imported-key auth without re-import | +| 2026-08-10 | 5 | [Alpha.5 signing and release-shrinker rejection](2026-08-10-alpha5-signing-update-progress.md) | resumable signing proof, physical connection failure, minified reproduction, and cbssh Ed25519 provider root cause | +| 2026-08-10 | 5 | [Alpha.6 Ed25519 shrinker correction](2026-08-10-alpha6-ed25519-shrinker-correction.md) | narrow provider keep rule, mapping/DEX regression gate, and minified SSH proof | diff --git a/gradle.properties b/gradle.properties index 59f0103..b5af8e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 android.useAndroidX=true android.nonTransitiveRClass=true threadline.releaseApplicationId=io.github.r055le.threadline -threadline.versionCode=10005 -threadline.versionName=0.1.0-alpha.5 +threadline.versionCode=10006 +threadline.versionName=0.1.0-alpha.6 diff --git a/scripts/verify-release-jni-contract.sh b/scripts/verify-release-shrinker-contracts.sh similarity index 79% rename from scripts/verify-release-jni-contract.sh rename to scripts/verify-release-shrinker-contracts.sh index 1f25da9..01c5dec 100755 --- a/scripts/verify-release-jni-contract.sh +++ b/scripts/verify-release-shrinker-contracts.sh @@ -81,11 +81,31 @@ verify_class_fields() { done } +verify_class_identity() { + class_name=$1 + if ! grep -Fqx "$class_name -> $class_name:" "$mapping_file"; then + echo "$class_name is missing or renamed in the release mapping." >&2 + exit 1 + fi + if ! awk -v expected_class="$class_name" ' + $1 == "C" && $6 == expected_class { found = 1 } + END { exit found ? 0 : 1 } + ' "$dex_listing" + then + echo "Release DEX is missing required runtime class $class_name." >&2 + exit 1 + fi +} + apkanalyzer_command=$(find_apkanalyzer) dex_listing=$(mktemp) trap 'rm -f "$dex_listing"' EXIT "$apkanalyzer_command" dex packages --defined-only "$release_apk" > "$dex_listing" +verify_class_identity org.connectbot.sshlib.crypto.ed25519.Ed25519Provider +verify_class_identity org.connectbot.sshlib.crypto.ed25519.Ed25519KeyFactory +verify_class_identity org.connectbot.sshlib.crypto.ed25519.Ed25519KeyPairGenerator + verify_class_fields org.connectbot.terminal.CellRun \ fgRed fgGreen fgBlue bgRed bgGreen bgBlue bold underline italic blink \ reverse strike font dwl dhl chars runLength @@ -93,4 +113,4 @@ verify_class_fields org.connectbot.terminal.ScreenCell \ char combiningChars fgRed fgGreen fgBlue bgRed bgGreen bgBlue bold italic \ underline reverse strike width -echo "Release JNI field-name contract verified." +echo "Release shrinker contracts verified."