diff --git a/cmd/root.go b/cmd/root.go index 4cc2989d..a9e15958 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -162,5 +162,6 @@ func NewPolycliCommand() *cobra.Command { dockerlogger.Cmd, contract.Cmd, ) + return cmd } diff --git a/cmd/signer/create/create.go b/cmd/signer/create/create.go new file mode 100644 index 00000000..1582213f --- /dev/null +++ b/cmd/signer/create/create.go @@ -0,0 +1,66 @@ +package create + +import ( + _ "embed" + "encoding/hex" + "fmt" + + "github.com/0xPolygon/polygon-cli/signer" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/crypto" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +//go:embed usage.md +var usage string + +var CreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new key.", + Long: usage, + Args: cobra.NoArgs, + PreRunE: signer.SanityCheck, + RunE: func(cmd *cobra.Command, args []string) error { + opts := signer.InputOpts + if opts.Keystore == "" && opts.KMS == "" { + log.Info().Msg("Generating new private hex key and writing to stdout") + pk, err := crypto.GenerateKey() + if err != nil { + return err + } + k := hex.EncodeToString(crypto.FromECDSA(pk)) + fmt.Println(k) + return nil + } + if opts.Keystore != "" { + ks := keystore.NewKeyStore(opts.Keystore, keystore.StandardScryptN, keystore.StandardScryptP) + pk, err := crypto.GenerateKey() + if err != nil { + return err + } + password, err := signer.GetKeystorePassword() + if err != nil { + return err + } + acc, err := ks.ImportECDSA(pk, password) + if err != nil { + return err + } + log.Info().Str("address", acc.Address.String()).Msg("imported new account") + return nil + } + if opts.KMS == "GCP" { + gcpKMS := signer.GCPKMS{} + err := gcpKMS.CreateKeyRing(cmd.Context()) + if err != nil { + return err + } + err = gcpKMS.CreateKey(cmd.Context()) + if err != nil { + return err + } + } + return nil + }, +} diff --git a/cmd/signer/createCmdUsage.md b/cmd/signer/create/usage.md similarity index 100% rename from cmd/signer/createCmdUsage.md rename to cmd/signer/create/usage.md diff --git a/cmd/signer/import/import.go b/cmd/signer/import/import.go new file mode 100644 index 00000000..39cb04b4 --- /dev/null +++ b/cmd/signer/import/import.go @@ -0,0 +1,51 @@ +package importcmd + +import ( + _ "embed" + "fmt" + + "github.com/0xPolygon/polygon-cli/signer" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/cobra" +) + +//go:embed usage.md +var usage string + +var ImportCmd = &cobra.Command{ + Use: "import", + Short: "Import a private key into the keyring / keystore.", + Long: usage, + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := signer.SanityCheck(cmd, args); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts := signer.InputOpts + if opts.Keystore != "" { + ks := keystore.NewKeyStore(opts.Keystore, keystore.StandardScryptN, keystore.StandardScryptP) + pk, err := crypto.HexToECDSA(opts.PrivateKey) + if err != nil { + return err + } + pass, err := signer.GetKeystorePassword() + if err != nil { + return err + } + _, err = ks.ImportECDSA(pk, pass) + return err + } + if opts.KMS == "GCP" { + gcpKMS := signer.GCPKMS{} + if err := gcpKMS.CreateImportJob(cmd.Context()); err != nil { + return err + } + return gcpKMS.ImportKey(cmd.Context()) + } + return fmt.Errorf("unable to import key") + }, +} diff --git a/cmd/signer/importCmdUsage.md b/cmd/signer/import/usage.md similarity index 100% rename from cmd/signer/importCmdUsage.md rename to cmd/signer/import/usage.md diff --git a/cmd/signer/list/list.go b/cmd/signer/list/list.go new file mode 100644 index 00000000..0a25d7b8 --- /dev/null +++ b/cmd/signer/list/list.go @@ -0,0 +1,38 @@ +package list + +import ( + _ "embed" + "fmt" + + "github.com/0xPolygon/polygon-cli/signer" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +//go:embed usage.md +var usage string + +var ListCmd = &cobra.Command{ + Use: "list", + Short: "List the keys in the keyring / keystore.", + Long: usage, + Args: cobra.NoArgs, + PreRunE: signer.SanityCheck, + RunE: func(cmd *cobra.Command, args []string) error { + opts := signer.InputOpts + if opts.Keystore != "" { + ks := keystore.NewKeyStore(opts.Keystore, keystore.StandardScryptN, keystore.StandardScryptP) + accounts := ks.Accounts() + for idx, a := range accounts { + log.Info().Str("account", a.Address.String()).Int("index", idx).Msg("Account") + } + return nil + } + if opts.KMS == "GCP" { + gcpKMS := signer.GCPKMS{} + return gcpKMS.ListKeyRingKeys(cmd.Context()) + } + return fmt.Errorf("unable to list accounts") + }, +} diff --git a/cmd/signer/listCmdUsage.md b/cmd/signer/list/usage.md similarity index 100% rename from cmd/signer/listCmdUsage.md rename to cmd/signer/list/usage.md diff --git a/cmd/signer/sign/sign.go b/cmd/signer/sign/sign.go new file mode 100644 index 00000000..90d027e4 --- /dev/null +++ b/cmd/signer/sign/sign.go @@ -0,0 +1,88 @@ +package sign + +import ( + _ "embed" + "fmt" + "os" + + "github.com/0xPolygon/polygon-cli/gethkeystore" + "github.com/0xPolygon/polygon-cli/signer" + accounts2 "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/crypto" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +//go:embed usage.md +var usage string + +var SignCmd = &cobra.Command{ + Use: "sign", + Short: "Sign tx data.", + Long: usage, + Args: cobra.NoArgs, + PreRunE: signer.SanityCheck, + RunE: func(cmd *cobra.Command, args []string) error { + opts := signer.InputOpts + if opts.Keystore == "" && opts.PrivateKey == "" && opts.KMS == "" { + return fmt.Errorf("no valid keystore was specified") + } + + if opts.Keystore != "" { + ks := keystore.NewKeyStore(opts.Keystore, keystore.StandardScryptN, keystore.StandardScryptP) + accounts := ks.Accounts() + var accountToUnlock *accounts2.Account + for _, a := range accounts { + if a.Address.String() == opts.KeyID { + accountToUnlock = &a + break + } + } + if accountToUnlock == nil { + accountStrings := "" + for _, a := range accounts { + accountStrings += a.Address.String() + " " + } + return fmt.Errorf("account with address %s not found in list [%s]", opts.KeyID, accountStrings) + } + password, err := signer.GetKeystorePassword() + if err != nil { + return err + } + + err = ks.Unlock(*accountToUnlock, password) + if err != nil { + return err + } + + log.Info().Str("path", accountToUnlock.URL.Path).Msg("Unlocked account") + encryptedKey, err := os.ReadFile(accountToUnlock.URL.Path) + if err != nil { + return err + } + privKey, err := gethkeystore.DecryptKeystoreFile(encryptedKey, password) + if err != nil { + return err + } + return signer.Sign(privKey) + } + + if opts.PrivateKey != "" { + pk, err := crypto.HexToECDSA(opts.PrivateKey) + if err != nil { + return err + } + return signer.Sign(pk) + } + if opts.KMS == "GCP" { + tx, err := signer.GetTxDataToSign() + if err != nil { + return err + } + gcpKMS := signer.GCPKMS{} + return gcpKMS.Sign(cmd.Context(), tx) + } + return fmt.Errorf("not implemented") + }, +} diff --git a/cmd/signer/signCmdUsage.md b/cmd/signer/sign/usage.md similarity index 100% rename from cmd/signer/signCmdUsage.md rename to cmd/signer/sign/usage.md diff --git a/cmd/signer/signer.go b/cmd/signer/signer.go index 4cf07b5d..a55e16f0 100644 --- a/cmd/signer/signer.go +++ b/cmd/signer/signer.go @@ -1,86 +1,26 @@ package signer import ( - "bytes" - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/x509" - "crypto/x509/pkix" _ "embed" - "encoding/asn1" - "encoding/hex" - "encoding/json" - "encoding/pem" - "fmt" - "hash/crc32" - "math/big" - "os" - "strings" - "time" - kms "cloud.google.com/go/kms/apiv1" - "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/0xPolygon/polygon-cli/cmd/signer/create" + importcmd "github.com/0xPolygon/polygon-cli/cmd/signer/import" + "github.com/0xPolygon/polygon-cli/cmd/signer/list" + "github.com/0xPolygon/polygon-cli/cmd/signer/sign" "github.com/0xPolygon/polygon-cli/flag" - "github.com/0xPolygon/polygon-cli/gethkeystore" - accounts2 "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/google/tink/go/kwp/subtle" - "github.com/manifoldco/promptui" - "github.com/rs/zerolog/log" + "github.com/0xPolygon/polygon-cli/signer" "github.com/spf13/cobra" - "google.golang.org/api/iterator" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/wrapperspb" ) -// signerOpts are the input arguments for these commands -type signerOpts struct { - keystore string - privateKey string - kms string - keyID string - unsafePassword string - dataFile string - signerType string - chainID uint64 - gcpProjectID string - gcpRegion string - gcpKeyRingID string - gcpImportJob string - gcpKeyVersion int -} - -var inputSignerOpts = signerOpts{} - //go:embed usage.md var signerUsage string -//go:embed signCmdUsage.md -var signCmdUsage string - -//go:embed createCmdUsage.md -var createCmdUsage string - -//go:embed listCmdUsage.md -var listCmdUsage string - -//go:embed importCmdUsage.md -var importCmdUsage string - var SignerCmd = &cobra.Command{ Use: "signer", Short: "Utilities for security signing transactions.", Long: signerUsage, PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { - inputSignerOpts.privateKey, err = flag.GetPrivateKey(cmd) + signer.InputOpts.PrivateKey, err = flag.GetPrivateKey(cmd) if err != nil { return err } @@ -89,779 +29,27 @@ var SignerCmd = &cobra.Command{ Args: cobra.NoArgs, } -var SignCmd = &cobra.Command{ - Use: "sign", - Short: "Sign tx data.", - Long: signCmdUsage, - Args: cobra.NoArgs, - PreRunE: sanityCheck, - RunE: func(cmd *cobra.Command, args []string) error { - if inputSignerOpts.keystore == "" && inputSignerOpts.privateKey == "" && inputSignerOpts.kms == "" { - return fmt.Errorf("no valid keystore was specified") - } - - if inputSignerOpts.keystore != "" { - ks := keystore.NewKeyStore(inputSignerOpts.keystore, keystore.StandardScryptN, keystore.StandardScryptP) - accounts := ks.Accounts() - var accountToUnlock *accounts2.Account - for _, a := range accounts { - if a.Address.String() == inputSignerOpts.keyID { - accountToUnlock = &a - break - } - } - if accountToUnlock == nil { - accountStrings := "" - for _, a := range accounts { - accountStrings += a.Address.String() + " " - } - return fmt.Errorf("account with address %s not found in list [%s]", inputSignerOpts.keyID, accountStrings) - } - password, err := getKeystorePassword() - if err != nil { - return err - } - - err = ks.Unlock(*accountToUnlock, password) - if err != nil { - return err - } - // chainID := new(big.Int).SetUint64(inputSignerOpts.chainID) - - // ks.SignTx(*accountToUnlock, &tx, chainID) - log.Info().Str("path", accountToUnlock.URL.Path).Msg("Unlocked account") - encryptedKey, err := os.ReadFile(accountToUnlock.URL.Path) - if err != nil { - return err - } - privKey, err := gethkeystore.DecryptKeystoreFile(encryptedKey, password) - if err != nil { - return err - } - return sign(privKey) - } - - if inputSignerOpts.privateKey != "" { - pk, err := crypto.HexToECDSA(inputSignerOpts.privateKey) - if err != nil { - return err - } - return sign(pk) - } - if inputSignerOpts.kms == "GCP" { - tx, err := getTxDataToSign() - if err != nil { - return err - } - gcpKMS := GCPKMS{} - return gcpKMS.Sign(cmd.Context(), tx) - } - return fmt.Errorf("not implemented") - }, -} - -var CreateCmd = &cobra.Command{ - Use: "create", - Short: "Create a new key.", - Long: createCmdUsage, - Args: cobra.NoArgs, - PreRunE: sanityCheck, - RunE: func(cmd *cobra.Command, args []string) error { - if inputSignerOpts.keystore == "" && inputSignerOpts.kms == "" { - log.Info().Msg("Generating new private hex key and writing to stdout") - pk, err := crypto.GenerateKey() - if err != nil { - return err - } - k := hex.EncodeToString(crypto.FromECDSA(pk)) - fmt.Println(k) - return nil - } - if inputSignerOpts.keystore != "" { - ks := keystore.NewKeyStore(inputSignerOpts.keystore, keystore.StandardScryptN, keystore.StandardScryptP) - pk, err := crypto.GenerateKey() - if err != nil { - return err - } - password, err := getKeystorePassword() - if err != nil { - return err - } - acc, err := ks.ImportECDSA(pk, password) - if err != nil { - return err - } - log.Info().Str("address", acc.Address.String()).Msg("imported new account") - return nil - } - if inputSignerOpts.kms == "GCP" { - gcpKMS := GCPKMS{} - err := gcpKMS.CreateKeyRing(cmd.Context()) - if err != nil { - return err - } - err = gcpKMS.CreateKey(cmd.Context()) - if err != nil { - return err - } - } - return nil - }, -} - -var ListCmd = &cobra.Command{ - Use: "list", - Short: "List the keys in the keyring / keystore.", - Long: listCmdUsage, - Args: cobra.NoArgs, - PreRunE: sanityCheck, - RunE: func(cmd *cobra.Command, args []string) error { - if inputSignerOpts.keystore != "" { - ks := keystore.NewKeyStore(inputSignerOpts.keystore, keystore.StandardScryptN, keystore.StandardScryptP) - accounts := ks.Accounts() - for idx, a := range accounts { - log.Info().Str("account", a.Address.String()).Int("index", idx).Msg("Account") - } - return nil - } - if inputSignerOpts.kms == "GCP" { - gcpKMS := GCPKMS{} - return gcpKMS.ListKeyRingKeys(cmd.Context()) - } - return fmt.Errorf("unable to list accounts") - }, -} - -var ImportCmd = &cobra.Command{ - Use: "import", - Short: "Import a private key into the keyring / keystore.", - Long: importCmdUsage, - Args: cobra.NoArgs, - PreRunE: func(cmd *cobra.Command, args []string) error { - if err := sanityCheck(cmd, args); err != nil { - return err - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - if inputSignerOpts.keystore != "" { - ks := keystore.NewKeyStore(inputSignerOpts.keystore, keystore.StandardScryptN, keystore.StandardScryptP) - pk, err := crypto.HexToECDSA(inputSignerOpts.privateKey) - if err != nil { - return err - } - pass, err := getKeystorePassword() - if err != nil { - return err - } - _, err = ks.ImportECDSA(pk, pass) - return err - } - if inputSignerOpts.kms == "GCP" { - gcpKMS := GCPKMS{} - if err := gcpKMS.CreateImportJob(cmd.Context()); err != nil { - return err - } - return gcpKMS.ImportKey(cmd.Context()) - } - return fmt.Errorf("unable to import key") - }, -} - -func getTxDataToSign() (*ethtypes.Transaction, error) { - if inputSignerOpts.dataFile == "" { - return nil, fmt.Errorf("datafile not specified") - } - dataToSign, err := os.ReadFile(inputSignerOpts.dataFile) - if err != nil { - return nil, err - } - - // TODO at some point we should support signing other data types besides transactions - var txArgs apitypes.SendTxArgs - if err = json.Unmarshal(dataToSign, &txArgs); err != nil { - return nil, err - } - var tx *ethtypes.Transaction - tx, err = txArgs.ToTransaction() - if err != nil { - log.Error().Err(err).Str("txArgs", txArgs.String()).Msg("unable to convert the arguments to a transaction") - return nil, err - } - return tx, nil - -} -func sign(pk *ecdsa.PrivateKey) error { - tx, err := getTxDataToSign() - if err != nil { - return err - } - signer, err := getSigner() - if err != nil { - return err - } - signedTx, err := ethtypes.SignTx(tx, signer, pk) - if err != nil { - return err - } - return outputSignedTx(signedTx) -} - -func outputSignedTx(signedTx *ethtypes.Transaction) error { - rawTx, err := signedTx.MarshalBinary() - if err != nil { - return err - } - rawHexString := hex.EncodeToString(rawTx) - out := make(map[string]any, 0) - out["signedTx"] = signedTx - out["rawSignedTx"] = rawHexString - outJSON, err := json.Marshal(out) - if err != nil { - return err - } - fmt.Println(string(outJSON)) - return nil -} - -type GCPKMS struct{} - -func (g *GCPKMS) ListKeyRingKeys(ctx context.Context) error { - // This snippet has been automatically generated and should be regarded as a code template only. - // It will require modifications to work: - // - It may require correct/in-range values for request initialization. - // - It may require specifying regional endpoints when creating the service client as shown in: - // https://pkg.go.dev/cloud.google.com/go#hdr-Client_Options - c, err := kms.NewKeyManagementClient(ctx) - if err != nil { - return err - } - defer c.Close() - parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID) - - req := &kmspb.ListCryptoKeysRequest{ - Parent: parent, - } - it := c.ListCryptoKeys(ctx, req) - for { - resp, err := it.Next() - if err == iterator.Done { - break - } - if err != nil { - return err - } - - pubKey, err := getPublicKeyByName(ctx, c, fmt.Sprintf("%s/cryptoKeyVersions/%d", resp.Name, inputSignerOpts.gcpKeyVersion)) - if err != nil { - log.Error().Err(err).Str("name", resp.Name).Msg("key not found") - continue - } - ethAddress := gcpPubKeyToEthAddress(pubKey) - - log.Info().Str("CryptoKeyBackend", resp.CryptoKeyBackend). - Str("DestroyScheduledDuration", resp.DestroyScheduledDuration.String()). - Str("CreateTime", resp.CreateTime.String()). - Str("Purpose", resp.Purpose.String()). - Str("ProtectionLevel", resp.VersionTemplate.ProtectionLevel.String()). - Str("Algorithm", resp.VersionTemplate.Algorithm.String()). - Str("ETHAddress", ethAddress.String()). - Str("Name", resp.Name).Msg("got key") - - } - return nil -} -func (g *GCPKMS) CreateKeyRing(ctx context.Context) error { - parent := fmt.Sprintf("projects/%s/locations/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion) - id := inputSignerOpts.gcpKeyRingID - log.Info().Str("parent", parent).Str("id", id).Msg("Creating keyring") - // Create the client. - client, err := kms.NewKeyManagementClient(ctx) - if err != nil { - return fmt.Errorf("failed to create kms client: %w", err) - } - defer client.Close() - - result, err := client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{Name: fmt.Sprintf("%s/keyRings/%s", parent, id)}) - if err != nil { - nf := strings.Contains(err.Error(), "not found") - if !nf { - return err - } - } - if err == nil { - log.Info().Str("name", result.Name).Msg("key ring already exists") - return nil - } - log.Info().Str("id", id).Msg("key ring not found - creating") - - // Build the request. - req := &kmspb.CreateKeyRingRequest{ - Parent: parent, - KeyRingId: id, - } - - // Call the API. - result, err = client.CreateKeyRing(ctx, req) - if err != nil { - return fmt.Errorf("failed to create key ring: %w", err) - } - log.Info().Str("name", result.Name).Msg("Created key ring") - return nil -} - -func (g *GCPKMS) CreateKey(ctx context.Context) error { - parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID) - id := inputSignerOpts.keyID - - client, err := kms.NewKeyManagementClient(ctx) - if err != nil { - return fmt.Errorf("failed to create kms client: %w", err) - } - defer client.Close() - - // Build the request. - req := &kmspb.CreateCryptoKeyRequest{ - Parent: parent, - CryptoKeyId: id, - CryptoKey: &kmspb.CryptoKey{ - Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, - VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ - Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256, - ProtectionLevel: kmspb.ProtectionLevel_HSM, - }, - - // Optional: customize how long key versions should be kept before destroying. - DestroyScheduledDuration: durationpb.New(24 * time.Hour), - }, - } - - // Call the API. - result, err := client.CreateCryptoKey(ctx, req) - if err != nil { - if strings.Contains(err.Error(), "already exists") { - log.Info().Str("parent", parent).Str("id", id).Msg("key already exists") - return nil - } - return fmt.Errorf("failed to create key: %w", err) - } - log.Info().Str("name", result.Name).Msg("created key") - return nil - -} - -func (g *GCPKMS) CreateImportJob(ctx context.Context) error { - // parent := "projects/PROJECT_ID/locations/global/keyRings/my-key-ring" - // id := "my-import-job" - parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID) - id := inputSignerOpts.gcpImportJob - - // Create the client. - client, err := kms.NewKeyManagementClient(ctx) - if err != nil { - return fmt.Errorf("failed to create kms client: %w", err) - } - defer client.Close() - - // Build the request. - req := &kmspb.CreateImportJobRequest{ - Parent: parent, - ImportJobId: id, - ImportJob: &kmspb.ImportJob{ - // See allowed values and their descriptions at - // https://cloud.google.com/kms/docs/algorithms#protection_levels - ProtectionLevel: kmspb.ProtectionLevel_HSM, - // See allowed values and their descriptions at - // https://cloud.google.com/kms/docs/key-wrapping#import_methods - ImportMethod: kmspb.ImportJob_RSA_OAEP_3072_SHA1_AES_256, - }, - } - - // Call the API. - result, err := client.CreateImportJob(ctx, req) - if err != nil { - if strings.Contains(err.Error(), "already exists") { - log.Info().Str("name", parent).Msg("import job already exists") - return nil - } - return fmt.Errorf("failed to create import job: %w", err) - } - log.Info().Str("name", result.Name).Msg("created import job") - - return nil -} - -func (g *GCPKMS) ImportKey(ctx context.Context) error { - name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID, inputSignerOpts.keyID) - importJob := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/importJobs/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID, inputSignerOpts.gcpImportJob) - client, err := kms.NewKeyManagementClient(ctx) - if err != nil { - return fmt.Errorf("failed to create kms client: %w", err) - } - defer client.Close() - - wrappedKey, err := wrapKeyForGCPKMS(ctx, client) - if err != nil { - return err - } - req := &kmspb.ImportCryptoKeyVersionRequest{ - Parent: name, - Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256, - WrappedKey: wrappedKey, - ImportJob: importJob, - } - - result, err := client.ImportCryptoKeyVersion(ctx, req) - if err != nil { - if strings.Contains(err.Error(), "already exists") { - log.Info().Str("name", name).Msg("key already exists") - return nil - } - return fmt.Errorf("failed to import key: %w", err) - } - log.Info().Str("name", result.Name).Msg("imported key") - return nil - -} - -func wrapKeyForGCPKMS(ctx context.Context, client *kms.KeyManagementClient) ([]byte, error) { - // Generate a ECDSA keypair, and format the private key as PKCS #8 DER. - key, err := crypto.HexToECDSA(inputSignerOpts.privateKey) - if err != nil { - return nil, err - } - // These are a lot of hacks because the default x509 library doesn't seem to support the secp256k1 curve - // START HACKS - // keyBytes, err := x509.MarshalPKCS8PrivateKey(key) - // if err != nil { - // return nil, fmt.Errorf("failed to format private key: %w", err) - // } - - // https://docs.rs/k256/latest/src/k256/lib.rs.html#116 - oidNamedCurveP256K1 := asn1.ObjectIdentifier{1, 3, 132, 0, 10} - oidBytes, err := asn1.Marshal(oidNamedCurveP256K1) - if err != nil { - return nil, fmt.Errorf("x509: failed to marshal curve OID: %w", err) - } - var privKey pkcs8 - oidPublicKeyECDSA := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} - privKey.Algo = pkix.AlgorithmIdentifier{ - Algorithm: oidPublicKeyECDSA, - Parameters: asn1.RawValue{ - FullBytes: oidBytes, - }, - } - privateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8) - privKey.PrivateKey, err = asn1.Marshal(ecPrivateKey{ - Version: 1, // This is not the GCP Cryptokey version! - PrivateKey: key.D.FillBytes(privateKey), - NamedCurveOID: nil, - // It looks like elliptic.Marshal is deprecated, but it's still being used in the core library as of go 1.21.5, so I don't want to switch to ecdh especially since it's not obvious how to do so - // https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/x509/x509.go;l=106 - PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)}, //nolint:staticcheck - }) - if err != nil { - return nil, fmt.Errorf("unable to marshal private key: %w", err) - } - keyBytes, err := asn1.Marshal(privKey) - if err != nil { - return nil, fmt.Errorf("unable to marshal full private key") - } - // END HACKS - - // Generate a temporary 32-byte key for AES-KWP and wrap the key material. - kwpKey := make([]byte, 32) - if _, err = rand.Read(kwpKey); err != nil { - return nil, fmt.Errorf("failed to generate AES-KWP key: %w", err) - } - kwp, err := subtle.NewKWP(kwpKey) - if err != nil { - return nil, fmt.Errorf("failed to create KWP cipher: %w", err) - } - wrappedTarget, err := kwp.Wrap(keyBytes) - if err != nil { - return nil, fmt.Errorf("failed to wrap target key with KWP: %w", err) - } - - importJobName := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/importJobs/%s", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID, inputSignerOpts.gcpImportJob) - - // Retrieve the public key from the import job. - importJob, err := client.GetImportJob(ctx, &kmspb.GetImportJobRequest{ - Name: importJobName, - }) - if err != nil { - return nil, fmt.Errorf("failed to retrieve import job: %w", err) - } - pubBlock, _ := pem.Decode([]byte(importJob.PublicKey.Pem)) - pubAny, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse import job public key: %w", err) - } - pub, ok := pubAny.(*rsa.PublicKey) - if !ok { - return nil, fmt.Errorf("unexpected public key type %T, want *rsa.PublicKey", pubAny) - } - - // Wrap the KWP key using the import job key. - wrappedWrappingKey, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, kwpKey, nil) - if err != nil { - return nil, fmt.Errorf("failed to wrap KWP key: %w", err) - } - - // Concatenate the wrapped KWP key and the wrapped target key. - combined := append(wrappedWrappingKey, wrappedTarget...) - return combined, nil - -} - -type ecPrivateKey struct { - Version int - PrivateKey []byte - NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` - PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` -} - -type publicKeyInfo struct { - Raw asn1.RawContent - Algorithm pkix.AlgorithmIdentifier - PublicKey asn1.BitString -} -type pkcs8 struct { - Version int - Algo pkix.AlgorithmIdentifier - PrivateKey []byte - // optional attributes omitted. -} - -func (g *GCPKMS) Sign(ctx context.Context, tx *ethtypes.Transaction) error { - name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%d", inputSignerOpts.gcpProjectID, inputSignerOpts.gcpRegion, inputSignerOpts.gcpKeyRingID, inputSignerOpts.keyID, inputSignerOpts.gcpKeyVersion) - - client, err := kms.NewKeyManagementClient(ctx) - if err != nil { - return fmt.Errorf("failed to create kms client: %w", err) - } - defer client.Close() - - signer, err := getSigner() - if err != nil { - return err - } - digest := signer.Hash(tx) - - // Optional but recommended: Compute digest's CRC32C. - crc32c := func(data []byte) uint32 { - t := crc32.MakeTable(crc32.Castagnoli) - return crc32.Checksum(data, t) - - } - digestCRC32C := crc32c(digest.Bytes()) - - req := &kmspb.AsymmetricSignRequest{ - Name: name, - Digest: &kmspb.Digest{ - Digest: &kmspb.Digest_Sha256{ - Sha256: digest.Bytes(), - }, - }, - DigestCrc32C: wrapperspb.Int64(int64(digestCRC32C)), - } - - // Call the API. - result, err := client.AsymmetricSign(ctx, req) - if err != nil { - return fmt.Errorf("failed to sign digest: %w", err) - } - - // Optional, but recommended: perform integrity verification on result. - // For more details on ensuring E2E in-transit integrity to and from Cloud KMS visit: - // https://cloud.google.com/kms/docs/data-integrity-guidelines - if !result.VerifiedDigestCrc32C { - return fmt.Errorf("asymmetric sign: request corrupted in-transit") - } - if result.Name != req.Name { - return fmt.Errorf("asymmetric sign: request corrupted in-transit") - } - if int64(crc32c(result.Signature)) != result.SignatureCrc32C.Value { - return fmt.Errorf("asymmetric sign: response corrupted in-transit") - } - - gcpPubKey, err := getPublicKeyByName(ctx, client, name) - if err != nil { - return err - } - - // Verify Elliptic Curve signature. - var parsedSig struct{ R, S *big.Int } - if _, err = asn1.Unmarshal(result.Signature, &parsedSig); err != nil { - return fmt.Errorf("asn1.Unmarshal: %w", err) - } - ethSig := make([]byte, 0) - - ethSig = append(ethSig, bigIntTo32Bytes(parsedSig.R)...) - ethSig = append(ethSig, bigIntTo32Bytes(parsedSig.S)...) - ethSig = append(ethSig, 0) - - // Feels like a hack, but I can't figure out a better way to determine the recovery ID than this since google isn't returning it. More research is required - pubKey, err := crypto.Ecrecover(digest.Bytes(), ethSig) - if err != nil || !bytes.Equal(pubKey, gcpPubKey.PublicKey.Bytes) { - ethSig[64] = 1 - } - pubKey, err = crypto.Ecrecover(digest.Bytes(), ethSig) - if err != nil || !bytes.Equal(pubKey, gcpPubKey.PublicKey.Bytes) { - return fmt.Errorf("unable to determine recovery identifier value: %w", err) - } - // pubKeyAddr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) - pubKeyAddr := gcpPubKeyToEthAddress(gcpPubKey) - log.Info(). - Str("hexSignature", hex.EncodeToString(result.Signature)). - Str("ethSignature", hex.EncodeToString(ethSig)). - Msg("Got signature") - - log.Info(). - Str("recoveredPub", hex.EncodeToString(pubKey)). - Str("gcpPub", hex.EncodeToString(gcpPubKey.PublicKey.Bytes)). - Stringer("ethAddress", pubKeyAddr). - Msg("Recovered pub key") - - signedTx, err := tx.WithSignature(signer, ethSig) - if err != nil { - return err - } - - return outputSignedTx(signedTx) -} - -func gcpPubKeyToEthAddress(gcpPubKey *publicKeyInfo) common.Address { - pubKeyAddr := common.BytesToAddress(crypto.Keccak256(gcpPubKey.PublicKey.Bytes[1:])[12:]) - return pubKeyAddr - -} -func getPublicKeyByName(ctx context.Context, client *kms.KeyManagementClient, name string) (*publicKeyInfo, error) { - pubKeyResponse, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: name}) - if err != nil { - return nil, err - } - block, _ := pem.Decode([]byte(pubKeyResponse.Pem)) - var gcpPubKey publicKeyInfo - if _, err = asn1.Unmarshal(block.Bytes, &gcpPubKey); err != nil { - return nil, err - } - return &gcpPubKey, nil -} -func bigIntTo32Bytes(num *big.Int) []byte { - // Convert big.Int to a 32-byte array - b := num.Bytes() - if len(b) < 32 { - // Left-pad with zeros if needed - b = append(make([]byte, 32-len(b)), b...) - } - return b -} -func getKeystorePassword() (string, error) { - if inputSignerOpts.unsafePassword != "" { - return inputSignerOpts.unsafePassword, nil - } - return passwordPrompt.Run() -} - -func sanityCheck(cmd *cobra.Command, args []string) error { - // Strip off the 0x if it's included in the private key hex - inputSignerOpts.privateKey = strings.TrimPrefix(inputSignerOpts.privateKey, "0x") - - // normalize the format of the kms argument - inputSignerOpts.kms = strings.ToUpper(inputSignerOpts.kms) - - keyStoreMethods := 0 - if inputSignerOpts.kms != "" { - keyStoreMethods += 1 - } - if inputSignerOpts.privateKey != "" && cmd.Name() != "import" { - keyStoreMethods += 1 - } - if inputSignerOpts.keystore != "" { - keyStoreMethods += 1 - } - if keyStoreMethods > 1 { - return fmt.Errorf("multiple conflicting keystore sources were specified") - } - pwErr := passwordValidation(inputSignerOpts.unsafePassword) - if inputSignerOpts.unsafePassword != "" && pwErr != nil { - return pwErr - } - - if inputSignerOpts.kms == "GCP" { - if inputSignerOpts.gcpProjectID == "" { - return fmt.Errorf("GCP project id must be specified") - } - - if inputSignerOpts.gcpRegion == "" { - return fmt.Errorf("location is required") - } - - if inputSignerOpts.gcpKeyRingID == "" { - return fmt.Errorf("GCP keyring ID is required") - } - if inputSignerOpts.keyID == "" && cmd.Name() != "list" { - return fmt.Errorf("key id is required") - } - } - - return nil -} - -func passwordValidation(inputPw string) error { - if len(inputPw) < 6 { - return fmt.Errorf("password only had %d characters, 8 or more required", len(inputPw)) - } - return nil -} - -var passwordPrompt = promptui.Prompt{ - Label: "Password", - Validate: passwordValidation, - Mask: '*', -} - -func getSigner() (ethtypes.Signer, error) { - chainID := new(big.Int).SetUint64(inputSignerOpts.chainID) - switch inputSignerOpts.signerType { - case "latest": - return ethtypes.LatestSignerForChainID(chainID), nil - case "cancun": - return ethtypes.NewCancunSigner(chainID), nil - case "london": - return ethtypes.NewLondonSigner(chainID), nil - case "eip2930": - return ethtypes.NewEIP2930Signer(chainID), nil - case "eip155": - return ethtypes.NewEIP155Signer(chainID), nil - } - return nil, fmt.Errorf("signer %s is not recognized", inputSignerOpts.signerType) -} - func init() { - SignerCmd.AddCommand(SignCmd) - SignerCmd.AddCommand(CreateCmd) - SignerCmd.AddCommand(ListCmd) - SignerCmd.AddCommand(ImportCmd) + SignerCmd.AddCommand(sign.SignCmd) + SignerCmd.AddCommand(create.CreateCmd) + SignerCmd.AddCommand(list.ListCmd) + SignerCmd.AddCommand(importcmd.ImportCmd) f := SignerCmd.PersistentFlags() - f.StringVar(&inputSignerOpts.keystore, "keystore", "", "use keystore in given folder or file") - f.StringVar(&inputSignerOpts.privateKey, flag.PrivateKey, "", "use provided hex encoded private key") - f.StringVar(&inputSignerOpts.kms, "kms", "", "AWS or GCP if key is stored in cloud") - f.StringVar(&inputSignerOpts.keyID, "key-id", "", "ID of key to be used for signing") - f.StringVar(&inputSignerOpts.unsafePassword, "unsafe-password", "", "non-interactively specified password for unlocking keystore") - - f.StringVar(&inputSignerOpts.signerType, "type", "london", "type of signer to use: latest, cancun, london, eip2930, eip155") - f.StringVar(&inputSignerOpts.dataFile, "data-file", "", "file name holding data to be signed") - - f.Uint64Var(&inputSignerOpts.chainID, "chain-id", 0, "chain ID for transactions") - - // https://github.com/golang/oauth2/issues/241 - f.StringVar(&inputSignerOpts.gcpProjectID, "gcp-project-id", "", "GCP project ID to use") - f.StringVar(&inputSignerOpts.gcpRegion, "gcp-location", "europe-west2", "GCP region to use") - // What is dead may never die https://cloud.google.com/kms/docs/faq#cannot_delete - f.StringVar(&inputSignerOpts.gcpKeyRingID, "gcp-keyring-id", "polycli-keyring", "GCP keyring ID to be used") - f.StringVar(&inputSignerOpts.gcpImportJob, "gcp-import-job-id", "", "GCP import job ID to use when importing key") - f.IntVar(&inputSignerOpts.gcpKeyVersion, "gcp-key-version", 1, "GCP crypto key version to use") + f.StringVar(&signer.InputOpts.Keystore, "keystore", "", "use keystore in given folder or file") + f.StringVar(&signer.InputOpts.PrivateKey, flag.PrivateKey, "", "use provided hex encoded private key") + f.StringVar(&signer.InputOpts.KMS, "kms", "", "AWS or GCP if key is stored in cloud") + f.StringVar(&signer.InputOpts.KeyID, "key-id", "", "ID of key to be used for signing") + f.StringVar(&signer.InputOpts.UnsafePassword, "unsafe-password", "", "non-interactively specified password for unlocking keystore") + + f.StringVar(&signer.InputOpts.SignerType, "type", "london", "type of signer to use: latest, cancun, london, eip2930, eip155") + f.StringVar(&signer.InputOpts.DataFile, "data-file", "", "file name holding data to be signed") + + f.Uint64Var(&signer.InputOpts.ChainID, "chain-id", 0, "chain ID for transactions") + + f.StringVar(&signer.InputOpts.GCPProjectID, "gcp-project-id", "", "GCP project ID to use") + f.StringVar(&signer.InputOpts.GCPRegion, "gcp-location", "europe-west2", "GCP region to use") + f.StringVar(&signer.InputOpts.GCPKeyRingID, "gcp-keyring-id", "polycli-keyring", "GCP keyring ID to be used") + f.StringVar(&signer.InputOpts.GCPImportJob, "gcp-import-job-id", "", "GCP import job ID to use when importing key") + f.IntVar(&signer.InputOpts.GCPKeyVersion, "gcp-key-version", 1, "GCP crypto key version to use") } diff --git a/signer/signer.go b/signer/signer.go new file mode 100644 index 00000000..fc06dba8 --- /dev/null +++ b/signer/signer.go @@ -0,0 +1,576 @@ +package signer + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "hash/crc32" + "math/big" + "os" + "strings" + "time" + + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/google/tink/go/kwp/subtle" + "github.com/manifoldco/promptui" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "google.golang.org/api/iterator" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// Opts are the input arguments for signer commands. +type Opts struct { + Keystore string + PrivateKey string + KMS string + KeyID string + UnsafePassword string + DataFile string + SignerType string + ChainID uint64 + GCPProjectID string + GCPRegion string + GCPKeyRingID string + GCPImportJob string + GCPKeyVersion int +} + +var InputOpts = Opts{} + +func GetTxDataToSign() (*ethtypes.Transaction, error) { + if InputOpts.DataFile == "" { + return nil, fmt.Errorf("datafile not specified") + } + dataToSign, err := os.ReadFile(InputOpts.DataFile) + if err != nil { + return nil, err + } + + // TODO at some point we should support signing other data types besides transactions + var txArgs apitypes.SendTxArgs + if err = json.Unmarshal(dataToSign, &txArgs); err != nil { + return nil, err + } + var tx *ethtypes.Transaction + tx, err = txArgs.ToTransaction() + if err != nil { + log.Error().Err(err).Str("txArgs", txArgs.String()).Msg("unable to convert the arguments to a transaction") + return nil, err + } + return tx, nil +} + +func Sign(pk *ecdsa.PrivateKey) error { + tx, err := GetTxDataToSign() + if err != nil { + return err + } + signer, err := GetSigner() + if err != nil { + return err + } + signedTx, err := ethtypes.SignTx(tx, signer, pk) + if err != nil { + return err + } + return OutputSignedTx(signedTx) +} + +func OutputSignedTx(signedTx *ethtypes.Transaction) error { + rawTx, err := signedTx.MarshalBinary() + if err != nil { + return err + } + rawHexString := hex.EncodeToString(rawTx) + out := make(map[string]any, 0) + out["signedTx"] = signedTx + out["rawSignedTx"] = rawHexString + outJSON, err := json.Marshal(out) + if err != nil { + return err + } + fmt.Println(string(outJSON)) + return nil +} + +type GCPKMS struct{} + +func (g *GCPKMS) ListKeyRingKeys(ctx context.Context) error { + c, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return err + } + defer c.Close() + parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID) + + req := &kmspb.ListCryptoKeysRequest{ + Parent: parent, + } + it := c.ListCryptoKeys(ctx, req) + for { + resp, err := it.Next() + if err == iterator.Done { + break + } + if err != nil { + return err + } + + pubKey, err := getPublicKeyByName(ctx, c, fmt.Sprintf("%s/cryptoKeyVersions/%d", resp.Name, InputOpts.GCPKeyVersion)) + if err != nil { + log.Error().Err(err).Str("name", resp.Name).Msg("key not found") + continue + } + ethAddress := gcpPubKeyToEthAddress(pubKey) + + log.Info().Str("CryptoKeyBackend", resp.CryptoKeyBackend). + Str("DestroyScheduledDuration", resp.DestroyScheduledDuration.String()). + Str("CreateTime", resp.CreateTime.String()). + Str("Purpose", resp.Purpose.String()). + Str("ProtectionLevel", resp.VersionTemplate.ProtectionLevel.String()). + Str("Algorithm", resp.VersionTemplate.Algorithm.String()). + Str("ETHAddress", ethAddress.String()). + Str("Name", resp.Name).Msg("got key") + } + return nil +} + +func (g *GCPKMS) CreateKeyRing(ctx context.Context) error { + parent := fmt.Sprintf("projects/%s/locations/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion) + id := InputOpts.GCPKeyRingID + log.Info().Str("parent", parent).Str("id", id).Msg("Creating keyring") + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return fmt.Errorf("failed to create kms client: %w", err) + } + defer client.Close() + + result, err := client.GetKeyRing(ctx, &kmspb.GetKeyRingRequest{Name: fmt.Sprintf("%s/keyRings/%s", parent, id)}) + if err != nil { + nf := strings.Contains(err.Error(), "not found") + if !nf { + return err + } + } + if err == nil { + log.Info().Str("name", result.Name).Msg("key ring already exists") + return nil + } + log.Info().Str("id", id).Msg("key ring not found - creating") + + req := &kmspb.CreateKeyRingRequest{ + Parent: parent, + KeyRingId: id, + } + + result, err = client.CreateKeyRing(ctx, req) + if err != nil { + return fmt.Errorf("failed to create key ring: %w", err) + } + log.Info().Str("name", result.Name).Msg("Created key ring") + return nil +} + +func (g *GCPKMS) CreateKey(ctx context.Context) error { + parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID) + id := InputOpts.KeyID + + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return fmt.Errorf("failed to create kms client: %w", err) + } + defer client.Close() + + req := &kmspb.CreateCryptoKeyRequest{ + Parent: parent, + CryptoKeyId: id, + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN, + VersionTemplate: &kmspb.CryptoKeyVersionTemplate{ + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256, + ProtectionLevel: kmspb.ProtectionLevel_HSM, + }, + DestroyScheduledDuration: durationpb.New(24 * time.Hour), + }, + } + + result, err := client.CreateCryptoKey(ctx, req) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + log.Info().Str("parent", parent).Str("id", id).Msg("key already exists") + return nil + } + return fmt.Errorf("failed to create key: %w", err) + } + log.Info().Str("name", result.Name).Msg("created key") + return nil +} + +func (g *GCPKMS) CreateImportJob(ctx context.Context) error { + parent := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID) + id := InputOpts.GCPImportJob + + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return fmt.Errorf("failed to create kms client: %w", err) + } + defer client.Close() + + req := &kmspb.CreateImportJobRequest{ + Parent: parent, + ImportJobId: id, + ImportJob: &kmspb.ImportJob{ + ProtectionLevel: kmspb.ProtectionLevel_HSM, + ImportMethod: kmspb.ImportJob_RSA_OAEP_3072_SHA1_AES_256, + }, + } + + result, err := client.CreateImportJob(ctx, req) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + log.Info().Str("name", parent).Msg("import job already exists") + return nil + } + return fmt.Errorf("failed to create import job: %w", err) + } + log.Info().Str("name", result.Name).Msg("created import job") + + return nil +} + +func (g *GCPKMS) ImportKey(ctx context.Context) error { + name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID, InputOpts.KeyID) + importJob := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/importJobs/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID, InputOpts.GCPImportJob) + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return fmt.Errorf("failed to create kms client: %w", err) + } + defer client.Close() + + wrappedKey, err := wrapKeyForGCPKMS(ctx, client) + if err != nil { + return err + } + req := &kmspb.ImportCryptoKeyVersionRequest{ + Parent: name, + Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256, + WrappedKey: wrappedKey, + ImportJob: importJob, + } + + result, err := client.ImportCryptoKeyVersion(ctx, req) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + log.Info().Str("name", name).Msg("key already exists") + return nil + } + return fmt.Errorf("failed to import key: %w", err) + } + log.Info().Str("name", result.Name).Msg("imported key") + return nil +} + +func wrapKeyForGCPKMS(ctx context.Context, client *kms.KeyManagementClient) ([]byte, error) { + key, err := crypto.HexToECDSA(InputOpts.PrivateKey) + if err != nil { + return nil, err + } + + oidNamedCurveP256K1 := asn1.ObjectIdentifier{1, 3, 132, 0, 10} + oidBytes, err := asn1.Marshal(oidNamedCurveP256K1) + if err != nil { + return nil, fmt.Errorf("x509: failed to marshal curve OID: %w", err) + } + var privKey pkcs8 + oidPublicKeyECDSA := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyECDSA, + Parameters: asn1.RawValue{ + FullBytes: oidBytes, + }, + } + privateKey := make([]byte, (key.Curve.Params().N.BitLen()+7)/8) + privKey.PrivateKey, err = asn1.Marshal(ecPrivateKey{ + Version: 1, + PrivateKey: key.D.FillBytes(privateKey), + NamedCurveOID: nil, + PublicKey: asn1.BitString{Bytes: elliptic.Marshal(key.Curve, key.X, key.Y)}, //nolint:staticcheck + }) + if err != nil { + return nil, fmt.Errorf("unable to marshal private key: %w", err) + } + keyBytes, err := asn1.Marshal(privKey) + if err != nil { + return nil, fmt.Errorf("unable to marshal full private key") + } + + kwpKey := make([]byte, 32) + if _, err = rand.Read(kwpKey); err != nil { + return nil, fmt.Errorf("failed to generate AES-KWP key: %w", err) + } + kwp, err := subtle.NewKWP(kwpKey) + if err != nil { + return nil, fmt.Errorf("failed to create KWP cipher: %w", err) + } + wrappedTarget, err := kwp.Wrap(keyBytes) + if err != nil { + return nil, fmt.Errorf("failed to wrap target key with KWP: %w", err) + } + + importJobName := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/importJobs/%s", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID, InputOpts.GCPImportJob) + + importJob, err := client.GetImportJob(ctx, &kmspb.GetImportJobRequest{ + Name: importJobName, + }) + if err != nil { + return nil, fmt.Errorf("failed to retrieve import job: %w", err) + } + pubBlock, _ := pem.Decode([]byte(importJob.PublicKey.Pem)) + pubAny, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse import job public key: %w", err) + } + pub, ok := pubAny.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("unexpected public key type %T, want *rsa.PublicKey", pubAny) + } + + wrappedWrappingKey, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, pub, kwpKey, nil) + if err != nil { + return nil, fmt.Errorf("failed to wrap KWP key: %w", err) + } + + combined := append(wrappedWrappingKey, wrappedTarget...) + return combined, nil +} + +type ecPrivateKey struct { + Version int + PrivateKey []byte + NamedCurveOID asn1.ObjectIdentifier `asn1:"optional,explicit,tag:0"` + PublicKey asn1.BitString `asn1:"optional,explicit,tag:1"` +} + +type publicKeyInfo struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +type pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte +} + +func (g *GCPKMS) Sign(ctx context.Context, tx *ethtypes.Transaction) error { + name := fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/%d", InputOpts.GCPProjectID, InputOpts.GCPRegion, InputOpts.GCPKeyRingID, InputOpts.KeyID, InputOpts.GCPKeyVersion) + + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + return fmt.Errorf("failed to create kms client: %w", err) + } + defer client.Close() + + signer, err := GetSigner() + if err != nil { + return err + } + digest := signer.Hash(tx) + + crc32c := func(data []byte) uint32 { + t := crc32.MakeTable(crc32.Castagnoli) + return crc32.Checksum(data, t) + } + digestCRC32C := crc32c(digest.Bytes()) + + req := &kmspb.AsymmetricSignRequest{ + Name: name, + Digest: &kmspb.Digest{ + Digest: &kmspb.Digest_Sha256{ + Sha256: digest.Bytes(), + }, + }, + DigestCrc32C: wrapperspb.Int64(int64(digestCRC32C)), + } + + result, err := client.AsymmetricSign(ctx, req) + if err != nil { + return fmt.Errorf("failed to sign digest: %w", err) + } + + if !result.VerifiedDigestCrc32C { + return fmt.Errorf("asymmetric sign: request corrupted in-transit") + } + if result.Name != req.Name { + return fmt.Errorf("asymmetric sign: request corrupted in-transit") + } + if int64(crc32c(result.Signature)) != result.SignatureCrc32C.Value { + return fmt.Errorf("asymmetric sign: response corrupted in-transit") + } + + gcpPubKey, err := getPublicKeyByName(ctx, client, name) + if err != nil { + return err + } + + var parsedSig struct{ R, S *big.Int } + if _, err = asn1.Unmarshal(result.Signature, &parsedSig); err != nil { + return fmt.Errorf("asn1.Unmarshal: %w", err) + } + ethSig := make([]byte, 0) + + ethSig = append(ethSig, bigIntTo32Bytes(parsedSig.R)...) + ethSig = append(ethSig, bigIntTo32Bytes(parsedSig.S)...) + ethSig = append(ethSig, 0) + + pubKey, err := crypto.Ecrecover(digest.Bytes(), ethSig) + if err != nil || !bytes.Equal(pubKey, gcpPubKey.PublicKey.Bytes) { + ethSig[64] = 1 + } + pubKey, err = crypto.Ecrecover(digest.Bytes(), ethSig) + if err != nil || !bytes.Equal(pubKey, gcpPubKey.PublicKey.Bytes) { + return fmt.Errorf("unable to determine recovery identifier value: %w", err) + } + pubKeyAddr := gcpPubKeyToEthAddress(gcpPubKey) + log.Info(). + Str("hexSignature", hex.EncodeToString(result.Signature)). + Str("ethSignature", hex.EncodeToString(ethSig)). + Msg("Got signature") + + log.Info(). + Str("recoveredPub", hex.EncodeToString(pubKey)). + Str("gcpPub", hex.EncodeToString(gcpPubKey.PublicKey.Bytes)). + Stringer("ethAddress", pubKeyAddr). + Msg("Recovered pub key") + + signedTx, err := tx.WithSignature(signer, ethSig) + if err != nil { + return err + } + + return OutputSignedTx(signedTx) +} + +func gcpPubKeyToEthAddress(gcpPubKey *publicKeyInfo) common.Address { + pubKeyAddr := common.BytesToAddress(crypto.Keccak256(gcpPubKey.PublicKey.Bytes[1:])[12:]) + return pubKeyAddr +} + +func getPublicKeyByName(ctx context.Context, client *kms.KeyManagementClient, name string) (*publicKeyInfo, error) { + pubKeyResponse, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: name}) + if err != nil { + return nil, err + } + block, _ := pem.Decode([]byte(pubKeyResponse.Pem)) + var gcpPubKey publicKeyInfo + if _, err = asn1.Unmarshal(block.Bytes, &gcpPubKey); err != nil { + return nil, err + } + return &gcpPubKey, nil +} + +func bigIntTo32Bytes(num *big.Int) []byte { + b := num.Bytes() + if len(b) < 32 { + b = append(make([]byte, 32-len(b)), b...) + } + return b +} + +func GetKeystorePassword() (string, error) { + if InputOpts.UnsafePassword != "" { + return InputOpts.UnsafePassword, nil + } + return PasswordPrompt.Run() +} + +func SanityCheck(cmd *cobra.Command, args []string) error { + InputOpts.PrivateKey = strings.TrimPrefix(InputOpts.PrivateKey, "0x") + InputOpts.KMS = strings.ToUpper(InputOpts.KMS) + + keyStoreMethods := 0 + if InputOpts.KMS != "" { + keyStoreMethods += 1 + } + if InputOpts.PrivateKey != "" && cmd.Name() != "import" { + keyStoreMethods += 1 + } + if InputOpts.Keystore != "" { + keyStoreMethods += 1 + } + if keyStoreMethods > 1 { + return fmt.Errorf("multiple conflicting keystore sources were specified") + } + pwErr := PasswordValidation(InputOpts.UnsafePassword) + if InputOpts.UnsafePassword != "" && pwErr != nil { + return pwErr + } + + if InputOpts.KMS == "GCP" { + if InputOpts.GCPProjectID == "" { + return fmt.Errorf("GCP project id must be specified") + } + + if InputOpts.GCPRegion == "" { + return fmt.Errorf("location is required") + } + + if InputOpts.GCPKeyRingID == "" { + return fmt.Errorf("GCP keyring ID is required") + } + if InputOpts.KeyID == "" && cmd.Name() != "list" { + return fmt.Errorf("key id is required") + } + } + + return nil +} + +func PasswordValidation(inputPw string) error { + if len(inputPw) < 6 { + return fmt.Errorf("password only had %d characters, 8 or more required", len(inputPw)) + } + return nil +} + +var PasswordPrompt = promptui.Prompt{ + Label: "Password", + Validate: PasswordValidation, + Mask: '*', +} + +func GetSigner() (ethtypes.Signer, error) { + chainID := new(big.Int).SetUint64(InputOpts.ChainID) + switch InputOpts.SignerType { + case "latest": + return ethtypes.LatestSignerForChainID(chainID), nil + case "cancun": + return ethtypes.NewCancunSigner(chainID), nil + case "london": + return ethtypes.NewLondonSigner(chainID), nil + case "eip2930": + return ethtypes.NewEIP2930Signer(chainID), nil + case "eip155": + return ethtypes.NewEIP155Signer(chainID), nil + } + return nil, fmt.Errorf("signer %s is not recognized", InputOpts.SignerType) +}