From d40c106a3b205280ccb4e32201b08d1981a0a1b7 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 21 Sep 2026 21:31:25 +0200 Subject: [PATCH 1/3] Bump OpenSSL to 3.5.8, and build arm64 with branch protection OpenSSL 3.0 has left support, so the statics we ship receive no more fixes. 3.5 is the current LTS and is maintained to 2030. The bump is also the only way to finish what #229 started. That change gave arm64 pointer authentication and branch target identification, but libhttrack.so came out with landing pads and no .note.gnu.property. The linker ANDs that note over every input, and the OpenSSL statics carried none. 3.0.15 cannot carry one, because its crypto/arm_arch.h has no BTI or PAC support, so no flag makes its assembly emit pads. Those macros arrived in 3.2.0. I built both to check. At 3.0.15 with the flag, 859 of 872 libcrypto.a members take the note, and the 13 without it are exactly the arm64 assembly. That is enough to clear the note for the whole library. At 3.5.8 all 982 take it, the engine still compiles and links, and libhttrack.so now reports BTI and PAC with retaa still at 0. x86_64 gets no control-flow flag, because lld drops the CET note on that target, which #230 measured. The image recipe now also builds on a pull request, without publishing. A broken OpenSSL build used to reach master before anything ran it. Signed-off-by: Xavier Roche Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/docker-image.yml | 9 ++++++++- docker/Dockerfile | 4 ++-- docker/build-openssl.sh | 12 +++++++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 6517a3ed..c0cc1525 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -8,6 +8,10 @@ on: push: branches: [master] paths: ['docker/**', '.github/workflows/docker-image.yml'] + # Build (without publishing) when a PR touches the recipe, so a broken OpenSSL + # build fails the PR instead of the next master push. + pull_request: + paths: ['docker/**', '.github/workflows/docker-image.yml'] schedule: - cron: "23 5 * * 1" # weekly: pick up OpenSSL patch releases workflow_dispatch: @@ -26,6 +30,7 @@ jobs: steps: - uses: actions/checkout@v7 - uses: docker/login-action@v4 + if: github.event_name != 'pull_request' with: registry: ghcr.io username: ${{ github.actor }} @@ -33,16 +38,18 @@ jobs: - uses: docker/build-push-action@v7 with: context: docker - push: true + push: ${{ github.event_name != 'pull_request' }} tags: | ghcr.io/xroche/httrack-android-build:latest ghcr.io/xroche/httrack-android-build:${{ github.sha }} - name: Extract OpenSSL statics (for the container-less emulator job) + if: github.event_name != 'pull_request' run: | id=$(docker create ghcr.io/xroche/httrack-android-build:latest) docker cp "$id":/opt/openssl-android ./openssl-android docker rm "$id" - uses: actions/upload-artifact@v7 + if: github.event_name != 'pull_request' with: name: openssl-android-statics path: openssl-android diff --git a/docker/Dockerfile b/docker/Dockerfile index f724bd67..f1c53e26 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,8 +21,8 @@ ARG ANDROID_BUILD_TOOLS=36.0.0 ARG ANDROID_PLATFORM_PREV=android-35 ARG ANDROID_BUILD_TOOLS_PREV=35.0.0 ARG NDK_VERSION=27.3.13750724 -ARG OPENSSL_VERSION=3.0.15 -ARG OPENSSL_SHA256=23c666d0edf20f14249b3d8f0368acaee9ab585b09e1de82107c66e1f3ec9533 +ARG OPENSSL_VERSION=3.5.8 +ARG OPENSSL_SHA256=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 ENV ANDROID_SDK_ROOT=/opt/android-sdk \ ANDROID_HOME=/opt/android-sdk \ diff --git a/docker/build-openssl.sh b/docker/build-openssl.sh index 5e51ccf4..cb4a85d9 100755 --- a/docker/build-openssl.sh +++ b/docker/build-openssl.sh @@ -13,9 +13,9 @@ # (wget, not curl: curl's threaded resolver fails on this host's Docker/kernel.) set -euo pipefail -OPENSSL_VERSION="${OPENSSL_VERSION:-3.0.15}" +OPENSSL_VERSION="${OPENSSL_VERSION:-3.5.8}" # Pin the hash — verify against https://www.openssl.org/source/ before bumping. -OPENSSL_SHA256="${OPENSSL_SHA256:-23c666d0edf20f14249b3d8f0368acaee9ab585b09e1de82107c66e1f3ec9533}" +OPENSSL_SHA256="${OPENSSL_SHA256:-a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2}" MIN_API="${MIN_API:-21}" OUT="${OUT:-/opt/openssl-android}" ABIS="${ABIS:-arm64-v8a x86_64}" @@ -46,8 +46,14 @@ for abi in $ABIS; do echo "=== Building OpenSSL ${OPENSSL_VERSION} for ${abi} (${target}) ===" builddir="$work/build-$abi" cp -a "$src" "$builddir" + # Match the branch protection Android.mk gives our own code. The linker ANDs + # .note.gnu.property over every input, so one unmarked member clears it. + harden=() + if [ "$abi" = arm64-v8a ]; then + harden=(-mbranch-protection=standard) + fi ( cd "$builddir" - ./Configure "$target" "-D__ANDROID_API__=${MIN_API}" \ + ./Configure "$target" "-D__ANDROID_API__=${MIN_API}" "${harden[@]}" \ no-shared no-tests no-ui-console no-engine no-comp no-dso no-legacy \ --prefix="$OUT/$abi" --libdir=lib make -j"$(nproc)" build_libs From 3bf673d795ac1837524a752099a346ced504a8a8 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 21 Sep 2026 21:37:58 +0200 Subject: [PATCH 2/3] Pin the TLS security level, so the bump changes no handshake 3.5 raises OPENSSL_TLS_SECURITY_LEVEL from 1 to 2, and ssl_cert.c reads that macro to set each context's default. The engine never calls SSL_CTX_set_security_level, so it inherits whatever the library shipped with. Level 2 lifts the floor from 80 bits to 112. That refuses a 1024-bit DH group and a SHA-1 signature algorithm, which servers untouched since Logjam still offer. HTTrack exists to copy old sites, so the bump would have broken crawls that work today. The certificate-side tightenings do not bite, because the engine verifies no certificate by design, but these two checks run whatever verify_mode says. The macro sits behind an #ifndef, so -DOPENSSL_TLS_SECURITY_LEVEL=1 pins what 3.0.15 shipped. I read it back out of the built libssl.a. ssl_cert_new stores 1 with the define and 2 without it, at the same instruction. Branch protection is unaffected, and all 982 libcrypto.a members still carry the note. Pinning also stops the next bump moving it again. The engine should set the level itself rather than inherit it, but that change belongs to the httrack repo. Signed-off-by: Xavier Roche Co-Authored-By: Claude Opus 5 (1M context) --- docker/build-openssl.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/build-openssl.sh b/docker/build-openssl.sh index cb4a85d9..8b26445f 100755 --- a/docker/build-openssl.sh +++ b/docker/build-openssl.sh @@ -48,12 +48,15 @@ for abi in $ABIS; do cp -a "$src" "$builddir" # Match the branch protection Android.mk gives our own code. The linker ANDs # .note.gnu.property over every input, so one unmarked member clears it. + # OPENSSL_TLS_SECURITY_LEVEL pins what 3.0.15 shipped. 3.5 defaults to 2, which + # refuses the sub-2048-bit DH groups that old sites still offer. harden=() if [ "$abi" = arm64-v8a ]; then harden=(-mbranch-protection=standard) fi ( cd "$builddir" ./Configure "$target" "-D__ANDROID_API__=${MIN_API}" "${harden[@]}" \ + -DOPENSSL_TLS_SECURITY_LEVEL=1 \ no-shared no-tests no-ui-console no-engine no-comp no-dso no-legacy \ --prefix="$OUT/$abi" --libdir=lib make -j"$(nproc)" build_libs From 29ab350087fcc428d68be9be3d5eba7350d8c00c Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 21 Sep 2026 21:40:52 +0200 Subject: [PATCH 3/3] Make build-openssl.sh the only place the OpenSSL version lives android.yml and codeql.yml each set OPENSSL_VERSION and passed it to the script without the matching OPENSSL_SHA256. After the bump they would have fetched 3.0.15 and checked it against 3.5.8's digest, so both jobs would fail. They now pass no version at all and the script's default rules. The cache key moves with them. It keyed on the version, so a new compile flag at an unchanged version would have restored a stale OpenSSL and hidden the flag. It now keys on a hash of the recipe, which the branch protection flag needs. A version override that carries no hash is refused outright, because the default hash belongs to the default version. Signed-off-by: Xavier Roche Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/android.yml | 7 ++++--- .github/workflows/codeql.yml | 3 +-- docker/build-openssl.sh | 12 ++++++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 8100a346..b5913f20 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -21,7 +21,6 @@ concurrency: env: NDK_VERSION: r27d - OPENSSL_VERSION: "3.0.15" # native and assemble are the two required checks. Never give either a job-level `if:`, # because a skipped check run counts as success and auto-merge would land on a build that never ran. @@ -51,12 +50,14 @@ jobs: uses: actions/cache@v6 with: path: /tmp/openssl-android - key: openssl-${{ env.OPENSSL_VERSION }}-ndk-${{ env.NDK_VERSION }}-${{ env.ABIS }} + # Key on the recipe itself. Keying on the version alone served a stale + # cache whenever a build flag changed but the version did not. + key: openssl-${{ hashFiles('docker/build-openssl.sh') }}-ndk-${{ env.NDK_VERSION }}-${{ env.ABIS }} - name: Build OpenSSL (cache miss) if: steps.ssl-cache.outputs.cache-hit != 'true' env: ANDROID_NDK_ROOT: ${{ steps.ndk.outputs.ndk-path }} - run: OUT=/tmp/openssl-android OPENSSL_VERSION="${OPENSSL_VERSION}" bash docker/build-openssl.sh + run: OUT=/tmp/openssl-android bash docker/build-openssl.sh - name: Vendor libiconv run: tools/ci/vendor-libiconv.sh - name: Stage OpenSSL statics into prebuild/ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5833acdf..f1c667ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,7 +23,6 @@ concurrency: env: NDK_VERSION: r27d - OPENSSL_VERSION: "3.0.15" jobs: analyze: @@ -62,7 +61,7 @@ jobs: env: ANDROID_NDK_ROOT: ${{ steps.ndk.outputs.ndk-path }} run: | - OUT=/tmp/openssl-android ABIS=arm64-v8a OPENSSL_VERSION="${OPENSSL_VERSION}" bash docker/build-openssl.sh + OUT=/tmp/openssl-android ABIS=arm64-v8a bash docker/build-openssl.sh tools/ci/vendor-libiconv.sh OPENSSL_ANDROID_ROOT=/tmp/openssl-android ABIS=arm64-v8a tools/ci/fetch-openssl-statics.sh ( cd app/src/main && "${ANDROID_NDK_ROOT}/ndk-build" APP_ABI=arm64-v8a APP_PLATFORM=android-24 -j"$(nproc)" ) diff --git a/docker/build-openssl.sh b/docker/build-openssl.sh index 8b26445f..c9d7238a 100755 --- a/docker/build-openssl.sh +++ b/docker/build-openssl.sh @@ -13,9 +13,17 @@ # (wget, not curl: curl's threaded resolver fails on this host's Docker/kernel.) set -euo pipefail -OPENSSL_VERSION="${OPENSSL_VERSION:-3.5.8}" +DEFAULT_VERSION=3.5.8 # Pin the hash — verify against https://www.openssl.org/source/ before bumping. -OPENSSL_SHA256="${OPENSSL_SHA256:-a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2}" +DEFAULT_SHA256=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 +OPENSSL_VERSION="${OPENSSL_VERSION:-$DEFAULT_VERSION}" +OPENSSL_SHA256="${OPENSSL_SHA256:-$DEFAULT_SHA256}" +# The default hash belongs to the default version, so a version override that +# brings no hash would check one release against another's digest. +if [ "$OPENSSL_VERSION" != "$DEFAULT_VERSION" ] && [ "$OPENSSL_SHA256" = "$DEFAULT_SHA256" ]; then + echo "set OPENSSL_SHA256 when overriding OPENSSL_VERSION" >&2 + exit 1 +fi MIN_API="${MIN_API:-21}" OUT="${OUT:-/opt/openssl-android}" ABIS="${ABIS:-arm64-v8a x86_64}"