Skip to content

Commit

Permalink
feat: add gblsminsig package
Browse files Browse the repository at this point in the history
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
mark-rushakoff committed Jan 8, 2025
1 parent 0f9c02c commit f1f42c0
Show file tree
Hide file tree
Showing 14 changed files with 1,484 additions and 1,418 deletions.
166 changes: 166 additions & 0 deletions gcrypto/gblsminsig/bls.go
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
}
93 changes: 93 additions & 0 deletions gcrypto/gblsminsig/bls_test.go
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))
}
17 changes: 17 additions & 0 deletions gcrypto/gblsminsig/doc.go
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
Loading

0 comments on commit f1f42c0

Please sign in to comment.