Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!(core): enable tls for grpc connection #3922

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion nodebuilder/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ type Config struct {
IP string
RPCPort string
GRPCPort string
// TLSPath specifies the directory path where the TLS certificates are stored.
// It should not include file names('cert.pem' and 'key.pem').
// If left empty, the client will be configured for an insecure (non-TLS) connection.
TLSPath string
// XTokenPath specifies the file path to the JSON file containing the X-Token for gRPC authentication.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments say this is path to the JSON file, but due to the use of the xtoken const, needs to be the path to the directory containing a file named xtoken.json

// The JSON file should have a key-value pair where the key is "x-token" and the value is the authentication token.
// If left empty, the client will not include the X-Token in its requests.
XTokenPath string
}

// DefaultConfig returns default configuration for managing the
// node's connection to a Celestia-Core endpoint.
func DefaultConfig() Config {
return Config{
IP: "",
RPCPort: DefaultRPCPort,
GRPCPort: DefaultGRPCPort,
}
Expand Down
31 changes: 28 additions & 3 deletions nodebuilder/core/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
)

var (
coreFlag = "core.ip"
coreRPCFlag = "core.rpc.port"
coreGRPCFlag = "core.grpc.port"
coreFlag = "core.ip"
coreRPCFlag = "core.rpc.port"
coreGRPCFlag = "core.grpc.port"
coreTLSPathFlag = "core.grpc.tls.path"
coreXTokenPathFlag = "core.grpc.xtoken.path" //nolint:gosec
)

// Flags gives a set of hardcoded Core flags.
Expand All @@ -34,6 +36,20 @@ func Flags() *flag.FlagSet {
DefaultGRPCPort,
"Set a custom gRPC port for the core node connection. The --core.ip flag must also be provided.",
)
flags.String(
coreTLSPathFlag,
"",
"specifies the directory path where the TLS certificates are stored. "+
"It should not include file names ('cert.pem' and 'key.pem'). "+
"If left empty, the client will be configured for an insecure (non-TLS) connection",
)
flags.String(
coreXTokenPathFlag,
"",
"specifies the file path to the JSON file containing the X-Token for gRPC authentication. "+
"The JSON file should have a key-value pair where the key is 'x-token' and the value is the authentication token. "+
"If left empty, the client will not include the X-Token in its requests.",
)
return flags
}

Expand All @@ -60,6 +76,15 @@ func ParseFlags(
cfg.GRPCPort = grpc
}

if cmd.Flag(coreTLSPathFlag).Changed {
path := cmd.Flag(coreTLSPathFlag).Value.String()
cfg.TLSPath = path
}

if cmd.Flag(coreXTokenPathFlag).Changed {
path := cmd.Flag(coreXTokenPathFlag).Value.String()
cfg.XTokenPath = path
}
cfg.IP = coreIP
return cfg.Validate()
}
82 changes: 82 additions & 0 deletions nodebuilder/core/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package core

import (
"crypto/tls"
"encoding/json"
"errors"
"os"
"path/filepath"

"github.com/celestiaorg/celestia-node/libs/utils"
)

const (
cert = "cert.pem"
key = "key.pem"
xtoken = "xtoken.json"
)

// TLS creates a TLS configuration using the certificate and key files from the specified path.
// It constructs the full paths to the certificate and key files by joining the provided directory path
// with their respective file names.
// If either file is missing, it returns an os.ErrNotExist error.
// If the files exist, it loads the X.509 key pair from the specified files and sets up a tls.Config
// with a minimum version of TLS 1.2.
// Parameters:
// * tlsPath: The directory path where the TLS certificate ("cert.pem") and key ("key.pem") files are located.
// Returns:
// * A tls.Config structure configured with the provided certificate and key.
// * An error if the certificate or key file does not exist, or if loading the key pair fails.
func TLS(tlsPath string) (*tls.Config, error) {
certPath := filepath.Join(tlsPath, cert)
keyPath := filepath.Join(tlsPath, key)
exist := utils.Exists(certPath) && utils.Exists(keyPath)
if !exist {
return nil, os.ErrNotExist
}
Comment on lines +37 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should just return empty tls config? Seems os.ErrNotExist is not needed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I added this behavior initially but then realized that we may have more usecases in the future so the caller will decide what to do with this error.


cfg := &tls.Config{MinVersion: tls.VersionTLS12}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
}
cfg.Certificates = append(cfg.Certificates, cert)
return cfg, nil
}

type AuthToken struct {
Token string `json:"x-token"`
}

// XToken retrieves the authentication token from a JSON file at the specified path.
// It first constructs the full file path by joining the provided directory path with the token file name.
// If the file does not exist, it returns an os.ErrNotExist error.
// If the file exists, it reads the content and unmarshals it.
// If the field in the unmarshaled struct is empty, an error is returned indicating that the token is missing.
// Parameters:
// * xtokenPath: The directory path where the JSON file containing the X-Token is located.
// Returns:
// * The X-Token as a string if successfully retrieved.
// * An error if the file does not exist, reading fails, unmarshalling fails, or the token is empty.
func XToken(xtokenPath string) (string, error) {
xtokenPath = filepath.Join(xtokenPath, xtoken)
exist := utils.Exists(xtokenPath)
if !exist {
return "", os.ErrNotExist
}

token, err := os.ReadFile(xtokenPath)
if err != nil {
return "", err
}

var auth AuthToken
err = json.Unmarshal(token, &auth)
if err != nil {
return "", err
}
if auth.Token == "" {
return "", errors.New("x-token is empty. Please setup a token or cleanup xtokenPath")
}
return auth.Token, nil
}
18 changes: 15 additions & 3 deletions nodebuilder/state/core.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package state

import (
"errors"
"os"

"github.com/cosmos/cosmos-sdk/crypto/keyring"

libfraud "github.com/celestiaorg/go-fraud"
Expand Down Expand Up @@ -30,14 +33,23 @@ func coreAccessor(
*modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader],
error,
) {
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, corecfg.IP, corecfg.GRPCPort,
network.String(), opts...)
tls, err := core.TLS(corecfg.TLSPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, nil, nil, err
}
xtoken, err := core.XToken(corecfg.XTokenPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, nil, nil, err
}

opts = append(opts, state.WithTLSConfig(tls), state.WithXToken(xtoken))
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync,
corecfg.IP, corecfg.GRPCPort, network.String(), opts...)

sBreaker := &modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]{
Service: ca,
FraudType: byzantine.BadEncoding,
FraudServ: fraudServ,
}

return ca, ca, sBreaker, err
}
90 changes: 72 additions & 18 deletions state/core_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package state

import (
"context"
"crypto/tls"
"errors"
"fmt"
"sync"
Expand All @@ -20,7 +21,9 @@ import (
"github.com/tendermint/tendermint/proto/tendermint/crypto"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"

"github.com/celestiaorg/celestia-app/v3/app"
"github.com/celestiaorg/celestia-app/v3/app/encoding"
Expand All @@ -46,6 +49,18 @@ var (
// to configure parameters.
type Option func(ca *CoreAccessor)

func WithTLSConfig(cfg *tls.Config) Option {
return func(ca *CoreAccessor) {
ca.tls = cfg
}
}

func WithXToken(xtoken string) Option {
return func(ca *CoreAccessor) {
ca.xtoken = xtoken
}
}

// CoreAccessor implements service over a gRPC connection
// with a celestia-core node.
type CoreAccessor struct {
Expand All @@ -72,6 +87,9 @@ type CoreAccessor struct {
grpcPort string
network string

tls *tls.Config
xtoken string

// these fields are mutatable and thus need to be protected by a mutex
lock sync.Mutex
lastPayForBlob int64
Expand All @@ -90,9 +108,7 @@ func NewCoreAccessor(
keyring keyring.Keyring,
keyname string,
getter libhead.Head[*header.ExtendedHeader],
coreIP,
grpcPort string,
network string,
coreIP, grpcPort, network string,
options ...Option,
) (*CoreAccessor, error) {
// create verifier
Expand Down Expand Up @@ -122,24 +138,11 @@ func (ca *CoreAccessor) Start(ctx context.Context) error {
}
ca.ctx, ca.cancel = context.WithCancel(context.Background())

// dial given celestia-core endpoint
endpoint := fmt.Sprintf("%s:%s", ca.coreIP, ca.grpcPort)
client, err := grpc.NewClient(
endpoint,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
err := ca.startGRPCClient(ctx)
if err != nil {
return err
}
// this ensures we can't start the node without core connection
client.Connect()
if !client.WaitForStateChange(ctx, connectivity.Ready) {
// hits the case when context is canceled
return fmt.Errorf("couldn't connect to core endpoint(%s): %w", endpoint, ctx.Err())
return fmt.Errorf("failed to start grpc client: %w", err)
}

ca.coreConn = client

// create the staking query client
ca.stakingCli = stakingtypes.NewQueryClient(ca.coreConn)
ca.feeGrantCli = feegrant.NewQueryClient(ca.coreConn)
Expand Down Expand Up @@ -601,6 +604,57 @@ func (ca *CoreAccessor) setupTxClient(ctx context.Context, keyName string) (*use
)
}

func (ca *CoreAccessor) startGRPCClient(ctx context.Context) error {
// dial given celestia-core endpoint
endpoint := fmt.Sprintf("%s:%s", ca.coreIP, ca.grpcPort)
// By default, the gRPC client is configured to handle an insecure connection.
// If the TLS configuration is not empty, it will be applied to the client's options.
// If the TLS configuration is empty but the X-Token is provided,
// the X-Token will be applied as an interceptor along with an empty TLS configuration.
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
if ca.tls != nil {
fmt.Println("TRANSPORT")
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(ca.tls)))
} else if ca.xtoken != "" {
fmt.Println("TOKEN")
authInterceptor := func(ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
ctx = metadata.AppendToOutgoingContext(ctx, "x-token", ca.xtoken)
return invoker(ctx, method, req, reply, cc, opts...)
}
Comment on lines +619 to +628
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, but would be easier to read if interceptor is extracted

Copy link
Member Author

@vgonkivs vgonkivs Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was planning to move it out. Thanks 🙏

// set the config with empty certificates along with the interceptor that will add x-token.
opts = append(opts,
grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}),
),
grpc.WithUnaryInterceptor(authInterceptor),
)
}

client, err := grpc.NewClient(
endpoint,
opts...,
)
if err != nil {
return err
}
// this ensures we can't start the node without core connection
client.Connect()
if !client.WaitForStateChange(ctx, connectivity.Ready) {
// hits the case when context is canceled
return fmt.Errorf("couldn't connect to core endpoint(%s): %w", endpoint, ctx.Err())
}
ca.coreConn = client

log.Infof("Connection with core endpoint(%s) established", endpoint)
return nil
}

func (ca *CoreAccessor) submitMsg(
ctx context.Context,
msg sdktypes.Msg,
Expand Down
Loading