Proposal Details
Background
crypto/tls provides CipherSuites() and InsecureCipherSuites() to let applications enumerate the cipher suites implemented by the package.
This makes it possible to build a name-to-ID map dynamically:
for _, suite := range tls.CipherSuites() {
ciphers[suite.Name] = suite.ID
}
At the moment there is no equivalent for key exchange mechanisms (Curves).
Applications that need to map user-facing names to CurveID values, for example, to populate Config.CurvePreferences from a configuration file or command-line flag, must maintain a hardcoded map like:
var curvePreferences = map[string]tls.CurveID{
"secp256r1": tls.CurveP256,
"secp384r1": tls.CurveP384,
"secp521r1": tls.CurveP521,
"X25519": tls.X25519,
"X25519MLKEM768": tls.X25519MLKEM768,
}
These consumer-facing maps are going stale every time Go adds a new key exchange mechanism.
For example, Go 1.24 added X25519MLKEM768, and Go 1.26 added SecP256r1MLKEM768 and SecP384r1MLKEM1024.
Each addition requires every downstream consumer to update their map, and the rate of change is about to increase significantly with the post-quantum transition. This is the same realm of problems that motivated proposal: crypto/tls: Expose maps for cipher suite IDs/names.
Real-world projects affected for example Kubernetes (my community), and likely any TLS-terminating proxy or server that want to expose curve configuration.
For this reason I'd like to suggest a way to mitigate this.
The following is just one example of doing this, and I'm not really married to any one approach.
So I'd be very happy to take on feedback and adjust this to fit Go's needs :)
Thanks!
Proposal
Add a new exported type and function:
// A CurvePreference is a key exchange mechanism implemented by this package.
// The name refers to elliptic curves for legacy reasons, see [CurveID].
type CurvePreference struct {
// ID is the [CurveID] for this key exchange mechanism.
ID CurveID
// Name is the standard name for this key exchange mechanism, from the
// IANA TLS Supported Groups registry.
// See https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8.
Name string
// SupportedVersions is the list of TLS protocol versions that can
// use this key exchange mechanism.
SupportedVersions []uint16
}
// SupportedCurves returns a list of key exchange mechanisms currently
// implemented by this package.
//
// The list is sorted by ID. Note that the default key exchange mechanisms
// selected by this package might depend on logic that can't be captured by
// a static list, and might not match those returned by this function.
func SupportedCurves() []*CurvePreference
The initial return value of SupportedCurves() would be covering the currently supported Curves:
| ID |
Name |
SupportedVersions |
| 23 |
secp256r1 |
TLS 1.0–1.3 |
| 24 |
secp384r1 |
TLS 1.0–1.3 |
| 25 |
secp521r1 |
TLS 1.0–1.3 |
| 29 |
X25519 |
TLS 1.0–1.3 |
| 4587 |
SecP256r1MLKEM768 |
TLS 1.3 |
| 4588 |
X25519MLKEM768 |
TLS 1.3 |
| 4589 |
SecP384r1MLKEM1024 |
TLS 1.3 |
With this API, applications can build name-to-ID maps automatically:
curves := map[string]tls.CurveID{}
for _, c := range tls.SupportedCurves() {
curves[strings.ToLower(c.Name)] = c.ID
}
Name field
The Name field uses the IANA "TLS Supported Groups" registry names, consistent with how CipherSuite.Name uses the IANA cipher suite names.
For the classic NIST curves this means secp256r1, secp384r1, secp521r1 rather than the Go constant names CurveP256, CurveP384, CurveP521.
The Go constant names remain available via CurveID.String().
Naming
I'm proposing SupportedCurves to match the existing ClientHelloInfo.SupportedCurves field and avoid collision with the Config.CurvePreferences field.
SupportedGroups would also be reasonable given the TLS 1.3 rename, though it would be inconsistent with the rest of the package's exported API which still uses the "Curve" terminology.
Rationale
The alternative is for every application that exposes curve configuration to maintain its own hardcoded map, which creates a rolling maintenance burden as Go adds support for new key exchanges.
This burden is increasing: Go went from 4 to 7 supported key exchanges in two releases (1.24 and 1.26), driven by the post-quantum transition.
CurveID.String() is not a substitute because: (1) it doesn't provide enumeration (you can't discover which values are valid),
and (2) it returns Go constant names (CurveP256) rather than the IANA standard names (secp256r1) that most applications and specifications use.
Compatibility
This is a purely additive API, so there should be no compatibility concerns
Implementation
The implementation could closely mirror CipherSuites() in cipher_suites.go.
/cc @FiloSottile @golang/security @golang/proposal-review
--
A few notes on choices I made in drafting this:
-
SupportedCurves vs CurvePreferences: I went with SupportedCurves to avoid name collision with Config.CurvePreferences. It is also coherent with ClientHelloInfo.SupportedCurves. That said, if the team prefers CurvePreferences() to match the field name, the return type could be renamed to CurvePreferenceInfo or similar to distinguish it.
-
IANA names in Name: CipherSuite.Name uses IANA names (e.g. TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), so curves should probably do it too. For the classic NIST curves this means secp256r1 rather than CurveP256.
-
Struct vs bare []CurveID: A struct with Name is more useful than just returning []CurveID, because the whole point is letting applications build name-to-ID maps without hardcoding. The SupportedVersions field felt like natural addition following the CipherSuite implementation, and useful for filtering TLS 1.3-only key exchanges.
-
The isTLS13OnlyKeyExchange and isPQKeyExchange functions already exist internally in common.go, so the implementation information is already tracked.
Proposal Details
Background
crypto/tlsprovidesCipherSuites()andInsecureCipherSuites()to let applications enumerate the cipher suites implemented by the package.This makes it possible to build a name-to-ID map dynamically:
At the moment there is no equivalent for key exchange mechanisms (Curves).
Applications that need to map user-facing names to
CurveIDvalues, for example, to populateConfig.CurvePreferencesfrom a configuration file or command-line flag, must maintain a hardcoded map like:These consumer-facing maps are going stale every time Go adds a new key exchange mechanism.
For example, Go 1.24 added
X25519MLKEM768, and Go 1.26 addedSecP256r1MLKEM768andSecP384r1MLKEM1024.Each addition requires every downstream consumer to update their map, and the rate of change is about to increase significantly with the post-quantum transition. This is the same realm of problems that motivated proposal: crypto/tls: Expose maps for cipher suite IDs/names.
Real-world projects affected for example Kubernetes (my community), and likely any TLS-terminating proxy or server that want to expose curve configuration.
For this reason I'd like to suggest a way to mitigate this.
The following is just one example of doing this, and I'm not really married to any one approach.
So I'd be very happy to take on feedback and adjust this to fit Go's needs :)
Thanks!
Proposal
Add a new exported type and function:
The initial return value of
SupportedCurves()would be covering the currently supported Curves:secp256r1secp384r1secp521r1X25519SecP256r1MLKEM768X25519MLKEM768SecP384r1MLKEM1024With this API, applications can build name-to-ID maps automatically:
Name field
The
Namefield uses the IANA "TLS Supported Groups" registry names, consistent with howCipherSuite.Nameuses the IANA cipher suite names.For the classic NIST curves this means
secp256r1,secp384r1,secp521r1rather than the Go constant namesCurveP256,CurveP384,CurveP521.The Go constant names remain available via
CurveID.String().Naming
I'm proposing
SupportedCurvesto match the existingClientHelloInfo.SupportedCurvesfield and avoid collision with theConfig.CurvePreferencesfield.SupportedGroupswould also be reasonable given the TLS 1.3 rename, though it would be inconsistent with the rest of the package's exported API which still uses the "Curve" terminology.Rationale
The alternative is for every application that exposes curve configuration to maintain its own hardcoded map, which creates a rolling maintenance burden as Go adds support for new key exchanges.
This burden is increasing: Go went from 4 to 7 supported key exchanges in two releases (1.24 and 1.26), driven by the post-quantum transition.
CurveID.String()is not a substitute because: (1) it doesn't provide enumeration (you can't discover which values are valid),and (2) it returns Go constant names (
CurveP256) rather than the IANA standard names (secp256r1) that most applications and specifications use.Compatibility
This is a purely additive API, so there should be no compatibility concerns
Implementation
The implementation could closely mirror
CipherSuites()incipher_suites.go./cc @FiloSottile @golang/security @golang/proposal-review
--
A few notes on choices I made in drafting this:
SupportedCurvesvsCurvePreferences: I went withSupportedCurvesto avoid name collision withConfig.CurvePreferences. It is also coherent withClientHelloInfo.SupportedCurves. That said, if the team prefersCurvePreferences()to match the field name, the return type could be renamed toCurvePreferenceInfoor similar to distinguish it.IANA names in
Name:CipherSuite.Nameuses IANA names (e.g.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), so curves should probably do it too. For the classic NIST curves this meanssecp256r1rather thanCurveP256.Struct vs bare
[]CurveID: A struct withNameis more useful than just returning[]CurveID, because the whole point is letting applications build name-to-ID maps without hardcoding. TheSupportedVersionsfield felt like natural addition following theCipherSuiteimplementation, and useful for filtering TLS 1.3-only key exchanges.The
isTLS13OnlyKeyExchangeandisPQKeyExchangefunctions already exist internally incommon.go, so the implementation information is already tracked.