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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 276 additions & 0 deletions .github/scripts/tls-anvil-test.sh
Original file line number Diff line number Diff line change
@@ -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 CPPFLAGS=-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"
96 changes: 96 additions & 0 deletions .github/workflows/tls-anvil.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 build-essential autoconf automake libtool jq psmisc || \
sudo apt-get install -y build-essential autoconf automake libtool jq

- 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 | \(.TotalTests // "N/A") |",
"| Strictly Passed | \(.StrictlySucceededTests // "N/A") |",
"| Conceptually OK | \(.ConceptuallySucceededTests // "N/A") |",
"| Partially Failed | \(.PartiallyFailedTests // "N/A") |",
"| Fully Failed | \(.FullyFailedTests // "N/A") |",
"| Disabled | \(.DisabledTests // "N/A") |"
' "$REPORT" 2>/dev/null || echo "| (could not parse report.json) | - |"
echo ""
echo "**Category scores:**"
jq -r '.Score | to_entries[] | "- \(.key): \(.value)%"' "$REPORT" 2>/dev/null || true
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