From 7b00e14cb3d9721884abf98cb727e27e9a8e9bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 13 Jul 2026 19:30:10 +0200 Subject: [PATCH 1/2] ci: add GitHub Actions test pipeline Add the wolfCert CI pipeline and supporting scripts. Workflows (.github/workflows): - pr.yml: the merge gate. Canonical config on CMake (-Werror) + autoconf + ASan/UBSan, plus per-PR feature/config gating -- EST-only, SCEP-only, server-off, key-alg variants (NO_RSA, ECC-only, RSA-only, no-3DES), TLS 1.3-only, ML-DSA per-level, static-mem, no-malloc, the header-only user_settings build, a macOS build, and the cheap configure-must-fail assertions. - nightly.yml: re-runs the wolfSSL-variant matrix against fresh master, seeds the wolfSSL prefix caches for PR restores, and runs the macOS extras and full negative-config set. - lint.yml: GPL header check and CMake<->autoconf parity of both the library and test source lists. - sanitizers.yml, interop.yml: ASan/UBSan/TSan/valgrind and third-party EST/SCEP interop (best-effort, neutral-skip on dependency failure). Scripts and actions (scripts/ci, .github/actions): - build-wolfssl.sh: single source of truth mapping a config name to wolfSSL ./configure flags; the composite build-wolfssl action caches the install prefix keyed on the wolfSSL commit + flag hash. interop restores only. - assert-configure-fails.sh, check-buildsystem-parity.sh (guards both the library- and test-source lists), sigpipe-launcher.sh. Build-system parity: - Register the EST integration tests Makefile.am was missing so `make check` and ctest run the same set. docs/CI.md documents the pipeline and how to reproduce a job locally. --- .github/actions/build-wolfcert-cli/action.yml | 44 +++ .github/actions/build-wolfssl/action.yml | 107 ++++++++ .github/workflows/interop.yml | 140 ++++++++++ .github/workflows/lint.yml | 57 ++++ .github/workflows/nightly.yml | 140 ++++++++++ .github/workflows/pr.yml | 251 ++++++++++++++++++ .github/workflows/sanitizers.yml | 134 ++++++++++ .gitignore | 17 +- Makefile.am | 19 +- README.md | 3 + docs/CI.md | 64 +++++ scripts/ci/assert-configure-fails.sh | 153 +++++++++++ scripts/ci/build-wolfssl.sh | 234 ++++++++++++++++ scripts/ci/check-buildsystem-parity.sh | 73 +++++ scripts/ci/sigpipe-launcher.sh | 17 ++ 15 files changed, 1446 insertions(+), 7 deletions(-) create mode 100644 .github/actions/build-wolfcert-cli/action.yml create mode 100644 .github/actions/build-wolfssl/action.yml create mode 100644 .github/workflows/interop.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/nightly.yml create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/sanitizers.yml create mode 100644 docs/CI.md create mode 100755 scripts/ci/assert-configure-fails.sh create mode 100755 scripts/ci/build-wolfssl.sh create mode 100755 scripts/ci/check-buildsystem-parity.sh create mode 100755 scripts/ci/sigpipe-launcher.sh diff --git a/.github/actions/build-wolfcert-cli/action.yml b/.github/actions/build-wolfcert-cli/action.yml new file mode 100644 index 0000000..8f56d87 --- /dev/null +++ b/.github/actions/build-wolfcert-cli/action.yml @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: build-wolfcert-cli +description: > + Build wolfSSL (cached, config `full`) and wolfCert with the CLIs and test + server enabled, then export WOLFCERT_BUILD so the interop scripts' + wolfcert_bin() helper resolves wolfcert-client / wolfcert-server. + +inputs: + wolfssl-ref: + description: "wolfSSL git ref to build" + required: false + default: master + +runs: + using: composite + steps: + - name: Install build dependencies + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake pkg-config python3 + + - name: Build/restore wolfSSL (full) + uses: ./.github/actions/build-wolfssl + with: + config: full + ref: ${{ inputs.wolfssl-ref }} + save: "false" # restore-only: nightly.yml owns the shared `full` cache + # (interop's 5 jobs run concurrently and would otherwise + # race to save the same key). + + - name: Build wolfCert (CLIs + server) + shell: bash + run: | + set -euo pipefail + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_CLI=ON \ + -DWOLFCERT_ENABLE_SERVER=ON + cmake --build build -j "$(nproc)" + echo "WOLFCERT_BUILD=${{ github.workspace }}/build" >> "$GITHUB_ENV" + # wolfCert links wolfSSL from the install prefix; make it findable at + # runtime for the CLI binaries the interop scripts invoke. + echo "LD_LIBRARY_PATH=$WOLFSSL_PREFIX/lib:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV" diff --git a/.github/actions/build-wolfssl/action.yml b/.github/actions/build-wolfssl/action.yml new file mode 100644 index 0000000..474b383 --- /dev/null +++ b/.github/actions/build-wolfssl/action.yml @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: build-wolfssl +description: > + Build and cache a named wolfSSL configuration for wolfCert. The install + prefix is cached (via actions/cache) keyed on the resolved wolfSSL commit + plus a hash of the exact ./configure flags, so a hit skips the whole build. + On pull requests the cache is restore-only; scheduled runs pass save=true to + (re)seed it. Exposes the install prefix as the `prefix` output and the + WOLFSSL_PREFIX environment variable. + +inputs: + config: + description: "wolfSSL config name understood by scripts/ci/build-wolfssl.sh" + required: true + ref: + description: "wolfSSL git ref to build (branch, tag or SHA)" + required: false + default: master + save: + description: "Save the cache after a miss (true on scheduled reseed, false on PR)" + required: false + default: "false" + +outputs: + prefix: + description: "Absolute path to the wolfSSL install prefix" + value: ${{ steps.meta.outputs.prefix }} + cache-hit: + description: "Whether the wolfSSL prefix was restored from cache" + value: ${{ steps.restore.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Resolve flags, wolfSSL commit and cache key + id: meta + shell: bash + run: | + set -euo pipefail + script="${GITHUB_ACTION_PATH}/../../../scripts/ci/build-wolfssl.sh" + repo="${WOLFSSL_REPO:-https://github.com/wolfSSL/wolfssl.git}" + + # Exact configure flags -> the single source of truth for the key. + flags="$("$script" --print-flags "${{ inputs.config }}")" + + # Resolve the ref to a commit so a moving branch (master) refreshes the + # cache when upstream advances. A raw 40-hex SHA is used as-is (it + # cannot be ls-remote'd); for a branch/tag we REQUIRE resolution to + # succeed -- otherwise a transient network failure would silently fall + # back to the literal ref, producing a cache key that never refreshes. + ref='${{ inputs.ref }}' + if printf '%s' "$ref" | grep -Eq '^[0-9a-f]{40}$'; then + sha="$ref" + else + sha="$(git ls-remote "$repo" "$ref" | head -1 | awk '{print $1}')" + if [ -z "$sha" ]; then + echo "::error::could not resolve wolfSSL ref '$ref' via git ls-remote $repo" >&2 + exit 1 + fi + fi + + cfg_hash="$(printf '%s\n%s\n%s\n%s\n%s\n%s\n' \ + "$sha" "$flags" "${CC:-}" "${CFLAGS:-}" "${RUNNER_OS}" "${RUNNER_ARCH}" \ + | sha256sum | cut -c1-16)" + + prefix="${GITHUB_WORKSPACE}/.wolfssl-install/${{ inputs.config }}" + key="wolfssl-${RUNNER_OS}-${RUNNER_ARCH}-${{ inputs.config }}-${sha:0:12}-${cfg_hash}" + + { + echo "prefix=$prefix" + echo "sha=$sha" + echo "key=$key" + } >> "$GITHUB_OUTPUT" + + # Exact-key match only (no restore-keys): a stale prefix from a different + # wolfSSL commit or flag set would carry a valid-looking wolfssl.pc, and the + # idempotent build script would then short-circuit on it and silently use + # the wrong config. Forcing a clean rebuild on any key change avoids that. + - name: Restore cached wolfSSL prefix + id: restore + uses: actions/cache/restore@v4 + with: + path: ${{ steps.meta.outputs.prefix }} + key: ${{ steps.meta.outputs.key }} + + - name: Build wolfSSL (cache miss) + if: steps.restore.outputs.cache-hit != 'true' + shell: bash + run: | + set -euo pipefail + "${GITHUB_ACTION_PATH}/../../../scripts/ci/build-wolfssl.sh" \ + "${{ inputs.config }}" \ + --prefix "${{ steps.meta.outputs.prefix }}" \ + --ref "${{ inputs.ref }}" + + - name: Save wolfSSL prefix to cache (reseed) + if: inputs.save == 'true' && steps.restore.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ steps.meta.outputs.prefix }} + key: ${{ steps.meta.outputs.key }} + + - name: Export prefix + shell: bash + run: | + echo "WOLFSSL_PREFIX=${{ steps.meta.outputs.prefix }}" >> "$GITHUB_ENV" + echo "PKG_CONFIG_PATH=${{ steps.meta.outputs.prefix }}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" >> "$GITHUB_ENV" diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml new file mode 100644 index 0000000..aa81c6b --- /dev/null +++ b/.github/workflows/interop.yml @@ -0,0 +1,140 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: Interop + +# Third-party EST/SCEP interop. Best-effort: a dependency that fails to install +# makes its script exit 77 (the automake "skip" convention wired into +# tests/interop/lib/common.sh via need()), which we treat as a neutral pass. +# A real interop regression (any other non-zero) fails the job. +on: + schedule: + - cron: "0 4 * * *" + workflow_dispatch: + +concurrency: + group: interop-${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +env: + WOLFSSL_REF: master + +jobs: + # OpenSSL is always present -> a cheap lower-bound cross-check. + openssl: + name: openssl cms/pkcs7 cross-check + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-wolfcert-cli + with: + wolfssl-ref: ${{ env.WOLFSSL_REF }} + - name: Run openssl_cms.sh + run: | + set +e + bash tests/interop/openssl_cms.sh; rc=$? + [ "$rc" -eq 77 ] && { echo "::notice::openssl_cms skipped (dep missing)"; exit 0; } + exit "$rc" + + micromdm-scep: + name: micromdm/scep + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-wolfcert-cli + with: + wolfssl-ref: ${{ env.WOLFSSL_REF }} + - name: Install micromdm scep + continue-on-error: true + run: sudo apt-get update && sudo apt-get install -y scep + - name: Run scep_micromdm.sh + run: | + set +e + bash tests/interop/scep_micromdm.sh; rc=$? + [ "$rc" -eq 77 ] && { echo "::notice::micromdm/scep skipped (dep missing)"; exit 0; } + exit "$rc" + + globalsign-est: + name: globalsign/est + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-wolfcert-cli + with: + wolfssl-ref: ${{ env.WOLFSSL_REF }} + - uses: actions/setup-go@v5 + with: + go-version: "stable" + - name: Install globalsign est (estserver/estclient) + continue-on-error: true + run: | + go install github.com/globalsign/est/cmd/estserver@latest + go install github.com/globalsign/est/cmd/estclient@latest + - name: Run est_globalsign.sh + run: | + # setup-go puts binaries in $(go env GOPATH)/bin. The script uses + # GLOBALSIGN_EST_BIN_DIR to disambiguate from libest's same-named bins. + GLOBALSIGN_EST_BIN_DIR="$(go env GOPATH)/bin" + export GLOBALSIGN_EST_BIN_DIR + export PATH="$GLOBALSIGN_EST_BIN_DIR:$PATH" + set +e + bash tests/interop/est_globalsign.sh; rc=$? + [ "$rc" -eq 77 ] && { echo "::notice::globalsign/est skipped (dep missing)"; exit 0; } + exit "$rc" + + libest: + name: cisco/libest + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-wolfcert-cli + with: + wolfssl-ref: ${{ env.WOLFSSL_REF }} + - name: Build Cisco libest from source + continue-on-error: true + run: | + set -euo pipefail + sudo apt-get install -y libssl-dev + git clone --depth 1 https://github.com/cisco/libest "$RUNNER_TEMP/libest" + cd "$RUNNER_TEMP/libest" + ./configure --prefix="$RUNNER_TEMP/libest-install" --with-ssl-dir=/usr + make -j "$(nproc)" + make install + echo "$RUNNER_TEMP/libest-install/bin" >> "$GITHUB_PATH" + - name: Run est_libest.sh + run: | + set +e + bash tests/interop/est_libest.sh; rc=$? + [ "$rc" -eq 77 ] && { echo "::notice::libest skipped (dep missing)"; exit 0; } + exit "$rc" + + stepca: + name: smallstep/step-ca + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/build-wolfcert-cli + with: + wolfssl-ref: ${{ env.WOLFSSL_REF }} + - uses: actions/setup-go@v5 + with: + go-version: "stable" + - name: Install step-ca and step CLI + continue-on-error: true + run: | + go install github.com/smallstep/certificates/cmd/step-ca@latest + go install github.com/smallstep/cli/cmd/step@latest + - name: Run est_stepca.sh + run: | + gobin="$(go env GOPATH)/bin" + export PATH="$gobin:$PATH" + set +e + bash tests/interop/est_stepca.sh; rc=$? + [ "$rc" -eq 77 ] && { echo "::notice::step-ca skipped (dep missing)"; exit 0; } + exit "$rc" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ad986e2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: Lint + +# Cheap, wolfSSL-free checks that fail fast before the expensive build matrix +# matters: source license headers and CMake<->autoconf source-list parity. +on: + # See pr.yml: push only on default branches so PR runs aren't doubled. + pull_request: + push: + branches: + - main + - master + +concurrency: + group: lint-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + license-headers: + name: GPL license headers + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Check every C source opens with the GPL block + shell: bash + run: | + set -euo pipefail + missing=0 + while IFS= read -r f; do + hdr="$(head -n 20 "$f")" + if ! grep -q "wolfSSL Inc." <<<"$hdr" \ + || ! grep -q "GNU General Public License" <<<"$hdr"; then + echo "MISSING license header: $f" + missing=1 + fi + done < <(git ls-files 'src/*.c' 'src/**/*.c' 'wolfcert/*.h' \ + 'cli/*.c' 'tests/*.c' 'tests/*.h' \ + 'tests/**/*.c' 'tests/**/*.h') + if [ "$missing" -ne 0 ]; then + echo "One or more sources lack the GPL copyright block (copy it" + echo "verbatim from any existing .c/.h)." + exit 1 + fi + echo "license headers OK" + + buildsystem-parity: + name: CMake <-> autoconf source parity + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Check library source lists match + run: scripts/ci/check-buildsystem-parity.sh diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..82efa6d --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,140 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: Nightly + +# Re-runs the wolfSSL-variant build matrix against fresh wolfSSL master and +# (re)seeds the wolfSSL prefix caches so the next day's PR runs restore instead +# of build. The feature/config gating these variants exercise now also runs on +# every PR (pr.yml); the entries kept here own the cache-seeding and the +# fresh-master retest. macOS extras and the full negative-config set are +# nightly-only. Sanitizers and interop are separate workflows. +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + +concurrency: + group: nightly-${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +env: + WOLFSSL_REF: master + +jobs: + matrix: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + # ---- Linux: full canonical, both build systems ---- + - { name: cmake-full, os: ubuntu-latest, build: cmake, wolfssl: full } + - { name: autoconf-full, os: ubuntu-latest, build: autoconf, wolfssl: full } + # ---- wolfSSL key-alg / crypto variants (also gated in pr.yml; kept + # here to seed their distinct prefixes + retest fresh master) ---- + - { name: cmake-est-only-nonrsa, os: ubuntu-latest, build: cmake, wolfssl: est-only-nonrsa, cmake_extra: -DWOLFCERT_ENABLE_SCEP=OFF } + - { name: autoconf-est-nonrsa, os: ubuntu-latest, build: autoconf, wolfssl: est-only-nonrsa, configure_extra: --disable-scep } + - { name: cmake-ecc-only-est, os: ubuntu-latest, build: cmake, wolfssl: ecc-only-est, cmake_extra: -DWOLFCERT_ENABLE_SCEP=OFF } + - { name: cmake-rsa-min, os: ubuntu-latest, build: cmake, wolfssl: rsa-min } + - { name: cmake-no-des3, os: ubuntu-latest, build: cmake, wolfssl: no-des3 } + - { name: cmake-tls13-only, os: ubuntu-latest, build: cmake, wolfssl: tls13-only } + # ---- ML-DSA per-level gating ---- + - { name: cmake-mldsa-44off, os: ubuntu-latest, build: cmake, wolfssl: mldsa-44off } + - { name: cmake-mldsa-65off, os: ubuntu-latest, build: cmake, wolfssl: mldsa-65off } + - { name: cmake-mldsa-87off, os: ubuntu-latest, build: cmake, wolfssl: mldsa-87off } + # ---- Constrained builds (unit tests only: single-thread / no-malloc) ---- + - { name: cmake-static-mem, os: ubuntu-latest, build: cmake, wolfssl: static-mem, cmake_extra: -DWOLFCERT_ENABLE_SERVER=OFF, ctest_exclude: "http|tls|roundtrip" } + - { name: cmake-no-malloc, os: ubuntu-latest, build: cmake, wolfssl: no-malloc, ctest_exclude: "http|tls|roundtrip|est" } + # ---- macOS reduced subset ---- + - { name: cmake-full-macos, os: macos-latest, build: cmake, wolfssl: full } + - { name: cmake-nonrsa-macos, os: macos-latest, build: cmake, wolfssl: est-only-nonrsa, cmake_extra: -DWOLFCERT_ENABLE_SCEP=OFF } + - { name: cmake-tls13-macos, os: macos-latest, build: cmake, wolfssl: tls13-only } + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + shell: bash + run: | + if [ "$RUNNER_OS" = "Linux" ]; then + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool \ + pkg-config python3 + else + brew install autoconf automake libtool pkg-config + fi + + - name: Build/restore wolfSSL (${{ matrix.wolfssl }}) + uses: ./.github/actions/build-wolfssl + with: + config: ${{ matrix.wolfssl }} + ref: ${{ env.WOLFSSL_REF }} + save: "true" # nightly (re)seeds the shared cache for PR runs + + - name: Build & test (CMake) + if: matrix.build == 'cmake' + shell: bash + run: | + set -euo pipefail + jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + # sigpipe-launcher.sh: make each test ignore SIGPIPE (see the script). + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_TESTS=ON \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/scripts/ci/sigpipe-launcher.sh" \ + ${{ matrix.cmake_extra }} + cmake --build build -j "$jobs" + # Pass the -E exclude regex as a single quoted argument so its '|' + # alternation is never seen by the shell as a pipe. Two explicit + # branches avoid the empty-array-under-`set -u` pitfall on bash 3.2 + # (macOS runners). + if [ -n "${{ matrix.ctest_exclude }}" ]; then + ctest --test-dir build -j "$jobs" --output-on-failure \ + -E "${{ matrix.ctest_exclude }}" + else + ctest --test-dir build -j "$jobs" --output-on-failure + fi + + - name: Build & test (autoconf) + if: matrix.build == 'autoconf' + shell: bash + run: | + set -euo pipefail + trap '' PIPE # ignore SIGPIPE: tests write to sockets after peer close + ./autogen.sh + ./configure --with-wolfssl="$WOLFSSL_PREFIX" --enable-tests \ + ${{ matrix.configure_extra }} + make -j "$(nproc)" + if ! make check; then + find . -name '*.log' -exec sh -c 'echo "== $1 =="; cat "$1"' _ {} \; + exit 1 + fi + + negative-config-full: + name: negative-config (full set, both build systems) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool \ + pkg-config python3 + # Stable key (no run_id): the negative wolfSSL configs are deterministic + # from build-wolfssl.sh, so its hash fully identifies them. This is the + # same key pr.yml uses, so both jobs share one entry -- nightly restores + # what a PR seeded (or vice versa) and reseeds only when the script hash + # changes, instead of minting a fresh cache every night. + - name: Cache negative-config wolfSSL prefixes + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.wolfssl-install + key: wolfssl-neg-${{ runner.os }}-${{ hashFiles('scripts/ci/build-wolfssl.sh') }} + - name: Assert every configure gate hard-fails + run: | + scripts/ci/assert-configure-fails.sh \ + --wolfssl-base "${{ github.workspace }}/.wolfssl-install" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..c83867e --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,251 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: PR + +# Fast merge gate. Every job builds (or restores from cache) a wolfSSL config, +# builds wolfCert to match, and runs the tests. Kept to a tight high-value set; +# the exhaustive matrix, sanitizers and interop live in the nightly workflows. +on: + # PRs are the gate. Push only fires for the default branches (post-merge + # validation): running on every feature-branch push too would double every + # PR run (the push event and the pull_request event both fire). + pull_request: + push: + branches: + - main + - master + +# Cancel superseded runs on the same ref to keep the queue short. +concurrency: + group: pr-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + WOLFSSL_REF: master # tracked, unpinned (per project decision) + +jobs: + build-test: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + # ---- Canonical full config: CMake (-Werror) + autoconf + sanitizers -- + - name: cmake-full-werror + os: ubuntu-latest + build: cmake + wolfssl: full + cmake_extra: -DWOLFCERT_WERROR=ON + - name: autoconf-full + os: ubuntu-latest + build: autoconf + wolfssl: full + - name: cmake-full-asan-ubsan + os: ubuntu-latest + build: cmake + wolfssl: full + cmake_extra: -DCMAKE_BUILD_TYPE=Debug -DWOLFCERT_BUILD_SHARED=OFF + cflags: -fsanitize=address,undefined -fno-sanitize-recover=all + # ---- Protocol / server feature gating (all reuse the `full` prefix) -- + - name: cmake-est-only + os: ubuntu-latest + build: cmake + wolfssl: full + cmake_extra: -DWOLFCERT_ENABLE_SCEP=OFF + - name: cmake-scep-only + os: ubuntu-latest + build: cmake + wolfssl: full + cmake_extra: -DWOLFCERT_ENABLE_EST=OFF + - name: cmake-server-off + os: ubuntu-latest + build: cmake + wolfssl: full + cmake_extra: -DWOLFCERT_ENABLE_SERVER=OFF + ctest_exclude: roundtrip + # ---- wolfSSL key-alg / crypto variants ---- + - name: cmake-est-only-nonrsa + os: ubuntu-latest + build: cmake + wolfssl: est-only-nonrsa + cmake_extra: -DWOLFCERT_ENABLE_SCEP=OFF + - name: cmake-ecc-only-est + os: ubuntu-latest + build: cmake + wolfssl: ecc-only-est + cmake_extra: -DWOLFCERT_ENABLE_SCEP=OFF + - name: cmake-rsa-min + os: ubuntu-latest + build: cmake + wolfssl: rsa-min + - name: cmake-no-des3 + os: ubuntu-latest + build: cmake + wolfssl: no-des3 + - name: cmake-tls13-only + os: ubuntu-latest + build: cmake + wolfssl: tls13-only + # ---- ML-DSA per-level gating ---- + - name: cmake-mldsa-44off + os: ubuntu-latest + build: cmake + wolfssl: mldsa-44off + - name: cmake-mldsa-65off + os: ubuntu-latest + build: cmake + wolfssl: mldsa-65off + - name: cmake-mldsa-87off + os: ubuntu-latest + build: cmake + wolfssl: mldsa-87off + # ---- Constrained builds (unit tests only: single-thread / no-malloc) -- + - name: cmake-static-mem + os: ubuntu-latest + build: cmake + wolfssl: static-mem + cmake_extra: -DWOLFCERT_ENABLE_SERVER=OFF + ctest_exclude: "http|tls|roundtrip" + - name: cmake-no-malloc + os: ubuntu-latest + build: cmake + wolfssl: no-malloc + # Non-TLS crypto/cert/PKCS7 units only: TLS + threads over a static + # pool is a separate wolfSSL concern, out of scope for the + # allocation-free validation. + ctest_exclude: "http|tls|roundtrip|est" + # ---- macOS ---- + - name: cmake-full-macos + os: macos-latest + build: cmake + wolfssl: full + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + shell: bash + run: | + if [ "$RUNNER_OS" = "Linux" ]; then + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool \ + pkg-config python3 + else + brew install autoconf automake libtool pkg-config + fi + + - name: Build/restore wolfSSL (${{ matrix.wolfssl }}) + uses: ./.github/actions/build-wolfssl + with: + config: ${{ matrix.wolfssl }} + ref: ${{ env.WOLFSSL_REF }} + save: "false" # PR is restore-only; the nightly reseeds the cache + + - name: Build & test (CMake) + if: matrix.build == 'cmake' + shell: bash + env: + ASAN_OPTIONS: halt_on_error=1:detect_leaks=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + run: | + set -euo pipefail + jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + extra_cflags=() + if [ -n "${{ matrix.cflags }}" ]; then + extra_cflags=(-DCMAKE_C_FLAGS="${{ matrix.cflags }}") + fi + # sigpipe-launcher.sh makes each test ignore SIGPIPE (see the script); + # wired via CMAKE_CROSSCOMPILING_EMULATOR so ctest applies it to all + # tests with no source change, for plain and sanitizer builds alike. + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_TESTS=ON \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/scripts/ci/sigpipe-launcher.sh" \ + ${{ matrix.cmake_extra }} ${extra_cflags[@]+"${extra_cflags[@]}"} + cmake --build build -j "$jobs" + # Pass the -E exclude regex as a single quoted argument so its '|' + # alternation is never seen by the shell as a pipe. Two explicit + # branches avoid the empty-array-under-`set -u` pitfall on bash 3.2 + # (macOS runners). + if [ -n "${{ matrix.ctest_exclude }}" ]; then + ctest --test-dir build -j "$jobs" --output-on-failure \ + -E "${{ matrix.ctest_exclude }}" + else + ctest --test-dir build -j "$jobs" --output-on-failure + fi + + - name: Build & test (autoconf) + if: matrix.build == 'autoconf' + shell: bash + run: | + set -euo pipefail + trap '' PIPE # see note in the CMake step above + ./autogen.sh + ./configure --with-wolfssl="$WOLFSSL_PREFIX" --enable-tests + make -j "$(nproc)" + if ! make check; then + find . -name '*.log' -exec sh -c 'echo "== $1 =="; cat "$1"' _ {} \; + exit 1 + fi + + header-only: + name: user_settings header-only build + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config python3 + - name: Build/restore wolfSSL (full) + uses: ./.github/actions/build-wolfssl + with: + config: full + ref: ${{ env.WOLFSSL_REF }} + save: "false" # PR is restore-only; the nightly reseeds the cache + - name: Configure with a user-supplied user_settings.h + shell: bash + run: | + set -euo pipefail + # The header-only path takes the feature set from user_settings.h + # instead of the generated options.h. Seed it from the shipped example. + mkdir -p "$RUNNER_TEMP/us" + cp examples/user_settings.h.example "$RUNNER_TEMP/us/user_settings.h" + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_TESTS=ON \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/scripts/ci/sigpipe-launcher.sh" \ + -DWOLFCERT_USER_SETTINGS=ON \ + -DWOLFCERT_USER_SETTINGS_DIR="$RUNNER_TEMP/us" + cmake --build build -j "$(nproc)" + ctest --test-dir build -j "$(nproc)" --output-on-failure + + negative-config: + name: negative-config (fail-fast gates) + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool \ + pkg-config python3 + # Read-write cache: the negative wolfSSL configs are deterministic from + # build-wolfssl.sh, so save+restore on this key actually functions (the + # first run seeds it, later runs on the branch restore it). The negative + # gate only needs these to build and be rejected, so it does not depend on + # the wolfSSL commit -- the script hash alone is a sufficient key. + - name: Cache negative-config wolfSSL prefixes + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.wolfssl-install + key: wolfssl-neg-${{ runner.os }}-${{ hashFiles('scripts/ci/build-wolfssl.sh') }} + - name: Assert configure hard-fails (both build systems) + run: | + scripts/ci/assert-configure-fails.sh \ + --wolfssl-base "${{ github.workspace }}/.wolfssl-install" diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 0000000..87de8ac --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,134 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +name: Sanitizers + +# Memory- and thread-safety runs. Slower than the PR gate (instrumented builds +# rebuild wolfSSL and run tests 2-5x slower), so they live on a nightly cadence. +on: + schedule: + - cron: "30 3 * * *" + workflow_dispatch: + +concurrency: + group: sanitizers-${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: read + +env: + WOLFSSL_REF: master + +jobs: + asan-ubsan: + name: ASan + UBSan (full ctest) + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + ASAN_OPTIONS: halt_on_error=1:detect_leaks=1:abort_on_error=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config python3 + - name: Build/restore wolfSSL (full) + uses: ./.github/actions/build-wolfssl + with: + config: full + ref: ${{ env.WOLFSSL_REF }} + # nightly.yml owns saving the shared `full` cache; don't also save it + # here (concurrent scheduled runs would race on the same key). + save: "false" + - name: Build (ASan+UBSan) & test + shell: bash + run: | + set -euo pipefail + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_TESTS=ON \ + -DWOLFCERT_BUILD_SHARED=OFF \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/scripts/ci/sigpipe-launcher.sh" \ + -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -g" + cmake --build build -j "$(nproc)" + ctest --test-dir build -j "$(nproc)" --output-on-failure + + tsan: + name: ThreadSanitizer (roundtrips) + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config python3 + # wolfSSL must ALSO be TSAN-instrumented, else races inside it are + # invisible and uninstrumented-library false negatives appear. + - name: Build/restore wolfSSL (full-tsan) + uses: ./.github/actions/build-wolfssl + with: + config: full-tsan + ref: ${{ env.WOLFSSL_REF }} + save: "true" + - name: Build (TSAN) & run threaded integration tests + shell: bash + run: | + set -euo pipefail + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_TESTS=ON \ + -DWOLFCERT_BUILD_SHARED=OFF \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/scripts/ci/sigpipe-launcher.sh" \ + -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" + cmake --build build -j "$(nproc)" + # The roundtrip tests spin the server on a background pthread while the + # client drives it on the main thread -> the real concurrency surface. + ctest --test-dir build -j "$(nproc)" --output-on-failure \ + -R 'roundtrip|tls_http' + + valgrind: + name: valgrind (representative subset) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential pkg-config python3 valgrind + - name: Build/restore wolfSSL (full) + uses: ./.github/actions/build-wolfssl + with: + config: full + ref: ${{ env.WOLFSSL_REF }} + save: "false" # nightly.yml owns the shared `full` cache + - name: Build (no sanitizer) & run under valgrind + shell: bash + run: | + set -euo pipefail + cmake -S . -B build \ + -DWITH_WOLFSSL="$WOLFSSL_PREFIX" \ + -DWOLFCERT_ENABLE_TESTS=ON \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_CROSSCOMPILING_EMULATOR="$PWD/scripts/ci/sigpipe-launcher.sh" + cmake --build build -j "$(nproc)" + # wolfCert-focused unit tests only. The TLS/SCEP *roundtrips* drive + # wolfSSL's TLS + AES-NI/asm crypto through Memcheck, a well-known + # source of false "uninitialised value" reports (and large slowdowns); + # covering them here would need a wolfSSL suppression file. They are + # already exercised under ASan/UBSan and TSAN above. + rc=0 + for bin in test_smoke test_keygen test_csr test_store test_parse_negative \ + test_scep_msg; do + path="build/tests/$bin" + [ -x "$path" ] || { echo "skip (not built): $bin"; continue; } + echo "== valgrind $bin ==" + valgrind --leak-check=full --errors-for-leak-kinds=definite \ + --error-exitcode=1 -q "$path" || rc=1 + done + exit "$rc" diff --git a/.gitignore b/.gitignore index f47c2ee..ec21399 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -# Build directories -build/ -build-*/ -_build/ -out/ +# Build directories (anchored to the repo root so they don't match unrelated +# paths such as .github/actions/build-*/). +/build/ +/build-*/ +/_build/ +/out/ # CMake CMakeCache.txt @@ -62,6 +63,12 @@ cmake/wolfCertTargets.cmake /wolfcert-client /wolfcert-server +# CI scratch: wolfSSL sources/installs and compiler caches produced by +# scripts/ci/build-wolfssl.sh and the workflows (never committed). +.wolfssl-src/ +.wolfssl-install/ +.ccache/ + # Editors / OS .vscode/ .idea/ diff --git a/Makefile.am b/Makefile.am index a595178..720e9a3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -143,10 +143,17 @@ test_est_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread test_csr_attrs_SOURCES = tests/unit/test_csr_attrs.c test_csr_attrs_LDADD = libwolfcert.la $(WOLFSSL_LIBS) if WOLFCERT_HAVE_SERVER -check_PROGRAMS += test_est_roundtrip test_est_pha_roundtrip test_est_async_roundtrip \ - test_est_csr_attrs_roundtrip test_est_mldsa_roundtrip +check_PROGRAMS += test_est_roundtrip test_est_tls_roundtrip test_est_mtls_roundtrip \ + test_est_pha_roundtrip test_est_async_roundtrip \ + test_est_csr_attrs_roundtrip test_est_csr_attrs_apply_roundtrip \ + test_est_csr_attrs_enforce test_est_chunked_robustness \ + test_est_pending_roundtrip test_est_mldsa_roundtrip test_est_roundtrip_SOURCES = tests/integration/test_est_roundtrip.c test_est_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_est_tls_roundtrip_SOURCES = tests/integration/test_est_tls_roundtrip.c +test_est_tls_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_est_mtls_roundtrip_SOURCES = tests/integration/test_est_mtls_roundtrip.c +test_est_mtls_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread test_est_mldsa_roundtrip_SOURCES = tests/integration/test_est_mldsa_roundtrip.c test_est_mldsa_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread test_est_pha_roundtrip_SOURCES = tests/integration/test_est_pha_roundtrip.c @@ -155,6 +162,14 @@ test_est_async_roundtrip_SOURCES = tests/integration/test_est_async_roundtrip.c test_est_async_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread test_est_csr_attrs_roundtrip_SOURCES = tests/integration/test_est_csr_attrs_roundtrip.c test_est_csr_attrs_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_est_csr_attrs_apply_roundtrip_SOURCES = tests/integration/test_est_csr_attrs_apply_roundtrip.c +test_est_csr_attrs_apply_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_est_csr_attrs_enforce_SOURCES = tests/integration/test_est_csr_attrs_enforce.c +test_est_csr_attrs_enforce_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_est_chunked_robustness_SOURCES = tests/integration/test_est_chunked_robustness.c +test_est_chunked_robustness_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread +test_est_pending_roundtrip_SOURCES = tests/integration/test_est_pending_roundtrip.c +test_est_pending_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread endif endif diff --git a/README.md b/README.md index ca24c94..f5b27fe 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,9 @@ caveats apply to SCEP and to some servers: flows, and the MCU / CryptoCb integration guide. - [`docs/EMBEDDED.md`](docs/EMBEDDED.md) — tuning wolfCert's RAM footprint for constrained targets. +- [`docs/CI.md`](docs/CI.md) — the GitHub Actions pipeline, the shared + `scripts/ci/build-wolfssl.sh` helper, and how to reproduce a CI config + locally. ## License diff --git a/docs/CI.md b/docs/CI.md new file mode 100644 index 0000000..55020a3 --- /dev/null +++ b/docs/CI.md @@ -0,0 +1,64 @@ +# Continuous Integration + +wolfCert's CI runs on GitHub Actions. Every job builds (or restores from cache) +a wolfSSL configuration, builds wolfCert to match, and runs the tests. wolfSSL +is tracked at `master` and built from source; the install *prefix* is cached so +a run with an unchanged wolfSSL commit + configure-flag set skips the wolfSSL +build entirely. + +## Workflows + +| Workflow | Trigger | What it does | +|----------|---------|--------------| +| `pr.yml` | PR + push | Merge gate: CMake (`-Werror`) + autoconf + ASan/UBSan on the canonical config, plus per-PR feature/config gating — EST-only, SCEP-only, server-off, the key-alg variants (NO_RSA, ECC-only, RSA-only, no-3DES), TLS 1.3-only, the three ML-DSA per-level builds, the static-memory and no-malloc constrained builds, the header-only (`WOLFCERT_USER_SETTINGS`) build, a macOS build, and the two cheapest configure-must-fail assertions. | +| `lint.yml` | PR + push | GPL license-header check and CMake↔autoconf parity of both the library and test source lists (`scripts/ci/check-buildsystem-parity.sh`). No wolfSSL build — fails in seconds. | +| `nightly.yml` | schedule + dispatch | Re-runs the wolfSSL-variant build matrix against fresh wolfSSL `master`, the macOS extras, and the full negative-config set. Also **reseeds the wolfSSL prefix caches** so the next day's PRs restore instead of build. The feature/config gating itself now runs per-PR (see `pr.yml`). | +| `sanitizers.yml` | schedule + dispatch | ASan+UBSan over the full test suite, ThreadSanitizer over the threaded integration roundtrips (against a TSAN-instrumented wolfSSL), and valgrind over a representative subset. | +| `interop.yml` | schedule + dispatch | Third-party EST/SCEP interop (openssl, micromdm/scep, globalsign/est, cisco/libest, smallstep/step-ca). Best-effort: a dependency that fails to install makes its script exit 77, which is treated as a neutral skip; a real interop regression fails. | + +## wolfSSL configurations + +`scripts/ci/build-wolfssl.sh` is the single source of truth mapping a short +config name to the exact wolfSSL `./configure` flags. CI hashes +`--print-flags ` into the cache key, so a cached prefix and a local build +can never disagree. + +```sh +scripts/ci/build-wolfssl.sh --list # every known config name +scripts/ci/build-wolfssl.sh --print-flags full # the flags for a config +scripts/ci/build-wolfssl.sh full --prefix /tmp/ws # build + install to /tmp/ws +``` + +Config names include `full` (all algorithms), `full-tsan`, `est-only-nonrsa` +(NO_RSA), `rsa-min`, `ecc-only-est`, `no-des3`, `tls13-only`, +`mldsa-{44,65,87}off`, `static-mem`, `no-malloc`, and the `neg-*` configs used +by the negative-config gate. + +## Reproducing a CI job locally + +```sh +# 1. Build the wolfSSL config the job uses. +scripts/ci/build-wolfssl.sh full --prefix /tmp/wolfssl + +# 2. Build wolfCert against it and run the tests (CMake). +cmake -S . -B build -DWITH_WOLFSSL=/tmp/wolfssl -DWOLFCERT_ENABLE_TESTS=ON +cmake --build build -j +ctest --test-dir build --output-on-failure + +# autoconf equivalent +./autogen.sh +PKG_CONFIG_PATH=/tmp/wolfssl/lib/pkgconfig ./configure \ + --with-wolfssl=/tmp/wolfssl --enable-tests +make -j && make check +``` + +The negative-config gate asserts wolfCert's configure hard-fails on an +unsupportable wolfSSL. It covers the cases a *buildable* wolfSSL can express +(`no-rsa-scep`, `no-pkcs7`); wolfSSL itself refuses to drop AES / SHA-256 / all +TLS / all key algorithms, so wolfCert's compile-time `#error` guards for those +(`wolfcert/check_config.h`) are validated at compile time, not by this gate. + +```sh +scripts/ci/assert-configure-fails.sh # all cases, both build systems +scripts/ci/assert-configure-fails.sh no-rsa-scep # a single case +``` diff --git a/scripts/ci/assert-configure-fails.sh b/scripts/ci/assert-configure-fails.sh new file mode 100755 index 0000000..86b1e4e --- /dev/null +++ b/scripts/ci/assert-configure-fails.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Negative-configuration gate: assert that wolfCert's configure step HARD-FAILS +# (non-zero exit) with the expected diagnostic when the underlying wolfSSL is +# built in a way wolfCert cannot support. Exercises the fail-fast guards in +# CMakeLists.txt and configure.ac, for BOTH build systems (their gate +# implementations are independent, so we check parity of the rejection too). +# +# Usage: +# assert-configure-fails.sh [--build-system cmake|autoconf|both] +# [--wolfssl-base DIR] [--ref REF] +# [CASE ...] +# +# With no CASE names, every case runs. +# +# Each case builds a purpose-built wolfSSL (via build-wolfssl.sh, cached under +# --wolfssl-base) then runs wolfCert configure and requires: (a) non-zero exit, +# (b) the expected message. A configure that unexpectedly SUCCEEDS is a failure +# of this gate. +# +# Scope: only misconfigurations that a *buildable* wolfSSL can express are +# covered here (RSA-off-with-SCEP, PKCS7 missing). wolfSSL's own configure +# refuses to drop AES / SHA-256 / all TLS / all key algorithms, so wolfCert's +# compile-time #error guards for those (check_config.h) can't be fed by a real +# wolfSSL build and are not exercised by this script. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$HERE/../.." && pwd)" +BUILD_WOLFSSL="$HERE/build-wolfssl.sh" + +BUILD_SYSTEM="both" +WOLFSSL_BASE="$PWD/.wolfssl-install" +REF="${WOLFSSL_REF:-master}" +CASES=() + +while [ "$#" -gt 0 ]; do + case "$1" in + --build-system) BUILD_SYSTEM="$2"; shift 2 ;; + --wolfssl-base) WOLFSSL_BASE="$2"; shift 2 ;; + --ref) REF="$2"; shift 2 ;; + -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + --*) echo "ERROR: unknown flag '$1'." >&2; exit 2 ;; + *) CASES+=("$1"); shift ;; + esac +done + +# ---------------------------------------------------------------------------- +# Case table. Fields (":"-separated): +# name : wolfssl-config : cmake-extra-opts : autoconf-extra-opts : grep -E pattern +# ---------------------------------------------------------------------------- +CASE_TABLE=( + "no-rsa-scep:neg-no-rsa:::SCEP is RSA-only" + "no-pkcs7:neg-no-pkcs7:::HAVE_PKCS7|missing a required feature" +) + +lookup_case() { + local want="$1" row + for row in "${CASE_TABLE[@]}"; do + [ "${row%%:*}" = "$want" ] && { echo "$row"; return 0; } + done + return 1 +} + +# Default: run all cases in table order. +if [ "${#CASES[@]}" -eq 0 ]; then + for row in "${CASE_TABLE[@]}"; do CASES+=("${row%%:*}"); done +fi + +PASS=0 +FAIL=0 + +run_cmake() { # + local prefix="$1" extra="$2" pattern="$3" name="$4" + local bdir log rc + bdir="$(mktemp -d)"; log="$bdir/configure.log" + set +e + # shellcheck disable=SC2086 + cmake -S "$REPO_ROOT" -B "$bdir" -DWITH_WOLFSSL="$prefix" $extra >"$log" 2>&1 + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + echo " [cmake] FAIL: configure unexpectedly SUCCEEDED for '$name'" + FAIL=$((FAIL+1)); rm -rf "$bdir"; return + fi + if grep -Eiq "$pattern" "$log"; then + echo " [cmake] OK: '$name' rejected (matched: $pattern)" + PASS=$((PASS+1)) + else + echo " [cmake] FAIL: '$name' failed but message did not match /$pattern/" + echo " --- tail of configure log ---"; tail -n 15 "$log" | sed 's/^/ /' + FAIL=$((FAIL+1)) + fi + rm -rf "$bdir" +} + +run_autoconf() { # + local prefix="$1" extra="$2" pattern="$3" name="$4" + local bdir log rc + # Autoconf needs a bootstrapped source tree once; build out-of-tree (VPATH). + if [ ! -x "$REPO_ROOT/configure" ]; then + ( cd "$REPO_ROOT" && ./autogen.sh >/dev/null 2>&1 ) + fi + # A VPATH build refuses to run when the source tree is already configured + # in-tree (leftover config.status). Clean it so local runs match CI's fresh + # checkout. + if [ -f "$REPO_ROOT/config.status" ]; then + ( cd "$REPO_ROOT" && make distclean >/dev/null 2>&1 ) || true + ( cd "$REPO_ROOT" && ./autogen.sh >/dev/null 2>&1 ) + fi + bdir="$(mktemp -d)"; log="$bdir/configure.log" + set +e + # shellcheck disable=SC2086 + ( cd "$bdir" && PKG_CONFIG_PATH="$prefix/lib/pkgconfig:${PKG_CONFIG_PATH:-}" \ + "$REPO_ROOT/configure" --with-wolfssl="$prefix" $extra ) >"$log" 2>&1 + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + echo " [autoconf] FAIL: configure unexpectedly SUCCEEDED for '$name'" + FAIL=$((FAIL+1)); rm -rf "$bdir"; return + fi + if grep -Eiq "$pattern" "$log"; then + echo " [autoconf] OK: '$name' rejected (matched: $pattern)" + PASS=$((PASS+1)) + else + echo " [autoconf] FAIL: '$name' failed but message did not match /$pattern/" + echo " --- tail of configure log ---"; tail -n 15 "$log" | sed 's/^/ /' + FAIL=$((FAIL+1)) + fi + rm -rf "$bdir" +} + +for name in "${CASES[@]}"; do + row="$(lookup_case "$name")" || { echo "ERROR: unknown case '$name'." >&2; exit 2; } + IFS=':' read -r _n wcfg cmake_extra ac_extra pattern <<<"$row" + echo "== case '$name' (wolfSSL: $wcfg) ==" + prefix="$WOLFSSL_BASE/$wcfg" + "$BUILD_WOLFSSL" "$wcfg" --prefix "$prefix" --ref "$REF" >/dev/null + + case "$BUILD_SYSTEM" in + cmake) run_cmake "$prefix" "$cmake_extra" "$pattern" "$name" ;; + autoconf) run_autoconf "$prefix" "$ac_extra" "$pattern" "$name" ;; + both) run_cmake "$prefix" "$cmake_extra" "$pattern" "$name" + run_autoconf "$prefix" "$ac_extra" "$pattern" "$name" ;; + *) echo "ERROR: --build-system must be cmake|autoconf|both." >&2; exit 2 ;; + esac +done + +echo "----------------------------------------" +echo "negative-config gate: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] diff --git a/scripts/ci/build-wolfssl.sh b/scripts/ci/build-wolfssl.sh new file mode 100755 index 0000000..ebdbe94 --- /dev/null +++ b/scripts/ci/build-wolfssl.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Build and install a named wolfSSL configuration for wolfCert CI (and for +# local developers who want to reproduce a CI config). +# +# This script is the single source of truth mapping a short config name to the +# exact wolfSSL ./configure flags. CI hashes `--print-flags ` into its +# cache key, so the cached prefix and a local build can never disagree. +# +# Usage: +# build-wolfssl.sh [--prefix DIR] [--ref REF] [--jobs N] [--src DIR] +# build-wolfssl.sh --print-flags # emit the configure flags (for hashing) +# build-wolfssl.sh --list # list known config names +# +# Defaults: --ref master, --prefix $PWD/.wolfssl-install/, --jobs nproc. +# +# The build is idempotent: if /lib/pkgconfig/wolfssl.pc already exists, +# the build is skipped (a warm CI cache short-circuits, local re-runs are fast). + +set -euo pipefail + +WOLFSSL_REPO="${WOLFSSL_REPO:-https://github.com/wolfSSL/wolfssl.git}" + +# ---------------------------------------------------------------------------- +# Config-name -> wolfSSL ./configure argument array. +# +# The canonical base (satisfies every hard wolfCert requirement plus all +# optional key algorithms) mirrors README.md / CLAUDE.md. Each variant layers +# a delta onto that base. VAR=VALUE assignments are passed to configure as +# single argv elements so embedded spaces survive word-splitting. +# ---------------------------------------------------------------------------- + +# wolfCert never links wolfSSL's own testsuite/benchmark, so skip building them +# -- a large CI wall-clock saving with no effect on the installed library. +_ci_flags() { + printf '%s\n' --disable-examples --disable-crypttests +} + +# Canonical "everything on" wolfSSL feature set. +_base_flags() { + _ci_flags + printf '%s\n' \ + --enable-pkcs7 --enable-certgen --enable-certreq --enable-certext \ + --enable-keygen --enable-ecc --enable-cryptocb --enable-base64encode \ + --enable-ed25519 --enable-ed448 --enable-mldsa \ + --enable-postauth --enable-opensslextra --enable-ip-alt-name \ + 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL' +} + +# List of every config name this script understands (kept in sync with the +# case in resolve_flags; used by --list and to validate input). +KNOWN_CONFIGS=( + full full-tsan + est-only-nonrsa rsa-min ecc-only-est + no-des3 tls13-only + mldsa-44off mldsa-65off mldsa-87off + static-mem no-malloc + # Negative configs consumed by assert-configure-fails.sh: a valid wolfSSL + # that wolfCert configure MUST reject. Only the two below are buildable -- + # wolfSSL's own configure refuses to drop AES/SHA-256/all-TLS/all-key-algs + # (those are cascade-required), so wolfCert's compile-time #error guards for + # them in check_config.h cannot be fed by a real wolfSSL build. + neg-no-rsa neg-no-pkcs7 +) + +# Emit the configure argument list (one per line) for a config name. +resolve_flags() { + local cfg="$1" + case "$cfg" in + full) + _base_flags ;; + full-tsan) + _base_flags + printf '%s\n' 'CFLAGS=-fsanitize=thread -g -O1' \ + 'LDFLAGS=-fsanitize=thread' ;; + est-only-nonrsa) + # EST-capable, RSA absent (NO_RSA). ECC + Ed + ML-DSA still present. + # RSA is default-on in wolfSSL, so --disable-rsa is the only delta. + _base_flags + printf '%s\n' --disable-rsa ;; + rsa-min) + # Minimal single-algorithm build: RSA only, no ECC/Ed/ML-DSA. + _ci_flags + printf '%s\n' \ + --enable-pkcs7 --enable-certgen --enable-certreq --enable-certext \ + --enable-keygen --enable-cryptocb --enable-base64encode \ + --enable-postauth --enable-opensslextra --enable-ip-alt-name \ + --disable-ecc --disable-ed25519 --disable-ed448 --disable-dilithium \ + 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL' ;; + ecc-only-est) + # EST with ECC keys, RSA absent (so SCEP must be disabled by caller). + _ci_flags + printf '%s\n' \ + --enable-pkcs7 --enable-certgen --enable-certreq --enable-certext \ + --enable-keygen --enable-ecc --enable-cryptocb --enable-base64encode \ + --enable-postauth --enable-opensslextra --enable-ip-alt-name \ + --disable-rsa --disable-ed25519 --disable-ed448 --disable-dilithium \ + 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL' ;; + no-des3) + # SCEP content encryption falls to AES-only (no 3DES fallback path). + _base_flags + printf '%s\n' --disable-des3 ;; + tls13-only) + # WOLFSSL_NO_TLS12 -> the TLS floor becomes 1.3; keeps PHA meaningful. + _base_flags + printf '%s\n' --disable-tlsv12 ;; + mldsa-44off) + _base_flags + printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DWOLFSSL_NO_ML_DSA_44' ;; + mldsa-65off) + _base_flags + printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DWOLFSSL_NO_ML_DSA_65' ;; + mldsa-87off) + _base_flags + printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DWOLFSSL_NO_ML_DSA_87' ;; + static-mem) + # Static memory pools + single-threaded (constrained-target shape). + _base_flags + printf '%s\n' --enable-staticmemory --enable-singlethreaded ;; + no-malloc) + # WOLFSSL_NO_MALLOC: no dynamic allocator at all, so pair it with + # static-memory pools (the only allocation source). Tests load a + # pool and register it as wolfCert's default heap. SCEP server needs + # MAX_SIGNED_ATTRIBS_SZ>=9 to carry the full RFC 8894 signed- + # attribute set without heap growth. + _base_flags + printf '%s\n' --enable-staticmemory \ + 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DWOLFSSL_NO_MALLOC -DMAX_SIGNED_ATTRIBS_SZ=9' ;; + + # -------- negative configs (a buildable wolfSSL wolfCert MUST reject) -- + neg-no-rsa) + # NO_RSA with SCEP still requested -> "SCEP is RSA-only". + _base_flags + printf '%s\n' --disable-rsa ;; + neg-no-pkcs7) + # Missing a tier-1 symbol (HAVE_PKCS7) -> "built without HAVE_PKCS7". + _ci_flags + printf '%s\n' \ + --enable-certgen --enable-certreq --enable-certext \ + --enable-keygen --enable-ecc --enable-cryptocb --enable-base64encode \ + --enable-opensslextra --enable-ip-alt-name \ + 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL' ;; + *) + echo "ERROR: unknown wolfSSL config '$cfg'." >&2 + echo " Known: ${KNOWN_CONFIGS[*]}" >&2 + exit 2 ;; + esac +} + +# ---------------------------------------------------------------------------- +# Argument parsing +# ---------------------------------------------------------------------------- +if [ "$#" -eq 0 ]; then + echo "ERROR: no config given. Try --list." >&2 + exit 2 +fi + +case "$1" in + --list) + printf '%s\n' "${KNOWN_CONFIGS[@]}" + exit 0 ;; + --print-flags) + [ "$#" -ge 2 ] || { echo "ERROR: --print-flags needs a config name." >&2; exit 2; } + resolve_flags "$2" + exit 0 ;; +esac + +CONFIG="$1"; shift +PREFIX="" +REF="${WOLFSSL_REF:-master}" +JOBS="" +SRC="" + +while [ "$#" -gt 0 ]; do + case "$1" in + --prefix) PREFIX="$2"; shift 2 ;; + --ref) REF="$2"; shift 2 ;; + --jobs) JOBS="$2"; shift 2 ;; + --src) SRC="$2"; shift 2 ;; + *) echo "ERROR: unknown argument '$1'." >&2; exit 2 ;; + esac +done + +# Validate config early (resolve_flags exits 2 on unknown). Read into the +# array with a while-read loop rather than `mapfile`: mapfile is bash 4+, but +# this script runs under the macOS runners' /bin/bash 3.2 on a cache miss. +CONFIGURE_FLAGS=() +while IFS= read -r _flag; do + CONFIGURE_FLAGS+=("$_flag") +done < <(resolve_flags "$CONFIG") + +: "${PREFIX:=$PWD/.wolfssl-install/$CONFIG}" +: "${JOBS:=$( (command -v nproc >/dev/null && nproc) || sysctl -n hw.ncpu 2>/dev/null || echo 2)}" +: "${SRC:=$PWD/.wolfssl-src/$CONFIG}" + +# Idempotent short-circuit: a warm cache already has the install tree. +if [ -f "$PREFIX/lib/pkgconfig/wolfssl.pc" ]; then + echo "wolfSSL '$CONFIG' already installed at $PREFIX (skipping build)." + echo "$PREFIX" + exit 0 +fi + +echo "==> Building wolfSSL config '$CONFIG'" +echo " ref: $REF" +echo " prefix: $PREFIX" +echo " flags: ${CONFIGURE_FLAGS[*]}" + +# Clone (shallow) at the requested ref if we don't have the source yet. +if [ ! -d "$SRC/.git" ]; then + rm -rf "$SRC" + # Fast path: a branch/tag ref clones directly (shallow). Fallback: $REF is + # a raw SHA, which --branch can't take, so do a full clone and check the + # commit out explicitly -- otherwise we'd silently build the default branch. + git clone --depth 1 --branch "$REF" "$WOLFSSL_REPO" "$SRC" 2>/dev/null \ + || { git clone "$WOLFSSL_REPO" "$SRC" \ + && git -C "$SRC" checkout "$REF"; } + if [ ! -e "$SRC/configure" ] && [ ! -f "$SRC/configure.ac" ]; then + echo "ERROR: wolfSSL checkout looks empty at $SRC" >&2 + exit 1 + fi +fi + +cd "$SRC" +if [ ! -x ./configure ]; then + ./autogen.sh +fi + +./configure --prefix="$PREFIX" "${CONFIGURE_FLAGS[@]}" +make "-j${JOBS}" +make install + +echo "==> Installed wolfSSL '$CONFIG' to $PREFIX" +echo "$PREFIX" diff --git a/scripts/ci/check-buildsystem-parity.sh b/scripts/ci/check-buildsystem-parity.sh new file mode 100755 index 0000000..ea8751e --- /dev/null +++ b/scripts/ci/check-buildsystem-parity.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Guard the "CMake is primary, autoconf is kept at parity" invariant. A source +# added to one build system but not the other is a common and easy-to-miss +# drift; this fails CI when it happens. Two independent sets are compared: +# +# 1. Library sources (src/*.c): CMakeLists.txt vs Makefile.am. +# 2. Test sources (tests/{unit,integration}/*.c): tests/CMakeLists.txt vs +# Makefile.am -- so `make check` and CTest register the same test set. + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" + +indent() { # prefix each input line with two spaces + while IFS= read -r line; do printf ' %s\n' "$line"; done +} + +# extract_lib : sorted-unique src/*.c references. +extract_lib() { + grep -oE 'src/[A-Za-z0-9_/]+\.c' "$1" | sort -u +} + +# extract_tests_am : sorted-unique test sources, normalized to +# the unit/*.c | integration/*.c form (the tests/ prefix is stripped). +extract_tests_am() { + grep -oE 'tests/(unit|integration)/[A-Za-z0-9_]+\.c' "$1" \ + | sed 's|^tests/||' | sort -u +} + +# extract_tests_cmake : same normalized form (paths are +# already relative to tests/). +extract_tests_cmake() { + grep -oE '(unit|integration)/[A-Za-z0-9_]+\.c' "$1" | sort -u +} + +status=0 + +# compare : report either-side drift. +compare() { + local what="$1" a="$2" na="$3" b="$4" nb="$5" + local only_a only_b + only_a="$(comm -23 <(printf '%s\n' "$a") <(printf '%s\n' "$b"))" + only_b="$(comm -13 <(printf '%s\n' "$a") <(printf '%s\n' "$b"))" + if [ -n "$only_a" ]; then + echo "ERROR: $what in $na but MISSING from $nb:" + printf '%s\n' "$only_a" | indent + status=1 + fi + if [ -n "$only_b" ]; then + echo "ERROR: $what in $nb but MISSING from $na:" + printf '%s\n' "$only_b" | indent + status=1 + fi +} + +cmake_srcs="$(extract_lib "$ROOT/CMakeLists.txt")" +am_srcs="$(extract_lib "$ROOT/Makefile.am")" +compare "library sources" \ + "$cmake_srcs" CMakeLists.txt "$am_srcs" Makefile.am + +cmake_tests="$(extract_tests_cmake "$ROOT/tests/CMakeLists.txt")" +am_tests="$(extract_tests_am "$ROOT/Makefile.am")" +compare "test sources" \ + "$cmake_tests" tests/CMakeLists.txt "$am_tests" Makefile.am + +if [ "$status" -eq 0 ]; then + echo "build-system parity OK: $(echo "$cmake_srcs" | wc -l | tr -d ' ') library"\ + "sources and $(echo "$cmake_tests" | wc -l | tr -d ' ') test sources match." +fi +exit "$status" diff --git a/scripts/ci/sigpipe-launcher.sh b/scripts/ci/sigpipe-launcher.sh new file mode 100755 index 0000000..698cef1 --- /dev/null +++ b/scripts/ci/sigpipe-launcher.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# +# CTest test launcher that runs each test with SIGPIPE ignored. +# +# Some integration tests write to a loopback socket after the peer has closed +# it (a benign teardown race); with the default SIGPIPE disposition that kills +# the process. ctest resets SIGPIPE to its default for the test processes it +# spawns, so ignoring it in the parent shell does not reach the tests. This +# launcher restores SIG_IGN and then execs the test, and an ignored signal +# disposition IS inherited across exec -- so the test sees EPIPE from write() +# instead of dying. Wired in via -DCMAKE_CROSSCOMPILING_EMULATOR so it applies +# to every test with no change to library or test source, and works uniformly +# for plain, ASan and TSAN builds (unlike an LD_PRELOAD shim, which collides +# with the sanitizer runtime ordering). +trap '' PIPE +exec "$@" From 6b3b6fed44061d839bddbb4a42a73030fe083414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Mon, 13 Jul 2026 19:30:10 +0200 Subject: [PATCH 2/2] Make wolfCert build and pass across the reduced wolfSSL CI configs The CI matrix exercises wolfCert against reduced wolfSSL builds (NO_RSA, NO_ECC, per-level ML-DSA, EST/SCEP/server subsets, static memory, NO_MALLOC). Make the library and tests portable across all of them, and land the CSR-attribute enforcement hardening alongside. Tests: - tls_test_util.h: mint self-signed identities with whatever signature algorithm the build provides (RSA, else ECC P-256); collapse the six duplicated identity/CA generators into shared helpers (mint_self_id, gen_server_identity). - Gate key types on WOLFCERT_HAVE_RSA/ECC and each ML-DSA level on WOLFSSL_NO_ML_DSA_{44,65,87}; add a TEST_ENROLL_KEY_TYPE default for enrollments where the key algorithm is incidental. - test_static_mem.h: load a wolfSSL static-memory pool and register it as the global heap (before wolfcert_init) so the NO_MALLOC unit tests can allocate. Library: - cli/wolfcert_server.c: guard the EST-only --csrattrs-file validation with WOLFCERT_HAVE_EST so a SCEP-only build links. - Move wolfcert_oid_to_dotted from est_server.c to csr_attrs.c so it is available in EST builds without the server. - internal.h: include for pid_t under HAVE_GETPID (static memory builds reference it in wolfssl/wolfcrypt/random.h). - csr_attrs_enforce: collapse the OID out-parameters into a single value-result size_t*, reset it to 0 on every non-missing path, and gate the caller so no error path can render an unpopulated stack buffer into the HTTP 400 body; add an end-to-end test asserting the reported OID. --- cli/wolfcert_server.c | 10 +- src/est/csr_attrs.c | 37 ++++ src/est/est_server.c | 70 ++++---- src/internal.h | 6 + tests/integration/test_est_async_roundtrip.c | 66 +------ .../test_est_csr_attrs_apply_roundtrip.c | 12 +- .../integration/test_est_csr_attrs_enforce.c | 134 +++++++++++++- tests/integration/test_est_mldsa_roundtrip.c | 7 + tests/integration/test_est_mtls_roundtrip.c | 73 +------- .../integration/test_est_pending_roundtrip.c | 2 +- tests/integration/test_est_pha_roundtrip.c | 70 +------- tests/integration/test_est_roundtrip.c | 14 +- tests/integration/test_est_tls_roundtrip.c | 60 +------ tests/integration/test_tls_http.c | 63 +------ tests/integration/tls_test_util.h | 167 +++++++++++++++--- tests/test_static_mem.h | 65 +++++++ tests/unit/test_csr.c | 8 + tests/unit/test_csr_attrs.c | 2 + tests/unit/test_est.c | 24 +-- tests/unit/test_keygen.c | 16 ++ tests/unit/test_parse_negative.c | 2 + tests/unit/test_scep_msg.c | 2 + tests/unit/test_smoke.c | 5 + tests/unit/test_store.c | 6 +- 24 files changed, 522 insertions(+), 399 deletions(-) create mode 100644 tests/test_static_mem.h diff --git a/cli/wolfcert_server.c b/cli/wolfcert_server.c index 28afc85..413b82e 100644 --- a/cli/wolfcert_server.c +++ b/cli/wolfcert_server.c @@ -26,7 +26,9 @@ #include #include -#include +#ifdef WOLFCERT_HAVE_EST +# include +#endif #include #include @@ -306,7 +308,10 @@ int main(int argc, char** argv) /* Parse --csrattrs-file at startup so a bad blob fails fast with a * clear operator-facing message instead of an obscure error from - * the first /csrattrs hit. */ + * the first /csrattrs hit. CsrAttrs is an EST-only concept (RFC 7030 + * section 4.5.2), so the validation is compiled only when EST is built; + * a SCEP-only server ignores the blob. */ +#ifdef WOLFCERT_HAVE_EST if (csr_attrs_blob != NULL && csr_attrs_blob_len > 0) { WolfCertCsrAttrs check; int prc = wolfcert_est_parse_csr_attrs(csr_attrs_blob, @@ -326,6 +331,7 @@ int main(int argc, char** argv) } wolfcert_csr_attrs_free(&check); } +#endif WolfCertServerCfgSrv cfg = { .protocol = sel, diff --git a/src/est/csr_attrs.c b/src/est/csr_attrs.c index 756b4ba..49f6bd9 100644 --- a/src/est/csr_attrs.c +++ b/src/est/csr_attrs.c @@ -42,6 +42,7 @@ #include +#include #include /* ---- minimal DER parsing helpers --------------------------------------- */ @@ -634,3 +635,39 @@ int wolfcert_csr_attrs_apply(const WolfCertCsrAttrs* attrs, return WOLFCERT_OK; } + +/* Render a DER-encoded OID as a dotted-decimal string into `out` (always + * NUL-terminated when out_cap > 0). Returns the number of bytes written + * (excluding the terminator). A general OID utility used by the EST server's + * missing-attribute diagnostic and unit-tested directly; it lives here rather + * than in est_server.c so it is available in EST builds without the server. */ +WOLFCERT_TEST_VIS size_t wolfcert_oid_to_dotted(const uint8_t* oid, size_t oid_len, + char* out, size_t out_cap) +{ + size_t off = 0; + + /* Always leave a valid C string, even for an empty OID or zero capacity. */ + if (out_cap > 0) + out[0] = '\0'; + + if (oid_len >= 1) { + /* First byte holds the first two arcs as 40*node1 + node2. node1 is + * capped at 2, so for a first byte >= 80 node2 is the remainder above + * 80 (node2 can exceed 40 only when node1 == 2). */ + unsigned first = oid[0] < 80 ? oid[0] / 40 : 2; + unsigned second = oid[0] < 80 ? oid[0] % 40 : oid[0] - 80u; + off += (size_t)snprintf(out + off, out_cap - off, "%u.%u", + first, second); + } + + unsigned long n = 0; + for (size_t i = 1; i < oid_len && off + 16 < out_cap; ++i) { + n = (n << 7) | (oid[i] & 0x7F); + if ((oid[i] & 0x80) == 0) { + off += (size_t)snprintf(out + off, out_cap - off, ".%lu", n); + n = 0; + } + } + + return off; +} diff --git a/src/est/est_server.c b/src/est/est_server.c index 118e895..e6967fb 100644 --- a/src/est/est_server.c +++ b/src/est/est_server.c @@ -760,12 +760,23 @@ static int csr__has_attr_oid(const uint8_t* csr, size_t csr_len, /* Enforce est_require_csr_attributes: every bare-OID policy item in * s->cfg_csr_attrs must appear as an attribute type OID in the CSR. - * `err_oid_out` / `err_oid_len` are set to the missing OID (pointer - * into the policy blob) when the check fails. */ + * `err_oid_len` is a value-result parameter: on entry it holds the capacity + * of the caller-provided `err_oid_buf`. It is reset to 0 up front and set + * non-zero only on the missing-OID path (which copies the missing OID into + * `err_oid_buf`), so the caller can treat `*err_oid_len > 0` as "an OID was + * captured" without coupling to the exact return code. The OID is copied out + * before the parsed policy is freed: the WolfCertCsrAttrs owns its OID + * storage, so returning a pointer into it would dangle once the policy is + * released. */ static int csr_attrs_enforce(const WolfCertServer* s, const uint8_t* csr_der, size_t csr_len, - const uint8_t** err_oid_out, size_t* err_oid_len) + uint8_t* err_oid_buf, size_t* err_oid_len) { + /* Read the caller's buffer capacity, then default the out-length to 0 so + * every non-missing return path reports "no OID captured". */ + size_t err_oid_cap = *err_oid_len; + *err_oid_len = 0; + if (!s->cfg.est_require_csr_attributes || s->cfg_csr_attrs == NULL || s->cfg_csr_attrs_len == 0) return WOLFCERT_OK; @@ -788,8 +799,12 @@ static int csr_attrs_enforce(const WolfCertServer* s, policy.items[i].oid, policy.items[i].oid_len); if (has != 1) { - *err_oid_out = policy.items[i].oid; - *err_oid_len = policy.items[i].oid_len; + size_t n = policy.items[i].oid_len; + if (n > err_oid_cap) + n = err_oid_cap; + if (n > 0) + memcpy(err_oid_buf, policy.items[i].oid, n); + *err_oid_len = n; missing = 1; break; } @@ -800,37 +815,6 @@ static int csr_attrs_enforce(const WolfCertServer* s, return missing ? WOLFCERT_ERR_PROTOCOL : WOLFCERT_OK; } -WOLFCERT_TEST_VIS size_t wolfcert_oid_to_dotted(const uint8_t* oid, size_t oid_len, - char* out, size_t out_cap) -{ - size_t off = 0; - - /* Always leave a valid C string, even for an empty OID or zero capacity. */ - if (out_cap > 0) - out[0] = '\0'; - - if (oid_len >= 1) { - /* First byte holds the first two arcs as 40*node1 + node2. node1 is - * capped at 2, so for a first byte >= 80 node2 is the remainder above - * 80 (node2 can exceed 40 only when node1 == 2). */ - unsigned first = oid[0] < 80 ? oid[0] / 40 : 2; - unsigned second = oid[0] < 80 ? oid[0] % 40 : oid[0] - 80u; - off += (size_t)snprintf(out + off, out_cap - off, "%u.%u", - first, second); - } - - unsigned long n = 0; - for (size_t i = 1; i < oid_len && off + 16 < out_cap; ++i) { - n = (n << 7) | (oid[i] & 0x7F); - if ((oid[i] & 0x80) == 0) { - off += (size_t)snprintf(out + off, out_cap - off, ".%lu", n); - n = 0; - } - } - - return off; -} - /* Emit a 400 Bad Request whose body lists the missing OID in dotted * decimal. Helpful for humans debugging the round-trip. */ static void send_missing_oid(WolfCertServer* s, int fd, @@ -905,12 +889,18 @@ static int handler_enroll(WolfCertServer* s, int fd, const EstRequest* req) * wolfcert_ca_issue so an offending client can't walk away with a * cert even if the underlying issuance would have accepted it. */ if (s->cfg.est_require_csr_attributes) { - const uint8_t* missing_oid = NULL; - size_t missing_len = 0; + /* Room for the largest OID we would report; DER attribute-type OIDs + * are far shorter than this in practice. */ + uint8_t missing_oid[64]; + size_t missing_len = sizeof(missing_oid); /* in: cap, out: OID len */ int erc = csr_attrs_enforce(s, csr.data, csr.len, - &missing_oid, &missing_len); + missing_oid, &missing_len); if (erc != WOLFCERT_OK) { - if (missing_oid != NULL) + /* csr_attrs_enforce sets missing_len > 0 only when it captured a + * missing required OID into missing_oid. Gate on that explicitly + * (not on the return code alone) so no future error path can + * render an unpopulated buffer into the response body. */ + if (erc == WOLFCERT_ERR_PROTOCOL && missing_len > 0) send_missing_oid(s, fd, missing_oid, missing_len); else send_status(s, fd, 400, "Bad Request"); diff --git a/src/internal.h b/src/internal.h index 1765120..7231459 100644 --- a/src/internal.h +++ b/src/internal.h @@ -34,6 +34,12 @@ #include #include +/* wolfssl/wolfcrypt/random.h declares a `pid_t` member under HAVE_GETPID + * (e.g. static-memory builds). Pull in its POSIX declaration first so that + * header compiles; harmless on configs that don't reference it. */ +#if defined(HAVE_GETPID) && !defined(WOLFSSL_NO_GETPID) + #include +#endif #include #include #include diff --git a/tests/integration/test_est_async_roundtrip.c b/tests/integration/test_est_async_roundtrip.c index d9a9fd9..2c87c43 100644 --- a/tests/integration/test_est_async_roundtrip.c +++ b/tests/integration/test_est_async_roundtrip.c @@ -43,6 +43,8 @@ #include #include #include + +#include "tls_test_util.h" #include #include @@ -59,64 +61,6 @@ } \ } while (0) -static int mint_rsa_id(const char* cn, int is_ca, - uint8_t** cert_pem, size_t* cert_pem_len, - uint8_t** key_pem, size_t* key_pem_len) -{ - RsaKey key; - WC_RNG rng; - if (wc_InitRng(&rng) != 0) - return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { - wc_FreeRng(&rng); - return -1; - } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - - Cert cert; - wc_InitCert(&cert); - strcpy(cert.subject.commonName, cn); - cert.selfSigned = 1; - cert.sigType = CTC_SHA256wRSA; - cert.daysValid = 1; - cert.isCA = is_ca ? 1 : 0; - if (!is_ca) { - static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; - memcpy(cert.altNames, san_seq, sizeof(san_seq)); - cert.altNamesSz = (int)sizeof(san_seq); - } - uint8_t cder[8192]; - int cs = wc_MakeSelfCert(&cert, cder, sizeof(cder), &key, &rng); - if (cs <= 0) - goto fail; - uint8_t cpem[16384]; - int cp = wc_DerToPem(cder, cs, cpem, sizeof(cpem), CERT_TYPE); - if (cp <= 0) - goto fail; - uint8_t kder[8192]; - int ks = wc_RsaKeyToDer(&key, kder, sizeof(kder)); - if (ks <= 0) - goto fail; - uint8_t kpem[16384]; - int kp = wc_DerToPem(kder, ks, kpem, sizeof(kpem), PRIVATEKEY_TYPE); - if (kp <= 0) - goto fail; - - *cert_pem = malloc((size_t)cp); - memcpy(*cert_pem, cpem, (size_t)cp); - *cert_pem_len = (size_t)cp; - *key_pem = malloc((size_t)kp); - memcpy(*key_pem, kpem, (size_t)kp); - *key_pem_len = (size_t)kp; - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return 0; -fail: - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return -1; -} static void* server_thread(void* arg) { @@ -180,13 +124,13 @@ int main(void) size_t tls_cert_len = 0; uint8_t* tls_key = NULL; size_t tls_key_len = 0; - REQUIRE(mint_rsa_id("127.0.0.1", 0, + REQUIRE(mint_self_id("127.0.0.1", 0, &tls_cert, &tls_cert_len, &tls_key, &tls_key_len) == 0); uint8_t* cli_cert = NULL; size_t cli_cert_len = 0; uint8_t* cli_key = NULL; size_t cli_key_len = 0; - REQUIRE(mint_rsa_id("async-bootstrap", 1, + REQUIRE(mint_self_id("async-bootstrap", 1, &cli_cert, &cli_cert_len, &cli_key, &cli_key_len) == 0); WolfCertServerCfgSrv cfg = { @@ -229,7 +173,7 @@ int main(void) REQUIRE(ca_pem.len > 0); /* Build a CSR off the event loop. */ - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); diff --git a/tests/integration/test_est_csr_attrs_apply_roundtrip.c b/tests/integration/test_est_csr_attrs_apply_roundtrip.c index 1eb75a3..822e839 100644 --- a/tests/integration/test_est_csr_attrs_apply_roundtrip.c +++ b/tests/integration/test_est_csr_attrs_apply_roundtrip.c @@ -115,6 +115,7 @@ static int build_policy(WolfCertBuffer* out) * hint influences the caller's CSR signature, not the CA's issued-cert * signature, so it's not observable on the issued PEM. That path is * covered by the unit tests instead. */ +#ifdef WOLFCERT_HAVE_ECC static int inspect_cert(const uint8_t* pem, size_t pem_len, int* out_curve_id) { @@ -188,8 +189,10 @@ static int auto_apply_pins_everything(WolfCertServer* s) wolfcert_client_free(cli); return 0; } +#endif /* WOLFCERT_HAVE_ECC */ /* Sub-test 2: caller explicitly set RSA-2048 -> server hints ignored. */ +#ifdef WOLFCERT_HAVE_RSA static int explicit_caller_wins(WolfCertServer* s) { char url[128]; @@ -228,6 +231,7 @@ static int explicit_caller_wins(WolfCertServer* s) wolfcert_client_free(cli); return 0; } +#endif /* WOLFCERT_HAVE_RSA */ /* Sub-test 3: wolfcert_client_fetch_meta surfaces the hash hint onto * an empty WolfCertCertMeta without any key_cfg in play. */ @@ -281,9 +285,15 @@ int main(void) pthread_t tid; REQUIRE(pthread_create(&tid, NULL, server_thread, srv) == 0); - int rc = auto_apply_pins_everything(srv); + int rc = 0; +#ifdef WOLFCERT_HAVE_ECC + if (rc == 0) + rc = auto_apply_pins_everything(srv); +#endif +#ifdef WOLFCERT_HAVE_RSA if (rc == 0) rc = explicit_caller_wins(srv); +#endif if (rc == 0) rc = fetch_meta_overlays_hash(srv); diff --git a/tests/integration/test_est_csr_attrs_enforce.c b/tests/integration/test_est_csr_attrs_enforce.c index 0157ff5..8ed28b5 100644 --- a/tests/integration/test_est_csr_attrs_enforce.c +++ b/tests/integration/test_est_csr_attrs_enforce.c @@ -34,19 +34,28 @@ #define _POSIX_C_SOURCE 200809L #define _DEFAULT_SOURCE +#define _DARWIN_C_SOURCE /* expose INADDR_LOOPBACK on macOS */ #define _GNU_SOURCE #include #include +#include +#include #include #include #include "tls_test_util.h" +#include + +#include +#include #include #include #include #include +#include +#include #define REQUIRE(cond) \ do { \ @@ -122,7 +131,7 @@ static int enroll_with_challenge(WolfCertServer* s) WolfCertClient* cli = NULL; REQUIRE(wolfcert_client_new(&cli) == WOLFCERT_OK); - WolfCertKeyCfg key_cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg key_cfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertCertMeta meta = { .subject_dn = "CN=enforce-ok", @@ -157,7 +166,7 @@ static int enroll_without_challenge(WolfCertServer* s) WolfCertClient* cli = NULL; REQUIRE(wolfcert_client_new(&cli) == WOLFCERT_OK); - WolfCertKeyCfg key_cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg key_cfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertCertMeta meta = { .subject_dn = "CN=enforce-reject", @@ -178,6 +187,98 @@ static int enroll_without_challenge(WolfCertServer* s) return 0; } +/* Dial 127.0.0.1:port over plain TCP, send the request, and read the whole + * response (headers + body) into `resp`. A receive timeout keeps a + * misbehaving server from hanging the test. Returns bytes read, or -1. */ +static int send_and_read_all(uint16_t port, const void* req, size_t req_len, + char* resp, size_t cap) +{ + struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; + struct sockaddr_in sa = { .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + const char* p = req; + size_t left = req_len; + size_t n = 0; + int cs = socket(AF_INET, SOCK_STREAM, 0); + if (cs < 0) + return -1; + setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + if (connect(cs, (struct sockaddr*)&sa, sizeof(sa)) < 0) { + close(cs); + return -1; + } + while (left > 0) { + ssize_t w = send(cs, p, left, 0); + if (w <= 0) { + close(cs); + return -1; + } + p += (size_t)w; + left -= (size_t)w; + } + while (n + 1 < cap) { + ssize_t r = recv(cs, resp + n, cap - 1 - n, 0); + if (r <= 0) + break; + n += (size_t)r; + } + resp[n] = '\0'; + close(cs); + return (int)n; +} + +/* Raw plain-HTTP probe of the enforcement 400 body: build a real CSR that + * omits challengePassword, POST it to /simpleenroll, and assert the response + * both fails with 400 and names the exact missing OID in dotted form. This is + * the end-to-end check that the value-result OID copy in csr_attrs_enforce + * renders the correct bytes; the client path above only observes rejection, + * not the body. */ +static int reject_body_names_missing_oid(uint16_t port) +{ + /* challengePassword dotted OID (see OID_CHALLENGE_PASSWORD above). */ + static const char EXPECT_OID[] = "1.2.840.113549.1.9.7"; + WolfCertKeyCfg key_cfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, + .dev_id = WOLFCERT_DEVID_SOFTWARE }; + WolfCertCertMeta meta = { .subject_dn = "CN=raw-enforce" }; + WolfCertKey* key = NULL; + WolfCertBuffer csr_der = { 0 }; + byte b64[2048]; + word32 b64_len = sizeof(b64); + char req[4096]; + char resp[1024] = { 0 }; + int rl, n; + + REQUIRE(wolfcert_key_generate(&key_cfg, &key) == WOLFCERT_OK); + REQUIRE(wolfcert_csr_build(key, &meta, &csr_der) == WOLFCERT_OK); + + /* EST simpleenroll carries base64 PKCS#10; the server base64-decodes the + * request body (embedded newlines are tolerated by the decoder). */ + REQUIRE(Base64_Encode(csr_der.data, (word32)csr_der.len, b64, &b64_len) == 0); + + rl = snprintf(req, sizeof(req), + "POST /.well-known/est/simpleenroll HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/pkcs10\r\n" + "Content-Transfer-Encoding: base64\r\n" + "Content-Length: %u\r\n" + "Connection: close\r\n" + "\r\n" + "%.*s", + (unsigned)b64_len, (int)b64_len, (const char*)b64); + REQUIRE(rl > 0 && (size_t)rl < sizeof(req)); + + n = send_and_read_all(port, req, (size_t)rl, resp, sizeof(resp)); + + wolfcert_buffer_free(&csr_der); + wolfcert_key_free(key); + + REQUIRE(n > 0); + REQUIRE(strstr(resp, "400") != NULL); + REQUIRE(strstr(resp, EXPECT_OID) != NULL); + return 0; +} + /* Client C - server advertises ONLY an Attribute-with-values item * (no bare OIDs). The CSR doesn't carry anything matching it. * Enforcement is presence-only on bare OIDs, so this must still @@ -197,7 +298,7 @@ static int values_only_policy_does_not_block(WolfCertServer* s) WolfCertClient* cli = NULL; REQUIRE(wolfcert_client_new(&cli) == WOLFCERT_OK); - WolfCertKeyCfg key_cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg key_cfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertCertMeta meta = { .subject_dn = "CN=enforce-values-advisory" }; @@ -282,6 +383,33 @@ int main(void) if (rc != 0) return rc; + /* Plain-HTTP server (no TLS) with the same bare-OID policy, so the raw + * 400 body is readable: asserts it names the missing OID in dotted form. + * The client path above proves rejection; this proves the reported OID + * content (i.e. the value-result OID copy renders the right bytes). */ + WolfCertBuffer policy_raw = { 0 }; + REQUIRE(build_policy(&policy_raw) == WOLFCERT_OK); + WolfCertServerCfgSrv cfg_raw = { + .protocol = WOLFCERT_PROTO_EST, + .bind_host = "127.0.0.1", .bind_port = 0, + .csr_attributes_der = policy_raw.data, + .csr_attributes_len = policy_raw.len, + .est_require_csr_attributes = 1, + }; + WolfCertServer* srv_raw = NULL; + REQUIRE(wolfcert_server_start(&cfg_raw, &srv_raw) == WOLFCERT_OK); + pthread_t tid_raw; + REQUIRE(pthread_create(&tid_raw, NULL, server_thread, srv_raw) == 0); + + rc = reject_body_names_missing_oid(wolfcert_server_port(srv_raw)); + + wolfcert_server_stop(srv_raw); + pthread_join(tid_raw, NULL); + wolfcert_server_free(srv_raw); + wolfcert_buffer_free(&policy_raw); + if (rc != 0) + return rc; + wolfcert_cleanup(); printf("OK\n"); return 0; diff --git a/tests/integration/test_est_mldsa_roundtrip.c b/tests/integration/test_est_mldsa_roundtrip.c index 733cb94..49914ca 100644 --- a/tests/integration/test_est_mldsa_roundtrip.c +++ b/tests/integration/test_est_mldsa_roundtrip.c @@ -150,9 +150,16 @@ int main(void) REQUIRE(wolfcert_est_get_cacerts(&client_cfg, &ca_pem) == WOLFCERT_OK); int rc = 0; + /* Each level can be disabled independently (WOLFSSL_NO_ML_DSA_{44,65,87}). */ +#ifndef WOLFSSL_NO_ML_DSA_44 rc |= enroll_mldsa(&client_cfg, WOLFCERT_KEY_MLDSA44, "mldsa44", &ca_pem); +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 rc |= enroll_mldsa(&client_cfg, WOLFCERT_KEY_MLDSA65, "mldsa65", &ca_pem); +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 rc |= enroll_mldsa(&client_cfg, WOLFCERT_KEY_MLDSA87, "mldsa87", &ca_pem); +#endif wolfcert_server_stop(s); pthread_join(tid, NULL); diff --git a/tests/integration/test_est_mtls_roundtrip.c b/tests/integration/test_est_mtls_roundtrip.c index 58cfcc3..142bb5e 100644 --- a/tests/integration/test_est_mtls_roundtrip.c +++ b/tests/integration/test_est_mtls_roundtrip.c @@ -48,6 +48,8 @@ #include #include #include + +#include "tls_test_util.h" #include #include @@ -65,67 +67,6 @@ } while (0) /* Build a self-signed RSA identity suitable for TLS usage. */ -static int mint_rsa_id(const char* cn, - uint8_t** cert_pem, size_t* cert_pem_len, - uint8_t** key_pem, size_t* key_pem_len, - int is_ca) -{ - RsaKey key; - WC_RNG rng; - if (wc_InitRng(&rng) != 0) - return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { - wc_FreeRng(&rng); - return -1; - } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - - Cert cert; - wc_InitCert(&cert); - strcpy(cert.subject.commonName, cn); - cert.selfSigned = 1; - cert.sigType = CTC_SHA256wRSA; - cert.daysValid = 1; - cert.isCA = is_ca ? 1 : 0; - if (!is_ca) { - static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; - memcpy(cert.altNames, san_seq, sizeof(san_seq)); - cert.altNamesSz = (int)sizeof(san_seq); - } - - uint8_t cder[8192]; - int cs = wc_MakeSelfCert(&cert, cder, sizeof(cder), &key, &rng); - if (cs <= 0) - goto fail; - uint8_t cpem[16384]; - int cp = wc_DerToPem(cder, cs, cpem, sizeof(cpem), CERT_TYPE); - if (cp <= 0) - goto fail; - - uint8_t kder[8192]; - int ks = wc_RsaKeyToDer(&key, kder, sizeof(kder)); - if (ks <= 0) - goto fail; - uint8_t kpem[16384]; - int kp = wc_DerToPem(kder, ks, kpem, sizeof(kpem), PRIVATEKEY_TYPE); - if (kp <= 0) - goto fail; - - *cert_pem = malloc((size_t)cp); - memcpy(*cert_pem, cpem, (size_t)cp); - *cert_pem_len = (size_t)cp; - *key_pem = malloc((size_t)kp); - memcpy(*key_pem, kpem, (size_t)kp); - *key_pem_len = (size_t)kp; - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return 0; -fail: - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return -1; -} static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } @@ -138,8 +79,8 @@ int main(void) size_t tls_cert_len = 0; uint8_t* tls_key = NULL; size_t tls_key_len = 0; - REQUIRE(mint_rsa_id("127.0.0.1", - &tls_cert, &tls_cert_len, &tls_key, &tls_key_len, 0) == 0); + REQUIRE(mint_self_id("127.0.0.1", 0, + &tls_cert, &tls_cert_len, &tls_key, &tls_key_len) == 0); /* Self-signed "bootstrap CA" that we both pin as the server's * tls_client_ca_pem AND use as the client's presented cert - a @@ -149,8 +90,8 @@ int main(void) size_t cli_cert_len = 0; uint8_t* cli_key = NULL; size_t cli_key_len = 0; - REQUIRE(mint_rsa_id("factory-bootstrap", - &cli_cert, &cli_cert_len, &cli_key, &cli_key_len, 1) == 0); + REQUIRE(mint_self_id("factory-bootstrap", 1, + &cli_cert, &cli_cert_len, &cli_key, &cli_key_len) == 0); WolfCertServerCfgSrv cfg = { .protocol = WOLFCERT_PROTO_EST, @@ -203,7 +144,7 @@ int main(void) REQUIRE(ca_pem.len > 0); wolfcert_buffer_free(&ca_pem); - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); diff --git a/tests/integration/test_est_pending_roundtrip.c b/tests/integration/test_est_pending_roundtrip.c index 964f85b..0be8f5c 100644 --- a/tests/integration/test_est_pending_roundtrip.c +++ b/tests/integration/test_est_pending_roundtrip.c @@ -97,7 +97,7 @@ static int pump_enroll_nb(WolfCertEstSession* s, const uint8_t* csr, static int make_csr(const char* subject, WolfCertKey** out_key, WolfCertBuffer* out_csr) { - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; if (wolfcert_key_generate(&kcfg, out_key) != WOLFCERT_OK) return 1; diff --git a/tests/integration/test_est_pha_roundtrip.c b/tests/integration/test_est_pha_roundtrip.c index 25c3e99..06fb64d 100644 --- a/tests/integration/test_est_pha_roundtrip.c +++ b/tests/integration/test_est_pha_roundtrip.c @@ -46,6 +46,8 @@ #include #include #include + +#include "tls_test_util.h" #include #include @@ -61,66 +63,6 @@ } \ } while (0) -static int mint_rsa_id(const char* cn, int is_ca, - uint8_t** cert_pem, size_t* cert_pem_len, - uint8_t** key_pem, size_t* key_pem_len) -{ - RsaKey key; - WC_RNG rng; - if (wc_InitRng(&rng) != 0) - return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { - wc_FreeRng(&rng); - return -1; - } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - - Cert cert; - wc_InitCert(&cert); - strcpy(cert.subject.commonName, cn); - cert.selfSigned = 1; - cert.sigType = CTC_SHA256wRSA; - cert.daysValid = 1; - cert.isCA = is_ca ? 1 : 0; - if (!is_ca) { - static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; - memcpy(cert.altNames, san_seq, sizeof(san_seq)); - cert.altNamesSz = (int)sizeof(san_seq); - } - - uint8_t cder[8192]; - int cs = wc_MakeSelfCert(&cert, cder, sizeof(cder), &key, &rng); - if (cs <= 0) - goto fail; - uint8_t cpem[16384]; - int cp = wc_DerToPem(cder, cs, cpem, sizeof(cpem), CERT_TYPE); - if (cp <= 0) - goto fail; - - uint8_t kder[8192]; - int ks = wc_RsaKeyToDer(&key, kder, sizeof(kder)); - if (ks <= 0) - goto fail; - uint8_t kpem[16384]; - int kp = wc_DerToPem(kder, ks, kpem, sizeof(kpem), PRIVATEKEY_TYPE); - if (kp <= 0) - goto fail; - - *cert_pem = malloc((size_t)cp); - memcpy(*cert_pem, cpem, (size_t)cp); - *cert_pem_len = (size_t)cp; - *key_pem = malloc((size_t)kp); - memcpy(*key_pem, kpem, (size_t)kp); - *key_pem_len = (size_t)kp; - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return 0; -fail: - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return -1; -} static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } @@ -132,7 +74,7 @@ int main(void) size_t tls_cert_len = 0; uint8_t* tls_key = NULL; size_t tls_key_len = 0; - REQUIRE(mint_rsa_id("127.0.0.1", 0, + REQUIRE(mint_self_id("127.0.0.1", 0, &tls_cert, &tls_cert_len, &tls_key, &tls_key_len) == 0); /* Self-signed client CA that also serves as the client's presented @@ -142,7 +84,7 @@ int main(void) size_t cli_cert_len = 0; uint8_t* cli_key = NULL; size_t cli_key_len = 0; - REQUIRE(mint_rsa_id("factory-bootstrap", 1, + REQUIRE(mint_self_id("factory-bootstrap", 1, &cli_cert, &cli_cert_len, &cli_key, &cli_key_len) == 0); WolfCertServerCfgSrv cfg = { @@ -190,7 +132,7 @@ int main(void) /* /simpleenroll on the same connection - this is the call that * triggers the server's wolfSSL_request_certificate(). */ - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); @@ -233,7 +175,7 @@ int main(void) REQUIRE(ca_pem.len > 0); wolfcert_buffer_free(&ca_pem); - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); diff --git a/tests/integration/test_est_roundtrip.c b/tests/integration/test_est_roundtrip.c index f767ff3..2f2118a 100644 --- a/tests/integration/test_est_roundtrip.c +++ b/tests/integration/test_est_roundtrip.c @@ -117,7 +117,7 @@ static int has_alt(const DNS_entry* list, int type, const char* val, int len) * email SAN from the issued cert entirely. */ static int enroll_check_san(const WolfCertServerCfg* client_cfg) { - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); @@ -218,8 +218,10 @@ int main(void) wolfcert_buffer_free(&ca_der); wolfcert_client_free(client); - /* One round-trip per key type: ECC always, Ed25519/Ed448 when enabled. */ - if (enroll_one(&client_cfg, WOLFCERT_KEY_ECC, 256, &ca_pem)) + /* One round-trip per key type: a supported default always, Ed25519/Ed448 + * when enabled. */ + if (enroll_one(&client_cfg, TEST_ENROLL_KEY_TYPE, TEST_ENROLL_KEY_PARAM, + &ca_pem)) return 1; #ifdef WOLFCERT_HAVE_ED25519 if (enroll_one(&client_cfg, WOLFCERT_KEY_ED25519, 0, &ca_pem)) @@ -239,7 +241,7 @@ int main(void) * signature value (DER structure stays intact so it still parses), and * confirm the server refuses to issue. Credentials are still valid here, * so a rejection can only come from the PoP check. */ - WolfCertKeyCfg pop_kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg pop_kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* pop_dk = NULL; REQUIRE(wolfcert_key_generate(&pop_kcfg, &pop_dk) == WOLFCERT_OK); @@ -258,7 +260,7 @@ int main(void) wolfcert_key_free(pop_dk); /* Auth failure path - needs a CSR to send. */ - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); @@ -301,7 +303,7 @@ int main(void) .verify_server = 1, .username = "alice", .password = "hunter" }; - WolfCertKeyCfg akcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg akcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* adk = NULL; REQUIRE(wolfcert_key_generate(&akcfg, &adk) == WOLFCERT_OK); diff --git a/tests/integration/test_est_tls_roundtrip.c b/tests/integration/test_est_tls_roundtrip.c index a6ec148..9841feb 100644 --- a/tests/integration/test_est_tls_roundtrip.c +++ b/tests/integration/test_est_tls_roundtrip.c @@ -41,6 +41,8 @@ #include #include #include + +#include "tls_test_util.h" #include #include @@ -56,62 +58,6 @@ } \ } while (0) -static int gen_server_identity(uint8_t** cert_pem, size_t* cert_pem_len, - uint8_t** key_pem, size_t* key_pem_len) -{ - RsaKey key; - WC_RNG rng; - if (wc_InitRng(&rng) != 0) - return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { - wc_FreeRng(&rng); - return -1; - } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - - Cert cert; - wc_InitCert(&cert); - strcpy(cert.subject.commonName, "127.0.0.1"); - cert.selfSigned = 1; - cert.sigType = CTC_SHA256wRSA; - cert.daysValid = 1; - static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; - memcpy(cert.altNames, san_seq, sizeof(san_seq)); - cert.altNamesSz = (int)sizeof(san_seq); - - uint8_t cder[8192]; - int cs = wc_MakeSelfCert(&cert, cder, sizeof(cder), &key, &rng); - if (cs <= 0) - goto fail; - uint8_t cpem[16384]; - int cp = wc_DerToPem(cder, cs, cpem, sizeof(cpem), CERT_TYPE); - if (cp <= 0) - goto fail; - - uint8_t kder[8192]; - int ks = wc_RsaKeyToDer(&key, kder, sizeof(kder)); - if (ks <= 0) - goto fail; - uint8_t kpem[16384]; - int kp = wc_DerToPem(kder, ks, kpem, sizeof(kpem), PRIVATEKEY_TYPE); - if (kp <= 0) - goto fail; - - *cert_pem = malloc((size_t)cp); - memcpy(*cert_pem, cpem, (size_t)cp); - *cert_pem_len = (size_t)cp; - *key_pem = malloc((size_t)kp); - memcpy(*key_pem, kpem, (size_t)kp); - *key_pem_len = (size_t)kp; - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return 0; -fail: - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return -1; -} static void* server_thread(void* arg) { wolfcert_server_run((WolfCertServer*)arg); return NULL; } @@ -155,7 +101,7 @@ int main(void) REQUIRE(ca_pem.len > 0); /* Generate a device key, build CSR, enroll over HTTPS. */ - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); diff --git a/tests/integration/test_tls_http.c b/tests/integration/test_tls_http.c index ad8ea97..797a016 100644 --- a/tests/integration/test_tls_http.c +++ b/tests/integration/test_tls_http.c @@ -36,6 +36,8 @@ #include #include +#include "tls_test_util.h" + #include #include #include @@ -112,69 +114,12 @@ static void* srv_thread(void* arg) return NULL; } -static int gen_server_identity(struct srv_ctx* sc) -{ - RsaKey key; - WC_RNG rng; - if (wc_InitRng(&rng) != 0) - return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { - wc_FreeRng(&rng); - return -1; - } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - - Cert cert; - wc_InitCert(&cert); - strcpy(cert.subject.commonName, "127.0.0.1"); - cert.selfSigned = 1; - cert.sigType = CTC_SHA256wRSA; - cert.daysValid = 1; - /* SAN entry for 127.0.0.1 (IP) */ - static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; - memcpy(cert.altNames, san_seq, sizeof(san_seq)); - cert.altNamesSz = (int)sizeof(san_seq); - - uint8_t cert_der[8192]; - int cs = wc_MakeSelfCert(&cert, cert_der, sizeof(cert_der), &key, &rng); - if (cs <= 0) - goto fail; - - uint8_t cert_pem_buf[16384]; - int cp = wc_DerToPem(cert_der, cs, cert_pem_buf, sizeof(cert_pem_buf), CERT_TYPE); - if (cp <= 0) - goto fail; - - uint8_t key_der[8192]; - int ks = wc_RsaKeyToDer(&key, key_der, sizeof(key_der)); - if (ks <= 0) - goto fail; - uint8_t key_pem_buf[16384]; - int kp = wc_DerToPem(key_der, ks, key_pem_buf, sizeof(key_pem_buf), PRIVATEKEY_TYPE); - if (kp <= 0) - goto fail; - - sc->cert_pem = malloc((size_t)cp); - memcpy(sc->cert_pem, cert_pem_buf, (size_t)cp); - sc->cert_pem_len = (size_t)cp; - sc->key_pem = malloc((size_t)kp); - memcpy(sc->key_pem, key_pem_buf, (size_t)kp); - sc->key_pem_len = (size_t)kp; - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return 0; -fail: - wc_FreeRsaKey(&key); - wc_FreeRng(&rng); - return -1; -} - int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); struct srv_ctx sc = { 0 }; - REQUIRE(gen_server_identity(&sc) == 0); + REQUIRE(gen_server_identity(&sc.cert_pem, &sc.cert_pem_len, + &sc.key_pem, &sc.key_pem_len) == 0); pthread_t tid; REQUIRE(pthread_create(&tid, NULL, srv_thread, &sc) == 0); diff --git a/tests/integration/tls_test_util.h b/tests/integration/tls_test_util.h index cd598ca..d6c3a9a 100644 --- a/tests/integration/tls_test_util.h +++ b/tests/integration/tls_test_util.h @@ -18,67 +18,172 @@ */ /* - * Shared integration-test helper: mint a self-signed RSA server identity - * (cert + key, PEM) with an iPAddress SAN for 127.0.0.1, used to stand up the - * in-tree test server behind TLS. EST mandates TLS (RFC 7030), so the EST - * integration tests run over HTTPS and pin this freshly-minted cert as their - * bootstrap trust anchor. + * Shared integration-test helper: mint self-signed identities (cert + key, + * PEM) with an iPAddress SAN for 127.0.0.1, used to stand up the in-tree test + * server behind TLS. EST mandates TLS (RFC 7030), so the EST integration tests + * run over HTTPS and pin a freshly-minted cert as their bootstrap trust + * anchor. The signing algorithm follows whatever the wolfSSL build provides + * (RSA when present, else ECC P-256), so the helpers work under reduced + * key-algorithm configurations. */ #ifndef WOLFCERT_TLS_TEST_UTIL_H #define WOLFCERT_TLS_TEST_UTIL_H +#include + +#include /* pid_t, referenced by wolfssl/wolfcrypt/random.h */ + #include #include #include #include #include +#include #include #include #include -/* Generate a self-signed RSA cert + key as PEM. Returns 0 on success; the +/* A key algorithm + parameter the current build supports, for client + * enrollments where the algorithm is incidental to what the test verifies. */ +#if defined(WOLFCERT_HAVE_ECC) + #define TEST_ENROLL_KEY_TYPE WOLFCERT_KEY_ECC + #define TEST_ENROLL_KEY_PARAM 256 +#elif defined(WOLFCERT_HAVE_RSA) + #define TEST_ENROLL_KEY_TYPE WOLFCERT_KEY_RSA + #define TEST_ENROLL_KEY_PARAM 2048 +#elif defined(WOLFCERT_HAVE_ED25519) + #define TEST_ENROLL_KEY_TYPE WOLFCERT_KEY_ED25519 + #define TEST_ENROLL_KEY_PARAM 0 +#elif defined(WOLFCERT_HAVE_ED448) + #define TEST_ENROLL_KEY_TYPE WOLFCERT_KEY_ED448 + #define TEST_ENROLL_KEY_PARAM 0 +#else + #error "tls_test_util: no supported enrollment key algorithm" +#endif + +/* The tests self-sign their identities with whatever signature-capable key + * algorithm the wolfSSL build provides: RSA when present, else ECC P-256. + * (A wolfCert build always has at least one of the two.) */ +#if !defined(NO_RSA) + typedef RsaKey test_signkey; + #define TEST_CERT_SIGTYPE CTC_SHA256wRSA + #define TEST_KEY_PEM_TYPE PRIVATEKEY_TYPE +#elif defined(HAVE_ECC) + typedef ecc_key test_signkey; + #define TEST_CERT_SIGTYPE CTC_SHA256wECDSA + #define TEST_KEY_PEM_TYPE ECC_PRIVATEKEY_TYPE +#else + #error "tls_test_util: tests need RSA or ECC for a signing identity" +#endif + +/* Init + generate a signing key. Returns 0 on success (free with + * test_signkey_free); non-zero on failure (nothing to free). */ +static inline int test_signkey_make(test_signkey* key, WC_RNG* rng) +{ +#if !defined(NO_RSA) + if (wc_InitRsaKey(key, NULL) != 0) + return -1; + if (wc_MakeRsaKey(key, 2048, WC_RSA_EXPONENT, rng) != 0) { + wc_FreeRsaKey(key); + return -1; + } +#else + if (wc_ecc_init(key) != 0) + return -1; + if (wc_ecc_make_key(rng, 32, key) != 0) { + wc_ecc_free(key); + return -1; + } +#endif + return 0; +} + +static inline void test_signkey_free(test_signkey* key) +{ +#if !defined(NO_RSA) + wc_FreeRsaKey(key); +#else + wc_ecc_free(key); +#endif +} + +/* Self-sign the (already populated) Cert into `der`. Returns the signed DER + * length, or <= 0 on error. */ +static inline int test_sign_selfcert(Cert* cert, uint8_t* der, int der_sz, + test_signkey* key, WC_RNG* rng) +{ +#if !defined(NO_RSA) + return wc_MakeSelfCert(cert, der, (word32)der_sz, key, rng); +#else + /* No ECC form of wc_MakeSelfCert; make the body self-issued and sign it. */ + cert->issuer = cert->subject; + if (wc_MakeCert(cert, der, (word32)der_sz, NULL, key, rng) <= 0) + return -1; + return wc_SignCert(cert->bodySz, cert->sigType, der, (word32)der_sz, + NULL, key, rng); +#endif +} + +/* Serialize the private key to DER. Returns DER length, or <= 0 on error. */ +static inline int test_signkey_to_der(test_signkey* key, uint8_t* der, + int der_sz) +{ +#if !defined(NO_RSA) + return wc_RsaKeyToDer(key, der, (word32)der_sz); +#else + return wc_EccKeyToDer(key, der, (word32)der_sz); +#endif +} + +/* Mint a self-signed cert + key (PEM) for common name `cn`. With is_ca == 0 + * the cert carries an iPAddress SAN for 127.0.0.1 (a usable TLS leaf); with + * is_ca != 0 it is marked CA and carries no SAN. Returns 0 on success; the * caller frees *cert_pem / *key_pem with free(). */ -static int gen_server_identity(uint8_t** cert_pem, size_t* cert_pem_len, +static inline int mint_self_id(const char* cn, int is_ca, + uint8_t** cert_pem, size_t* cert_pem_len, uint8_t** key_pem, size_t* key_pem_len) { - RsaKey key; + static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; + test_signkey key; WC_RNG rng; + Cert cert; + uint8_t cder[8192]; + uint8_t cpem[16384]; + uint8_t kder[8192]; + uint8_t kpem[16384]; + int cs, cp, ks, kp; + if (wc_InitRng(&rng) != 0) return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { + if (test_signkey_make(&key, &rng) != 0) { wc_FreeRng(&rng); return -1; } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - Cert cert; wc_InitCert(&cert); - strcpy(cert.subject.commonName, "127.0.0.1"); + strcpy(cert.subject.commonName, cn); cert.selfSigned = 1; - cert.sigType = CTC_SHA256wRSA; + cert.sigType = TEST_CERT_SIGTYPE; cert.daysValid = 1; - static const uint8_t san_seq[] = { 0x30, 0x06, 0x87, 0x04, 127, 0, 0, 1 }; - memcpy(cert.altNames, san_seq, sizeof(san_seq)); - cert.altNamesSz = (int)sizeof(san_seq); + cert.isCA = is_ca ? 1 : 0; + if (!is_ca) { + memcpy(cert.altNames, san_seq, sizeof(san_seq)); + cert.altNamesSz = (int)sizeof(san_seq); + } - uint8_t cder[8192]; - int cs = wc_MakeSelfCert(&cert, cder, sizeof(cder), &key, &rng); + cs = test_sign_selfcert(&cert, cder, (int)sizeof(cder), &key, &rng); if (cs <= 0) goto fail; - uint8_t cpem[16384]; - int cp = wc_DerToPem(cder, cs, cpem, sizeof(cpem), CERT_TYPE); + cp = wc_DerToPem(cder, (word32)cs, cpem, sizeof(cpem), CERT_TYPE); if (cp <= 0) goto fail; - uint8_t kder[8192]; - int ks = wc_RsaKeyToDer(&key, kder, sizeof(kder)); + ks = test_signkey_to_der(&key, kder, (int)sizeof(kder)); if (ks <= 0) goto fail; - uint8_t kpem[16384]; - int kp = wc_DerToPem(kder, ks, kpem, sizeof(kpem), PRIVATEKEY_TYPE); + kp = wc_DerToPem(kder, (word32)ks, kpem, sizeof(kpem), TEST_KEY_PEM_TYPE); if (kp <= 0) goto fail; @@ -88,13 +193,21 @@ static int gen_server_identity(uint8_t** cert_pem, size_t* cert_pem_len, *key_pem = (uint8_t*)malloc((size_t)kp); memcpy(*key_pem, kpem, (size_t)kp); *key_pem_len = (size_t)kp; - wc_FreeRsaKey(&key); + test_signkey_free(&key); wc_FreeRng(&rng); return 0; fail: - wc_FreeRsaKey(&key); + test_signkey_free(&key); wc_FreeRng(&rng); return -1; } +/* Self-signed TLS server identity (cert + key, PEM) for 127.0.0.1. */ +static inline int gen_server_identity(uint8_t** cert_pem, size_t* cert_pem_len, + uint8_t** key_pem, size_t* key_pem_len) +{ + return mint_self_id("127.0.0.1", 0, cert_pem, cert_pem_len, + key_pem, key_pem_len); +} + #endif /* WOLFCERT_TLS_TEST_UTIL_H */ diff --git a/tests/test_static_mem.h b/tests/test_static_mem.h new file mode 100644 index 0000000..c03c916 --- /dev/null +++ b/tests/test_static_mem.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCert. + * + * wolfCert 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. + * + * wolfCert 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 wolfCert. If not, see . + */ + +/* + * Test helper for WOLFSSL_NO_MALLOC builds. With no dynamic allocator, wolfCert + * can only allocate from a wolfSSL static-memory pool, so a test loads one and + * registers it as wolfCert's default heap; every wolfcert_* call then draws + * from it. On any other build test_static_mem_init() is a no-op. Call it once + * from a test's main() right after wolfcert_init(). + */ + +#ifndef WOLFCERT_TEST_STATIC_MEM_H +#define WOLFCERT_TEST_STATIC_MEM_H + +#include + +#include + +#if defined(WOLFSSL_STATIC_MEMORY) && defined(WOLFSSL_NO_MALLOC) + +#include + +/* Sized generously for the unit tests' peak concurrent use (RSA/ECC/ML-DSA + * keygen, CSR + PKCS7 buffers, and a loopback TLS handshake). wolfSSL's + * default, feature-aware bucket distribution partitions this buffer; only the + * total size is tuned here, not the bucket layout. */ +static unsigned char g_test_static_pool[4 * 1024 * 1024]; +static WOLFSSL_HEAP_HINT* g_test_heap_hint = NULL; + +static inline int test_static_mem_init(void) +{ + if (wc_LoadStaticMemory(&g_test_heap_hint, g_test_static_pool, + sizeof(g_test_static_pool), WOLFMEM_GENERAL, 1) != 0) + return -1; + /* Register the pool as wolfSSL's global heap so wolfSSL_Init() (invoked by + * wolfcert_init) and any NULL-heap allocation draw from it. Must run before + * wolfcert_init. */ + wolfSSL_SetGlobalHeapHint(g_test_heap_hint); + wolfcert_set_default_heap(g_test_heap_hint); + return 0; +} + +#else + +static inline int test_static_mem_init(void) { return 0; } + +#endif /* WOLFSSL_STATIC_MEMORY && WOLFSSL_NO_MALLOC */ + +#endif /* WOLFCERT_TEST_STATIC_MEM_H */ diff --git a/tests/unit/test_csr.c b/tests/unit/test_csr.c index 6accecd..fc8cb55 100644 --- a/tests/unit/test_csr.c +++ b/tests/unit/test_csr.c @@ -21,6 +21,7 @@ #define _DARWIN_C_SOURCE /* expose memmem/strcasestr/INADDR_LOOPBACK on macOS */ #include +#include "../test_static_mem.h" #include #include @@ -132,11 +133,16 @@ static int build_with_extras(void) int main(void) { + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); +#ifdef WOLFCERT_HAVE_ECC if (build_and_reparse(WOLFCERT_KEY_ECC, 256)) return 1; +#endif +#ifdef WOLFCERT_HAVE_RSA if (build_and_reparse(WOLFCERT_KEY_RSA, 2048)) return 1; +#endif #ifdef WOLFCERT_HAVE_ED25519 if (build_and_reparse(WOLFCERT_KEY_ED25519, 0)) return 1; @@ -145,8 +151,10 @@ int main(void) if (build_and_reparse(WOLFCERT_KEY_ED448, 0)) return 1; #endif +#ifdef WOLFCERT_HAVE_ECC if (build_with_extras()) return 1; +#endif wolfcert_cleanup(); printf("OK\n"); return 0; diff --git a/tests/unit/test_csr_attrs.c b/tests/unit/test_csr_attrs.c index 2ae7a08..62b194c 100644 --- a/tests/unit/test_csr_attrs.c +++ b/tests/unit/test_csr_attrs.c @@ -31,6 +31,7 @@ #define _POSIX_C_SOURCE 200809L #include +#include "../test_static_mem.h" #include #include @@ -72,6 +73,7 @@ static const uint8_t OID_SECP384R1[] = { 0x2B, 0x81, 0x04, 0x00, 0x22 }; int main(void) { + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); /* ---- Empty input -> OK, no items, all hint fields zero. */ diff --git a/tests/unit/test_est.c b/tests/unit/test_est.c index 496f1b7..b5b26b9 100644 --- a/tests/unit/test_est.c +++ b/tests/unit/test_est.c @@ -28,6 +28,7 @@ #define _GNU_SOURCE #include +#include "../test_static_mem.h" #include "internal.h" #include @@ -58,34 +59,34 @@ static int make_test_ca(uint8_t* out, size_t cap, size_t* out_len) { - RsaKey key; + test_signkey key; WC_RNG rng; + Cert cert; + int sz; + if (wc_InitRng(&rng) != 0) return -1; - if (wc_InitRsaKey(&key, NULL) != 0) { + if (test_signkey_make(&key, &rng) != 0) { wc_FreeRng(&rng); return -1; } - if (wc_MakeRsaKey(&key, 2048, WC_RSA_EXPONENT, &rng) != 0) - goto fail; - Cert cert; wc_InitCert(&cert); strcpy(cert.subject.commonName, "wolfCert Test CA"); strcpy(cert.subject.org, "wolfCert"); strcpy(cert.subject.country, "US"); cert.isCA = 1; - cert.sigType = CTC_SHA256wRSA; + cert.sigType = TEST_CERT_SIGTYPE; cert.selfSigned = 1; - int sz = wc_MakeSelfCert(&cert, out, (word32)cap, &key, &rng); + sz = test_sign_selfcert(&cert, out, (int)cap, &key, &rng); if (sz <= 0) goto fail; *out_len = (size_t)sz; - wc_FreeRsaKey(&key); + test_signkey_free(&key); wc_FreeRng(&rng); return 0; fail: - wc_FreeRsaKey(&key); + test_signkey_free(&key); wc_FreeRng(&rng); return -1; } @@ -234,7 +235,7 @@ static int test_est_require_server_auth(void) }; WolfCertBuffer out = { 0 }; WolfCertEstSession* sess = NULL; - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* rk = NULL; @@ -288,6 +289,7 @@ int main(void) * the resulting SIGPIPE. */ signal(SIGPIPE, SIG_IGN); + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_oid_to_dotted()) @@ -343,7 +345,7 @@ int main(void) REQUIRE(memmem(ca_pem.data, ca_pem.len, "BEGIN CERTIFICATE", 17) != NULL); wolfcert_buffer_free(&ca_pem); - WolfCertKeyCfg kcfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg kcfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* dk = NULL; REQUIRE(wolfcert_key_generate(&kcfg, &dk) == WOLFCERT_OK); diff --git a/tests/unit/test_keygen.c b/tests/unit/test_keygen.c index 3afae36..beb3d80 100644 --- a/tests/unit/test_keygen.c +++ b/tests/unit/test_keygen.c @@ -18,6 +18,9 @@ */ #include +#include "../test_static_mem.h" + +#include /* WOLFSSL_NO_ML_DSA_{44,65,87} for per-level gating */ #include #include @@ -68,13 +71,18 @@ static int roundtrip(WolfCertKeyType type, int param) int main(void) { + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); +#ifdef WOLFCERT_HAVE_ECC if (roundtrip(WOLFCERT_KEY_ECC, 256)) return 1; if (roundtrip(WOLFCERT_KEY_ECC, 384)) return 1; +#endif +#ifdef WOLFCERT_HAVE_RSA if (roundtrip(WOLFCERT_KEY_RSA, 2048)) return 1; +#endif #ifdef WOLFCERT_HAVE_ED25519 if (roundtrip(WOLFCERT_KEY_ED25519, 0)) return 1; @@ -84,12 +92,20 @@ int main(void) return 1; #endif #ifdef WOLFCERT_HAVE_MLDSA + /* Each ML-DSA level can be disabled independently in wolfSSL + * (WOLFSSL_NO_ML_DSA_{44,65,87}); only exercise the ones present. */ +#ifndef WOLFSSL_NO_ML_DSA_44 if (roundtrip(WOLFCERT_KEY_MLDSA44, 0)) return 1; +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 if (roundtrip(WOLFCERT_KEY_MLDSA65, 0)) return 1; +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 if (roundtrip(WOLFCERT_KEY_MLDSA87, 0)) return 1; +#endif #else /* Runtime rejection when the wolfSSL build lacks Dilithium. */ { diff --git a/tests/unit/test_parse_negative.c b/tests/unit/test_parse_negative.c index e89d5ca..c0bf66e 100644 --- a/tests/unit/test_parse_negative.c +++ b/tests/unit/test_parse_negative.c @@ -28,6 +28,7 @@ #define _DEFAULT_SOURCE #include +#include "../test_static_mem.h" #include "internal.h" #include @@ -213,6 +214,7 @@ static int test_csr_pem(void) int main(void) { + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_url()) return 1; diff --git a/tests/unit/test_scep_msg.c b/tests/unit/test_scep_msg.c index e856d6c..88f3a8c 100644 --- a/tests/unit/test_scep_msg.c +++ b/tests/unit/test_scep_msg.c @@ -34,6 +34,7 @@ #define _GNU_SOURCE #include +#include "../test_static_mem.h" #include "internal.h" #include @@ -719,6 +720,7 @@ static int test_signer_subject_fallback(void) int main(void) { + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_non_success_has_no_envelope()) return 1; diff --git a/tests/unit/test_smoke.c b/tests/unit/test_smoke.c index b7a7220..d1eff5a 100644 --- a/tests/unit/test_smoke.c +++ b/tests/unit/test_smoke.c @@ -18,12 +18,17 @@ */ #include +#include "../test_static_mem.h" #include #include int main(void) { + if (test_static_mem_init() != 0) { + fprintf(stderr, "static mem init failed\n"); + return 1; + } if (wolfcert_init(NULL) != WOLFCERT_OK) { fprintf(stderr, "wolfcert_init failed\n"); return 1; diff --git a/tests/unit/test_store.c b/tests/unit/test_store.c index c5be937..9003a5c 100644 --- a/tests/unit/test_store.c +++ b/tests/unit/test_store.c @@ -22,6 +22,9 @@ #define _DARWIN_C_SOURCE /* expose memmem/strcasestr/INADDR_LOOPBACK on macOS */ #include +#include "../test_static_mem.h" + +#include "../integration/tls_test_util.h" #include #include @@ -45,7 +48,7 @@ static int test_posix(void) WolfCertStoreOps* store = wolfcert_store_posix_open(dir, NULL); REQUIRE(store != NULL); - WolfCertKeyCfg cfg = { .type = WOLFCERT_KEY_ECC, .param = 256, + WolfCertKeyCfg cfg = { .type = TEST_ENROLL_KEY_TYPE, .param = TEST_ENROLL_KEY_PARAM, .dev_id = WOLFCERT_DEVID_SOFTWARE }; WolfCertKey* k = NULL; REQUIRE(wolfcert_key_generate(&cfg, &k) == WOLFCERT_OK); @@ -102,6 +105,7 @@ static int test_memory(void) int main(void) { + REQUIRE(test_static_mem_init() == 0); REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); if (test_posix()) return 1;