Add TLS-Anvil RFC compliance GitHub Actions workflow

Runs the TLS-Anvil combinatorial test suite nightly against wolfSSL in
all four roles: TLS 1.2/1.3 server and TLS 1.2/1.3 client. Results are
summarized in the job summary and uploaded as artifacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Blankenhorn
2026-02-19 10:35:12 -06:00
parent 3a1aa8310e
commit 0898046113
2 changed files with 366 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
#!/bin/bash
#
# TLS-Anvil RFC compliance test script for wolfSSL
# Usage: ./tls-anvil-test.sh <mode> [extra_configure_flags]
# mode: 'server' or 'client'
# extra_configure_flags: additional ./configure options (optional)
#
# This script:
# 1. Builds wolfSSL with appropriate TLS options
# 2. Runs TLS-Anvil Docker container against wolfSSL
# 3. Collects and reports results
#
# Must be run from the wolfSSL source root directory.
set -e
MODE="${1:-server}"
EXTRA_FLAGS="${2:-}"
# Unique name for port/container isolation (set externally or default)
TEST_NAME="${TLS_ANVIL_TEST_NAME:-default}"
RESULTS_DIR="tls-anvil-results"
TLS_ANVIL_IMAGE="ghcr.io/tls-attacker/tlsanvil:latest"
TIMEOUT_SECONDS=1200
STRENGTH="${TLS_ANVIL_STRENGTH:-1}"
# Derive a unique port from the test name to avoid conflicts on parallel runs.
# Produces a port in the range 11111-11999.
PORT_HASH=$(echo -n "$TEST_NAME" | cksum | awk '{print $1}')
WOLFSSL_PORT=$((11111 + (PORT_HASH % 889)))
# Unique container name per run
CONTAINER_NAME="tls-anvil-${TEST_NAME}-$$"
log_info() { echo "[INFO] $1"; }
log_warn() { echo "[WARN] $1"; }
log_error() { echo "[ERROR] $1"; }
cleanup() {
log_info "Cleaning up..."
if [[ -f "$RESULTS_DIR/server.pid" ]]; then
local pid
pid=$(cat "$RESULTS_DIR/server.pid")
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
rm -f "$RESULTS_DIR/server.pid"
fi
if command -v fuser &> /dev/null; then
fuser -k "${WOLFSSL_PORT}/tcp" 2>/dev/null || true
fi
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
sleep 1
}
ensure_port_available() {
local port=$1
local attempt=0
if command -v fuser &> /dev/null; then
fuser -k "${port}/tcp" 2>/dev/null || true
elif command -v lsof &> /dev/null; then
lsof -ti:"${port}" | xargs kill -9 2>/dev/null || true
fi
while [ $attempt -lt 10 ]; do
if ! (ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -q ":${port} "; then
return 0
fi
log_warn "Port ${port} still in use, waiting..."
sleep 1
attempt=$((attempt + 1))
done
log_error "Port ${port} still in use after 10 attempts"
return 1
}
trap cleanup EXIT
# Clear any state from a previous run
cleanup
if [[ "$MODE" != "server" && "$MODE" != "client" ]]; then
log_error "Invalid mode: $MODE. Must be 'server' or 'client'"
exit 1
fi
log_info "TLS-Anvil Test - Mode: $MODE, Test: $TEST_NAME (port: $WOLFSSL_PORT)"
log_info "Extra configure flags: $EXTRA_FLAGS"
mkdir -p "$RESULTS_DIR"
# ---------------------------------------------------------------------------
# Build wolfSSL
# ---------------------------------------------------------------------------
log_info "Building wolfSSL..."
./autogen.sh
CONFIGURE_OPTS="--enable-asn=all"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-ocspstapling"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-tlsx"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-dtls"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-opensslextra"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-opensslall"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-supportedcurves"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-session-ticket"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-sni"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-alpn"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-truncatedhmac"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-extended-master"
CONFIGURE_OPTS="$CONFIGURE_OPTS --enable-enc-then-mac"
CONFIGURE_OPTS="$CONFIGURE_OPTS C_EXTRA_FLAGS='-DWOLFSSL_EXTRA_ALERTS'"
if [[ -n "$EXTRA_FLAGS" ]]; then
CONFIGURE_OPTS="$CONFIGURE_OPTS $EXTRA_FLAGS"
fi
log_info "Configure options: $CONFIGURE_OPTS"
# shellcheck disable=SC2086
./configure $CONFIGURE_OPTS
make clean
make -j"$(nproc)"
# ---------------------------------------------------------------------------
# Server mode: wolfSSL listens, TLS-Anvil probes as client
# ---------------------------------------------------------------------------
if [[ "$MODE" == "server" ]]; then
log_info "Starting wolfSSL server on port $WOLFSSL_PORT..."
ensure_port_available "$WOLFSSL_PORT"
if [[ ! -f "certs/server-cert.pem" ]] || [[ ! -f "certs/server-key.pem" ]]; then
log_error "Certificate files not found in certs/ directory"
exit 1
fi
# Wrapper loop: restarts the server if it exits so TLS-Anvil can reconnect
# between test cases without the whole run failing.
cat > "$RESULTS_DIR/run-server.sh" << 'SERVERSCRIPT'
#!/bin/bash
CHILD_PID=
cleanup() {
[[ -n "$CHILD_PID" ]] && kill "$CHILD_PID" 2>/dev/null; wait "$CHILD_PID" 2>/dev/null
exit 0
}
trap cleanup SIGTERM SIGINT
while true; do
./examples/server/server -p "$1" -C 4 -r -i -d -x \
-c certs/server-cert.pem -k certs/server-key.pem -v d 2>&1 &
CHILD_PID=$!
wait "$CHILD_PID"
echo "Server exited, restarting in 1 second..."
sleep 1
done
SERVERSCRIPT
chmod +x "$RESULTS_DIR/run-server.sh"
"$RESULTS_DIR/run-server.sh" "$WOLFSSL_PORT" > "$RESULTS_DIR/server.log" 2>&1 &
SERVER_PID=$!
echo "$SERVER_PID" > "$RESULTS_DIR/server.pid"
sleep 1
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
log_error "wolfSSL server failed to start"
cat "$RESULTS_DIR/server.log" || true
exit 1
fi
log_info "wolfSSL server started (PID: $SERVER_PID)"
if command -v openssl &> /dev/null; then
log_info "Quick connectivity check..."
echo "Q" | timeout 5 openssl s_client \
-connect "127.0.0.1:$WOLFSSL_PORT" -tls1_2 2>&1 | head -5 \
|| log_warn "Pre-check had issues (not fatal)"
fi
log_info "Running TLS-Anvil (client mode, timeout: ${TIMEOUT_SECONDS}s, strength: $STRENGTH)..."
ANVIL_EXIT_CODE=0
timeout "$TIMEOUT_SECONDS" docker run --rm \
--name "$CONTAINER_NAME" \
--network host \
-v "$(pwd)/$RESULTS_DIR:/output" \
"$TLS_ANVIL_IMAGE" \
-outputFolder /output \
-parallelHandshakes 4 \
-strength "$STRENGTH" \
-connectionTimeout 200 \
server \
-connect "127.0.0.1:$WOLFSSL_PORT" \
|| ANVIL_EXIT_CODE=$?
log_info "Stopping wolfSSL server..."
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
if [[ "$ANVIL_EXIT_CODE" -ne 0 ]]; then
log_warn "TLS-Anvil exited $ANVIL_EXIT_CODE - last 50 lines of server log:"
tail -50 "$RESULTS_DIR/server.log" || true
fi
# ---------------------------------------------------------------------------
# Client mode: TLS-Anvil listens, wolfSSL connects on each test case
# ---------------------------------------------------------------------------
else
log_info "Running TLS-Anvil (server mode, wolfSSL as client, timeout: ${TIMEOUT_SECONDS}s)..."
ensure_port_available "$WOLFSSL_PORT"
WOLFSSL_DIR="$(pwd)"
# TLS-Anvil calls this script once per test case to trigger a client connection.
cat > "$RESULTS_DIR/trigger-client.sh" << EOF
#!/bin/bash
cd "$WOLFSSL_DIR"
exec ./examples/client/client -h "127.0.0.1" -p "$WOLFSSL_PORT" -d -g -v d
EOF
chmod +x "$RESULTS_DIR/trigger-client.sh"
ANVIL_EXIT_CODE=0
timeout "$TIMEOUT_SECONDS" docker run --rm \
--name "$CONTAINER_NAME" \
--network host \
-v "$(pwd)/$RESULTS_DIR:/output" \
-v "$WOLFSSL_DIR:$WOLFSSL_DIR" \
"$TLS_ANVIL_IMAGE" \
-outputFolder /output \
-parallelHandshakes 3 \
-parallelTests 3 \
-strength "$STRENGTH" \
client \
-port "$WOLFSSL_PORT" \
-triggerScript "$WOLFSSL_DIR/$RESULTS_DIR/trigger-client.sh" \
|| ANVIL_EXIT_CODE=$?
fi
# ---------------------------------------------------------------------------
# Results
# ---------------------------------------------------------------------------
log_info "Checking results..."
if [[ -f "$RESULTS_DIR/report.json" ]]; then
log_info "report.json found"
if command -v jq &> /dev/null; then
TOTAL=$(jq '.Score.Total // "N/A"' "$RESULTS_DIR/report.json" 2>/dev/null || echo "N/A")
PASS=$( jq '.Score.Succeeded // "N/A"' "$RESULTS_DIR/report.json" 2>/dev/null || echo "N/A")
FAIL=$( jq '.Score.Failed // "N/A"' "$RESULTS_DIR/report.json" 2>/dev/null || echo "N/A")
log_info " Total: $TOTAL"
log_info " Passed: $PASS"
log_info " Failed: $FAIL"
cat > "$RESULTS_DIR/summary.txt" << EOF
TLS-Anvil Test Summary
======================
Mode: $MODE
Date: $(date)
Config: $CONFIGURE_OPTS
Results:
Total: $TOTAL
Passed: $PASS
Failed: $FAIL
EOF
fi
else
log_warn "No report.json found"
ls -la "$RESULTS_DIR/" || true
fi
if [[ "$ANVIL_EXIT_CODE" -ne 0 ]]; then
log_error "TLS-Anvil exited with code $ANVIL_EXIT_CODE"
# Exit non-zero so the workflow step is marked failed
exit "$ANVIL_EXIT_CODE"
fi
log_info "TLS-Anvil testing complete"
+90
View File
@@ -0,0 +1,90 @@
name: TLS-Anvil RFC Compliance
on:
schedule:
# Nightly at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
inputs:
strength:
description: 'TLS-Anvil test strength (1=quick, 2=medium, 3=full)'
default: '1'
required: false
type: choice
options: ['1', '2', '3']
jobs:
tls-anvil:
name: ${{ matrix.test-name }}
# Only run from the wolfssl org to avoid burning forks' CI minutes
if: github.repository_owner == 'wolfssl'
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- test-name: tls12-server
mode: server
extra-flags: '--disable-tls13'
- test-name: tls13-server
mode: server
extra-flags: '--enable-tls13'
- test-name: tls12-client
mode: client
extra-flags: '--disable-tls13'
- test-name: tls13-client
mode: client
extra-flags: '--enable-tls13'
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y autoconf automake libtool gcc make jq psmisc
- name: Pull TLS-Anvil Docker image
run: docker pull ghcr.io/tls-attacker/tlsanvil:latest
- name: Run TLS-Anvil (${{ matrix.test-name }})
env:
TLS_ANVIL_TEST_NAME: ${{ matrix.test-name }}
TLS_ANVIL_STRENGTH: ${{ inputs.strength || '1' }}
run: |
bash .github/scripts/tls-anvil-test.sh \
"${{ matrix.mode }}" \
"${{ matrix.extra-flags }}"
- name: Summarize results
if: always()
run: |
REPORT="tls-anvil-results/report.json"
{
echo "## TLS-Anvil: ${{ matrix.test-name }}"
echo ""
if [[ -f "$REPORT" ]]; then
echo "| | Count |"
echo "|---|---|"
jq -r '
"| Total | \(.Score.Total // "N/A") |",
"| Passed | \(.Score.Succeeded // "N/A") |",
"| Failed | \(.Score.Failed // "N/A") |",
"| Disabled | \(.Score.Disabled // "N/A") |"
' "$REPORT" 2>/dev/null || echo "| (could not parse report.json) | — |"
else
echo "No report.json found — check step logs for errors."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: tls-anvil-results-${{ matrix.test-name }}
path: tls-anvil-results/
retention-days: 30
if-no-files-found: warn