-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This uses the supranational/blst package for an implementation of BLS signatures. We use the minimized signature form, hence the package name. The package includes a BLS-backed gcrypto.PubKey implementation and a CommonMessageSignatureProof implementation. There are likely some missing details in the BLS setup, at least in adding configurability for the signatures' salt. I think the domain separation tag is correct. I cannot find the whitepaper or technical document that introduced me to the concept, but the model of arranging the validators in a tree for key and signature aggregation is based on the concept that the leftmost validators are trustworthy and likely to be online, while the rightmost validators are the least expected to vote. See further documentation on the SignatureProof type. It has not been fully integrated yet, but we have tests around the usage of both of those types. Next up are some slight interface changes that will affect the ed25519 implementation too. And I think we may need some special handling such that the signature committed to chain is a "final aggregation" of keys that would not normally be aggregated together. For instance, if there are only four validators, then during consensus gossip, we would pair 0-1 and 2-3, but never 0-2 or 1-3. If the final votes were only from 0, 1, and 3 -- that is to say validator 2 was absent or perhaps voted nil -- then we would commit with the aggregated signaure 0-1-3, to minimize space used for signatures. This also removes the gmerkle package, which was left in for specifically this use case, but which turned out to be an incorrect fit. Note that with the introduction of this package, we have a dependency on supranational/blst, which is a C dependency; and there is currently the outstanding issue supranational/blst#245 which prevents building blst with Go 1.24. However, supranational/blst#247 is an open PR that fixes the build for Go 1.24 and Go tip.
- Loading branch information
1 parent
0f9c02c
commit f1f42c0
Showing
14 changed files
with
1,484 additions
and
1,418 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package gblsminsig | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/gordian-engine/gordian/gcrypto" | ||
blst "github.com/supranational/blst/bindings/go" | ||
) | ||
|
||
const keyTypeName = "bls-minsig" | ||
|
||
// The domain separation tag is a requirement per RFC9380 (Hashing to Elliptic Curves). | ||
// See sections 2.2.5 (domain separation), | ||
// 3.1 (domain separation requirements), | ||
// and 8.10 (suite ID naming conventions). | ||
// | ||
// Furthermore, see also draft-irtf-cfrg-bls-signature-05, | ||
// section 4.1 (ciphersuite format), | ||
// as that is the actual format being followed here. | ||
// | ||
// The ciphersuite ID according to the BLS signature document is: | ||
// | ||
// "BLS_SIG_" || H2C_SUITE_ID || SC_TAG || "_" | ||
// | ||
// And the H2C_SUITE_ID, per RFC9380 section 8.8.1, is: | ||
// | ||
// BLS12381G1_XMD:SHA-256_SSWU_RO_ | ||
// | ||
// Which only leaves the SC_TAG value, which is "NUL" for the basic scheme. | ||
var DomainSeparationTag = []byte("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_") | ||
|
||
// Register registers the BLS minimzed-signature key type with the given Registry. | ||
func Register(reg *gcrypto.Registry) { | ||
reg.Register(keyTypeName, PubKey{}, NewPubKey) | ||
} | ||
|
||
// PubKey wraps a blst.P2Affine and defines methods for the [gcrypto.PubKey] interface. | ||
type PubKey blst.P2Affine | ||
|
||
// NewPubKey decodes a compressed p2 affine point | ||
// and returns the public key for it. | ||
func NewPubKey(b []byte) (gcrypto.PubKey, error) { | ||
// This is checked inside Uncompress too, | ||
// but checking it here is an opportunity to return a more meaningful error. | ||
if len(b) != blst.BLST_P2_COMPRESS_BYTES { | ||
return nil, fmt.Errorf("expected %d compressed bytes, got %d", blst.BLST_P2_COMPRESS_BYTES, len(b)) | ||
} | ||
|
||
p2a := new(blst.P2Affine) | ||
p2a = p2a.Uncompress(b) | ||
|
||
if p2a == nil { | ||
return nil, errors.New("failed to decompress input") | ||
} | ||
|
||
if !p2a.KeyValidate() { | ||
return nil, errors.New("input key failed validation") | ||
} | ||
|
||
pk := PubKey(*p2a) | ||
return pk, nil | ||
} | ||
|
||
// Equal reports whether other is the same public key as k. | ||
func (k PubKey) Equal(other gcrypto.PubKey) bool { | ||
o, ok := other.(PubKey) | ||
if !ok { | ||
return false | ||
} | ||
|
||
p2a := blst.P2Affine(k) | ||
|
||
p2o := blst.P2Affine(o) | ||
return p2a.Equals(&p2o) | ||
} | ||
|
||
// PubKeyBytes returns the compressed bytes underlying k's P2 affine point. | ||
func (k PubKey) PubKeyBytes() []byte { | ||
p2a := blst.P2Affine(k) | ||
return p2a.Compress() | ||
} | ||
|
||
// Verify reports whether sig matches k for msg. | ||
func (k PubKey) Verify(msg, sig []byte) bool { | ||
// Signature is P1, and we assume the signature is compressed. | ||
p1a := new(blst.P1Affine) | ||
p1a = p1a.Uncompress(sig) | ||
if p1a == nil { | ||
return false | ||
} | ||
|
||
// Unclear if false is the correct input here. | ||
if !p1a.SigValidate(false) { | ||
return false | ||
} | ||
|
||
// Cast the public key back to p2, | ||
// so we can verify it against the p1 signature. | ||
p2a := blst.P2Affine(k) | ||
|
||
return p1a.Verify(false, &p2a, false, blst.Message(msg), DomainSeparationTag) | ||
} | ||
|
||
// TypeName returns the type name for minimized-signature BLS signatures. | ||
func (k PubKey) TypeName() string { | ||
return keyTypeName | ||
} | ||
|
||
// Signer satisfies [gcrypto.Signer] for minimized-signature BLS. | ||
type Signer struct { | ||
// The secret is a scalar, | ||
// but the blst package aliases it as SecretKey | ||
// to add a few more methods. | ||
secret blst.SecretKey | ||
|
||
// The point is the effective public key. | ||
// The point on its own is insufficient to derive the secret. | ||
point blst.P2Affine | ||
} | ||
|
||
// NewSigner returns a new signer. | ||
// The initial key material must be at least 32 bytes, | ||
// and should be cryptographically random. | ||
func NewSigner(ikm []byte) (Signer, error) { | ||
if len(ikm) < blst.BLST_SCALAR_BYTES { | ||
return Signer{}, fmt.Errorf( | ||
"ikm data too short: got %d, need at least %d", | ||
len(ikm), blst.BLST_SCALAR_BYTES, | ||
) | ||
} | ||
salt := []byte("TODO") // Need to decide how to get the salt configurable. | ||
secretKey := blst.KeyGenV5(ikm, salt) | ||
|
||
point := new(blst.P2Affine) | ||
point = point.From(secretKey) | ||
|
||
return Signer{ | ||
secret: *secretKey, | ||
point: *point, | ||
}, nil | ||
} | ||
|
||
// PubKey returns the [PubKey] for s | ||
// (which is actually the p2 point). | ||
func (s Signer) PubKey() gcrypto.PubKey { | ||
return PubKey(s.point) | ||
} | ||
|
||
// Sign produces the signed point for the given input. | ||
// | ||
// It uses the [DomainSeparationTag], | ||
// which must be provided to verification too. | ||
// The [PubKey] type in this package is hardcoded to use the same DST. | ||
func (s Signer) Sign(_ context.Context, input []byte) ([]byte, error) { | ||
sig := new(blst.P1Affine).Sign(&s.secret, input, DomainSeparationTag, true) | ||
|
||
// sig could be nil only if option parsing failed. | ||
if sig == nil { | ||
return nil, errors.New("failed to sign") | ||
} | ||
|
||
// The signature is a new point on the p1 affine curve. | ||
return sig.Compress(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package gblsminsig_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/gordian-engine/gordian/gcrypto/gblsminsig" | ||
"github.com/stretchr/testify/require" | ||
blst "github.com/supranational/blst/bindings/go" | ||
) | ||
|
||
func TestSignAndVerify_single(t *testing.T) { | ||
t.Parallel() | ||
|
||
ikm := make([]byte, 32) | ||
for i := range ikm { | ||
ikm[i] = byte(i) | ||
} | ||
|
||
s, err := gblsminsig.NewSigner(ikm) | ||
require.NoError(t, err) | ||
|
||
msg := []byte("hello world") | ||
|
||
sig, err := s.Sign(context.Background(), msg) | ||
require.NoError(t, err) | ||
|
||
require.True(t, s.PubKey().Verify(msg, sig)) | ||
|
||
// Modifying the message fails verification. | ||
msg[0]++ | ||
require.False(t, s.PubKey().Verify(msg, sig)) | ||
msg[0]-- | ||
|
||
// Modifying the signature fails verification too. | ||
sig[0]++ | ||
require.False(t, s.PubKey().Verify(msg, sig)) | ||
} | ||
|
||
func TestSignAndVerify_multiple(t *testing.T) { | ||
t.Parallel() | ||
|
||
ikm1 := make([]byte, 32) | ||
ikm2 := make([]byte, 32) | ||
for i := range ikm1 { | ||
ikm1[i] = byte(i) | ||
ikm2[i] = byte(i) + 32 | ||
} | ||
|
||
s1, err := gblsminsig.NewSigner(ikm1) | ||
require.NoError(t, err) | ||
s2, err := gblsminsig.NewSigner(ikm2) | ||
require.NoError(t, err) | ||
|
||
msg := []byte("hello world") | ||
|
||
sig1, err := s1.Sign(context.Background(), msg) | ||
require.NoError(t, err) | ||
|
||
sig2, err := s2.Sign(context.Background(), msg) | ||
require.NoError(t, err) | ||
|
||
sigp11 := new(blst.P1Affine).Uncompress(sig1) | ||
require.NotNil(t, sigp11) | ||
sigp12 := new(blst.P1Affine).Uncompress(sig2) | ||
require.NotNil(t, sigp12) | ||
|
||
// Aggregate the signatures into a single affine point. | ||
sigAgg := new(blst.P1Aggregate) | ||
require.True(t, sigAgg.AggregateCompressed([][]byte{sig1, sig2}, true)) | ||
finalSig := sigAgg.ToAffine().Compress() | ||
|
||
// Aggregate the keys too. | ||
keyAgg := new(blst.P2Aggregate) | ||
require.True(t, keyAgg.AggregateCompressed([][]byte{ | ||
s1.PubKey().PubKeyBytes(), | ||
s2.PubKey().PubKeyBytes(), | ||
}, true)) | ||
|
||
finalKeyAffine := keyAgg.ToAffine() | ||
finalKey := gblsminsig.PubKey(*finalKeyAffine) | ||
|
||
require.True(t, finalKey.Verify(msg, finalSig)) | ||
|
||
// Changing the message fails verification. | ||
msg[0]++ | ||
require.False(t, finalKey.Verify(msg, finalSig)) | ||
msg[0]-- | ||
|
||
// Modifying the signature fails verification too. | ||
finalSig[0]++ | ||
require.False(t, finalKey.Verify(msg, finalSig)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Package gblsminsig wraps [github.com/supranational/blst/bindings/go] | ||
// to provide a [gcrypto.PubKey] implementation backed by BLS keys, | ||
// where the BLS keys have minimized signatures. | ||
// | ||
// We are not currently providing an alternate implementation with minimized keys, | ||
// as signatures are expected to be transmitted and stored much more frequently than keys. | ||
// | ||
// The blst dependency requires CGo, | ||
// so therefore this package also requires CGo. | ||
// | ||
// Two key references for correctly understanding and using BLS keys are | ||
// [RFC9380] (Hashing to Elliptic Curves) | ||
// and the IETF draft for [BLS Signatures]. | ||
// | ||
// [RFC9380]: https://www.rfc-editor.org/rfc/rfc9380.html | ||
// [BLS Signatures]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05 | ||
package gblsminsig |
Oops, something went wrong.