From 24daab5c801a77b3bc55295aaba9b7f5c15ee454 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Wed, 8 Nov 2023 09:44:11 +0900 Subject: [PATCH 01/21] Add an example adding new EC algo and key type --- examples/go.mod | 1 + examples/go.sum | 38 +++++++++ .../jwx_register_ec_and_key_example_test.go | 84 +++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 examples/jwx_register_ec_and_key_example_test.go diff --git a/examples/go.mod b/examples/go.mod index 5d2ebcaf4..b63da8890 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/cloudflare/circl v1.3.3 + github.com/emmansun/gmsm v0.21.5 github.com/lestrrat-go/jwx/v3 v3.0.0 ) diff --git a/examples/go.sum b/examples/go.sum index db79a400b..52f1c9caf 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/emmansun/gmsm v0.21.5 h1:G4HwuiqNQGZmAlZi233iwDPcfWKcoax0/GzS3eR+l7o= +github.com/emmansun/gmsm v0.21.5/go.mod h1:5hRB+YZ3dy/llu3dcKyBHieRe5Z2V6sqvNJOWEsIcqQ= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= @@ -24,10 +26,46 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go new file mode 100644 index 000000000..5d7814abf --- /dev/null +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -0,0 +1,84 @@ +package examples_test + +import ( + "bytes" + "crypto/rand" + "fmt" + + "github.com/emmansun/gmsm/sm2" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + ourecdsa "github.com/lestrrat-go/jwx/v3/jwk/ecdsa" + "github.com/lestrrat-go/jwx/v3/jws" +) + +// Setup. This is something that you probably should do in your adapter +// library, or in your application's init() function. + +// I could not readily find what the exact curve notation is for ShangMi SM2 +// (either I'm just bad at researching or it's not in an RFC as of this writing) +// so I'm faking it as "SM2". +// +// For demonstration purposes, it could as well be a random string, as long +// as its consistent in your usage. +const SM2 jwa.EllipticCurveAlgorithm = "SM2" + +func init() { + shangmi2pk, _ := sm2.GenerateKey(rand.Reader) + + // Register the algorithm name so it can be looked up + jwa.RegisterEllipticCurveAlgorithm(SM2) + + // Register the actual ECDSA curve. Notice that we need to tell this + // to our jwk library, so that the JWK lookup can be done properly + // when a raw SM2 key is passed to various key operations. + ourecdsa.RegisterCurve(SM2, sm2.P256()) + + // We only need one converter for the private key, because the public key + // is exactly the same type as *ecdsa.PublicKey + jwk.RegisterKeyConverter(shangmi2pk, jwk.KeyConvertFunc(convertShangMiSm2)) +} + +func convertShangMiSm2(key interface{}) (jwk.Key, error) { + shangmi2pk, ok := key.(*sm2.PrivateKey) + if !ok { + return nil, fmt.Errorf("invalid SM2 private key") + } + return jwk.FromRaw(shangmi2pk.PrivateKey) +} + +// End setup + +func ExampleShangMiSm2() { + shangmi2pk, _ := sm2.GenerateKey(rand.Reader) + shangmi2JWK, err := jwk.FromRaw(shangmi2pk) + if err != nil { + fmt.Println(err) + return + } + + payload := []byte("Lorem ipsum") + signed, err := jws.Sign(payload, jws.WithKey(jwa.ES256, shangmi2JWK)) + if err != nil { + fmt.Println(err) + return + } + + shangmi2PubJWK, err := jwk.PublicKeyOf(shangmi2JWK) + if err != nil { + fmt.Println(err) + return + } + + verified, err := jws.Verify(signed, jws.WithKey(jwa.ES256, shangmi2PubJWK)) + if err != nil { + fmt.Println(err) + return + } + + if !bytes.Equal(payload, verified) { + fmt.Println("payload does not match") + return + } + //OUTPUT: +} From 1a4e1ed762927466bb69623bbad139e708349471 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Thu, 16 Nov 2023 15:12:19 +0900 Subject: [PATCH 02/21] Add failing example --- examples/jwx_register_ec_and_key_example_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index 5d7814abf..74d013ada 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -51,12 +51,23 @@ func convertShangMiSm2(key interface{}) (jwk.Key, error) { func ExampleShangMiSm2() { shangmi2pk, _ := sm2.GenerateKey(rand.Reader) + + // Create a jwk.Key from ShangMi SM2 private key shangmi2JWK, err := jwk.FromRaw(shangmi2pk) if err != nil { fmt.Println(err) return } + { + // Create a ShangMi SM2 private key back from the jwk.Key + var clone sm2.PrivateKey + if err := shangmi2JWK.Raw(&clone); err != nil { + fmt.Println(err) + return + } + } + payload := []byte("Lorem ipsum") signed, err := jws.Sign(payload, jws.WithKey(jwa.ES256, shangmi2JWK)) if err != nil { From ef40deef9508955eece3afa6a9d2fcf3eda19ac2 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Sat, 18 Nov 2023 19:27:33 +0900 Subject: [PATCH 03/21] Kind of fix, but still need to review spec --- .../jwx_register_ec_and_key_example_test.go | 75 +++++++++- jwk/convert.go | 134 ++++++++++++++---- jwk/ecdsa.go | 52 +++++-- jwk/interface_gen.go | 9 +- jwk/jwk.go | 6 +- tools/cmd/genjwk/main.go | 9 +- 6 files changed, 234 insertions(+), 51 deletions(-) diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index 74d013ada..b40aa1b7c 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -2,8 +2,11 @@ package examples_test import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "fmt" + "math/big" "github.com/emmansun/gmsm/sm2" "github.com/lestrrat-go/jwx/v3/jwa" @@ -36,7 +39,9 @@ func init() { // We only need one converter for the private key, because the public key // is exactly the same type as *ecdsa.PublicKey - jwk.RegisterKeyConverter(shangmi2pk, jwk.KeyConvertFunc(convertShangMiSm2)) + jwk.RegisterRJKeyConverter(shangmi2pk, jwk.RJKeyConvertFunc(convertShangMiSm2)) + + jwk.RegisterJRKeyConverter(jwa.EC, jwk.JRKeyConvertFunc(convertJWKToShangMiSm2)) } func convertShangMiSm2(key interface{}) (jwk.Key, error) { @@ -47,6 +52,26 @@ func convertShangMiSm2(key interface{}) (jwk.Key, error) { return jwk.FromRaw(shangmi2pk.PrivateKey) } +func convertJWKToShangMiSm2(key jwk.Key, hint interface{}) (interface{}, error) { + ecdsaKey := key.(jwk.ECDSAPrivateKey) + if ecdsaKey.Crv() != SM2 { + return nil, fmt.Errorf(`cannot convert curve of type %s to ShangMi key: %w`, ecdsaKey.Crv(), jwk.ContinueError()) + } + + switch hint.(type) { + case *sm2.PrivateKey, *interface{}: + default: + return nil, fmt.Errorf(`can only convert SM2 key to *sm2.PrivateKey (got %T): %w`, hint, jwk.ContinueError()) + } + + var ret sm2.PrivateKey + ret.PublicKey.Curve = sm2.P256() + ret.D = (&big.Int{}).SetBytes(ecdsaKey.D()) + ret.PublicKey.X = (&big.Int{}).SetBytes(ecdsaKey.X()) + ret.PublicKey.Y = (&big.Int{}).SetBytes(ecdsaKey.Y()) + return &ret, nil +} + // End setup func ExampleShangMiSm2() { @@ -66,6 +91,54 @@ func ExampleShangMiSm2() { fmt.Println(err) return } + + // Clone should have same Crv, D, X, and Y values + if clone.Curve != shangmi2pk.Curve { + fmt.Println("curve does not match") + return + } + + if clone.D.Cmp(shangmi2pk.D) != 0 { + fmt.Println("D does not match") + return + } + + if clone.X.Cmp(shangmi2pk.X) != 0 { + fmt.Println("X does not match") + return + } + + if clone.Y.Cmp(shangmi2pk.Y) != 0 { + fmt.Println("Y does not match") + return + } + } + + { // Can do the same thing for interface{} + var clone interface{} + if err := shangmi2JWK.Raw(&clone); err != nil { + fmt.Println(err) + return + } + } + + { + // Of course, ecdsa.PrivateKeys are also supported separately + ecprivkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + fmt.Println(err) + return + } + eckjwk, err := jwk.FromRaw(ecprivkey) + if err != nil { + fmt.Println(err) + return + } + var clone ecdsa.PrivateKey + if err := eckjwk.Raw(&clone); err != nil { + fmt.Println(err) + return + } } payload := []byte("Lorem ipsum") diff --git a/jwk/convert.go b/jwk/convert.go index 3c71f3f32..3677d1c79 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -8,73 +8,119 @@ import ( "fmt" "reflect" "sync" + + "github.com/lestrrat-go/blackmagic" + "github.com/lestrrat-go/jwx/v3/jwa" ) -// # Converting Raw Keys To `jwk.Key`s +// # Converting between Raw Keys and `jwk.Key`s // -// You can register a convert from a raw key to a `jwk.Key` by calling -// `jwk.RegisterKeyConverter`. +// A converter that converts from a raw key to a `jwk.Key` is called a RJKeyConverter. +// A converter that converts from a `jwk.Key` to a raw key is called a JRKeyConverter. // +// You can register a convert from a raw key to a `jwk.Key` by calling +// `jwk.RegisterRJKeyConverter`. -var keyConverters = make(map[reflect.Type]KeyConverter) +var rjConverters = make(map[reflect.Type]RJKeyConverter) +var jrConverters = make(map[jwa.KeyType][]JRKeyConverter) -var muKeyConverters sync.RWMutex +var muRJConverters sync.RWMutex +var muJRConverters sync.RWMutex -func RegisterKeyConverter(from interface{}, conv KeyConverter) { - muKeyConverters.Lock() - defer muKeyConverters.Unlock() - keyConverters[reflect.TypeOf(from)] = conv +func RegisterRJKeyConverter(from interface{}, conv RJKeyConverter) { + muRJConverters.Lock() + defer muRJConverters.Unlock() + rjConverters[reflect.TypeOf(from)] = conv } -type KeyConverter interface { +func RegisterJRKeyConverter(kty jwa.KeyType, conv JRKeyConverter) { + muJRConverters.Lock() + defer muJRConverters.Unlock() + convs, ok := jrConverters[kty] + if !ok { + convs = []JRKeyConverter{conv} + } else { + convs = append([]JRKeyConverter{conv}, convs...) + } + jrConverters[kty] = convs +} + +type RJKeyConverter interface { FromRaw(interface{}) (Key, error) } -type KeyConvertFunc func(interface{}) (Key, error) +type RJKeyConvertFunc func(interface{}) (Key, error) -func (f KeyConvertFunc) FromRaw(raw interface{}) (Key, error) { +func (f RJKeyConvertFunc) FromRaw(raw interface{}) (Key, error) { return f(raw) } +// JRKeyConverter is used to convert from a `jwk.Key` to a raw key. +type JRKeyConverter interface { + // Raw takes the `jwk.Key` to be converted, and a hint (the raw key to be converted to). + // The hint is the object that the user requested the result to be assigned to. + // The method should return the converted raw key, or an error if the conversion fails. + // + // Third party modules MUST NOT create raw + // + // When the user calls `key.Raw(dst)`, the `dst` object is a _pointer_ to the + // object that the user wants the result to be assigned to, but the converter + // receives the _value_ that this pointer points to, to make it easier to + // detect the type of the result. + // + // Note that the the second argument may be an `interface{}` (which means that the + // user has delegated the type detection to the converter). + // + // Raw must NOT modify the hint object, and should return jwk.ContinueError + // if the hint object is not compatible with the converter. + Raw(Key, interface{}) (interface{}, error) +} + +type JRKeyConvertFunc func(Key, interface{}) (interface{}, error) + +func (f JRKeyConvertFunc) Raw(key Key, hint interface{}) (interface{}, error) { + return f(key, hint) +} + func init() { { - f := KeyConvertFunc(rsaPrivateKeyToJWK) + f := RJKeyConvertFunc(rsaPrivateKeyToJWK) k := rsa.PrivateKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterRJKeyConverter(k, f) + RegisterRJKeyConverter(&k, f) } { - f := KeyConvertFunc(rsaPublicKeyToJWK) + f := RJKeyConvertFunc(rsaPublicKeyToJWK) k := rsa.PublicKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterRJKeyConverter(k, f) + RegisterRJKeyConverter(&k, f) } { - f := KeyConvertFunc(ecdsaPrivateKeyToJWK) + f := RJKeyConvertFunc(ecdsaPrivateKeyToJWK) k := ecdsa.PrivateKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterRJKeyConverter(k, f) + RegisterRJKeyConverter(&k, f) } { - f := KeyConvertFunc(ecdsaPublicKeyToJWK) + f := RJKeyConvertFunc(ecdsaPublicKeyToJWK) k := ecdsa.PublicKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterRJKeyConverter(k, f) + RegisterRJKeyConverter(&k, f) } { - f := KeyConvertFunc(okpPrivateKeyToJWK) + f := RJKeyConvertFunc(okpPrivateKeyToJWK) for _, k := range []interface{}{ed25519.PrivateKey(nil), ecdh.PrivateKey{}, &ecdh.PrivateKey{}} { - RegisterKeyConverter(k, f) + RegisterRJKeyConverter(k, f) } } { - f := KeyConvertFunc(okpPublicKeyToJWK) + f := RJKeyConvertFunc(okpPublicKeyToJWK) for _, k := range []interface{}{ed25519.PublicKey(nil), ecdh.PublicKey{}, &ecdh.PublicKey{}} { - RegisterKeyConverter(k, f) + RegisterRJKeyConverter(k, f) } } - RegisterKeyConverter([]byte(nil), KeyConvertFunc(bytesToKey)) + RegisterRJKeyConverter([]byte(nil), RJKeyConvertFunc(bytesToKey)) } // These may seem a bit repetitive and redandunt, but the problem is that @@ -198,3 +244,33 @@ func bytesToKey(src interface{}) (Key, error) { } return k, nil } + +// All objects call this method to convert themselves to a raw key. +// It's done this way to centralize the logic (mapping) of which keys are converted +// to what raw key. +func raw(key Key, dst interface{}) error { + muRJConverters.RLock() + defer muRJConverters.RUnlock() + // dst better be a pointer + rv := reflect.ValueOf(dst) + if rv.Kind() != reflect.Ptr { + return fmt.Errorf(`destination object must be a pointer`) + } + if convs, ok := jrConverters[key.KeyType()]; ok { + for _, conv := range convs { + v, err := conv.Raw(key, dst) + if err != nil { + if IsContinueError(err) { + continue + } + return fmt.Errorf(`failed to convert jwk.Key to raw format: %w`, err) + } + + if err := blackmagic.AssignIfCompatible(dst, v); err != nil { + return fmt.Errorf(`failed to assign key: %w`, err) + } + return nil + } + } + return fmt.Errorf(`failed to find converter for key type '%T'`, key) +} diff --git a/jwk/ecdsa.go b/jwk/ecdsa.go index fdf6e3363..c061b7117 100644 --- a/jwk/ecdsa.go +++ b/jwk/ecdsa.go @@ -18,6 +18,8 @@ func init() { ourecdsa.RegisterCurve(jwa.P256, elliptic.P256()) ourecdsa.RegisterCurve(jwa.P384, elliptic.P384()) ourecdsa.RegisterCurve(jwa.P521, elliptic.P521()) + + RegisterJRKeyConverter(jwa.EC, JRKeyConvertFunc(ecdsaPrivateJWKToRaw)) } func (k *ecdsaPublicKey) FromRaw(rawKey *ecdsa.PublicKey) error { @@ -115,21 +117,47 @@ func (k *ecdsaPublicKey) Raw(v interface{}) error { } func (k *ecdsaPrivateKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() + return raw(k, v) +} - pubk, err := buildECDSAPublicKey(k.Crv(), k.x, k.y) - if err != nil { - return fmt.Errorf(`failed to build public key: %w`, err) - } +func ecdsaPrivateJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { + switch k := keyif.(type) { + case *ecdsaPublicKey: + switch hint.(type) { + case ecdsa.PublicKey, *ecdsa.PublicKey, interface{}: + default: + return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError()) + } - var key ecdsa.PrivateKey - var d big.Int - d.SetBytes(k.d) - key.D = &d - key.PublicKey = *pubk + k.mu.RLock() + defer k.mu.RUnlock() + + return buildECDSAPublicKey(k.Crv(), k.x, k.y) + case *ecdsaPrivateKey: + switch hint.(type) { + case ecdsa.PrivateKey, *ecdsa.PrivateKey, interface{}: + default: + return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError()) + } + + k.mu.RLock() + defer k.mu.RUnlock() - return blackmagic.AssignIfCompatible(v, &key) + pubk, err := buildECDSAPublicKey(k.Crv(), k.x, k.y) + if err != nil { + return nil, fmt.Errorf(`failed to build public key: %w`, err) + } + + var key ecdsa.PrivateKey + var d big.Int + d.SetBytes(k.d) + key.D = &d + key.PublicKey = *pubk + + return &key, nil + default: + return nil, ContinueError() + } } func makeECDSAPublicKey(src Key) (Key, error) { diff --git a/jwk/interface_gen.go b/jwk/interface_gen.go index 5820fd680..2ec82798d 100644 --- a/jwk/interface_gen.go +++ b/jwk/interface_gen.go @@ -67,13 +67,16 @@ type Key interface { // called by the user Validate() error - // Raw creates the corresponding raw key. For example, - // EC types would create *ecdsa.PublicKey or *ecdsa.PrivateKey, + // Raw creates the corresponding raw key from the jwk.Key. For example, + // EC keys would create *ecdsa.PublicKey or *ecdsa.PrivateKey, // and OctetSeq types create a []byte key. // // If you do not know the exact type of a jwk.Key before attempting // to obtain the raw key, you can simply pass a pointer to an - // empty interface as the first argument. + // empty interface as the first argument (important caveat: this can only + // be done for keys that are defined in this package. If you are using keys + // imported from third party modules, you will need to know the exact type + // before calling this method). // // If you already know the exact type, it is recommended that you // pass a pointer to the zero value of the actual key type (e.g. &rsa.PrivateKey) diff --git a/jwk/jwk.go b/jwk/jwk.go index 2c97d7d7f..c26340ab9 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -189,9 +189,9 @@ func FromRaw(raw interface{}) (Key, error) { return nil, fmt.Errorf(`jwk.FromRaw requires a non-nil key`) } - muKeyConverters.RLock() - conv, ok := keyConverters[reflect.TypeOf(raw)] - muKeyConverters.RUnlock() + muRJConverters.RLock() + conv, ok := rjConverters[reflect.TypeOf(raw)] + muRJConverters.RUnlock() if !ok { return nil, fmt.Errorf(`jwk.FromRaw: failed to convert %T to jwk.Key: no converters were able to convert`, raw) } diff --git a/tools/cmd/genjwk/main.go b/tools/cmd/genjwk/main.go index 7f7bf4b18..cd975e946 100644 --- a/tools/cmd/genjwk/main.go +++ b/tools/cmd/genjwk/main.go @@ -687,12 +687,15 @@ func generateGenericHeaders(fields codegen.FieldList) error { o.L("// Validate is never called by `UnmarshalJSON()` or `Set`. It must explicitly be") o.L("// called by the user") o.L("Validate() error") - o.LL("// Raw creates the corresponding raw key. For example,") - o.L("// EC types would create *ecdsa.PublicKey or *ecdsa.PrivateKey,") + o.LL("// Raw creates the corresponding raw key from the jwk.Key. For example,") + o.L("// EC keys would create *ecdsa.PublicKey or *ecdsa.PrivateKey,") o.L("// and OctetSeq types create a []byte key.") o.L("//\n// If you do not know the exact type of a jwk.Key before attempting") o.L("// to obtain the raw key, you can simply pass a pointer to an") - o.L("// empty interface as the first argument.") + o.L("// empty interface as the first argument (important caveat: this can only") + o.L("// be done for keys that are defined in this package. If you are using keys") + o.L("// imported from third party modules, you will need to know the exact type") + o.L("// before calling this method).") o.L("//\n// If you already know the exact type, it is recommended that you") o.L("// pass a pointer to the zero value of the actual key type (e.g. &rsa.PrivateKey)") o.L("// for efficiency.") From 8c4c9db26139d79602a680ac078d2b08ac3e59c6 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Sat, 18 Nov 2023 19:32:29 +0900 Subject: [PATCH 04/21] Appease linter --- jwk/convert.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwk/convert.go b/jwk/convert.go index 3677d1c79..b2b67222a 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -68,7 +68,7 @@ type JRKeyConverter interface { // receives the _value_ that this pointer points to, to make it easier to // detect the type of the result. // - // Note that the the second argument may be an `interface{}` (which means that the + // Note that the second argument may be an `interface{}` (which means that the // user has delegated the type detection to the converter). // // Raw must NOT modify the hint object, and should return jwk.ContinueError From 3c4abdf99a7760b67e27b395404da714580d55a3 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Sat, 18 Nov 2023 19:44:21 +0900 Subject: [PATCH 05/21] Add more context for debugging --- examples/jwx_register_ec_and_key_example_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index b40aa1b7c..b7eaa8f96 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -80,7 +80,7 @@ func ExampleShangMiSm2() { // Create a jwk.Key from ShangMi SM2 private key shangmi2JWK, err := jwk.FromRaw(shangmi2pk) if err != nil { - fmt.Println(err) + fmt.Printf("failed to create jwk.Key from raw ShangMi private key: %w\n", err) return } @@ -88,7 +88,7 @@ func ExampleShangMiSm2() { // Create a ShangMi SM2 private key back from the jwk.Key var clone sm2.PrivateKey if err := shangmi2JWK.Raw(&clone); err != nil { - fmt.Println(err) + fmt.Printf("failed to create ShangMi private key from jwk.Key: %w\n", err) return } @@ -117,7 +117,7 @@ func ExampleShangMiSm2() { { // Can do the same thing for interface{} var clone interface{} if err := shangmi2JWK.Raw(&clone); err != nil { - fmt.Println(err) + fmt.Printf("failed to create ShangMi private key from jwk.Key (via interface{}): %w\n", err) return } } @@ -131,12 +131,12 @@ func ExampleShangMiSm2() { } eckjwk, err := jwk.FromRaw(ecprivkey) if err != nil { - fmt.Println(err) + fmt.Printf("failed to create jwk.Key from raw ShangMi public key: %w\n", err) return } var clone ecdsa.PrivateKey if err := eckjwk.Raw(&clone); err != nil { - fmt.Println(err) + fmt.Printf("failed to create ShangMi public key from jwk.Key: %w\n", err) return } } @@ -144,19 +144,19 @@ func ExampleShangMiSm2() { payload := []byte("Lorem ipsum") signed, err := jws.Sign(payload, jws.WithKey(jwa.ES256, shangmi2JWK)) if err != nil { - fmt.Println(err) + fmt.Printf("Failed to sign using ShangMi key: %w\n", err) return } shangmi2PubJWK, err := jwk.PublicKeyOf(shangmi2JWK) if err != nil { - fmt.Println(err) + fmt.Printf("Failed to create public JWK using ShangMi key: %w\n", err) return } verified, err := jws.Verify(signed, jws.WithKey(jwa.ES256, shangmi2PubJWK)) if err != nil { - fmt.Println(err) + fmt.Printf("Failed to verify using ShangMi key: %w\n", err) return } From fc90294b2780457c41bda4956be569318ab31f8b Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Sat, 18 Nov 2023 20:14:19 +0900 Subject: [PATCH 06/21] fix directive --- examples/jwx_register_ec_and_key_example_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index b7eaa8f96..c4db8ea3f 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -80,7 +80,7 @@ func ExampleShangMiSm2() { // Create a jwk.Key from ShangMi SM2 private key shangmi2JWK, err := jwk.FromRaw(shangmi2pk) if err != nil { - fmt.Printf("failed to create jwk.Key from raw ShangMi private key: %w\n", err) + fmt.Printf("failed to create jwk.Key from raw ShangMi private key: %s\n", err) return } @@ -88,7 +88,7 @@ func ExampleShangMiSm2() { // Create a ShangMi SM2 private key back from the jwk.Key var clone sm2.PrivateKey if err := shangmi2JWK.Raw(&clone); err != nil { - fmt.Printf("failed to create ShangMi private key from jwk.Key: %w\n", err) + fmt.Printf("failed to create ShangMi private key from jwk.Key: %s\n", err) return } @@ -117,7 +117,7 @@ func ExampleShangMiSm2() { { // Can do the same thing for interface{} var clone interface{} if err := shangmi2JWK.Raw(&clone); err != nil { - fmt.Printf("failed to create ShangMi private key from jwk.Key (via interface{}): %w\n", err) + fmt.Printf("failed to create ShangMi private key from jwk.Key (via interface{}): %s\n", err) return } } @@ -131,12 +131,12 @@ func ExampleShangMiSm2() { } eckjwk, err := jwk.FromRaw(ecprivkey) if err != nil { - fmt.Printf("failed to create jwk.Key from raw ShangMi public key: %w\n", err) + fmt.Printf("failed to create jwk.Key from raw ShangMi public key: %s\n", err) return } var clone ecdsa.PrivateKey if err := eckjwk.Raw(&clone); err != nil { - fmt.Printf("failed to create ShangMi public key from jwk.Key: %w\n", err) + fmt.Printf("failed to create ShangMi public key from jwk.Key: %s\n", err) return } } @@ -144,19 +144,19 @@ func ExampleShangMiSm2() { payload := []byte("Lorem ipsum") signed, err := jws.Sign(payload, jws.WithKey(jwa.ES256, shangmi2JWK)) if err != nil { - fmt.Printf("Failed to sign using ShangMi key: %w\n", err) + fmt.Printf("Failed to sign using ShangMi key: %s\n", err) return } shangmi2PubJWK, err := jwk.PublicKeyOf(shangmi2JWK) if err != nil { - fmt.Printf("Failed to create public JWK using ShangMi key: %w\n", err) + fmt.Printf("Failed to create public JWK using ShangMi key: %s\n", err) return } verified, err := jws.Verify(signed, jws.WithKey(jwa.ES256, shangmi2PubJWK)) if err != nil { - fmt.Printf("Failed to verify using ShangMi key: %w\n", err) + fmt.Printf("Failed to verify using ShangMi key: %s\n", err) return } From afe6585d9aae0df12f1af0721673997631d5d8e9 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Sat, 18 Nov 2023 20:25:10 +0900 Subject: [PATCH 07/21] See if bumping go version to 1.21 only makes a difference --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a478db30e..a609394f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: go_tags: [ 'stdlib', 'goccy', 'es256k', 'asmbase64', 'alltags'] - go: [ '1.21', '1.20' ] + go: [ '1.21' ] name: "Test [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]" steps: - name: Checkout repository From 3735f3636e0a97ef295b8e774998fef3662489b5 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Sun, 19 Nov 2023 06:31:11 +0900 Subject: [PATCH 08/21] fix one more --- .github/workflows/smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 462215cf9..2def825ea 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: go_tags: [ 'stdlib', 'goccy', 'es256k', 'alltags' ] - go: [ '1.21', '1.20' ] + go: [ '1.21' ] name: "Smoke [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]" steps: - name: Checkout repository From 0b1df840e9776e59c8fea37c8c39a9cfc83015e1 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 07:43:56 +0900 Subject: [PATCH 09/21] Rename RJ/JR to Import/Export --- jwk/convert.go | 85 ++++++++++++++++++++++++-------------------------- jwk/ecdsa.go | 2 +- jwk/jwk.go | 6 ++-- 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/jwk/convert.go b/jwk/convert.go index b2b67222a..78d212777 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -15,48 +15,45 @@ import ( // # Converting between Raw Keys and `jwk.Key`s // -// A converter that converts from a raw key to a `jwk.Key` is called a RJKeyConverter. -// A converter that converts from a `jwk.Key` to a raw key is called a JRKeyConverter. -// -// You can register a convert from a raw key to a `jwk.Key` by calling -// `jwk.RegisterRJKeyConverter`. +// A converter that converts from a raw key to a `jwk.Key` is called a KeyImporter. +// A converter that converts from a `jwk.Key` to a raw key is called a KeyExporter. -var rjConverters = make(map[reflect.Type]RJKeyConverter) -var jrConverters = make(map[jwa.KeyType][]JRKeyConverter) +var keyImporters = make(map[reflect.Type]KeyImporter) +var keyExporters = make(map[jwa.KeyType][]KeyExporter) -var muRJConverters sync.RWMutex +var myKeyImporters sync.RWMutex var muJRConverters sync.RWMutex -func RegisterRJKeyConverter(from interface{}, conv RJKeyConverter) { - muRJConverters.Lock() - defer muRJConverters.Unlock() - rjConverters[reflect.TypeOf(from)] = conv +func RegisterKeyImporter(from interface{}, conv KeyImporter) { + myKeyImporters.Lock() + defer myKeyImporters.Unlock() + keyImporters[reflect.TypeOf(from)] = conv } -func RegisterJRKeyConverter(kty jwa.KeyType, conv JRKeyConverter) { +func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) { muJRConverters.Lock() defer muJRConverters.Unlock() - convs, ok := jrConverters[kty] + convs, ok := keyExporters[kty] if !ok { - convs = []JRKeyConverter{conv} + convs = []KeyExporter{conv} } else { - convs = append([]JRKeyConverter{conv}, convs...) + convs = append([]KeyExporter{conv}, convs...) } - jrConverters[kty] = convs + keyExporters[kty] = convs } -type RJKeyConverter interface { +type KeyImporter interface { FromRaw(interface{}) (Key, error) } -type RJKeyConvertFunc func(interface{}) (Key, error) +type KeyImportFunc func(interface{}) (Key, error) -func (f RJKeyConvertFunc) FromRaw(raw interface{}) (Key, error) { +func (f KeyImportFunc) FromRaw(raw interface{}) (Key, error) { return f(raw) } -// JRKeyConverter is used to convert from a `jwk.Key` to a raw key. -type JRKeyConverter interface { +// KeyExporter is used to convert from a `jwk.Key` to a raw key. +type KeyExporter interface { // Raw takes the `jwk.Key` to be converted, and a hint (the raw key to be converted to). // The hint is the object that the user requested the result to be assigned to. // The method should return the converted raw key, or an error if the conversion fails. @@ -76,51 +73,51 @@ type JRKeyConverter interface { Raw(Key, interface{}) (interface{}, error) } -type JRKeyConvertFunc func(Key, interface{}) (interface{}, error) +type KeyExportFunc func(Key, interface{}) (interface{}, error) -func (f JRKeyConvertFunc) Raw(key Key, hint interface{}) (interface{}, error) { +func (f KeyExportFunc) Raw(key Key, hint interface{}) (interface{}, error) { return f(key, hint) } func init() { { - f := RJKeyConvertFunc(rsaPrivateKeyToJWK) + f := KeyImportFunc(rsaPrivateKeyToJWK) k := rsa.PrivateKey{} - RegisterRJKeyConverter(k, f) - RegisterRJKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := RJKeyConvertFunc(rsaPublicKeyToJWK) + f := KeyImportFunc(rsaPublicKeyToJWK) k := rsa.PublicKey{} - RegisterRJKeyConverter(k, f) - RegisterRJKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := RJKeyConvertFunc(ecdsaPrivateKeyToJWK) + f := KeyImportFunc(ecdsaPrivateKeyToJWK) k := ecdsa.PrivateKey{} - RegisterRJKeyConverter(k, f) - RegisterRJKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := RJKeyConvertFunc(ecdsaPublicKeyToJWK) + f := KeyImportFunc(ecdsaPublicKeyToJWK) k := ecdsa.PublicKey{} - RegisterRJKeyConverter(k, f) - RegisterRJKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := RJKeyConvertFunc(okpPrivateKeyToJWK) + f := KeyImportFunc(okpPrivateKeyToJWK) for _, k := range []interface{}{ed25519.PrivateKey(nil), ecdh.PrivateKey{}, &ecdh.PrivateKey{}} { - RegisterRJKeyConverter(k, f) + RegisterKeyImporter(k, f) } } { - f := RJKeyConvertFunc(okpPublicKeyToJWK) + f := KeyImportFunc(okpPublicKeyToJWK) for _, k := range []interface{}{ed25519.PublicKey(nil), ecdh.PublicKey{}, &ecdh.PublicKey{}} { - RegisterRJKeyConverter(k, f) + RegisterKeyImporter(k, f) } } - RegisterRJKeyConverter([]byte(nil), RJKeyConvertFunc(bytesToKey)) + RegisterKeyImporter([]byte(nil), KeyImportFunc(bytesToKey)) } // These may seem a bit repetitive and redandunt, but the problem is that @@ -249,14 +246,14 @@ func bytesToKey(src interface{}) (Key, error) { // It's done this way to centralize the logic (mapping) of which keys are converted // to what raw key. func raw(key Key, dst interface{}) error { - muRJConverters.RLock() - defer muRJConverters.RUnlock() + myKeyImporters.RLock() + defer myKeyImporters.RUnlock() // dst better be a pointer rv := reflect.ValueOf(dst) if rv.Kind() != reflect.Ptr { return fmt.Errorf(`destination object must be a pointer`) } - if convs, ok := jrConverters[key.KeyType()]; ok { + if convs, ok := keyExporters[key.KeyType()]; ok { for _, conv := range convs { v, err := conv.Raw(key, dst) if err != nil { diff --git a/jwk/ecdsa.go b/jwk/ecdsa.go index c061b7117..6abf8b5bc 100644 --- a/jwk/ecdsa.go +++ b/jwk/ecdsa.go @@ -19,7 +19,7 @@ func init() { ourecdsa.RegisterCurve(jwa.P384, elliptic.P384()) ourecdsa.RegisterCurve(jwa.P521, elliptic.P521()) - RegisterJRKeyConverter(jwa.EC, JRKeyConvertFunc(ecdsaPrivateJWKToRaw)) + RegisterKeyExporter(jwa.EC, KeyExportFunc(ecdsaPrivateJWKToRaw)) } func (k *ecdsaPublicKey) FromRaw(rawKey *ecdsa.PublicKey) error { diff --git a/jwk/jwk.go b/jwk/jwk.go index c26340ab9..17e4f284c 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -189,9 +189,9 @@ func FromRaw(raw interface{}) (Key, error) { return nil, fmt.Errorf(`jwk.FromRaw requires a non-nil key`) } - muRJConverters.RLock() - conv, ok := rjConverters[reflect.TypeOf(raw)] - muRJConverters.RUnlock() + myKeyImporters.RLock() + conv, ok := keyImporters[reflect.TypeOf(raw)] + myKeyImporters.RUnlock() if !ok { return nil, fmt.Errorf(`jwk.FromRaw: failed to convert %T to jwk.Key: no converters were able to convert`, raw) } From 8a86ca758aeb9b2d6fae017963afdb728df34136 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 09:12:41 +0900 Subject: [PATCH 10/21] fix usage --- .../jwx_register_ec_and_key_example_test.go | 4 ++-- jwk/convert.go | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index c4db8ea3f..9086365c6 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -39,9 +39,9 @@ func init() { // We only need one converter for the private key, because the public key // is exactly the same type as *ecdsa.PublicKey - jwk.RegisterRJKeyConverter(shangmi2pk, jwk.RJKeyConvertFunc(convertShangMiSm2)) + jwk.RegisterKeyImporter(shangmi2pk, jwk.KeyImportFunc(convertShangMiSm2)) - jwk.RegisterJRKeyConverter(jwa.EC, jwk.JRKeyConvertFunc(convertJWKToShangMiSm2)) + jwk.RegisterKeyExporter(jwa.EC, jwk.KeyExportFunc(convertJWKToShangMiSm2)) } func convertShangMiSm2(key interface{}) (jwk.Key, error) { diff --git a/jwk/convert.go b/jwk/convert.go index 78d212777..5b413f64e 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -22,17 +22,23 @@ var keyImporters = make(map[reflect.Type]KeyImporter) var keyExporters = make(map[jwa.KeyType][]KeyExporter) var myKeyImporters sync.RWMutex -var muJRConverters sync.RWMutex +var muKeyExporters sync.RWMutex +// RegisterKeyImporter registers a KeyImporter for the given raw key. When `jwk.FromRaw()` is called, +// the library will look up the appropriate KeyImporter for the given raw key type (via `reflect`) +// and execute the KeyImporters in succession until either one of them succeeds, or all of them fail. func RegisterKeyImporter(from interface{}, conv KeyImporter) { myKeyImporters.Lock() defer myKeyImporters.Unlock() keyImporters[reflect.TypeOf(from)] = conv } +// RegisterKeyExporter registers a KeyExporter for the given key type. When `key.Raw()` is called, +// the library will look up the appropriate KeyExporter for the given key type and execute the +// KeyExporters in succession until either one of them succeeds, or all of them fail. func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) { - muJRConverters.Lock() - defer muJRConverters.Unlock() + muKeyExporters.Lock() + defer muKeyExporters.Unlock() convs, ok := keyExporters[kty] if !ok { convs = []KeyExporter{conv} @@ -42,23 +48,28 @@ func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) { keyExporters[kty] = convs } +// KeyImporter is used to convert from a raw key to a `jwk.Key`. mneumonic: from the PoV of the `jwk.Key`, +// we're _importing_ a raw key. type KeyImporter interface { + // FromRaw takes the raw key to be converted, and returns a `jwk.Key` or an error if the conversion fails. FromRaw(interface{}) (Key, error) } +// KeyImportFunc is a convenience type to implement KeyImporter as a function. type KeyImportFunc func(interface{}) (Key, error) func (f KeyImportFunc) FromRaw(raw interface{}) (Key, error) { return f(raw) } -// KeyExporter is used to convert from a `jwk.Key` to a raw key. +// KeyExporter is used to convert from a `jwk.Key` to a raw key. mneumonic: from the PoV of the `jwk.Key`, +// we're _exporting_ it to a raw key. type KeyExporter interface { // Raw takes the `jwk.Key` to be converted, and a hint (the raw key to be converted to). // The hint is the object that the user requested the result to be assigned to. // The method should return the converted raw key, or an error if the conversion fails. // - // Third party modules MUST NOT create raw + // Third party modules MUST NOT modifiy the hint object. // // When the user calls `key.Raw(dst)`, the `dst` object is a _pointer_ to the // object that the user wants the result to be assigned to, but the converter @@ -73,6 +84,7 @@ type KeyExporter interface { Raw(Key, interface{}) (interface{}, error) } +// KeyExportFunc is a convenience type to implement KeyExporter as a function. type KeyExportFunc func(Key, interface{}) (interface{}, error) func (f KeyExportFunc) Raw(key Key, hint interface{}) (interface{}, error) { From 61c5ea5ea9cdb99819eb143ef29b4279ec4a65ea Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 14:50:20 +0900 Subject: [PATCH 11/21] docs --- jwk/convert.go | 18 ++++---- jwk/jwk.go | 112 +------------------------------------------------ 2 files changed, 11 insertions(+), 119 deletions(-) diff --git a/jwk/convert.go b/jwk/convert.go index 5b413f64e..3c9a39950 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -51,27 +51,27 @@ func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) { // KeyImporter is used to convert from a raw key to a `jwk.Key`. mneumonic: from the PoV of the `jwk.Key`, // we're _importing_ a raw key. type KeyImporter interface { - // FromRaw takes the raw key to be converted, and returns a `jwk.Key` or an error if the conversion fails. - FromRaw(interface{}) (Key, error) + // Import takes the raw key to be converted, and returns a `jwk.Key` or an error if the conversion fails. + Import(interface{}) (Key, error) } // KeyImportFunc is a convenience type to implement KeyImporter as a function. type KeyImportFunc func(interface{}) (Key, error) -func (f KeyImportFunc) FromRaw(raw interface{}) (Key, error) { +func (f KeyImportFunc) Import(raw interface{}) (Key, error) { return f(raw) } // KeyExporter is used to convert from a `jwk.Key` to a raw key. mneumonic: from the PoV of the `jwk.Key`, // we're _exporting_ it to a raw key. type KeyExporter interface { - // Raw takes the `jwk.Key` to be converted, and a hint (the raw key to be converted to). + // Export takes the `jwk.Key` to be converted, and a hint (the raw key to be converted to). // The hint is the object that the user requested the result to be assigned to. // The method should return the converted raw key, or an error if the conversion fails. // // Third party modules MUST NOT modifiy the hint object. // - // When the user calls `key.Raw(dst)`, the `dst` object is a _pointer_ to the + // When the user calls `key.Export(dst)`, the `dst` object is a _pointer_ to the // object that the user wants the result to be assigned to, but the converter // receives the _value_ that this pointer points to, to make it easier to // detect the type of the result. @@ -79,15 +79,15 @@ type KeyExporter interface { // Note that the second argument may be an `interface{}` (which means that the // user has delegated the type detection to the converter). // - // Raw must NOT modify the hint object, and should return jwk.ContinueError + // Export must NOT modify the hint object, and should return jwk.ContinueError // if the hint object is not compatible with the converter. - Raw(Key, interface{}) (interface{}, error) + Export(Key, interface{}) (interface{}, error) } // KeyExportFunc is a convenience type to implement KeyExporter as a function. type KeyExportFunc func(Key, interface{}) (interface{}, error) -func (f KeyExportFunc) Raw(key Key, hint interface{}) (interface{}, error) { +func (f KeyExportFunc) Export(key Key, hint interface{}) (interface{}, error) { return f(key, hint) } @@ -267,7 +267,7 @@ func raw(key Key, dst interface{}) error { } if convs, ok := keyExporters[key.KeyType()]; ok { for _, conv := range convs { - v, err := conv.Raw(key, dst) + v, err := conv.Export(key, dst) if err != nil { if IsContinueError(err) { continue diff --git a/jwk/jwk.go b/jwk/jwk.go index 17e4f284c..6a43db20a 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -1,6 +1,5 @@ //go:generate ../tools/cmd/genjwk.sh -// Package jwk implements JWK as described in https://tools.ietf.org/html/rfc7517 package jwk import ( @@ -47,117 +46,10 @@ func init() { } } -// # Registering a key type -// -// You can add the ability to use a JWK that this library does not -// implement out of the box. You can do this by registering your own -// KeyParser instance. -// -// func init() { -// // optional -// jwk.RegiserProbeField(reflect.StructField{Name: "SomeHint", Type: reflect.TypeOf(""), Tag: `json:"some_hint"`}) -// jwk.RegisterKeyParser(&MyKeyParser{}) -// } -// -// In order to understand how this works, you need to understand -// how the `jwk.ParseKey()` works. -// -// The first thing that occurs when parsing a key is a partial -// unmarshaling of the payload into a hint / probe object. -// -// Because the `json.Unmarshal` works by calling the `UnmarshalJSON` -// method on a concrete object, we need to create one first. In order -// to create the appropriate Go object, we need to peek into the -// payload and figure out what type of key it is. -// -// In order to do this, we create a new KeyProber to partially populate -// the object with hints from the payload. For example, a JWK representing -// an RSA key would look like: -// -// { "kty": "RSA", "n": ..., "e": ..., ... } -// -// Therefore, a KeyProbe that can unmarshal the value of the field "kty" -// would be able to tell us that this is an RSA key. -// -// Also, if said payload contains some value in the "d" field, we can -// also tell that this is a private key, as only private keys need -// this field. -// -// For most cases, the default KeyProbe implementation should be sufficient. -// You would be able to query "kty" and "d" fields via the `Get()` method. -// -// var kty string -// _ = probe.Get("Kty", &kty) -// -// However, if you need extra pieces of information, you can specify -// additional fields to be probed. For example, if you want to know the -// value of the field "my_hint" (which holds a string value) from the payload, -// you can register it to be probed by registering an additional probe field like this: -// -// jwk.RegisterProbeField(reflect.StructField{Name: "MyHint", Type: reflect.TypeOf(""), Tag: `json:"my_hint"`}) -// -// Once the probe is done, the library will iterate over the registered parsers -// and attempt to parse the key by calling their `ParseKey()` methods. -// The parsers will be called in reverse order that they were registered. -// This means that it will try all parsers that were registered by third -// parties, and once those are exhausted, the default parser will be used. -// -// Each parser's `ParseKey()`` method will receive three arguments: the probe object, a -// KeyUnmarshaler, and the raw payload. The probe object can be used -// as a hint to determine what kind of key to instantiate. An example -// pseudocode may look like this: -// -// var kty string -// _ = probe.Get("Kty", &kty) -// switch kty { -// case "RSA": -// // create an RSA key -// case "EC": -// // create an EC key -// ... -// } -// -// The `KeyUnmarshaler` is a thin wrapper around `json.Unmarshal` it -// works almost identical to `json.Unmarshal`, but it allows us to -// add extra magic that is specific to this library before calling -// the actual `json.Unmarshal`. If you want to try to unmarshal the -// payload, please use this instead of `json.Unmarshal`. -// -// func init() { -// jwk.RegisterFieldProbe(reflect.StructField{Name: "MyHint", Type: reflect.TypeOf(""), Tag: `json:"my_hint"`}) -// jwk.RegisterParser(&MyKeyParser{}) -// } -// -// type MyKeyParser struct { ... } -// func(*MyKeyParser) ParseKey(rawProbe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) (jwk.Key, error) { -// // Create concrete type -// var hint string -// if err := probe.Get("MyHint", &hint); err != nil { -// // if it doesn't have the `my_hint` field, it probably means -// // it's not for us, so we return ContinueParseError so that -// // the next parser can pick it up -// return nil, jwk.ContinueParseError() -// } -// -// // Use hint to determine concrete key type -// var key jwk.Key -// switch hint { -// case ...: -// key = = myNewAwesomeJWK() -// ... -// -// return unmarshaler.Unmarshal(data, key) -// } -// -// This functionality should be considered experimental. While we -// expect that the functionality itself will remain, the API may -// change in backward incompatible ways, even during minor version -// releases. - var cpe = &continueError{} // ContinueError returns an opaque error that can be returned -// when a `KeyParser` or `KeyConverter` cannot handle the given payload, +// when a `KeyParser`, `KeyImporter`, or `KeyExporter` cannot handle the given payload, // but would like the process to continue with the next handler. func ContinueError() error { return cpe @@ -196,7 +88,7 @@ func FromRaw(raw interface{}) (Key, error) { return nil, fmt.Errorf(`jwk.FromRaw: failed to convert %T to jwk.Key: no converters were able to convert`, raw) } - return conv.FromRaw(raw) + return conv.Import(raw) } // PublicSetOf returns a new jwk.Set consisting of From ebfaa54c64918fbfe0463a39a6f9c0f4c32c5d15 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 15:09:36 +0900 Subject: [PATCH 12/21] oops, add missing file --- jwk/doc.go | 284 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 jwk/doc.go diff --git a/jwk/doc.go b/jwk/doc.go new file mode 100644 index 000000000..d783ed295 --- /dev/null +++ b/jwk/doc.go @@ -0,0 +1,284 @@ +// Package jwk implements JWK as described in https://tools.ietf.org/html/rfc7517 +// +// This package implements jwk.Key to represent a single JWK, and jwk.Set to represent +// a set of JWKs. +// +// The `jwk.Key` type is an interface, which hides the underlying implementation for +// each key type. Each key type can further be converted to interfaces for known +// types, such as `jwk.ECDSAPrivateKey`, `jwk.RSAPublicKey`, etc. This may not necessarily +// work for third party key types (see section on "Registering a key type" below). +// +// Users can create a JWK in two ways. One is to unmarshal a JSON representation of a +// key. The second one is to use `jwk.FromRaw()` to import a raw key and convert it to +// a jwk.Key. +// +// # Simple Usage +// +// You can parse a JWK from a JSON payload: +// +// jwk.ParseKey([]byte(`{"kty":"EC",...}`)) +// +// You can go back and forth between raw key types and JWKs: +// +// jwkKey, _ := jwk.FromRaw(rsaPrivateKey) +// var rawKey *rsa.PRrivateKey +// jwkKey.Raw(&rawKey) +// +// You can use them to sign/verify/encrypt/decrypt: +// +// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey)) +// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey)) +// +// See examples/jwk_parse_example_test.go and other files in the exmaples/ directory for more. +// +// # Registering a custom key type +// +// (Caveat Emptor) This functionality should be considered experimental. While we +// expect that the functionality itself will remain, the API may +// change in backward incompatible ways, even during minor version +// releases. +// +// You can add the ability to use a JWK type that this library does not +// implement out of the box. You can do this by registering your own +// KeyParser, KeyImporter, and KeyExporter instances. +// +// func init() { +// jwk.RegiserProbeField(reflect.StructField{Name: "SomeHint", Type: reflect.TypeOf(""), Tag: `json:"some_hint"`}) +// jwk.RegisterKeyParser(&MyKeyParser{}) +// jwk.RegisterKeyImporter(&MyKeyImporter{}) +// jwk.RegisterKeyExporter(&MyKeyExporter{}) +// } +// +// The KeyParser is used to parse JSON payloads and conver them into a jwk.Key. +// The KeyImporter is used to convert a raw key (e.g. *rsa.PrivateKey, *ecdsa.PrivateKey, etc) into a jwk.Key. +// The KeyExporter is used to convert a jwk.Key into a raw key. +// +// Although we believe the mechanism has been streamline quite a lot, it is also true +// that the entire process of parsing and converting keys are much more convoluted than you might +// think. Please know before hand that if you intend to add support for a new key type, +// it _WILL_ require you to learn this module pretty much in-and-out. +// +// Read on for more explanation. +// +// ## Registering a KeyParser +// +// In order to understand how parsing works, we need to explain how the `jwk.ParseKey()` works. +// +// The first thing that occurs when parsing a key is a partial +// unmarshaling of the payload into a hint / probe object. +// +// Because the `json.Unmarshal` works by calling the `UnmarshalJSON` +// method on a concrete object, we need to create a concrete object first. +// In order/ to create the appropriate Go object, we need to know which concrete +// object to create from the JSON payload, meaning we need to peek into the +// payload and figure out what type of key it is. +// +// In order to do this, we effectively need to parse the JSON payload twice. +// First, we "probe" the payload to figure out what kind of key it is, then +// we parse it again to create the actual key object. +// +// For probing, we create a new "probe" object (KeyProbe, which is not +// directly available to end users) to populate the object with hints from the payload. +// For example, a JWK representing an RSA key would look like: +// +// { "kty": "RSA", "n": ..., "e": ..., ... } +// +// The default KeyProbe is constructed to unmarshal "kty" and "d" fields, +// because that is enough information to determine what kind of key to +// construct. +// +// For example, if the payload contains "kty" field with the value "RSA", +// we know that it's an RSA key. If it contains "EC", we know that it's +// an EC key. Furthermore, if the payload contains some value in the "d" field, we can +// also tell that this is a private key, as only private keys need +// this field. +// +// For most cases, the default KeyProbe implementation should be sufficient. +// However, there may be cases in the future where there are new key types +// that require further information. Perhaps you are embedding another hint +// in your JWK to further specify what kind of key it is. In that case, you +// would need to probe more. +// +// Normally you can only change how an object is unmarshaled by specifying +// JSON tags when defining a struct, but we use `reflect` package capabilities +// to create an object dynamically, which is shared among all parsing operations. +// +// To add a new field to be probed, you need to register a new `reflect.StructField` +// object that has all of the information. For example, the code below would +// register a field named "MyHint" that is of type string, and has a JSON tag +// of "my_hint". +// +// jwk.RegisterProbeField(reflect.StructField{Name: "MyHint", Type: reflect.TypeOf(""), Tag: `json:"my_hint"`}) +// +// The value of this field can be retrieved by calling `Get()` method on the +// KeyProbe object (from the `KeyParser`'s `ParseKey()` method discussed later) +// +// var myhint string +// _ = probe.Get("MyHint", &myhint) +// +// var kty string +// _ = probe.Get("Kty", &kty) +// +// This mechanism allows you to be flexible when trying to determine the key type +// to instantiate. +// +// # Registering a KeyParser +// +// When `jwk.Parse` / `jwk.ParseKey` is called, the library will first probe +// the payload as discussed above. +// +// Once the probe is done, the library will iterate over the registered parsers +// and attempt to parse the key by calling their `ParseKey()` methods. +// +// The parsers will be called in reverse order that they were registered. +// This means that it will try all parsers that were registered by third +// parties, and once those are exhausted, the default parser will be used. +// +// Each parser's `ParseKey()“ method will receive three arguments: the probe object, a +// KeyUnmarshaler, and the raw payload. The probe object can be used +// as a hint to determine what kind of key to instantiate. An example +// pseudocode may look like this: +// +// var kty string +// _ = probe.Get("Kty", &kty) +// switch kty { +// case "RSA": +// // create an RSA key +// case "EC": +// // create an EC key +// ... +// } +// +// The `KeyUnmarshaler` is a thin wrapper around `json.Unmarshal`. It works almost +// identical to `json.Unmarshal`, but it allows us to add extra magic that is +// specific to this library (which users do not need to be aware of) before calling +// the actual `json.Unmarshal`. Please use the `KeyUnmarshaler` to unmarshal JWKs instead of `json.Unmarshal`. +// +// Putting it all together, the boiler plate for registering a new parser may look like this: +// +// func init() { +// jwk.RegisterFieldProbe(reflect.StructField{Name: "MyHint", Type: reflect.TypeOf(""), Tag: `json:"my_hint"`}) +// jwk.RegisterParser(&MyKeyParser{}) +// } +// +// type MyKeyParser struct { ... } +// func(*MyKeyParser) ParseKey(rawProbe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) (jwk.Key, error) { +// // Create concrete type +// var hint string +// if err := probe.Get("MyHint", &hint); err != nil { +// // if it doesn't have the `my_hint` field, it probably means +// // it's not for us, so we return ContinueParseError so that +// // the next parser can pick it up +// return nil, jwk.ContinueParseError() +// } +// +// // Use hint to determine concrete key type +// var key jwk.Key +// switch hint { +// case ...: +// key = = myNewAwesomeJWK() +// ... +// } +// +// return unmarshaler.Unmarshal(data, key) +// } +// +// ## Registering KeyImporter/KeyExporter +// +// If you are going to do anything with the key that was parsed by your KeyParser, +// you will need to tell the library how to convert back and forth between +// raw keys and JWKs. Conversion from raw keys to jwk.Keys are done by KeyImporters, +// and conversion from jwk.Keys to raw keys are done by KeyExporters. +// +// ## Using jwk.FromRaw() using KeyImporter +// +// Each KeyImporter is hooked to run against a specific raw key type. +// +// When `jwk.FromRaw()` is called, the library will iterate over all registered +// KeyImporters for the specified raw key type, and attempt to convert the raw +// key to a JWK by calling the `Import()` method on each KeyImporter. +// +// The KeyImporter's `Import()` method will receive the raw key to be converted, +// and should return a JWK or an error if the conversion fails, or the return +// `jwk.ContinueError()` if the specified raw key cannot be handled by ths/ KeyImporter. +// +// Once a KeyImporter is available, you will be able to pass the raw key to `jwk.FromRaw()`. +// The following example shows how you might register a KeyImporter for a hypotheical +// mypkg.SuperSecretKey: +// +// jwk.RegisterKeyImporter(&mypkg.SuperSecretKey{}, jwk.KeyImportFunc(imnportSuperSecretKey)) +// +// func importSuperSecretKey(key interface{}) (jwk.Key, error) { +// mykey, ok := key.(*mypkg.SuperSecretKey) +// if !ok { +// // You must return jwk.ContinueError here, or otherwise +// // processing will stop with an error +// return nil, fmt.Errorf("invalid key type %T for importer: %w", key, jwk.ContinueError()) +// } +// +// return mypkg.SuperSecretJWK{ .... }, nil // You could reuse existing JWK types if you can +// } +// +// ## Registering a KeyExporter +// +// KeyExporters are the opposite of KeyImporters: they convert a JWK to a raw key when `key.Raw(...)` is +// called. If you intend to use `key.Raw(...)` for a JWK created using one of your KeyImporters, +// you will also +// +// KeyExporters are registered by key type. For example, if you want to register a KeyExporter for +// RSA keys, you would do: +// +// jwk.RegisterKeyExporter(jwa.RSA, jwk.KeyExportFunc(exportRSAKey)) +// +// For a given JWK, it will be passed a "destination" object to store the exported raw key. For example, +// an RSA-based private JWK can be exported to a `*rsa.PrivateKey` or to a `*interface{}`, but not +// to a `*ecdsa.PrivateKey`: +// +// var dst *rsa.PrivateKey +// key.Raw(&dst) // OK +// +// var dst interface{} +// key.Raw(&dst) // OK +// +// var dst *ecdsa.PrivateKey +// key.Raw(&dst) // Error, if key is an RSA key +// +// You will need to handle this distinction youself in your KeyImporter. For example, certain +// elliptic curve keys can be expressed in JWK in the same format, minus the "kty". In that case +// you will need to check for the type of the destination object and return an error if it is +// not compatible with your key. +// +// var raw mypkg.PrivateKey // assume a hypothetical private key type using a different curve than standard ones lie P-256 +// key, _ := jwk.FromRaw(raw) +// // key could be jwk.ECDSAPrivateKey, with different curve than P-256 +// +// var dst *ecdsa.PrivateKey +// key.Raw(&dst) // your KeyImporter will be called with *ecdsa.PrivateKey, which is not compatible with your key +// +// To implement this your code should look like the following: +// +// jwk.RegisterKeyExporter(jwk.EC, jwk.KeyExportFunc(exportMyKey)) +// +// func exportMyKey(key jwk.Key, hint interface{}) (interface{}, error) { +// // check if the type of object in hint is compatible with your key +// switch hint.(type) { +// case *mypkg.PrivateKey, *interface{}: +// // OK, we can proceed +// default: +// // Not compatible, return jwk.ContinueError +// return nil, jwk.ContinueError() +// } +// +// // key is a jwk.ECDSAPrivateKey or jwk.ECDSAPublicKey +// switch key := key.(type) { +// case jwk.ECDSAPrivateKey: +// // convert key to mypkg.PrivateKey +// case jwk.ECDSAPublicKey: +// // convert key to mypkg.PublicKey +// default: +// // Not compatible, return jwk.ContinueError +// return nil, jwk.ContinueError() +// } +// return ..., nil +// } +package jwk From 72066951fd1fc139f270033ea6538563b837147c Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 21:26:05 +0900 Subject: [PATCH 13/21] Streamline exporting from jwk.Key to raw key Remove Raw() from keys, and implement jwk.Export --- Changes-v3.md | 10 +- examples/jwk_example_test.go | 2 +- .../jwx_register_ec_and_key_example_test.go | 11 +- internal/jwxtest/jwxtest.go | 10 +- internal/keyconv/keyconv.go | 14 +- jwe/internal/keyenc/keyenc_test.go | 4 +- jwe/jwe.go | 8 +- jwe/jwe_test.go | 8 +- jwk/convert.go | 41 ++++-- jwk/ecdsa.go | 28 +--- jwk/interface_gen.go | 16 -- jwk/jwk.go | 16 +- jwk/jwk_test.go | 26 ++-- jwk/okp.go | 35 +++-- jwk/rsa.go | 138 ++++++++++-------- jwk/symmetric.go | 25 +++- jws/jws_test.go | 10 +- jwx_test.go | 8 +- tools/cmd/genjwk/main.go | 13 -- 19 files changed, 213 insertions(+), 210 deletions(-) diff --git a/Changes-v3.md b/Changes-v3.md index 466a7859f..87ff46b7f 100644 --- a/Changes-v3.md +++ b/Changes-v3.md @@ -8,7 +8,7 @@ These are changes that are incompatible with the v2.x.x version. ## Module -* This module now requires Go 1.20.x +* This module now requires Go 1.21 * All `xxx.Get()` methods have been changed from `Get(string) (interface{}, error)` to `Get(string, interface{}) error`, where the second argument should be a pointer @@ -42,7 +42,9 @@ These are changes that are incompatible with the v2.x.x version. type to instantiate, and aids implementing your own `jwk.KeyParser`. Also see `jwk.RegisterKeyProbe()` -* Conversion between raw keys and `jwk.Key` can be customized using `jwk.KeyConverter`. - Also see `jwk.RegisterKeyConverter()` +* Conversion between raw keys and `jwk.Key` can be customized using `jwk.KeyImporter` and `jwk.KeyExporter`. + Also see `jwk.RegisterKeyImporter()` and `jwk.RegisterKeyExporter()` -* Added `jwk/ecdsa` to keep track of which curves are available for ECDSA keys. \ No newline at end of file +* Added `jwk/ecdsa` to keep track of which curves are available for ECDSA keys. + +* `(jwk.Key).Raw()` has been deprecated. Use `jwk.Export()` instead. diff --git a/examples/jwk_example_test.go b/examples/jwk_example_test.go index 96332056e..ec38bfac4 100644 --- a/examples/jwk_example_test.go +++ b/examples/jwk_example_test.go @@ -37,7 +37,7 @@ func ExampleJWK_Usage() { // jws and jwe operations can be performed using jwk.Key, but you could also // covert it to their "raw" forms, such as *rsa.PrivateKey or *ecdsa.PrivateKey - if err := key.Raw(&rawkey); err != nil { + if err := jwk.Export(key, &rawkey); err != nil { log.Printf("failed to create public key: %s", err) return } diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index 9086365c6..e03779566 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -53,7 +53,10 @@ func convertShangMiSm2(key interface{}) (jwk.Key, error) { } func convertJWKToShangMiSm2(key jwk.Key, hint interface{}) (interface{}, error) { - ecdsaKey := key.(jwk.ECDSAPrivateKey) + ecdsaKey, ok := key.(jwk.ECDSAPrivateKey) + if !ok { + return nil, fmt.Errorf(`invalid key type %T: %w`, key, jwk.ContinueError()) + } if ecdsaKey.Crv() != SM2 { return nil, fmt.Errorf(`cannot convert curve of type %s to ShangMi key: %w`, ecdsaKey.Crv(), jwk.ContinueError()) } @@ -87,7 +90,7 @@ func ExampleShangMiSm2() { { // Create a ShangMi SM2 private key back from the jwk.Key var clone sm2.PrivateKey - if err := shangmi2JWK.Raw(&clone); err != nil { + if err := jwk.Export(shangmi2JWK, &clone); err != nil { fmt.Printf("failed to create ShangMi private key from jwk.Key: %s\n", err) return } @@ -116,7 +119,7 @@ func ExampleShangMiSm2() { { // Can do the same thing for interface{} var clone interface{} - if err := shangmi2JWK.Raw(&clone); err != nil { + if err := jwk.Export(shangmi2JWK, &clone); err != nil { fmt.Printf("failed to create ShangMi private key from jwk.Key (via interface{}): %s\n", err) return } @@ -135,7 +138,7 @@ func ExampleShangMiSm2() { return } var clone ecdsa.PrivateKey - if err := eckjwk.Raw(&clone); err != nil { + if err := jwk.Export(eckjwk, &clone); err != nil { fmt.Printf("failed to create ShangMi public key from jwk.Key: %s\n", err) return } diff --git a/internal/jwxtest/jwxtest.go b/internal/jwxtest/jwxtest.go index 11ea59399..594b00246 100644 --- a/internal/jwxtest/jwxtest.go +++ b/internal/jwxtest/jwxtest.go @@ -267,7 +267,7 @@ func DecryptJweFile(ctx context.Context, file string, alg jwa.KeyEncryptionAlgor } var rawkey interface{} - if err := key.Raw(&rawkey); err != nil { + if err := jwk.Export(key, &rawkey); err != nil { return nil, fmt.Errorf(`failed to obtain raw key from JWK: %w`, err) } @@ -285,19 +285,19 @@ func EncryptJweFile(ctx context.Context, payload []byte, keyalg jwa.KeyEncryptio switch keyalg { case jwa.RSA1_5, jwa.RSA_OAEP, jwa.RSA_OAEP_256: var rawkey rsa.PrivateKey - if err := key.Raw(&rawkey); err != nil { + if err := jwk.Export(key, &rawkey); err != nil { return "", nil, fmt.Errorf(`failed to obtain raw key: %w`, err) } keyif = rawkey.PublicKey case jwa.ECDH_ES, jwa.ECDH_ES_A128KW, jwa.ECDH_ES_A192KW, jwa.ECDH_ES_A256KW: var rawkey ecdsa.PrivateKey - if err := key.Raw(&rawkey); err != nil { + if err := jwk.Export(key, &rawkey); err != nil { return "", nil, fmt.Errorf(`failed to obtain raw key: %w`, err) } keyif = rawkey.PublicKey default: var rawkey []byte - if err := key.Raw(&rawkey); err != nil { + if err := jwk.Export(key, &rawkey); err != nil { return "", nil, fmt.Errorf(`failed to obtain raw key: %w`, err) } keyif = rawkey @@ -323,7 +323,7 @@ func VerifyJwsFile(ctx context.Context, file string, alg jwa.SignatureAlgorithm, } var rawkey, pubkey interface{} - if err := key.Raw(&rawkey); err != nil { + if err := jwk.Export(key, &rawkey); err != nil { return nil, fmt.Errorf(`failed to obtain raw key from JWK: %w`, err) } pubkey = rawkey diff --git a/internal/keyconv/keyconv.go b/internal/keyconv/keyconv.go index a8b291a2b..044ca49bc 100644 --- a/internal/keyconv/keyconv.go +++ b/internal/keyconv/keyconv.go @@ -17,7 +17,7 @@ import ( func RSAPrivateKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw rsa.PrivateKey - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce rsa.PrivateKey from %T: %w`, src, err) } src = &raw @@ -42,7 +42,7 @@ func RSAPrivateKey(dst, src interface{}) error { func RSAPublicKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw rsa.PublicKey - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce rsa.PublicKey from %T: %w`, src, err) } src = &raw @@ -66,7 +66,7 @@ func RSAPublicKey(dst, src interface{}) error { func ECDSAPrivateKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw ecdsa.PrivateKey - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce ecdsa.PrivateKey from %T: %w`, src, err) } src = &raw @@ -89,7 +89,7 @@ func ECDSAPrivateKey(dst, src interface{}) error { func ECDSAPublicKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw ecdsa.PublicKey - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce ecdsa.PublicKey from %T: %w`, src, err) } src = &raw @@ -110,7 +110,7 @@ func ECDSAPublicKey(dst, src interface{}) error { func ByteSliceKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw []byte - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce []byte from %T: %w`, src, err) } src = raw @@ -125,7 +125,7 @@ func ByteSliceKey(dst, src interface{}) error { func Ed25519PrivateKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw ed25519.PrivateKey - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce ed25519.PrivateKey from %T: %w`, src, err) } src = &raw @@ -146,7 +146,7 @@ func Ed25519PrivateKey(dst, src interface{}) error { func Ed25519PublicKey(dst, src interface{}) error { if jwkKey, ok := src.(jwk.Key); ok { var raw ed25519.PublicKey - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return fmt.Errorf(`failed to produce ed25519.PublicKey from %T: %w`, src, err) } src = &raw diff --git a/jwe/internal/keyenc/keyenc_test.go b/jwe/internal/keyenc/keyenc_test.go index 808d27331..396a257c1 100644 --- a/jwe/internal/keyenc/keyenc_test.go +++ b/jwe/internal/keyenc/keyenc_test.go @@ -101,7 +101,7 @@ func TestDeriveECDHES(t *testing.T) { if !assert.NoError(t, err, `jwk.ParseKey should succeed`) { return } - if !assert.NoError(t, aliceWebKey.Raw(&aliceKey), `aliceWebKey.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(aliceWebKey, &aliceKey), `jwk.Export(aliceWebKey) should succeed`) { return } @@ -109,7 +109,7 @@ func TestDeriveECDHES(t *testing.T) { if !assert.NoError(t, err, `jwk.ParseKey should succeed`) { return } - if !assert.NoError(t, bobWebKey.Raw(&bobKey), `bobWebKey.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(bobWebKey, &bobKey), `jwk.Export(bobWebKey) should succeed`) { return } diff --git a/jwe/jwe.go b/jwe/jwe.go index c63abf724..f7a161e1c 100644 --- a/jwe/jwe.go +++ b/jwe/jwe.go @@ -76,7 +76,7 @@ func (b *recipientBuilder) Build(cek []byte, calg jwa.ContentEncryptionAlgorithm keyID = jwkKey.KeyID() var raw interface{} - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return nil, nil, fmt.Errorf(`failed to retrieve raw key out of %T: %w`, b.key, err) } @@ -572,7 +572,7 @@ func (dctx *decryptCtx) try(ctx context.Context, recipient Recipient, keyUsed in func (dctx *decryptCtx) decryptContent(alg jwa.KeyEncryptionAlgorithm, key interface{}, recipient Recipient) ([]byte, error) { if jwkKey, ok := key.(jwk.Key); ok { var raw interface{} - if err := jwkKey.Raw(&raw); err != nil { + if err := jwk.Export(jwkKey, &raw); err != nil { return nil, fmt.Errorf(`failed to retrieve raw key from %T: %w`, key, err) } key = raw @@ -608,13 +608,13 @@ func (dctx *decryptCtx) decryptContent(alg jwa.KeyEncryptionAlgorithm, key inter switch epk := epk.(type) { case jwk.ECDSAPublicKey: var pubkey ecdsa.PublicKey - if err := epk.Raw(&pubkey); err != nil { + if err := jwk.Export(epk, &pubkey); err != nil { return nil, fmt.Errorf(`failed to get public key: %w`, err) } dec.PublicKey(&pubkey) case jwk.OKPPublicKey: var pubkey interface{} - if err := epk.Raw(&pubkey); err != nil { + if err := jwk.Export(epk, &pubkey); err != nil { return nil, fmt.Errorf(`failed to get public key: %w`, err) } dec.PublicKey(pubkey) diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go index 53ba2065a..e3b2f7982 100644 --- a/jwe/jwe_test.go +++ b/jwe/jwe_test.go @@ -50,7 +50,7 @@ func init() { panic(err) } - if err := privkey.Raw(&rsaPrivKey); err != nil { + if err := jwk.Export(privkey, &rsaPrivKey); err != nil { panic(err) } } @@ -168,7 +168,7 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) { } var rawkey rsa.PrivateKey - if !assert.NoError(t, privkey.Raw(&rawkey), `obtaining raw key should succeed`) { + if !assert.NoError(t, jwk.Export(privkey, &rawkey), `obtaining raw key should succeed`) { return } @@ -501,7 +501,7 @@ func Test_GHIssue207(t *testing.T) { } var key ecdsa.PrivateKey - if !assert.NoError(t, webKey.Raw(&key), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(webKey, &key), `jwk.Export should succeed`) { return } @@ -628,7 +628,7 @@ func TestDecodePredefined_Direct(t *testing.T) { } var key []byte - if !assert.NoError(t, webKey.Raw(&key), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(webKey, &key), `jwk.Export should succeed`) { return } diff --git a/jwk/convert.go b/jwk/convert.go index 3c9a39950..517b10cf0 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -21,15 +21,15 @@ import ( var keyImporters = make(map[reflect.Type]KeyImporter) var keyExporters = make(map[jwa.KeyType][]KeyExporter) -var myKeyImporters sync.RWMutex +var muKeyImporters sync.RWMutex var muKeyExporters sync.RWMutex // RegisterKeyImporter registers a KeyImporter for the given raw key. When `jwk.FromRaw()` is called, // the library will look up the appropriate KeyImporter for the given raw key type (via `reflect`) // and execute the KeyImporters in succession until either one of them succeeds, or all of them fail. func RegisterKeyImporter(from interface{}, conv KeyImporter) { - myKeyImporters.Lock() - defer myKeyImporters.Unlock() + muKeyImporters.Lock() + defer muKeyImporters.Unlock() keyImporters[reflect.TypeOf(from)] = conv } @@ -254,32 +254,43 @@ func bytesToKey(src interface{}) (Key, error) { return k, nil } -// All objects call this method to convert themselves to a raw key. -// It's done this way to centralize the logic (mapping) of which keys are converted -// to what raw key. -func raw(key Key, dst interface{}) error { - myKeyImporters.RLock() - defer myKeyImporters.RUnlock() +// Export converts a `jwk.Key` to a Export key. The dst argument must be a pointer to the +// object that the user wants the result to be assigned to. +// +// Normally you would pass a pointer to the zero value of the raw key type +// such as &(*rsa.PrivateKey) or &(*ecdsa.PublicKey), which gets assigned +// the converted key. +// +// If you do not know the exact type of a jwk.Key before attempting +// to obtain the raw key, you can simply pass a pointer to an +// empty interface as the second argument +// +// If you already know the exact type, it is recommended that you +// pass a pointer to the zero value of the actual key type for efficiency. +func Export(key Key, dst interface{}) error { // dst better be a pointer rv := reflect.ValueOf(dst) if rv.Kind() != reflect.Ptr { - return fmt.Errorf(`destination object must be a pointer`) + return fmt.Errorf(`jwk.Export: destination object must be a pointer`) } - if convs, ok := keyExporters[key.KeyType()]; ok { - for _, conv := range convs { + 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 IsContinueError(err) { continue } - return fmt.Errorf(`failed to convert jwk.Key to raw format: %w`, err) + 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(`failed to assign key: %w`, err) + return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err) } return nil } } - return fmt.Errorf(`failed to find converter for key type '%T'`, key) + return fmt.Errorf(`jwk.Export: failed to find exporter for key type '%T'`, key) } diff --git a/jwk/ecdsa.go b/jwk/ecdsa.go index 6abf8b5bc..6ecec962d 100644 --- a/jwk/ecdsa.go +++ b/jwk/ecdsa.go @@ -7,7 +7,6 @@ import ( "fmt" "math/big" - "github.com/lestrrat-go/blackmagic" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/ecutil" "github.com/lestrrat-go/jwx/v3/jwa" @@ -19,7 +18,7 @@ func init() { ourecdsa.RegisterCurve(jwa.P384, elliptic.P384()) ourecdsa.RegisterCurve(jwa.P521, elliptic.P521()) - RegisterKeyExporter(jwa.EC, KeyExportFunc(ecdsaPrivateJWKToRaw)) + RegisterKeyExporter(jwa.EC, KeyExportFunc(ecdsaJWKToRaw)) } func (k *ecdsaPublicKey) FromRaw(rawKey *ecdsa.PublicKey) error { @@ -103,24 +102,7 @@ func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ec return &ecdsa.PublicKey{Curve: crv, X: &x, Y: &y}, nil } -// Raw returns the EC-DSA public key represented by this JWK -func (k *ecdsaPublicKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() - - pubk, err := buildECDSAPublicKey(k.Crv(), k.x, k.y) - if err != nil { - return fmt.Errorf(`failed to build public key: %w`, err) - } - - return blackmagic.AssignIfCompatible(v, pubk) -} - -func (k *ecdsaPrivateKey) Raw(v interface{}) error { - return raw(k, v) -} - -func ecdsaPrivateJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { +func ecdsaJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { switch k := keyif.(type) { case *ecdsaPublicKey: switch hint.(type) { @@ -131,7 +113,6 @@ func ecdsaPrivateJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { k.mu.RLock() defer k.mu.RUnlock() - return buildECDSAPublicKey(k.Crv(), k.x, k.y) case *ecdsaPrivateKey: switch hint.(type) { @@ -142,7 +123,6 @@ func ecdsaPrivateJWKToRaw(keyif Key, hint interface{}) (interface{}, error) { k.mu.RLock() defer k.mu.RUnlock() - pubk, err := buildECDSAPublicKey(k.Crv(), k.x, k.y) if err != nil { return nil, fmt.Errorf(`failed to build public key: %w`, err) @@ -210,7 +190,7 @@ func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { defer k.mu.RUnlock() var key ecdsa.PublicKey - if err := k.Raw(&key); err != nil { + if err := Export(&k, &key); err != nil { return nil, fmt.Errorf(`failed to materialize ecdsa.PublicKey for thumbprint generation: %w`, err) } @@ -234,7 +214,7 @@ func (k ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { defer k.mu.RUnlock() var key ecdsa.PrivateKey - if err := k.Raw(&key); err != nil { + if err := Export(&k, &key); err != nil { return nil, fmt.Errorf(`failed to materialize ecdsa.PrivateKey for thumbprint generation: %w`, err) } diff --git a/jwk/interface_gen.go b/jwk/interface_gen.go index 2ec82798d..1d08dd984 100644 --- a/jwk/interface_gen.go +++ b/jwk/interface_gen.go @@ -67,22 +67,6 @@ type Key interface { // called by the user Validate() error - // Raw creates the corresponding raw key from the jwk.Key. For example, - // EC keys would create *ecdsa.PublicKey or *ecdsa.PrivateKey, - // and OctetSeq types create a []byte key. - // - // If you do not know the exact type of a jwk.Key before attempting - // to obtain the raw key, you can simply pass a pointer to an - // empty interface as the first argument (important caveat: this can only - // be done for keys that are defined in this package. If you are using keys - // imported from third party modules, you will need to know the exact type - // before calling this method). - // - // If you already know the exact type, it is recommended that you - // pass a pointer to the zero value of the actual key type (e.g. &rsa.PrivateKey) - // for efficiency. - Raw(interface{}) error - // Thumbprint returns the JWK thumbprint using the indicated // hashing algorithm, according to RFC 7638 Thumbprint(crypto.Hash) ([]byte, error) diff --git a/jwk/jwk.go b/jwk/jwk.go index 6a43db20a..6948d10db 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -81,9 +81,9 @@ func FromRaw(raw interface{}) (Key, error) { return nil, fmt.Errorf(`jwk.FromRaw requires a non-nil key`) } - myKeyImporters.RLock() + muKeyImporters.RLock() conv, ok := keyImporters[reflect.TypeOf(raw)] - myKeyImporters.RUnlock() + muKeyImporters.RUnlock() if !ok { return nil, fmt.Errorf(`jwk.FromRaw: failed to convert %T to jwk.Key: no converters were able to convert`, raw) } @@ -174,7 +174,7 @@ func PublicRawKeyOf(v interface{}) (interface{}, error) { } var raw interface{} - if err := pubk.Raw(&raw); err != nil { + if err := Export(pubk, &raw); err != nil { return nil, fmt.Errorf(`jwk.PublicRawKeyOf: failed to obtain raw key from %T: %w`, pubk, err) } return raw, nil @@ -201,9 +201,9 @@ const ( // The second return value is the encoded byte sequence. func EncodeX509(v interface{}) (string, []byte, error) { // we can't import jwk, so just use the interface - if key, ok := v.(interface{ Raw(interface{}) error }); ok { + if key, ok := v.(Key); ok { var raw interface{} - if err := key.Raw(&raw); err != nil { + if err := Export(key, &raw); err != nil { return "", nil, fmt.Errorf(`failed to get raw key out of %T: %w`, key, err) } @@ -318,7 +318,7 @@ func ParseRawKey(data []byte, rawkey interface{}) error { return fmt.Errorf(`failed to parse key: %w`, err) } - if err := key.Raw(rawkey); err != nil { + if err := Export(key, rawkey); err != nil { return fmt.Errorf(`failed to assign to raw key variable: %w`, err) } @@ -603,7 +603,7 @@ func asnEncode(key Key) (string, []byte, error) { switch key := key.(type) { case RSAPrivateKey, ECDSAPrivateKey, OKPPrivateKey: var rawkey interface{} - if err := key.Raw(&rawkey); err != nil { + if err := Export(key, &rawkey); err != nil { return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err) } buf, err := x509.MarshalPKCS8PrivateKey(rawkey) @@ -613,7 +613,7 @@ func asnEncode(key Key) (string, []byte, error) { return pmPrivateKey, buf, nil case RSAPublicKey, ECDSAPublicKey, OKPPublicKey: var rawkey interface{} - if err := key.Raw(&rawkey); err != nil { + if err := Export(key, &rawkey); err != nil { return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err) } buf, err := x509.MarshalPKIXPublicKey(rawkey) diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go index 1c35b9a17..e5ec212de 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -267,7 +267,7 @@ func VerifyKey(t *testing.T, def map[string]keyDef) { typ := expectedRawKeyType(key) var rawkey interface{} - if !assert.NoError(t, key.Raw(&rawkey), `Raw() should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `Raw() should succeed`) { return } if !assert.IsType(t, rawkey, typ, `raw key should be of this type`) { @@ -377,7 +377,7 @@ func TestParse(t *testing.T) { t.Helper() var irawkey interface{} - if !assert.NoError(t, key.Raw(&irawkey), `key.Raw(&interface) should ucceed`) { + if !assert.NoError(t, jwk.Export(key, &irawkey), `key.Raw(&interface) should ucceed`) { return } @@ -393,7 +393,7 @@ func TestParse(t *testing.T) { return } var rawkey rsa.PrivateKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&rsa.PrivateKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&rsa.PrivateKey) should succeed`) { return } crawkey = &rawkey @@ -402,7 +402,7 @@ func TestParse(t *testing.T) { return } var rawkey rsa.PublicKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&rsa.PublicKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&rsa.PublicKey) should succeed`) { return } crawkey = &rawkey @@ -411,7 +411,7 @@ func TestParse(t *testing.T) { return } var rawkey ecdsa.PrivateKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&ecdsa.PrivateKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&ecdsa.PrivateKey) should succeed`) { return } crawkey = &rawkey @@ -422,13 +422,13 @@ func TestParse(t *testing.T) { switch k.Crv() { case jwa.Ed25519: var rawkey ed25519.PrivateKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&ed25519.PrivateKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&ed25519.PrivateKey) should succeed`) { return } crawkey = rawkey case jwa.X25519: var rawkey ecdh.PrivateKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&ecdh.PrivateKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&ecdh.PrivateKey) should succeed`) { return } crawkey = &rawkey @@ -445,13 +445,13 @@ func TestParse(t *testing.T) { switch k.Crv() { case jwa.Ed25519: var rawkey ed25519.PublicKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&ed25519.PublicKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&ed25519.PublicKey) should succeed`) { return } crawkey = rawkey case jwa.X25519: var rawkey ecdh.PublicKey - if !assert.NoError(t, key.Raw(&rawkey), `key.Raw(&ecdh.PublicKey) should succeed`) { + if !assert.NoError(t, jwk.Export(key, &rawkey), `key.Raw(&ecdh.PublicKey) should succeed`) { return } crawkey = &rawkey @@ -940,7 +940,7 @@ func TestPublicKeyOf(t *testing.T) { // Get the raw key to compare var rawKey interface{} - if !assert.NoError(t, pubJwkKey.Raw(&rawKey), `pubJwkKey.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(pubJwkKey, &rawKey), `pubJwkKey.Raw should succeed`) { return } @@ -993,7 +993,7 @@ func TestPublicKeyOf(t *testing.T) { // Get the raw key to compare var rawKey interface{} - if !assert.NoError(t, setKey.Raw(&rawKey), `pubJwkKey.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(setKey, &rawKey), `pubJwkKey.Raw should succeed`) { return } @@ -1476,7 +1476,7 @@ c4wOvhbalcX0FqTM3mXCgMFRbibquhwdxbU= } var pubkey rsa.PublicKey - if !assert.NoError(t, key.Raw(&pubkey), `key.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(key, &pubkey), `key.Raw should succeed`) { return } @@ -2182,7 +2182,7 @@ func TestGH947(t *testing.T) { k, err := jwk.ParseKey(raw) require.NoError(t, err, `jwk.ParseKey should succeed`) var exported []byte - require.Error(t, k.Raw(&exported), `(okpkey).Raw with 0-length OKP key should fail`) + require.Error(t, jwk.Export(k, &exported), `(okpkey).Raw with 0-length OKP key should fail`) } func TestValidation(t *testing.T) { diff --git a/jwk/okp.go b/jwk/okp.go index f78200d09..77a3576eb 100644 --- a/jwk/okp.go +++ b/jwk/okp.go @@ -12,6 +12,10 @@ import ( "github.com/lestrrat-go/jwx/v3/jwa" ) +func init() { + RegisterKeyExporter(jwa.OKP, KeyExportFunc(okpJWKToRaw)) +} + // Mental note: // // Curve25519 refers to a particular curve, and is represented in its Montgomery form. @@ -134,19 +138,30 @@ func buildOKPPrivateKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte, dbuf []byte } } -func (k *okpPrivateKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() +// This is half baked. I think it will blow up if we used ecdh.* keys and/or x25519 keys +func okpJWKToRaw(key Key, hint interface{}) (interface{}, error) { + switch key := key.(type) { + case *okpPrivateKey: + key.mu.RLock() + defer key.mu.RUnlock() - privk, err := buildOKPPrivateKey(k.Crv(), k.x, k.d) - if err != nil { - return fmt.Errorf(`jwk.OKPPrivateKey: failed to build public key: %w`, err) - } + privk, err := buildOKPPrivateKey(key.Crv(), key.x, key.d) + if err != nil { + return nil, fmt.Errorf(`jwk.OKPPrivateKey: failed to build public key: %w`, err) + } + return privk, nil + case *okpPublicKey: + key.mu.RLock() + defer key.mu.RUnlock() - if err := blackmagic.AssignIfCompatible(v, privk); err != nil { - return fmt.Errorf(`jwk.OKPPrivateKey: failed to assign to destination variable: %w`, err) + pubk, err := buildOKPPublicKey(key.Crv(), key.x) + if err != nil { + return nil, fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err) + } + return pubk, nil + default: + return nil, ContinueError() } - return nil } func makeOKPPublicKey(src Key) (Key, error) { diff --git a/jwk/rsa.go b/jwk/rsa.go index f4104a837..6382316f6 100644 --- a/jwk/rsa.go +++ b/jwk/rsa.go @@ -7,11 +7,15 @@ import ( "fmt" "math/big" - "github.com/lestrrat-go/blackmagic" "github.com/lestrrat-go/jwx/v3/internal/base64" "github.com/lestrrat-go/jwx/v3/internal/pool" + "github.com/lestrrat-go/jwx/v3/jwa" ) +func init() { + RegisterKeyExporter(jwa.RSA, KeyExportFunc(rsaJWKToRaw)) +} + func (k *rsaPrivateKey) FromRaw(rawKey *rsa.PrivateKey) error { k.mu.Lock() defer k.mu.Unlock() @@ -97,78 +101,86 @@ func (k *rsaPublicKey) FromRaw(rawKey *rsa.PublicKey) error { return nil } -func (k *rsaPrivateKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() - - var d, q, p big.Int // note: do not use from sync.Pool - - d.SetBytes(k.d) - q.SetBytes(k.q) - p.SetBytes(k.p) - - // optional fields - var dp, dq, qi *big.Int - if len(k.dp) > 0 { - dp = &big.Int{} // note: do not use from sync.Pool - dp.SetBytes(k.dp) - } - - if len(k.dq) > 0 { - dq = &big.Int{} // note: do not use from sync.Pool - dq.SetBytes(k.dq) - } +func buildRSAPublicKey(key *rsa.PublicKey, n, e []byte) { + bin := pool.GetBigInt() + bie := pool.GetBigInt() + defer pool.ReleaseBigInt(bie) - if len(k.qi) > 0 { - qi = &big.Int{} // note: do not use from sync.Pool - qi.SetBytes(k.qi) - } + bin.SetBytes(n) + bie.SetBytes(e) - var key rsa.PrivateKey + key.N = bin + key.E = int(bie.Int64()) +} - pubk := newRSAPublicKey() - pubk.n = k.n - pubk.e = k.e - if err := pubk.Raw(&key.PublicKey); err != nil { - return fmt.Errorf(`failed to materialize RSA public key: %w`, err) - } +func rsaJWKToRaw(key Key, hint interface{}) (interface{}, error) { + switch key := key.(type) { + case *rsaPublicKey: + switch hint.(type) { + case *rsa.PublicKey, *interface{}: + default: + return nil, fmt.Errorf(`invalid destination object type %T for public RSA JWK: %w`, hint, ContinueError()) + } - key.D = &d - key.Primes = []*big.Int{&p, &q} + key.mu.RLock() + defer key.mu.RUnlock() + var pubkey rsa.PublicKey + buildRSAPublicKey(&pubkey, key.n, key.e) - if dp != nil { - key.Precomputed.Dp = dp - } - if dq != nil { - key.Precomputed.Dq = dq - } - if qi != nil { - key.Precomputed.Qinv = qi - } - key.Precomputed.CRTValues = []rsa.CRTValue{} + return &pubkey, nil + case *rsaPrivateKey: + switch hint.(type) { + case *rsa.PrivateKey, *interface{}: + default: + return nil, fmt.Errorf(`invalid destination object type %T for private RSA JWK: %w`, hint, ContinueError()) + } + key.mu.RLock() + defer key.mu.RUnlock() - return blackmagic.AssignIfCompatible(v, &key) -} + var d, q, p big.Int // note: do not use from sync.Pool -// Raw takes the values stored in the Key object, and creates the -// corresponding *rsa.PublicKey object. -func (k *rsaPublicKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() + d.SetBytes(key.d) + q.SetBytes(key.q) + p.SetBytes(key.p) - var key rsa.PublicKey + // optional fields + var dp, dq, qi *big.Int + if len(key.dp) > 0 { + dp = &big.Int{} // note: do not use from sync.Pool + dp.SetBytes(key.dp) + } - n := pool.GetBigInt() - e := pool.GetBigInt() - defer pool.ReleaseBigInt(e) + if len(key.dq) > 0 { + dq = &big.Int{} // note: do not use from sync.Pool + dq.SetBytes(key.dq) + } - n.SetBytes(k.n) - e.SetBytes(k.e) + if len(key.qi) > 0 { + qi = &big.Int{} // note: do not use from sync.Pool + qi.SetBytes(key.qi) + } - key.N = n - key.E = int(e.Int64()) + var privkey rsa.PrivateKey + buildRSAPublicKey(&privkey.PublicKey, key.n, key.e) + privkey.D = &d + privkey.Primes = []*big.Int{&p, &q} - return blackmagic.AssignIfCompatible(v, &key) + if dp != nil { + privkey.Precomputed.Dp = dp + } + if dq != nil { + privkey.Precomputed.Dq = dq + } + if qi != nil { + privkey.Precomputed.Qinv = qi + } + // This may look like a no-op, but it's required if we want to + // compare it against a key generated by rsa.GenerateKey + privkey.Precomputed.CRTValues = []rsa.CRTValue{} + return &privkey, nil + default: + return nil, ContinueError() + } } func makeRSAPublicKey(src Key) (Key, error) { @@ -208,7 +220,7 @@ func (k rsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) { defer k.mu.RUnlock() var key rsa.PrivateKey - if err := k.Raw(&key); err != nil { + if err := Export(&k, &key); err != nil { return nil, fmt.Errorf(`failed to materialize RSA private key: %w`, err) } return rsaThumbprint(hash, &key.PublicKey) @@ -219,7 +231,7 @@ func (k rsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) { defer k.mu.RUnlock() var key rsa.PublicKey - if err := k.Raw(&key); err != nil { + if err := Export(&k, &key); err != nil { return nil, fmt.Errorf(`failed to materialize RSA public key: %w`, err) } return rsaThumbprint(hash, &key) diff --git a/jwk/symmetric.go b/jwk/symmetric.go index af582f429..e58f17920 100644 --- a/jwk/symmetric.go +++ b/jwk/symmetric.go @@ -4,10 +4,14 @@ import ( "crypto" "fmt" - "github.com/lestrrat-go/blackmagic" "github.com/lestrrat-go/jwx/v3/internal/base64" + "github.com/lestrrat-go/jwx/v3/jwa" ) +func init() { + RegisterKeyExporter(jwa.OctetSeq, KeyExportFunc(octetSeqToRaw)) +} + func (k *symmetricKey) FromRaw(rawKey []byte) error { k.mu.Lock() defer k.mu.Unlock() @@ -21,12 +25,17 @@ func (k *symmetricKey) FromRaw(rawKey []byte) error { return nil } -// Raw returns the octets for this symmetric key. -// Since this is a symmetric key, this just calls Octets -func (k *symmetricKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() - return blackmagic.AssignIfCompatible(v, k.octets) +func octetSeqToRaw(key Key, hint interface{}) (interface{}, error) { + switch key := key.(type) { + case *symmetricKey: + key.mu.RLock() + defer key.mu.RUnlock() + octets := make([]byte, len(key.octets)) + copy(octets, key.octets) + return octets, nil + default: + return nil, ContinueError() + } } // Thumbprint returns the JWK thumbprint using the indicated @@ -35,7 +44,7 @@ func (k *symmetricKey) Thumbprint(hash crypto.Hash) ([]byte, error) { k.mu.RLock() defer k.mu.RUnlock() var octets []byte - if err := k.Raw(&octets); err != nil { + if err := Export(k, &octets); err != nil { return nil, fmt.Errorf(`failed to materialize symmetric key: %w`, err) } diff --git a/jws/jws_test.go b/jws/jws_test.go index 08a4a8eb4..8e73ff985 100644 --- a/jws/jws_test.go +++ b/jws/jws_test.go @@ -511,7 +511,7 @@ func TestEncode(t *testing.T) { t.Fatal("Failed to parse JWK") } var key interface{} - if !assert.NoError(t, jwkKey.Raw(&key), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(jwkKey, &key), `jwk.Export should succeed`) { return } var jwsCompact []byte @@ -583,7 +583,7 @@ func TestEncode(t *testing.T) { } var rawkey rsa.PrivateKey - if !assert.NoError(t, privkey.Raw(&rawkey), `obtaining raw key should succeed`) { + if !assert.NoError(t, jwk.Export(privkey, &rawkey), `obtaining raw key should succeed`) { return } @@ -660,7 +660,7 @@ func TestEncode(t *testing.T) { } var rawkey ecdsa.PrivateKey - if !assert.NoError(t, privkey.Raw(&rawkey), `obtaining raw key should succeed`) { + if !assert.NoError(t, jwk.Export(privkey, &rawkey), `obtaining raw key should succeed`) { return } @@ -745,7 +745,7 @@ func TestEncode(t *testing.T) { } var rawkey ed25519.PrivateKey - if !assert.NoError(t, privkey.Raw(&rawkey), `obtaining raw key should succeed`) { + if !assert.NoError(t, jwk.Export(privkey, &rawkey), `obtaining raw key should succeed`) { return } @@ -1024,7 +1024,7 @@ func TestDecode_ES384Compact_NoSigTrim(t *testing.T) { } var rawkey ecdsa.PublicKey - if !assert.NoError(t, pubkey.Raw(&rawkey), `obtaining raw key should succeed`) { + if !assert.NoError(t, jwk.Export(pubkey, &rawkey), `obtaining raw key should succeed`) { return } diff --git a/jwx_test.go b/jwx_test.go index b3dbca8d7..d9e466a0b 100644 --- a/jwx_test.go +++ b/jwx_test.go @@ -162,7 +162,7 @@ func TestJoseCompatibility(t *testing.T) { } } - if !assert.NoError(t, webkey.Raw(&tc.Raw), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(webkey, &tc.Raw), `jwk.Export should succeed`) { return } }) @@ -293,17 +293,17 @@ func joseInteropTest(ctx context.Context, spec interopTest, t *testing.T) { switch spec.alg { case jwa.RSA1_5, jwa.RSA_OAEP, jwa.RSA_OAEP_256: var rawkey rsa.PrivateKey - if !assert.NoError(t, jwxJwk.Raw(&rawkey), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(jwxJwk, &rawkey), `jwk.Export should succeed`) { return } case jwa.ECDH_ES, jwa.ECDH_ES_A128KW, jwa.ECDH_ES_A192KW, jwa.ECDH_ES_A256KW: var rawkey ecdsa.PrivateKey - if !assert.NoError(t, jwxJwk.Raw(&rawkey), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(jwxJwk, &rawkey), `jwk.Export should succeed`) { return } default: var rawkey []byte - if !assert.NoError(t, jwxJwk.Raw(&rawkey), `jwk.Raw should succeed`) { + if !assert.NoError(t, jwk.Export(jwxJwk, &rawkey), `jwk.Export should succeed`) { return } } diff --git a/tools/cmd/genjwk/main.go b/tools/cmd/genjwk/main.go index cd975e946..40992d776 100644 --- a/tools/cmd/genjwk/main.go +++ b/tools/cmd/genjwk/main.go @@ -687,19 +687,6 @@ func generateGenericHeaders(fields codegen.FieldList) error { o.L("// Validate is never called by `UnmarshalJSON()` or `Set`. It must explicitly be") o.L("// called by the user") o.L("Validate() error") - o.LL("// Raw creates the corresponding raw key from the jwk.Key. For example,") - o.L("// EC keys would create *ecdsa.PublicKey or *ecdsa.PrivateKey,") - o.L("// and OctetSeq types create a []byte key.") - o.L("//\n// If you do not know the exact type of a jwk.Key before attempting") - o.L("// to obtain the raw key, you can simply pass a pointer to an") - o.L("// empty interface as the first argument (important caveat: this can only") - o.L("// be done for keys that are defined in this package. If you are using keys") - o.L("// imported from third party modules, you will need to know the exact type") - o.L("// before calling this method).") - o.L("//\n// If you already know the exact type, it is recommended that you") - o.L("// pass a pointer to the zero value of the actual key type (e.g. &rsa.PrivateKey)") - o.L("// for efficiency.") - o.L("Raw(interface{}) error") o.LL("// Thumbprint returns the JWK thumbprint using the indicated") o.L("// hashing algorithm, according to RFC 7638") o.L("Thumbprint(crypto.Hash) ([]byte, error)") From 479c643e8a5557bcfcc26ac48d3a231416504c7d Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 21:29:46 +0900 Subject: [PATCH 14/21] fix typo --- jwk/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jwk/doc.go b/jwk/doc.go index d783ed295..e622053d4 100644 --- a/jwk/doc.go +++ b/jwk/doc.go @@ -243,7 +243,7 @@ // var dst *ecdsa.PrivateKey // key.Raw(&dst) // Error, if key is an RSA key // -// You will need to handle this distinction youself in your KeyImporter. For example, certain +// You will need to handle this distinction yourself in your KeyImporter. For example, certain // elliptic curve keys can be expressed in JWK in the same format, minus the "kty". In that case // you will need to check for the type of the destination object and return an error if it is // not compatible with your key. From 94d41d20a66f6f97110c046bd0769ce5bd70e6a4 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 21:32:55 +0900 Subject: [PATCH 15/21] appease linter --- jwk/okp.go | 2 +- jwk/symmetric.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/jwk/okp.go b/jwk/okp.go index 77a3576eb..2bbbc7041 100644 --- a/jwk/okp.go +++ b/jwk/okp.go @@ -139,7 +139,7 @@ func buildOKPPrivateKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte, dbuf []byte } // This is half baked. I think it will blow up if we used ecdh.* keys and/or x25519 keys -func okpJWKToRaw(key Key, hint interface{}) (interface{}, error) { +func okpJWKToRaw(key Key, _ interface{} /* this is unused because this is half baked */) (interface{}, error) { switch key := key.(type) { case *okpPrivateKey: key.mu.RLock() diff --git a/jwk/symmetric.go b/jwk/symmetric.go index e58f17920..1df7c6c15 100644 --- a/jwk/symmetric.go +++ b/jwk/symmetric.go @@ -28,6 +28,9 @@ func (k *symmetricKey) FromRaw(rawKey []byte) error { func octetSeqToRaw(key Key, hint interface{}) (interface{}, error) { switch key := key.(type) { case *symmetricKey: + if _, ok := hint.([]byte); !ok { + return nil, fmt.Errorf(`invalid destination object type %T for symmetric key: %w`, hint, ContinueError()) + } key.mu.RLock() defer key.mu.RUnlock() octets := make([]byte, len(key.octets)) From 81f489fe4e078b7fe967c24355852a02a79c1d73 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Tue, 21 Nov 2023 21:37:45 +0900 Subject: [PATCH 16/21] fix type detection for symmetric keys --- jwk/symmetric.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jwk/symmetric.go b/jwk/symmetric.go index 1df7c6c15..36e429618 100644 --- a/jwk/symmetric.go +++ b/jwk/symmetric.go @@ -28,7 +28,9 @@ func (k *symmetricKey) FromRaw(rawKey []byte) error { func octetSeqToRaw(key Key, hint interface{}) (interface{}, error) { switch key := key.(type) { case *symmetricKey: - if _, ok := hint.([]byte); !ok { + switch hint.(type) { + case *[]byte, *interface{}: + default: return nil, fmt.Errorf(`invalid destination object type %T for symmetric key: %w`, hint, ContinueError()) } key.mu.RLock() From 1ef99f75a22487b61f539f6f9ec0e3cba1be1232 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Thu, 30 Nov 2023 18:47:30 +0900 Subject: [PATCH 17/21] rework OKP tests so that crypto/ecdh keys are tested --- jwk/jwk_test.go | 155 +++++++++++++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 55 deletions(-) diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go index e5ec212de..6d5bb23df 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -7,6 +7,7 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" + "crypto/rand" "crypto/rsa" "fmt" "io" @@ -1329,67 +1330,111 @@ func TestSymmetric(t *testing.T) { func TestOKP(t *testing.T) { t.Parallel() - t.Run("Ed25519", func(t *testing.T) { - t.Parallel() - t.Run("PrivateKey", func(t *testing.T) { - t.Parallel() - VerifyKey(t, map[string]keyDef{ - jwk.KeyTypeKey: { - Method: "KeyType", - Value: jwa.OKP, + ecdhkey, err := ecdh.P256().GenerateKey(rand.Reader) + require.NoError(t, err, `ecdh.P256().GenerateKey should succeed`) + + _, ed25519privkey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err, `ed25519.GenerateKey should succeed`) + + keys := map[string][]struct { + Name string + Data map[string]keyDef + }{ + "Ed25519": { + { + Name: "PrivateKey", + Data: map[string]keyDef{ + jwk.KeyTypeKey: { + Method: "KeyType", + Value: jwa.OKP, + }, + jwk.OKPDKey: expectBase64(keyDef{ + Method: "D", + Value: base64.EncodeToString(ed25519privkey.Seed()), + }), + jwk.OKPXKey: expectBase64(keyDef{ + Method: "X", + Value: base64.EncodeToString(ed25519privkey.Public().(ed25519.PublicKey)), + }), + jwk.OKPCrvKey: { + Method: "Crv", + Value: jwa.Ed25519, + }, }, - jwk.OKPDKey: expectBase64(keyDef{ - Method: "D", - Value: "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", - }), - jwk.OKPXKey: expectBase64(keyDef{ - Method: "X", - Value: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - }), - jwk.OKPCrvKey: { - Method: "Crv", - Value: jwa.Ed25519, + }, + { + Name: "PublicKey", + Data: map[string]keyDef{ + jwk.KeyTypeKey: { + Method: "KeyType", + Value: jwa.OKP, + }, + jwk.OKPXKey: expectBase64(keyDef{ + Method: "X", + Value: base64.EncodeToString(ed25519privkey.Public().(ed25519.PublicKey)), + }), + jwk.OKPCrvKey: { + Method: "Crv", + Value: jwa.Ed25519, + }, }, - }) - }) - t.Run("PublicKey", func(t *testing.T) { - t.Parallel() - VerifyKey(t, map[string]keyDef{ - jwk.KeyTypeKey: { - Method: "KeyType", - Value: jwa.OKP, + }, + }, + "ECDH": { + { + Name: "PrivateKey", + Data: map[string]keyDef{ + jwk.KeyTypeKey: { + Method: "KeyType", + Value: jwa.OKP, + }, + jwk.OKPDKey: expectBase64(keyDef{ + Method: "D", + Value: base64.EncodeToString(ecdhkey.Bytes()), + }), + jwk.OKPXKey: expectBase64(keyDef{ + Method: "X", + Value: base64.EncodeToString(ecdhkey.Public().(*ecdh.PublicKey).Bytes()), + }), + jwk.OKPCrvKey: { + Method: "Crv", + Value: jwa.X25519, + }, }, - jwk.OKPXKey: expectBase64(keyDef{ - Method: "X", - Value: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", - }), - jwk.OKPCrvKey: { - Method: "Crv", - Value: jwa.Ed25519, + }, + { + Name: "PublicKey", + Data: map[string]keyDef{ + jwk.KeyTypeKey: { + Method: "KeyType", + Value: jwa.OKP, + }, + jwk.OKPXKey: expectBase64(keyDef{ + Method: "X", + Value: base64.EncodeToString(ecdhkey.Public().(*ecdh.PublicKey).Bytes()), + }), + jwk.OKPCrvKey: { + Method: "Crv", + Value: jwa.X25519, + }, }, - }) - }) - }) - t.Run("X25519", func(t *testing.T) { - t.Parallel() - t.Run("PublicKey", func(t *testing.T) { + }, + }, + } + + for typ, keys := range keys { + keys := keys + t.Run(typ, func(t *testing.T) { t.Parallel() - VerifyKey(t, map[string]keyDef{ - jwk.KeyTypeKey: { - Method: "KeyType", - Value: jwa.OKP, - }, - jwk.OKPXKey: expectBase64(keyDef{ - Method: "X", - Value: "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08", - }), - jwk.OKPCrvKey: { - Method: "Crv", - Value: jwa.X25519, - }, - }) + for _, key := range keys { + key := key + t.Run(key.Name, func(t *testing.T) { + t.Parallel() + VerifyKey(t, key.Data) + }) + } }) - }) + } } func TestCustomField(t *testing.T) { From abafb9f089603f451ec214463157f5e6e62df8bb Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Fri, 1 Dec 2023 09:31:51 +0900 Subject: [PATCH 18/21] fix handling of x25519 keys --- jwk/jwk_test.go | 21 ++++++++++----------- jwk/okp.go | 16 ++++++---------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go index 6d5bb23df..16f70a4b7 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -11,6 +11,7 @@ import ( "crypto/rsa" "fmt" "io" + "log" "math/big" "net/http" "net/http/httptest" @@ -174,9 +175,7 @@ func VerifyKey(t *testing.T, def map[string]keyDef) { def = complimentDef(def) key, err := jwk.ParseKey(makeKeyJSON(def)) - if !assert.NoError(t, err, `jwk.ParseKey should succeed`) { - return - } + require.NoError(t, err, `jwk.ParseKey should succeed`) t.Run("Fields", func(t *testing.T) { for k, kdef := range def { @@ -268,12 +267,8 @@ func VerifyKey(t *testing.T, def map[string]keyDef) { typ := expectedRawKeyType(key) var rawkey interface{} - if !assert.NoError(t, jwk.Export(key, &rawkey), `Raw() should succeed`) { - return - } - if !assert.IsType(t, rawkey, typ, `raw key should be of this type`) { - return - } + require.NoError(t, jwk.Export(key, &rawkey), `Raw() should succeed`) + require.IsType(t, rawkey, typ, `raw key should be of this type`) }) t.Run("PublicKey", func(t *testing.T) { _, err := jwk.PublicKeyOf(key) @@ -1332,6 +1327,10 @@ func TestOKP(t *testing.T) { ecdhkey, err := ecdh.P256().GenerateKey(rand.Reader) require.NoError(t, err, `ecdh.P256().GenerateKey should succeed`) + x, err := ecdhkey.ECDH(ecdhkey.PublicKey()) + require.NoError(t, err, `ecdhkey.ECDH should succeed`) + + log.Printf("ecdhkey.PublicKey().Bytes(): %x", ecdhkey.PublicKey().Bytes()) _, ed25519privkey, err := ed25519.GenerateKey(rand.Reader) require.NoError(t, err, `ed25519.GenerateKey should succeed`) @@ -1394,7 +1393,7 @@ func TestOKP(t *testing.T) { }), jwk.OKPXKey: expectBase64(keyDef{ Method: "X", - Value: base64.EncodeToString(ecdhkey.Public().(*ecdh.PublicKey).Bytes()), + Value: base64.EncodeToString(x), }), jwk.OKPCrvKey: { Method: "Crv", @@ -1411,7 +1410,7 @@ func TestOKP(t *testing.T) { }, jwk.OKPXKey: expectBase64(keyDef{ Method: "X", - Value: base64.EncodeToString(ecdhkey.Public().(*ecdh.PublicKey).Bytes()), + Value: base64.EncodeToString(x), }), jwk.OKPCrvKey: { Method: "Crv", diff --git a/jwk/okp.go b/jwk/okp.go index 2bbbc7041..d1617c3b2 100644 --- a/jwk/okp.go +++ b/jwk/okp.go @@ -67,7 +67,7 @@ func (k *okpPrivateKey) FromRaw(rawKeyIf interface{}) error { case *ecdh.PrivateKey: // k.d = rawKey.Seed() k.d = rawKey.Bytes() - k.x = rawKey.Public().(*ecdh.PublicKey).Bytes() //nolint:forcetypeassert + k.x = rawKey.PublicKey().Bytes() crv = jwa.X25519 k.crv = &crv default: @@ -84,7 +84,7 @@ func buildOKPPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte) (interface{} case jwa.X25519: ret, err := ecdh.X25519().NewPublicKey(xbuf) if err != nil { - return nil, fmt.Errorf(`failed to parse x25519 public key: %w`, err) + return nil, fmt.Errorf(`failed to parse x25519 public key %x (size %d): %w`, xbuf, len(xbuf), err) } return ret, nil default: @@ -115,22 +115,18 @@ func buildOKPPrivateKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte, dbuf []byte switch alg { case jwa.Ed25519: if len(dbuf) != ed25519.SeedSize { - return nil, fmt.Errorf(`wrong private key size`) + return nil, fmt.Errorf(`ed25519: wrong private key size`) } ret := ed25519.NewKeyFromSeed(dbuf) //nolint:forcetypeassert if !bytes.Equal(xbuf, ret.Public().(ed25519.PublicKey)) { - return nil, fmt.Errorf(`invalid x value given d value`) + return nil, fmt.Errorf(`ed25519: invalid x value given d value`) } return ret, nil case jwa.X25519: ret, err := ecdh.X25519().NewPrivateKey(dbuf) if err != nil { - return nil, fmt.Errorf(`unable to construct x25519 private key from seed: %w`, err) - } - //nolint:forcetypeassert - if !bytes.Equal(xbuf, ret.Public().(*ecdh.PublicKey).Bytes()) { - return nil, fmt.Errorf(`invalid x value given d value`) + return nil, fmt.Errorf(`x25519: unable to construct x25519 private key from seed: %w`, err) } return ret, nil default: @@ -147,7 +143,7 @@ func okpJWKToRaw(key Key, _ interface{} /* this is unused because this is half b privk, err := buildOKPPrivateKey(key.Crv(), key.x, key.d) if err != nil { - return nil, fmt.Errorf(`jwk.OKPPrivateKey: failed to build public key: %w`, err) + return nil, fmt.Errorf(`jwk.OKPPrivateKey: failed to build private key: %w`, err) } return privk, nil case *okpPublicKey: From 23ae5b978559d4cb54d68009de3d73cd34db6da8 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Fri, 1 Dec 2023 21:46:43 +0900 Subject: [PATCH 19/21] tweak --- examples/jwx_register_ec_and_key_example_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/jwx_register_ec_and_key_example_test.go b/examples/jwx_register_ec_and_key_example_test.go index e03779566..4c0261588 100644 --- a/examples/jwx_register_ec_and_key_example_test.go +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -17,7 +17,7 @@ import ( // Setup. This is something that you probably should do in your adapter // library, or in your application's init() function. - +// // I could not readily find what the exact curve notation is for ShangMi SM2 // (either I'm just bad at researching or it's not in an RFC as of this writing) // so I'm faking it as "SM2". @@ -27,8 +27,6 @@ import ( const SM2 jwa.EllipticCurveAlgorithm = "SM2" func init() { - shangmi2pk, _ := sm2.GenerateKey(rand.Reader) - // Register the algorithm name so it can be looked up jwa.RegisterEllipticCurveAlgorithm(SM2) @@ -39,7 +37,7 @@ func init() { // We only need one converter for the private key, because the public key // is exactly the same type as *ecdsa.PublicKey - jwk.RegisterKeyImporter(shangmi2pk, jwk.KeyImportFunc(convertShangMiSm2)) + jwk.RegisterKeyImporter(&sm2.PrivateKey{}, jwk.KeyImportFunc(convertShangMiSm2)) jwk.RegisterKeyExporter(jwa.EC, jwk.KeyExportFunc(convertJWKToShangMiSm2)) } From 23dc02176211a34de2654960c0fae1f8a2007ed8 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Wed, 6 Dec 2023 10:45:27 +0900 Subject: [PATCH 20/21] tweak docs --- jwk/doc.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/jwk/doc.go b/jwk/doc.go index e622053d4..7e665e4b8 100644 --- a/jwk/doc.go +++ b/jwk/doc.go @@ -26,18 +26,28 @@ // // You can use them to sign/verify/encrypt/decrypt: // -// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey)) -// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey)) +// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey)) +// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey)) // // See examples/jwk_parse_example_test.go and other files in the exmaples/ directory for more. // -// # Registering a custom key type +// # Advanced Usage: Registering a custom key type and conversion routines // -// (Caveat Emptor) This functionality should be considered experimental. While we -// expect that the functionality itself will remain, the API may +// Caveat Emptor: Functionality around registering keys +// (KeyProbe/KeyParser/KeyImporter/KeyExporter) should be considered experimental. +// While we expect that the functionality itself will remain, the API may // change in backward incompatible ways, even during minor version // releases. // +// ## tl;dr +// +// * KeyProbe: Used for parsing JWKs in JSON format. Probes hint fields to be used for later parsing by KeyParser +// * KeyParser: Used for parsing JWKs in JSON format. Parses the JSON payload into a jwk.Key using the KeyProbe as hint +// * KeyImporter: Used for converting raw key into jwk.Key. +// * KeyExporter: Used for converting jwk.Key into raw key. +// +// ## Overview +// // You can add the ability to use a JWK type that this library does not // implement out of the box. You can do this by registering your own // KeyParser, KeyImporter, and KeyExporter instances. @@ -122,7 +132,7 @@ // This mechanism allows you to be flexible when trying to determine the key type // to instantiate. // -// # Registering a KeyParser +// ## Parse via the KeyParser // // When `jwk.Parse` / `jwk.ParseKey` is called, the library will first probe // the payload as discussed above. From af0cd498ee1bea344e1fdaf8c9a3799c0001f1b0 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Wed, 6 Dec 2023 10:49:23 +0900 Subject: [PATCH 21/21] gofmt --- jwk/doc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jwk/doc.go b/jwk/doc.go index 7e665e4b8..a35e8b6e3 100644 --- a/jwk/doc.go +++ b/jwk/doc.go @@ -26,8 +26,8 @@ // // You can use them to sign/verify/encrypt/decrypt: // -// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey)) -// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey)) +// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey)) +// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey)) // // See examples/jwk_parse_example_test.go and other files in the exmaples/ directory for more. //