mirror of
https://github.com/luxfi/dkg.git
synced 2026-10-30 16:58:45 +00:00
transcript/: canonical byte-stable MPC transcript. SP 800-185
cSHAKE256/KMAC256 + encoders ported verbatim from the KAT-pinned pulsar
reference. Transcript is append-only with TupleHash256 framing so the
chain-committed TranscriptHash can never suffer boundary confusion.
channel/: authenticated message layer (ML-KEM-768 sealed + ML-DSA-65
signed). TWO structural security fixes over pulsar v0.1:
- SHARE-ONLY ENVELOPES: Seal carries ONLY the per-recipient Shamir
share, never the full contribution c_i. The v0.1 leak (every opener
learned a full contribution => the master secret) is closed by
construction; TestEnvelope_ShareOnly pins sealed size = share+tag.
- COMMIT-THEN-REVEAL (component 8): commit.go binds each party's
contribution before it sees others', killing last-mover key bias.
sign.go authenticates broadcasts/complaints under mandatory context
separation; DirectoryVerifier adapts the identity directory for blame.
Tests: round-trip, share-only regression, tamper/context/dealer
rejection, deterministic encap, context-separated sign/verify,
commit binding+hiding. circl pinned v1.6.3 (consumer-aligned).
71 lines
2.5 KiB
Go
71 lines
2.5 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package channel
|
|
|
|
import (
|
|
"io"
|
|
|
|
"github.com/luxfi/dkg/transcript"
|
|
)
|
|
|
|
// commit.go — commit-then-reveal (DESIGN component 8: last-mover bias control).
|
|
//
|
|
// In a dealerless DKG the joint key is the sum of every party's contribution.
|
|
// A rushing adversary who learns the honest parties' contributions before
|
|
// fixing its own can bias the joint output (choose c_adv so the sum lands in a
|
|
// chosen set). Commit-then-reveal removes that advantage: every party first
|
|
// broadcasts a hiding, binding commitment to its contribution; only after all
|
|
// commitments are collected does anyone reveal. The adversary must commit
|
|
// before it sees anything, so its contribution is independent of the honest
|
|
// parties' — the joint key is unbiased given one honest contributor.
|
|
//
|
|
// The commitment is a cSHAKE256 hash of (payload ‖ nonce). Hiding: the 32-byte
|
|
// nonce makes the commitment indistinguishable from random without the opening
|
|
// (cSHAKE256 modelled as a random oracle). Binding: finding a second
|
|
// (payload', nonce') for the same commitment is a cSHAKE256 collision.
|
|
|
|
// commitTag is the SP 800-185 customization for commitments.
|
|
const commitTag = "LUX-DKG-COMMIT-REVEAL-V1"
|
|
|
|
// Commitment is a 32-byte hiding, binding commitment to a contribution.
|
|
type Commitment [32]byte
|
|
|
|
// Opening is the reveal half: the committed payload and its nonce.
|
|
type Opening struct {
|
|
Payload []byte
|
|
Nonce [32]byte
|
|
}
|
|
|
|
// Commit returns a commitment to payload under nonce. The nonce MUST be fresh
|
|
// and secret until reveal (use NewNonce). Binds (payload ‖ nonce) so the
|
|
// payload boundary is unambiguous via TupleHash framing.
|
|
func Commit(payload []byte, nonce [32]byte) Commitment {
|
|
t := transcript.NewWithDomain(transcript.FuncName, commitTag)
|
|
t.Append("payload", payload)
|
|
t.Append("nonce", nonce[:])
|
|
return Commitment(t.Hash())
|
|
}
|
|
|
|
// CommitWith builds the commitment from an Opening.
|
|
func CommitWith(o Opening) Commitment { return Commit(o.Payload, o.Nonce) }
|
|
|
|
// VerifyOpening verifies that o is a valid opening of c, in constant time over
|
|
// the 32-byte commitment comparison.
|
|
func VerifyOpening(c Commitment, o Opening) bool {
|
|
got := Commit(o.Payload, o.Nonce)
|
|
return ctEqual(got[:], c[:])
|
|
}
|
|
|
|
// NewNonce draws a fresh 32-byte commitment nonce from rng.
|
|
func NewNonce(rng io.Reader) ([32]byte, error) {
|
|
var n [32]byte
|
|
if rng == nil {
|
|
return n, ErrShortRand
|
|
}
|
|
if _, err := io.ReadFull(rng, n[:]); err != nil {
|
|
return n, ErrShortRand
|
|
}
|
|
return n, nil
|
|
}
|