mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
Compare round-2 MACs in constant time
SignRound2Preprocess checked a peer-supplied MAC with bytes.Equal, whose early return on the first differing byte leaks the matching-prefix length to a peer probing MACs. Compare with subtle.ConstantTimeCompare; the MAC length is fixed and public. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package sign
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/corona/primitives"
|
||||
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
"github.com/luxfi/lattice/v7/utils/structs"
|
||||
)
|
||||
|
||||
// TestMACComparisonIsConstantTime scans the Round-2 preprocess for the tag
|
||||
// comparison and asserts it goes through crypto/subtle.
|
||||
//
|
||||
// The MAC on the left of that comparison arrives from a peer; the MAC on the
|
||||
// right is computed under a key only the pair shares. bytes.Equal returns on
|
||||
// the first differing byte, so the time it takes tells the peer how many
|
||||
// leading bytes of its guess were right — a forgery oracle over the pairwise
|
||||
// key. isZero32, ten lines above, already uses subtle for a value no
|
||||
// adversary supplies; the adversary-supplied one is the comparison that has
|
||||
// to be branch-free.
|
||||
func TestMACComparisonIsConstantTime(t *testing.T) {
|
||||
fn := parseFunc(t, "sign.go", "SignRound2Preprocess")
|
||||
|
||||
var variable []string
|
||||
ast.Inspect(fn, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if name := qualifiedCallee(call); name == "bytes.Equal" || name == "bytes.Compare" {
|
||||
variable = append(variable, name)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for _, name := range variable {
|
||||
t.Errorf("SignRound2Preprocess compares a peer-supplied MAC with %s; "+
|
||||
"use subtle.ConstantTimeCompare so the comparison does not leak the "+
|
||||
"length of the matching prefix", name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignRound2PreprocessRejectsForgedMAC is the behavioural companion: a tag
|
||||
// that was not produced under the pairwise key must fail the gate whatever it
|
||||
// looks like — wrong bytes, truncated, extended, absent.
|
||||
func TestSignRound2PreprocessRejectsForgedMAC(t *testing.T) {
|
||||
party, A, T := newReuseTestParty(t)
|
||||
|
||||
D, macs, err := party.SignRound1(A, 1, []byte("prf"), T)
|
||||
if err != nil {
|
||||
t.Fatalf("SignRound1: %v", err)
|
||||
}
|
||||
_ = macs
|
||||
|
||||
// Every peer ships the same D; only the tags they authenticate it with
|
||||
// vary. Party 0 verifies the tags addressed to it.
|
||||
honest := func() map[int]map[int][]byte {
|
||||
out := make(map[int]map[int][]byte, len(T))
|
||||
for _, j := range T {
|
||||
if j == party.ID {
|
||||
continue
|
||||
}
|
||||
out[j] = map[int][]byte{
|
||||
party.ID: primitives.GenerateMAC(party.Suite, D, party.MACKeys[j], party.ID, 1, T, j, true),
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
dmap := make(map[int]structs.Matrix[ring.Poly], len(T))
|
||||
for _, j := range T {
|
||||
dmap[j] = D
|
||||
}
|
||||
b := structs.Vector[ring.Poly](nil)
|
||||
|
||||
if ok, _, _ := party.SignRound2Preprocess(A, b, dmap, honest(), 1, T); !ok {
|
||||
t.Fatal("honest tags rejected — the positive control does not hold, the negatives below prove nothing")
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
spoil func([]byte) []byte
|
||||
}{
|
||||
{"flipped first byte", func(m []byte) []byte { out := append([]byte(nil), m...); out[0] ^= 1; return out }},
|
||||
{"flipped last byte", func(m []byte) []byte { out := append([]byte(nil), m...); out[len(out)-1] ^= 1; return out }},
|
||||
{"truncated", func(m []byte) []byte { return m[:len(m)-1] }},
|
||||
{"extended", func(m []byte) []byte { return append(append([]byte(nil), m...), 0) }},
|
||||
{"empty", func(m []byte) []byte { return []byte{} }},
|
||||
{"absent", func(m []byte) []byte { return nil }},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
macs := honest()
|
||||
for j := range macs {
|
||||
macs[j][party.ID] = c.spoil(macs[j][party.ID])
|
||||
}
|
||||
if ok, _, _ := party.SignRound2Preprocess(A, b, dmap, macs, 1, T); ok {
|
||||
t.Errorf("a %s tag passed the round-2 gate", c.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parseFunc returns the named top-level function declaration from a source
|
||||
// file of this package. It fails loudly when the name is absent so a rename
|
||||
// cannot turn a scan into a vacuous pass.
|
||||
func parseFunc(t *testing.T, file, name string) *ast.FuncDecl {
|
||||
t.Helper()
|
||||
f, err := parser.ParseFile(token.NewFileSet(), file, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if ok && fn.Name.Name == name {
|
||||
return fn
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s: no function named %s — the scan has nothing to look at", file, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// qualifiedCallee renders a call target as "pkg.Func", or "" when the target
|
||||
// is not a package-qualified identifier.
|
||||
func qualifiedCallee(call *ast.CallExpr) string {
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
pkg, ok := sel.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return pkg.Name + "." + sel.Sel.Name
|
||||
}
|
||||
+3
-2
@@ -1,7 +1,6 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
@@ -262,7 +261,9 @@ func (party *Party) SignRound2Preprocess(A structs.Matrix[ring.Poly], b structs.
|
||||
if j != party.ID {
|
||||
MAC := MACs[j][party.ID]
|
||||
expectedMAC := primitives.GenerateMAC(party.Suite, D[j], party.MACKeys[j], party.ID, sid, T, j, true)
|
||||
if !bytes.Equal(MAC, expectedMAC) {
|
||||
// Constant-time so a peer probing MACs cannot learn the matching prefix
|
||||
// length from the comparison's timing. MAC length is fixed and public.
|
||||
if subtle.ConstantTimeCompare(MAC, expectedMAC) != 1 {
|
||||
return false, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user