Files
zeekay 2c63fd34fc lens: classical curve threshold kernel — sister to Pulsar
Lens is the curve-based threshold signing kernel for Lux. It is the
classical analogue of Pulsar (the lattice kernel at github.com/luxfi/pulsar).
Both kernels share the same lifecycle shape — Bootstrap → Reshare* →
Reanchor — and the same activation circuit-breaker contract. They differ
only in the math field: Pulsar over R_q (Module-LWE), Lens over a
prime-order curve group (discrete log).

Curves shipped: Ed25519, secp256k1, Ristretto255. Each has a
deterministically-derived secondary Pedersen generator H from a
Lens-specific nothing-up-my-sleeve tag.

Sign math: FROST 2-round Schnorr threshold signing per RFC 9591
(Komlo-Goldberg 2020). Verifier checks z·G ?= R + c·X under the
unchanged group public key.

Reshare math: HJKY97 zero-poly Refresh + Desmedt-Jajodia ReshareToNewSet
over the curve scalar field. Master secret s is preserved; only the
share distribution rotates per epoch.

Activation cert message format: QUASAR-LENS-ACTIVATE-v1, byte-equivalent
shape to QUASAR-PULSAR-ACTIVATE-v1.

Hash suite: HashSuite interface with Lens-SHA3 (production, cSHAKE256 /
KMAC256 / TupleHash256) and Lens-BLAKE3 (legacy) profiles.

DKG: distributed FROST DKG with Feldman commitments. No trusted dealer
required at genesis if the chain prefers proper distributed DKG.

KAT oracles: cmd/reshare_oracle and cmd/dkg_oracle emit deterministic
JSON vectors for cross-language byte-equality validation against a
future luxcpp/crypto/lens/ port.

Tests: full unit + integration coverage on every shipped curve. 60
unique tests, 0 failures.
2026-03-03 12:00:00 -08:00

190 lines
5.1 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reshare
// Pairwise authenticated key-exchange material for VSR.
//
// The Reshare and Refresh protocols require:
//
// 1. Authenticated, encrypted point-to-point channels OLD → NEW for
// private share delivery. The authentication MUST be tied to each
// party's wire-identity key (Ed25519 / ML-DSA-65) so a passive
// observer cannot spoof a delivery.
//
// 2. Per-pair PRF seeds and MAC keys for the post-resharing signing
// epoch. These derive from a Diffie-Hellman shared secret that
// each pair establishes via authenticated KEX. After the share is
// delivered, the PRF/MAC material is derived locally on each side
// via DeriveSeeds / DeriveMACKeys.
//
// We use X25519 + Ed25519 here as the kernel KEX. The auth_kex_ij is
// derived via a transcript-bound mix of the X25519 output produced by
// the canonical Lens HashSuite (cSHAKE256 under Lens-SHA3, BLAKE3
// under the legacy suite). For the hybrid post-quantum mode, swap
// X25519 for ML-KEM-768 + X25519 — out of scope for this kernel.
import (
"crypto/ed25519"
"errors"
"fmt"
"golang.org/x/crypto/curve25519"
"github.com/luxfi/lens/hash"
)
// PairwiseKeyMaterial holds the X25519 keys + signed ephemerals that
// together produce auth_kex_ij. One instance per pair (canonicalized
// by the smaller party ID first).
type PairwiseKeyMaterial struct {
PartyI int
PartyJ int
AuthKex []byte
TranscriptHash [32]byte
}
// X25519Pair runs an X25519 key exchange and returns the 32-byte
// shared secret.
func X25519Pair(privA, pubB []byte) ([]byte, error) {
if len(privA) != 32 {
return nil, fmt.Errorf("reshare: X25519 private key must be 32 bytes, got %d", len(privA))
}
if len(pubB) != 32 {
return nil, fmt.Errorf("reshare: X25519 public key must be 32 bytes, got %d", len(pubB))
}
shared, err := curve25519.X25519(privA, pubB)
if err != nil {
return nil, fmt.Errorf("reshare: X25519 failed: %w", err)
}
allZero := true
for _, b := range shared {
if b != 0 {
allZero = false
break
}
}
if allZero {
return nil, errors.New("reshare: X25519 produced all-zero shared secret (low-order point)")
}
return shared, nil
}
// AuthenticatedKex runs the pairwise authenticated key exchange and
// returns the auth_kex_ij value. The signed ephemeral protects against
// active man-in-the-middle.
//
// suite=nil resolves to the production default (Lens-SHA3).
func AuthenticatedKex(
privIEph []byte,
pubJEph []byte,
sigJEph []byte,
jStaticKey ed25519.PublicKey,
transcriptHash [32]byte,
suite hash.HashSuite,
) ([]byte, error) {
signedMsg := append([]byte("lens.reshare.kex-bind.v1"), transcriptHash[:]...)
signedMsg = append(signedMsg, pubJEph...)
if !ed25519.Verify(jStaticKey, signedMsg, sigJEph) {
return nil, errors.New("reshare: peer ephemeral signature invalid")
}
shared, err := X25519Pair(privIEph, pubJEph)
if err != nil {
return nil, err
}
s := hash.Resolve(suite)
out := s.TranscriptHash(
[]byte("lens.reshare.auth-kex.v1"),
transcriptHash[:],
shared,
)
return out[:], nil
}
// SignEphemeral produces the signature j ships with its ephemeral
// public key.
func SignEphemeral(
priv ed25519.PrivateKey,
pubEph []byte,
transcriptHash [32]byte,
) []byte {
signedMsg := append([]byte("lens.reshare.kex-bind.v1"), transcriptHash[:]...)
signedMsg = append(signedMsg, pubEph...)
return ed25519.Sign(priv, signedMsg)
}
// DeriveSeeds populates the per-pair PRF seed map for a committee of
// size K. suite=nil resolves to the production default.
func DeriveSeeds(
K int,
authKex map[[2]int][]byte,
selfSeeds map[int][]byte,
chainID, groupID []byte,
eraID, generation uint64,
suite hash.HashSuite,
outLen int,
) (map[[2]int][]byte, error) {
out := make(map[[2]int][]byte, K*K)
for i := 0; i < K; i++ {
for j := i; j < K; j++ {
pair := [2]int{i, j}
var keyMat []byte
if i == j {
keyMat = selfSeeds[i]
if len(keyMat) == 0 {
return nil, fmt.Errorf("reshare: missing self-seed for party %d", i)
}
} else {
keyMat = authKex[pair]
if len(keyMat) == 0 {
return nil, fmt.Errorf("reshare: missing auth_kex for pair (%d, %d)", i, j)
}
}
out[pair] = KDFOutput(
suite,
"lens.reshare.prf-seed.v1",
keyMat,
chainID, groupID,
eraID, generation,
i, j,
outLen,
)
}
}
return out, nil
}
// DeriveMACKeys mirrors DeriveSeeds with the "lens.reshare.mac-key.v1"
// tag and only off-diagonal entries.
func DeriveMACKeys(
K int,
authKex map[[2]int][]byte,
chainID, groupID []byte,
eraID, generation uint64,
suite hash.HashSuite,
outLen int,
) (map[[2]int][]byte, error) {
out := make(map[[2]int][]byte, K*(K-1)/2)
for i := 0; i < K; i++ {
for j := i + 1; j < K; j++ {
pair := [2]int{i, j}
keyMat := authKex[pair]
if len(keyMat) == 0 {
return nil, fmt.Errorf("reshare: missing auth_kex for pair (%d, %d)", i, j)
}
out[pair] = KDFOutput(
suite,
"lens.reshare.mac-key.v1",
keyMat,
chainID, groupID,
eraID, generation,
i, j,
outLen,
)
}
}
return out, nil
}