From a8a6d3be9e2d7e5538f860410a5521949069134b Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Mon, 10 Nov 2025 23:24:59 +0200 Subject: [PATCH] fix: separate cipher suites and curve preferences into FIPS and non FIPS, and use them accordingly (#3523) See: https://github.com/project-zot/zot/actions/runs/19209741002/job/54910194536 `failed to ping registry localhost:11448: Get "https://localhost:11448/v2/": crypto/ecdh: use of X25519 is not allowed in FIPS 140-only mode` Signed-off-by: Andrei Aaron --- .github/workflows/tls.yaml | 32 +++++- pkg/api/controller.go | 44 +++++--- test/scripts/tls_cipher_check.sh | 185 +++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 17 deletions(-) create mode 100755 test/scripts/tls_cipher_check.sh diff --git a/.github/workflows/tls.yaml b/.github/workflows/tls.yaml index fbcd664c..aa757e32 100644 --- a/.github/workflows/tls.yaml +++ b/.github/workflows/tls.yaml @@ -12,7 +12,15 @@ permissions: read-all jobs: tls-check: runs-on: ubuntu-latest - name: TLS check + strategy: + matrix: + mode: [non-fips, fips] + include: + - mode: non-fips + godebug: "" + - mode: fips + godebug: "fips140=only" + name: TLS check (${{ matrix.mode }}) steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v6 @@ -25,15 +33,31 @@ jobs: mkdir -p test/data cd test/data ../scripts/gen_certs.sh - - name: Check for TLS settings + - name: Build binary run: | cd $GITHUB_WORKSPACE make binary + - name: Start zot server (${{ matrix.mode }}) + run: | + cd $GITHUB_WORKSPACE + if [[ -n "${{ matrix.godebug }}" ]]; then + export GODEBUG="${{ matrix.godebug }}" + fi bin/zot-linux-amd64 serve examples/config-tls.json & echo $! > zot.PID + if [[ -n "${{ matrix.godebug }}" ]]; then + unset GODEBUG + fi sleep 5 # Check if zot server is running cat /proc/$(cat zot.PID)/status | grep State || exit 1 curl -k --connect-timeout 3 --max-time 5 --retry 60 --retry-delay 1 --retry-max-time 180 --retry-connrefused https://localhost:8080/v2/ - - # zot server is running: proceed to testing + - name: Run TLS tests (${{ matrix.mode }}) + run: | + cd $GITHUB_WORKSPACE ./test/scripts/tls_scan.sh + ./test/scripts/tls_cipher_check.sh ${{ matrix.mode }} localhost:8080 + - name: Cleanup + if: always() + run: | + cd $GITHUB_WORKSPACE + [[ -f zot.PID ]] && kill $(cat zot.PID) 2>/dev/null || true diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 69ec6086..d42dce0f 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -204,21 +204,39 @@ func (c *Controller) Run() error { tlsConfig := c.Config.CopyTLSConfig() if tlsConfig != nil && tlsConfig.Key != "" && tlsConfig.Cert != "" { - server.TLSConfig = &tls.Config{ - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + // These are the same as the cipher suites in defaultCipherSuitesFIPS for TLS 1.2 + // see https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=123 + // Note: Order doesn't matter - Go 1.17+ automatically orders cipher suites based on + // hardware capabilities and security properties. See https://go.dev/blog/tls-cipher-suites + cipherSuites := []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + } + if !fips140.Enabled() { + // CHACHA20_POLY1305 is not FIPS-compliant + cipherSuites = append(cipherSuites, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - }, - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.X25519, - }, - PreferServerCipherSuites: true, - MinVersion: tls.VersionTLS12, + ) + } + + // This is a subset of the default curve preferences in defaultCurvePreferencesFIPS for TLS 1.2 + // see https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=106 + curvePreferences := []tls.CurveID{ + tls.CurveP256, + } + if !fips140.Enabled() { + // X25519 is not FIPS-compliant + curvePreferences = append(curvePreferences, tls.X25519) + } + + server.TLSConfig = &tls.Config{ + CipherSuites: cipherSuites, + CurvePreferences: curvePreferences, + // PreferServerCipherSuites is ignored in Go 1.17+ - Go automatically orders cipher suites + MinVersion: tls.VersionTLS12, } if tlsConfig.CACert != "" { diff --git a/test/scripts/tls_cipher_check.sh b/test/scripts/tls_cipher_check.sh new file mode 100755 index 00000000..eedfdd38 --- /dev/null +++ b/test/scripts/tls_cipher_check.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +# Script to check TLS cipher suites used in FIPS vs non-FIPS mode +# Usage: ./tls_cipher_check.sh [fips|non-fips] [host:port] + +set -e + +MODE="${1:-non-fips}" +HOST="${2:-localhost:8080}" + +# FIPS-compliant cipher suites (TLS 1.2) +# See https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=123 +FIPS_TLS12_CIPHERS=( + "ECDHE-ECDSA-AES128-GCM-SHA256" + "ECDHE-ECDSA-AES256-GCM-SHA384" + "ECDHE-RSA-AES128-GCM-SHA256" + "ECDHE-RSA-AES256-GCM-SHA384" +) + +# FIPS-compliant cipher suites (TLS 1.3) +# See https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=131 +FIPS_TLS13_CIPHERS=( + "TLS_AES_128_GCM_SHA256" + "TLS_AES_256_GCM_SHA384" +) + +# Non-FIPS cipher suites (TLS 1.2) +NON_FIPS_TLS12_CIPHERS=( + "ECDHE-ECDSA-CHACHA20-POLY1305" + "ECDHE-RSA-CHACHA20-POLY1305" +) + +# Non-FIPS cipher suites (TLS 1.3) +NON_FIPS_TLS13_CIPHERS=( + "TLS_CHACHA20_POLY1305_SHA256" +) + +echo "=== TLS Cipher Suite Check (Mode: $MODE) ===" +echo "Testing connection to: $HOST" +echo "" + +# Test a specific cipher suite +# Returns 0 if connection succeeds, 1 if it fails +test_cipher() { + local tls_version=$1 + local cipher=$2 + local output + + # -no_ticket disables TLS session tickets to ensure each connection performs + # a full handshake, providing consistent and accurate cipher suite detection + if [[ "$tls_version" == "1.2" ]]; then + output=$(echo | openssl s_client -connect "$HOST" -tls1_2 -cipher "$cipher" -no_ticket 2>&1 || true) + elif [[ "$tls_version" == "1.3" ]]; then + output=$(echo | openssl s_client -connect "$HOST" -tls1_3 -ciphersuites "$cipher" -no_ticket 2>&1 || true) + else + echo "Unknown TLS version: $tls_version" + return 1 + fi + + # Output openssl output for debugging + echo "Debug: Testing TLS $tls_version cipher '$cipher':" + echo "----------------------------------------" + echo "$output" + echo "----------------------------------------" + + # Check if handshake completed successfully + # Successful handshakes show "New, TLSv1.2" or "New, TLSv1.3" with a cipher + # Failed handshakes show "New, (NONE), Cipher is (NONE)" or "Cipher : 0000" + if echo "$output" | grep -qE "New, TLSv[0-9]"; then + # Verify the cipher was actually used and is not (NONE) + if echo "$output" | grep -qiE "Cipher is.*$cipher|Cipher\s*:.*$cipher" && \ + ! echo "$output" | grep -qiE "Cipher is.*\(NONE\)|Cipher\s*:\s*0000"; then + return 0 + fi + fi + + return 1 +} + +# Test TLS 1.2 FIPS ciphers +echo "--- Testing TLS 1.2 FIPS-compliant cipher suites ---" +TLS12_FIPS_PASSED=0 +for cipher in "${FIPS_TLS12_CIPHERS[@]}"; do + if test_cipher "1.2" "$cipher"; then + echo "✓ TLS 1.2 FIPS cipher '$cipher': SUCCESS" + TLS12_FIPS_PASSED=$((TLS12_FIPS_PASSED + 1)) + else + echo "✗ TLS 1.2 FIPS cipher '$cipher': FAILED" + fi +done + +# In FIPS mode, require at least one TLS 1.2 FIPS cipher to work. +# We can't require ALL TLS 1.2 FIPS ciphers because certificate type determines which ones work: +# - RSA certificates: only RSA-based ciphers (ECDHE-RSA-*) work +# - ECDSA certificates: only ECDSA-based ciphers (ECDHE-ECDSA-*) work +# If none work, it indicates a configuration issue or a certificate mismatch. +if [[ "$MODE" == "fips" ]] && [[ $TLS12_FIPS_PASSED -eq 0 ]]; then + echo "" + echo "✗ ERROR: No TLS 1.2 FIPS-compliant cipher suites work (expected at least 1)" + echo " This may indicate a certificate mismatch or configuration issue" + exit 1 +fi + +# Test TLS 1.3 FIPS ciphers - all must work in both FIPS and non-FIPS modes +echo "" +echo "--- Testing TLS 1.3 FIPS-compliant cipher suites ---" +TLS13_FIPS_PASSED=0 +for cipher in "${FIPS_TLS13_CIPHERS[@]}"; do + if test_cipher "1.3" "$cipher"; then + echo "✓ TLS 1.3 FIPS cipher '$cipher': SUCCESS" + TLS13_FIPS_PASSED=$((TLS13_FIPS_PASSED + 1)) + else + echo "✗ TLS 1.3 FIPS cipher '$cipher': FAILED" + echo "" + echo "✗ ERROR: Required TLS 1.3 FIPS cipher '$cipher' failed" + echo " TLS 1.3 FIPS ciphers must work in both FIPS and non-FIPS modes" + echo " This may indicate a configuration issue (e.g., TLS 1.3 disabled)" + exit 1 + fi +done + +# Test TLS 1.2 non-FIPS ciphers - must fail in FIPS mode +echo "" +echo "--- Testing TLS 1.2 non-FIPS cipher suites ---" +for cipher in "${NON_FIPS_TLS12_CIPHERS[@]}"; do + if test_cipher "1.2" "$cipher"; then + echo "✗ TLS 1.2 non-FIPS cipher '$cipher': SUCCESS (should fail in FIPS mode)" + if [[ "$MODE" == "fips" ]]; then + echo "" + echo "✗ ERROR: Non-FIPS cipher '$cipher' was accepted (should be rejected in FIPS mode)" + exit 1 + fi + else + echo "✓ TLS 1.2 non-FIPS cipher '$cipher': FAILED (expected in FIPS mode)" + fi +done + +# Test TLS 1.3 non-FIPS ciphers - must fail in FIPS mode +echo "" +echo "--- Testing TLS 1.3 non-FIPS cipher suites ---" +for cipher in "${NON_FIPS_TLS13_CIPHERS[@]}"; do + if test_cipher "1.3" "$cipher"; then + echo "✗ TLS 1.3 non-FIPS cipher '$cipher': SUCCESS (should fail in FIPS mode)" + if [[ "$MODE" == "fips" ]]; then + echo "" + echo "✗ ERROR: Non-FIPS cipher '$cipher' was accepted (should be rejected in FIPS mode)" + exit 1 + fi + else + echo "✓ TLS 1.3 non-FIPS cipher '$cipher': FAILED (expected in FIPS mode)" + fi +done + +echo "" +echo "=== Verification Results ===" + +# Summary for FIPS mode +if [[ "$MODE" == "fips" ]]; then + echo "FIPS Mode: Summary..." + echo " TLS 1.2 FIPS: $TLS12_FIPS_PASSED/${#FIPS_TLS12_CIPHERS[@]} passed (at least 1 required)" + echo " TLS 1.3 FIPS: $TLS13_FIPS_PASSED/${#FIPS_TLS13_CIPHERS[@]} passed (all required)" + echo "" + echo "Note: TLS 1.2 cipher suites depend on certificate type:" + echo " - RSA certificates: only RSA-based ciphers (ECDHE-RSA-*) work" + echo " - ECDSA certificates: only ECDSA-based ciphers (ECDHE-ECDSA-*) work" + echo " - TLS 1.3 cipher suites work with any certificate type" + echo "Note: All non-FIPS cipher suites were correctly rejected" +fi + +# Summary for non-FIPS mode +if [[ "$MODE" == "non-fips" ]]; then + echo "Non-FIPS Mode: Summary..." + echo " TLS 1.2 FIPS: $TLS12_FIPS_PASSED/${#FIPS_TLS12_CIPHERS[@]} passed" + echo " TLS 1.3 FIPS: $TLS13_FIPS_PASSED/${#FIPS_TLS13_CIPHERS[@]} passed" + echo "" + echo "Note: Non-FIPS mode should accept both FIPS and non-FIPS cipher suites" + echo "Note: TLS 1.2 cipher suites depend on certificate type:" + echo " - RSA certificates: only RSA-based ciphers (ECDHE-RSA-*) work" + echo " - ECDSA certificates: only ECDSA-based ciphers (ECDHE-ECDSA-*) work" + echo " - TLS 1.3 cipher suites work with any certificate type" +fi + +echo "" +echo "=== All checks passed ===" +exit 0