From 8b74d01e5c8871f1c0abeecbec4f274c5800fd0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 00:33:30 +0000 Subject: [PATCH 1/2] chore(deps): bump github.com/cloudflare/circl from 1.6.3 to 1.6.5 Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.3 to 1.6.5. - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.3...v1.6.5) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c82bf083c4..2e8a9edbf3 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/ccoveille/go-safecast/v2 v2.0.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cespare/xxhash/v2 v2.3.0 - github.com/cloudflare/circl v1.6.3 + github.com/cloudflare/circl v1.6.5 github.com/consensys/gnark v0.16.3 github.com/consensys/gnark-crypto v0.21.0 github.com/coocood/freecache v1.2.7 diff --git a/go.sum b/go.sum index 92e1e0caed..dab40134fe 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= +github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/consensys/gnark v0.16.3 h1:S7BtIQSX2WLHV2857HrLmrQ5xIl0ZRL8kT6rcLn8gow= github.com/consensys/gnark v0.16.3/go.mod h1:ChMGCGi8KztMtuQXgxprorLVJY29FPnKkjN19RXB/KU= From 999e86498e994d5c72fcd17a1277db834262cb41 Mon Sep 17 00:00:00 2001 From: Alexey Kiselev Date: Tue, 8 Sep 2026 11:05:35 +0400 Subject: [PATCH 2/2] Fixed improper use of Circl library. Function to aggregate public keys added and tested. --- itests/config/config.go | 4 +- itests/fixtures/base_fixtures.go | 2 +- itests/init_internal_test.go | 4 +- pkg/crypto/bls/bls.go | 56 +++++++++---- pkg/crypto/bls/bls_test.go | 136 +++++++++++++++++++++++++++++++ pkg/crypto/bls/pop.go | 8 +- 6 files changed, 186 insertions(+), 24 deletions(-) diff --git a/itests/config/config.go b/itests/config/config.go index 7b4d23b6e2..cf248dc6ab 100644 --- a/itests/config/config.go +++ b/itests/config/config.go @@ -32,8 +32,8 @@ const ( goContainerName = "go-node" ScalaImageRepository = "wavesplatform/wavesnode" + ScalaImageTag = "1.6.4" goImageRepository = "go-node" - DefaultImageTag = "1.6.4" ) const ( @@ -144,7 +144,7 @@ func (c *ScalaConfigurator) DockerRunOptions() *dockertest.RunOptions { } if c.imageTag == "" { - c.imageTag = DefaultImageTag + c.imageTag = ScalaImageTag } kps := new(strings.Builder) diff --git a/itests/fixtures/base_fixtures.go b/itests/fixtures/base_fixtures.go index 800bd11060..94e4145fad 100644 --- a/itests/fixtures/base_fixtures.go +++ b/itests/fixtures/base_fixtures.go @@ -26,7 +26,7 @@ type BaseSuite struct { } func (suite *BaseSuite) BaseSetup(options ...config.BlockchainOption) { - suite.BaseSetupWithImages(config.ScalaImageRepository, config.DefaultImageTag, options...) + suite.BaseSetupWithImages(config.ScalaImageRepository, config.ScalaImageTag, options...) } func (suite *BaseSuite) BaseSetupWithImages(scalaRepository, scalaTag string, diff --git a/itests/init_internal_test.go b/itests/init_internal_test.go index ddead9a1cc..73f11d1443 100644 --- a/itests/init_internal_test.go +++ b/itests/init_internal_test.go @@ -70,8 +70,8 @@ func testsSetup() error { slog.Info("Pulling scala-node image", "platform", platform) if plErr := pool.Client.PullImage( dc.PullImageOptions{ - Repository: "wavesplatform/wavesnode", - Tag: "latest", + Repository: config.ScalaImageRepository, + Tag: config.ScalaImageTag, Platform: platform, }, dc.AuthConfiguration{}); plErr != nil { diff --git a/pkg/crypto/bls/bls.go b/pkg/crypto/bls/bls.go index f8beff5b11..eb3283aa68 100644 --- a/pkg/crypto/bls/bls.go +++ b/pkg/crypto/bls/bls.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/cloudflare/circl/ecc/bls12381" cbls "github.com/cloudflare/circl/sign/bls" "github.com/mr-tron/base58" @@ -23,8 +24,11 @@ const ( ) var ( - ErrNoSignatures = errors.New("no signatures") - ErrDuplicateSignature = errors.New("duplicate signature") + ErrNoSignatures = errors.New("no signatures") + ErrDuplicateSignature = errors.New("duplicate signature") + ErrNoPublicKeys = errors.New("no public keys") + ErrDuplicatePublicKey = errors.New("duplicate public key") + ErrIdentityAggregatedPublicKey = errors.New("aggregated public key is the identity") ) // SecretKey is 32-byte BLS secret key. @@ -266,25 +270,49 @@ func AggregateSignatures(signatures []Signature) (Signature, error) { return NewSignatureFromBytes(aggregate) } -// VerifyAggregate verifies aggregated signature over the same message. -func VerifyAggregate(pks []PublicKey, msg []byte, sig Signature) bool { +// AggregatePublicKeys combines the given public keys into a single aggregated public key by adding up the +// corresponding points of G1. Every key is validated (correct encoding, on-curve, in the correct subgroup and +// not the identity) while being deserialized. Duplicate keys are rejected. +func AggregatePublicKeys(pks []PublicKey) (PublicKey, error) { if len(pks) == 0 { - return false + return PublicKey{}, ErrNoPublicKeys } if !isUnique(pks) { - return false + return PublicKey{}, ErrDuplicatePublicKey } - ks := make([]*cbls.PublicKey[cbls.G1], len(pks)) - ms := make([][]byte, len(pks)) + var agg bls12381.G1 + agg.SetIdentity() for i := range pks { - k := new(cbls.PublicKey[cbls.G1]) - if err := k.UnmarshalBinary(pks[i].Bytes()); err != nil { - return false + var p bls12381.G1 + if err := p.SetBytes(pks[i].Bytes()); err != nil { // SetBytes rejects points outside of the G1 subgroup. + return PublicKey{}, fmt.Errorf("failed to aggregate public keys: invalid public key #%d: %w", i, err) } - ks[i] = k - ms[i] = msg + if p.IsIdentity() { + return PublicKey{}, fmt.Errorf("failed to aggregate public keys: public key #%d is the identity", i) + } + agg.Add(&agg, &p) + } + if agg.IsIdentity() { // Aggregation of valid keys can still cancel out to the identity, such a key is unusable. + return PublicKey{}, ErrIdentityAggregatedPublicKey + } + return NewPublicKeyFromBytes(agg.BytesCompressed()) +} + +// VerifyAggregate verifies aggregated signature over the same message. +// This is the FastAggregateVerify algorithm of the BLS signature scheme with proof of possession: the public keys +// are aggregated into a single one, and the aggregated signature is verified against it. +// Security note: aggregation of public keys is only safe against rogue public key attacks if every key was proven +// to be possessed by its owner, see VerifyPoP. Keys passed here MUST have been registered with a valid PoP. +func VerifyAggregate(pks []PublicKey, msg []byte, sig Signature) bool { + apk, err := AggregatePublicKeys(pks) + if err != nil { + return false + } + ok, err := Verify(apk, msg, sig) + if err != nil { + return false } - return cbls.VerifyAggregate[cbls.G1](ks, ms, sig.Bytes()) + return ok } func isUnique[T comparable](in []T) bool { diff --git a/pkg/crypto/bls/bls_test.go b/pkg/crypto/bls/bls_test.go index 7aff81e56c..634136e175 100644 --- a/pkg/crypto/bls/bls_test.go +++ b/pkg/crypto/bls/bls_test.go @@ -88,6 +88,142 @@ func TestAggregateFromWavesSecrets_SameMessage(t *testing.T) { require.False(t, ok, "aggregate must fail on different message") } +// generateKeyPairs creates n BLS key pairs from random Waves secret keys. +func generateKeyPairs(t *testing.T, n int) ([]bls.SecretKey, []bls.PublicKey) { + t.Helper() + sks := make([]bls.SecretKey, n) + pks := make([]bls.PublicKey, n) + for i := range n { + sk, err := secretKeyFromWavesSecretKey(randWavesSK(t)) + require.NoError(t, err) + pk, err := sk.PublicKey() + require.NoError(t, err) + sks[i], pks[i] = sk, pk + } + return sks, pks +} + +// signAll signs the given message with every given secret key. +func signAll(t *testing.T, sks []bls.SecretKey, msg []byte) []bls.Signature { + t.Helper() + sigs := make([]bls.Signature, len(sks)) + for i, sk := range sks { + sig, err := bls.Sign(sk, msg) + require.NoError(t, err) + sigs[i] = sig + } + return sigs +} + +// TestAggregatePublicKeys_SameMessage checks that an aggregated public key acts as an ordinary public key of the +// group of signers: every signer signs the same message, each signature is verified separately by the corresponding +// public key, and the aggregated signature is verified by the aggregated public key. Aggregating a public key of a +// signer who did not sign, or leaving out a signer who did, must break the verification. +func TestAggregatePublicKeys_SameMessage(t *testing.T) { + const n = 4 + msg := []byte("aggregated public key test") + + sks, pks := generateKeyPairs(t, n+1) // The last key pair is an outsider, it doesn't sign the message. + signers, signerPKs := sks[:n], pks[:n] + outsiderSK, outsiderPK := sks[n], pks[n] + + // Every signature is valid for its own public key and invalid for the public key of another signer. + sigs := signAll(t, signers, msg) + for i := range signers { + ok, err := bls.Verify(signerPKs[i], msg, sigs[i]) + require.NoError(t, err) + assert.True(t, ok, "signature %d must be valid for its own public key", i) + + ok, err = bls.Verify(signerPKs[(i+1)%n], msg, sigs[i]) + require.NoError(t, err) + assert.False(t, ok, "signature %d must be invalid for the public key of another signer", i) + } + + // The aggregated signature is valid for the aggregated public key of all the signers. + aggSig, err := bls.AggregateSignatures(sigs) + require.NoError(t, err) + aggPK, err := bls.AggregatePublicKeys(signerPKs) + require.NoError(t, err) + + ok, err := bls.Verify(aggPK, msg, aggSig) + require.NoError(t, err) + assert.True(t, ok, "aggregated signature must be valid for the aggregated public key") + + // The same verification through VerifyAggregate must produce the same result. + assert.True(t, bls.VerifyAggregate(signerPKs, msg, aggSig)) + + // A different message must not verify. + ok, err = bls.Verify(aggPK, []byte("another message"), aggSig) + require.NoError(t, err) + assert.False(t, ok, "aggregated signature must be invalid for a different message") + + // A public key of a signer who didn't sign the message added to the aggregate must break the verification. + extendedPKs := append(slices.Clone(signerPKs), outsiderPK) + extendedPK, err := bls.AggregatePublicKeys(extendedPKs) + require.NoError(t, err) + ok, err = bls.Verify(extendedPK, msg, aggSig) + require.NoError(t, err) + assert.False(t, ok, "aggregated public key of a larger set of signers must not verify the signature") + assert.False(t, bls.VerifyAggregate(extendedPKs, msg, aggSig)) + + // But it verifies again as soon as the signature of that signer joins the aggregate. + outsiderSig, err := bls.Sign(outsiderSK, msg) + require.NoError(t, err) + extendedSig, err := bls.AggregateSignatures(append(slices.Clone(sigs), outsiderSig)) + require.NoError(t, err) + ok, err = bls.Verify(extendedPK, msg, extendedSig) + require.NoError(t, err) + assert.True(t, ok, "aggregated signature of all the signers must be valid for their aggregated public key") + + // A missing signer breaks the verification in the same way. + reducedPKs := signerPKs[:n-1] + reducedPK, err := bls.AggregatePublicKeys(reducedPKs) + require.NoError(t, err) + ok, err = bls.Verify(reducedPK, msg, aggSig) + require.NoError(t, err) + assert.False(t, ok, "aggregated public key of a smaller set of signers must not verify the signature") + assert.False(t, bls.VerifyAggregate(reducedPKs, msg, aggSig)) +} + +// TestAggregatePublicKeys_OrderIndependent checks that aggregation of public keys is commutative. +func TestAggregatePublicKeys_OrderIndependent(t *testing.T) { + _, pks := generateKeyPairs(t, 5) + + expected, err := bls.AggregatePublicKeys(pks) + require.NoError(t, err) + + slices.Reverse(pks) + actual, err := bls.AggregatePublicKeys(pks) + require.NoError(t, err) + assert.Equal(t, expected, actual, "order of public keys must not affect the aggregate") + + // A single public key aggregates into itself. + single, err := bls.AggregatePublicKeys(pks[:1]) + require.NoError(t, err) + assert.Equal(t, pks[0], single) +} + +// TestAggregatePublicKeys_Errors checks that invalid sets of public keys are rejected. +func TestAggregatePublicKeys_Errors(t *testing.T) { + _, pks := generateKeyPairs(t, 2) + + _, err := bls.AggregatePublicKeys(nil) + assert.ErrorIs(t, err, bls.ErrNoPublicKeys) + + _, err = bls.AggregatePublicKeys([]bls.PublicKey{pks[0], pks[1], pks[0]}) + assert.ErrorIs(t, err, bls.ErrDuplicatePublicKey) + + // Keys that are not valid points of G1 are rejected. + _, err = bls.AggregatePublicKeys([]bls.PublicKey{pks[0], {}}) + assert.Error(t, err) + + // The identity, a valid encoding but an unusable public key, is rejected as well. + var identity bls.PublicKey + identity[0] = 0xc0 // Compressed point at infinity. + _, err = bls.AggregatePublicKeys([]bls.PublicKey{pks[0], identity}) + assert.Error(t, err) +} + func TestVerifyAggregate_RejectsDuplicatePublicKeys(t *testing.T) { sk1, err := secretKeyFromWavesSecretKey(randWavesSK(t)) require.NoError(t, err) diff --git a/pkg/crypto/bls/pop.go b/pkg/crypto/bls/pop.go index eea6ab924a..39cc6ea670 100644 --- a/pkg/crypto/bls/pop.go +++ b/pkg/crypto/bls/pop.go @@ -3,6 +3,8 @@ package bls import ( "encoding/binary" "fmt" + + cbls "github.com/cloudflare/circl/sign/bls" ) const PoPMessageSize = PublicKeySize + 4 @@ -44,9 +46,5 @@ func VerifyPoP(pk PublicKey, height uint32, sig Signature) (bool, error) { return false, fmt.Errorf("failed to verify PoP, invalid public key") } msg := BuildPoPMessage(pk, height) - ok, err := Verify(pk, msg, sig) - if err != nil { - return false, fmt.Errorf("failed to verify PoP: %w", err) - } - return ok, nil + return cbls.Verify[cbls.G1](cpk, msg, sig.Bytes()), nil }