Skip to content

Commit 9945629

Browse files
committed
integrity: implement basic structs needed for it
Closes TNTP-4191
1 parent 28bbd84 commit 9945629

File tree

7 files changed

+818
-0
lines changed

7 files changed

+818
-0
lines changed

integrity/builder.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Package integrity provides integrity-protected storage operations.
2+
// It includes generators, validators, and builders for creating typed storage
3+
// with hash and signature verification.
4+
package integrity
5+
6+
import (
7+
"slices"
8+
9+
"github.com/tarantool/go-storage"
10+
"github.com/tarantool/go-storage/crypto"
11+
"github.com/tarantool/go-storage/hasher"
12+
"github.com/tarantool/go-storage/marshaller"
13+
"github.com/tarantool/go-storage/namer"
14+
)
15+
16+
// TypedBuilder builds typed storage instances with integrity protection.
17+
type TypedBuilder[T any] struct {
18+
storage storage.Storage
19+
hashers []hasher.Hasher
20+
signers []crypto.Signer
21+
verifiers []crypto.Verifier
22+
marshaller marshaller.TypedYamlMarshaller[T]
23+
24+
prefix string
25+
namer namer.Namer
26+
}
27+
28+
// NewTypedBuilder creates a new TypedBuilder for the given storage instance.
29+
func NewTypedBuilder[T any](storageInstance storage.Storage) TypedBuilder[T] {
30+
return TypedBuilder[T]{
31+
storage: storageInstance,
32+
hashers: []hasher.Hasher{},
33+
signers: []crypto.Signer{},
34+
verifiers: []crypto.Verifier{},
35+
marshaller: marshaller.NewTypedYamlMarshaller[T](),
36+
37+
prefix: "/",
38+
namer: nil, // TODO: implement default namer.
39+
}
40+
}
41+
42+
func (s TypedBuilder[T]) copy() TypedBuilder[T] {
43+
return TypedBuilder[T]{
44+
storage: s.storage,
45+
hashers: slices.Clone(s.hashers),
46+
signers: slices.Clone(s.signers),
47+
verifiers: slices.Clone(s.verifiers),
48+
marshaller: s.marshaller,
49+
50+
prefix: s.prefix,
51+
namer: s.namer,
52+
}
53+
}
54+
55+
// WithHasher adds a hasher to the builder.
56+
func (s TypedBuilder[T]) WithHasher(h hasher.Hasher) TypedBuilder[T] {
57+
out := s.copy()
58+
59+
s.hashers = append(s.hashers, h)
60+
61+
return out
62+
}
63+
64+
// WithSignerVerifier adds a signer/verifier to the builder.
65+
func (s TypedBuilder[T]) WithSignerVerifier(sv crypto.SignerVerifier) TypedBuilder[T] {
66+
out := s.copy()
67+
68+
s.signers = append(s.signers, sv)
69+
s.verifiers = append(s.verifiers, sv)
70+
71+
return out
72+
}
73+
74+
// WithSigner adds a signer to the builder.
75+
func (s TypedBuilder[T]) WithSigner(signer crypto.Signer) TypedBuilder[T] {
76+
out := s.copy()
77+
78+
s.signers = append(s.signers, signer)
79+
80+
return out
81+
}
82+
83+
// WithVerifier adds a verifier to the builder.
84+
func (s TypedBuilder[T]) WithVerifier(verifier crypto.Verifier) TypedBuilder[T] {
85+
out := s.copy()
86+
87+
s.verifiers = append(s.verifiers, verifier)
88+
89+
return out
90+
}
91+
92+
// WithMarshaller sets the marshaller for the builder.
93+
func (s TypedBuilder[T]) WithMarshaller(marshaller marshaller.TypedYamlMarshaller[T]) TypedBuilder[T] {
94+
out := s.copy()
95+
96+
s.marshaller = marshaller
97+
98+
return out
99+
}
100+
101+
// WithPrefix sets the key prefix for the builder.
102+
func (s TypedBuilder[T]) WithPrefix(prefix string) TypedBuilder[T] {
103+
out := s.copy()
104+
105+
s.prefix = prefix
106+
107+
return out
108+
}
109+
110+
// WithNamer sets the namer for the builder.
111+
func (s TypedBuilder[T]) WithNamer(namer namer.Namer) TypedBuilder[T] {
112+
out := s.copy()
113+
114+
s.namer = namer
115+
116+
return out
117+
}
118+
119+
// Build creates a new Typed storage instance with the configured options.
120+
func (s TypedBuilder[T]) Build() *Typed[T] {
121+
var defaultNamer *namer.DefaultNamer
122+
if s.namer == nil {
123+
defaultNamer = namer.NewDefaultNamer(s.prefix, []string{}, []string{})
124+
} else {
125+
var ok bool
126+
127+
defaultNamer, ok = s.namer.(*namer.DefaultNamer)
128+
if !ok {
129+
panic("namer must be *namer.DefaultNamer")
130+
}
131+
}
132+
133+
gen := NewGenerator(
134+
defaultNamer,
135+
s.marshaller,
136+
s.hashers,
137+
s.signers,
138+
)
139+
140+
val := NewValidator(
141+
defaultNamer,
142+
s.marshaller,
143+
s.hashers,
144+
s.verifiers,
145+
)
146+
147+
return &Typed[T]{
148+
base: s.storage,
149+
gen: gen,
150+
val: val,
151+
namer: defaultNamer,
152+
}
153+
}

integrity/generator.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package integrity
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/tarantool/go-storage/crypto"
8+
"github.com/tarantool/go-storage/hasher"
9+
"github.com/tarantool/go-storage/kv"
10+
"github.com/tarantool/go-storage/marshaller"
11+
"github.com/tarantool/go-storage/namer"
12+
)
13+
14+
var (
15+
// ErrHasherNotFound is returned when a required hasher is not configured.
16+
ErrHasherNotFound = errors.New("hasher not found")
17+
// ErrSignerNotFound is returned when a required signer is not configured.
18+
ErrSignerNotFound = errors.New("signer not found")
19+
// ErrUnknownKeyType is returned for unexpected key types.
20+
ErrUnknownKeyType = errors.New("unknown key type")
21+
// ErrInvalidName is returned for invalid object names.
22+
ErrInvalidName = errors.New("invalid name")
23+
// ErrNoKeyValuePairs is returned when no key-value pairs are provided.
24+
ErrNoKeyValuePairs = errors.New("no key-value pairs provided")
25+
// ErrMultipleObjects is returned when multiple objects are found instead of one.
26+
ErrMultipleObjects = errors.New("expected exactly one object")
27+
// ErrMissingExpectedKey is returned when an expected key is missing.
28+
ErrMissingExpectedKey = errors.New("missing expected key")
29+
// ErrNoValueData is returned when no value data is found.
30+
ErrNoValueData = errors.New("no value data found")
31+
// ErrHashMismatch is returned when a hash doesn't match the expected value.
32+
ErrHashMismatch = errors.New("hash mismatch")
33+
// ErrVerifierNotFound is returned when a required verifier is not configured.
34+
ErrVerifierNotFound = errors.New("verifier not found")
35+
// ErrSignatureFailed is returned when signature verification fails.
36+
ErrSignatureFailed = errors.New("signature verification failed")
37+
)
38+
39+
// Generator creates integrity-protected key-value pairs for storage.
40+
type Generator[T any] struct {
41+
namer *namer.DefaultNamer
42+
marshaller marshaller.TypedYamlMarshaller[T]
43+
hashers map[string]hasher.Hasher
44+
signers map[string]crypto.Signer
45+
}
46+
47+
// NewGenerator creates a new Generator instance.
48+
func NewGenerator[T any](
49+
namer *namer.DefaultNamer,
50+
marshaller marshaller.TypedYamlMarshaller[T],
51+
hashers []hasher.Hasher,
52+
signers []crypto.Signer,
53+
) Generator[T] {
54+
hasherMap := make(map[string]hasher.Hasher)
55+
for _, h := range hashers {
56+
hasherMap[h.Name()] = h
57+
}
58+
59+
signerMap := make(map[string]crypto.Signer)
60+
for _, s := range signers {
61+
signerMap[s.Name()] = s
62+
}
63+
64+
return Generator[T]{
65+
namer: namer,
66+
marshaller: marshaller,
67+
hashers: hasherMap,
68+
signers: signerMap,
69+
}
70+
}
71+
72+
// Generate creates integrity-protected key-value pairs for the given object.
73+
func (g Generator[T]) Generate(name string, value T) ([]kv.KeyValue, error) {
74+
keys, err := g.namer.GenerateNames(name)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to generate keys: %w", err)
77+
}
78+
79+
marshalledValue, err := g.marshaller.Marshal(value)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to marshal value: %w", err)
82+
}
83+
84+
results := make([]kv.KeyValue, 0, len(keys))
85+
86+
for _, key := range keys {
87+
var valueData []byte
88+
89+
switch key.Type() {
90+
case namer.KeyTypeValue:
91+
valueData = marshalledValue
92+
93+
case namer.KeyTypeHash:
94+
hasherInstance, exists := g.hashers[key.Property()]
95+
if !exists {
96+
return nil, fmt.Errorf("%w: %s", ErrHasherNotFound, key.Property())
97+
}
98+
99+
var err error
100+
101+
valueData, err = hasherInstance.Hash(marshalledValue)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to compute hash: %w", err)
104+
}
105+
106+
case namer.KeyTypeSignature:
107+
signer, exists := g.signers[key.Property()]
108+
if !exists {
109+
return nil, fmt.Errorf("%w: %s", ErrSignerNotFound, key.Property())
110+
}
111+
112+
var err error
113+
114+
valueData, err = signer.Sign(marshalledValue)
115+
if err != nil {
116+
return nil, fmt.Errorf("failed to generate signature: %w", err)
117+
}
118+
119+
default:
120+
return nil, fmt.Errorf("%w: %v", ErrUnknownKeyType, key.Type())
121+
}
122+
123+
results = append(results, kv.KeyValue{
124+
Key: []byte(key.Build()),
125+
Value: valueData,
126+
ModRevision: 0,
127+
})
128+
}
129+
130+
return results, nil
131+
}

integrity/integrity_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package integrity_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
"github.com/tarantool/go-storage/hasher"
9+
"github.com/tarantool/go-storage/integrity"
10+
"github.com/tarantool/go-storage/kv"
11+
"github.com/tarantool/go-storage/marshaller"
12+
"github.com/tarantool/go-storage/namer"
13+
)
14+
15+
type TestStruct struct {
16+
Name string `yaml:"name"`
17+
Value int `yaml:"value"`
18+
}
19+
20+
func TestGeneratorAndValidator(t *testing.T) {
21+
t.Parallel()
22+
23+
namer := namer.NewDefaultNamer("test", []string{"sha256"}, []string{})
24+
marshaller := marshaller.NewTypedYamlMarshaller[TestStruct]()
25+
hashers := []hasher.Hasher{hasher.NewSHA256Hasher()}
26+
27+
gen := integrity.NewGenerator[TestStruct](
28+
namer,
29+
marshaller,
30+
hashers,
31+
nil, // no signers for this test.
32+
)
33+
34+
val := integrity.NewValidator[TestStruct](
35+
namer,
36+
marshaller,
37+
hashers,
38+
nil, // no verifiers for this test.
39+
)
40+
41+
testData := TestStruct{
42+
Name: "test",
43+
Value: 42,
44+
}
45+
46+
kvs, err := gen.Generate("myobject", testData)
47+
require.NoError(t, err)
48+
assert.NotEmpty(t, kvs)
49+
50+
validated, err := val.Validate(kvs)
51+
require.NoError(t, err)
52+
assert.Equal(t, testData, validated)
53+
}
54+
55+
func TestValidatorWithMissingHash(t *testing.T) {
56+
t.Parallel()
57+
58+
namer := namer.NewDefaultNamer("test", []string{"sha256"}, []string{})
59+
marshaller := marshaller.NewTypedYamlMarshaller[TestStruct]()
60+
hashers := []hasher.Hasher{hasher.NewSHA256Hasher()}
61+
62+
gen := integrity.NewGenerator[TestStruct](
63+
namer,
64+
marshaller,
65+
hashers,
66+
nil,
67+
)
68+
69+
val := integrity.NewValidator[TestStruct](
70+
namer,
71+
marshaller,
72+
hashers,
73+
nil,
74+
)
75+
76+
testData := TestStruct{
77+
Name: "test",
78+
Value: 42,
79+
}
80+
81+
kvs, err := gen.Generate("myobject", testData)
82+
require.NoError(t, err)
83+
84+
// Remove hash key to simulate missing hash.
85+
var filteredKvs []kv.KeyValue
86+
for _, kv := range kvs {
87+
keyStr := string(kv.Key)
88+
if !contains(keyStr, "hash") {
89+
filteredKvs = append(filteredKvs, kv)
90+
}
91+
}
92+
93+
_, err = val.Validate(filteredKvs)
94+
assert.Error(t, err)
95+
// Should fail because hash key is in parsed results but not in input.
96+
}
97+
98+
func contains(s, substr string) bool {
99+
for i := 0; i <= len(s)-len(substr); i++ {
100+
if s[i:i+len(substr)] == substr {
101+
return true
102+
}
103+
}
104+
105+
return false
106+
}

0 commit comments

Comments
 (0)