Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto/x509: Encode & decode ECDH only works for 1 out of 4 curves; P256, P384, P521 not working #71919

Open
justincranford opened this issue Feb 24, 2025 · 5 comments
Labels
BugReport Issues describing a possible bug in the Go implementation. NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.

Comments

@justincranford
Copy link

Go version

go version go1.24.0 windows/amd64

Output of go env in your module/workspace:

set AR=ar
set CC=gcc
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_ENABLED=1
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set CXX=g++
set GCCGO=gccgo
set GO111MODULE=on
set GOAMD64=v1
set GOARCH=amd64
set GOAUTH=netrc
set GOBIN=
set GOCACHE=C:\Users\justi\AppData\Local\go-build
set GOCACHEPROG=
set GODEBUG=
set GOENV=C:\Users\justi\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFIPS140=off
set GOFLAGS=
set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=E:/go-tmp/go-build2302615705=/tmp/go-build -gno-record-gcc-switches
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMOD=C:\Dev\Projects\certutil\go.mod
set GOMODCACHE=C:\Users\justi\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\justi\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:/Program Files/Go
set GOSUMDB=sum.golang.org
set GOTELEMETRY=local
set GOTELEMETRYDIR=C:\Users\justi\AppData\Roaming\go\telemetry
set GOTMPDIR=E:/go-tmp/
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.24.0
set GOWORK=
set PKG_CONFIG=pkg-config

What did you do?

https://go.dev/play/p/r0_VyaLBFeL

package main

import (
	"crypto/ecdh"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"github.com/stretchr/testify/assert"
	"testing"
)

var ecdhTestCurves = []struct {
	Name  string
	Curve ecdh.Curve
}{
	{"ECDH X25519", ecdh.X25519()}, // PASS
	{"ECDH P256", ecdh.P256()},     // FAIL => encode+decode returns ECDSA instead of ECDH
	{"ECDH P384", ecdh.P384()},     // FAIL => encode+decode returns ECDSA instead of ECDH
	{"ECDH P521", ecdh.P521()},     // FAIL => encode+decode returns ECDSA instead of ECDH
}

var ecdsaTestCurves = []struct {
	Name  string
	Curve elliptic.Curve
}{
	{"ECDSA P224", elliptic.P224()}, // PASS
	{"ECDSA P256", elliptic.P256()}, // PASS
	{"ECDSA P384", elliptic.P384()}, // PASS
	{"ECDSA P521", elliptic.P521()}, // PASS
}

func TestEncodeDecodeECDH(t *testing.T) {
	for _, curve := range ecdhTestCurves {
		t.Run(curve.Name, func(t *testing.T) {
			original, err := curve.Curve.GenerateKey(rand.Reader)
			if err != nil {
				t.Errorf("generate failed: %v", err)
			}
			assert.IsType(t, &ecdh.PrivateKey{}, original)

			decoded, err := pkcs8EncodeDecode(t, original)
			if err != nil {
				t.Errorf("generate failed: %v", err)
			}
			assert.IsType(t, &ecdh.PrivateKey{}, decoded)
		})
	}
}

func TestEncodeDecodeECDSA(t *testing.T) {
	for _, curve := range ecdsaTestCurves {
		t.Run(curve.Name, func(t *testing.T) {
			original, err := ecdsa.GenerateKey(curve.Curve, rand.Reader)
			if err != nil {
				t.Errorf("generate failed: %v", err)
			}
			assert.IsType(t, &ecdsa.PrivateKey{}, original)

			decoded, err := pkcs8EncodeDecode(t, original)
			if err != nil {
				t.Errorf("generate failed: %v", err)
			}
			assert.IsType(t, &ecdsa.PrivateKey{}, decoded)
		})
	}
}

func pkcs8EncodeDecode(t *testing.T, key any) (any, error) {
	encodedBytes, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return nil, fmt.Errorf("encode failed: %w", err)
	}

	pemBytes := pem.EncodeToMemory(&pem.Block{Bytes: encodedBytes, Type: "PRIVATE KEY"})
	t.Logf("PKCS#8 PEM of private Key :\n%s", string(pemBytes))

	decodedKey, err := x509.ParsePKCS8PrivateKey(encodedBytes)
	if err != nil {
		return nil, fmt.Errorf("decode failed: %w", err)
	}
	return decodedKey, nil
}

What did you see happen?

Output from go test

=== RUN   TestEncodeDecodeECDH
=== RUN   TestEncodeDecodeECDH/ECDH_P256
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgo4RiM5ZuvJpvXUgV
        +UNmMNueNE/QWXrR+XPDwHg8cn6hRANCAAQ+qZlrVShfxIMl2oi/Ppl0/cXqlWDF
        FQ0a76zezlbzkmGmrAneJFtdnjvlL8eRpiMW5jUXLWu/Hh3NZ9I9GnC4
        -----END PRIVATE KEY-----
    main_test.go:48: 
        	Error Trace:	C:/Dev/Projects/certutil/main_test.go:48
        	Error:      	Object expected to be of type *ecdh.PrivateKey, but was *ecdsa.PrivateKey
        	Test:       	TestEncodeDecodeECDH/ECDH_P256
--- FAIL: TestEncodeDecodeECDH/ECDH_P256 (0.00s)

=== RUN   TestEncodeDecodeECDH/ECDH_P384
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCFp1MYtEJ/ukwhBIwA
        E/MFg5IcPM9+RMW+uOQ1xbxgPFOrXebZ9S5PIgGsuW9d/KyhZANiAARi1hwRy81s
        GycYcTU7Fox4k5YfRSjKdgiv6n31FPc25YjUJ29f7FaJcWz66KoZUu6oOtLBIsqH
        UQXu0FH6BC/vjhtPgNEbspp1Vb/Fnv4SCeEN72+Qx8N/+EwNYiySUFs=
        -----END PRIVATE KEY-----
    main_test.go:48: 
        	Error Trace:	C:/Dev/Projects/certutil/main_test.go:48
        	Error:      	Object expected to be of type *ecdh.PrivateKey, but was *ecdsa.PrivateKey
        	Test:       	TestEncodeDecodeECDH/ECDH_P384
--- FAIL: TestEncodeDecodeECDH/ECDH_P384 (0.00s)

=== RUN   TestEncodeDecodeECDH/ECDH_P521
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIA6dsRuU6LciA5EaSa
        +iPx+7d6aqLTVkuvK6vxqnwVkkm5VVWzBq0F/uzVr6YQZsgqwiTNH2PtaMNKdcKV
        quz3EWyhgYkDgYYABACq7RCto7P19th0QmWZIsPHPMtbC+6jyQYwyOLGLY23GE+x
        Oko4eGdXFoo9J9NrNaFP60aiYeTW6eqTGYTanSFG7wCDiRwLSbTKDjQxmaPODdQJ
        V5F+pfSiJpPU+bLnhGhhM87UmaXkjB4/c+X2BI5Qig3dGKljtvfCKkGhmlVZGm0N
        9Q==
        -----END PRIVATE KEY-----
    main_test.go:48: 
        	Error Trace:	C:/Dev/Projects/certutil/main_test.go:48
        	Error:      	Object expected to be of type *ecdh.PrivateKey, but was *ecdsa.PrivateKey
        	Test:       	TestEncodeDecodeECDH/ECDH_P521
--- FAIL: TestEncodeDecodeECDH/ECDH_P521 (0.00s)

=== RUN   TestEncodeDecodeECDH/ECDH_X25519
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MC4CAQAwBQYDK2VuBCIEINPytT8SXA0LU6sFf1Tq5lmZ1y+ILmJWLM39wrpQ1C69
        -----END PRIVATE KEY-----
--- PASS: TestEncodeDecodeECDH/ECDH_X25519 (0.00s)
--- FAIL: TestEncodeDecodeECDH (0.01s)

=== RUN   TestEncodeDecodeECDSA
=== RUN   TestEncodeDecodeECDSA/ECDSA_P224
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MHgCAQAwEAYHKoZIzj0CAQYFK4EEACEEYTBfAgEBBBysCNNRbmymBwxp8STsHWWD
        5feP2R6mkINTRCa0oTwDOgAE7XfvGvo3XxXKri/t+9OMxO0Tikq6kJqQXMd5jriA
        H4rfB4AKG5a4nizu00LksaBLewm+JRV4Blg=
        -----END PRIVATE KEY-----
--- PASS: TestEncodeDecodeECDSA/ECDSA_P224 (0.00s)
=== RUN   TestEncodeDecodeECDSA/ECDSA_P256
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgmSI7x4hO8iJ/beIo
        KILJKF2b98JM7JW9Y3fGAeERkUehRANCAAT5yOjGMQ+hxiXM+2H20wplGph8ot4s
        oStxC5eYo9ykqR4odPBNkubdA7zgLf6EQhO/XCVoC37tT50gaBqNX2uJ
        -----END PRIVATE KEY-----
--- PASS: TestEncodeDecodeECDSA/ECDSA_P256 (0.00s)
=== RUN   TestEncodeDecodeECDSA/ECDSA_P384
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAaFk2jNn/rK5DTqEai
        hPkBuscpawBtNnOixaBaC53BG/1lUdhvRMR3WobuDWuYrWChZANiAAQYqmyJwnX3
        iHfAOtietZum8oo6PZ0Ebp0b0rFqfr7ow3kj8WfTUNCC42BsRMuugRw8XDUgCWQ1
        yfqqxgZskzfLfN/vrjmiq5FGTkSzkSKZV2tkqHf5v2udF6suPt5hNtI=
        -----END PRIVATE KEY-----
--- PASS: TestEncodeDecodeECDSA/ECDSA_P384 (0.00s)
=== RUN   TestEncodeDecodeECDSA/ECDSA_P521
    main_test.go:78: PKCS#8 PEM of private Key :
        -----BEGIN PRIVATE KEY-----
        MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIALlt69BM7wwuMKug1
        3cwXBYOPYskyoNe9UyIOHJqZd+DkitbhFcpho5aeNhoFMLt3ULzNrv51LVGlpgoN
        7pqX8HWhgYkDgYYABAEiSP7EXI5+8Ej4H/IflmyWTUnoDrArSldaGoI3GxQ4m4OJ
        yOBk6F2CbSXyHpGKXq5QtkJggNDuLL8XZ/hTsc1cNAHLfH6OYDgsQ+e4q4UmwK3n
        GKesededu41Gw+FU2L82ACt6AK8iOuqKoOeKSfOAaOldTQodpCvFFCenDtc7VMrX
        4A==
        -----END PRIVATE KEY-----
--- PASS: TestEncodeDecodeECDSA/ECDSA_P521 (0.00s)
--- PASS: TestEncodeDecodeECDSA (0.00s)
FAIL

Process finished with the exit code 1

What did you expect to see?

I was expecting all of the tests to pass. Each test passes an ECDH or ECDSA private key through x509.MarshalPKCS8PrivateKey and then x509.ParsePKCS8PrivateKey. The decoded private key is expected to be the same type as the original.

All 4 ECDSA tests pass. Only 1 ECDH test passes (i.e. X25519). The other 3 ECDH tests fail (i.e. P256, P384, P521). They pass in an ECDH private key, but the coded key is ECDSA instead of expected ECDH.

I log the PKCS#8 private keys as PEM in the tests. I copied the ECDH P256 PEM into an ASN.1 decoder (e.g. https://lapo.it/asn1js/). The expected OID is ECDH P256 (i.e. 1.3.132.1.12). Actual OID is ECDSA P256 (i.e. 1.2.840.10045.3.1.7), so it appears to be marshalled with the wrong OID. Here is a screenshot of the PEM from the ECDH P256 test, showing unexpected ECDSA OID.

Image

I debugged into x509.MarshalPKCS8PrivateKey. For the ECDH P256 test, it picks the wrong OID. Here is a link to Go x509.go source where ECDH marshall picks its OID (i.e. 1.2.840.10045.3.1.7).

go/src/crypto/x509/x509.go

Lines 530 to 535 in fba83cd

var (
oidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33}
oidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}
oidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34}
oidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35}
)

I think it is a marshalling bug. The ECDH and ECDSA OIDs are meant to be distinct. However, it looks like they OIDs are mixed together in the code.

@gabyhelp gabyhelp added the BugReport Issues describing a possible bug in the Go implementation. label Feb 24, 2025
@ianlancetaylor
Copy link
Member

CC @golang/security

@ianlancetaylor ianlancetaylor changed the title crypto/x509/x509.go: Encode & decode ECDH only works for 1 out of 4 curves; P256, P384, P521 not working crypto/x509: Encode & decode ECDH only works for 1 out of 4 curves; P256, P384, P521 not working Feb 24, 2025
@mateusz834
Copy link
Member

mateusz834 commented Feb 24, 2025

This is intentional, made so for backwards compatibility, as ECDH support for ParsePKCS8PrivateKey was added later.

go/src/crypto/ecdh/ecdh.go

Lines 95 to 100 in fba83cd

// PrivateKey is an ECDH private key, usually kept secret.
//
// These keys can be parsed with [crypto/x509.ParsePKCS8PrivateKey] and encoded
// with [crypto/x509.MarshalPKCS8PrivateKey]. For NIST curves, they then need to
// be converted with [crypto/ecdsa.PrivateKey.ECDH] after parsing.
type PrivateKey struct {

Possibly the docs of ParsePKCS8PrivateKey could be improved to mention that also.

@seankhliao
Copy link
Member

for context #56088

@FiloSottile
Copy link
Contributor

FiloSottile commented Feb 24, 2025

Looks like I was wrong in #56088 (comment) and didn't find the id-ecDH definition in RFC 5480, section 2.1.2.

Note that id-ecDH (1.3.132.1.12) is an alternative to id-ecPublicKey (1.2.840.10045.2.1), NOT to the namedCurve parameter (e.g. 1.2.840.10045.3.1.7 for P-256) which is used with both.

Supporting id-ecDH in ParsePKCS8PrivateKey should be straightforward. The question is whether we should switch MarshalPKCS8PrivateKey. For sure it would require a GODEBUG.

How commonly supported is id-ecDH? If we generate encodings that round-trip for us but won't work with anything else, that's not great.

What a mess. Given how sparsely supported id-ecDH is, I am leaning towards not supporting it and just documenting the roundtrip issue better in ParsePKCS8PrivateKey, and mention what OID we use in MarshalPKCS8PrivateKey.

@prattmic prattmic added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Feb 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BugReport Issues describing a possible bug in the Go implementation. NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Projects
None yet
Development

No branches or pull requests

7 participants