Skip to content

Commit 8676f41

Browse files
AlgebraicWolfLeonidVas
authored andcommitted
integrity: scaffolding for integrity check
This patch adds necessary scaffolding for performing integrity checks, along with an appropriate instrumentation of operations with files that must be scrutinized before working with them.
1 parent fab1554 commit 8676f41

16 files changed

+384
-33
lines changed

cli/cmd/root.go

+27-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path/filepath"
88

9+
"github.com/tarantool/tt/cli/integrity"
910
"github.com/tarantool/tt/cli/util"
1011

1112
"github.com/apex/log"
@@ -132,6 +133,8 @@ func NewCmdRoot() *cobra.Command {
132133
rootCmd.Flags().BoolVarP(&cmdCtx.Cli.Verbose, "verbose", "V",
133134
false, "Verbose output")
134135

136+
integrity.RegisterIntegrityCheckFlag(rootCmd.Flags(), &cmdCtx.Cli.IntegrityCheck)
137+
135138
rootCmd.Flags().SetInterspersed(false)
136139

137140
rootCmd.AddCommand(
@@ -199,13 +202,32 @@ func InitRoot() {
199202
if err := configure.ValidateCliOpts(&cmdCtx.Cli); err != nil {
200203
log.Fatal(err.Error())
201204
}
205+
var err error
202206

207+
if cmdCtx.Cli.IntegrityCheck != "" {
208+
currentDir, err := os.Getwd()
209+
if err != nil {
210+
log.Fatalf("can't get current dir: %s", err.Error())
211+
}
212+
213+
configPath, _ := util.GetYamlFileName(
214+
filepath.Join(currentDir, configure.ConfigName),
215+
false,
216+
)
217+
218+
err = integrity.InitializeIntegrityCheck(
219+
cmdCtx.Cli.IntegrityCheck,
220+
filepath.Dir(configPath),
221+
)
222+
if err != nil {
223+
log.Fatalf("integrity check failed: %s", err)
224+
}
225+
}
203226
// Configure Tarantool CLI.
204227
if err := configure.Cli(&cmdCtx); err != nil {
205228
log.Fatalf("Failed to configure Tarantool CLI: %s", err)
206229
}
207230

208-
var err error
209231
cliOpts, cmdCtx.Cli.ConfigPath, err = configure.GetCliOpts(cmdCtx.Cli.ConfigPath)
210232
if err != nil {
211233
log.Fatalf("Failed to get Tarantool CLI configuration: %s", err)
@@ -232,5 +254,8 @@ func InitRoot() {
232254
}
233255

234256
// Configure help command.
235-
configureHelpCommand(&cmdCtx, rootCmd)
257+
err = configureHelpCommand(&cmdCtx, rootCmd)
258+
if err != nil {
259+
log.Fatalf("Failed to set up help command: %s", err)
260+
}
236261
}

cli/cmd/start.go

+33-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import (
44
"fmt"
55
"os"
66
"os/exec"
7+
"strconv"
78
"syscall"
9+
"time"
810

911
"github.com/apex/log"
1012
"github.com/spf13/cobra"
1113
"github.com/tarantool/tt/cli/cmd/internal"
1214
"github.com/tarantool/tt/cli/cmdcontext"
15+
"github.com/tarantool/tt/cli/integrity"
1316
"github.com/tarantool/tt/cli/modules"
1417
"github.com/tarantool/tt/cli/process_utils"
1518
"github.com/tarantool/tt/cli/running"
@@ -20,6 +23,9 @@ var (
2023
// In go, we can't just fork the process (reason - goroutines).
2124
// So, for daemonize, we restarts the process with "watchdog" flag.
2225
watchdog bool
26+
// integrityCheckPeriod is a flag enables periodic integrity checks.
27+
// The default period is 1 day.
28+
integrityCheckPeriod = 24 * 60 * 60
2329
)
2430

2531
// NewStartCmd creates start command.
@@ -47,6 +53,8 @@ func NewStartCmd() *cobra.Command {
4753
startCmd.Flags().BoolVar(&watchdog, "watchdog", false, "")
4854
startCmd.Flags().MarkHidden("watchdog")
4955

56+
integrity.RegisterIntegrityCheckPeriodFlag(startCmd.Flags(), &integrityCheckPeriod)
57+
5058
return startCmd
5159
}
5260

@@ -61,9 +69,25 @@ func startWatchdog(ttExecutable string, instance running.InstanceCtx) error {
6169
return nil
6270
}
6371

64-
log.Infof("Starting an instance [%s]...", appName)
72+
newArgs := []string{}
73+
if cmdCtx.Cli.IntegrityCheck != "" {
74+
newArgs = append(newArgs, "--integrity-check", cmdCtx.Cli.IntegrityCheck)
75+
}
76+
77+
newArgs = append(newArgs, "start", "--watchdog", appName)
78+
79+
if cmdCtx.Cli.IntegrityCheck != "" {
80+
newArgs = append(newArgs, "--integrity-check-period",
81+
strconv.Itoa(integrityCheckPeriod))
82+
}
83+
84+
f, err := integrity.FileRepository.Read(ttExecutable)
85+
if err != nil {
86+
return err
87+
}
88+
f.Close()
6589

66-
newArgs := []string{"start", "--watchdog", appName}
90+
log.Infof("Starting an instance [%s]...", appName)
6791

6892
wdCmd := exec.Command(ttExecutable, newArgs...)
6993
// Set new pgid for watchdog process, so it will not be killed after a session is closed.
@@ -113,7 +137,13 @@ func internalStartModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
113137
return nil
114138
}
115139

116-
if err := running.Start(cmdCtx, &runningCtx.Instances[0]); err != nil {
140+
checkPeriod := time.Duration(0)
141+
142+
if cmdCtx.Cli.IntegrityCheck != "" && integrityCheckPeriod > 0 {
143+
checkPeriod = time.Duration(integrityCheckPeriod * int(time.Second))
144+
}
145+
146+
if err := running.Start(cmdCtx, &runningCtx.Instances[0], checkPeriod); err != nil {
117147
return err
118148
}
119149
return nil

cli/cmdcontext/cmdcontext.go

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os/exec"
66
"strings"
77

8+
"github.com/tarantool/tt/cli/integrity"
89
"github.com/tarantool/tt/cli/version"
910
)
1011

@@ -16,6 +17,9 @@ type CmdCtx struct {
1617
Cli CliCtx
1718
// CommandName contains name of the command.
1819
CommandName string
20+
// FileRepository is used for reading files that require
21+
// integrity control.
22+
FileRepository integrity.Repository
1923
}
2024

2125
// TarantoolCli describes tarantool executable.
@@ -80,4 +84,6 @@ type CliCtx struct {
8084
Verbose bool
8185
// TarantoolCli is current tarantool cli.
8286
TarantoolCli TarantoolCli
87+
// IntegrityCheck is a public key used for integrity check.
88+
IntegrityCheck string
8389
}

cli/configure/configure.go

+14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/spf13/cobra"
1414
"github.com/tarantool/tt/cli/cmdcontext"
1515
"github.com/tarantool/tt/cli/config"
16+
"github.com/tarantool/tt/cli/integrity"
1617
"github.com/tarantool/tt/cli/modules"
1718
"github.com/tarantool/tt/cli/util"
1819
)
@@ -195,8 +196,14 @@ func GetCliOpts(configurePath string) (*config.CliOpts, string, error) {
195196
var cfg *config.CliOpts = GetDefaultCliOpts()
196197
// Config could not be processed.
197198
configPath, err := util.GetYamlFileName(configurePath, true)
199+
// Before loading configure file, we'll initialize integrity checking.
198200
if err == nil {
199201
// Config file is found, load it.
202+
f, err := integrity.FileRepository.Read(configPath)
203+
if err != nil {
204+
return nil, "", fmt.Errorf("failed to validate integrity of %q: %w", configPath, err)
205+
}
206+
f.Close()
200207
rawConfigOpts, err := util.ParseYAML(configPath)
201208
if err != nil {
202209
return nil, "", fmt.Errorf("failed to parse Tarantool CLI configuration: %s", err)
@@ -524,6 +531,13 @@ func configureLocalCli(cmdCtx *cmdcontext.CmdCtx) error {
524531
`found tt binary in local directory "%s" isn't executable: %s`, launchDir, err)
525532
}
526533

534+
// Before switching to local cli, we shall check its integrity.
535+
f, err := integrity.FileRepository.Read(localCli)
536+
if err != nil {
537+
return err
538+
}
539+
f.Close()
540+
527541
// We are not using the "RunExec" function because we have no reason to have several
528542
// "tt" processes. Moreover, it looks strange when we start "tt", which starts "tt",
529543
// which starts tarantool or some external module.

cli/integrity/dummy_repository.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package integrity
2+
3+
import (
4+
"io"
5+
"os"
6+
)
7+
8+
// dummyRepository implements repository with no checks performed.
9+
type dummyRepository struct{}
10+
11+
// AddHash is a stub of method used to add hashing algorithm in the
12+
// proper implementation.
13+
func (dummyRepository) AddHash(hasher any) {}
14+
15+
// Add is a stub of method used to add file with hashes to a repository.
16+
func (dummyRepository) Add(path string) error { return nil }
17+
18+
// Read opens the supplied file.
19+
func (dummyRepository) Read(path string) (io.ReadCloser, error) {
20+
return os.Open(path)
21+
}
22+
23+
// ValidateAll doesn't do anything since dummyRepository does not implement
24+
// validation.
25+
func (dummyRepository) ValidateAll() error { return nil }
26+
27+
var _ Repository = dummyRepository{}

cli/integrity/integrity.go

+17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import (
66
"github.com/spf13/pflag"
77
)
88

9+
var FileRepository Repository = dummyRepository{}
10+
11+
var HashesName = ""
12+
913
// Signer implements high-level API for package signing.
1014
type Signer interface {
1115
// Sign generates data to sign a package.
@@ -20,3 +24,16 @@ func NewSigner(path string) (Signer, error) {
2024
// RegisterWithIntegrityFlag is a noop function that is intended to add
2125
// flags to `tt pack` command.
2226
func RegisterWithIntegrityFlag(flagset *pflag.FlagSet, dst *string) {}
27+
28+
// RegisterIntegrityCheckFlag is a noop function that is intended to add
29+
// root flag enabling integrity checks.
30+
func RegisterIntegrityCheckFlag(flagset *pflag.FlagSet, dst *string) {}
31+
32+
// RegisterIntegrityCheckPeriodFlag is a noop function that is intended to
33+
// add flag specifying how often should integrity checks run in watchdog.
34+
func RegisterIntegrityCheckPeriodFlag(flagset *pflag.FlagSet, dst *int) {}
35+
36+
// InitializeIntegrityCheck is a noop setup of integrity checking.
37+
func InitializeIntegrityCheck(publicKeyPath string, configDir string) error {
38+
return errors.New("integrity checks should never be initialized in ce")
39+
}

cli/integrity/integrity_test.go

+120
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,42 @@ func TestNewSigner(t *testing.T) {
3232
}
3333
}
3434

35+
func InitializeIntegrityCheck(t *testing.T) {
36+
testCases := []struct {
37+
name string
38+
publicKeyPath string
39+
configDir string
40+
}{
41+
{
42+
name: "Empty key and config path",
43+
publicKeyPath: "",
44+
configDir: "",
45+
},
46+
{
47+
name: "Arbitrary key path, empty config path",
48+
publicKeyPath: "public.pem",
49+
configDir: "",
50+
},
51+
{
52+
name: "Empty key path, arbitrary config path",
53+
publicKeyPath: "",
54+
configDir: "app",
55+
},
56+
{
57+
name: "Arbitrary key and config path",
58+
publicKeyPath: "public.pem",
59+
configDir: "app",
60+
},
61+
}
62+
63+
for _, testCase := range testCases {
64+
t.Run(testCase.name, func(t *testing.T) {
65+
err := integrity.InitializeIntegrityCheck(testCase.publicKeyPath, testCase.configDir)
66+
require.EqualError(t, err, "integrity checks should never be initialized in ce", "an error should be produced")
67+
})
68+
}
69+
}
70+
3571
func TestRegisterWithIntegritySigner(t *testing.T) {
3672
someStr := ""
3773

@@ -73,3 +109,87 @@ func TestRegisterWithIntegritySigner(t *testing.T) {
73109
})
74110
}
75111
}
112+
113+
func TestRegisterIntegrityCheckFlag(t *testing.T) {
114+
someStr := ""
115+
116+
testCases := []struct {
117+
name string
118+
flagSet *pflag.FlagSet
119+
dst *string
120+
}{
121+
{
122+
name: "Empty flagSet and dst",
123+
flagSet: nil,
124+
dst: nil,
125+
},
126+
{
127+
name: "Empty dst",
128+
flagSet: &pflag.FlagSet{},
129+
dst: nil,
130+
},
131+
{
132+
name: "Empty flagSet",
133+
flagSet: nil,
134+
dst: &someStr,
135+
},
136+
{
137+
name: "Nothing empty",
138+
flagSet: &pflag.FlagSet{},
139+
dst: nil,
140+
},
141+
}
142+
143+
for _, testCase := range testCases {
144+
t.Run(testCase.name, func(t *testing.T) {
145+
integrity.RegisterIntegrityCheckFlag(testCase.flagSet, testCase.dst)
146+
147+
if testCase.flagSet != nil {
148+
require.False(t, testCase.flagSet.HasFlags(),
149+
"command must not be modified")
150+
}
151+
})
152+
}
153+
}
154+
155+
func TestRegisterIntegrityCheckPeriodFlag(t *testing.T) {
156+
someInt := 0
157+
158+
testCases := []struct {
159+
name string
160+
flagSet *pflag.FlagSet
161+
dst *int
162+
}{
163+
{
164+
name: "Empty flagSet and dst",
165+
flagSet: nil,
166+
dst: nil,
167+
},
168+
{
169+
name: "Empty dst",
170+
flagSet: &pflag.FlagSet{},
171+
dst: nil,
172+
},
173+
{
174+
name: "Empty flagSet",
175+
flagSet: nil,
176+
dst: &someInt,
177+
},
178+
{
179+
name: "Nothing empty",
180+
flagSet: &pflag.FlagSet{},
181+
dst: nil,
182+
},
183+
}
184+
185+
for _, testCase := range testCases {
186+
t.Run(testCase.name, func(t *testing.T) {
187+
integrity.RegisterIntegrityCheckPeriodFlag(testCase.flagSet, testCase.dst)
188+
189+
if testCase.flagSet != nil {
190+
require.False(t, testCase.flagSet.HasFlags(),
191+
"command must not be modified")
192+
}
193+
})
194+
}
195+
}

0 commit comments

Comments
 (0)