From 39b4b150d7d1ba5a1a88606094aa6e2e2bc08219 Mon Sep 17 00:00:00 2001 From: lestrrat <49281+lestrrat@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:00:20 +0900 Subject: [PATCH] Custom elliptic curve example (#1012) * Add an example adding new EC algo and key type * Add failing example * Kind of fix, but still need to review spec * Appease linter * Add more context for debugging * fix directive * See if bumping go version to 1.21 only makes a difference * fix one more * Rename RJ/JR to Import/Export * fix usage * docs * oops, add missing file * Streamline exporting from jwk.Key to raw key Remove Raw() from keys, and implement jwk.Export * fix typo * appease linter * fix type detection for symmetric keys * rework OKP tests so that crypto/ecdh keys are tested * fix handling of x25519 keys * tweak * tweak docs * gofmt --- .github/workflows/ci.yml | 2 +- .github/workflows/smoke.yml | 2 +- Changes-v3.md | 10 +- examples/go.mod | 1 + examples/go.sum | 2 + examples/jwk_example_test.go | 2 +- .../jwx_register_ec_and_key_example_test.go | 169 ++++++++++ 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 | 158 ++++++++-- jwk/doc.go | 294 ++++++++++++++++++ jwk/ecdsa.go | 62 ++-- jwk/interface_gen.go | 13 - jwk/jwk.go | 130 +------- jwk/jwk_test.go | 196 +++++++----- jwk/okp.go | 49 +-- jwk/rsa.go | 138 ++++---- jwk/symmetric.go | 30 +- jws/jws_test.go | 10 +- jwx_test.go | 8 +- tools/cmd/genjwk/main.go | 10 - 24 files changed, 926 insertions(+), 404 deletions(-) create mode 100644 examples/jwx_register_ec_and_key_example_test.go create mode 100644 jwk/doc.go 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 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 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/go.mod b/examples/go.mod index 7c84d927d..28dd22fec 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 16de4a22e..d528685ba 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= 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 new file mode 100644 index 000000000..4c0261588 --- /dev/null +++ b/examples/jwx_register_ec_and_key_example_test.go @@ -0,0 +1,169 @@ +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" + "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() { + // 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.RegisterKeyImporter(&sm2.PrivateKey{}, jwk.KeyImportFunc(convertShangMiSm2)) + + jwk.RegisterKeyExporter(jwa.EC, jwk.KeyExportFunc(convertJWKToShangMiSm2)) +} + +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) +} + +func convertJWKToShangMiSm2(key jwk.Key, hint interface{}) (interface{}, error) { + 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()) + } + + 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() { + shangmi2pk, _ := sm2.GenerateKey(rand.Reader) + + // 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: %s\n", err) + return + } + + { + // Create a ShangMi SM2 private key back from the jwk.Key + var clone sm2.PrivateKey + if err := jwk.Export(shangmi2JWK, &clone); err != nil { + fmt.Printf("failed to create ShangMi private key from jwk.Key: %s\n", 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 := jwk.Export(shangmi2JWK, &clone); err != nil { + fmt.Printf("failed to create ShangMi private key from jwk.Key (via interface{}): %s\n", 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.Printf("failed to create jwk.Key from raw ShangMi public key: %s\n", err) + return + } + var clone ecdsa.PrivateKey + if err := jwk.Export(eckjwk, &clone); err != nil { + fmt.Printf("failed to create ShangMi public key from jwk.Key: %s\n", err) + return + } + } + + 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: %s\n", err) + return + } + + shangmi2PubJWK, err := jwk.PublicKeyOf(shangmi2JWK) + if err != nil { + 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: %s\n", err) + return + } + + if !bytes.Equal(payload, verified) { + fmt.Println("payload does not match") + return + } + //OUTPUT: +} 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 fe1cd3d17..1a54c6f4d 100644 --- a/jwe/jwe.go +++ b/jwe/jwe.go @@ -92,7 +92,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) } @@ -617,7 +617,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 @@ -654,13 +654,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 44c55901f..63938842c 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 3c71f3f32..517b10cf0 100644 --- a/jwk/convert.go +++ b/jwk/convert.go @@ -8,73 +8,128 @@ import ( "fmt" "reflect" "sync" + + "github.com/lestrrat-go/blackmagic" + "github.com/lestrrat-go/jwx/v3/jwa" ) -// # Converting Raw Keys To `jwk.Key`s -// -// You can register a convert from a raw key to a `jwk.Key` by calling -// `jwk.RegisterKeyConverter`. +// # Converting between Raw Keys and `jwk.Key`s // +// 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 keyConverters = make(map[reflect.Type]KeyConverter) +var keyImporters = make(map[reflect.Type]KeyImporter) +var keyExporters = make(map[jwa.KeyType][]KeyExporter) -var muKeyConverters sync.RWMutex +var muKeyImporters sync.RWMutex +var muKeyExporters sync.RWMutex -func RegisterKeyConverter(from interface{}, conv KeyConverter) { - muKeyConverters.Lock() - defer muKeyConverters.Unlock() - keyConverters[reflect.TypeOf(from)] = conv +// 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) { + muKeyImporters.Lock() + defer muKeyImporters.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) { + muKeyExporters.Lock() + defer muKeyExporters.Unlock() + convs, ok := keyExporters[kty] + if !ok { + convs = []KeyExporter{conv} + } else { + convs = append([]KeyExporter{conv}, convs...) + } + keyExporters[kty] = convs } -type KeyConverter interface { - FromRaw(interface{}) (Key, error) +// 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 { + // Import takes the raw key to be converted, and returns a `jwk.Key` or an error if the conversion fails. + Import(interface{}) (Key, error) } -type KeyConvertFunc func(interface{}) (Key, error) +// KeyImportFunc is a convenience type to implement KeyImporter as a function. +type KeyImportFunc func(interface{}) (Key, error) -func (f KeyConvertFunc) 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 { + // 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.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. + // + // Note that the second argument may be an `interface{}` (which means that the + // user has delegated the type detection to the converter). + // + // Export must NOT modify the hint object, and should return jwk.ContinueError + // if the hint object is not compatible with the converter. + 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) Export(key Key, hint interface{}) (interface{}, error) { + return f(key, hint) +} + func init() { { - f := KeyConvertFunc(rsaPrivateKeyToJWK) + f := KeyImportFunc(rsaPrivateKeyToJWK) k := rsa.PrivateKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := KeyConvertFunc(rsaPublicKeyToJWK) + f := KeyImportFunc(rsaPublicKeyToJWK) k := rsa.PublicKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := KeyConvertFunc(ecdsaPrivateKeyToJWK) + f := KeyImportFunc(ecdsaPrivateKeyToJWK) k := ecdsa.PrivateKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := KeyConvertFunc(ecdsaPublicKeyToJWK) + f := KeyImportFunc(ecdsaPublicKeyToJWK) k := ecdsa.PublicKey{} - RegisterKeyConverter(k, f) - RegisterKeyConverter(&k, f) + RegisterKeyImporter(k, f) + RegisterKeyImporter(&k, f) } { - f := KeyConvertFunc(okpPrivateKeyToJWK) + f := KeyImportFunc(okpPrivateKeyToJWK) for _, k := range []interface{}{ed25519.PrivateKey(nil), ecdh.PrivateKey{}, &ecdh.PrivateKey{}} { - RegisterKeyConverter(k, f) + RegisterKeyImporter(k, f) } } { - f := KeyConvertFunc(okpPublicKeyToJWK) + f := KeyImportFunc(okpPublicKeyToJWK) for _, k := range []interface{}{ed25519.PublicKey(nil), ecdh.PublicKey{}, &ecdh.PublicKey{}} { - RegisterKeyConverter(k, f) + RegisterKeyImporter(k, f) } } - RegisterKeyConverter([]byte(nil), KeyConvertFunc(bytesToKey)) + RegisterKeyImporter([]byte(nil), KeyImportFunc(bytesToKey)) } // These may seem a bit repetitive and redandunt, but the problem is that @@ -198,3 +253,44 @@ func bytesToKey(src interface{}) (Key, error) { } return k, nil } + +// 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(`jwk.Export: destination object must be a pointer`) + } + 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(`jwk.Export: failed to export jwk.Key to raw format: %w`, err) + } + + if err := blackmagic.AssignIfCompatible(dst, v); err != nil { + return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err) + } + return nil + } + } + return fmt.Errorf(`jwk.Export: failed to find exporter for key type '%T'`, key) +} diff --git a/jwk/doc.go b/jwk/doc.go new file mode 100644 index 000000000..a35e8b6e3 --- /dev/null +++ b/jwk/doc.go @@ -0,0 +1,294 @@ +// 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. +// +// # Advanced Usage: Registering a custom key type and conversion routines +// +// 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. +// +// 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. +// +// ## Parse via the 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 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. +// +// 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 diff --git a/jwk/ecdsa.go b/jwk/ecdsa.go index fdf6e3363..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" @@ -18,6 +17,8 @@ func init() { ourecdsa.RegisterCurve(jwa.P256, elliptic.P256()) ourecdsa.RegisterCurve(jwa.P384, elliptic.P384()) ourecdsa.RegisterCurve(jwa.P521, elliptic.P521()) + + RegisterKeyExporter(jwa.EC, KeyExportFunc(ecdsaJWKToRaw)) } func (k *ecdsaPublicKey) FromRaw(rawKey *ecdsa.PublicKey) error { @@ -101,35 +102,42 @@ 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() +func ecdsaJWKToRaw(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()) + } - pubk, err := buildECDSAPublicKey(k.Crv(), k.x, k.y) - if err != nil { - return fmt.Errorf(`failed to build public key: %w`, err) - } + 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()) + } - return blackmagic.AssignIfCompatible(v, pubk) -} + 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) + } -func (k *ecdsaPrivateKey) Raw(v interface{}) error { - k.mu.RLock() - defer k.mu.RUnlock() + var key ecdsa.PrivateKey + var d big.Int + d.SetBytes(k.d) + key.D = &d + key.PublicKey = *pubk - pubk, err := buildECDSAPublicKey(k.Crv(), k.x, k.y) - if err != nil { - return fmt.Errorf(`failed to build public key: %w`, err) + return &key, nil + default: + return nil, ContinueError() } - - var key ecdsa.PrivateKey - var d big.Int - d.SetBytes(k.d) - key.D = &d - key.PublicKey = *pubk - - return blackmagic.AssignIfCompatible(v, &key) } func makeECDSAPublicKey(src Key) (Key, error) { @@ -182,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) } @@ -206,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 5820fd680..1d08dd984 100644 --- a/jwk/interface_gen.go +++ b/jwk/interface_gen.go @@ -67,19 +67,6 @@ 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, - // 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. - // - // 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 2c97d7d7f..6948d10db 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 @@ -189,14 +81,14 @@ 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() + muKeyImporters.RLock() + conv, ok := keyImporters[reflect.TypeOf(raw)] + muKeyImporters.RUnlock() if !ok { 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 @@ -282,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 @@ -309,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) } @@ -426,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) } @@ -711,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) @@ -721,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..16f70a4b7 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -7,9 +7,11 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/ed25519" + "crypto/rand" "crypto/rsa" "fmt" "io" + "log" "math/big" "net/http" "net/http/httptest" @@ -173,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 { @@ -267,12 +267,8 @@ 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`) { - 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) @@ -377,7 +373,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 +389,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 +398,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 +407,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 +418,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 +441,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 +936,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 +989,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 } @@ -1329,67 +1325,115 @@ 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`) + 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`) + + 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(x), + }), + 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(x), + }), + 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) { @@ -1476,7 +1520,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 +2226,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..d1617c3b2 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. @@ -63,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: @@ -80,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: @@ -111,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: @@ -134,19 +134,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, _ interface{} /* this is unused because this is half baked */) (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 private 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..36e429618 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,22 @@ 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: + 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() + 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 +49,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 17d65dba7..1fd721f24 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 7f7bf4b18..40992d776 100644 --- a/tools/cmd/genjwk/main.go +++ b/tools/cmd/genjwk/main.go @@ -687,16 +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. For example,") - o.L("// EC types 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("//\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)")