diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1b575794ff..afcc42e934 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -176,3 +176,4 @@ Deploy exporter to prod: only: - main + diff --git a/cli/bootnode/boot_node.go b/cli/bootnode/boot_node.go index 21fb483f32..ddf69d71e4 100644 --- a/cli/bootnode/boot_node.go +++ b/cli/bootnode/boot_node.go @@ -31,7 +31,17 @@ var StartBootNodeCmd = &cobra.Command{ log.Fatal(err) } - if err := logging.SetGlobalLogger(cfg.LogLevel, cfg.LogLevelFormat, cfg.LogFormat, cfg.LogFilePath); err != nil { + err := logging.SetGlobalLogger( + cfg.LogLevel, + cfg.LogLevelFormat, + cfg.LogFormat, + &logging.LogFileOptions{ + FileName: cfg.LogFilePath, + MaxSize: cfg.LogFileSize, + MaxBackups: cfg.LogFileBackups, + }, + ) + if err != nil { log.Fatal(err) } diff --git a/cli/config/config.go b/cli/config/config.go index bff083e8a5..0d074c9361 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -17,6 +17,8 @@ type GlobalConfig struct { LogFormat string `yaml:"LogFormat" env:"LOG_FORMAT" env-default:"console" env-description:"Defines logger's encoding, valid values are 'json' (default) and 'console''"` LogLevelFormat string `yaml:"LogLevelFormat" env:"LOG_LEVEL_FORMAT" env-default:"capitalColor" env-description:"Defines logger's level format, valid values are 'capitalColor' (default), 'capital' or 'lowercase''"` LogFilePath string `yaml:"LogFilePath" env:"LOG_FILE_PATH" env-default:"./data/debug.log" env-description:"Defines a file path to write logs into"` + LogFileSize int `yaml:"LogFileSize" env:"LOG_FILE_SIZE" env-default:"500" env-description:"Defines a file size in megabytes to rotate logs"` + LogFileBackups int `yaml:"LogFileBackups" env:"LOG_FILE_BACKUPS" env-default:"3" env-description:"Defines a number of backups to keep when rotating logs"` } // ProcessArgs processes and handles CLI arguments diff --git a/cli/export_keys_from_mnemonic.go b/cli/export_keys_from_mnemonic.go index 385ef3f73a..add9fc4a98 100644 --- a/cli/export_keys_from_mnemonic.go +++ b/cli/export_keys_from_mnemonic.go @@ -19,7 +19,7 @@ var exportKeysCmd = &cobra.Command{ Use: "export-keys", Short: "exports private/public keys based on given mnemonic", Run: func(cmd *cobra.Command, args []string) { - if err := logging.SetGlobalLogger("dpanic", "capital", "console", ""); err != nil { + if err := logging.SetGlobalLogger("dpanic", "capital", "console", nil); err != nil { log.Fatal(err) } diff --git a/cli/generate_operator_keys.go b/cli/generate_operator_keys.go index 7f846b25f9..1a4c85fd16 100644 --- a/cli/generate_operator_keys.go +++ b/cli/generate_operator_keys.go @@ -1,115 +1,61 @@ package cli import ( - "crypto/x509" "encoding/base64" "encoding/json" - "encoding/pem" - "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" - + "log" "os" "path/filepath" "github.com/bloxapp/ssv/logging" "github.com/bloxapp/ssv/utils/rsaencryption" "github.com/spf13/cobra" + "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" "go.uber.org/zap" ) -// generateOperatorKeysCmd is the command to generate operator private/public keys var generateOperatorKeysCmd = &cobra.Command{ Use: "generate-operator-keys", Short: "generates ssv operator keys", Run: func(cmd *cobra.Command, args []string) { - logger := zap.L().Named(RootCmd.Short) + if err := logging.SetGlobalLogger("debug", "capital", "console", nil); err != nil { + log.Fatal(err) + } + logger := zap.L().Named(logging.NameExportKeys) passwordFilePath, _ := cmd.Flags().GetString("password-file") privateKeyFilePath, _ := cmd.Flags().GetString("operator-key-file") - pk, sk, err := rsaencryption.GenerateKeys() - if err != nil && privateKeyFilePath == "" { - logger.Fatal("Failed to create key and operator key wasn't provided", zap.Error(err)) - } - - // Resolve to absolute path - passwordAbsPath, err := filepath.Abs(passwordFilePath) - if err != nil { - logger.Fatal("Failed to read absolute path of password file", zap.Error(err)) - } - // Now read the file - // #nosec G304 - passwordBytes, err := os.ReadFile(passwordAbsPath) + pk, sk, err := rsaencryption.GenerateKeys() if err != nil { - logger.Fatal("Failed to read password file", zap.Error(err)) + logger.Fatal("Failed to generate keys", zap.Error(err)) } - encryptionPassword := string(passwordBytes) - if privateKeyFilePath != "" { - // Resolve to absolute path - privateKeyAbsPath, err := filepath.Abs(privateKeyFilePath) + keyBytes, err := readFile(privateKeyFilePath) if err != nil { - logger.Fatal("Failed to read absolute path of private key file", zap.Error(err)) + logger.Fatal("Failed to read private key from file", zap.Error(err)) } - - // Now read the file - // #nosec G304 - privateKeyBytes, _ := os.ReadFile(privateKeyAbsPath) - if privateKeyBytes != nil { - keyBytes, err := base64.StdEncoding.DecodeString(string(privateKeyBytes)) - if err != nil { - logger.Fatal("base64 decoding failed", zap.Error(err)) - } - - keyPem, _ := pem.Decode(keyBytes) - if keyPem == nil { - logger.Fatal("failed to decode PEM", zap.Error(err)) - } - - rsaKey, err := x509.ParsePKCS1PrivateKey(keyPem.Bytes) - if err != nil { - logger.Fatal("failed to parse RSA private key", zap.Error(err)) - } - - skPem := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(rsaKey), - }, - ) - - operatorPublicKey, _ := rsaencryption.ExtractPublicKey(rsaKey) - publicKey, _ := base64.StdEncoding.DecodeString(operatorPublicKey) - sk = skPem - pk = publicKey + sk, pk, err = parsePrivateKey(keyBytes) + if err != nil { + logger.Fatal("Failed to read private key from file", zap.Error(err)) } } - if err := logging.SetGlobalLogger("debug", "capital", "console", ""); err != nil { - logger.Fatal("", zap.Error(err)) - } - - if err != nil { - logger.Fatal("Failed to generate operator keys", zap.Error(err)) - } - logger.Info("generated public key (base64)", zap.String("pk", base64.StdEncoding.EncodeToString(pk))) - - if encryptionPassword != "" { - encryptedData, err := keystorev4.New().Encrypt(sk, encryptionPassword) + if passwordFilePath != "" { + passwordBytes, err := readFile(passwordFilePath) if err != nil { - logger.Fatal("Failed to encrypt private key", zap.Error(err)) + logger.Fatal("Failed to read password file", zap.Error(err)) } - - encryptedJSON, err := json.Marshal(encryptedData) - if err != nil { - logger.Fatal("Failed to marshal encrypted data to JSON", zap.Error(err)) + encryptedJSON, encryptedJSONErr := encryptPrivateKey(sk, pk, passwordBytes) + if encryptedJSONErr != nil { + logger.Fatal("Failed to encrypt private key", zap.Error(err)) } - - err = os.WriteFile("encrypted_private_key.json", encryptedJSON, 0600) + err = writeFile("encrypted_private_key.json", encryptedJSON) if err != nil { - logger.Fatal("Failed to write encrypted private key to file", zap.Error(err)) + logger.Fatal("Failed to save private key", zap.Error(err)) + } else { + logger.Info("private key encrypted and stored in encrypted_private_key.json") } - - logger.Info("private key encrypted and stored in encrypted_private_key.json") } else { logger.Info("generated public key (base64)", zap.String("pk", base64.StdEncoding.EncodeToString(pk))) logger.Info("generated private key (base64)", zap.String("sk", base64.StdEncoding.EncodeToString(sk))) @@ -117,6 +63,57 @@ var generateOperatorKeysCmd = &cobra.Command{ }, } +func parsePrivateKey(keyBytes []byte) ([]byte, []byte, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(string(keyBytes)) + if err != nil { + return nil, nil, err + } + rsaKey, err := rsaencryption.ConvertPemToPrivateKey(string(decodedBytes)) + if err != nil { + return nil, nil, err + } + + skPem := rsaencryption.PrivateKeyToByte(rsaKey) + + operatorPublicKey, err := rsaencryption.ExtractPublicKey(rsaKey) + if err != nil { + return nil, nil, err + } + pk, err := base64.StdEncoding.DecodeString(operatorPublicKey) + if err != nil { + return nil, nil, err + } + return skPem, pk, nil +} + +func encryptPrivateKey(sk []byte, pk []byte, passwordBytes []byte) ([]byte, error) { + encryptionPassword := string(passwordBytes) + encryptedData, err := keystorev4.New().Encrypt(sk, encryptionPassword) + if err != nil { + return nil, err + } + encryptedData["publicKey"] = base64.StdEncoding.EncodeToString(pk) + encryptedJSON, err := json.Marshal(encryptedData) + if err != nil { + return nil, err + } + return encryptedJSON, nil +} + +func writeFile(fileName string, data []byte) error { + return os.WriteFile(fileName, data, 0600) +} + +func readFile(filePath string) ([]byte, error) { + absPath, err := filepath.Abs(filePath) + if err != nil { + return nil, err + } + // #nosec G304 + contentBytes, err := os.ReadFile(absPath) + return contentBytes, err +} + func init() { generateOperatorKeysCmd.Flags().StringP("password-file", "p", "", "File path to the password used to encrypt the private key") generateOperatorKeysCmd.Flags().StringP("operator-key-file", "o", "", "File path to the operator private key") diff --git a/cli/operator/node.go b/cli/operator/node.go index f7539575ad..4dd14f558a 100644 --- a/cli/operator/node.go +++ b/cli/operator/node.go @@ -334,7 +334,17 @@ func setupGlobal(cmd *cobra.Command) (*zap.Logger, error) { } } - if err := logging.SetGlobalLogger(cfg.LogLevel, cfg.LogLevelFormat, cfg.LogFormat, cfg.LogFilePath); err != nil { + err := logging.SetGlobalLogger( + cfg.LogLevel, + cfg.LogLevelFormat, + cfg.LogFormat, + &logging.LogFileOptions{ + FileName: cfg.LogFilePath, + MaxSize: cfg.LogFileSize, + MaxBackups: cfg.LogFileBackups, + }, + ) + if err != nil { return nil, fmt.Errorf("logging.SetGlobalLogger: %w", err) } diff --git a/cli/threshold.go b/cli/threshold.go index b161db709d..ca673eabaa 100644 --- a/cli/threshold.go +++ b/cli/threshold.go @@ -18,7 +18,7 @@ var createThresholdCmd = &cobra.Command{ Use: "create-threshold", Short: "Turns a private key into a threshold key", Run: func(cmd *cobra.Command, args []string) { - if err := logging.SetGlobalLogger("debug", "capital", "console", ""); err != nil { + if err := logging.SetGlobalLogger("debug", "capital", "console", nil); err != nil { log.Fatal(err) } logger := zap.L().Named(logging.NameCreateThreshold) diff --git a/integration/qbft/tests/setup_test.go b/integration/qbft/tests/setup_test.go index 3a5dcb4d39..f8c4222dbc 100644 --- a/integration/qbft/tests/setup_test.go +++ b/integration/qbft/tests/setup_test.go @@ -34,7 +34,7 @@ func GetSharedData(t *testing.T) SharedData { //singleton B-) func TestMain(m *testing.M) { ctx := context.Background() - if err := logging.SetGlobalLogger("debug", "capital", "console", ""); err != nil { + if err := logging.SetGlobalLogger("debug", "capital", "console", nil); err != nil { panic(err) } diff --git a/logging/global.go b/logging/global.go index 7ff7d8740f..11b0eb8ab7 100644 --- a/logging/global.go +++ b/logging/global.go @@ -13,19 +13,6 @@ import ( "go.uber.org/zap/zapcore" ) -// TODO: Log rotation out of the app -func getFileWriter(logFileName string) io.Writer { - fileLogger := &lumberjack.Logger{ - Filename: logFileName, - MaxSize: 500, // megabytes - MaxBackups: 3, - MaxAge: 28, // days - Compress: false, - } - - return fileLogger -} - func parseConfigLevel(levelName string) (zapcore.Level, error) { return zapcore.ParseLevel(levelName) } @@ -43,7 +30,17 @@ func parseConfigLevelEncoder(levelEncoderName string) zapcore.LevelEncoder { } } -func SetGlobalLogger(levelName string, levelEncoderName string, logFormat string, logFilePath string) error { +func SetGlobalLogger(levelName string, levelEncoderName string, logFormat string, fileOptions *LogFileOptions) (err error) { + defer func() { + if err == nil { + zap.L().Debug("logger is ready", + zap.String("level", levelName), + zap.String("encoder", levelEncoderName), + zap.String("format", logFormat), + zap.Any("file_options", fileOptions), + ) + } + }() level, err := parseConfigLevel(levelName) if err != nil { return err @@ -77,25 +74,39 @@ func SetGlobalLogger(levelName string, levelEncoderName string, logFormat string consoleCore := zapcore.NewCore(zapcore.NewConsoleEncoder(cfg.EncoderConfig), os.Stdout, lv) - if logFilePath == "" { + if fileOptions == nil { zap.ReplaceGlobals(zap.New(consoleCore)) return nil } - logFileWriter := getFileWriter(logFilePath) - lv2 := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return true // debug log returns all logs }) dev := zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig()) - fileCore := zapcore.NewCore(dev, zapcore.AddSync(logFileWriter), lv2) + fileWriter := fileOptions.writer(fileOptions) + fileCore := zapcore.NewCore(dev, zapcore.AddSync(fileWriter), lv2) zap.ReplaceGlobals(zap.New(zapcore.NewTee(consoleCore, fileCore))) - return nil } +type LogFileOptions struct { + FileName string + MaxSize int + MaxBackups int +} + +func (o LogFileOptions) writer(options *LogFileOptions) io.Writer { + return &lumberjack.Logger{ + Filename: options.FileName, + MaxSize: options.MaxSize, // megabytes + MaxBackups: options.MaxBackups, + MaxAge: 28, // days + Compress: false, + } +} + func CapturePanic(logger *zap.Logger) { if r := recover(); r != nil { // defer logger.Sync() diff --git a/logging/testing.go b/logging/testing.go index b73a22ec91..6b6abd8326 100644 --- a/logging/testing.go +++ b/logging/testing.go @@ -8,13 +8,13 @@ import ( ) func TestLogger(t *testing.T) *zap.Logger { - err := SetGlobalLogger("debug", "capital", "console", "") + err := SetGlobalLogger("debug", "capital", "console", nil) require.NoError(t, err) return zap.L().Named(t.Name()) } func BenchLogger(b *testing.B) *zap.Logger { - err := SetGlobalLogger("debug", "capital", "console", "") + err := SetGlobalLogger("debug", "capital", "console", nil) require.NoError(b, err) return zap.L().Named(b.Name()) }