diff --git a/.github/scripts/big-endian-test.sh b/.github/scripts/big-endian-test.sh
new file mode 100755
index 00000000..a3f8bb9e
--- /dev/null
+++ b/.github/scripts/big-endian-test.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+# big-endian-test.sh
+#
+# Copyright (C) 2006-2025 wolfSSL Inc.
+#
+# This file is part of wolfProvider.
+#
+# wolfProvider is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# wolfProvider is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with wolfProvider. If not, see .
+#
+# Runs the standard build and unit tests on a big-endian target. Meant to
+# run inside an s390x container; the test-deps image is amd64-only, so the
+# toolchain is installed here instead.
+#
+# The unit runner already continues past a failing test, so only a signal
+# stops the suite. This restarts at the next case after a crash and takes a
+# backtrace, so one crash does not hide the rest of the results.
+
+set -euo pipefail
+
+LOG_DIR=${LOG_DIR:-/wolfprov/be-logs}
+RUN_LOG="${LOG_DIR}/unit-run.log"
+SUMMARY="${LOG_DIR}/summary.txt"
+
+export DEBIAN_FRONTEND=noninteractive
+apt-get update
+apt-get install -y --no-install-recommends \
+ autoconf automake build-essential ca-certificates gdb git libtool make \
+ perl pkg-config
+
+# The workspace is bind-mounted from the runner, so its owner is not root.
+git config --global --add safe.directory /wolfprov
+
+# Fail loudly rather than silently retesting x86-64 if the qemu platform
+# selection ever regresses.
+cat > /tmp/endian-check.c <<'EOF'
+int main(void)
+{
+#if !defined(__BYTE_ORDER__) || (__BYTE_ORDER__ != __ORDER_BIG_ENDIAN__)
+#error "not a big-endian target"
+#endif
+ return 0;
+}
+EOF
+gcc -o /tmp/endian-check /tmp/endian-check.c
+/tmp/endian-check
+echo "confirmed big-endian target: $(uname -m)"
+
+mkdir -p "${LOG_DIR}"
+
+# Build only. The suite is driven below so a crash does not end the run.
+WOLFPROV_SKIP_TEST=1 ./scripts/build-wolfprovider.sh
+
+source ./scripts/env-setup
+
+total=$(./test/unit.test --list | grep -cE '^ *[0-9]+: ')
+echo "unit test cases: ${total}"
+
+crashed=()
+failed=()
+next=1
+
+while [ "${next}" -le "${total}" ]; do
+ echo "=== running cases ${next}..${total} ==="
+ rc=0
+ # shellcheck disable=SC2046
+ ./test/unit.test $(seq "${next}" "${total}") 2>&1 | tee -a "${RUN_LOG}" \
+ || rc=${PIPESTATUS[0]}
+
+ # Under 128 means the runner reached the end on its own, reporting any
+ # failures as it went. Only a signal leaves cases unrun.
+ if [ "${rc}" -lt 128 ]; then
+ break
+ fi
+
+ last=$(grep -oE '^#### Start: [0-9]+' "${RUN_LOG}" | tail -1 \
+ | grep -oE '[0-9]+$')
+ name=$(./test/unit.test --list | grep -E "^ *${last}: " \
+ | sed -E 's/^ *[0-9]+: //')
+ echo "=== case ${last} (${name}) died with exit ${rc}; getting backtrace ==="
+ crashed+=("${last} ${name}")
+
+ ./libtool --mode=execute gdb -batch \
+ -ex run -ex 'bt full' -ex 'info registers' \
+ -ex 'thread apply all bt' \
+ --args ./test/unit.test "${last}" \
+ > "${LOG_DIR}/backtrace-${last}.log" 2>&1 || true
+ echo "--- backtrace for case ${last} (${name}) ---"
+ cat "${LOG_DIR}/backtrace-${last}.log"
+
+ next=$((last + 1))
+done
+
+mapfile -t failed < <(grep -oE '^#### FAILED: [0-9]+ - [^ ]+' "${RUN_LOG}" \
+ | sed -E 's/^#### FAILED: //' | sort -u || true)
+
+{
+ echo "big-endian unit results on $(uname -m)"
+ echo "cases: ${total}"
+ echo
+ echo "crashed (${#crashed[@]}):"
+ printf ' %s\n' "${crashed[@]:-none}"
+ echo
+ echo "failed (${#failed[@]}):"
+ printf ' %s\n' "${failed[@]:-none}"
+ echo
+ echo "byte-order import path (the reason this job exists):"
+ grep -E '^#### (SUCCESS|FAILED): [0-9]+ - test_(rsa|dh|ec)_fromdata' \
+ "${RUN_LOG}" || echo " never reached"
+} | tee "${SUMMARY}"
+
+if [ "${#crashed[@]}" -gt 0 ] || [ "${#failed[@]}" -gt 0 ]; then
+ exit 1
+fi
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
index b199923d..02f3514b 100644
--- a/.github/workflows/README.md
+++ b/.github/workflows/README.md
@@ -96,7 +96,7 @@ gh workflow run pr-osp-select.yml --ref -f jobs="all"
### What runs in the nightly fan-out
-43 workflows total: 40 third-party OSS integrations, 2 internal
+44 workflows total: 40 third-party OSS integrations, 3 internal
validations, and the static-analysis suite. Every one of these patches
the upstream project (where needed) via `osp/wolfProvider//*.patch`
from [wolfssl/osp](https://github.com/wolfssl/osp), builds it against
@@ -176,6 +176,7 @@ exercised, with and without `WOLFPROV_FORCE_FAIL=1`.
| `debian-package.yml` | End-to-end check: builds the wolfprov `.deb`s and confirms they install cleanly on a fresh container and the provider loads. |
| `openssl-version.yml` | Sweeps every upstream `openssl-3.X.Y` release tag — catches breakage from OpenSSL point releases before they hit our matrix defaults. |
| `static-analysis.yml` | cppcheck, clang scan-build, Facebook Infer. Heavy enough that it lives in the nightly fan-out rather than per-PR. |
+| `big-endian.yml` | Builds and runs the unit tests on s390x (big-endian) under qemu emulation. Guards the `#ifdef LITTLE_ENDIAN_ORDER` byte-order paths, which no other job exercises. |
Sanitizers (ASan+UBSan, TSan) run on every PR/push — see the PR table
above. They're fast enough with caching to gate merges, so they don't
@@ -264,6 +265,35 @@ The scan-build and infer thresholds are baseline-based, not strict —
they let pre-existing issues slide but flag obvious regressions.
Bringing them to 0 is a future cleanup.
+## Big-endian coverage
+
+`big-endian.yml` runs nightly (and via `ci:big-endian` / `ci:all`). Every
+other job in this repo runs on x86-64, so the `#else` half of each
+`#ifdef LITTLE_ENDIAN_ORDER` block in `src/wp_params.c` is otherwise never
+compiled or executed. This job closes that gap.
+
+GitHub offers no big-endian runners, so it emulates s390x with qemu
+binfmt (`docker/setup-qemu-action`) and builds inside a `debian:bookworm`
+s390x container. Emulation is roughly an order of magnitude slower than
+native, which is why it is nightly-only rather than a PR gate.
+
+`.github/scripts/big-endian-test.sh` installs a toolchain — the test-deps
+image is amd64-only, so it cannot be reused here — asserts the target
+really is big-endian, then hands off to `scripts/build-wolfprovider.sh`
+with no arguments. That is the same entry point `simple.yml` uses, at its
+default OpenSSL + wolfSSL refs, and it runs `make test` itself. The
+endian assert means a qemu or platform regression fails the job instead
+of silently retesting x86-64.
+
+Everything after the emulation setup is therefore the stock build. The
+job is slow because every instruction is emulated, including the full
+OpenSSL and wolfSSL source builds, not because it does anything special.
+
+No big-endian-specific test case exists or is needed: `test_rsa_fromdata`
+already compares a wolfProvider `EVP_PKEY_fromdata` import against
+OpenSSL's via `EVP_PKEY_cmp`, which is exactly the comparison a byte-order
+mistake in the `OSSL_PARAM` import path breaks.
+
## Overhead regression testing
`perf-regression.yml` (workflow display name **Overhead Regression**) runs nightly at 07:00 UTC (and on
diff --git a/.github/workflows/big-endian.yml b/.github/workflows/big-endian.yml
new file mode 100644
index 00000000..e66e03f7
--- /dev/null
+++ b/.github/workflows/big-endian.yml
@@ -0,0 +1,64 @@
+name: Big-Endian
+
+# GitHub has no big-endian runners, so s390x runs under qemu binfmt
+# emulation. Emulation is slow, which is why this is nightly / ci:all
+# only and not a per-PR check.
+
+on:
+ workflow_call: {}
+ workflow_dispatch: {}
+
+# run-scoped: a reusable workflow's group keys off the caller, so a shared
+# group would let one nightly run cancel another's big-endian job.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.run_id }}
+ cancel-in-progress: false
+
+jobs:
+ big-endian:
+ name: Big-Endian s390x
+ runs-on: ubuntu-22.04
+ permissions:
+ contents: read
+ timeout-minutes: 240
+ steps:
+ - name: Checkout wolfProvider
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Enable s390x emulation
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: s390x
+
+ - name: Build and test on s390x
+ run: |
+ docker run --rm --platform linux/s390x \
+ -v "$PWD:/wolfprov" -w /wolfprov \
+ debian:bookworm \
+ /bin/bash /wolfprov/.github/scripts/big-endian-test.sh
+
+ - name: Summary
+ if: ${{ always() }}
+ run: |
+ if [ -f be-logs/summary.txt ] ; then
+ cat be-logs/summary.txt >> "$GITHUB_STEP_SUMMARY"
+ cat be-logs/summary.txt
+ fi
+
+ - name: Print build log on failure
+ if: ${{ failure() }}
+ run: |
+ if [ -f scripts/build-release.log ] ; then
+ tail -200 scripts/build-release.log
+ fi
+
+ # The full run is far too long for the console; backtraces live here.
+ - name: Upload logs
+ if: ${{ always() }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: big-endian-logs
+ path: be-logs/
+ if-no-files-found: warn
diff --git a/.github/workflows/nightly-osp.yml b/.github/workflows/nightly-osp.yml
index 32a423b9..b4eb0f03 100644
--- a/.github/workflows/nightly-osp.yml
+++ b/.github/workflows/nightly-osp.yml
@@ -167,5 +167,6 @@ jobs:
xmlsec-584: { needs: wave1-done, if: always(), uses: ./.github/workflows/xmlsec.yml, with: { wolfssl_refs_json: '["v5.8.4-stable"]' } }
# === Out-of-wave: not wolfssl-version-split ===
+ big-endian: { uses: ./.github/workflows/big-endian.yml }
static-analysis: { uses: ./.github/workflows/static-analysis.yml }
multi-compiler: { uses: ./.github/workflows/nightly-multi-compiler.yml }
diff --git a/.github/workflows/pr-osp-select.yml b/.github/workflows/pr-osp-select.yml
index c98eb7c8..a4c6a0e4 100644
--- a/.github/workflows/pr-osp-select.yml
+++ b/.github/workflows/pr-osp-select.yml
@@ -219,6 +219,10 @@ jobs:
needs: select
if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' xmlsec ')
uses: ./.github/workflows/xmlsec.yml
+ big-endian:
+ needs: select
+ if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' big-endian ')
+ uses: ./.github/workflows/big-endian.yml
multi-compiler:
needs: select
if: contains(needs.select.outputs.run, ' all ') || contains(needs.select.outputs.run, ' multi-compiler ')
diff --git a/src/wp_params.c b/src/wp_params.c
index 9ac19417..9b935969 100644
--- a/src/wp_params.c
+++ b/src/wp_params.c
@@ -27,10 +27,10 @@
/**
- * Read little-endian array of bytes representing a large number.
+ * Read local endian array of bytes representing a large number.
*
* @param [in, out] mp Multi-precision number.
- * @param [in] data Little-endian array of bytes.
+ * @param [in] data Local endian array of bytes.
* @param [in] len Length of array in bytes.
* @return 1 on success.
* @return 0 on failure.
@@ -39,12 +39,15 @@ int wp_mp_read_unsigned_bin_le(mp_int* mp, const unsigned char* data,
size_t len)
{
int ok = 1;
+ int rc;
+#ifdef LITTLE_ENDIAN_ORDER
unsigned char rdata[1024];
size_t i;
- int rc;
+#endif
WOLFPROV_ENTER(WP_LOG_COMP_PROVIDER, "wp_mp_read_unsigned_bin_le");
+#ifdef LITTLE_ENDIAN_ORDER
if (len > sizeof(rdata)) {
ok = 0;
}
@@ -65,6 +68,13 @@ int wp_mp_read_unsigned_bin_le(mp_int* mp, const unsigned char* data,
/* rdata may hold private key material (RSA d/p/q, EC/DH private). */
OPENSSL_cleanse(rdata, sizeof(rdata));
+#else
+ rc = mp_read_unsigned_bin(mp, data, (word32)len);
+ if (rc != 0) {
+ WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_LEVEL_DEBUG, "mp_read_unsigned_bin", rc);
+ ok = 0;
+ }
+#endif
WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);
return ok;
@@ -100,6 +110,9 @@ int wp_mp_to_unsigned_bin_le(mp_int* mp, unsigned char* data, size_t len)
data[len - 1 - i] = t;
}
}
+#else
+ (void)i;
+ (void)len;
#endif
WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok);