-
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 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
1 parent
4eefb14
commit 68b9937
Showing
5 changed files
with
276 additions
and
0 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 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 | ||
} |
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/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)) | ||
} |
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,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 |
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
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