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 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