Skip to content

Commit f1f42c0

Browse files
feat: add gblsminsig package
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.
1 parent 0f9c02c commit f1f42c0

File tree

14 files changed

+1484
-1418
lines changed

14 files changed

+1484
-1418
lines changed

gcrypto/gblsminsig/bls.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package gblsminsig
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/gordian-engine/gordian/gcrypto"
9+
blst "github.com/supranational/blst/bindings/go"
10+
)
11+
12+
const keyTypeName = "bls-minsig"
13+
14+
// The domain separation tag is a requirement per RFC9380 (Hashing to Elliptic Curves).
15+
// See sections 2.2.5 (domain separation),
16+
// 3.1 (domain separation requirements),
17+
// and 8.10 (suite ID naming conventions).
18+
//
19+
// Furthermore, see also draft-irtf-cfrg-bls-signature-05,
20+
// section 4.1 (ciphersuite format),
21+
// as that is the actual format being followed here.
22+
//
23+
// The ciphersuite ID according to the BLS signature document is:
24+
//
25+
// "BLS_SIG_" || H2C_SUITE_ID || SC_TAG || "_"
26+
//
27+
// And the H2C_SUITE_ID, per RFC9380 section 8.8.1, is:
28+
//
29+
// BLS12381G1_XMD:SHA-256_SSWU_RO_
30+
//
31+
// Which only leaves the SC_TAG value, which is "NUL" for the basic scheme.
32+
var DomainSeparationTag = []byte("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_")
33+
34+
// Register registers the BLS minimzed-signature key type with the given Registry.
35+
func Register(reg *gcrypto.Registry) {
36+
reg.Register(keyTypeName, PubKey{}, NewPubKey)
37+
}
38+
39+
// PubKey wraps a blst.P2Affine and defines methods for the [gcrypto.PubKey] interface.
40+
type PubKey blst.P2Affine
41+
42+
// NewPubKey decodes a compressed p2 affine point
43+
// and returns the public key for it.
44+
func NewPubKey(b []byte) (gcrypto.PubKey, error) {
45+
// This is checked inside Uncompress too,
46+
// but checking it here is an opportunity to return a more meaningful error.
47+
if len(b) != blst.BLST_P2_COMPRESS_BYTES {
48+
return nil, fmt.Errorf("expected %d compressed bytes, got %d", blst.BLST_P2_COMPRESS_BYTES, len(b))
49+
}
50+
51+
p2a := new(blst.P2Affine)
52+
p2a = p2a.Uncompress(b)
53+
54+
if p2a == nil {
55+
return nil, errors.New("failed to decompress input")
56+
}
57+
58+
if !p2a.KeyValidate() {
59+
return nil, errors.New("input key failed validation")
60+
}
61+
62+
pk := PubKey(*p2a)
63+
return pk, nil
64+
}
65+
66+
// Equal reports whether other is the same public key as k.
67+
func (k PubKey) Equal(other gcrypto.PubKey) bool {
68+
o, ok := other.(PubKey)
69+
if !ok {
70+
return false
71+
}
72+
73+
p2a := blst.P2Affine(k)
74+
75+
p2o := blst.P2Affine(o)
76+
return p2a.Equals(&p2o)
77+
}
78+
79+
// PubKeyBytes returns the compressed bytes underlying k's P2 affine point.
80+
func (k PubKey) PubKeyBytes() []byte {
81+
p2a := blst.P2Affine(k)
82+
return p2a.Compress()
83+
}
84+
85+
// Verify reports whether sig matches k for msg.
86+
func (k PubKey) Verify(msg, sig []byte) bool {
87+
// Signature is P1, and we assume the signature is compressed.
88+
p1a := new(blst.P1Affine)
89+
p1a = p1a.Uncompress(sig)
90+
if p1a == nil {
91+
return false
92+
}
93+
94+
// Unclear if false is the correct input here.
95+
if !p1a.SigValidate(false) {
96+
return false
97+
}
98+
99+
// Cast the public key back to p2,
100+
// so we can verify it against the p1 signature.
101+
p2a := blst.P2Affine(k)
102+
103+
return p1a.Verify(false, &p2a, false, blst.Message(msg), DomainSeparationTag)
104+
}
105+
106+
// TypeName returns the type name for minimized-signature BLS signatures.
107+
func (k PubKey) TypeName() string {
108+
return keyTypeName
109+
}
110+
111+
// Signer satisfies [gcrypto.Signer] for minimized-signature BLS.
112+
type Signer struct {
113+
// The secret is a scalar,
114+
// but the blst package aliases it as SecretKey
115+
// to add a few more methods.
116+
secret blst.SecretKey
117+
118+
// The point is the effective public key.
119+
// The point on its own is insufficient to derive the secret.
120+
point blst.P2Affine
121+
}
122+
123+
// NewSigner returns a new signer.
124+
// The initial key material must be at least 32 bytes,
125+
// and should be cryptographically random.
126+
func NewSigner(ikm []byte) (Signer, error) {
127+
if len(ikm) < blst.BLST_SCALAR_BYTES {
128+
return Signer{}, fmt.Errorf(
129+
"ikm data too short: got %d, need at least %d",
130+
len(ikm), blst.BLST_SCALAR_BYTES,
131+
)
132+
}
133+
salt := []byte("TODO") // Need to decide how to get the salt configurable.
134+
secretKey := blst.KeyGenV5(ikm, salt)
135+
136+
point := new(blst.P2Affine)
137+
point = point.From(secretKey)
138+
139+
return Signer{
140+
secret: *secretKey,
141+
point: *point,
142+
}, nil
143+
}
144+
145+
// PubKey returns the [PubKey] for s
146+
// (which is actually the p2 point).
147+
func (s Signer) PubKey() gcrypto.PubKey {
148+
return PubKey(s.point)
149+
}
150+
151+
// Sign produces the signed point for the given input.
152+
//
153+
// It uses the [DomainSeparationTag],
154+
// which must be provided to verification too.
155+
// The [PubKey] type in this package is hardcoded to use the same DST.
156+
func (s Signer) Sign(_ context.Context, input []byte) ([]byte, error) {
157+
sig := new(blst.P1Affine).Sign(&s.secret, input, DomainSeparationTag, true)
158+
159+
// sig could be nil only if option parsing failed.
160+
if sig == nil {
161+
return nil, errors.New("failed to sign")
162+
}
163+
164+
// The signature is a new point on the p1 affine curve.
165+
return sig.Compress(), nil
166+
}

gcrypto/gblsminsig/bls_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package gblsminsig_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gordian-engine/gordian/gcrypto/gblsminsig"
8+
"github.com/stretchr/testify/require"
9+
blst "github.com/supranational/blst/bindings/go"
10+
)
11+
12+
func TestSignAndVerify_single(t *testing.T) {
13+
t.Parallel()
14+
15+
ikm := make([]byte, 32)
16+
for i := range ikm {
17+
ikm[i] = byte(i)
18+
}
19+
20+
s, err := gblsminsig.NewSigner(ikm)
21+
require.NoError(t, err)
22+
23+
msg := []byte("hello world")
24+
25+
sig, err := s.Sign(context.Background(), msg)
26+
require.NoError(t, err)
27+
28+
require.True(t, s.PubKey().Verify(msg, sig))
29+
30+
// Modifying the message fails verification.
31+
msg[0]++
32+
require.False(t, s.PubKey().Verify(msg, sig))
33+
msg[0]--
34+
35+
// Modifying the signature fails verification too.
36+
sig[0]++
37+
require.False(t, s.PubKey().Verify(msg, sig))
38+
}
39+
40+
func TestSignAndVerify_multiple(t *testing.T) {
41+
t.Parallel()
42+
43+
ikm1 := make([]byte, 32)
44+
ikm2 := make([]byte, 32)
45+
for i := range ikm1 {
46+
ikm1[i] = byte(i)
47+
ikm2[i] = byte(i) + 32
48+
}
49+
50+
s1, err := gblsminsig.NewSigner(ikm1)
51+
require.NoError(t, err)
52+
s2, err := gblsminsig.NewSigner(ikm2)
53+
require.NoError(t, err)
54+
55+
msg := []byte("hello world")
56+
57+
sig1, err := s1.Sign(context.Background(), msg)
58+
require.NoError(t, err)
59+
60+
sig2, err := s2.Sign(context.Background(), msg)
61+
require.NoError(t, err)
62+
63+
sigp11 := new(blst.P1Affine).Uncompress(sig1)
64+
require.NotNil(t, sigp11)
65+
sigp12 := new(blst.P1Affine).Uncompress(sig2)
66+
require.NotNil(t, sigp12)
67+
68+
// Aggregate the signatures into a single affine point.
69+
sigAgg := new(blst.P1Aggregate)
70+
require.True(t, sigAgg.AggregateCompressed([][]byte{sig1, sig2}, true))
71+
finalSig := sigAgg.ToAffine().Compress()
72+
73+
// Aggregate the keys too.
74+
keyAgg := new(blst.P2Aggregate)
75+
require.True(t, keyAgg.AggregateCompressed([][]byte{
76+
s1.PubKey().PubKeyBytes(),
77+
s2.PubKey().PubKeyBytes(),
78+
}, true))
79+
80+
finalKeyAffine := keyAgg.ToAffine()
81+
finalKey := gblsminsig.PubKey(*finalKeyAffine)
82+
83+
require.True(t, finalKey.Verify(msg, finalSig))
84+
85+
// Changing the message fails verification.
86+
msg[0]++
87+
require.False(t, finalKey.Verify(msg, finalSig))
88+
msg[0]--
89+
90+
// Modifying the signature fails verification too.
91+
finalSig[0]++
92+
require.False(t, finalKey.Verify(msg, finalSig))
93+
}

gcrypto/gblsminsig/doc.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Package gblsminsig wraps [github.com/supranational/blst/bindings/go]
2+
// to provide a [gcrypto.PubKey] implementation backed by BLS keys,
3+
// where the BLS keys have minimized signatures.
4+
//
5+
// We are not currently providing an alternate implementation with minimized keys,
6+
// as signatures are expected to be transmitted and stored much more frequently than keys.
7+
//
8+
// The blst dependency requires CGo,
9+
// so therefore this package also requires CGo.
10+
//
11+
// Two key references for correctly understanding and using BLS keys are
12+
// [RFC9380] (Hashing to Elliptic Curves)
13+
// and the IETF draft for [BLS Signatures].
14+
//
15+
// [RFC9380]: https://www.rfc-editor.org/rfc/rfc9380.html
16+
// [BLS Signatures]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05
17+
package gblsminsig

0 commit comments

Comments
 (0)