diff --git a/cert/cert.go b/cert/cert.go new file mode 100644 index 0000000..f76005c --- /dev/null +++ b/cert/cert.go @@ -0,0 +1,162 @@ +package cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "os" + "time" + + logger "github.com/multiversx/mx-chain-logger-go" +) + +var log = logger.GetOrCreate("cert") + +// CertificateCfg holds necessary config to generate certificate files +type CertificateCfg struct { + CertCfg CertCfg + CertFileCfg FileCfg +} + +// CertCfg holds necessary config to generate a certificate and private key +type CertCfg struct { + Organization string + DNSName string + Availability int64 +} + +// FileCfg holds necessary config for certificate files +type FileCfg struct { + CertFile string + PkFile string +} + +const day = time.Hour * 24 + +// GenerateCert will generate a certificate and private key with specified configuration +func GenerateCert(cfg CertCfg) ([]byte, *rsa.PrivateKey, error) { + pk, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, err + } + + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{cfg.Organization}, + CommonName: cfg.Organization, + }, + DNSNames: []string{cfg.DNSName}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Duration(cfg.Availability) * day), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + cert, err := x509.CreateCertificate(rand.Reader, template, template, pk.Public(), pk) + if err != nil { + return nil, nil, err + } + + return cert, pk, nil +} + +// GenerateCertFiles will generate a certificate and private key files with specified configuration +func GenerateCertFiles(cfg CertificateCfg) error { + cert, pk, err := GenerateCert(cfg.CertCfg) + if err != nil { + return err + } + + certOut, err := os.Create(cfg.CertFileCfg.CertFile) + if err != nil { + return fmt.Errorf("cannot create certificate file, cert file: %s,error: %w", cfg.CertFileCfg.CertFile, err) + } + defer func() { + err = certOut.Close() + log.LogIfError(err) + }() + + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert}) + if err != nil { + return fmt.Errorf("cannot create pem encoded file, cert file: %s,error: %w", cfg.CertFileCfg.CertFile, err) + } + + keyOut, err := os.Create(cfg.CertFileCfg.PkFile) + if err != nil { + return fmt.Errorf("cannot create certificate private key file, cert pk file: %s,error: %w", cfg.CertFileCfg.PkFile, err) + } + defer func() { + err = keyOut.Close() + log.LogIfError(err) + }() + + pkBytes := x509.MarshalPKCS1PrivateKey(pk) + err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkBytes}) + if err != nil { + return fmt.Errorf("cannot create certificate pk file, cert pk file: %s,error: %w", cfg.CertFileCfg.PkFile, err) + } + + return nil +} + +// LoadTLSServerConfig will load a tls server config +func LoadTLSServerConfig(cfg FileCfg) (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.PkFile) + if err != nil { + return nil, err + } + + certPool, err := createCertPool(cert) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientCAs: certPool, + ClientAuth: tls.RequireAndVerifyClientCert, + }, nil +} + +// LoadTLSClientConfig will load a tls client config +func LoadTLSClientConfig(cfg FileCfg) (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.PkFile) + if err != nil { + return nil, err + } + + certPool, err := createCertPool(cert) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: certPool, + }, nil +} + +func createCertPool(cert tls.Certificate) (*x509.CertPool, error) { + certLeaf, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return nil, err + } + + certPool := x509.NewCertPool() + certPool.AddCert(certLeaf) + + return certPool, nil +} diff --git a/cert/cmd/cert/flags.go b/cert/cmd/cert/flags.go new file mode 100644 index 0000000..70252db --- /dev/null +++ b/cert/cmd/cert/flags.go @@ -0,0 +1,21 @@ +package main + +import "github.com/urfave/cli" + +var ( + organizationFlag = cli.StringFlag{ + Name: "organization", + Usage: "This flag specifies the organization name which will generate the certificate", + Value: "MultiversX", + } + dnsFlag = cli.StringFlag{ + Name: "dns", + Usage: "This flag specifies the server's dns for tls connection", + Value: "localhost", + } + availabilityFlag = cli.StringFlag{ + Name: "availability", + Usage: "This flag specifies the certificate's availability in days starting from current timestamp", + Value: "365", + } +) diff --git a/cert/cmd/cert/main.go b/cert/cmd/cert/main.go new file mode 100644 index 0000000..275dfab --- /dev/null +++ b/cert/cmd/cert/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "os" + + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/multiversx/mx-chain-sovereign-bridge-go/cert" + "github.com/urfave/cli" +) + +var log = logger.GetOrCreate("cert") + +func main() { + + app := cli.NewApp() + app.Name = "Certificate generator" + app.Usage = "Generate certificate (.crt + .pem) for grpc tls connection between server and client.\n" + + "->Certificate Generation: To enable secure communication, generate a certificate pair containing a .crt (certificate) " + + "and a .pem (private key) for both the server and the sovereign nodes (clients). This will facilitate the encryption and " + + "authentication required for the gRPC TLS connection.\n" + + "->Authentication of Clients: The server, acting as the hot wallet binary, should authenticate and validate the sovereign nodes (clients) " + + "attempting to connect. Only trusted clients with the matching certificate will be granted access to interact with the hot wallet binary.\n" + + "->Ensuring Secure Transactions: Utilize the certificate-based authentication mechanism to ensure that only authorized sovereign nodes can access the hot wallet binary. " + + "This step is crucial in maintaining the integrity and security of transactions being sent from the sovereign shards to the main chain.\n" + + "->Ongoing Security Measures: Regularly review and update the certificate mechanism to maintain security. This includes renewal of certificates, " + + "implementing security best practices, and promptly revoking access for compromised or unauthorized clients." + app.Action = generateCertificate + app.Flags = []cli.Flag{ + organizationFlag, + dnsFlag, + availabilityFlag, + } + + err := app.Run(os.Args) + if err != nil { + log.Error(err.Error()) + os.Exit(1) + } + +} + +func generateCertificate(ctx *cli.Context) error { + organization := ctx.GlobalString(organizationFlag.Name) + dns := ctx.GlobalString(dnsFlag.Name) + availability := ctx.GlobalInt64(availabilityFlag.Name) + + err := cert.GenerateCertFiles(cert.CertificateCfg{ + CertCfg: cert.CertCfg{ + Organization: organization, + DNSName: dns, + Availability: availability, + }, + CertFileCfg: cert.FileCfg{ + CertFile: "certificate.crt", + PkFile: "private_key.pem", + }, + }) + if err != nil { + return err + } + + log.Info("generated certificate files successfully") + return nil +} diff --git a/client/cmd/client/.env b/client/cmd/client/.env index ae9f104..954ec2c 100644 --- a/client/cmd/client/.env +++ b/client/cmd/client/.env @@ -2,3 +2,9 @@ GRPC_HOST="localhost" # GRPC server port GRPC_PORT="8085" +# Client certificate for tls secured connection with server. +# One should use the same certificate for server as well. +# You can generate your own certificate files with the binary found in +# this repository in cert/cmd/cert +CERT_FILE="certificate.crt" +CERT_PK_FILE="private_key.pem" diff --git a/client/cmd/client/main.go b/client/cmd/client/main.go index 9ebd2b4..949d9cf 100644 --- a/client/cmd/client/main.go +++ b/client/cmd/client/main.go @@ -9,6 +9,7 @@ import ( "github.com/joho/godotenv" "github.com/multiversx/mx-chain-core-go/data/sovereign" logger "github.com/multiversx/mx-chain-logger-go" + "github.com/multiversx/mx-chain-sovereign-bridge-go/cert" "github.com/multiversx/mx-chain-sovereign-bridge-go/client" "github.com/multiversx/mx-chain-sovereign-bridge-go/client/config" "github.com/urfave/cli" @@ -17,8 +18,10 @@ import ( var log = logger.GetOrCreate("client-tx-sender") const ( - envGRPCHost = "GRPC_HOST" - envGRPCPort = "GRPC_PORT" + envGRPCHost = "GRPC_HOST" + envGRPCPort = "GRPC_PORT" + envCertFile = "CERT_FILE" + envCertPkFile = "CERT_PK_FILE" ) func main() { @@ -136,13 +139,22 @@ func loadConfig() (*config.ClientConfig, error) { grpcHost := os.Getenv(envGRPCHost) grpcPort := os.Getenv(envGRPCPort) + certFile := os.Getenv(envCertFile) + certPkFile := os.Getenv(envCertPkFile) log.Info("loaded config", "grpc host", grpcHost) log.Info("loaded config", "grpc port", grpcPort) + log.Info("loaded config", "certificate file", certFile) + log.Info("loaded config", "certificate pk", certPkFile) + return &config.ClientConfig{ GRPCHost: grpcHost, GRPCPort: grpcPort, + CertificateCfg: cert.FileCfg{ + CertFile: certFile, + PkFile: certPkFile, + }, }, nil } diff --git a/client/config/config.go b/client/config/config.go index e15a80d..b91e640 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -1,7 +1,10 @@ package config +import "github.com/multiversx/mx-chain-sovereign-bridge-go/cert" + // ClientConfig holds all grpc client's config type ClientConfig struct { - GRPCHost string - GRPCPort string + GRPCHost string + GRPCPort string + CertificateCfg cert.FileCfg } diff --git a/client/errors.go b/client/errors.go index 07950ce..863869d 100644 --- a/client/errors.go +++ b/client/errors.go @@ -3,5 +3,3 @@ package client import "errors" var errNilClientConnection = errors.New("nil grpc client connection provided") - -var errCannotOpenConnection = errors.New("cannot open connection") diff --git a/client/factory.go b/client/factory.go index da633d0..0423b1c 100644 --- a/client/factory.go +++ b/client/factory.go @@ -6,14 +6,14 @@ import ( "github.com/multiversx/mx-chain-core-go/data/sovereign" logger "github.com/multiversx/mx-chain-logger-go" + "github.com/multiversx/mx-chain-sovereign-bridge-go/cert" "github.com/multiversx/mx-chain-sovereign-bridge-go/client/config" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/credentials" ) const ( - maxConnectionRetries = 100 - waitTime = 5 + waitTime = 5 ) var log = logger.GetOrCreate("client") @@ -21,7 +21,7 @@ var log = logger.GetOrCreate("client") // CreateClient creates a grpc client with retries func CreateClient(cfg *config.ClientConfig) (ClientHandler, error) { dialTarget := fmt.Sprintf("%s:%s", cfg.GRPCHost, cfg.GRPCPort) - conn, err := connectWithRetries(dialTarget) + conn, err := connectWithRetries(dialTarget, cfg.CertificateCfg) if err != nil { return nil, err } @@ -30,23 +30,24 @@ func CreateClient(cfg *config.ClientConfig) (ClientHandler, error) { return NewClient(bridgeClient, conn) } -func connectWithRetries(host string) (GRPCConn, error) { - credentials := insecure.NewCredentials() - opts := grpc.WithTransportCredentials(credentials) +func connectWithRetries(host string, cfg cert.FileCfg) (GRPCConn, error) { + tlsConfig, err := cert.LoadTLSClientConfig(cfg) + if err != nil { + return nil, err + } - for i := 0; i < maxConnectionRetries; i++ { - cc, err := grpc.Dial(host, opts) - if err == nil { - return cc, err + for i := 0; ; i++ { + tlsCredentials := credentials.NewTLS(tlsConfig) + cc, errConnection := grpc.Dial(host, grpc.WithTransportCredentials(tlsCredentials)) + if errConnection == nil { + return cc, errConnection } time.Sleep(time.Second * waitTime) log.Warn("could not establish connection, retrying", - "error", err, + "error", errConnection, "host", host, - "retrial", i+1) + "retries", i+1) } - - return nil, errCannotOpenConnection } diff --git a/server/cmd/config/config.go b/server/cmd/config/config.go index 368f804..6c0c912 100644 --- a/server/cmd/config/config.go +++ b/server/cmd/config/config.go @@ -1,10 +1,14 @@ package config -import "github.com/multiversx/mx-chain-sovereign-bridge-go/server/txSender" +import ( + "github.com/multiversx/mx-chain-sovereign-bridge-go/cert" + "github.com/multiversx/mx-chain-sovereign-bridge-go/server/txSender" +) // ServerConfig holds necessary config for the grpc server type ServerConfig struct { - GRPCPort string - TxSenderConfig txSender.TxSenderConfig - WalletConfig txSender.WalletConfig + GRPCPort string + TxSenderConfig txSender.TxSenderConfig + WalletConfig txSender.WalletConfig + CertificateConfig cert.FileCfg } diff --git a/server/cmd/server/.env b/server/cmd/server/.env index 52ae988..45c94c0 100644 --- a/server/cmd/server/.env +++ b/server/cmd/server/.env @@ -12,3 +12,9 @@ MULTIVERSX_PROXY="https://testnet-gateway.multiversx.com" BRIDGE_SC_ADDRESS="erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" # Max retries to wait in seconds for account nonce update after sending bridge txs MAX_RETRIES_SECONDS_WAIT_NONCE=60 +# Server certificate for tls secured connection with clients. +# One should use the same certificate for clients as well. +# You can generate your own certificate files with the binary found in +# this repository in cert/cmd/cert +CERT_FILE="certificate.crt" +CERT_PK_FILE="private_key.pem" diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index ed6fcdc..2871049 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -15,12 +15,13 @@ import ( "github.com/multiversx/mx-chain-core-go/data/sovereign" logger "github.com/multiversx/mx-chain-logger-go" "github.com/multiversx/mx-chain-logger-go/file" + "github.com/multiversx/mx-chain-sovereign-bridge-go/cert" "github.com/multiversx/mx-chain-sovereign-bridge-go/server" "github.com/multiversx/mx-chain-sovereign-bridge-go/server/cmd/config" "github.com/multiversx/mx-chain-sovereign-bridge-go/server/txSender" - "github.com/urfave/cli" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ) var log = logger.GetOrCreate("sov-bridge-sender") @@ -40,6 +41,8 @@ const ( envBridgeSCAddr = "BRIDGE_SC_ADDRESS" envMultiversXProxy = "MULTIVERSX_PROXY" envMaxRetriesWaitNonce = "MAX_RETRIES_SECONDS_WAIT_NONCE" + envCertFile = "CERT_FILE" + envCertPkFile = "CERT_PK_FILE" ) func main() { @@ -73,7 +76,15 @@ func startServer(ctx *cli.Context) error { return err } - grpcServer := grpc.NewServer() + tlsConfig, err := cert.LoadTLSServerConfig(cfg.CertificateConfig) + if err != nil { + return err + } + + tlsCredentials := credentials.NewTLS(tlsConfig) + grpcServer := grpc.NewServer( + grpc.Creds(tlsCredentials), + ) bridgeServer, err := server.CreateServer(cfg) if err != nil { return err @@ -119,6 +130,8 @@ func loadConfig() (*config.ServerConfig, error) { bridgeSCAddress := os.Getenv(envBridgeSCAddr) proxy := os.Getenv(envMultiversXProxy) maxRetriesWaitNonceStr := os.Getenv(envMaxRetriesWaitNonce) + certFile := os.Getenv(envCertFile) + certPkFile := os.Getenv(envCertPkFile) maxRetriesWaitNonce, err := strconv.Atoi(maxRetriesWaitNonceStr) if err != nil { @@ -130,6 +143,9 @@ func loadConfig() (*config.ServerConfig, error) { log.Info("loaded config", "proxy", proxy) log.Info("loaded config", "maxRetriesWaitNonce", maxRetriesWaitNonce) + log.Info("loaded config", "certificate file", certFile) + log.Info("loaded config", "certificate pk", certPkFile) + return &config.ServerConfig{ GRPCPort: grpcPort, WalletConfig: txSender.WalletConfig{ @@ -141,6 +157,10 @@ func loadConfig() (*config.ServerConfig, error) { Proxy: proxy, MaxRetriesSecondsWaitNonce: maxRetriesWaitNonce, }, + CertificateConfig: cert.FileCfg{ + CertFile: certFile, + PkFile: certPkFile, + }, }, nil }