diff --git a/.github/actions/setup-go-wolfssl/action.yml b/.github/actions/setup-go-wolfssl/action.yml new file mode 100644 index 0000000..7bcc53b --- /dev/null +++ b/.github/actions/setup-go-wolfssl/action.yml @@ -0,0 +1,31 @@ +name: Setup Go for go-wolfssl +description: >- + Select a Go toolchain baked into the go-wolfssl-ci image, keep it from + switching toolchains, and add the one module dependency (golang.org/x/term, + used by the aes-encrypt example) at a version that still supports the + oldest Go in CI. The image's module cache already holds it. + +inputs: + go-version: + description: A Go minor baked into the image (1.21, 1.26), or stable or oldstable. + required: false + default: stable + +runs: + using: composite + steps: + - name: Select Go ${{ inputs.go-version }} + shell: bash + env: + GO_VERSION: ${{ inputs.go-version }} + run: | + set -euo pipefail + goroot="/opt/go/$GO_VERSION" + if [ ! -x "$goroot/bin/go" ]; then + echo "::error::Go $GO_VERSION is not in the CI image; have: $(ls /opt/go | tr '\n' ' ')" + exit 1 + fi + echo "$goroot/bin" >> "$GITHUB_PATH" + echo "GOTOOLCHAIN=local" >> "$GITHUB_ENV" + "$goroot/bin/go" get golang.org/x/term@v0.25.0 + "$goroot/bin/go" version diff --git a/.github/actions/setup-wolfssl/action.yml b/.github/actions/setup-wolfssl/action.yml new file mode 100644 index 0000000..9a95af6 --- /dev/null +++ b/.github/actions/setup-wolfssl/action.yml @@ -0,0 +1,140 @@ +name: Setup wolfSSL +description: >- + Build (or restore from cache) wolfSSL and install it under a prefix. A git + ref is resolved to its commit so the cache tracks the upstream tree, and + every build-affecting input is folded into the cache key. With + fips-ready-version set, the public FIPS Ready bundle is built instead. + +inputs: + ref: + description: wolfSSL git ref (branch, tag or commit). Ignored for FIPS Ready. + required: false + default: master + configure-flags: + description: wolfSSL ./configure flags. + required: false + default: "" + cflags: + description: Extra CFLAGS, passed only when non-empty so wolfSSL keeps its default optimization. + required: false + default: "" + ldflags: + description: Extra LDFLAGS, passed only when non-empty. + required: false + default: "" + cc: + description: C compiler to build wolfSSL with. + required: false + default: gcc + prefix: + description: Install prefix. + required: true + fips-ready-version: + description: Build the wolfSSL FIPS Ready bundle of this version instead of a git ref. + required: false + default: "" + fips-ready-sha256: + description: Expected sha256 of the FIPS Ready zip. + required: false + default: "" + +outputs: + sha: + description: The wolfSSL commit built, or fips-ready-. + value: ${{ steps.key.outputs.sha }} + +runs: + using: composite + steps: + - name: Resolve wolfSSL and compute the cache key + id: key + shell: bash + env: + REF: ${{ inputs.ref }} + FLAGS: ${{ inputs.configure-flags }} + XCFLAGS: ${{ inputs.cflags }} + XLDFLAGS: ${{ inputs.ldflags }} + XCC: ${{ inputs.cc }} + PREFIX: ${{ inputs.prefix }} + FIPS_READY: ${{ inputs.fips-ready-version }} + FIPS_READY_SHA256: ${{ inputs.fips-ready-sha256 }} + run: | + set -euo pipefail + if [ -n "$FIPS_READY" ]; then + if [ -z "$FIPS_READY_SHA256" ]; then + echo "::error::fips-ready-sha256 is required with fips-ready-version" + exit 1 + fi + SHA="fips-ready-$FIPS_READY-${FIPS_READY_SHA256:0:16}" + else + URL=https://github.com/wolfSSL/wolfssl.git + # Peel annotated tags to their commit, then fall back to a branch, + # then treat the ref as a raw commit. + SHA=$(git ls-remote "$URL" "$REF^{}" | awk 'NR==1{print $1}') + [ -n "$SHA" ] || SHA=$(git ls-remote "$URL" "$REF" | awk 'NR==1{print $1}') + [ -n "$SHA" ] || SHA=$REF + fi + CFGHASH=$(printf '%s\037%s\037%s\037%s\037%s' "$FLAGS" "$XCFLAGS" "$XLDFLAGS" "$XCC" "$PREFIX" \ + | sha256sum | cut -c1-16) + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + echo "cache-key=go-wolfssl-ci-v1-wolfssl-${RUNNER_OS}-${SHA}-${CFGHASH}" >> "$GITHUB_OUTPUT" + echo "wolfSSL ${FIPS_READY:+FIPS Ready }${FIPS_READY:-$REF} -> $SHA (cfg $CFGHASH)" + + - name: Restore the wolfSSL install + id: cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ${{ inputs.prefix }} + key: ${{ steps.key.outputs.cache-key }} + + - name: Fetch wolfSSL + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + SHA: ${{ steps.key.outputs.sha }} + FIPS_READY: ${{ inputs.fips-ready-version }} + FIPS_READY_SHA256: ${{ inputs.fips-ready-sha256 }} + run: | + set -euo pipefail + rm -rf "$RUNNER_TEMP/wolfssl-src" + if [ -n "$FIPS_READY" ]; then + curl -fsSL -o "$RUNNER_TEMP/fips-ready.zip" \ + "https://www.wolfssl.com/wolfssl-$FIPS_READY-gplv3-fips-ready.zip" + echo "$FIPS_READY_SHA256 $RUNNER_TEMP/fips-ready.zip" | sha256sum -c - + unzip -q "$RUNNER_TEMP/fips-ready.zip" -d "$RUNNER_TEMP/fips-ready" + mv "$RUNNER_TEMP/fips-ready/wolfssl-$FIPS_READY-gplv3-fips-ready" "$RUNNER_TEMP/wolfssl-src" + else + git init -q "$RUNNER_TEMP/wolfssl-src" + cd "$RUNNER_TEMP/wolfssl-src" + git fetch -q --depth 1 https://github.com/wolfSSL/wolfssl.git "$SHA" + git checkout -q --detach FETCH_HEAD + ./autogen.sh + fi + + - name: Build and install wolfSSL + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + FLAGS: ${{ inputs.configure-flags }} + XCFLAGS: ${{ inputs.cflags }} + XLDFLAGS: ${{ inputs.ldflags }} + XCC: ${{ inputs.cc }} + PREFIX: ${{ inputs.prefix }} + FIPS_READY: ${{ inputs.fips-ready-version }} + run: | + set -euo pipefail + cd "$RUNNER_TEMP/wolfssl-src" + args=(--prefix="$PREFIX") + # Flags come from .github/scripts/run-config.sh and may carry + # CPPFLAGS=..., so split them the way a shell command line would. + eval "args+=($FLAGS)" + [ -z "$XCFLAGS" ] || args+=("CFLAGS=$XCFLAGS") + [ -z "$XLDFLAGS" ] || args+=("LDFLAGS=$XLDFLAGS") + CC="$XCC" ./configure "${args[@]}" + make -j"$(nproc)" + if [ -n "$FIPS_READY" ]; then + # FIPS Ready checks its in-core hash, which changes with every build. + ./fips-hash.sh + make -j"$(nproc)" + fi + make install diff --git a/.github/docker/go-wolfssl-ci/Dockerfile b/.github/docker/go-wolfssl-ci/Dockerfile new file mode 100644 index 0000000..2e9c872 --- /dev/null +++ b/.github/docker/go-wolfssl-ci/Dockerfile @@ -0,0 +1,42 @@ +# Shared dependencies for go-wolfssl Linux CI. Rebuild through publish-ci-image.yml. +FROM ubuntu:24.04 + +LABEL org.opencontainers.image.source="https://github.com/wolfSSL/go-wolfssl" \ + org.opencontainers.image.description="go-wolfssl Linux CI toolchains and test dependencies" \ + org.opencontainers.image.licenses="GPL-3.0-or-later" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ENV DEBIAN_FRONTEND=noninteractive \ + GOPATH=/opt/gopath \ + GOTOOLCHAIN=local \ + GOTELEMETRY=off \ + PATH=/opt/go-tools/bin:/opt/go/stable/bin:$PATH + +# Package installation belongs here, rather than in individual CI jobs. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential gcc-12 gcc-13 gcc-14 \ + clang clang-16 clang-17 clang-18 libclang-rt-18-dev llvm \ + autoconf automake libtool libtool-bin pkg-config m4 \ + git curl ca-certificates jq openssl iproute2 procps \ + nodejs python3 python3-yaml shellcheck \ + zip unzip xz-utils zstd file \ + && rm -rf /var/lib/apt/lists/* + +# Go 1.21 (the CI floor), the analysis toolchain, and the stable and oldstable +# releases, resolved at image build time rather than downloaded in every job. +ARG ANALYSIS_GO=1.26 +COPY .github/docker/go-wolfssl-ci/install-go.sh /tmp/install-go.sh +RUN /tmp/install-go.sh 1.21 "$ANALYSIS_GO" && rm /tmp/install-go.sh + +# staticcheck v0.8.1 reads export data only up to Go 1.26; bump both together. +RUN GOBIN=/opt/go-tools/bin "/opt/go/$ANALYSIS_GO/bin/go" install honnef.co/go/tools/cmd/staticcheck@v0.8.1 \ + && GOBIN=/opt/go-tools/bin "/opt/go/$ANALYSIS_GO/bin/go" install golang.org/x/vuln/cmd/govulncheck@v1.8.0 \ + && GOBIN=/opt/go-tools/bin go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 \ + && prefetch=$(mktemp -d) && cd "$prefetch" \ + && go mod init prefetch && go get golang.org/x/term@v0.25.0 \ + && cd / && rm -rf "$prefetch" "$(go env GOCACHE)" + +COPY .github/docker/go-wolfssl-ci/smoke-test.sh /usr/local/bin/go-wolfssl-ci-smoke +RUN chmod +x /usr/local/bin/go-wolfssl-ci-smoke && go-wolfssl-ci-smoke diff --git a/.github/docker/go-wolfssl-ci/Dockerfile.dockerignore b/.github/docker/go-wolfssl-ci/Dockerfile.dockerignore new file mode 100644 index 0000000..c3a9d41 --- /dev/null +++ b/.github/docker/go-wolfssl-ci/Dockerfile.dockerignore @@ -0,0 +1,6 @@ +# Only send the image assets to Docker. +** +!.github/ +!.github/docker/ +!.github/docker/go-wolfssl-ci/ +!.github/docker/go-wolfssl-ci/** diff --git a/.github/docker/go-wolfssl-ci/README.md b/.github/docker/go-wolfssl-ci/README.md new file mode 100644 index 0000000..705fea9 --- /dev/null +++ b/.github/docker/go-wolfssl-ci/README.md @@ -0,0 +1,48 @@ +# go-wolfssl CI image + +Linux workflows pin `ghcr.io/wolfssl/go-wolfssl-ci` by its immutable image +digest. The image contains gcc-12 to 14, clang-16 to 18 with the sanitizer +runtimes, the autotools wolfSSL needs, OpenSSL, Go 1.21, the analysis Go +(1.26), the stable and oldstable Go releases, staticcheck, govulncheck, +actionlint, and shellcheck. Each Go toolchain lives under `/opt/go/`, +with `/opt/go/stable` and `/opt/go/oldstable` links, and +`.github/actions/setup-go-wolfssl` selects one by name. + +From the repository root: + +```sh +docker build --platform linux/amd64 \ + -f .github/docker/go-wolfssl-ci/Dockerfile -t go-wolfssl-ci . +docker run --rm --platform linux/amd64 go-wolfssl-ci go-wolfssl-ci-smoke +``` + +The Dockerfile-specific ignore file restricts the build context to the image +assets. Add shared dependencies here instead of installing them in workflow +jobs. Go patch releases and the stable and oldstable minors are resolved when +the image is rebuilt, and each archive is checked against go.dev's sha256. + +In `wolfSSL/go-wolfssl`, `publish-ci-image.yml` runs on relevant pushes to +`master` and manual dispatches targeting `master`. It builds and smoke-tests +the image, then publishes `latest` and `sha-` tags. Publication is +serialized and uses the repository's `GITHUB_TOKEN`. The GHCR package must be +public so fork PRs can pull anonymously; the publisher checks this. + +After publishing, a container job uses the build's output digest to repeat the +smoke checks in the GitHub runner environment. + +Consumers should never fall back to building the image or installing packages. + +wolfSSL caches use the `go-wolfssl-ci-v1` prefix to separate them from +previous host builds. Bump this prefix if an image change makes cached builds +incompatible. + +Container jobs trust the mounted GitHub workspace after checkout so Git, and +Go's VCS stamping, can read a repository owned by the host runner. Their +explicit Bash command keeps the previous host shell's error behavior. The +sanitizer jobs run privileged so they can lower `vm.mmap_rnd_bits`. + +To adopt a rebuilt image, inspect its `sha-` tag with +`docker buildx imagetools inspect` and copy the reported image digest into the +consumer workflows in a reviewed change. The publisher's smoke job tests the +new digest immediately, while consumer jobs keep using their reviewed digest. +To roll back, restore the previous digest in those workflows. diff --git a/.github/docker/go-wolfssl-ci/install-go.sh b/.github/docker/go-wolfssl-ci/install-go.sh new file mode 100755 index 0000000..c00be12 --- /dev/null +++ b/.github/docker/go-wolfssl-ci/install-go.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Install the newest patch of each requested Go minor, plus the current stable +# and oldstable minors, under /opt/go/, checked against go.dev's sha256. +set -euo pipefail + +releases=$(mktemp) +trap 'rm -f "$releases"' EXIT +curl --fail --silent --show-error --location \ + 'https://go.dev/dl/?mode=json&include=all' -o "$releases" + +mapfile -t latest < <(jq -r '[.[] | select(.stable) | .version + | capture("^go(?[0-9]+\\.[0-9]+)").m] | unique + | sort_by(split(".") | map(tonumber)) | reverse | .[0:2][]' "$releases") +test "${#latest[@]}" -eq 2 + +mkdir -p /opt/go +for minor in $(printf '%s\n' "$@" "${latest[@]}" | sort -u); do + read -r archive sha < <(jq -r --arg re "^go${minor//./\\.}\\.[0-9]+$" ' + [.[] | select(.stable and (.version | test($re)))] | first | .files[] + | select(.os == "linux" and .arch == "amd64" and .kind == "archive") + | [.filename, .sha256] | @tsv' "$releases") + test -n "$archive" && test -n "$sha" + curl --fail --silent --show-error --location \ + "https://go.dev/dl/$archive" -o /tmp/go.tar.gz + echo "$sha /tmp/go.tar.gz" | sha256sum --check - + mkdir "/opt/go/$minor" + tar -xzf /tmp/go.tar.gz -C "/opt/go/$minor" --strip-components=1 + rm /tmp/go.tar.gz +done +ln -s "${latest[0]}" /opt/go/stable +ln -s "${latest[1]}" /opt/go/oldstable diff --git a/.github/docker/go-wolfssl-ci/smoke-test.sh b/.github/docker/go-wolfssl-ci/smoke-test.sh new file mode 100755 index 0000000..00a309c --- /dev/null +++ b/.github/docker/go-wolfssl-ci/smoke-test.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Exercise the installed toolchains before an image is published. +set -euo pipefail + +smoke_dir=$(mktemp -d) +trap 'rm -rf "$smoke_dir"' EXIT +cat > "$smoke_dir/main.c" <<'EOF' +int main(void) +{ + return 0; +} +EOF + +for compiler in gcc gcc-12 gcc-13 gcc-14 clang clang-16 clang-17 clang-18; do + "$compiler" --version | head -n 1 + "$compiler" -Wall -Wextra -Werror "$smoke_dir/main.c" -o "$smoke_dir/compiler-test" + "$smoke_dir/compiler-test" +done +for sanitizer in address undefined memory; do + clang -fsanitize="$sanitizer" "$smoke_dir/main.c" -o "$smoke_dir/sanitizer-test" +done + +mkdir "$smoke_dir/cgo" +cat > "$smoke_dir/cgo/main.go" <<'EOF' +package main + +// int answer(void) { return 42; } +import "C" + +func main() { + if C.answer() != 42 { + panic("cgo") + } +} +EOF +printf 'module smoke\n\ngo 1.21\n' > "$smoke_dir/cgo/go.mod" +for goroot in /opt/go/1.21 /opt/go/oldstable /opt/go/stable; do + "$goroot/bin/go" version + (cd "$smoke_dir/cgo" && "$goroot/bin/go" run .) +done +for tool in autoconf automake libtoolize make pkg-config git curl jq openssl \ + ss sysctl node python3 shellcheck zip unzip zstd llvm-symbolizer; do + command -v "$tool" +done +python3 -c 'import yaml' +staticcheck -version +govulncheck -version +actionlint -version +test -d "$GOPATH/pkg/mod/golang.org/x/term@v0.25.0" diff --git a/.github/scripts/analyze.sh b/.github/scripts/analyze.sh new file mode 100755 index 0000000..d0d6557 --- /dev/null +++ b/.github/scripts/analyze.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Usage: analyze.sh +# Runs go vet (every analyzer), staticcheck and govulncheck over the +# go-wolfssl packages and each example program, against the wolfSSL +# installed at . staticcheck and govulncheck must be on PATH. + +set -euo pipefail + +PKGS=". ./handles ./sha256 ./wolftls ./wolfx509" + +./generateOptions.sh "$1" +export LD_LIBRARY_PATH="$1/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +# shellcheck disable=SC2086 +go vet $PKGS +# Example directories hold several main programs, so vet each file alone, +# then vet directories with tests as packages so the tests are covered too. +grep -rl --include='*.go' --exclude='*_test.go' '^package main' examples | while read -r f; do + echo "go vet $f" + go vet "$f" +done +find examples -name '*_test.go' -exec dirname {} \; | sort -u | while read -r d; do + echo "go vet ./$d" + go vet "./$d" +done + +# shellcheck disable=SC2086 +staticcheck $PKGS +# Examples get the correctness checks; the ST* style checks are for the library. +grep -rl --include='*.go' --exclude='*_test.go' '^package main' examples | while read -r f; do + echo "staticcheck $f" + staticcheck -checks 'inherit,-ST*' "$f" +done + +# shellcheck disable=SC2086 +govulncheck $PKGS +# Examples cannot be loaded as packages, so scan each built program. +bin=$(mktemp -d) +trap 'rm -rf "$bin"' EXIT +grep -rl --include='*.go' --exclude='*_test.go' '^package main' examples | while read -r f; do + out="$bin/$(echo "$f" | tr '/.' '__')" + go build -o "$out" "$f" + echo "govulncheck -mode=binary $f" + govulncheck -mode=binary "$out" +done diff --git a/.github/scripts/openssl-interop.sh b/.github/scripts/openssl-interop.sh new file mode 100755 index 0000000..6ab7b30 --- /dev/null +++ b/.github/scripts/openssl-interop.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# Usage: openssl-interop.sh +# Runs the example TLS client against openssl s_server and the example TLS +# server against openssl s_client, and checks the exact messages each side +# receives. Run from the repository root. + +set -euo pipefail + +PORT=11111 +WORK=$(mktemp -d) +trap 'kill $(jobs -p) 2>/dev/null || true; rm -rf "$WORK"' EXIT + +# The examples load ../certs, so run them from a scratch tree whose certs are +# minted here: checked-in fixtures expire, a fresh CA does not. +mkdir -p "$WORK/client" "$WORK/server" "$WORK/certs" +CERTS=$WORK/certs +openssl req -x509 -newkey rsa:2048 -nodes -days 2 -subj "/CN=interop CA" \ + -keyout "$CERTS/ca-key.pem" -out "$CERTS/ca-cert.pem" 2>/dev/null +openssl req -newkey rsa:2048 -nodes -subj "/CN=example.com" \ + -keyout "$CERTS/server-key.pem" -out "$CERTS/server.csr" 2>/dev/null +openssl x509 -req -in "$CERTS/server.csr" -CA "$CERTS/ca-cert.pem" -CAkey "$CERTS/ca-key.pem" \ + -CAcreateserial -days 2 -out "$CERTS/server-cert.pem" \ + -extfile <(printf 'subjectAltName=DNS:example.com,DNS:localhost,IP:127.0.0.1\n') 2>/dev/null + +./generateOptions.sh "$1" +export LD_LIBRARY_PATH="$1/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +(cd examples/client && go build -o "$WORK/client/client" client.go) +(cd examples/server && go build -o "$WORK/server/server" server.go) + +# Checks for a listening socket without connecting: a probe connection would +# use up a server that accepts only one client. +wait_for_port() { + local _ + for _ in $(seq 50); do + if [ -n "$(ss -Hltn "sport = :$PORT")" ]; then + return 0 + fi + sleep 0.2 + done + echo "::error::nothing listening on port $PORT" + return 1 +} + +expect() { + if ! grep -qF "$2" "$1"; then + echo "::error::$3" + echo "--- $1" + cat "$1" + exit 1 + fi +} + +# Fails unless the named process exited 0, showing its output. +require_ok() { + if [ "$2" -ne 0 ]; then + echo "::error::$1 exited with status $2" + cat "$3" + exit 1 + fi +} + +echo "== example client -> openssl s_server" +(sleep 3; echo "I hear ya fashizzle!") | openssl s_server -accept "$PORT" -naccept 1 -quiet \ + -cert "$CERTS/server-cert.pem" -key "$CERTS/server-key.pem" > "$WORK/s_server.out" 2>&1 & +srv=$! +wait_for_port +st=0 +(cd "$WORK/client" && timeout 20 ./client) > "$WORK/client.out" 2>&1 || st=$? +sst=0 +wait "$srv" || sst=$? +expect "$WORK/client.out" "Server says : I hear ya fashizzle!" "client did not receive the server reply" +expect "$WORK/s_server.out" "Can you hear me?" "openssl s_server did not receive the client message" +require_ok "example client" "$st" "$WORK/client.out" +require_ok "openssl s_server" "$sst" "$WORK/s_server.out" + +echo "== openssl s_client -> example server" +(cd "$WORK/server" && timeout 20 ./server) > "$WORK/server.out" 2>&1 & +srv=$! +wait_for_port +st=0 +(echo "Can you hear me?"; sleep 3) | timeout 20 openssl s_client -connect "127.0.0.1:$PORT" \ + -CAfile "$CERTS/ca-cert.pem" -verify_return_error -tls1_2 -quiet > "$WORK/s_client.out" 2>&1 || st=$? +sst=0 +wait "$srv" || sst=$? +expect "$WORK/server.out" "Client says : Can you hear me?" "server did not receive the client message" +expect "$WORK/s_client.out" "I hear ya fashizzle!" "openssl s_client did not receive the server reply" +require_ok "openssl s_client" "$st" "$WORK/s_client.out" +require_ok "example server" "$sst" "$WORK/server.out" + +echo "OpenSSL interop passed" diff --git a/.github/scripts/run-config.sh b/.github/scripts/run-config.sh new file mode 100755 index 0000000..475af35 --- /dev/null +++ b/.github/scripts/run-config.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# Usage: +# run-config.sh flags print the wolfSSL ./configure flags +# run-config.sh test build and test go-wolfssl against +# the wolfSSL installed at +# +# Each config states which wolfSSL defines it must (or must not) have, which +# packages are tested, which are only built and linked, and which must fail +# to build with a given message because the build lacks a required feature. + +set -euo pipefail + +ALL="--enable-all --enable-opensslall --enable-opensslextra" +PKGS_ALL=". ./handles ./sha256 ./wolftls ./wolfx509" +X509_MISSING="wolfx509 requires wolfSSL built with" +# Tests on master that assume OPENSSL_EXTRA, ALPN or peer-cert support instead +# of skipping without it. Drop names from this list as they learn to skip. +FEATURE_TESTS="^(TestVerifyConnectionCallback|TestPeerCertificatesPopulated" +FEATURE_TESTS="$FEATURE_TESTS|TestVerifyPeerCertificateCallback|TestMinMaxVersion" +FEATURE_TESTS="$FEATURE_TESTS|TestGetCertificate|TestPeerCertsSurviveClose" +FEATURE_TESTS="$FEATURE_TESTS|TestALPN|TestUnknownClientAuthFailsClosed)$" + +config() { + FLAGS="" HAVE="" LACK="" TEST="" BUILD="" FAIL="" FAIL_MSG="" RACE=0 SKIP="" + STRICT=0 TLS10=0 + EXAMPLES=1 + case "$1" in + all) + STRICT=1 + FLAGS="$ALL" + HAVE="OPENSSL_ALL SESSION_CERTS KEEP_PEER_CERT HAVE_SNI WOLFSSL_CERT_SETUP_CB" + TEST="$PKGS_ALL" RACE=1 ;; + default) + LACK="OPENSSL_EXTRA SESSION_CERTS WOLFSSL_CERT_GEN" + TEST=". ./handles ./sha256 ./wolftls" SKIP="$FEATURE_TESTS" + FAIL="./wolfx509" FAIL_MSG="$X509_MISSING" ;; + opensslextra) + FLAGS="--enable-opensslextra" + HAVE="OPENSSL_EXTRA" LACK="OPENSSL_ALL WOLFSSL_CERT_GEN" + TEST=". ./handles ./sha256 ./wolftls" SKIP="$FEATURE_TESTS" + FAIL="./wolfx509" FAIL_MSG="$X509_MISSING" ;; + writedup) + STRICT=1 + FLAGS="$ALL --enable-writedup" + HAVE="HAVE_WRITE_DUP" + TEST="./wolftls ./handles" RACE=1 EXAMPLES=0 ;; + oldtls) + STRICT=1 + FLAGS="$ALL --enable-oldtls --enable-tlsv10" + HAVE="WOLFSSL_ALLOW_TLSV10" LACK="NO_OLD_TLS" TLS10=1 + TEST="./wolftls" EXAMPLES=0 ;; + notls13) + # --enable-all pulls in QUIC and DTLS 1.3, which need TLS 1.3. + FLAGS="--enable-opensslall --enable-opensslextra --enable-certgen" + FLAGS="$FLAGS --enable-certext --enable-certreq --enable-keygen --enable-sni" + FLAGS="$FLAGS --enable-alpn --enable-sessioncerts --disable-tls13" + FLAGS="$FLAGS CPPFLAGS='-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_SETUP_CB'" + HAVE="OPENSSL_ALL WOLFSSL_CERT_GEN" LACK="WOLFSSL_TLS13" + TEST="$PKGS_ALL" SKIP='^TestMinMaxVersion$/^TLS_1\.3_only$' ;; + reduced1) + FLAGS="--disable-sni --disable-aes --disable-aesgcm --disable-aescbc" + FLAGS="$FLAGS --enable-certgen --enable-certext CPPFLAGS=-DWOLFSSL_ALT_NAMES" + HAVE="NO_AES WOLFSSL_ALT_NAMES" LACK="HAVE_SNI" + BUILD=". ./wolftls ./wolfx509" EXAMPLES=0 ;; + reduced2) + FLAGS="--disable-sha256 --disable-rsa --enable-pwdbased" + FLAGS="$FLAGS --disable-pkcs8 --disable-pkcs12 CPPFLAGS=-DWOLFSSL_NO_PEM" + # wolfSSL's own unit tests, and 5.9.4's random.c under -Werror, break without SHA-256. + FLAGS="$FLAGS --disable-examples ac_cv_vcs_checkout=no" + HAVE="NO_SHA256 NO_RSA NO_PKCS8 WOLFSSL_NO_PEM" + BUILD=". ./wolftls ./handles" EXAMPLES=0 ;; + certgen-noext) + FLAGS="--enable-certgen" + HAVE="WOLFSSL_CERT_GEN" LACK="WOLFSSL_CERT_EXT" + BUILD=". ./handles" + FAIL="./wolfx509" FAIL_MSG="$X509_MISSING" EXAMPLES=0 ;; + fips-ready) + STRICT=1 + FLAGS="--enable-fips=ready --enable-opensslextra --enable-certgen" + FLAGS="$FLAGS --enable-certext --enable-certreq --enable-keygen --enable-sni" + FLAGS="$FLAGS --enable-alpn --enable-sessioncerts" + FLAGS="$FLAGS CPPFLAGS='-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_SETUP_CB'" + HAVE="HAVE_FIPS OPENSSL_EXTRA WOLFSSL_CERT_GEN HAVE_ALPN SESSION_CERTS" + TEST="$PKGS_ALL" ;; + *) + echo "unknown config: $1" >&2 + exit 2 ;; + esac +} + +has_define() { + grep -q "^#define $1\b" "$2" +} + +check_defines() { + local opts=$1/include/wolfssl/options.h d + for d in $HAVE; do + has_define "$d" "$opts" || { echo "::error::expected $d in $opts"; exit 1; } + done + for d in $LACK; do + if has_define "$d" "$opts"; then + echo "::error::did not expect $d in $opts" + exit 1 + fi + done +} + +# Builds each example program by file, since several share a directory, +# then runs the tests of example directories that have them. +build_examples() { + local f d + grep -rl --include='*.go' --exclude='*_test.go' '^package main' examples | while read -r f; do + echo "go build $f" + # shellcheck disable=SC2086 + go build $1 -o /dev/null "$f" + done + find examples -name '*_test.go' -exec dirname {} \; | sort -u | while read -r d; do + # shellcheck disable=SC2086 + go test -count=1 $1 "./$d" + done +} + +run_tests() { + local prefix=$2 tflags pkg out + config "$1" + check_defines "$prefix" + + if [ "$STRICT" = 1 ]; then + export WOLFTLS_INTEROP_STRICT=1 + fi + if [ "$TLS10" = 1 ]; then + export WOLFTLS_INTEROP_TLS10=1 + fi + ./generateOptions.sh "$prefix" + export LD_LIBRARY_PATH="$prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + go clean -cache + + # -race also turns on checkptr; other configs ask for checkptr directly. + # GO_TEST_FLAGS replaces both, for example with -asan. + if [ -n "${GO_TEST_FLAGS:-}" ]; then + tflags=$GO_TEST_FLAGS + elif [ "$RACE" = 1 ]; then + tflags="-race" + else + tflags="-gcflags=all=-d=checkptr" + fi + + if [ -n "$TEST$BUILD" ]; then + # shellcheck disable=SC2086 + go vet $TEST $BUILD + fi + if [ -n "$TEST" ]; then + # shellcheck disable=SC2086 + go test -count=1 -timeout 15m $tflags ${SKIP:+-skip "$SKIP"} $TEST + fi + if [ -n "$BUILD" ]; then + # shellcheck disable=SC2086 + go test -count=1 -run '^$' $BUILD + fi + for pkg in $FAIL; do + if out=$(go build "$pkg" 2>&1); then + echo "::error::$pkg built, but this config lacks a feature it requires" + exit 1 + fi + if ! grep -qF "$FAIL_MSG" <<<"$out"; then + echo "$out" + echo "::error::$pkg failed to build for an unexpected reason" + exit 1 + fi + echo "$pkg refused the build as expected: $FAIL_MSG" + done + if [ "$EXAMPLES" = 1 ]; then + build_examples "$tflags" + fi +} + +case "${1:-}" in +flags) + config "$2" + echo "$FLAGS" ;; +test) + run_tests "$2" "$3" ;; +*) + echo "usage: $0 flags | test " >&2 + exit 2 ;; +esac diff --git a/.github/workflows/_resolve-wolfssl.yml b/.github/workflows/_resolve-wolfssl.yml new file mode 100644 index 0000000..9e5fa95 --- /dev/null +++ b/.github/workflows/_resolve-wolfssl.yml @@ -0,0 +1,43 @@ +name: Resolve wolfSSL versions + +on: + workflow_call: + outputs: + refs: + description: 'JSON array of wolfSSL refs ([latest -stable, master]) for use as a matrix axis' + value: ${{ jobs.resolve.outputs.refs }} + latest_stable: + description: 'Latest wolfSSL v*-stable tag resolved at run time' + value: ${{ jobs.resolve.outputs.latest_stable }} + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + resolve: + name: Resolve wolfSSL versions + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 5 + outputs: + refs: ${{ steps.resolve.outputs.refs }} + latest_stable: ${{ steps.resolve.outputs.latest_stable }} + steps: + - name: Resolve latest -stable wolfSSL tag + id: resolve + run: | + set -euo pipefail + LATEST=$(git ls-remote --tags --refs https://github.com/wolfSSL/wolfssl.git 'v*-stable' \ + | awk -F/ '{print $NF}' | sort -V | tail -n 1) + if [ -z "${LATEST:-}" ]; then + echo "::error::Could not resolve the latest wolfSSL -stable tag" + exit 1 + fi + echo "Latest stable wolfSSL: $LATEST" + echo "latest_stable=$LATEST" >> "$GITHUB_OUTPUT" + echo "refs=$(jq -nc --arg latest "$LATEST" '[$latest, "master"]')" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d22f8e6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,83 @@ +name: CodeQL +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + analyze: + name: CodeQL (Go) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 40 + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + + # Built before CodeQL starts tracing so only go-wolfssl is analyzed. + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ needs.discover.outputs.latest_stable }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Point cgo at wolfSSL + run: | + ./generateOptions.sh /tmp/wolfssl-install + echo "LD_LIBRARY_PATH=/tmp/wolfssl-install/lib" >> "$GITHUB_ENV" + + - uses: github/codeql-action/init@9f759ee644a3e7c15c1390abf49868036c00067b # v3 + with: + languages: go + build-mode: manual + queries: security-extended + + - name: Build go-wolfssl + run: | + go build . ./handles ./sha256 ./wolftls ./wolfx509 + grep -rl --include='*.go' '^package main' examples | while read -r f; do + go build -o /dev/null "$f" + done + + - uses: github/codeql-action/analyze@9f759ee644a3e7c15c1390abf49868036c00067b # v3 + with: + category: /language:go diff --git a/.github/workflows/config-matrix.yml b/.github/workflows/config-matrix.yml new file mode 100644 index 0000000..084eee6 --- /dev/null +++ b/.github/workflows/config-matrix.yml @@ -0,0 +1,105 @@ +name: wolfSSL Config Matrix +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + config: + name: ${{ matrix.config }} (wolfSSL ${{ matrix.wolfssl-ref }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + config: [ all, default, opensslextra, writedup, oldtls, notls13, + reduced1, reduced2, certgen-noext ] + wolfssl-ref: ${{ fromJson(needs.discover.outputs.refs) }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + env: + CONFIG: ${{ matrix.config }} + run: echo "flags=$(.github/scripts/run-config.sh flags "$CONFIG")" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ matrix.wolfssl-ref }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Test go-wolfssl + env: + CONFIG: ${{ matrix.config }} + run: .github/scripts/run-config.sh test "$CONFIG" /tmp/wolfssl-install + + fips-ready: + name: fips-ready (wolfSSL ${{ matrix.version }} FIPS Ready) + if: github.repository_owner == 'wolfssl' + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - version: 5.9.4 + sha256: f1dc8081a41c8cbb02fd6c67edff9d79e35038d647b6290b1e2ff7d9f81ac647 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags fips-ready)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + fips-ready-version: ${{ matrix.version }} + fips-ready-sha256: ${{ matrix.sha256 }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Test go-wolfssl + run: .github/scripts/run-config.sh test fips-ready /tmp/wolfssl-install diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..fb7c051 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,88 @@ +name: Fuzz +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# Seed corpora run with every go test in the config matrix; this workflow +# does the long coverage-guided runs, dispatched nightly. +on: + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + fuzztime: + description: How long to fuzz each target (Go duration) + type: string + default: 10m + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + fuzz: + name: ${{ matrix.target }} (${{ matrix.pkg }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + include: + - { pkg: ., target: FuzzX509DER } + - { pkg: ., target: FuzzX509PEM } + - { pkg: ., target: FuzzRsaPrivateKeyDecode } + - { pkg: ./wolfx509, target: FuzzParseCertificate } + - { pkg: ./wolfx509, target: FuzzParseCertificates } + - { pkg: ./wolfx509, target: FuzzVerify } + - { pkg: ./handles, target: FuzzParseECPrivateKeyDER } + - { pkg: ./handles, target: FuzzParsePKCS8ECPrivateKeyDER } + - { pkg: ./handles, target: FuzzParseECPrivateKeyPEM } + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ needs.discover.outputs.latest_stable }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Fuzz + env: + PKG: ${{ matrix.pkg }} + TARGET: ${{ matrix.target }} + FUZZTIME: ${{ inputs.fuzztime || '10m' }} + run: | + ./generateOptions.sh /tmp/wolfssl-install + export LD_LIBRARY_PATH=/tmp/wolfssl-install/lib + go test -run '^$' -fuzz "^${TARGET}\$" -fuzztime "$FUZZTIME" "$PKG" + + - name: Upload the failing input + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: fuzz-${{ matrix.target }} + path: '**/testdata/fuzz/${{ matrix.target }}/' + retention-days: 30 diff --git a/.github/workflows/go-versions.yml b/.github/workflows/go-versions.yml new file mode 100644 index 0000000..b3f3224 --- /dev/null +++ b/.github/workflows/go-versions.yml @@ -0,0 +1,67 @@ +name: Go Versions +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + go: + name: Go ${{ matrix.go }} (wolfSSL ${{ needs.discover.outputs.latest_stable }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # 1.21 is the oldest Go CI supports; the aliases follow each CI image rebuild. + go: [ '1.21', oldstable, stable ] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ needs.discover.outputs.latest_stable }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + with: + go-version: ${{ matrix.go }} + + - name: Test go-wolfssl + run: .github/scripts/run-config.sh test all /tmp/wolfssl-install diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml new file mode 100644 index 0000000..f5ebf92 --- /dev/null +++ b/.github/workflows/interop.yml @@ -0,0 +1,64 @@ +name: OpenSSL Interop +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + openssl: + name: Examples vs OpenSSL (wolfSSL ${{ matrix.wolfssl-ref }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + wolfssl-ref: ${{ fromJson(needs.discover.outputs.refs) }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ matrix.wolfssl-ref }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Example client and server against OpenSSL + run: .github/scripts/openssl-interop.sh /tmp/wolfssl-install diff --git a/.github/workflows/multi-compiler.yml b/.github/workflows/multi-compiler.yml new file mode 100644 index 0000000..327f673 --- /dev/null +++ b/.github/workflows/multi-compiler.yml @@ -0,0 +1,69 @@ +name: Multi-Compiler +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + compiler: + name: ${{ matrix.cc }} (wolfSSL ${{ needs.discover.outputs.latest_stable }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # Compilers baked into the go-wolfssl-ci image. + cc: [ gcc-12, gcc-13, gcc-14, clang-16, clang-17, clang-18 ] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ needs.discover.outputs.latest_stable }} + configure-flags: ${{ steps.flags.outputs.flags }} + cc: ${{ matrix.cc }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Build and test with warnings as errors + env: + CC: ${{ matrix.cc }} + CGO_CFLAGS: -O2 -g -Wall -Werror + run: .github/scripts/run-config.sh test all /tmp/wolfssl-install diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..034d091 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,154 @@ +name: Nightly CI + +# Dispatches the workflows listed below on the default branch, waits for them, +# and keeps one tracking issue: opened (or commented) when a run fails, closed +# once a later nightly passes in full, which also clears flaky failures. +on: + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +permissions: + actions: write + issues: write + contents: read + +defaults: + run: + shell: bash -e {0} + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + nightly: + name: Run and report nightly CI + if: github.repository == 'wolfSSL/go-wolfssl' + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 240 + steps: + - name: Dispatch, wait, and update the tracking issue + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const LABEL = 'nightly-failure'; + const POLL_MS = 60 * 1000; + const TIMEOUT_MS = 210 * 60 * 1000; + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + const { owner, repo } = context.repo; + const ref = context.payload.repository?.default_branch || 'master'; + + // Each child names its run "Nightly ", so only this nightly's + // runs are collected even if someone dispatches one by hand. + const id = String(context.runId); + const title = `Nightly ${id}`; + const workflows = { + 'config-matrix.yml': {}, + 'go-versions.yml': {}, + 'static-analysis.yml': {}, + 'codeql.yml': {}, + 'multi-compiler.yml': {}, + 'sanitizer.yml': { nightly: 'true' }, + 'fuzz.yml': { fuzztime: '10m' }, + 'interop.yml': {}, + }; + const names = Object.keys(workflows); + + // A minute of margin so clock skew cannot hide the dispatched runs. + const since = new Date(Date.now() - 60 * 1000).toISOString(); + const notDispatched = new Set(); + for (const [workflow_id, inputs] of Object.entries(workflows)) { + try { + await github.rest.actions.createWorkflowDispatch({ + owner, repo, workflow_id, ref, inputs: { ...inputs, nightly_id: id }, + }); + core.info(`Dispatched ${workflow_id}`); + } catch (error) { + notDispatched.add(workflow_id); + core.warning(`Could not dispatch ${workflow_id}: ${error.message}`); + } + } + const waitFor = names.filter((n) => !notDispatched.has(n)); + + const basename = (p) => (p || '').split('/').pop(); + async function latestRuns() { + const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + owner, repo, event: 'workflow_dispatch', created: `>=${since}`, per_page: 100, + }); + const latest = new Map(); + for (const run of runs) { + const file = basename(run.path); + if (!names.includes(file) || run.display_title !== title) continue; + const prev = latest.get(file); + if (!prev || run.run_number > prev.run_number) latest.set(file, run); + } + return latest; + } + + await sleep(30 * 1000); + const deadline = Date.now() + TIMEOUT_MS; + let latest = new Map(); + for (;;) { + latest = await latestRuns(); + const pending = [...latest.values()].filter((r) => r.status !== 'completed'); + core.info(`${latest.size}/${waitFor.length} runs found, ${pending.length} still running`); + if (latest.size === waitFor.length && pending.length === 0) break; + if (Date.now() >= deadline) { + core.warning('Timed out waiting for the nightly runs'); + break; + } + await sleep(POLL_MS); + } + + const results = names.map((name) => { + const run = latest.get(name); + const state = notDispatched.has(name) ? 'dispatch failed' + : !run ? 'not found' : run.status !== 'completed' ? 'still running' : run.conclusion; + return { name, run, state }; + }); + const failed = results.filter((r) => r.state !== 'success'); + + let table = '| Workflow | Result | Run |\n|---|---|---|\n'; + for (const r of results) { + table += `| ${r.name} | ${r.state} | ${r.run ? `[#${r.run.run_number}](${r.run.html_url})` : '-'} |\n`; + } + await core.summary.addRaw(`## Nightly CI\n\n${table}`).write(); + + const today = since.slice(0, 10); + const nightlyUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + const open = (await github.rest.issues.listForRepo({ + owner, repo, state: 'open', labels: LABEL, per_page: 1, + })).data[0]; + + if (failed.length === 0) { + if (open) { + await github.rest.issues.createComment({ + owner, repo, issue_number: open.number, + body: `Nightly CI passed in full on ${today} ([nightly run](${nightlyUrl})), so the failures above did not reproduce. Closing.\n\n${table}`, + }); + await github.rest.issues.update({ + owner, repo, issue_number: open.number, state: 'closed', state_reason: 'completed', + }); + core.info(`Closed #${open.number}`); + } + core.info('All nightly workflows passed'); + return; + } + + const body = `Nightly CI on ${today} had ${failed.length} workflow(s) not passing ([nightly run](${nightlyUrl})).\n\n${table}`; + await github.rest.issues.createLabel({ + owner, repo, name: LABEL, color: 'b60205', description: 'Automated nightly CI failure report', + }).catch(() => {}); + if (open) { + await github.rest.issues.createComment({ owner, repo, issue_number: open.number, body }); + core.info(`Commented on #${open.number}`); + } else { + const created = await github.rest.issues.create({ + owner, repo, title: `Nightly CI failures (${today})`, body, labels: [LABEL], + }); + core.info(`Opened #${created.data.number}`); + } + core.setFailed(`${failed.length} nightly workflow(s) did not pass`); diff --git a/.github/workflows/publish-ci-image.yml b/.github/workflows/publish-ci-image.yml new file mode 100644 index 0000000..f38cf4f --- /dev/null +++ b/.github/workflows/publish-ci-image.yml @@ -0,0 +1,87 @@ +name: Publish CI image + +on: + push: + branches: [master] + paths: + - '.github/docker/go-wolfssl-ci/**' + - '.github/workflows/publish-ci-image.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: publish-ci-image + cancel-in-progress: false + +jobs: + publish: + name: Publish CI image + if: github.repository == 'wolfSSL/go-wolfssl' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build, smoke-test, and push image + id: build + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: .github/docker/go-wolfssl-ci/Dockerfile + platforms: linux/amd64 + pull: true + push: true + tags: | + ghcr.io/wolfssl/go-wolfssl-ci:latest + ghcr.io/wolfssl/go-wolfssl-ci:sha-${{ github.sha }} + labels: org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=registry,ref=ghcr.io/wolfssl/go-wolfssl-ci:latest + cache-to: type=inline + - name: Report the image digest + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: echo "::notice::ghcr.io/wolfssl/go-wolfssl-ci@$DIGEST" + + smoke: + name: Test published CI image + needs: publish + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@${{ needs.publish.outputs.digest }} + timeout-minutes: 10 + defaults: + run: + shell: bash -e {0} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Exercise installed tools + run: go-wolfssl-ci-smoke + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + public: + name: Check anonymous pulls for fork PRs + needs: publish + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Pull the manifest without credentials + env: + DIGEST: ${{ needs.publish.outputs.digest }} + run: | + if ! docker manifest inspect "ghcr.io/wolfssl/go-wolfssl-ci@$DIGEST" > /dev/null; then + echo '::error::Make the go-wolfssl-ci package public in its GHCR package settings before merging the consumer workflows.' + exit 1 + fi diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml new file mode 100644 index 0000000..827bcc1 --- /dev/null +++ b/.github/workflows/sanitizer.yml @@ -0,0 +1,107 @@ +name: Sanitizers +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + nightly: + description: Run every sanitizer against every wolfSSL ref + type: boolean + default: false + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +jobs: + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + sanitize: + name: ${{ matrix.sanitizer }} (wolfSSL ${{ matrix.wolfssl-ref }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + # The sanitizer runtimes need the kernel ASLR workaround below. + options: --privileged + timeout-minutes: 45 + # MSan needs every C dependency instrumented and is still being triaged. + continue-on-error: ${{ matrix.sanitizer == 'msan' }} + strategy: + fail-fast: false + matrix: + sanitizer: ${{ fromJson(inputs.nightly && '["asan","ubsan","msan"]' || '["asan","ubsan"]') }} + wolfssl-ref: ${{ fromJson(inputs.nightly && needs.discover.outputs.refs || format('["{0}"]', needs.discover.outputs.latest_stable)) }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Sanitizer flags + id: san + env: + SANITIZER: ${{ matrix.sanitizer }} + run: | + set -euo pipefail + echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + case "$SANITIZER" in + asan) + echo "cflags=-fsanitize=address -fno-omit-frame-pointer -g -O1" >> "$GITHUB_OUTPUT" + echo "ldflags=-fsanitize=address" >> "$GITHUB_OUTPUT" + echo "gotest=-asan" >> "$GITHUB_OUTPUT" + echo "ASAN_OPTIONS=detect_leaks=0" >> "$GITHUB_ENV" ;; + ubsan) + # The function check trips on wolfSSL's own stack free callbacks. + u="-fsanitize=undefined -fno-sanitize=function -fno-sanitize-recover=all" + echo "cflags=$u -g -O1" >> "$GITHUB_OUTPUT" + echo "ldflags=-fsanitize=undefined" >> "$GITHUB_OUTPUT" + # Go's race runtime cannot link next to the UBSan runtime. + echo "gotest=-gcflags=all=-d=checkptr" >> "$GITHUB_OUTPUT" + echo "CGO_CFLAGS=$u" >> "$GITHUB_ENV" + echo "CGO_LDFLAGS=-fsanitize=undefined" >> "$GITHUB_ENV" + echo "UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1" >> "$GITHUB_ENV" ;; + msan) + echo "cflags=-fsanitize=memory -fsanitize-memory-track-origins=2 -fno-omit-frame-pointer -g -O1" >> "$GITHUB_OUTPUT" + echo "ldflags=-fsanitize=memory" >> "$GITHUB_OUTPUT" + echo "gotest=-msan" >> "$GITHUB_OUTPUT" ;; + esac + + - name: Work around high-entropy ASLR for sanitizers + run: sysctl -w vm.mmap_rnd_bits=28 + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ matrix.wolfssl-ref }} + configure-flags: ${{ steps.san.outputs.flags }} + cflags: ${{ steps.san.outputs.cflags }} + ldflags: ${{ steps.san.outputs.ldflags }} + cc: clang + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + + - name: Test go-wolfssl + env: + CC: clang + GO_TEST_FLAGS: ${{ steps.san.outputs.gotest }} + run: .github/scripts/run-config.sh test all /tmp/wolfssl-install diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..2422edb --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,125 @@ +name: Static Analysis +run-name: ${{ inputs.nightly_id && format('Nightly {0}', inputs.nightly_id) || github.event.pull_request.title || github.event.head_commit.message || github.workflow }} + +# START OF COMMON SECTION +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + workflow_dispatch: + inputs: + nightly_id: + description: Set by the nightly dispatcher to identify its runs + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ inputs.nightly_id }} + cancel-in-progress: true +# END OF COMMON SECTION + +permissions: + contents: read + +defaults: + run: + shell: bash -e {0} + +env: + # The CI image builds staticcheck with this Go; bump both together. + ANALYSIS_GO: '1.26' + +jobs: + gofmt: + name: gofmt (changed files) + if: github.repository_owner == 'wolfssl' && github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Check formatting of changed Go files + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + mapfile -t files < <(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- '*.go') + if [ "${#files[@]}" -eq 0 ]; then + echo "No Go files changed." + exit 0 + fi + bad=$(gofmt -l "${files[@]}") + if [ -n "$bad" ]; then + echo "::error::These changed files need gofmt:" + echo "$bad" + gofmt -d "${files[@]}" + exit 1 + fi + + workflows: + name: actionlint and nightly script syntax + if: github.repository_owner == 'wolfssl' + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: actionlint + # SC2046 is pre-existing in the older build, lint and test-tls workflows. + run: actionlint -ignore SC2046 + + - name: Check the nightly github-script parses + run: | + set -euo pipefail + { echo '(async () => {' + python3 -c 'import sys, yaml; print(yaml.safe_load(open(sys.argv[1]))["jobs"]["nightly"]["steps"][0]["with"]["script"])' \ + .github/workflows/nightly.yml + echo '})();'; } > "$RUNNER_TEMP/nightly.js" + node --check "$RUNNER_TEMP/nightly.js" + + discover: + if: github.repository_owner == 'wolfssl' + uses: ./.github/workflows/_resolve-wolfssl.yml + + analyze: + name: vet, staticcheck, govulncheck (wolfSSL ${{ needs.discover.outputs.latest_stable }}) + needs: discover + runs-on: ubuntu-24.04 + container: + image: ghcr.io/wolfssl/go-wolfssl-ci@sha256:243df6018d71ed9b47be8cb4c0472b1ad626b5fe583683686c72134115225003 + timeout-minutes: 30 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: wolfSSL configure flags + id: flags + run: echo "flags=$(.github/scripts/run-config.sh flags all)" >> "$GITHUB_OUTPUT" + + - uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ needs.discover.outputs.latest_stable }} + configure-flags: ${{ steps.flags.outputs.flags }} + prefix: /tmp/wolfssl-install + + - uses: ./.github/actions/setup-go-wolfssl + with: + go-version: ${{ env.ANALYSIS_GO }} + + - name: Analyze + run: .github/scripts/analyze.sh /tmp/wolfssl-install diff --git a/fuzz_test.go b/fuzz_test.go new file mode 100644 index 0000000..f30f28e --- /dev/null +++ b/fuzz_test.go @@ -0,0 +1,94 @@ +/* fuzz_test.go + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL 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. + * + * wolfSSL 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 this program. If not, see . + */ + +package wolfSSL + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" +) + +// fuzzSeedCertsDER returns the DER of every certificate in examples/certs. +func fuzzSeedCertsDER(f *testing.F) [][]byte { + f.Helper() + files, err := filepath.Glob(filepath.Join("examples", "certs", "*.pem")) + if err != nil { + f.Fatal(err) + } + var ders [][]byte + for _, file := range files { + data, err := os.ReadFile(file) + if err != nil { + f.Fatal(err) + } + for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { + if block.Type == "CERTIFICATE" { + ders = append(ders, block.Bytes) + } + } + } + return ders +} + +func FuzzX509DER(f *testing.F) { + for _, der := range fuzzSeedCertsDER(f) { + f.Add(der) + } + f.Fuzz(func(t *testing.T, der []byte) { + if x := WolfSSL_d2i_X509(der); x != nil { + WolfSSL_X509_free(x) + } + if x := WolfSSL_X509_load_certificate_buffer(der, len(der), SSL_FILETYPE_ASN1); x != nil { + WolfSSL_X509_free(x) + } + }) +} + +func FuzzX509PEM(f *testing.F) { + for _, der := range fuzzSeedCertsDER(f) { + f.Add(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) + } + f.Fuzz(func(t *testing.T, p []byte) { + if x := WolfSSL_X509_load_certificate_buffer(p, len(p), SSL_FILETYPE_PEM); x != nil { + WolfSSL_X509_free(x) + } + }) +} + +func FuzzRsaPrivateKeyDecode(f *testing.F) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + f.Fatal(err) + } + f.Add(x509.MarshalPKCS1PrivateKey(key)) + f.Fuzz(func(t *testing.T, der []byte) { + var k RsaKey + if Wc_InitRsaKey(&k) != 0 { + t.Skip("RSA unavailable in this build") + } + defer Wc_FreeRsaKey(&k) + Wc_RsaPrivateKeyDecode(der, &k) + }) +} diff --git a/handles/fuzz_test.go b/handles/fuzz_test.go new file mode 100644 index 0000000..9f6391f --- /dev/null +++ b/handles/fuzz_test.go @@ -0,0 +1,91 @@ +/* fuzz_test.go + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL 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. + * + * wolfSSL 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 this program. If not, see . + */ + +package handles + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "testing" +) + +// fuzzSeedECKeys returns SEC 1 and PKCS #8 DER for a P-256 and a P-384 key. +func fuzzSeedECKeys(f *testing.F) (sec1, pkcs8 [][]byte) { + f.Helper() + for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384()} { + key, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + f.Fatal(err) + } + der, err := x509.MarshalECPrivateKey(key) + if err != nil { + f.Fatal(err) + } + p8, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + f.Fatal(err) + } + sec1 = append(sec1, der) + pkcs8 = append(pkcs8, p8) + } + return sec1, pkcs8 +} + +func FuzzParseECPrivateKeyDER(f *testing.F) { + sec1, _ := fuzzSeedECKeys(f) + for _, der := range sec1 { + f.Add(der) + } + f.Fuzz(func(t *testing.T, der []byte) { + if k, err := ParseECPrivateKeyDER(der); err == nil { + k.Free() + } + }) +} + +func FuzzParsePKCS8ECPrivateKeyDER(f *testing.F) { + _, pkcs8 := fuzzSeedECKeys(f) + for _, der := range pkcs8 { + f.Add(der) + } + f.Fuzz(func(t *testing.T, der []byte) { + if k, err := ParsePKCS8ECPrivateKeyDER(der); err == nil { + k.Free() + } + }) +} + +func FuzzParseECPrivateKeyPEM(f *testing.F) { + sec1, pkcs8 := fuzzSeedECKeys(f) + for _, der := range sec1 { + f.Add(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})) + } + for _, der := range pkcs8 { + f.Add(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})) + } + f.Fuzz(func(t *testing.T, p []byte) { + if k, err := ParseECPrivateKeyPEM(p); err == nil { + k.Free() + } + }) +} diff --git a/wolftls/interop_test.go b/wolftls/interop_test.go new file mode 100644 index 0000000..a8905d7 --- /dev/null +++ b/wolftls/interop_test.go @@ -0,0 +1,382 @@ +/* interop_test.go + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL 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. + * + * wolfSSL 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 this program. If not, see . + */ + +package wolftls + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "math/big" + "net" + "os" + "strings" + "testing" + "time" +) + +const interopHost = "interop.test" + +// interopPKI is a CA and a server certificate for interopHost, generated +// with crypto/x509 so both TLS stacks load the same identity. +type interopPKI struct { + caPEM, certPEM, keyPEM []byte + stdCert tls.Certificate + pool *x509.CertPool +} + +func newInteropPKI(t *testing.T) *interopPKI { + t.Helper() + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + now := time.Now() + caTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "interop CA"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(24 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign, + } + caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + ca, err := x509.ParseCertificate(caDER) + if err != nil { + t.Fatal(err) + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + leafDER, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: interopHost}, + DNSNames: []string{interopHost}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }, ca, &key.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + p := &interopPKI{ + caPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}), + certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}), + keyPEM: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), + pool: x509.NewCertPool(), + } + p.pool.AddCert(ca) + if p.stdCert, err = tls.X509KeyPair(p.certPEM, p.keyPEM); err != nil { + t.Fatal(err) + } + return p +} + +// skipIfUnsupported skips when err says this wolfSSL build lacks a feature +// the case needs; any other error is a real failure. CI sets +// WOLFTLS_INTEROP_STRICT for full-featured builds, where nothing may skip. +func skipIfUnsupported(t *testing.T, err error) { + t.Helper() + if err == nil || os.Getenv("WOLFTLS_INTEROP_STRICT") != "" { + return + } + msg := err.Error() + for _, s := range []string{"(-174)", "requires wolfSSL built with", "proto_version"} { + if strings.Contains(msg, s) { + t.Skipf("not supported by this wolfSSL build: %v", err) + } + } +} + +// interopCase pins a TLS version and offers ALPN; the zero case uses each +// stack's defaults so builds without either still interoperate. +type interopCase struct { + name string + version uint16 + alpn bool +} + +var interopCases = []interopCase{ + {"defaults", 0, false}, + {"TLS 1.0", VersionTLS10, false}, + {"TLS 1.2 with ALPN", VersionTLS12, true}, + {"TLS 1.3 with ALPN", VersionTLS13, true}, +} + +// skipLegacy skips the TLS 1.0 case unless CI marks the build as having it. +func (c interopCase) skipLegacy(t *testing.T) { + t.Helper() + if c.version == VersionTLS10 && os.Getenv("WOLFTLS_INTEROP_TLS10") == "" { + t.Skip("TLS 1.0 runs only against the oldtls wolfSSL build") + } +} + +func (c interopCase) protos(p ...string) []string { + if c.alpn { + return p + } + return nil +} + +func (c interopCase) checkState(t *testing.T, clientVer, serverVer uint16, clientProto, serverProto string) { + t.Helper() + want := c.version + if want == 0 && clientVer < VersionTLS12 { + t.Errorf("negotiated version %#x, want TLS 1.2 or later", clientVer) + } + if want != 0 && clientVer != want { + t.Errorf("client version %#x, want %#x", clientVer, want) + } + if clientVer != serverVer { + t.Errorf("client version %#x, server version %#x", clientVer, serverVer) + } + wantProto := "" + if c.alpn { + wantProto = "h2" + } + if clientProto != wantProto || serverProto != wantProto { + t.Errorf("ALPN client %q server %q, want %q", clientProto, serverProto, wantProto) + } +} + +type interopResult struct { + state tls.ConnectionState + got string + err error +} + +// echoStd runs a crypto/tls server that echoes one line. +func echoStd(t *testing.T, cfg *tls.Config) (string, <-chan interopResult) { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + done := make(chan interopResult, 1) + go func() { + defer ln.Close() + raw, err := ln.Accept() + if err != nil { + done <- interopResult{err: err} + return + } + raw.SetDeadline(time.Now().Add(10 * time.Second)) + srv := tls.Server(raw, cfg) + defer srv.Close() + var r interopResult + if r.err = srv.Handshake(); r.err == nil { + buf := make([]byte, 4) + if _, r.err = io.ReadFull(srv, buf); r.err == nil { + r.got = string(buf) + _, r.err = srv.Write(buf) + } + } + r.state = srv.ConnectionState() + done <- r + }() + return ln.Addr().String(), done +} + +func TestInteropWolftlsClientStdlibServer(t *testing.T) { + pki := newInteropPKI(t) + for _, c := range interopCases { + c := c + t.Run(c.name, func(t *testing.T) { + c.skipLegacy(t) + addr, done := echoStd(t, &tls.Config{ + Certificates: []tls.Certificate{pki.stdCert}, + MinVersion: c.version, + MaxVersion: c.version, + NextProtos: c.protos("h2", "http/1.1"), + }) + raw, err := net.Dial("tcp", addr) + if err != nil { + t.Fatal(err) + } + cli := Client(raw, &Config{ + ServerName: interopHost, + RootCAPEMs: [][]byte{pki.caPEM}, + MinVersion: c.version, + MaxVersion: c.version, + NextProtos: c.protos("h2"), + }) + defer cli.Close() + cli.SetDeadline(time.Now().Add(10 * time.Second)) + err = cli.Handshake() + skipIfUnsupported(t, err) + if err != nil { + t.Fatalf("handshake: %v", err) + } + if _, err := cli.Write([]byte("ping")); err != nil { + t.Fatalf("write: %v", err) + } + buf := make([]byte, 4) + if _, err := io.ReadFull(cli, buf); err != nil || string(buf) != "ping" { + t.Fatalf("echo %q: %v", buf, err) + } + r := <-done + if r.err != nil { + t.Fatalf("server: %v", r.err) + } + cs := cli.ConnectionState() + c.checkState(t, cs.Version, r.state.Version, cs.NegotiatedProtocol, r.state.NegotiatedProtocol) + if r.state.ServerName != interopHost { + t.Errorf("server saw SNI %q, want %q", r.state.ServerName, interopHost) + } + }) + } +} + +func TestInteropStdlibClientWolftlsServer(t *testing.T) { + pki := newInteropPKI(t) + for _, c := range interopCases { + c := c + t.Run(c.name, func(t *testing.T) { + c.skipLegacy(t) + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + done := make(chan error, 1) + var srvState ConnectionState + go func() { + raw, err := ln.Accept() + if err != nil { + done <- err + return + } + srv := Server(raw, &Config{ + Certificates: []Certificate{{CertPEM: pki.certPEM, KeyPEM: pki.keyPEM}}, + MinVersion: c.version, + MaxVersion: c.version, + NextProtos: c.protos("h2", "http/1.1"), + }) + defer srv.Close() + srv.SetDeadline(time.Now().Add(10 * time.Second)) + if err := srv.Handshake(); err != nil { + done <- err + return + } + buf := make([]byte, 4) + _, err = io.ReadFull(srv, buf) + if err == nil { + _, err = srv.Write(buf) + } + srvState = srv.ConnectionState() + done <- err + }() + cli, err := tls.Dial("tcp", ln.Addr().String(), &tls.Config{ + ServerName: interopHost, + RootCAs: pki.pool, + MinVersion: c.version, + MaxVersion: c.version, + NextProtos: c.protos("h2"), + }) + if err != nil { + srvErr := <-done + skipIfUnsupported(t, srvErr) + t.Fatalf("handshake: client %v, server %v", err, srvErr) + } + defer cli.Close() + cli.SetDeadline(time.Now().Add(10 * time.Second)) + if _, err := cli.Write([]byte("pong")); err != nil { + t.Fatalf("write: %v", err) + } + buf := make([]byte, 4) + if _, err := io.ReadFull(cli, buf); err != nil || string(buf) != "pong" { + t.Fatalf("echo %q: %v", buf, err) + } + if err := <-done; err != nil { + t.Fatalf("server: %v", err) + } + cs := cli.ConnectionState() + c.checkState(t, cs.Version, srvState.Version, cs.NegotiatedProtocol, srvState.NegotiatedProtocol) + }) + } +} + +func TestInteropRejectsBadPeer(t *testing.T) { + pki, other := newInteropPKI(t), newInteropPKI(t) + for name, cfg := range map[string]*Config{ + "wrong hostname": {ServerName: "other.test", RootCAPEMs: [][]byte{pki.caPEM}}, + "untrusted CA": {ServerName: interopHost, RootCAPEMs: [][]byte{other.caPEM}}, + } { + cfg := cfg + t.Run(name, func(t *testing.T) { + addr, done := echoStd(t, &tls.Config{Certificates: []tls.Certificate{pki.stdCert}}) + raw, err := net.Dial("tcp", addr) + if err != nil { + t.Fatal(err) + } + cli := Client(raw, cfg) + cli.SetDeadline(time.Now().Add(10 * time.Second)) + err = cli.Handshake() + cli.Close() + <-done + if err == nil { + t.Fatal("wolftls client accepted a server it should reject") + } + }) + } + + t.Run("stdlib rejects untrusted wolftls server", func(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + go func() { + raw, err := ln.Accept() + if err != nil { + return + } + srv := Server(raw, &Config{ + Certificates: []Certificate{{CertPEM: pki.certPEM, KeyPEM: pki.keyPEM}}, + }) + srv.SetDeadline(time.Now().Add(10 * time.Second)) + srv.Handshake() + srv.Close() + }() + cli, err := tls.Dial("tcp", ln.Addr().String(), &tls.Config{ + ServerName: interopHost, + RootCAs: other.pool, + }) + if err == nil { + cli.Close() + t.Fatal("crypto/tls accepted a wolftls server signed by an untrusted CA") + } + }) +} diff --git a/wolfx509/fuzz_test.go b/wolfx509/fuzz_test.go new file mode 100644 index 0000000..f2c6c6c --- /dev/null +++ b/wolfx509/fuzz_test.go @@ -0,0 +1,180 @@ +/* fuzz_test.go + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL 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. + * + * wolfSSL 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 this program. If not, see . + */ + +package wolfx509 + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" +) + +// fuzzSeedCerts returns the DER of every certificate in examples/certs. +func fuzzSeedCerts(f *testing.F) [][]byte { + f.Helper() + files, err := filepath.Glob(filepath.Join("..", "examples", "certs", "*.pem")) + if err != nil { + f.Fatal(err) + } + var ders [][]byte + for _, file := range files { + data, err := os.ReadFile(file) + if err != nil { + f.Fatal(err) + } + for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { + if block.Type == "CERTIFICATE" { + ders = append(ders, block.Bytes) + } + } + } + if len(ders) == 0 { + f.Fatal("no seed certificates found") + } + return ders +} + +func FuzzParseCertificate(f *testing.F) { + for _, der := range fuzzSeedCerts(f) { + f.Add(der) + } + f.Fuzz(func(t *testing.T, der []byte) { + c, err := ParseCertificate(der) + if err != nil { + return + } + defer c.Free() + again, err := ParseCertificate(c.Raw) + if err != nil { + t.Fatalf("Raw of a parsed certificate does not parse: %v", err) + } + defer again.Free() + if !again.Equal(c) { + t.Fatal("reparsing Raw gave a different certificate") + } + }) +} + +func FuzzParseCertificates(f *testing.F) { + seeds := fuzzSeedCerts(f) + for _, der := range seeds { + f.Add(der) + } + f.Add(bytes.Join(seeds, nil)) + f.Fuzz(func(t *testing.T, der []byte) { + certs, err := ParseCertificates(der) + if err != nil { + return + } + for _, c := range certs { + c.Free() + } + }) +} + +// fuzzFreshChain returns a CA and a www.wolfssl.com leaf it signed, valid +// around now, so FuzzVerify keeps a passing seed after the fixtures expire. +func fuzzFreshChain(f *testing.F) (caDER, leafDER []byte) { + f.Helper() + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + f.Fatal(err) + } + now := time.Now() + caTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "fuzz CA"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(7 * 24 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign, + } + if caDER, err = x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey); err != nil { + f.Fatal(err) + } + ca, err := x509.ParseCertificate(caDER) + if err != nil { + f.Fatal(err) + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + f.Fatal(err) + } + leafDER, err = x509.CreateCertificate(rand.Reader, &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "www.wolfssl.com"}, + DNSNames: []string{"www.wolfssl.com"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(7 * 24 * time.Hour), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }, ca, &key.PublicKey, caKey) + if err != nil { + f.Fatal(err) + } + return caDER, leafDER +} + +func FuzzVerify(f *testing.F) { + caDER, leafDER := fuzzFreshChain(f) + seeds := append(fuzzSeedCerts(f), caDER) + for _, der := range seeds { + f.Add(der) + } + f.Add(leafDER) + f.Fuzz(func(t *testing.T, der []byte) { + c, err := ParseCertificate(der) + if err != nil { + return + } + defer c.Free() + roots := NewCertPool() + defer roots.Free() + for _, s := range seeds { + if root, err := ParseCertificate(s); err == nil { + roots.AddCert(root) + root.Free() + } + } + chains, err := c.Verify(VerifyOptions{Roots: roots, DNSName: "www.wolfssl.com"}) + if err == nil && (len(chains) == 0 || len(chains[0]) == 0) { + t.Fatal("Verify succeeded without returning a chain") + } + if err != nil && bytes.Equal(der, leafDER) { + t.Fatalf("the fresh leaf did not verify: %v", err) + } + for _, chain := range chains { + for _, cert := range chain { + if cert != c { + cert.Free() + } + } + } + }) +}