Skip to content

Commit

Permalink
feat: initial sketch of BLS
Browse files Browse the repository at this point in the history
This first implementation uses the minimized-signature form. I don't
think we have a use case for the minimized-key form, but the packages
are structured such that we can add it later.

I don't think this internal API is finalized yet; we will need to try it
out with the existing gcrypto signature proof interfaces to see what
refining it needs.

We may need some adjustments to account for the salt. I need to double
check whether we need any further custom data for the Domain Separation
Tag. Using the compressed forms of points is useful for serialization,
but we may want separate methods to expose the raw points, in order to
more efficiently aggregate keys and signatures.

There is an outstanding issue on the blst repository about compatibility
with go1.24, so the dependency on blst means that for now, using BLS
means sticking with go1.23.
  • Loading branch information
mark-rushakoff committed Dec 19, 2024
1 parent 0f9c02c commit 933a4cf
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 0 deletions.
166 changes: 166 additions & 0 deletions gcrypto/gbls/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 simply wraps a blst.P2Affine, for now.
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/gbls/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/gbls/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))
}
14 changes: 14 additions & 0 deletions gcrypto/gbls/gblsminsig/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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.
//
// 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/neilotoole/slogt v1.1.0
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/supranational/blst v0.3.13
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
golang.org/x/crypto v0.27.0
golang.org/x/tools v0.22.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk=
github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
Expand Down

0 comments on commit 933a4cf

Please sign in to comment.