Skip to content

Commit 8614e3b

Browse files
tmshortclaude
andauthored
Preserve Mozilla v5.8 old TLS profile and harden update script (#2632)
- Move oldTLSProfile to a static old_profile.go (removed from v6+ spec) - Add version-based early exit to update-tls-profiles.sh - Validate profile existence, tls_versions, ciphers, and curves fields - Make jq/sed/cat invocations null-safe and consistently quote variables - Add unit tests for old profile content; fix global state leak in tests Signed-off-by: Todd Short <tshort@redhat.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d32aa02 commit 8614e3b

File tree

4 files changed

+157
-82
lines changed

4 files changed

+157
-82
lines changed

hack/tools/update-tls-profiles.sh

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,52 +8,112 @@ if [ -z "${JQ}" ]; then
88
fi
99

1010
OUTPUT=internal/shared/util/tlsprofiles/mozilla_data.go
11-
INPUT=https://ssl-config.mozilla.org/guidelines/5.8.json
11+
INPUT=https://ssl-config.mozilla.org/guidelines/latest.json
1212

1313
TMPFILE="$(mktemp)"
1414
trap 'rm -rf "$TMPFILE"' EXIT
1515

16-
curl -L -s ${INPUT} > ${TMPFILE}
16+
if ! curl -L -s -f "${INPUT}" > "${TMPFILE}"; then
17+
echo "ERROR: Failed to download ${INPUT} (HTTP error or connection failure)" >&2
18+
exit 1
19+
fi
20+
21+
if ! ${JQ} empty "${TMPFILE}" 2>/dev/null; then
22+
echo "ERROR: Downloaded data from ${INPUT} is not valid JSON" >&2
23+
exit 1
24+
fi
1725

18-
version=$(${JQ} -r '.version' ${TMPFILE})
26+
# Extract stored version from current output file (may be empty on first run)
27+
STORED_VERSION=$(grep '^// DATA VERSION:' "${OUTPUT}" 2>/dev/null | awk '{print $4}' || true)
1928

20-
cat > ${OUTPUT} <<EOF
29+
# Extract version from downloaded JSON and fail early if missing
30+
NEW_VERSION=$(${JQ} -r '.version' "${TMPFILE}")
31+
if [ -z "${NEW_VERSION}" ] || [ "${NEW_VERSION}" = "null" ]; then
32+
echo "ERROR: Could not read .version from ${INPUT}" >&2
33+
exit 1
34+
fi
35+
36+
if [ "${NEW_VERSION}" = "${STORED_VERSION}" ]; then
37+
echo "Mozilla TLS data is already at version ${NEW_VERSION}, skipping regeneration."
38+
exit 0
39+
fi
40+
echo "Updating Mozilla TLS data from version ${STORED_VERSION:-unknown} to ${NEW_VERSION}"
41+
42+
cat > "${OUTPUT}" <<EOF
2143
package tlsprofiles
2244
2345
// DO NOT EDIT, GENERATED BY ${0}
2446
// DATA SOURCE: ${INPUT}
25-
// DATA VERSION: ${version}
47+
// DATA VERSION: ${NEW_VERSION}
2648
2749
import (
2850
"crypto/tls"
2951
)
3052
EOF
3153

3254
function generate_profile {
33-
cat >> ${OUTPUT} <<EOF
34-
35-
var ${1}TLSProfile = tlsProfile{
55+
local profile="${1}"
56+
57+
# Validate the profile key exists before writing any output
58+
local exists
59+
exists=$(${JQ} -r ".configurations | has(\"${profile}\")" "${TMPFILE}")
60+
if [ "${exists}" != "true" ]; then
61+
echo "ERROR: Profile '${profile}' not found in ${INPUT} (version ${NEW_VERSION})" >&2
62+
echo "Available profiles: $(${JQ} -r '.configurations | keys | join(", ")' "${TMPFILE}")" >&2
63+
exit 1
64+
fi
65+
66+
# Validate tls_versions is a non-empty array with a non-null first entry
67+
if ! ${JQ} -e ".configurations.${profile}.tls_versions | type == \"array\" and length > 0 and .[0] != null" "${TMPFILE}" >/dev/null; then
68+
echo "ERROR: Missing or empty .configurations.${profile}.tls_versions[0] in ${INPUT}" >&2
69+
exit 1
70+
fi
71+
72+
# Validate that at least one cipher is present across ciphersuites and ciphers.iana
73+
# (modern has only ciphersuites; intermediate has both; either alone is valid)
74+
local cipher_count
75+
cipher_count=$(${JQ} -r "
76+
[
77+
(.configurations.${profile}.ciphersuites // []),
78+
(.configurations.${profile}.ciphers.iana // [])
79+
] | add | length" "${TMPFILE}")
80+
if [ "${cipher_count}" -eq 0 ] 2>/dev/null; then
81+
echo "ERROR: Profile '${profile}' has no ciphers in ciphersuites or ciphers.iana" >&2
82+
exit 1
83+
fi
84+
85+
# Validate tls_curves is non-empty
86+
local curve_count
87+
curve_count=$(${JQ} -r ".configurations.${profile}.tls_curves | length" "${TMPFILE}")
88+
if [ "${curve_count}" -eq 0 ] 2>/dev/null; then
89+
echo "ERROR: Profile '${profile}' has no entries in tls_curves" >&2
90+
exit 1
91+
fi
92+
93+
cat >> "${OUTPUT}" <<EOF
94+
95+
var ${profile}TLSProfile = tlsProfile{
3696
ciphers: cipherSlice{
3797
cipherNums: []uint16{
3898
EOF
3999

40-
${JQ} -r ".configurations.$1.ciphersuites.[] | . |= \"tls.\" + . + \",\"" ${TMPFILE} >> ${OUTPUT}
41-
${JQ} -r ".configurations.$1.ciphers.iana[] | . |= \"tls.\" + . + \",\"" ${TMPFILE} >> ${OUTPUT}
100+
${JQ} -r "(.configurations.${profile}.ciphersuites // [])[] | . |= \"tls.\" + . + \",\"" "${TMPFILE}" >> "${OUTPUT}"
101+
${JQ} -r "(.configurations.${profile}.ciphers.iana // [])[] | . |= \"tls.\" + . + \",\"" "${TMPFILE}" >> "${OUTPUT}"
42102

43-
cat >> ${OUTPUT} <<EOF
103+
cat >> "${OUTPUT}" <<EOF
44104
},
45105
},
46106
curves: curveSlice{
47107
curveNums: []tls.CurveID{
48108
EOF
49109

50-
${JQ} -r ".configurations.$1.tls_curves[] | . |= . + \",\"" ${TMPFILE} >> ${OUTPUT}
110+
${JQ} -r ".configurations.${profile}.tls_curves[] | . |= . + \",\"" "${TMPFILE}" >> "${OUTPUT}"
51111

52-
version=$(${JQ} -r ".configurations.$1.tls_versions[0]" ${TMPFILE})
112+
version=$(${JQ} -r ".configurations.${profile}.tls_versions[0]" "${TMPFILE}")
53113
version=${version/TLSv1./tls.VersionTLS1}
54114
version=${version/TLSv1/tls.VersionTLS10}
55115

56-
cat >> ${OUTPUT} <<EOF
116+
cat >> "${OUTPUT}" <<EOF
57117
},
58118
},
59119
minTLSVersion: ${version},
@@ -63,11 +123,10 @@ EOF
63123

64124
generate_profile "modern"
65125
generate_profile "intermediate"
66-
generate_profile "old"
67126

68-
# Remove unsupported ciphers from Go's crypto/tls package (Mozilla v5.8 includes these but Go doesn't support them)
69-
sed -i.bak '/TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384/d; /TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384/d; /TLS_RSA_WITH_AES_256_CBC_SHA256/d' ${OUTPUT}
70-
rm -f ${OUTPUT}.bak
127+
# Remove unsupported ciphers from Go's crypto/tls package
128+
sed -i.bak '/TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384/d; /TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384/d; /TLS_RSA_WITH_AES_256_CBC_SHA256/d' "${OUTPUT}"
129+
rm -f "${OUTPUT}.bak"
71130

72131
# Make go happy
73-
go fmt ${OUTPUT}
132+
go fmt "${OUTPUT}"

internal/shared/util/tlsprofiles/mozilla_data.go

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package tlsprofiles
22

33
// DO NOT EDIT, GENERATED BY hack/tools/update-tls-profiles.sh
4-
// DATA SOURCE: https://ssl-config.mozilla.org/guidelines/5.8.json
5-
// DATA VERSION: 5.8
4+
// DATA SOURCE: https://ssl-config.mozilla.org/guidelines/latest.json
5+
// DATA VERSION: 6
66

77
import (
88
"crypto/tls"
@@ -51,40 +51,3 @@ var intermediateTLSProfile = tlsProfile{
5151
},
5252
minTLSVersion: tls.VersionTLS12,
5353
}
54-
55-
var oldTLSProfile = tlsProfile{
56-
ciphers: cipherSlice{
57-
cipherNums: []uint16{
58-
tls.TLS_AES_128_GCM_SHA256,
59-
tls.TLS_AES_256_GCM_SHA384,
60-
tls.TLS_CHACHA20_POLY1305_SHA256,
61-
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
62-
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
63-
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
64-
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
65-
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
66-
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
67-
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
68-
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
69-
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
70-
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
71-
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
72-
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
73-
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
74-
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
75-
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
76-
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
77-
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
78-
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
79-
},
80-
},
81-
curves: curveSlice{
82-
curveNums: []tls.CurveID{
83-
X25519MLKEM768,
84-
X25519,
85-
prime256v1,
86-
secp384r1,
87-
},
88-
},
89-
minTLSVersion: tls.VersionTLS10,
90-
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package tlsprofiles
2+
3+
// This file contains a static copy of the Mozilla "old" TLS compatibility profile
4+
// from version 5.8 of the SSL Configuration Generator. The "old" profile was
5+
// removed from the Mozilla specification in later versions but is preserved here
6+
// for backward compatibility with older clients.
7+
//
8+
// Source: https://ssl-config.mozilla.org/guidelines/5.8.json
9+
10+
import "crypto/tls"
11+
12+
var oldTLSProfile = tlsProfile{
13+
ciphers: cipherSlice{
14+
cipherNums: []uint16{
15+
tls.TLS_AES_128_GCM_SHA256,
16+
tls.TLS_AES_256_GCM_SHA384,
17+
tls.TLS_CHACHA20_POLY1305_SHA256,
18+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
19+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
20+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
21+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
22+
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
23+
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
24+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
25+
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
26+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
27+
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
28+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
29+
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
30+
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
31+
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
32+
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
33+
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
34+
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
35+
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
36+
},
37+
},
38+
curves: curveSlice{
39+
curveNums: []tls.CurveID{
40+
X25519MLKEM768,
41+
X25519,
42+
prime256v1,
43+
secp384r1,
44+
},
45+
},
46+
minTLSVersion: tls.VersionTLS10,
47+
}

internal/shared/util/tlsprofiles/tlsprofiles_test.go

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,20 @@
11
package tlsprofiles
22

33
import (
4+
"crypto/tls"
45
"testing"
56

67
"github.com/stretchr/testify/require"
78
)
89

9-
func TestGetProfiles(t *testing.T) {
10-
tests := []struct {
11-
name tlsProfileName
12-
result bool
13-
}{
14-
{"modern", true},
15-
{"intermediate", true},
16-
{"old", true},
17-
{"custom", true},
18-
{"does-not-exist", false},
19-
}
20-
21-
for _, test := range tests {
22-
p, err := findTLSProfile(test.name)
23-
if !test.result {
24-
require.Error(t, err)
25-
} else {
26-
require.NoError(t, err)
27-
require.NotNil(t, p)
28-
}
29-
}
30-
}
31-
3210
func TestGetTLSConfigFunc(t *testing.T) {
3311
f, err := GetTLSConfigFunc()
3412
require.NoError(t, err)
3513
require.NotNil(t, f)
3614

37-
// Set an invalid profile
15+
// Set an invalid profile and restore afterwards
16+
orig := configuredProfile
17+
t.Cleanup(func() { configuredProfile = orig })
3818
configuredProfile = "does-not-exist"
3919
f, err = GetTLSConfigFunc()
4020
require.Error(t, err)
@@ -114,6 +94,7 @@ func TestSetCustomCurves(t *testing.T) {
11494
name string
11595
result bool
11696
}{
97+
{"X25519MLKEM768", true}, // Post-quantum hybrid curve (Go 1.24+)
11798
{"X25519", true},
11899
{"prime256v1", true},
119100
{"secp384r1", true},
@@ -158,3 +139,28 @@ func TestSetCustomVersion(t *testing.T) {
158139
}
159140
}
160141
}
142+
143+
func TestOldProfileMinVersion(t *testing.T) {
144+
require.EqualValues(t, tls.VersionTLS10, oldTLSProfile.minTLSVersion)
145+
}
146+
147+
func TestOldProfileCiphers(t *testing.T) {
148+
ciphers := oldTLSProfile.ciphers.cipherNums
149+
// v5.8 old profile has exactly 21 ciphers
150+
require.Len(t, ciphers, 21, "old profile cipher count changed unexpectedly")
151+
// Legacy CBC cipher present in old but not modern/intermediate
152+
require.Contains(t, ciphers, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA)
153+
// Most legacy cipher (3DES insecure)
154+
require.Contains(t, ciphers, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA)
155+
// TLS 1.3 cipher shared with all profiles
156+
require.Contains(t, ciphers, tls.TLS_AES_128_GCM_SHA256)
157+
}
158+
159+
func TestProfilesMapCompleteness(t *testing.T) {
160+
for _, name := range []string{"modern", "intermediate", "old", "custom"} {
161+
p, err := findTLSProfile(tlsProfileName(name))
162+
require.NoErrorf(t, err, "profile %q must be in profiles map", name)
163+
require.NotNilf(t, p, "profile %q must not be nil", name)
164+
}
165+
require.GreaterOrEqual(t, len(profiles), 4, "profiles map must contain at least the required built-in profiles")
166+
}

0 commit comments

Comments
 (0)