From 23f03de6b2a4d328510c025cd93bd48597cb11f6 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Thu, 16 Jan 2025 09:54:53 +0900 Subject: [PATCH] Allow exporting EC keys to ECDH keys if asked explicitly --- jwk/convert.go | 28 +++++++++---------- jwk/ecdsa.go | 74 +++++++++++++++++++++++++++++++++++++++++++++---- jwk/jwk_test.go | 64 +++++++++++++++++++++++------------------- 3 files changed, 118 insertions(+), 48 deletions(-) diff --git a/jwk/convert.go b/jwk/convert.go index 37531959..3b70771c 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -373,21 +373,21 @@ func Export(key Key, dst interface{}) error { muKeyExporters.RLock() exporters, ok := keyExporters[key.KeyType()] muKeyExporters.RUnlock() - if ok { - for _, conv := range exporters { - v, err := conv.Export(key, dst) - if err != nil { - if errors.Is(err, ContinueError()) { - continue - } - return fmt.Errorf(`jwk.Export: failed to export jwk.Key to raw format: %w`, err) - } - - if err := blackmagic.AssignIfCompatible(dst, v); err != nil { - return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err) + if !ok { + return fmt.Errorf(`jwk.Export: no exporters registered for key type '%T'`, key) + } + for _, conv := range exporters { + v, err := conv.Export(key, dst) + if err != nil { + if errors.Is(err, ContinueError()) { + continue } - return nil + return fmt.Errorf(`jwk.Export: failed to export jwk.Key to raw format: %w`, err) + } + if err := blackmagic.AssignIfCompatible(dst, v); err != nil { + return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err) } + return nil } - return fmt.Errorf(`jwk.Export: failed to find exporter for key type '%T'`, key) + return fmt.Errorf(`jwk.Export: no suitable exporter found for key type '%T'`, key) } diff --git a/jwk/ecdsa.go b/jwk/ecdsa.go index 9b4027a2..eb41ffd2 100644 --- a/jwk/ecdsa.go +++ b/jwk/ecdsa.go @@ -2,10 +2,12 @@ package jwk import ( "crypto" + "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" "fmt" "math/big" + "reflect" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/ecutil" @@ -102,13 +104,58 @@ func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ec return &ecdsa.PublicKey{Curve: crv, X: &x, Y: &y}, nil } +func buildECDHPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ecdh.PublicKey, error) { + var ecdhcrv ecdh.Curve + switch alg { + case jwa.X25519(): + ecdhcrv = ecdh.X25519() + case jwa.P256(): + ecdhcrv = ecdh.P256() + case jwa.P384(): + ecdhcrv = ecdh.P384() + case jwa.P521(): + ecdhcrv = ecdh.P521() + default: + return nil, fmt.Errorf(`jwk: unsupported ECDH curve %s`, alg) + } + + return ecdhcrv.NewPublicKey(append([]byte{0x04}, append(xbuf, ybuf...)...)) +} + +func buildECDHPrivateKey(alg jwa.EllipticCurveAlgorithm, dbuf []byte) (*ecdh.PrivateKey, error) { + var ecdhcrv ecdh.Curve + switch alg { + case jwa.X25519(): + ecdhcrv = ecdh.X25519() + case jwa.P256(): + ecdhcrv = ecdh.P256() + case jwa.P384(): + ecdhcrv = ecdh.P384() + case jwa.P521(): + ecdhcrv = ecdh.P521() + default: + return nil, fmt.Errorf(`jwk: unsupported ECDH curve %s`, alg) + } + + return ecdhcrv.NewPrivateKey(dbuf) +} + func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { + var isECDH bool switch k := keyif.(type) { case *ecdsaPublicKey: switch hint.(type) { - case ecdsa.PublicKey, *ecdsa.PublicKey, interface{}: + case ecdsa.PublicKey, *ecdsa.PublicKey: + case ecdh.PublicKey, *ecdh.PublicKey: + isECDH = true default: - return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError()) + rv := reflect.ValueOf(hint) + if rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Interface { + // pointer to an interface value, presumably they want us to dynamically + // create an object of the right type + } else { + return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError()) + } } k.mu.RLock() @@ -118,12 +165,25 @@ func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { if !ok { return nil, fmt.Errorf(`missing "crv" field`) } - return buildECDSAPublicKey(crv, k.x, k.y) + + if isECDH { + return buildECDHPublicKey(crv, k.x, k.y) + } else { + return buildECDSAPublicKey(crv, k.x, k.y) + } case *ecdsaPrivateKey: switch hint.(type) { - case ecdsa.PrivateKey, *ecdsa.PrivateKey, interface{}: + case ecdsa.PrivateKey, *ecdsa.PrivateKey: + case ecdh.PrivateKey, *ecdh.PrivateKey: + isECDH = true default: - return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError()) + rv := reflect.ValueOf(hint) + if rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Interface { + // pointer to an interface value, presumably they want us to dynamically + // create an object of the right type + } else { + return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError()) + } } k.mu.RLock() @@ -133,6 +193,10 @@ func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { if !ok { return nil, fmt.Errorf(`missing "crv" field`) } + + if isECDH { + return buildECDHPrivateKey(crv, k.d) + } pubk, err := buildECDSAPublicKey(crv, k.x, k.y) if err != nil { return nil, fmt.Errorf(`failed to build public key: %w`, err) diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go index c7d75a52..57286c44 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -7,7 +7,6 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" - "crypto/elliptic" "crypto/rand" "crypto/rsa" "fmt" @@ -2000,35 +1999,42 @@ func TestParse_fail(t *testing.T) { } func TestGH1262(t *testing.T) { - t.Run("x25519", func(t *testing.T) { - key, err := jwxtest.GenerateX25519Key() - require.NoError(t, err, `jwx.GenerateX25519Key should succeed`) + t.Run("Updated Example test", func(t *testing.T) { + keyCli, err := ecdh.P384().GenerateKey(rand.Reader) + require.NoError(t, err, `ecdh.P384().GenerateKey should succeed`) - imported, err := jwk.Import(key) + jwkCliPriv, err := jwk.Import(keyCli) require.NoError(t, err, `jwk.Import should succeed`) - require.Equal(t, imported.KeyType(), jwa.OKP(), `key type should be OKP`) - }) - t.Run("elliptic", func(t *testing.T) { - for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { - expectedAlg, err := ourecdsa.AlgorithmFromCurve(curve) - require.NoError(t, err, `ourecdsa.AlgorithmFromCurve should succeed`) - - var alg jwa.EllipticCurveAlgorithm - key1, err := ecdsa.GenerateKey(curve, rand.Reader) - require.NoError(t, err, `ecdsa.GenerateKey should succeed`) - pub1 := &key1.PublicKey - key2, err := key1.ECDH() - require.NoError(t, err, `key1.ECDH should succeed`) - pub2, err := pub1.ECDH() - require.NoError(t, err, `pub1.ECDH should succeed`) - - for _, key := range []any{key1, pub1, key2, pub2} { - imported, err := jwk.Import(key) - require.NoError(t, err, `jwk.Import should succeed`) - require.Equal(t, imported.KeyType(), jwa.EC(), `key type should be EC`) - require.NoError(t, imported.Get(jwk.ECDSACrvKey, &alg), `calling Get should succeed`) - require.Equal(t, alg, expectedAlg, `alg should be P384`) - } - } + _ = jwkCliPriv + + var rawCliPriv ecdh.PrivateKey + require.NoError(t, jwk.Export(jwkCliPriv, &rawCliPriv), `jwk.Export should succeed`) + + pubCli := keyCli.PublicKey() // server is able to retrieve the pub key part of client + + keySrv, err := ecdh.P384().GenerateKey(rand.Reader) + require.NoError(t, err, `ecdh.P384().GenerateKey should succeed`) + + jwkSrv, err := jwk.Import(keySrv.PublicKey()) + require.NoError(t, err, `jwk.Import should succeed`) + jwkBuf, err := json.Marshal(jwkSrv) + + require.NoError(t, err, `json.Marshal should succeed`) + + secretSrv, err := keySrv.ECDH(pubCli) + require.NoError(t, err, `keySrv.ECDH should succeed`) + + _ = secretSrv // doing some non-standard encryption & response with encrypted data + + // client + pubSrv := &ecdh.PublicKey{} + jwkCli, err := jwk.ParseKey(jwkBuf) // extract jwkBuf + require.NoError(t, err, `jwk.ParseKey should succeed`) + + require.NoError(t, jwk.Export(jwkCli, pubSrv), `jwk.Export should succeed`) + secretCli, err := keyCli.ECDH(pubSrv) + require.NoError(t, err, `keyCli.ECDH should succeed`) + + _ = secretCli // doing some non-standard encryption }) }