package config import ( kms "cloud.google.com/go/kms/apiv1" "context" "errors" "fmt" "github.com/davecgh/go-spew/spew" "github.com/jftuga/ellipsis" "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/ethergo/signer/signer" "github.com/synapsecns/sanguine/ethergo/signer/signer/awssigner" "github.com/synapsecns/sanguine/ethergo/signer/signer/gcpsigner" "github.com/synapsecns/sanguine/ethergo/signer/signer/localsigner" "github.com/synapsecns/sanguine/ethergo/signer/wallet" "google.golang.org/api/option" "gopkg.in/yaml.v2" "os" "path/filepath" "slices" "strings" ) // SignerConfig contains a signer config. Currently this config // only supports local based signers due to a lack of isomorphic types // when we parse yaml. type SignerConfig struct { // Type is the driver used for the signer Type string // File is the file used for the key. File string } // IsValid determines if the config is valid. func (s SignerConfig) IsValid(_ context.Context) (ok bool, err error) { if !slices.ContainsFunc(AllSignerTypes, func(signerType SignerType) bool { return strings.EqualFold(signerType.String(), s.Type) }) { return false, fmt.Errorf("%w: %s. must be one of: %s", ErrUnsupportedSignerType, s.Type, allSignerTypesList()) } if strings.EqualFold(s.Type, FileType.String()) { // TODO: we'll need to switch validity here based on type once we have more then one supported configuration type // alternatively, we could try to use an awsconfig type file, but this makes the virtual box setup more tedious. A third option is a json blob _, err = wallet.FromKeyFile(s.File) if err != nil { return false, fmt.Errorf("file %s invalid: %w", s.File, err) } } return true, nil } // ErrUnsupportedSignerType indicates the signer type being used is unsupported. var ErrUnsupportedSignerType = errors.New("unsupported signer type") // SignerType is the signer type // //go:generate go run golang.org/x/tools/cmd/stringer -type=SignerType -linecomment type SignerType int const ( // FileType is a file-based signer. FileType SignerType = iota + 1 // File // AWSType is an aws kms based signer. AWSType // AWS // GCPType is a gcp cloud based signer. GCPType // GCP ) // lString returns the lowercase string of the signer type. func (s SignerType) lString() string { return strings.ToLower(s.String()) } // AllSignerTypes is a list of all contract types. Since we use stringer and this is a testing library, instead // of manually copying all these out we pull the names out of stringer. In order to make sure stringer is updated, we panic on // any method called where the index is higher than the stringer array length. var AllSignerTypes []SignerType // set all contact types. func init() { for i := 0; i < len(_SignerType_index); i++ { contractType := SignerType(i) AllSignerTypes = append(AllSignerTypes, contractType) } } // allSignerTypesList prints a list of all signer types. This is useful for returning errors. func allSignerTypesList() string { var res []string for _, signerType := range AllSignerTypes { res = append(res, signerType.String()) } return strings.Join(res, ",") } // SignerFromConfig creates a new signer from a signer config. // TODO: this needs to be moved to some kind of common package. // in the old code configs were split into responsible packages. Maybe something like that works here? func SignerFromConfig(ctx context.Context, config SignerConfig) (signer.Signer, error) { switch strings.ToLower(config.Type) { case FileType.lString(): wall, err := wallet.FromKeyFile(core.ExpandOrReturnPath(config.File)) if err != nil { return nil, fmt.Errorf("could not add signer: %w", err) } res := localsigner.NewSigner(wall.PrivateKey()) return res, nil case AWSType.lString(): awsConfig, err := DecodeAWSConfig(config.File) if err != nil { return nil, fmt.Errorf("could not decode aws config: %w", err) } res, err := awssigner.NewKmsSigner(ctx, awsConfig.Region, awsConfig.AccessKey, awsConfig.AccessSecret, awsConfig.KeyID) if err != nil { return nil, fmt.Errorf("could not decode aws config: %w", err) } return res, nil case GCPType.lString(): gcpConfig, err := DecodeGCPConfig(config.File) if err != nil { return nil, fmt.Errorf("could not decode gcp config: %w", err) } return makeGCPSigner(ctx, gcpConfig) default: return nil, fmt.Errorf("could not create signer: %w", ErrUnsupportedSignerType) } } func makeGCPSigner(ctx context.Context, gcpConfig GCPConfig) (signer.Signer, error) { var options []option.ClientOption if gcpConfig.CredentialFile != "" { options = append(options, option.WithCredentialsFile(gcpConfig.CredentialFile)) } if gcpConfig.Endpoint != "" { options = append(options, option.WithEndpoint(gcpConfig.Endpoint)) } keyClient, err := kms.NewKeyManagementClient(ctx, options...) if err != nil { return nil, fmt.Errorf("could not create key client: %w", err) } res, err := gcpsigner.NewManagedKey(ctx, keyClient, gcpConfig.KeyName) if err != nil { return nil, fmt.Errorf("could not create managed key: %w", err) } return res, nil } // GCPConfig is the config for a GCP signer. type GCPConfig struct { // KeyName is the name of the key to use. KeyName string `yaml:"key_name"` // CredentialFile is the path to the credentials file. // note: this is not recommended for production use. // workload identity federation is recommended. CredentialFile string `yaml:"credential_file"` // Endpoint is the endpoint to use. This is useful for testing. Endpoint string `yaml:"endpoint"` } // Encode encodes the config to yaml. func (a GCPConfig) Encode() ([]byte, error) { output, err := yaml.Marshal(&a) if err != nil { return nil, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(spew.Sdump(a), 20), err) } return output, nil } // DecodeGCPConfig decodes the config from a file. func DecodeGCPConfig(filePath string) (cfg GCPConfig, err error) { input, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return GCPConfig{}, fmt.Errorf("failed to read file: %w", err) } err = yaml.Unmarshal(input, &cfg) if err != nil { return GCPConfig{}, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(string(input), 30), err) } return cfg, nil } // AWSConfig is the config for an AWS signer. // this should match the schema of the file passed in. type AWSConfig struct { // Region is the region the signer is in. Region string `yaml:"region"` // AccessKey is the access key for the signer. AccessKey string `yaml:"access_key"` // AccessSecret is the access secret for the signer. AccessSecret string `yaml:"access_secret"` // KeyID is the key id for the signer. KeyID string `yaml:"key_id"` } // Encode encodes the config to yaml. func (a AWSConfig) Encode() ([]byte, error) { output, err := yaml.Marshal(&a) if err != nil { return nil, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(spew.Sdump(a), 20), err) } return output, nil } // DecodeAWSConfig decodes the config from a file. func DecodeAWSConfig(filePath string) (cfg AWSConfig, err error) { input, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return AWSConfig{}, fmt.Errorf("failed to read file: %w", err) } err = yaml.Unmarshal(input, &cfg) if err != nil { return AWSConfig{}, fmt.Errorf("could not unmarshall config %s: %w", ellipsis.Shorten(string(input), 30), err) } return cfg, nil }