frostfs: Fix invalid signatures issued by key from json

Ephemeral keys worked fine while keys loaded from filesystem would
generate invalid signatures. This was caused by destroying private key
material during calls to Wallet.Close() and Account.Close(). Since these
calls do nothing except wiping the private key, we omit them now.
Responsibility for private key security is delegated to caller of getKey()

Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
This commit is contained in:
Vitaliy Potyarkin 2024-10-16 12:05:26 +03:00
parent 254983fbe2
commit 9ff9d5be25
2 changed files with 88 additions and 16 deletions

View file

@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"time"
@ -73,6 +74,9 @@ func (s *Storage) Save(ctx context.Context, data []byte, attr ...string) (oid st
if err != nil {
return "", fmt.Errorf("signing object: %w", err)
}
if !obj.VerifyIDSignature() {
return "", errors.New("signing object: invalid signature was generated")
}
ctx, cancel := context.WithTimeout(ctx, storageRequestTimeout)
defer cancel()
@ -163,19 +167,21 @@ func keyval2attrs(attr ...string) ([]object.Attribute, error) {
}
// Load private key from wallet file.
func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) {
func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) { //nolint:gocyclo
if walletPath == "" {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("generating ephemeral key: %w", err)
}
return key, nil
}
// This function intentionally omits calls to w.Close() and account.Close()
// because that would destroy the underlying ecdsa.PrivateKey.
w, err := wallet.NewWalletFromFile(walletPath)
if err != nil {
return nil, err
}
defer w.Close()
if len(w.Accounts) == 0 {
return nil, fmt.Errorf("no accounts in wallet: %s", walletPath)
}
@ -185,6 +191,12 @@ func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey
if err != nil {
return nil, fmt.Errorf("invalid account address: %w", err)
}
if len(decode) != 21 {
return nil, fmt.Errorf("invalid account address length: %d bytes", len(decode))
}
if decode[0] != 0x35 {
return nil, fmt.Errorf("invalid account address first byte: %s -> %#x", walletAccount, decode[0])
}
hash, err := util.Uint160DecodeBytesBE(decode[1:])
if err != nil {
return nil, fmt.Errorf("invalid account hash: %w", err)
@ -194,10 +206,11 @@ func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey
return nil, fmt.Errorf("account not found: %s", walletAccount)
}
}
defer account.Close()
err = account.Decrypt(walletPassword, w.Scrypt)
if err != nil {
return nil, fmt.Errorf("failed to decrypt wallet: %w", err)
if account.PrivateKey() == nil {
err = account.Decrypt(walletPassword, w.Scrypt)
if err != nil {
return nil, fmt.Errorf("failed to decrypt wallet: %w", err)
}
}
key := account.PrivateKey().PrivateKey
return &key, nil

View file

@ -3,27 +3,32 @@ package frostfs
// Tests for our FrostFS client code
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"crypto/sha256"
"os"
"sync"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
// Initialize storage backend for tests.
func openStorage(t *testing.T) *Storage {
t.Helper()
cid := os.Getenv("FROSTFS_CID") // sample: "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM"
cid := os.Getenv("FROSTFS_CONTAINER") // sample: "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM"
endpoint := os.Getenv("FROSTFS_ENDPOINT") // sample: "grpc://localhost:8802"
if cid == "" || endpoint == "" {
t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CID")
t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CONTAINER")
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars
key, err := getKey(
os.Getenv("FROSTFS_WALLET"),
os.Getenv("FROSTFS_WALLET_ACCOUNT"),
os.Getenv("FROSTFS_WALLET_PASSWORD"),
)
if err != nil {
t.Fatal(err)
}
@ -76,7 +81,8 @@ func TestMulipleObjects(t *testing.T) {
continue
}
if obj != baseline {
panic(fmt.Errorf("non-identical object id: %s != %s", baseline, obj))
t.Errorf("non-identical object id: %s != %s", baseline, obj)
return
}
}
}
@ -90,11 +96,18 @@ func TestMulipleObjects(t *testing.T) {
defer wg.Done()
oid, err := storage.Save(ctx, payload, "FileName", "CAS.txt", "Tag", "test")
if err != nil {
panic(err)
t.Error(err)
return
}
err = storage.Delete(ctx, oid)
if err != nil {
panic(err)
t.Error(err)
return
}
select {
case objects <- oid:
case <-ctx.Done():
return
}
t.Log(oid)
}()
@ -129,3 +142,49 @@ func key2addr(k *ecdsa.PrivateKey) string {
user.IDFromKey(&owner, k.PublicKey)
return owner.String()
}
// Check that loaded wallet key generates valid signature.
func TestLoadWalletAndSign(t *testing.T) {
const (
walletPath = "client_test_wallet.json"
walletAccount = "NWZnjbTKbzwtX6w1q5R3kbEKrnJ5bp1kn7"
walletAccountPassword = ""
walletAccountIndex = 0
dataString = "some data to sign in this test"
)
w, err := wallet.NewWalletFromFile(walletPath)
if err != nil {
t.Fatalf("wallet from file: %v", err)
}
accountFromWallet := w.Accounts[walletAccountIndex]
err = accountFromWallet.Decrypt(walletAccountPassword, w.Scrypt)
if err != nil {
t.Fatalf("wallet decrypt: %v", err)
}
key, err := getKey(walletPath, walletAccount, walletAccountPassword)
if err != nil {
t.Fatal(err)
}
wrappedKey := &keys.PrivateKey{PrivateKey: *key}
accountFromKey := wallet.NewAccountFromPrivateKey(wrappedKey)
if !accountFromKey.PublicKey().Equal(accountFromWallet.PublicKey()) {
t.Fatalf("corrupted key: want %s, got %s", accountFromWallet.PublicKey().Address(), accountFromKey.PublicKey().Address())
}
data := []byte(dataString)
sig := accountFromKey.PrivateKey().Sign(data)
hash := sha256.Sum256(data)
hashSig := accountFromKey.PrivateKey().SignHash(hash)
if !bytes.Equal(sig, hashSig) {
t.Fatalf("different signatures for data (%x) and for hash (%x)", sig, hashSig)
}
ok := accountFromKey.PublicKey().Verify(sig, hash[:])
if !ok {
t.Errorf("signature %x (%d bytes): check failed: signing key", sig, len(sig))
}
ok = accountFromWallet.PublicKey().Verify(sig, hash[:])
if !ok {
t.Errorf("signature %x (%d bytes): check failed: wallet key", sig, len(sig))
}
}