Skip to content

Commit 7805e8a

Browse files
lestrratDaisuke Maki
andauthored
[jwk] Change ecdh importer/exporter (#1315)
* Change key importer for ECDH keys * remove check for now (cleanup later) * Allow exporting EC keys to ECDH keys if asked explicitly * refuse to remove block * fix comment * Update Changes * Appease linter * fix error message --------- Co-authored-by: Daisuke Maki <lestrrat+github@github.com>
1 parent d0bb461 commit 7805e8a

File tree

4 files changed

+228
-21
lines changed

4 files changed

+228
-21
lines changed

Changes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Changes
44
v3 has many incompatibilities with v2. To see the full list of differences between
55
v2 and v3, please read the Changes-v3.md file (https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes-v3.md)
66

7+
v3.0.0-alpha3
8+
* [jwk] Importing/Exporting from jwk.Key to ecdh.PrivateKey/ecdh.PublicKey should now work. Previously these keys were not properly
9+
recognized by the exporter/importer. Note that keys that use X25519 and
10+
P256/P384/P521 behave differently: X25519 keys can only be exported to/imported from OKP keys, which P256/P384/P521 can be exported to either ecdsa or ecdh keys.
11+
712
v3.0.0-alpha2 25 Feb 2025
813
* Update to work with go1.24
914
* Update tests to work with latest latchset/jose

jwk/convert.go

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import (
44
"crypto/ecdh"
55
"crypto/ecdsa"
66
"crypto/ed25519"
7+
"crypto/elliptic"
78
"crypto/rsa"
89
"errors"
910
"fmt"
11+
"math/big"
1012
"reflect"
1113
"sync"
1214

1315
"github.com/lestrrat-go/blackmagic"
16+
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
1417
"github.com/lestrrat-go/jwx/v3/jwa"
1518
)
1619

@@ -119,20 +122,113 @@ func init() {
119122
}
120123
{
121124
f := KeyImportFunc(okpPrivateKeyToJWK)
122-
for _, k := range []interface{}{ed25519.PrivateKey(nil), ecdh.PrivateKey{}, &ecdh.PrivateKey{}} {
125+
for _, k := range []interface{}{ed25519.PrivateKey(nil)} {
126+
RegisterKeyImporter(k, f)
127+
}
128+
}
129+
{
130+
f := KeyImportFunc(ecdhPrivateKeyToJWK)
131+
for _, k := range []interface{}{ecdh.PrivateKey{}, &ecdh.PrivateKey{}} {
123132
RegisterKeyImporter(k, f)
124133
}
125134
}
126135
{
127136
f := KeyImportFunc(okpPublicKeyToJWK)
128-
for _, k := range []interface{}{ed25519.PublicKey(nil), ecdh.PublicKey{}, &ecdh.PublicKey{}} {
137+
for _, k := range []interface{}{ed25519.PublicKey(nil)} {
138+
RegisterKeyImporter(k, f)
139+
}
140+
}
141+
{
142+
f := KeyImportFunc(ecdhPublicKeyToJWK)
143+
for _, k := range []interface{}{ecdh.PublicKey{}, &ecdh.PublicKey{}} {
129144
RegisterKeyImporter(k, f)
130145
}
131146
}
132-
133147
RegisterKeyImporter([]byte(nil), KeyImportFunc(bytesToKey))
134148
}
135149

150+
func ecdhPrivateKeyToJWK(src interface{}) (Key, error) {
151+
var raw *ecdh.PrivateKey
152+
switch src := src.(type) {
153+
case *ecdh.PrivateKey:
154+
raw = src
155+
case ecdh.PrivateKey:
156+
raw = &src
157+
default:
158+
return nil, fmt.Errorf(`cannot convert key type '%T' to ECDH jwk.Key`, src)
159+
}
160+
161+
switch raw.Curve() {
162+
case ecdh.X25519():
163+
return okpPrivateKeyToJWK(raw)
164+
case ecdh.P256():
165+
return ecdhPrivateKeyToECJWK(raw, elliptic.P256())
166+
case ecdh.P384():
167+
return ecdhPrivateKeyToECJWK(raw, elliptic.P384())
168+
case ecdh.P521():
169+
return ecdhPrivateKeyToECJWK(raw, elliptic.P521())
170+
default:
171+
return nil, fmt.Errorf(`unsupported curve %s`, raw.Curve())
172+
}
173+
}
174+
175+
func ecdhPrivateKeyToECJWK(raw *ecdh.PrivateKey, crv elliptic.Curve) (Key, error) {
176+
pub := raw.PublicKey()
177+
rawpub := pub.Bytes()
178+
179+
size := ecutil.CalculateKeySize(crv)
180+
var x, y, d big.Int
181+
x.SetBytes(rawpub[1 : 1+size])
182+
y.SetBytes(rawpub[1+size:])
183+
d.SetBytes(raw.Bytes())
184+
185+
var ecdsaPriv ecdsa.PrivateKey
186+
ecdsaPriv.Curve = crv
187+
ecdsaPriv.D = &d
188+
ecdsaPriv.X = &x
189+
ecdsaPriv.Y = &y
190+
return ecdsaPrivateKeyToJWK(&ecdsaPriv)
191+
}
192+
193+
func ecdhPublicKeyToJWK(src interface{}) (Key, error) {
194+
var raw *ecdh.PublicKey
195+
switch src := src.(type) {
196+
case *ecdh.PublicKey:
197+
raw = src
198+
case ecdh.PublicKey:
199+
raw = &src
200+
default:
201+
return nil, fmt.Errorf(`cannot convert key type '%T' to ECDH jwk.Key`, src)
202+
}
203+
204+
switch raw.Curve() {
205+
case ecdh.X25519():
206+
return okpPublicKeyToJWK(raw)
207+
case ecdh.P256():
208+
return ecdhPublicKeyToECJWK(raw, elliptic.P256())
209+
case ecdh.P384():
210+
return ecdhPublicKeyToECJWK(raw, elliptic.P384())
211+
case ecdh.P521():
212+
return ecdhPublicKeyToECJWK(raw, elliptic.P521())
213+
default:
214+
return nil, fmt.Errorf(`unsupported curve %s`, raw.Curve())
215+
}
216+
}
217+
218+
func ecdhPublicKeyToECJWK(raw *ecdh.PublicKey, crv elliptic.Curve) (Key, error) {
219+
rawbytes := raw.Bytes()
220+
size := ecutil.CalculateKeySize(crv)
221+
var x, y big.Int
222+
223+
x.SetBytes(rawbytes[1 : 1+size])
224+
y.SetBytes(rawbytes[1+size:])
225+
var ecdsaPriv ecdsa.PublicKey
226+
ecdsaPriv.Curve = crv
227+
ecdsaPriv.X = &x
228+
ecdsaPriv.Y = &y
229+
return ecdsaPublicKeyToJWK(&ecdsaPriv)
230+
}
231+
136232
// These may seem a bit repetitive and redandunt, but the problem is that
137233
// each key type has its own Import method -- for example, Import(*ecdsa.PrivateKey)
138234
// vs Import(*rsa.PrivateKey), and therefore they can't just be bundled into
@@ -277,21 +373,21 @@ func Export(key Key, dst interface{}) error {
277373
muKeyExporters.RLock()
278374
exporters, ok := keyExporters[key.KeyType()]
279375
muKeyExporters.RUnlock()
280-
if ok {
281-
for _, conv := range exporters {
282-
v, err := conv.Export(key, dst)
283-
if err != nil {
284-
if errors.Is(err, ContinueError()) {
285-
continue
286-
}
287-
return fmt.Errorf(`jwk.Export: failed to export jwk.Key to raw format: %w`, err)
288-
}
289-
290-
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
291-
return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err)
376+
if !ok {
377+
return fmt.Errorf(`jwk.Export: no exporters registered for key type '%T'`, key)
378+
}
379+
for _, conv := range exporters {
380+
v, err := conv.Export(key, dst)
381+
if err != nil {
382+
if errors.Is(err, ContinueError()) {
383+
continue
292384
}
293-
return nil
385+
return fmt.Errorf(`jwk.Export: failed to export jwk.Key to raw format: %w`, err)
386+
}
387+
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
388+
return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err)
294389
}
390+
return nil
295391
}
296-
return fmt.Errorf(`jwk.Export: failed to find exporter for key type '%T'`, key)
392+
return fmt.Errorf(`jwk.Export: no suitable exporter found for key type '%T'`, key)
297393
}

jwk/ecdsa.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package jwk
22

33
import (
44
"crypto"
5+
"crypto/ecdh"
56
"crypto/ecdsa"
67
"crypto/elliptic"
78
"fmt"
89
"math/big"
10+
"reflect"
911

1012
"github.com/lestrrat-go/jwx/v3/internal/base64"
1113
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
@@ -102,13 +104,59 @@ func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ec
102104
return &ecdsa.PublicKey{Curve: crv, X: &x, Y: &y}, nil
103105
}
104106

107+
func buildECDHPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ecdh.PublicKey, error) {
108+
var ecdhcrv ecdh.Curve
109+
switch alg {
110+
case jwa.X25519():
111+
ecdhcrv = ecdh.X25519()
112+
case jwa.P256():
113+
ecdhcrv = ecdh.P256()
114+
case jwa.P384():
115+
ecdhcrv = ecdh.P384()
116+
case jwa.P521():
117+
ecdhcrv = ecdh.P521()
118+
default:
119+
return nil, fmt.Errorf(`jwk: unsupported ECDH curve %s`, alg)
120+
}
121+
122+
return ecdhcrv.NewPublicKey(append([]byte{0x04}, append(xbuf, ybuf...)...))
123+
}
124+
125+
func buildECDHPrivateKey(alg jwa.EllipticCurveAlgorithm, dbuf []byte) (*ecdh.PrivateKey, error) {
126+
var ecdhcrv ecdh.Curve
127+
switch alg {
128+
case jwa.X25519():
129+
ecdhcrv = ecdh.X25519()
130+
case jwa.P256():
131+
ecdhcrv = ecdh.P256()
132+
case jwa.P384():
133+
ecdhcrv = ecdh.P384()
134+
case jwa.P521():
135+
ecdhcrv = ecdh.P521()
136+
default:
137+
return nil, fmt.Errorf(`jwk: unsupported ECDH curve %s`, alg)
138+
}
139+
140+
return ecdhcrv.NewPrivateKey(dbuf)
141+
}
142+
105143
func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) {
144+
var isECDH bool
106145
switch k := keyif.(type) {
107146
case *ecdsaPublicKey:
108147
switch hint.(type) {
109-
case ecdsa.PublicKey, *ecdsa.PublicKey, interface{}:
148+
case ecdsa.PublicKey, *ecdsa.PublicKey:
149+
case ecdh.PublicKey, *ecdh.PublicKey:
150+
isECDH = true
110151
default:
111-
return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError())
152+
rv := reflect.ValueOf(hint)
153+
//nolint:revive
154+
if rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Interface {
155+
// pointer to an interface value, presumably they want us to dynamically
156+
// create an object of the right type
157+
} else {
158+
return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError())
159+
}
112160
}
113161

114162
k.mu.RLock()
@@ -118,12 +166,25 @@ func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) {
118166
if !ok {
119167
return nil, fmt.Errorf(`missing "crv" field`)
120168
}
169+
170+
if isECDH {
171+
return buildECDHPublicKey(crv, k.x, k.y)
172+
}
121173
return buildECDSAPublicKey(crv, k.x, k.y)
122174
case *ecdsaPrivateKey:
123175
switch hint.(type) {
124-
case ecdsa.PrivateKey, *ecdsa.PrivateKey, interface{}:
176+
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
177+
case ecdh.PrivateKey, *ecdh.PrivateKey:
178+
isECDH = true
125179
default:
126-
return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError())
180+
rv := reflect.ValueOf(hint)
181+
//nolint:revive
182+
if rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Interface {
183+
// pointer to an interface value, presumably they want us to dynamically
184+
// create an object of the right type
185+
} else {
186+
return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError())
187+
}
127188
}
128189

129190
k.mu.RLock()
@@ -133,6 +194,10 @@ func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) {
133194
if !ok {
134195
return nil, fmt.Errorf(`missing "crv" field`)
135196
}
197+
198+
if isECDH {
199+
return buildECDHPrivateKey(crv, k.d)
200+
}
136201
pubk, err := buildECDSAPublicKey(crv, k.x, k.y)
137202
if err != nil {
138203
return nil, fmt.Errorf(`failed to build public key: %w`, err)

jwk/jwk_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2004,3 +2004,44 @@ func TestParse_fail(t *testing.T) {
20042004
})
20052005
})
20062006
}
2007+
2008+
func TestGH1262(t *testing.T) {
2009+
t.Run("Updated Example test", func(t *testing.T) {
2010+
keyCli, err := ecdh.P384().GenerateKey(rand.Reader)
2011+
require.NoError(t, err, `ecdh.P384().GenerateKey should succeed`)
2012+
2013+
jwkCliPriv, err := jwk.Import(keyCli)
2014+
require.NoError(t, err, `jwk.Import should succeed`)
2015+
_ = jwkCliPriv
2016+
2017+
var rawCliPriv ecdh.PrivateKey
2018+
require.NoError(t, jwk.Export(jwkCliPriv, &rawCliPriv), `jwk.Export should succeed`)
2019+
2020+
pubCli := keyCli.PublicKey() // server is able to retrieve the pub key part of client
2021+
2022+
keySrv, err := ecdh.P384().GenerateKey(rand.Reader)
2023+
require.NoError(t, err, `ecdh.P384().GenerateKey should succeed`)
2024+
2025+
jwkSrv, err := jwk.Import(keySrv.PublicKey())
2026+
require.NoError(t, err, `jwk.Import should succeed`)
2027+
jwkBuf, err := json.Marshal(jwkSrv)
2028+
2029+
require.NoError(t, err, `json.Marshal should succeed`)
2030+
2031+
secretSrv, err := keySrv.ECDH(pubCli)
2032+
require.NoError(t, err, `keySrv.ECDH should succeed`)
2033+
2034+
_ = secretSrv // doing some non-standard encryption & response with encrypted data
2035+
2036+
// client
2037+
pubSrv := &ecdh.PublicKey{}
2038+
jwkCli, err := jwk.ParseKey(jwkBuf) // extract jwkBuf
2039+
require.NoError(t, err, `jwk.ParseKey should succeed`)
2040+
2041+
require.NoError(t, jwk.Export(jwkCli, pubSrv), `jwk.Export should succeed`)
2042+
secretCli, err := keyCli.ECDH(pubSrv)
2043+
require.NoError(t, err, `keyCli.ECDH should succeed`)
2044+
2045+
_ = secretCli // doing some non-standard encryption
2046+
})
2047+
}

0 commit comments

Comments
 (0)