Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/build-nassl.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions docs/packages/nassl.yaml
Original file line number Diff line number Diff line change
@@ -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
98 changes: 98 additions & 0 deletions patches/nassl/5.4.0/0001-build_config-add-a-riscv64-target.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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 <git@ludovic.dev>
---
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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 <git@ludovic.dev>
---
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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
"<repo>-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 <git@ludovic.dev>
---
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
Loading