Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ coveralls-deps:
.PHONY: lint-deps
lint-deps:
@echo "Installing lint deps"
@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2

.PHONY: lint
lint: lint-deps
Expand Down
2 changes: 1 addition & 1 deletion crypto/interfaces.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package crypto implements crypto interfaces.
// Package crypto implements verification interfaces.
package crypto

// Signer implements high-level API for package signing.
Expand Down
8 changes: 4 additions & 4 deletions crypto/rsa.go → crypto/rsapss.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package crypto
package crypto //nolint:revive

import (
"crypto"
Expand Down Expand Up @@ -29,12 +29,12 @@ func NewRSAPSS(privKey rsa.PrivateKey, pubKey rsa.PublicKey) RSAPSS {
}

// Name implements SignerVerifier interface.
func (r *RSAPSS) Name() string {
func (r RSAPSS) Name() string {
return "RSASSA-PSS"
}

// Sign generates SHA-256 digest and signs it using RSASSA-PSS.
func (r *RSAPSS) Sign(data []byte) ([]byte, error) {
func (r RSAPSS) Sign(data []byte) ([]byte, error) {
digest, err := r.hasher.Hash(data)
if err != nil {
return []byte{}, fmt.Errorf("failed to get hash: %w", err)
Expand All @@ -52,7 +52,7 @@ func (r *RSAPSS) Sign(data []byte) ([]byte, error) {
}

// Verify compares data with signature.
func (r *RSAPSS) Verify(data []byte, signature []byte) error {
func (r RSAPSS) Verify(data []byte, signature []byte) error {
digest, err := r.hasher.Hash(data)
if err != nil {
return fmt.Errorf("failed to get hash: %w", err)
Expand Down
1 change: 1 addition & 0 deletions crypto/rsa_test.go → crypto/rsapss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/stretchr/testify/require"

"github.com/tarantool/go-storage/crypto"
)

Expand Down
8 changes: 4 additions & 4 deletions hasher/hasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ func TestSHA256Hasher(t *testing.T) {
t.Parallel()

tests := []struct {
name string
in []byte
out string
name string
in []byte
expected string
}{
{"empty", []byte(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{"abc", []byte("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
Expand All @@ -77,7 +77,7 @@ func TestSHA256Hasher(t *testing.T) {

result, _ := h.Hash(test.in)

assert.Equal(t, test.out, hex.EncodeToString(result))
assert.Equal(t, test.expected, hex.EncodeToString(result))
})
}
}
Expand Down
153 changes: 153 additions & 0 deletions integrity/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Package integrity provides integrity-protected storage operations.
// It includes generators, validators, and builders for creating typed storage
// with hash and signature verification.
package integrity

import (
"slices"

"github.com/tarantool/go-storage"
"github.com/tarantool/go-storage/crypto"
"github.com/tarantool/go-storage/hasher"
"github.com/tarantool/go-storage/marshaller"
"github.com/tarantool/go-storage/namer"
)

// TypedBuilder builds typed storage instances with integrity protection.
type TypedBuilder[T any] struct {
storage storage.Storage
hashers []hasher.Hasher
signers []crypto.Signer
verifiers []crypto.Verifier
marshaller marshaller.TypedYamlMarshaller[T]

prefix string
namer namer.Namer
}

// NewTypedBuilder creates a new TypedBuilder for the given storage instance.
func NewTypedBuilder[T any](storageInstance storage.Storage) TypedBuilder[T] {
return TypedBuilder[T]{
storage: storageInstance,
hashers: []hasher.Hasher{},
signers: []crypto.Signer{},
verifiers: []crypto.Verifier{},
marshaller: marshaller.NewTypedYamlMarshaller[T](),

prefix: "/",
namer: nil, // TODO: implement default namer.
}
}

func (s TypedBuilder[T]) copy() TypedBuilder[T] {
return TypedBuilder[T]{
storage: s.storage,
hashers: slices.Clone(s.hashers),
signers: slices.Clone(s.signers),
verifiers: slices.Clone(s.verifiers),
marshaller: s.marshaller,

prefix: s.prefix,
namer: s.namer,
}
}

// WithHasher adds a hasher to the builder.
func (s TypedBuilder[T]) WithHasher(h hasher.Hasher) TypedBuilder[T] {
out := s.copy()

s.hashers = append(s.hashers, h)

return out
}

// WithSignerVerifier adds a signer/verifier to the builder.
func (s TypedBuilder[T]) WithSignerVerifier(sv crypto.SignerVerifier) TypedBuilder[T] {
out := s.copy()

s.signers = append(s.signers, sv)
s.verifiers = append(s.verifiers, sv)

return out
}

// WithSigner adds a signer to the builder.
func (s TypedBuilder[T]) WithSigner(signer crypto.Signer) TypedBuilder[T] {
out := s.copy()

s.signers = append(s.signers, signer)

return out
}

// WithVerifier adds a verifier to the builder.
func (s TypedBuilder[T]) WithVerifier(verifier crypto.Verifier) TypedBuilder[T] {
out := s.copy()

s.verifiers = append(s.verifiers, verifier)

return out
}

// WithMarshaller sets the marshaller for the builder.
func (s TypedBuilder[T]) WithMarshaller(marshaller marshaller.TypedYamlMarshaller[T]) TypedBuilder[T] {
out := s.copy()

s.marshaller = marshaller

return out
}

// WithPrefix sets the key prefix for the builder.
func (s TypedBuilder[T]) WithPrefix(prefix string) TypedBuilder[T] {
out := s.copy()

s.prefix = prefix

return out
}

// WithNamer sets the namer for the builder.
func (s TypedBuilder[T]) WithNamer(namer namer.Namer) TypedBuilder[T] {
out := s.copy()

s.namer = namer

return out
}

// Build creates a new Typed storage instance with the configured options.
func (s TypedBuilder[T]) Build() *Typed[T] {
var defaultNamer *namer.DefaultNamer
if s.namer == nil {
defaultNamer = namer.NewDefaultNamer(s.prefix, []string{}, []string{})
} else {
var ok bool

defaultNamer, ok = s.namer.(*namer.DefaultNamer)
if !ok {
panic("namer must be *namer.DefaultNamer")
}
}

gen := NewGenerator(
defaultNamer,
s.marshaller,
s.hashers,
s.signers,
)

val := NewValidator(
defaultNamer,
s.marshaller,
s.hashers,
s.verifiers,
)

return &Typed[T]{
base: s.storage,
gen: gen,
val: val,
namer: defaultNamer,
}
}
131 changes: 131 additions & 0 deletions integrity/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package integrity

import (
"errors"
"fmt"

"github.com/tarantool/go-storage/crypto"
"github.com/tarantool/go-storage/hasher"
"github.com/tarantool/go-storage/kv"
"github.com/tarantool/go-storage/marshaller"
"github.com/tarantool/go-storage/namer"
)

var (
// ErrHasherNotFound is returned when a required hasher is not configured.
ErrHasherNotFound = errors.New("hasher not found")
// ErrSignerNotFound is returned when a required signer is not configured.
ErrSignerNotFound = errors.New("signer not found")
// ErrUnknownKeyType is returned for unexpected key types.
ErrUnknownKeyType = errors.New("unknown key type")
// ErrInvalidName is returned for invalid object names.
ErrInvalidName = errors.New("invalid name")
// ErrNoKeyValuePairs is returned when no key-value pairs are provided.
ErrNoKeyValuePairs = errors.New("no key-value pairs provided")
// ErrMultipleObjects is returned when multiple objects are found instead of one.
ErrMultipleObjects = errors.New("expected exactly one object")
// ErrMissingExpectedKey is returned when an expected key is missing.
ErrMissingExpectedKey = errors.New("missing expected key")
// ErrNoValueData is returned when no value data is found.
ErrNoValueData = errors.New("no value data found")
// ErrHashMismatch is returned when a hash doesn't match the expected value.
ErrHashMismatch = errors.New("hash mismatch")
// ErrVerifierNotFound is returned when a required verifier is not configured.
ErrVerifierNotFound = errors.New("verifier not found")
// ErrSignatureFailed is returned when signature verification fails.
ErrSignatureFailed = errors.New("signature verification failed")
)

// Generator creates integrity-protected key-value pairs for storage.
type Generator[T any] struct {
namer *namer.DefaultNamer
marshaller marshaller.TypedYamlMarshaller[T]
hashers map[string]hasher.Hasher
signers map[string]crypto.Signer
}

// NewGenerator creates a new Generator instance.
func NewGenerator[T any](
namer *namer.DefaultNamer,
marshaller marshaller.TypedYamlMarshaller[T],
hashers []hasher.Hasher,
signers []crypto.Signer,
) Generator[T] {
hasherMap := make(map[string]hasher.Hasher)
for _, h := range hashers {
hasherMap[h.Name()] = h
}

signerMap := make(map[string]crypto.Signer)
for _, s := range signers {
signerMap[s.Name()] = s
}

return Generator[T]{
namer: namer,
marshaller: marshaller,
hashers: hasherMap,
signers: signerMap,
}
}

// Generate creates integrity-protected key-value pairs for the given object.
func (g Generator[T]) Generate(name string, value T) ([]kv.KeyValue, error) {
keys, err := g.namer.GenerateNames(name)
if err != nil {
return nil, fmt.Errorf("failed to generate keys: %w", err)
}

marshalledValue, err := g.marshaller.Marshal(value)
if err != nil {
return nil, fmt.Errorf("failed to marshal value: %w", err)
}

results := make([]kv.KeyValue, 0, len(keys))

for _, key := range keys {
var valueData []byte

switch key.Type() {
case namer.KeyTypeValue:
valueData = marshalledValue

case namer.KeyTypeHash:
hasherInstance, exists := g.hashers[key.Property()]
if !exists {
return nil, fmt.Errorf("%w: %s", ErrHasherNotFound, key.Property())
}

var err error

valueData, err = hasherInstance.Hash(marshalledValue)
if err != nil {
return nil, fmt.Errorf("failed to compute hash: %w", err)
}

case namer.KeyTypeSignature:
signer, exists := g.signers[key.Property()]
if !exists {
return nil, fmt.Errorf("%w: %s", ErrSignerNotFound, key.Property())
}

var err error

valueData, err = signer.Sign(marshalledValue)
if err != nil {
return nil, fmt.Errorf("failed to generate signature: %w", err)
}

default:
return nil, fmt.Errorf("%w: %v", ErrUnknownKeyType, key.Type())
}

results = append(results, kv.KeyValue{
Key: []byte(key.Build()),
Value: valueData,
ModRevision: 0,
})
}

return results, nil
}
Loading
Loading