Skip to content
Open
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ fmt: $(YAMLFMT) #EXHELP Formats code
$(YAMLFMT) -gitignore_excludes testdata

.PHONY: update-tls-profiles
update-tls-profiles: $(GOJQ) #EXHELP Update TLS profiles from the Mozilla wiki
env JQ=$(GOJQ) hack/tools/update-tls-profiles.sh
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.

Seems to me here we drop the only usage of gojq, but we keep it still in bingo files (.bingo/gojq.mod, .bingo/Variables.mk, ...).
Should we drop it also from bingo?
Should be something like:

bingo-v0.9.0 get gojq@none

update-tls-profiles: #EXHELP Update TLS profiles from the Mozilla wiki
hack/tools/update-tls-profiles.sh

.PHONY: update-registryv1-bundle-schema
update-registryv1-bundle-schema: #EXHELP Update registry+v1 bundle configuration JSON schema
Expand Down
125 changes: 2 additions & 123 deletions hack/tools/update-tls-profiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,131 +2,10 @@

set -e

if [ -z "${JQ}" ]; then
echo "JQ not defined"
exit 1
fi

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

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

if ! curl -L -s -f "${INPUT}" > "${TMPFILE}"; then
if ! curl -L -s -f "${INPUT}" -o "${OUTPUT}"; 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

# 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)

# 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: ${NEW_VERSION}

import (
"crypto/tls"
)
EOF

function generate_profile {
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.${profile}.ciphersuites // [])[] | . |= \"tls.\" + . + \",\"" "${TMPFILE}" >> "${OUTPUT}"
${JQ} -r "(.configurations.${profile}.ciphers.iana // [])[] | . |= \"tls.\" + . + \",\"" "${TMPFILE}" >> "${OUTPUT}"

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

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

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

cat >> "${OUTPUT}" <<EOF
},
},
minTLSVersion: ${version},
}
EOF
}

generate_profile "modern"
generate_profile "intermediate"

# 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}"
131 changes: 88 additions & 43 deletions internal/shared/util/tlsprofiles/mozilla_data.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,98 @@
package tlsprofiles

// DO NOT EDIT, GENERATED BY hack/tools/update-tls-profiles.sh
// DATA SOURCE: https://ssl-config.mozilla.org/guidelines/latest.json
// DATA VERSION: 6
// This file embeds the Mozilla SSL/TLS Configuration Guidelines JSON and parses
// it at init() time to populate the modern and intermediate TLS profiles.
// Run `make update-tls-profiles` to refresh mozilla_data.json from the upstream spec.

import (
"crypto/tls"
_ "embed"
"encoding/json"
"fmt"
)

var modernTLSProfile = tlsProfile{
ciphers: cipherSlice{
cipherNums: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
},
},
curves: curveSlice{
curveNums: []tls.CurveID{
X25519MLKEM768,
X25519,
prime256v1,
secp384r1,
},
},
minTLSVersion: tls.VersionTLS13,
//go:embed mozilla_data.json
var mozillaDataJSON []byte

// skippedCiphers records cipher names from mozilla_data.json that are not
// supported by Go's crypto/tls and were omitted from the profiles.
var skippedCiphers []string

var (
modernTLSProfile tlsProfile
intermediateTLSProfile tlsProfile
)

type mozillaConfiguration struct {
Ciphersuites []string `json:"ciphersuites"`
Ciphers struct {
IANA []string `json:"iana"`
} `json:"ciphers"`
TLSCurves []string `json:"tls_curves"`
TLSVersions []string `json:"tls_versions"`
}

var intermediateTLSProfile = 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,
},
},
curves: curveSlice{
curveNums: []tls.CurveID{
X25519MLKEM768,
X25519,
prime256v1,
secp384r1,
},
},
minTLSVersion: tls.VersionTLS12,
type mozillaSpec struct {
Configurations map[string]mozillaConfiguration `json:"configurations"`
}

func init() {
var spec mozillaSpec
if err := json.Unmarshal(mozillaDataJSON, &spec); err != nil {
panic(fmt.Sprintf("tlsprofiles: failed to parse embedded mozilla_data.json: %v", err))
}

for _, name := range []string{"modern", "intermediate"} {
cfg, ok := spec.Configurations[name]
if !ok {
panic(fmt.Sprintf("tlsprofiles: profile %q not found in embedded mozilla_data.json", name))
}

p, skipped := parseProfile(name, cfg)
skippedCiphers = append(skippedCiphers, skipped...)

switch name {
case "modern":
modernTLSProfile = p
case "intermediate":
intermediateTLSProfile = p
}
}
}

func parseProfile(name string, cfg mozillaConfiguration) (tlsProfile, []string) {
var skipped []string
var cipherNums []uint16
for _, c := range append(cfg.Ciphersuites, cfg.Ciphers.IANA...) {
id := cipherSuiteId(c)
if id == 0 {
skipped = append(skipped, c)
continue
}
cipherNums = append(cipherNums, id)
}

var curveNums []tls.CurveID
for _, c := range cfg.TLSCurves {
id := curveId(c)
if id == 0 {
continue
}
curveNums = append(curveNums, id)
}

if len(cfg.TLSVersions) == 0 {
panic(fmt.Sprintf("tlsprofiles: profile %q has no tls_versions in embedded mozilla_data.json", name))
}

var version tlsVersion
if err := version.Set(cfg.TLSVersions[0]); err != nil {
panic(fmt.Sprintf("tlsprofiles: profile %q has unrecognized tls_versions[0] %q: %v", name, cfg.TLSVersions[0], err))
}

return tlsProfile{
ciphers: cipherSlice{cipherNums: cipherNums},
curves: curveSlice{curveNums: curveNums},
minTLSVersion: version,
}, skipped
}
70 changes: 70 additions & 0 deletions internal/shared/util/tlsprofiles/mozilla_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"version": 6.0,
"href": "https://ssl-config.mozilla.org/guidelines/6.0.json",
"configurations": {
"modern": {
"certificate_curves": ["prime256v1", "secp384r1"],
"certificate_signatures": ["ecdsa-with-SHA256", "ecdsa-with-SHA384", "ecdsa-with-SHA512"],
"certificate_types": ["ecdsa"],
"ciphers": {
"iana": [],
"openssl": []
},
"ciphersuites": [
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256"
],
"dh_param_size": null,
"ecdh_param_size": 256,
"hsts_min_age": 63072000,
"maximum_certificate_lifespan": 90,
"ocsp_staple": true,
"oldest_clients": ["Firefox 63", "Android 10.0", "Chrome 70", "Edge 75", "Java 11", "OpenSSL 1.1.1", "Opera 57", "Safari 12.1"],
"recommended_certificate_lifespan": 90,
"rsa_key_size": null,
"server_preferred_order": false,
"tls_curves": ["X25519MLKEM768", "X25519", "prime256v1", "secp384r1"],
"tls_versions": ["TLSv1.3"]
},
"intermediate": {
"certificate_curves": ["prime256v1", "secp384r1"],
"certificate_signatures": ["sha256WithRSAEncryption", "ecdsa-with-SHA256", "ecdsa-with-SHA384", "ecdsa-with-SHA512"],
"certificate_types": ["ecdsa", "rsa"],
"ciphers": {
"iana": [
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
],
"openssl": [
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-RSA-CHACHA20-POLY1305"
]
},
"ciphersuites": [
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256"
],
"dh_param_size": 2048,
"ecdh_param_size": 256,
"hsts_min_age": 63072000,
"maximum_certificate_lifespan": 366,
"ocsp_staple": true,
"oldest_clients": ["Firefox 31.3.0", "Android 4.4.2", "Chrome 49", "Edge 15 on Windows 10", "IE 11 on Windows 10", "Java 8u161", "OpenSSL 1.0.1l", "Opera 20", "Safari 9"],
"recommended_certificate_lifespan": 90,
"rsa_key_size": 2048,
"server_preferred_order": false,
"tls_curves": ["X25519MLKEM768", "X25519", "prime256v1", "secp384r1"],
"tls_versions": ["TLSv1.2", "TLSv1.3"]
}
}
}
9 changes: 9 additions & 0 deletions internal/shared/util/tlsprofiles/tlsprofiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,12 @@ func TestProfilesMapCompleteness(t *testing.T) {
}
require.GreaterOrEqual(t, len(profiles), 4, "profiles map must contain at least the required built-in profiles")
}

// TestNoSkippedCiphers verifies that all ciphers in mozilla_data.json are
// supported by Go's crypto/tls. If this fails, the JSON contains a cipher that
// needs to be handled — either it has been added to Go's crypto/tls, or it must
// be explicitly excluded from the profiles.
func TestNoSkippedCiphers(t *testing.T) {
require.Empty(t, skippedCiphers,
"cipher(s) in mozilla_data.json are not supported by Go's crypto/tls and were omitted: %v", skippedCiphers)
}
Loading