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): added support of the TLS certificates along with x-token authorisation token for the gRPC connection #3954

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
8 changes: 7 additions & 1 deletion nodebuilder/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ type Config struct {
IP string
RPCPort string
GRPCPort string
// TLSEnabled specifies whether the connection is secure or not.
// PLEASE NOTE: it should be set to true in order to handle XTokenPath.
TLSEnabled bool
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this if we just provide a TLSPath?

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, we need it for a use case when we have to provide an empty config.

// XTokenPath specifies the path to the directory with 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.
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
33 changes: 30 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"
Copy link
Member

Choose a reason for hiding this comment

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

if you rebase, you won't need this anymore and also won't need to specify grpc in the name of the flag

so core.grpc.tls.path --> core.tls.path and same for xtoken

coreGRPCFlag = "core.grpc.port"
coreTLS = "core.tls"
coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
)

// Flags gives a set of hardcoded Core flags.
Expand All @@ -34,6 +36,19 @@ func Flags() *flag.FlagSet {
DefaultGRPCPort,
"Set a custom gRPC port for the core node connection. The --core.ip flag must also be provided.",
)
flags.Bool(
coreTLS,
false,
"Specifies whether TLS is enabled or not. Default: false",
)
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. "+
"NOTE: the path is parsed only if coreTLS enabled."+
"If left empty, the client will not include the X-Token in its requests.",
)
return flags
}

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

enabled, err := cmd.Flags().GetBool(coreTLS)
if err != nil {
panic(err)
}

if enabled {
cfg.TLSEnabled = true
if cmd.Flag(coreXTokenPathFlag).Changed {
path := cmd.Flag(coreXTokenPathFlag).Value.String()
cfg.XTokenPath = path
}
}
cfg.IP = coreIP
return cfg.Validate()
}
58 changes: 58 additions & 0 deletions nodebuilder/core/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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"
)

func EmptyTLSConfig() *tls.Config {
return &tls.Config{MinVersion: tls.VersionTLS12}
}

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 unmarshalls it.
// If the field in the unmarshalled 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...)
if corecfg.TLSEnabled {
// set an empty config if path is empty under `TLSEnabled=true`
tlsCfg := core.EmptyTLSConfig()
xtoken, err := core.XToken(corecfg.XTokenPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, nil, nil, err
}
opts = append(opts, state.WithTLSConfig(tlsCfg), state.WithXToken(xtoken))
Copy link
Member Author

Choose a reason for hiding this comment

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

}

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
}
91 changes: 73 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,41 @@ 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 {
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(ca.tls)))
}

if interceptor := authInterceptor(ca.xtoken); interceptor != nil {
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
}

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 Expand Up @@ -657,3 +695,20 @@ func convertToSdkTxResponse(resp *user.TxResponse) *TxResponse {
Height: resp.Height,
}
}

func authInterceptor(xtoken string) grpc.UnaryClientInterceptor {
if xtoken == "" {
return nil
}
return 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", xtoken)
return invoker(ctx, method, req, reply, cc, opts...)
}
}