From 47c10a42f94de278b25c23ce40e8d8a17aafa4c1 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 5 Oct 2026 15:54:51 +0000 Subject: [PATCH 1/2] nassl: add riscv64 wheel build nassl statically links its own from-source OpenSSL (a legacy OpenSSL_1_0_2e build and a modern OpenSSL_1_1_1w build) and zlib rather than the system libraries, selected per-platform in build_config.py. That file has no riscv64 branch at all: its 64-bit Linux detection only special-cases aarch64, so riscv64 silently fell through to the x86_64 target. Patch build_config.py to add a riscv64 platform: the modern build uses OpenSSL's own "linux64-riscv64" Configure target (1.1.1w carries no riscv64 assembly at all, added only in the 3.x series, so this is a plain portable-C build with none of the large-shared-object asm relocation pitfalls a newer OpenSSL's riscv64 asm can hit); the legacy build's pinned 1.0.2e predates riscv64 entirely and falls back to "linux-generic64", mirroring how it already special-cases OSX_ARM64 two lines above. Workflow mirrors upstream's own build_wheels.yml/build_wheels_linux_aarch64.yml (cibuildwheel driving invoke's build.deps/build.nassl tasks), narrowed to riscv64, with one extra CIBW_BEFORE_ALL_LINUX step installing the riscv64 manylinux image's full perl package (its minimal perl-interpreter lacks FindBin/lib, which OpenSSL's Configure needs). Matrix is cp310..cp314, matching upstream's own non-abi3 wheel set; no cp314t, which upstream does not build either. --- .github/workflows/build-nassl.yml | 119 ++++++++++++++++++ docs/packages/nassl.yaml | 5 + ...01-build_config-add-a-riscv64-target.patch | 98 +++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 .github/workflows/build-nassl.yml create mode 100644 docs/packages/nassl.yaml create mode 100644 patches/nassl/5.4.0/0001-build_config-add-a-riscv64-target.patch diff --git a/.github/workflows/build-nassl.yml b/.github/workflows/build-nassl.yml new file mode 100644 index 00000000000..135cb03fe1a --- /dev/null +++ b/.github/workflows/build-nassl.yml @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +# +# Upstream builds Linux wheels via cibuildwheel in .github/workflows/build_wheels.yml +# (manylinux2014 x86_64/i686) and build_wheels_linux_aarch64.yml (same recipe under QEMU). +# Both drive invoke's build.deps/build.nassl tasks, which build OpenSSL (twice: a legacy +# OpenSSL_1_0_2e and a modern OpenSSL_1_1_1w extension) and zlib from source via +# build_config.py before compiling the C extension against the result. +--- +name: Build nassl wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/nassl.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-nassl.yml' + - 'docs/packages/nassl.yaml' + - 'patches/nassl/**' + push: + branches: [main] + paths: + - '.github/workflows/build-nassl.yml' + - 'docs/packages/nassl.yaml' + - 'patches/nassl/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: nassl + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build nassl ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + python: ["cp310", "cp311", "cp312", "cp313", "cp314"] + + env: + NASSL_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout nassl ${{ env.NASSL_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: nabla-c0d3/nassl + ref: ${{ env.NASSL_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch nassl source + run: git apply python-wheels/patches/nassl/${{ env.NASSL_VERSION }}/*.patch + + - uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + env: + CIBW_ARCHS: riscv64 + CIBW_BUILD: ${{ matrix.python }}-manylinux_riscv64 + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # The riscv64 image's minimal perl-interpreter lacks FindBin/lib, which + # OpenSSL's Configure needs (CLAUDE.md gotcha 46). + CIBW_BEFORE_ALL_LINUX: >- + dnf -y install perl && + python -m pip install setuptools invoke && + invoke build.deps + CIBW_BEFORE_BUILD: python -m pip install setuptools invoke && invoke build.nassl + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: python -m pytest {project}/tests + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nassl-${{ env.NASSL_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish nassl ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: nassl-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/nassl.yaml b/docs/packages/nassl.yaml new file mode 100644 index 00000000000..ae0f07ffc41 --- /dev/null +++ b/docs/packages/nassl.yaml @@ -0,0 +1,5 @@ +package-name: nassl +source-code: https://github.com/nabla-c0d3/nassl +license: AGPL-3.0-only +versions: +- version: 5.4.0 diff --git a/patches/nassl/5.4.0/0001-build_config-add-a-riscv64-target.patch b/patches/nassl/5.4.0/0001-build_config-add-a-riscv64-target.patch new file mode 100644 index 00000000000..688d9c20eb5 --- /dev/null +++ b/patches/nassl/5.4.0/0001-build_config-add-a-riscv64-target.patch @@ -0,0 +1,98 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 5 Oct 2026 00:00:00 +0000 +Subject: [PATCH] build_config: add a riscv64 target + +CURRENT_PLATFORM's Linux/64-bit branch only checks machine() == "aarch64" +before falling back to LINUX_64 for everything else, so on riscv64 it +silently selects LINUX_64 and both OpenSSL builds Configure with +"linux-x86_64" - the wrong target, picked only because riscv64 was never +considered rather than because it was ruled out. + +Add a LINUX_RISCV64 platform and route it to each OpenSSL build's own +target: + +- ModernOpenSslBuildConfig (pinned to OpenSSL_1_1_1w) uses + "linux64-riscv64", a real Configure target on that tag. It carries no + riscv64-specific assembly in this OpenSSL release (RISC-V crypto asm + landed in the 3.x series, well after 1.1.1 went EOL) - verified there is + no crypto/*/asm/*riscv64* source anywhere in the OpenSSL_1_1_1w tree - + so the build is effectively the same portable C codegen as any other + target here, without the relocation-range asm pitfalls newer OpenSSL + riscv64 asm can hit in a large shared object (see CLAUDE.md gotcha 625 + in the RISE python-wheels repo, hit by a different, newer-OpenSSL + package). +- LegacyOpenSslBuildConfig (pinned to OpenSSL_1_0_2e) predates riscv64 + entirely - that tag's Configure has no riscv* target at all - so it + falls back to "linux-generic64" the same way this class already special + -cases OSX_ARM64 to "linux-aarch64" two lines above. _OPENSSL_CONF_CMD + already hardcodes "no-asm" for this legacy build on every platform, so + the generic target's C fallback is exactly what was going to be built + anyway. + +Also add LINUX_RISCV64 to all_linux_platforms(), so setup.py's +-Wl,--exclude-libs=ALL (hiding the statically-linked OpenSSL symbols to +avoid colliding with a system libssl/libcrypto the interpreter may also +load, see upstream issue #95) applies on riscv64 too, the same as on +every other Linux architecture nassl supports. + +Upstream-Status: To upstream [not yet submitted; riscv64 is not part of nabla-c0d3/nassl's own CI matrix, so nothing depends on it, but the fix is small, has no RISE-specific assumptions, and is generally applicable] + +Signed-off-by: Ludovic Henry +--- + build_config.py | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/build_config.py b/build_config.py +index 4cf8af5..41da25f 100644 +--- a/build_config.py ++++ b/build_config.py +@@ -30,6 +30,7 @@ class SupportedPlatformEnum(Enum): + OSX_ARM64 = 7 + LINUX_ARM64 = 8 + LINUX_ARM32 = 9 ++ LINUX_RISCV64 = 10 + + @classmethod + def all_linux_platforms(cls) -> List["SupportedPlatformEnum"]: +@@ -38,6 +39,7 @@ class SupportedPlatformEnum(Enum): + cls.LINUX_64, + cls.LINUX_ARM32, + cls.LINUX_ARM64, ++ cls.LINUX_RISCV64, + ] + + +@@ -51,6 +53,8 @@ if architecture()[0] == "64bit": + elif platform in ["linux", "linux2"]: + if machine() == "aarch64": + CURRENT_PLATFORM = SupportedPlatformEnum.LINUX_ARM64 ++ elif machine() == "riscv64": ++ CURRENT_PLATFORM = SupportedPlatformEnum.LINUX_RISCV64 + else: + CURRENT_PLATFORM = SupportedPlatformEnum.LINUX_64 + elif platform == "win32": +@@ -158,6 +162,8 @@ class OpenSslBuildConfig(BuildConfig, ABC): + openssl_target = "linux-aarch64" + elif self.platform == SupportedPlatformEnum.LINUX_ARM32: + openssl_target = "linux-armv4" ++ elif self.platform == SupportedPlatformEnum.LINUX_RISCV64: ++ openssl_target = "linux64-riscv64" + else: + raise ValueError("Unknown platform") + +@@ -263,6 +269,12 @@ class LegacyOpenSslBuildConfig(OpenSslBuildConfig): + if self.platform == SupportedPlatformEnum.OSX_ARM64: + # Note there's no debug build variant available here + return "linux-aarch64" ++ if self.platform == SupportedPlatformEnum.LINUX_RISCV64: ++ # OpenSSL 1.0.2e (this legacy build's pinned tag) predates riscv64 support; ++ # it has no "linux64-riscv64" Configure target at all. Fall back to the ++ # generic 64-bit target, which the base _OPENSSL_CONF_CMD above already ++ # builds with "no-asm" regardless of target. ++ return "linux-generic64" + return super()._get_build_target(should_build_for_debug) + + @property +-- +2.43.0 From 330e0092fa8e9768a2fa4474cecbadbc639bf087 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 5 Oct 2026 18:15:12 +0000 Subject: [PATCH 2/2] nassl: fix riscv64 CI red (stale cert-chain test, flaky zlib fetch) CI run https://github.com/riseproject-dev/python-wheels/actions/runs/37336888859 was red on 3 of 5 interpreter legs, from two independent, non-riscv64 causes: - cp313/cp314 both failed tests/cert_chain_verifier_test.py::test_valid_certificate_chain with OpenSSL error 20 ("unable to get local issuer certificate"). That test fetches www.github.com's live leaf cert at test time and hardcodes a 2018-era Sectigo intermediate to chain it against tests/mozilla.pem; GitHub's real issuing CA has since rotated (now "Sectigo Public Server Authentication CA DV E36"), so the hardcoded intermediate no longer matches the live leaf's issuer on any platform today, not just riscv64. No newer upstream release fixes this past the 5.4.0 tag, and nassl's Kali Linux packaging has hit the same test break before (error 10 instead of 20, same live-fetch-vs-hardcoded-cert root cause). Pinning today's correct intermediate would only move the next CA rotation's break further out, so skip the test instead (0002). - cp311 timed out (~1018s) and cp310/cp312 hung to the 90-minute job ceiling, all fetching the same https://www.zlib.net/fossils/zlib-1.2.13.tar.gz within the same few seconds, while cp313/cp314 fetched it in seconds -- a thundering-herd/ under-provisioned-server pattern on zlib.net's CDN-less legacy archive, not a one-off single-job flake. Fetch zlib from GitHub's tag archive instead (0003), which this same file already does for OpenSSL, carries the identical tree, and needs no src_path change (GitHub's vX.Y.Z-tag archive already extracts to zlib-1.2.13, same convention build-cvxopt.yml already relies on for SuiteSparse). Both patches verified to apply together, in sequence, against a clean 5.4.0 checkout. --- ...live-github.com-dependent-cert-chain.patch | 89 +++++++++++++++++++ ...fetch-zlib-from-GitHub-s-tag-archive.patch | 59 ++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 patches/nassl/5.4.0/0002-tests-skip-the-live-github.com-dependent-cert-chain.patch create mode 100644 patches/nassl/5.4.0/0003-build_config-fetch-zlib-from-GitHub-s-tag-archive.patch diff --git a/patches/nassl/5.4.0/0002-tests-skip-the-live-github.com-dependent-cert-chain.patch b/patches/nassl/5.4.0/0002-tests-skip-the-live-github.com-dependent-cert-chain.patch new file mode 100644 index 00000000000..1b455da83f2 --- /dev/null +++ b/patches/nassl/5.4.0/0002-tests-skip-the-live-github.com-dependent-cert-chain.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 5 Oct 2026 00:00:00 +0000 +Subject: [PATCH] tests: skip the live-github.com-dependent cert chain test + +tests/cert_chain_verifier_test.py::TestCertificateChainVerifier:: +test_valid_certificate_chain's certificate_chain_as_x509 fixture does not +use a bundled fixture chain: it calls ssl.get_server_certificate( +("www.github.com", 443)) to fetch whatever leaf certificate GitHub is +serving right now, then pairs that live leaf with a hardcoded PEM for +"Sectigo ECC Domain Validation Secure Server CA" (captured 2018-11-02, +per the cert's own notBefore) as the "intermediate", and asks nassl's +CertificateChainVerifier to build a chain from those two up to a root in +the bundled tests/mozilla.pem trust store. + +This reproduced as a deterministic failure - not a flake - identically on +both the cp313 and cp314 legs of this riscv64 build: + + nassl.cert_chain_verifier.CertificateChainVerificationFailed: + Verification failed with OpenSSL error code 20: "unable to get local + issuer certificate" + +Root cause has nothing to do with riscv64, this build, or nassl's +statically-linked OpenSSL_1_1_1w: + +- mozilla.pem is present and loads correctly (the test gets as far as a + real OpenSSL chain-validation error, not a file/collection error), so + this is not a staged-fixture/packaging problem. +- CertificateChainVerifier.verify() builds the chain purely from the PEM + bytes it is handed (X509_STORE_CTX.set0_trusted_stack/set0_untrusted + over in-memory certs); it never touches the system or build's default + CA path, so no OpenSSL build flag affecting CA-path defaults is + involved either. +- ssl.get_server_certificate() goes through Python's stdlib ssl module + (the runner's/manylinux image's system OpenSSL), not nassl's own + bundled OpenSSL_1_1_1w, and it is that *live* leaf, not a stale one, + that fails to chain. +- What has actually changed is GitHub's own serving CA: as of this run, + www.github.com's ECDSA leaf is issued by "Sectigo Public Server + Authentication CA DV E36", a newer Sectigo intermediate generation + that replaced "Sectigo ECC Domain Validation Secure Server CA" since + this test's fixture was written. The hardcoded intermediate's subject + no longer matches the live leaf's issuer at all, so OpenSSL can't find + an issuer for the leaf - exactly what error 20 means - regardless of + interpreter, architecture, or OpenSSL build. +- This is a known failure mode of this exact test, not introduced by + this port: nassl's Kali Linux packaging has separately hit this same + test failing from the same live-fetch-vs-hardcoded-cert design, there + with error 10 ("certificate has expired") instead of error 20 - a + different symptom of the same root cause (an external, live TLS + endpoint checked against certificate material frozen in source at + some point in the past). +- The sibling test, test_expired_certificate_chain, uses a fully static, + self-contained (if actually expired) chain and is unaffected; only the + live-fetch-dependent happy-path test breaks under CA rotation. + +Since GitHub will keep rotating its serving CA on its own schedule, +hardcoding today's correct intermediate would only move the next break +further out, not fix the underlying fragility - so skip the test rather +than re-pin another intermediate that will go stale again. + +Upstream-Status: To upstream [not yet submitted; the live-github.com dependency and the hardcoded intermediate are both upstream's own test design, not anything riscv64/RISE-specific, but no PR has been opened yet] + +Signed-off-by: Ludovic Henry +--- + tests/cert_chain_verifier_test.py | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/tests/cert_chain_verifier_test.py b/tests/cert_chain_verifier_test.py +index cd561bc..4160645 100644 +--- a/tests/cert_chain_verifier_test.py ++++ b/tests/cert_chain_verifier_test.py +@@ -43,6 +43,14 @@ cS0chWBNKPFNpvDKdrdKRe+oAkr2jU+ubgIxAODheSr2XhcA7oz9HmedGdMhlrd9 + + + class TestCertificateChainVerifier: ++ @pytest.mark.skip( ++ reason="certificate_chain_as_x509 hardcodes the 2018-era 'Sectigo ECC Domain " ++ "Validation Secure Server CA' intermediate and chains it against a live leaf " ++ "fetched from www.github.com:443 at test time; github.com's real issuing CA has " ++ "since rotated (now 'Sectigo Public Server Authentication CA DV E36'), so the " ++ "hardcoded intermediate no longer matches the live leaf's issuer and chain " ++ "building fails with OpenSSL error 20 on any platform, not just riscv64." ++ ) + def test_valid_certificate_chain(self, certificate_chain_as_x509: list[X509]) -> None: + path_validator = CertificateChainVerifier.from_file(Path(__file__).absolute().parent / "mozilla.pem") + path_validator.verify(certificate_chain_as_x509) +-- +2.43.0 diff --git a/patches/nassl/5.4.0/0003-build_config-fetch-zlib-from-GitHub-s-tag-archive.patch b/patches/nassl/5.4.0/0003-build_config-fetch-zlib-from-GitHub-s-tag-archive.patch new file mode 100644 index 00000000000..8cd6160db12 --- /dev/null +++ b/patches/nassl/5.4.0/0003-build_config-fetch-zlib-from-GitHub-s-tag-archive.patch @@ -0,0 +1,59 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 5 Oct 2026 00:00:00 +0000 +Subject: [PATCH] build_config: fetch zlib from GitHub's tag archive + +ZlibBuildConfig.src_tar_gz_url points at +"https://www.zlib.net/fossils/zlib-1.2.13.tar.gz" - the "fossils" path +zlib.net uses for anything that isn't the single current release, served +with none of the CDN the front-page download gets. + +In one riscv64 run of this workflow, the 5 interpreter legs all start +their CIBW_BEFORE_ALL_LINUX (and so this exact fetch) within the same +few seconds. 3 of the 5 (cp310, cp311, cp312) got stuck on it: cp311's +urlopen() timed out after ~1018s and failed the job in ~20 minutes; +cp310 and cp312 hung with no timeout firing at all until the job's own +90-minute ceiling cancelled them. The other two legs (cp313, cp314) +fetched the same URL in seconds. That split - most legs fine, a few +stuck on the exact same small legacy archive at the exact same moment - +is a thundering-herd/under-provisioned-server pattern, not a one-off +single-job blip or a general riscv64 runner reachability problem. + +GitHub's own tag archive for zlib carries the identical tree (zlib's +release tarball is a plain export of the tagged commit, nothing +generated on top), sits behind GitHub's CDN, and is already exactly how +this same file fetches OpenSSL two classes up +(OpenSslBuildConfig.src_tar_gz_url). Swapping to it needs no change to +src_path: GitHub's archive for a "vX.Y.Z"-shaped tag already extracts to +"-X.Y.Z" (the leading "v" dropped), i.e. "zlib-1.2.13" - the exact +directory src_path already points at, the same convention this repo's +own build-cvxopt.yml already relies on for SuiteSparse's GitHub tag +archive. + +Upstream-Status: Inappropriate [our own CI matrix's concurrent fetches are what overload zlib.net's small, CDN-less fossils archive; GitHub's CDN-backed tag archive carries the identical tree and isn't worth carrying as an upstream change] + +Signed-off-by: Ludovic Henry +--- + build_config.py | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/build_config.py b/build_config.py +index 41da25f..2358f5e 100644 +--- a/build_config.py ++++ b/build_config.py +@@ -377,7 +377,12 @@ class ModernOpenSslBuildConfig(OpenSslBuildConfig): + class ZlibBuildConfig(BuildConfig): + @property + def src_tar_gz_url(self) -> str: +- return "https://www.zlib.net/fossils/zlib-1.2.13.tar.gz" ++ # zlib.net's "fossils" archive (old releases aren't on the front page/CDN) ++ # stalls under concurrent fetches: 3 of 5 riscv64 CI jobs in one run either ++ # timed out or hung to the 90-minute job ceiling fetching this exact URL while ++ # the other 2 fetched it in seconds. Same tree, via GitHub's tag archive, which ++ # this file already relies on for OpenSSL two classes up. ++ return "https://github.com/madler/zlib/archive/refs/tags/v1.2.13.tar.gz" + + @property + def src_path(self) -> Path: +-- +2.43.0