mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
Bind authorization scope on the strict-PQ order path
The classical SignedOrder binds ChainID, NetworkID and Expiry so a captured order cannot replay onto another chain or after it expires. The strict-PQ order carried none of them, so an ML-DSA-signed order was good on every chain forever — the exact replay the classical path already closed. Mirror the three scope fields onto SignedOrderPQ and fold them into the shared SigningHash, so one digest binds the same scope under either scheme. Mutating any scope field now breaks the signature (regression test). Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -50,6 +50,14 @@ type SignedOrderPQ struct {
|
||||
// the signature, so a malicious party cannot substitute their
|
||||
// own pubkey on someone else's order.
|
||||
Sender common.Address
|
||||
// ChainID, NetworkID and Expiry are the authorization scope,
|
||||
// mirroring SignedOrder so the shared SigningHash binds the
|
||||
// same chain, network and deadline for a strict-PQ order as for
|
||||
// a classical one — a captured order does not replay onto
|
||||
// another chain or after it expires under either scheme.
|
||||
ChainID [32]byte
|
||||
NetworkID uint32
|
||||
Expiry uint64
|
||||
}
|
||||
|
||||
// HasPQEvidence implements pq.PQEvidencer. A non-nil order with a
|
||||
@@ -100,7 +108,13 @@ func VerifyOrderPQ(order *SignedOrderPQ) error {
|
||||
// across classical + strict-PQ paths — a client can compute
|
||||
// one digest and sign it with whichever scheme the chain
|
||||
// requires.
|
||||
asClassical := SignedOrder{Order: order.Order, Sender: order.Sender}
|
||||
asClassical := SignedOrder{
|
||||
Order: order.Order,
|
||||
Sender: order.Sender,
|
||||
ChainID: order.ChainID,
|
||||
NetworkID: order.NetworkID,
|
||||
Expiry: order.Expiry,
|
||||
}
|
||||
hash, err := asClassical.SigningHash()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lx/dex: SigningHash: %w", err)
|
||||
|
||||
@@ -138,3 +138,43 @@ func TestVerifyOrderForMode_StrictPQAcceptsPQ(t *testing.T) {
|
||||
t.Fatalf("VerifyOrderForMode(StrictPQ, *SignedOrderPQ): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifyOrderPQ_BindsAuthorizationScope pins that the strict-PQ path binds
|
||||
// the same authorization scope the classical path does: a captured order does
|
||||
// not replay onto another chain or network, or after it expires. Mutating any
|
||||
// scope field breaks the ML-DSA signature because VerifyOrderPQ folds ChainID,
|
||||
// NetworkID and Expiry into the shared SigningHash.
|
||||
func TestVerifyOrderPQ_BindsAuthorizationScope(t *testing.T) {
|
||||
priv, pubBytes, order := signingMaterial(t)
|
||||
sender := AddressFromMLDSAPubKey(pubBytes)
|
||||
chain := [32]byte{0x11, 0x22, 0x33}
|
||||
|
||||
signed := SignedOrder{Order: order, Sender: sender, ChainID: chain, NetworkID: 7, Expiry: 1000}
|
||||
hash, err := signed.SigningHash()
|
||||
if err != nil {
|
||||
t.Fatalf("SigningHash: %v", err)
|
||||
}
|
||||
sig, err := priv.Sign(rand.Reader, hash[:], nil)
|
||||
if err != nil {
|
||||
t.Fatalf("mldsa sign: %v", err)
|
||||
}
|
||||
base := SignedOrderPQ{
|
||||
Order: order, Sig: sig, PubKey: pubBytes, Sender: sender,
|
||||
ChainID: chain, NetworkID: 7, Expiry: 1000,
|
||||
}
|
||||
if err := VerifyOrderPQ(&base); err != nil {
|
||||
t.Fatalf("a scoped PQ order must verify under its own scope: %v", err)
|
||||
}
|
||||
|
||||
for name, mutate := range map[string]func(*SignedOrderPQ){
|
||||
"chain": func(o *SignedOrderPQ) { o.ChainID[0] ^= 0xff },
|
||||
"network": func(o *SignedOrderPQ) { o.NetworkID++ },
|
||||
"expiry": func(o *SignedOrderPQ) { o.Expiry++ },
|
||||
} {
|
||||
bad := base
|
||||
mutate(&bad)
|
||||
if err := VerifyOrderPQ(&bad); err == nil {
|
||||
t.Fatalf("mutating the %s scope must break the PQ order signature", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user