Skip to content

Commit

Permalink
Merge pull request #6 from multiversx/MX-14814-certificate-auth
Browse files Browse the repository at this point in the history
Certificate authentication
  • Loading branch information
mariusmihaic authored Jan 16, 2024
2 parents 1592281 + 5256011 commit 83009cb
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 27 deletions.
162 changes: 162 additions & 0 deletions cert/cert.go
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions cert/cmd/cert/flags.go
Original file line number Diff line number Diff line change
@@ -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",
}
)
64 changes: 64 additions & 0 deletions cert/cmd/cert/main.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions client/cmd/client/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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"
16 changes: 14 additions & 2 deletions client/cmd/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand Down Expand Up @@ -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
}

Expand Down
7 changes: 5 additions & 2 deletions client/config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 0 additions & 2 deletions client/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ package client
import "errors"

var errNilClientConnection = errors.New("nil grpc client connection provided")

var errCannotOpenConnection = errors.New("cannot open connection")
31 changes: 16 additions & 15 deletions client/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ 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")

// 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
}
Expand All @@ -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
}
Loading

0 comments on commit 83009cb

Please sign in to comment.