Skip to content

Commit f797bfa

Browse files
committed
node/object: Refactor verification of objects' signatures
Prolongs approach from 8d3054d to objects. The code is shared as much as possible. The panic described in nspcc-dev/neofs-sdk-go#673 is prevented in the reused code. Refs #3194. Refs #2795. Signed-off-by: Leonard Lyubich <[email protected]>
1 parent bcce6ed commit f797bfa

File tree

9 files changed

+236
-57
lines changed

9 files changed

+236
-57
lines changed

Diff for: internal/crypto/common.go

+20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
11
package crypto
22

3+
import (
4+
"crypto/ecdsa"
5+
"fmt"
6+
7+
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
8+
)
9+
310
type signedDataFunc = func() []byte
11+
12+
func verifySignature(sig neofscrypto.Signature, signedData signedDataFunc) (*ecdsa.PublicKey, error) {
13+
switch scheme := sig.Scheme(); scheme {
14+
default:
15+
return nil, fmt.Errorf("unsupported scheme %v", scheme)
16+
case neofscrypto.ECDSA_SHA512, neofscrypto.ECDSA_DETERMINISTIC_SHA256, neofscrypto.ECDSA_WALLETCONNECT:
17+
pub, err := verifyECDSAFns[scheme](sig.PublicKeyBytes(), sig.Value(), signedData)
18+
if err != nil {
19+
return nil, schemeError(scheme, err)
20+
}
21+
return pub, nil
22+
}
23+
}

Diff for: internal/crypto/common_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package crypto_test
2+
3+
import (
4+
"github.com/nspcc-dev/neofs-sdk-go/user"
5+
)
6+
7+
var (
8+
// ECDSA private key used in tests.
9+
// privECDSA = ecdsa.PrivateKey{
10+
// PublicKey: ecdsa.PublicKey{
11+
// Curve: elliptic.P256(),
12+
// X: new(big.Int).SetBytes([]byte{206, 67, 193, 231, 254, 180, 127, 78, 101, 154, 23, 161, 134, 77, 122, 34, 234, 85,
13+
// 149, 44, 32, 223, 244, 140, 28, 194, 76, 214, 239, 121, 174, 40}),
14+
// Y: new(big.Int).SetBytes([]byte{170, 190, 155, 176, 31, 11, 4, 14, 103, 210, 53, 0, 73, 46, 81, 129, 163, 217, 81, 51, 111,
15+
// 135, 223, 253, 48, 104, 240, 197, 122, 37, 197, 78}),
16+
// },
17+
// D: new(big.Int).SetBytes([]byte{185, 97, 226, 151, 175, 3, 234, 11, 168, 211, 53, 141, 136, 102, 100, 222, 73, 174, 234, 157,
18+
// 139, 192, 66, 145, 13, 173, 12, 120, 22, 134, 52, 180}),
19+
// }
20+
// corresponds to the private key.
21+
pubECDSA = []byte{2, 206, 67, 193, 231, 254, 180, 127, 78, 101, 154, 23, 161, 134, 77, 122, 34, 234, 85, 149, 44, 32, 223,
22+
244, 140, 28, 194, 76, 214, 239, 121, 174, 40}
23+
// corresponds to pubECDSA.
24+
issuer = user.ID{53, 57, 243, 96, 136, 255, 217, 227, 204, 13, 243, 228, 109, 31, 226, 226, 236, 62, 13, 190, 156, 135, 252, 236, 8}
25+
)

Diff for: internal/crypto/errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
var errIssuerMismatch = errors.New("issuer mismatches signature")
1111

12+
var errMissingSignature = errors.New("missing signature")
13+
1214
func schemeError(s neofscrypto.Scheme, cause error) error {
1315
return fmt.Errorf("scheme %v: %w", s, cause)
1416
}

Diff for: internal/crypto/object.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package crypto
2+
3+
import (
4+
"github.com/nspcc-dev/neofs-sdk-go/object"
5+
)
6+
7+
// AuthenticateObject checks whether obj is signed correctly by its owner.
8+
func AuthenticateObject(obj object.Object) error {
9+
sig := obj.Signature()
10+
if sig == nil {
11+
return errMissingSignature
12+
}
13+
_, err := verifySignature(*sig, obj.SignedData)
14+
return err
15+
}

Diff for: internal/crypto/object_test.go

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package crypto_test
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"slices"
7+
"testing"
8+
9+
icrypto "github.com/nspcc-dev/neofs-node/internal/crypto"
10+
"github.com/nspcc-dev/neofs-sdk-go/checksum"
11+
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
12+
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
13+
"github.com/nspcc-dev/neofs-sdk-go/object"
14+
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
15+
"github.com/nspcc-dev/neofs-sdk-go/version"
16+
"github.com/nspcc-dev/tzhash/tz"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestAuthenticateObject(t *testing.T) {
21+
t.Run("without signature", func(t *testing.T) {
22+
obj := getUnsignedObject()
23+
require.EqualError(t, icrypto.AuthenticateObject(obj), "missing signature")
24+
})
25+
t.Run("unsupported scheme", func(t *testing.T) {
26+
obj := objectECDSASHA512
27+
sig := *obj.Signature()
28+
sig.SetScheme(3)
29+
obj.SetSignature(&sig)
30+
require.EqualError(t, icrypto.AuthenticateObject(obj), "unsupported scheme 3")
31+
})
32+
t.Run("invalid public key", func(t *testing.T) {
33+
for _, tc := range []struct {
34+
name, err string
35+
changePub func([]byte) []byte
36+
}{
37+
{name: "nil", err: "EOF", changePub: func([]byte) []byte { return nil }},
38+
{name: "empty", err: "EOF", changePub: func([]byte) []byte { return []byte{} }},
39+
{name: "undersize", err: "unexpected EOF", changePub: func(k []byte) []byte { return k[:len(k)-1] }},
40+
{name: "oversize", err: "extra data", changePub: func(k []byte) []byte { return append(k, 1) }},
41+
{name: "prefix 0", err: "invalid prefix 0", changePub: func(k []byte) []byte { return []byte{0x00} }},
42+
{name: "prefix 1", err: "invalid prefix 1", changePub: func(k []byte) []byte { return []byte{0x01} }},
43+
{name: "prefix 4", err: "EOF", changePub: func(k []byte) []byte { return []byte{0x04} }},
44+
{name: "prefix 5", err: "invalid prefix 5", changePub: func(k []byte) []byte { return []byte{0x05} }},
45+
} {
46+
t.Run(tc.name, func(t *testing.T) {
47+
obj := objectECDSASHA512
48+
sig := *obj.Signature()
49+
sig.SetPublicKeyBytes(tc.changePub(sig.PublicKeyBytes()))
50+
obj.SetSignature(&sig)
51+
err := icrypto.AuthenticateObject(obj)
52+
require.EqualError(t, err, "scheme ECDSA_SHA512: decode public key: "+tc.err)
53+
})
54+
}
55+
})
56+
t.Run("signature mismatch", func(t *testing.T) {
57+
for _, tc := range []struct {
58+
scheme neofscrypto.Scheme
59+
obj object.Object
60+
}{
61+
{neofscrypto.ECDSA_SHA512, objectECDSASHA512},
62+
{neofscrypto.ECDSA_DETERMINISTIC_SHA256, objectECDSARFC6979},
63+
{neofscrypto.ECDSA_WALLETCONNECT, objectECDSAWalletConnect},
64+
} {
65+
t.Run(tc.scheme.String(), func(t *testing.T) {
66+
sig := *tc.obj.Signature()
67+
validSig := sig.Value()
68+
for i := range validSig {
69+
cp := slices.Clone(validSig)
70+
cp[i]++
71+
sig.SetValue(cp)
72+
tc.obj.SetSignature(&sig)
73+
err := icrypto.AuthenticateObject(tc.obj)
74+
require.EqualError(t, err, fmt.Sprintf("scheme %s: signature mismatch", tc.scheme))
75+
}
76+
})
77+
}
78+
})
79+
for _, tc := range []struct {
80+
scheme neofscrypto.Scheme
81+
object object.Object
82+
}{
83+
{scheme: neofscrypto.ECDSA_SHA512, object: objectECDSASHA512},
84+
{scheme: neofscrypto.ECDSA_DETERMINISTIC_SHA256, object: objectECDSARFC6979},
85+
{scheme: neofscrypto.ECDSA_WALLETCONNECT, object: objectECDSAWalletConnect},
86+
} {
87+
require.NoError(t, icrypto.AuthenticateObject(tc.object))
88+
}
89+
}
90+
91+
// set in init.
92+
var (
93+
objectECDSASHA512 object.Object
94+
objectECDSARFC6979 object.Object
95+
objectECDSAWalletConnect object.Object
96+
)
97+
98+
func getUnsignedObject() object.Object {
99+
const creationEpoch = 1362292619
100+
const typ = 43308543
101+
ver := version.New(123, 456)
102+
cnr := cid.ID{61, 208, 16, 128, 106, 78, 90, 196, 156, 65, 180, 142, 62, 137, 245, 242, 69, 250, 212, 176, 35, 114, 239, 114, 53,
103+
231, 19, 14, 46, 67, 163, 155}
104+
childPayload := []byte("Hello,")
105+
payload := slices.Concat(childPayload, []byte("world!"))
106+
107+
var par object.Object
108+
par.SetPayload(payload)
109+
par.SetPayloadSize(uint64(len(payload)))
110+
par.SetVersion(&ver)
111+
par.SetContainerID(cnr)
112+
par.SetOwner(issuer)
113+
par.SetCreationEpoch(creationEpoch)
114+
par.SetType(typ)
115+
par.SetPayloadHomomorphicHash(checksum.NewTillichZemor(tz.Sum(payload)))
116+
par.SetPayloadChecksum(checksum.NewSHA256(sha256.Sum256(payload)))
117+
par.SetAttributes(*object.NewAttribute("690530953", "39483258"), *object.NewAttribute("7208331", "31080424839"))
118+
par.SetID(oid.ID{156, 209, 245, 87, 177, 145, 183, 8, 181, 92, 171, 193, 58, 125, 77, 11, 28, 161, 217, 151, 17, 212, 232, 88, 6,
119+
180, 184, 86, 250, 85, 25, 180})
120+
121+
var child object.Object
122+
child.SetPayload(childPayload)
123+
child.SetPayloadSize(uint64(len(childPayload)))
124+
child.SetVersion(&ver)
125+
child.SetContainerID(cnr)
126+
child.SetOwner(issuer)
127+
child.SetCreationEpoch(creationEpoch)
128+
child.SetType(typ)
129+
child.SetPayloadHomomorphicHash(checksum.NewTillichZemor(tz.Sum(childPayload)))
130+
child.SetPayloadChecksum(checksum.NewSHA256(sha256.Sum256(childPayload)))
131+
child.SetSessionToken(&objectSessionECDSASHA512)
132+
child.SetAttributes(*object.NewAttribute("31429585", "689450942"), *object.NewAttribute("129849325", "859384298"))
133+
child.SetParent(&par)
134+
child.SetID(oid.ID{25, 17, 250, 236, 165, 250, 160, 140, 87, 82, 187, 44, 12, 8, 158, 58, 100, 16, 41, 157, 111, 174, 154, 150,
135+
232, 233, 226, 172, 238, 99, 141, 247})
136+
137+
return child
138+
}
139+
140+
func init() {
141+
objectECDSASHA512 = getUnsignedObject()
142+
objectECDSASHA512Sig := neofscrypto.NewSignatureFromRawKey(neofscrypto.ECDSA_SHA512, pubECDSA, []byte{
143+
4, 120, 189, 133, 160, 231, 85, 45, 168, 156, 247, 131, 90, 93, 201, 80, 135, 207, 211, 4, 181, 153, 60, 59, 125, 134, 10, 176,
144+
42, 211, 225, 114, 11, 148, 215, 152, 237, 37, 67, 172, 191, 210, 254, 104, 66, 140, 25, 27, 60, 63, 150, 185, 253, 238, 17,
145+
124, 147, 228, 53, 117, 177, 0, 2, 76, 52,
146+
})
147+
objectECDSASHA512.SetSignature(&objectECDSASHA512Sig)
148+
149+
objectECDSARFC6979 = getUnsignedObject()
150+
objectECDSARFC6979Sig := neofscrypto.NewSignatureFromRawKey(neofscrypto.ECDSA_DETERMINISTIC_SHA256, pubECDSA, []byte{
151+
107, 253, 17, 84, 1, 224, 98, 101, 53, 167, 26, 89, 223, 49, 127, 98, 167, 187, 83, 0, 254, 50, 1, 155, 25, 96, 247, 197, 44,
152+
65, 81, 71, 86, 248, 232, 234, 140, 157, 75, 111, 205, 226, 86, 236, 119, 67, 174, 242, 107, 239, 51, 161, 190, 46, 47, 106,
153+
125, 187, 139, 136, 157, 13, 155, 226,
154+
})
155+
objectECDSARFC6979.SetSignature(&objectECDSARFC6979Sig)
156+
157+
objectECDSAWalletConnect = getUnsignedObject()
158+
objectECDSAWalletConnectSig := neofscrypto.NewSignatureFromRawKey(neofscrypto.ECDSA_WALLETCONNECT, pubECDSA, []byte{
159+
84, 5, 202, 158, 44, 80, 124, 72, 23, 229, 165, 239, 50, 132, 58, 19, 38, 11, 103, 133, 9, 72, 247, 40, 181, 106, 4, 127,
160+
33, 7, 208, 247, 38, 4, 68, 35, 143, 94, 113, 45, 183, 33, 111, 214, 194, 132, 219, 152, 198, 52, 91, 63, 20, 228, 177, 38,
161+
13, 118, 175, 115, 27, 196, 106, 86, 76, 12, 50, 243, 225, 202, 234, 92, 207, 59, 179, 225, 148, 35, 212, 200,
162+
})
163+
objectECDSAWalletConnect.SetSignature(&objectECDSAWalletConnectSig)
164+
}

Diff for: internal/crypto/tokens.go

+8-31
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
package crypto
22

33
import (
4-
"crypto/ecdsa"
54
"errors"
6-
"fmt"
75

86
neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto"
97
"github.com/nspcc-dev/neofs-sdk-go/user"
108
)
119

1210
// TODO: https://github.com/nspcc-dev/neofs-node/issues/2795 after API stabilization, move some components to SDK
1311

14-
// SignedToken is a common interface of signed NeoFS tokens.
15-
type SignedToken interface {
16-
SignedData() []byte
17-
Signature() (neofscrypto.Signature, bool)
18-
}
19-
2012
// AuthenticateToken checks whether t is signed correctly by its issuer.
2113
func AuthenticateToken[T interface {
22-
SignedToken
14+
SignedData() []byte
15+
Signature() (neofscrypto.Signature, bool)
2316
Issuer() user.ID
2417
}](token T) error {
25-
return authToken(token, token.Issuer())
26-
}
27-
28-
func authToken[T SignedToken](token T, issuer user.ID) error {
18+
issuer := token.Issuer()
2919
if issuer.IsZero() {
3020
return errors.New("missing issuer")
3121
}
32-
pub, err := verifyTokenSignature(token)
22+
sig, ok := token.Signature()
23+
if !ok {
24+
return errMissingSignature
25+
}
26+
pub, err := verifySignature(sig, token.SignedData)
3327
if err != nil {
3428
return err
3529
}
@@ -38,20 +32,3 @@ func authToken[T SignedToken](token T, issuer user.ID) error {
3832
}
3933
return nil
4034
}
41-
42-
func verifyTokenSignature[T SignedToken](token T) (*ecdsa.PublicKey, error) {
43-
sig, ok := token.Signature()
44-
if !ok {
45-
return nil, errors.New("missing signature")
46-
}
47-
switch scheme := sig.Scheme(); scheme {
48-
default:
49-
return nil, fmt.Errorf("unsupported scheme %v", scheme)
50-
case neofscrypto.ECDSA_SHA512, neofscrypto.ECDSA_DETERMINISTIC_SHA256, neofscrypto.ECDSA_WALLETCONNECT:
51-
pub, err := verifyECDSAFns[scheme](sig.PublicKeyBytes(), sig.Value(), token.SignedData)
52-
if err != nil {
53-
return nil, schemeError(scheme, err)
54-
}
55-
return pub, nil
56-
}
57-
}

Diff for: internal/crypto/tokens_test.go

-20
Original file line numberDiff line numberDiff line change
@@ -318,26 +318,6 @@ func TestAuthenticateSessionToken_Container(t *testing.T) {
318318
}
319319
}
320320

321-
var (
322-
// ECDSA private key used in tests.
323-
// privECDSA = ecdsa.PrivateKey{
324-
// PublicKey: ecdsa.PublicKey{
325-
// Curve: elliptic.P256(),
326-
// X: new(big.Int).SetBytes([]byte{206, 67, 193, 231, 254, 180, 127, 78, 101, 154, 23, 161, 134, 77, 122, 34, 234, 85,
327-
// 149, 44, 32, 223, 244, 140, 28, 194, 76, 214, 239, 121, 174, 40}),
328-
// Y: new(big.Int).SetBytes([]byte{170, 190, 155, 176, 31, 11, 4, 14, 103, 210, 53, 0, 73, 46, 81, 129, 163, 217, 81, 51, 111,
329-
// 135, 223, 253, 48, 104, 240, 197, 122, 37, 197, 78}),
330-
// },
331-
// D: new(big.Int).SetBytes([]byte{185, 97, 226, 151, 175, 3, 234, 11, 168, 211, 53, 141, 136, 102, 100, 222, 73, 174, 234, 157,
332-
// 139, 192, 66, 145, 13, 173, 12, 120, 22, 134, 52, 180}),
333-
// }
334-
// corresponds to the private key.
335-
pubECDSA = []byte{2, 206, 67, 193, 231, 254, 180, 127, 78, 101, 154, 23, 161, 134, 77, 122, 34, 234, 85, 149, 44, 32, 223,
336-
244, 140, 28, 194, 76, 214, 239, 121, 174, 40}
337-
// corresponds to pubECDSA.
338-
issuer = user.ID{53, 57, 243, 96, 136, 255, 217, 227, 204, 13, 243, 228, 109, 31, 226, 226, 236, 62, 13, 190, 156, 135, 252, 236, 8}
339-
)
340-
341321
// set in init.
342322
var (
343323
bearerECDSASHA512 bearer.Token

Diff for: pkg/core/object/fmt.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func validateSignature(obj *object.Object) error {
187187
return errors.New("missing signature")
188188
}
189189

190-
if !obj.VerifySignature() {
190+
if err := icrypto.AuthenticateObject(*obj); err != nil {
191191
return errors.New("invalid signature")
192192
}
193193

Diff for: pkg/core/object/fmt_test.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -142,21 +142,17 @@ func TestFormatValidator_Validate(t *testing.T) {
142142
for _, tc := range []struct {
143143
name string
144144
changePub func([]byte) []byte
145-
skip bool
146145
}{
147146
{name: "nil", changePub: func([]byte) []byte { return nil }},
148147
{name: "empty", changePub: func([]byte) []byte { return []byte{} }},
149148
{name: "undersize", changePub: func(k []byte) []byte { return k[:len(k)-1] }},
150149
{name: "oversize", changePub: func(k []byte) []byte { return append(k, 1) }},
151-
{name: "prefix 0", changePub: func(k []byte) []byte { return []byte{0x00} }, skip: true},
150+
{name: "prefix 0", changePub: func(k []byte) []byte { return []byte{0x00} }},
152151
{name: "prefix 1", changePub: func(k []byte) []byte { return []byte{0x01} }},
153152
{name: "prefix 4", changePub: func(k []byte) []byte { return []byte{0x04} }},
154153
{name: "prefix 5", changePub: func(k []byte) []byte { return []byte{0x05} }},
155154
} {
156155
t.Run(tc.name, func(t *testing.T) {
157-
if tc.skip {
158-
t.Skip()
159-
}
160156
pub := slices.Clone(signer.PublicKeyBytes)
161157
sig.SetPublicKeyBytes(tc.changePub(pub))
162158
obj.SetSignature(&sig)

0 commit comments

Comments
 (0)