Skip to content

Commit 1493a22

Browse files
committed
feat(core): added support of the TLS certificates along with x-token authorisation token for the gRPC connection (#3954)
1 parent 46c6e9b commit 1493a22

File tree

5 files changed

+163
-23
lines changed

5 files changed

+163
-23
lines changed

nodebuilder/core/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ var MetricsEnabled bool
1717
type Config struct {
1818
IP string
1919
Port string
20+
// TLSEnabled specifies whether the connection is secure or not.
21+
// PLEASE NOTE: it should be set to true in order to handle XTokenPath.
22+
TLSEnabled bool
23+
// XTokenPath specifies the path to the directory with JSON file containing the X-Token for gRPC authentication.
24+
// The JSON file should have a key-value pair where the key is "x-token" and the value is the authentication token.
25+
// If left empty, the client will not include the X-Token in its requests.
26+
XTokenPath string
2027
}
2128

2229
// DefaultConfig returns default configuration for managing the

nodebuilder/core/flags.go

+29-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
)
99

1010
var (
11-
coreFlag = "core.ip"
12-
coreGRPCFlag = "core.grpc.port"
11+
coreFlag = "core.ip"
12+
coreGRPCFlag = "core.grpc.port"
13+
coreTLS = "core.tls"
14+
coreXTokenPathFlag = "core.xtoken.path" //nolint:gosec
1315
)
1416

1517
// Flags gives a set of hardcoded Core flags.
@@ -28,6 +30,19 @@ func Flags() *flag.FlagSet {
2830
DefaultPort,
2931
"Set a custom gRPC port for the core node connection. The --core.ip flag must also be provided.",
3032
)
33+
flags.Bool(
34+
coreTLS,
35+
false,
36+
"Specifies whether TLS is enabled or not. Default: false",
37+
)
38+
flags.String(
39+
coreXTokenPathFlag,
40+
"",
41+
"specifies the file path to the JSON file containing the X-Token for gRPC authentication. "+
42+
"The JSON file should have a key-value pair where the key is 'x-token' and the value is the authentication token. "+
43+
"NOTE: the path is parsed only if coreTLS enabled."+
44+
"If left empty, the client will not include the X-Token in its requests.",
45+
)
3146
return flags
3247
}
3348

@@ -49,6 +64,18 @@ func ParseFlags(
4964
cfg.Port = grpc
5065
}
5166

67+
enabled, err := cmd.Flags().GetBool(coreTLS)
68+
if err != nil {
69+
return err
70+
}
71+
72+
if enabled {
73+
cfg.TLSEnabled = true
74+
if cmd.Flag(coreXTokenPathFlag).Changed {
75+
path := cmd.Flag(coreXTokenPathFlag).Value.String()
76+
cfg.XTokenPath = path
77+
}
78+
}
5279
cfg.IP = coreIP
5380
return cfg.Validate()
5481
}

nodebuilder/core/tls.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package core
2+
3+
import (
4+
"crypto/tls"
5+
"encoding/json"
6+
"errors"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/celestiaorg/celestia-node/libs/utils"
11+
)
12+
13+
const xtokenFileName = "xtoken.json"
14+
15+
func EmptyTLSConfig() *tls.Config {
16+
return &tls.Config{MinVersion: tls.VersionTLS12}
17+
}
18+
19+
// XToken retrieves the authentication token from a JSON file at the specified path.
20+
func XToken(xtokenPath string) (string, error) {
21+
xtokenPath = filepath.Join(xtokenPath, xtokenFileName)
22+
exist := utils.Exists(xtokenPath)
23+
if !exist {
24+
return "", os.ErrNotExist
25+
}
26+
27+
token, err := os.ReadFile(xtokenPath)
28+
if err != nil {
29+
return "", err
30+
}
31+
32+
auth := struct {
33+
Token string `json:"x-token"`
34+
}{}
35+
36+
err = json.Unmarshal(token, &auth)
37+
if err != nil {
38+
return "", err
39+
}
40+
if auth.Token == "" {
41+
return "", errors.New("x-token is empty. Please setup a token or cleanup xtokenPath")
42+
}
43+
return auth.Token, nil
44+
}

nodebuilder/state/core.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package state
22

33
import (
4+
"errors"
5+
"os"
6+
47
"github.com/cosmos/cosmos-sdk/crypto/keyring"
58

69
libfraud "github.com/celestiaorg/go-fraud"
@@ -30,14 +33,22 @@ func coreAccessor(
3033
*modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader],
3134
error,
3235
) {
33-
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync, corecfg.IP, corecfg.Port,
34-
network.String(), opts...)
36+
if corecfg.TLSEnabled {
37+
tlsCfg := core.EmptyTLSConfig()
38+
xtoken, err := core.XToken(corecfg.XTokenPath)
39+
if err != nil && !errors.Is(err, os.ErrNotExist) {
40+
return nil, nil, nil, err
41+
}
42+
opts = append(opts, state.WithTLSConfig(tlsCfg), state.WithXToken(xtoken))
43+
}
44+
45+
ca, err := state.NewCoreAccessor(keyring, string(keyname), sync,
46+
corecfg.IP, corecfg.Port, network.String(), opts...)
3547

3648
sBreaker := &modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]{
3749
Service: ca,
3850
FraudType: byzantine.BadEncoding,
3951
FraudServ: fraudServ,
4052
}
41-
4253
return ca, ca, sBreaker, err
4354
}

state/core_access.go

+69-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package state
22

33
import (
44
"context"
5+
"crypto/tls"
56
"errors"
67
"fmt"
78
"net"
@@ -21,7 +22,9 @@ import (
2122
"github.com/tendermint/tendermint/proto/tendermint/crypto"
2223
"google.golang.org/grpc"
2324
"google.golang.org/grpc/connectivity"
25+
"google.golang.org/grpc/credentials"
2426
"google.golang.org/grpc/credentials/insecure"
27+
"google.golang.org/grpc/metadata"
2528

2629
"github.com/celestiaorg/celestia-app/v3/app"
2730
"github.com/celestiaorg/celestia-app/v3/app/encoding"
@@ -47,6 +50,18 @@ var (
4750
// to configure parameters.
4851
type Option func(ca *CoreAccessor)
4952

53+
func WithTLSConfig(cfg *tls.Config) Option {
54+
return func(ca *CoreAccessor) {
55+
ca.tls = cfg
56+
}
57+
}
58+
59+
func WithXToken(xtoken string) Option {
60+
return func(ca *CoreAccessor) {
61+
ca.xtoken = xtoken
62+
}
63+
}
64+
5065
// CoreAccessor implements service over a gRPC connection
5166
// with a celestia-core node.
5267
type CoreAccessor struct {
@@ -73,6 +88,9 @@ type CoreAccessor struct {
7388
port string
7489
network string
7590

91+
tls *tls.Config
92+
xtoken string
93+
7694
// these fields are mutatable and thus need to be protected by a mutex
7795
lock sync.Mutex
7896
lastPayForBlob int64
@@ -91,9 +109,7 @@ func NewCoreAccessor(
91109
keyring keyring.Keyring,
92110
keyname string,
93111
getter libhead.Head[*header.ExtendedHeader],
94-
coreIP,
95-
port string,
96-
network string,
112+
coreIP, port, network string,
97113
options ...Option,
98114
) (*CoreAccessor, error) {
99115
// create verifier
@@ -123,23 +139,10 @@ func (ca *CoreAccessor) Start(ctx context.Context) error {
123139
}
124140
ca.ctx, ca.cancel = context.WithCancel(context.Background())
125141

126-
// dial given celestia-core endpoint
127-
endpoint := net.JoinHostPort(ca.coreIP, ca.port)
128-
client, err := grpc.NewClient(
129-
endpoint,
130-
grpc.WithTransportCredentials(insecure.NewCredentials()),
131-
)
142+
err := ca.startGRPCClient(ctx)
132143
if err != nil {
133-
return err
144+
return fmt.Errorf("failed to start grpc client: %w", err)
134145
}
135-
// this ensures we can't start the node without core connection
136-
client.Connect()
137-
if !client.WaitForStateChange(ctx, connectivity.Ready) {
138-
// hits the case when context is canceled
139-
return fmt.Errorf("couldn't connect to core endpoint(%s): %w", endpoint, ctx.Err())
140-
}
141-
142-
ca.coreConn = client
143146

144147
// create the staking query client
145148
ca.stakingCli = stakingtypes.NewQueryClient(ca.coreConn)
@@ -602,6 +605,40 @@ func (ca *CoreAccessor) setupTxClient(ctx context.Context, keyName string) (*use
602605
)
603606
}
604607

608+
func (ca *CoreAccessor) startGRPCClient(ctx context.Context) error {
609+
// dial given celestia-core endpoint
610+
endpoint := net.JoinHostPort(ca.coreIP, ca.port)
611+
// By default, the gRPC client is configured to handle an insecure connection.
612+
// If the TLS configuration is not empty, it will be applied to the client's options.
613+
// If the TLS configuration is empty but the X-Token is provided,
614+
// the X-Token will be applied as an interceptor along with an empty TLS configuration.
615+
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
616+
if ca.tls != nil {
617+
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(ca.tls)))
618+
}
619+
if ca.xtoken != "" {
620+
opts = append(opts, grpc.WithUnaryInterceptor(authInterceptor(ca.xtoken)))
621+
}
622+
623+
client, err := grpc.NewClient(
624+
endpoint,
625+
opts...,
626+
)
627+
if err != nil {
628+
return err
629+
}
630+
// this ensures we can't start the node without core connection
631+
client.Connect()
632+
if !client.WaitForStateChange(ctx, connectivity.Ready) {
633+
// hits the case when context is canceled
634+
return fmt.Errorf("couldn't connect to core endpoint(%s): %w", endpoint, ctx.Err())
635+
}
636+
ca.coreConn = client
637+
638+
log.Infof("Connection with core endpoint(%s) established", endpoint)
639+
return nil
640+
}
641+
605642
func (ca *CoreAccessor) submitMsg(
606643
ctx context.Context,
607644
msg sdktypes.Msg,
@@ -658,3 +695,17 @@ func convertToSdkTxResponse(resp *user.TxResponse) *TxResponse {
658695
Height: resp.Height,
659696
}
660697
}
698+
699+
func authInterceptor(xtoken string) grpc.UnaryClientInterceptor {
700+
return func(
701+
ctx context.Context,
702+
method string,
703+
req, reply interface{},
704+
cc *grpc.ClientConn,
705+
invoker grpc.UnaryInvoker,
706+
opts ...grpc.CallOption,
707+
) error {
708+
ctx = metadata.AppendToOutgoingContext(ctx, "x-token", xtoken)
709+
return invoker(ctx, method, req, reply, cc, opts...)
710+
}
711+
}

0 commit comments

Comments
 (0)