Skip to content

Commit

Permalink
Allow exporting EC keys to ECDH keys if asked explicitly
Browse files Browse the repository at this point in the history
  • Loading branch information
Daisuke Maki committed Jan 16, 2025
1 parent 583a77d commit 9f85924
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 48 deletions.
28 changes: 14 additions & 14 deletions jwk/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
74 changes: 69 additions & 5 deletions jwk/ecdsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {

Check failure on line 153 in jwk/ecdsa.go

View workflow job for this annotation

GitHub Actions / lint

empty-block: this block is empty, you can remove it (revive)
// 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()
Expand All @@ -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 {

Check failure on line 181 in jwk/ecdsa.go

View workflow job for this annotation

GitHub Actions / lint

empty-block: this block is empty, you can remove it (revive)
// 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()
Expand All @@ -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)
Expand Down
64 changes: 35 additions & 29 deletions jwk/jwk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"fmt"
Expand Down Expand Up @@ -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
})
}

0 comments on commit 9f85924

Please sign in to comment.