Skip to content

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

Open
@justincranford

Description

@justincranford

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsDecisionFeedback is required from experts, contributors, and/or the community before a change can be made.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions