Skip to content

proposal: crypto/tls: add SupportedCurves to enumerate supported key exchange mechanisms #77712

@damdo

Description

@damdo

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:

  1. 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.

  2. 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.

  3. 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.

  4. The isTLS13OnlyKeyExchange and isPQKeyExchange functions already exist internally in common.go, so the implementation information is already tracked.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions