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
97 changes: 78 additions & 19 deletions hack/tools/update-tls-profiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,112 @@ if [ -z "${JQ}" ]; then
fi

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

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

curl -L -s ${INPUT} > ${TMPFILE}
if ! curl -L -s -f "${INPUT}" > "${TMPFILE}"; then
echo "ERROR: Failed to download ${INPUT} (HTTP error or connection failure)" >&2
exit 1
fi

if ! ${JQ} empty "${TMPFILE}" 2>/dev/null; then
echo "ERROR: Downloaded data from ${INPUT} is not valid JSON" >&2
exit 1
fi

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the "|| true" may be dropped here as the grep is in a pipe with awk and the overall result would never fail (as the awk command would not fail). Still looks more robust this way.


cat > ${OUTPUT} <<EOF
# Extract version from downloaded JSON and fail early if missing
NEW_VERSION=$(${JQ} -r '.version' "${TMPFILE}")
if [ -z "${NEW_VERSION}" ] || [ "${NEW_VERSION}" = "null" ]; then
echo "ERROR: Could not read .version from ${INPUT}" >&2
exit 1
fi

if [ "${NEW_VERSION}" = "${STORED_VERSION}" ]; then
echo "Mozilla TLS data is already at version ${NEW_VERSION}, skipping regeneration."
exit 0
fi
echo "Updating Mozilla TLS data from version ${STORED_VERSION:-unknown} to ${NEW_VERSION}"

cat > "${OUTPUT}" <<EOF
package tlsprofiles

// DO NOT EDIT, GENERATED BY ${0}
// DATA SOURCE: ${INPUT}
// DATA VERSION: ${version}
// DATA VERSION: ${NEW_VERSION}

import (
"crypto/tls"
)
EOF

function generate_profile {
cat >> ${OUTPUT} <<EOF

var ${1}TLSProfile = tlsProfile{
local profile="${1}"

# Validate the profile key exists before writing any output
local exists
exists=$(${JQ} -r ".configurations | has(\"${profile}\")" "${TMPFILE}")
if [ "${exists}" != "true" ]; then
echo "ERROR: Profile '${profile}' not found in ${INPUT} (version ${NEW_VERSION})" >&2
echo "Available profiles: $(${JQ} -r '.configurations | keys | join(", ")' "${TMPFILE}")" >&2
exit 1
fi

# Validate tls_versions is a non-empty array with a non-null first entry
if ! ${JQ} -e ".configurations.${profile}.tls_versions | type == \"array\" and length > 0 and .[0] != null" "${TMPFILE}" >/dev/null; then
echo "ERROR: Missing or empty .configurations.${profile}.tls_versions[0] in ${INPUT}" >&2
exit 1
fi

# Validate that at least one cipher is present across ciphersuites and ciphers.iana
# (modern has only ciphersuites; intermediate has both; either alone is valid)
local cipher_count
cipher_count=$(${JQ} -r "
[
(.configurations.${profile}.ciphersuites // []),
(.configurations.${profile}.ciphers.iana // [])
] | add | length" "${TMPFILE}")
if [ "${cipher_count}" -eq 0 ] 2>/dev/null; then
echo "ERROR: Profile '${profile}' has no ciphers in ciphersuites or ciphers.iana" >&2
exit 1
fi

# Validate tls_curves is non-empty
local curve_count
curve_count=$(${JQ} -r ".configurations.${profile}.tls_curves | length" "${TMPFILE}")
if [ "${curve_count}" -eq 0 ] 2>/dev/null; then
echo "ERROR: Profile '${profile}' has no entries in tls_curves" >&2
exit 1
fi

cat >> "${OUTPUT}" <<EOF

var ${profile}TLSProfile = tlsProfile{
ciphers: cipherSlice{
cipherNums: []uint16{
EOF

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

cat >> ${OUTPUT} <<EOF
cat >> "${OUTPUT}" <<EOF
},
},
curves: curveSlice{
curveNums: []tls.CurveID{
EOF

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

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

cat >> ${OUTPUT} <<EOF
cat >> "${OUTPUT}" <<EOF
},
},
minTLSVersion: ${version},
Expand All @@ -63,11 +123,10 @@ EOF

generate_profile "modern"
generate_profile "intermediate"
generate_profile "old"

# Remove unsupported ciphers from Go's crypto/tls package (Mozilla v5.8 includes these but Go doesn't support them)
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}
rm -f ${OUTPUT}.bak
# Remove unsupported ciphers from Go's crypto/tls package
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}"
rm -f "${OUTPUT}.bak"

# Make go happy
go fmt ${OUTPUT}
go fmt "${OUTPUT}"
41 changes: 2 additions & 39 deletions internal/shared/util/tlsprofiles/mozilla_data.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package tlsprofiles

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

import (
"crypto/tls"
Expand Down Expand Up @@ -51,40 +51,3 @@ var intermediateTLSProfile = tlsProfile{
},
minTLSVersion: tls.VersionTLS12,
}

var oldTLSProfile = tlsProfile{
ciphers: cipherSlice{
cipherNums: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
},
},
curves: curveSlice{
curveNums: []tls.CurveID{
X25519MLKEM768,
X25519,
prime256v1,
secp384r1,
},
},
minTLSVersion: tls.VersionTLS10,
}
47 changes: 47 additions & 0 deletions internal/shared/util/tlsprofiles/old_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tlsprofiles

// This file contains a static copy of the Mozilla "old" TLS compatibility profile
// from version 5.8 of the SSL Configuration Generator. The "old" profile was
// removed from the Mozilla specification in later versions but is preserved here
// for backward compatibility with older clients.
//
// Source: https://ssl-config.mozilla.org/guidelines/5.8.json

import "crypto/tls"

var oldTLSProfile = tlsProfile{
ciphers: cipherSlice{
cipherNums: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
},
},
curves: curveSlice{
curveNums: []tls.CurveID{
X25519MLKEM768,
X25519,
prime256v1,
secp384r1,
},
},
minTLSVersion: tls.VersionTLS10,
}
54 changes: 30 additions & 24 deletions internal/shared/util/tlsprofiles/tlsprofiles_test.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,20 @@
package tlsprofiles

import (
"crypto/tls"
"testing"

"github.com/stretchr/testify/require"
)

func TestGetProfiles(t *testing.T) {
tests := []struct {
name tlsProfileName
result bool
}{
{"modern", true},
{"intermediate", true},
{"old", true},
{"custom", true},
{"does-not-exist", false},
}

for _, test := range tests {
p, err := findTLSProfile(test.name)
if !test.result {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, p)
}
}
}

func TestGetTLSConfigFunc(t *testing.T) {
f, err := GetTLSConfigFunc()
require.NoError(t, err)
require.NotNil(t, f)

// Set an invalid profile
// Set an invalid profile and restore afterwards
orig := configuredProfile
t.Cleanup(func() { configuredProfile = orig })
configuredProfile = "does-not-exist"
f, err = GetTLSConfigFunc()
require.Error(t, err)
Expand Down Expand Up @@ -114,6 +94,7 @@ func TestSetCustomCurves(t *testing.T) {
name string
result bool
}{
{"X25519MLKEM768", true}, // Post-quantum hybrid curve (Go 1.24+)
{"X25519", true},
{"prime256v1", true},
{"secp384r1", true},
Expand Down Expand Up @@ -158,3 +139,28 @@ func TestSetCustomVersion(t *testing.T) {
}
}
}

func TestOldProfileMinVersion(t *testing.T) {
require.EqualValues(t, tls.VersionTLS10, oldTLSProfile.minTLSVersion)
}

func TestOldProfileCiphers(t *testing.T) {
ciphers := oldTLSProfile.ciphers.cipherNums
// v5.8 old profile has exactly 21 ciphers
require.Len(t, ciphers, 21, "old profile cipher count changed unexpectedly")
// Legacy CBC cipher present in old but not modern/intermediate
require.Contains(t, ciphers, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA)
// Most legacy cipher (3DES insecure)
require.Contains(t, ciphers, tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA)
// TLS 1.3 cipher shared with all profiles
require.Contains(t, ciphers, tls.TLS_AES_128_GCM_SHA256)
}

func TestProfilesMapCompleteness(t *testing.T) {
for _, name := range []string{"modern", "intermediate", "old", "custom"} {
p, err := findTLSProfile(tlsProfileName(name))
require.NoErrorf(t, err, "profile %q must be in profiles map", name)
require.NotNilf(t, p, "profile %q must not be nil", name)
}
require.GreaterOrEqual(t, len(profiles), 4, "profiles map must contain at least the required built-in profiles")
}
Loading