mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
bls: reject G2 identity signature in SignatureFromBytes (INFO-4)
Symmetric with the isIdentityG1 pubkey guard in Verify. The canonical
compressed G2 identity (0xc0 || zeros) is a well-formed encoding both
deserializers accepted:
- purego (CIRCL): G2.SetBytes returns at the isInfinity branch BEFORE
IsOnG2 — add an explicit g.IsIdentity() reject.
- blst: SigValidate(false) skipped the infinity check — switch to
SigValidate(true), which is is_inf(p)==false && in_g2(p) (rejects
infinity AND keeps the r-torsion subgroup check; strictly additive).
The identity sig does not forge against a real key, but admitting a
degenerate point into the verifier is an asymmetry worth closing.
TDD: TestSignatureFromBytes_RejectsIdentity — identity sig rejected; a
real sig still round-trips byte-identically and verifies. Proven to fail
without the guard.
This commit is contained in:
+10
@@ -301,6 +301,16 @@ func SignatureFromBytes(sigBytes []byte) (*Signature, error) {
|
||||
if err := g.SetBytes(sigBytes); err != nil {
|
||||
return nil, ErrFailedSignatureDecompress
|
||||
}
|
||||
// Reject the G2 identity (point at infinity) — symmetric with the isIdentityG1
|
||||
// pubkey guard in Verify (INFO-4). CIRCL's SetBytes accepts a well-formed
|
||||
// infinity encoding (0xc0 || zeros) and returns at the isInfinity branch BEFORE
|
||||
// IsOnG2, so without this the identity signature would deserialize cleanly; blst
|
||||
// SigValidate(false) likewise skips the infinity check. The identity sig does
|
||||
// not forge against a real key, but accepting it is an asymmetry with the pubkey
|
||||
// path and admits a degenerate point into the verifier — reject it here.
|
||||
if g.IsIdentity() {
|
||||
return nil, ErrFailedSignatureDecompress
|
||||
}
|
||||
// Store the validated compressed bytes; blssign.Signature is the raw
|
||||
// compressed form and blssign.Verify re-derives the point internally, so we
|
||||
// keep the canonical wire bytes (round-trips through SignatureToBytes).
|
||||
|
||||
+9
-1
@@ -247,7 +247,15 @@ func SignatureFromBytes(sigBytes []byte) (*Signature, error) {
|
||||
return nil, ErrFailedSignatureDecompress
|
||||
}
|
||||
|
||||
if !sig.SigValidate(false) {
|
||||
// SigValidate(true) checks BOTH r-torsion subgroup membership AND rejects the
|
||||
// G2 identity (point at infinity) — go_p2_affine_validate(p, infcheck=true) is
|
||||
// `if is_inf(p) return false; return in_g2(p)`. The prior SigValidate(false)
|
||||
// skipped the infinity check, accepting the identity signature (INFO-4); this
|
||||
// makes the blst path symmetric with the purego SignatureFromBytes identity
|
||||
// reject and with the isIdentityG1 pubkey guard. The identity sig does not forge
|
||||
// against a real key, but admitting a degenerate point into the verifier is an
|
||||
// asymmetry worth closing.
|
||||
if !sig.SigValidate(true) {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// sig_identity_reject_test.go — INFO-4 regression: SignatureFromBytes must reject
|
||||
// the G2 identity (point at infinity), symmetric with the isIdentityG1 pubkey
|
||||
// guard in Verify. The canonical compressed G2 identity is 0xc0 || 95 zero bytes
|
||||
// (compression bit + infinity bit set; all coordinate bytes zero — see blst
|
||||
// e1.c:205 "compressed and infinity bits"). It is a WELL-FORMED encoding that
|
||||
// both deserializers used to accept:
|
||||
// - purego (CIRCL): G2.SetBytes returns at the isInfinity branch BEFORE IsOnG2.
|
||||
// - blst: SigValidate(false) skipped the infinity check.
|
||||
// The fix rejects it on both paths (purego: explicit IsIdentity reject; blst:
|
||||
// SigValidate(true) does is_inf + in_g2). This test is build-tag agnostic so it
|
||||
// runs against whichever implementation is compiled in, asserting BOTH paths
|
||||
// behave identically.
|
||||
package bls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// compressedG2Identity is the canonical compressed encoding of the G2 point at
|
||||
// infinity: 0xc0 (compression|infinity bits) followed by zeros for the whole
|
||||
// SignatureLen-byte field. This is the exact blob INFO-4 was accepting.
|
||||
func compressedG2Identity() []byte {
|
||||
b := make([]byte, SignatureLen)
|
||||
b[0] = 0xc0
|
||||
return b
|
||||
}
|
||||
|
||||
func TestSignatureFromBytes_RejectsIdentity(t *testing.T) {
|
||||
// (1) The canonical compressed G2 identity MUST be rejected. Before the fix
|
||||
// this deserialized cleanly (purego: returned at the infinity branch; blst:
|
||||
// SigValidate(false) skipped the infinity check).
|
||||
if sig, err := SignatureFromBytes(compressedG2Identity()); err == nil {
|
||||
t.Fatalf("INFO-4: SignatureFromBytes accepted the G2 identity (point at infinity); "+
|
||||
"got sig=%v, want rejection", sig)
|
||||
}
|
||||
|
||||
// (2) A REAL signature still round-trips and verifies — the identity reject
|
||||
// must not block valid signatures.
|
||||
sk, err := NewSecretKey()
|
||||
if err != nil {
|
||||
t.Fatalf("NewSecretKey: %v", err)
|
||||
}
|
||||
msg := []byte("info-4 identity reject — real sig must survive")
|
||||
sig, err := sk.Sign(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign: %v", err)
|
||||
}
|
||||
sigBytes := SignatureToBytes(sig)
|
||||
|
||||
roundTripped, err := SignatureFromBytes(sigBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("SignatureFromBytes rejected a VALID signature: %v", err)
|
||||
}
|
||||
if !bytes.Equal(SignatureToBytes(roundTripped), sigBytes) {
|
||||
t.Fatal("valid signature did not round-trip byte-identically through SignatureFromBytes")
|
||||
}
|
||||
if !Verify(sk.PublicKey(), roundTripped, msg) {
|
||||
t.Fatal("round-tripped valid signature failed to verify against its key+message")
|
||||
}
|
||||
|
||||
// (3) A real signature is NOT the identity — sanity that the guard is precise
|
||||
// (it rejects only the degenerate point, not the first byte 0xc0 generically;
|
||||
// real compressed G2 sigs may legitimately have the compression bit set).
|
||||
if bytes.Equal(sigBytes, compressedG2Identity()) {
|
||||
t.Fatal("a real signature must never equal the G2 identity encoding")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user