Skip to content

Commit a8a6d3b

Browse files
authored
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 <andreifdaaron@gmail.com>
1 parent b64add7 commit a8a6d3b

File tree

3 files changed

+244
-17
lines changed

3 files changed

+244
-17
lines changed

.github/workflows/tls.yaml

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@ permissions: read-all
1212
jobs:
1313
tls-check:
1414
runs-on: ubuntu-latest
15-
name: TLS check
15+
strategy:
16+
matrix:
17+
mode: [non-fips, fips]
18+
include:
19+
- mode: non-fips
20+
godebug: ""
21+
- mode: fips
22+
godebug: "fips140=only"
23+
name: TLS check (${{ matrix.mode }})
1624
steps:
1725
- uses: actions/checkout@v4
1826
- uses: actions/setup-go@v6
@@ -25,15 +33,31 @@ jobs:
2533
mkdir -p test/data
2634
cd test/data
2735
../scripts/gen_certs.sh
28-
- name: Check for TLS settings
36+
- name: Build binary
2937
run: |
3038
cd $GITHUB_WORKSPACE
3139
make binary
40+
- name: Start zot server (${{ matrix.mode }})
41+
run: |
42+
cd $GITHUB_WORKSPACE
43+
if [[ -n "${{ matrix.godebug }}" ]]; then
44+
export GODEBUG="${{ matrix.godebug }}"
45+
fi
3246
bin/zot-linux-amd64 serve examples/config-tls.json & echo $! > zot.PID
47+
if [[ -n "${{ matrix.godebug }}" ]]; then
48+
unset GODEBUG
49+
fi
3350
sleep 5
3451
# Check if zot server is running
3552
cat /proc/$(cat zot.PID)/status | grep State || exit 1
3653
curl -k --connect-timeout 3 --max-time 5 --retry 60 --retry-delay 1 --retry-max-time 180 --retry-connrefused https://localhost:8080/v2/
37-
38-
# zot server is running: proceed to testing
54+
- name: Run TLS tests (${{ matrix.mode }})
55+
run: |
56+
cd $GITHUB_WORKSPACE
3957
./test/scripts/tls_scan.sh
58+
./test/scripts/tls_cipher_check.sh ${{ matrix.mode }} localhost:8080
59+
- name: Cleanup
60+
if: always()
61+
run: |
62+
cd $GITHUB_WORKSPACE
63+
[[ -f zot.PID ]] && kill $(cat zot.PID) 2>/dev/null || true

pkg/api/controller.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -204,21 +204,39 @@ func (c *Controller) Run() error {
204204

205205
tlsConfig := c.Config.CopyTLSConfig()
206206
if tlsConfig != nil && tlsConfig.Key != "" && tlsConfig.Cert != "" {
207-
server.TLSConfig = &tls.Config{
208-
CipherSuites: []uint16{
209-
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
210-
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
207+
// These are the same as the cipher suites in defaultCipherSuitesFIPS for TLS 1.2
208+
// see https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=123
209+
// Note: Order doesn't matter - Go 1.17+ automatically orders cipher suites based on
210+
// hardware capabilities and security properties. See https://go.dev/blog/tls-cipher-suites
211+
cipherSuites := []uint16{
212+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
213+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
214+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
215+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
216+
}
217+
if !fips140.Enabled() {
218+
// CHACHA20_POLY1305 is not FIPS-compliant
219+
cipherSuites = append(cipherSuites,
211220
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
212221
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
213-
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
214-
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
215-
},
216-
CurvePreferences: []tls.CurveID{
217-
tls.CurveP256,
218-
tls.X25519,
219-
},
220-
PreferServerCipherSuites: true,
221-
MinVersion: tls.VersionTLS12,
222+
)
223+
}
224+
225+
// This is a subset of the default curve preferences in defaultCurvePreferencesFIPS for TLS 1.2
226+
// see https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=106
227+
curvePreferences := []tls.CurveID{
228+
tls.CurveP256,
229+
}
230+
if !fips140.Enabled() {
231+
// X25519 is not FIPS-compliant
232+
curvePreferences = append(curvePreferences, tls.X25519)
233+
}
234+
235+
server.TLSConfig = &tls.Config{
236+
CipherSuites: cipherSuites,
237+
CurvePreferences: curvePreferences,
238+
// PreferServerCipherSuites is ignored in Go 1.17+ - Go automatically orders cipher suites
239+
MinVersion: tls.VersionTLS12,
222240
}
223241

224242
if tlsConfig.CACert != "" {

test/scripts/tls_cipher_check.sh

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env bash
2+
3+
# Script to check TLS cipher suites used in FIPS vs non-FIPS mode
4+
# Usage: ./tls_cipher_check.sh [fips|non-fips] [host:port]
5+
6+
set -e
7+
8+
MODE="${1:-non-fips}"
9+
HOST="${2:-localhost:8080}"
10+
11+
# FIPS-compliant cipher suites (TLS 1.2)
12+
# See https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=123
13+
FIPS_TLS12_CIPHERS=(
14+
"ECDHE-ECDSA-AES128-GCM-SHA256"
15+
"ECDHE-ECDSA-AES256-GCM-SHA384"
16+
"ECDHE-RSA-AES128-GCM-SHA256"
17+
"ECDHE-RSA-AES256-GCM-SHA384"
18+
)
19+
20+
# FIPS-compliant cipher suites (TLS 1.3)
21+
# See https://cs.opensource.google/go/go/+/refs/tags/go1.24.9:src/crypto/tls/defaults.go;l=131
22+
FIPS_TLS13_CIPHERS=(
23+
"TLS_AES_128_GCM_SHA256"
24+
"TLS_AES_256_GCM_SHA384"
25+
)
26+
27+
# Non-FIPS cipher suites (TLS 1.2)
28+
NON_FIPS_TLS12_CIPHERS=(
29+
"ECDHE-ECDSA-CHACHA20-POLY1305"
30+
"ECDHE-RSA-CHACHA20-POLY1305"
31+
)
32+
33+
# Non-FIPS cipher suites (TLS 1.3)
34+
NON_FIPS_TLS13_CIPHERS=(
35+
"TLS_CHACHA20_POLY1305_SHA256"
36+
)
37+
38+
echo "=== TLS Cipher Suite Check (Mode: $MODE) ==="
39+
echo "Testing connection to: $HOST"
40+
echo ""
41+
42+
# Test a specific cipher suite
43+
# Returns 0 if connection succeeds, 1 if it fails
44+
test_cipher() {
45+
local tls_version=$1
46+
local cipher=$2
47+
local output
48+
49+
# -no_ticket disables TLS session tickets to ensure each connection performs
50+
# a full handshake, providing consistent and accurate cipher suite detection
51+
if [[ "$tls_version" == "1.2" ]]; then
52+
output=$(echo | openssl s_client -connect "$HOST" -tls1_2 -cipher "$cipher" -no_ticket 2>&1 || true)
53+
elif [[ "$tls_version" == "1.3" ]]; then
54+
output=$(echo | openssl s_client -connect "$HOST" -tls1_3 -ciphersuites "$cipher" -no_ticket 2>&1 || true)
55+
else
56+
echo "Unknown TLS version: $tls_version"
57+
return 1
58+
fi
59+
60+
# Output openssl output for debugging
61+
echo "Debug: Testing TLS $tls_version cipher '$cipher':"
62+
echo "----------------------------------------"
63+
echo "$output"
64+
echo "----------------------------------------"
65+
66+
# Check if handshake completed successfully
67+
# Successful handshakes show "New, TLSv1.2" or "New, TLSv1.3" with a cipher
68+
# Failed handshakes show "New, (NONE), Cipher is (NONE)" or "Cipher : 0000"
69+
if echo "$output" | grep -qE "New, TLSv[0-9]"; then
70+
# Verify the cipher was actually used and is not (NONE)
71+
if echo "$output" | grep -qiE "Cipher is.*$cipher|Cipher\s*:.*$cipher" && \
72+
! echo "$output" | grep -qiE "Cipher is.*\(NONE\)|Cipher\s*:\s*0000"; then
73+
return 0
74+
fi
75+
fi
76+
77+
return 1
78+
}
79+
80+
# Test TLS 1.2 FIPS ciphers
81+
echo "--- Testing TLS 1.2 FIPS-compliant cipher suites ---"
82+
TLS12_FIPS_PASSED=0
83+
for cipher in "${FIPS_TLS12_CIPHERS[@]}"; do
84+
if test_cipher "1.2" "$cipher"; then
85+
echo "✓ TLS 1.2 FIPS cipher '$cipher': SUCCESS"
86+
TLS12_FIPS_PASSED=$((TLS12_FIPS_PASSED + 1))
87+
else
88+
echo "✗ TLS 1.2 FIPS cipher '$cipher': FAILED"
89+
fi
90+
done
91+
92+
# In FIPS mode, require at least one TLS 1.2 FIPS cipher to work.
93+
# We can't require ALL TLS 1.2 FIPS ciphers because certificate type determines which ones work:
94+
# - RSA certificates: only RSA-based ciphers (ECDHE-RSA-*) work
95+
# - ECDSA certificates: only ECDSA-based ciphers (ECDHE-ECDSA-*) work
96+
# If none work, it indicates a configuration issue or a certificate mismatch.
97+
if [[ "$MODE" == "fips" ]] && [[ $TLS12_FIPS_PASSED -eq 0 ]]; then
98+
echo ""
99+
echo "✗ ERROR: No TLS 1.2 FIPS-compliant cipher suites work (expected at least 1)"
100+
echo " This may indicate a certificate mismatch or configuration issue"
101+
exit 1
102+
fi
103+
104+
# Test TLS 1.3 FIPS ciphers - all must work in both FIPS and non-FIPS modes
105+
echo ""
106+
echo "--- Testing TLS 1.3 FIPS-compliant cipher suites ---"
107+
TLS13_FIPS_PASSED=0
108+
for cipher in "${FIPS_TLS13_CIPHERS[@]}"; do
109+
if test_cipher "1.3" "$cipher"; then
110+
echo "✓ TLS 1.3 FIPS cipher '$cipher': SUCCESS"
111+
TLS13_FIPS_PASSED=$((TLS13_FIPS_PASSED + 1))
112+
else
113+
echo "✗ TLS 1.3 FIPS cipher '$cipher': FAILED"
114+
echo ""
115+
echo "✗ ERROR: Required TLS 1.3 FIPS cipher '$cipher' failed"
116+
echo " TLS 1.3 FIPS ciphers must work in both FIPS and non-FIPS modes"
117+
echo " This may indicate a configuration issue (e.g., TLS 1.3 disabled)"
118+
exit 1
119+
fi
120+
done
121+
122+
# Test TLS 1.2 non-FIPS ciphers - must fail in FIPS mode
123+
echo ""
124+
echo "--- Testing TLS 1.2 non-FIPS cipher suites ---"
125+
for cipher in "${NON_FIPS_TLS12_CIPHERS[@]}"; do
126+
if test_cipher "1.2" "$cipher"; then
127+
echo "✗ TLS 1.2 non-FIPS cipher '$cipher': SUCCESS (should fail in FIPS mode)"
128+
if [[ "$MODE" == "fips" ]]; then
129+
echo ""
130+
echo "✗ ERROR: Non-FIPS cipher '$cipher' was accepted (should be rejected in FIPS mode)"
131+
exit 1
132+
fi
133+
else
134+
echo "✓ TLS 1.2 non-FIPS cipher '$cipher': FAILED (expected in FIPS mode)"
135+
fi
136+
done
137+
138+
# Test TLS 1.3 non-FIPS ciphers - must fail in FIPS mode
139+
echo ""
140+
echo "--- Testing TLS 1.3 non-FIPS cipher suites ---"
141+
for cipher in "${NON_FIPS_TLS13_CIPHERS[@]}"; do
142+
if test_cipher "1.3" "$cipher"; then
143+
echo "✗ TLS 1.3 non-FIPS cipher '$cipher': SUCCESS (should fail in FIPS mode)"
144+
if [[ "$MODE" == "fips" ]]; then
145+
echo ""
146+
echo "✗ ERROR: Non-FIPS cipher '$cipher' was accepted (should be rejected in FIPS mode)"
147+
exit 1
148+
fi
149+
else
150+
echo "✓ TLS 1.3 non-FIPS cipher '$cipher': FAILED (expected in FIPS mode)"
151+
fi
152+
done
153+
154+
echo ""
155+
echo "=== Verification Results ==="
156+
157+
# Summary for FIPS mode
158+
if [[ "$MODE" == "fips" ]]; then
159+
echo "FIPS Mode: Summary..."
160+
echo " TLS 1.2 FIPS: $TLS12_FIPS_PASSED/${#FIPS_TLS12_CIPHERS[@]} passed (at least 1 required)"
161+
echo " TLS 1.3 FIPS: $TLS13_FIPS_PASSED/${#FIPS_TLS13_CIPHERS[@]} passed (all required)"
162+
echo ""
163+
echo "Note: TLS 1.2 cipher suites depend on certificate type:"
164+
echo " - RSA certificates: only RSA-based ciphers (ECDHE-RSA-*) work"
165+
echo " - ECDSA certificates: only ECDSA-based ciphers (ECDHE-ECDSA-*) work"
166+
echo " - TLS 1.3 cipher suites work with any certificate type"
167+
echo "Note: All non-FIPS cipher suites were correctly rejected"
168+
fi
169+
170+
# Summary for non-FIPS mode
171+
if [[ "$MODE" == "non-fips" ]]; then
172+
echo "Non-FIPS Mode: Summary..."
173+
echo " TLS 1.2 FIPS: $TLS12_FIPS_PASSED/${#FIPS_TLS12_CIPHERS[@]} passed"
174+
echo " TLS 1.3 FIPS: $TLS13_FIPS_PASSED/${#FIPS_TLS13_CIPHERS[@]} passed"
175+
echo ""
176+
echo "Note: Non-FIPS mode should accept both FIPS and non-FIPS cipher suites"
177+
echo "Note: TLS 1.2 cipher suites depend on certificate type:"
178+
echo " - RSA certificates: only RSA-based ciphers (ECDHE-RSA-*) work"
179+
echo " - ECDSA certificates: only ECDSA-based ciphers (ECDHE-ECDSA-*) work"
180+
echo " - TLS 1.3 cipher suites work with any certificate type"
181+
fi
182+
183+
echo ""
184+
echo "=== All checks passed ==="
185+
exit 0

0 commit comments

Comments
 (0)