Files
crypto/pq/mldsa/mldsa_cross_test.go
zeekay d753a24a30 crypto/pq/mldsa: export NewKeyFromSeed for all three parameter sets
Unblocks deterministic ML-DSA key derivation from cSHAKE-derived child
seeds (HD wallet derivation, validator identity, DKG round seeds).

mldsa44, mldsa65, mldsa87 each expose:
  NewKeyFromSeed(seed []byte) (PublicKey, PrivateKey, error)

Seed contract:
  len < 32        -> ErrSeedTooShort (FIPS 204 minimum)
  len == 32       -> used verbatim as xi (FIPS 204 5.1)
  len > 32        -> xi = cSHAKE-256(seed, S=per-set customization)

cSHAKE customization strings ("LUX/FIPS204/MLDSA{44,65,87}/seed") provide
parameter-set domain separation: the same oversized parent seed handed to
all three packages yields three independent keypairs (NIST SP 800-185 3.3).

Each subpackage also re-exports GenerateKey / Sign / Verify so consumers
do not have to import the underlying primitive alongside.

Patch-bump.
2026-05-10 19:56:00 -07:00

94 lines
3.3 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mldsa_test
import (
"bytes"
"testing"
"github.com/luxfi/crypto/pq/mldsa/mldsa44"
"github.com/luxfi/crypto/pq/mldsa/mldsa65"
"github.com/luxfi/crypto/pq/mldsa/mldsa87"
)
// TestCrossParameterSet_OversizedSeedDomainSeparation asserts that the
// per-parameter-set cSHAKE customization string actually domain-separates
// mldsa44 / mldsa65 / mldsa87 when the same oversized parent seed is
// re-used across all three.
//
// The expansion rule (see internal/seed) is:
//
// ξ_44 = cSHAKE-256(seed, S="LUX/FIPS204/MLDSA44/seed")
// ξ_65 = cSHAKE-256(seed, S="LUX/FIPS204/MLDSA65/seed")
// ξ_87 = cSHAKE-256(seed, S="LUX/FIPS204/MLDSA87/seed")
//
// The three S strings differ, so ξ_44 ≠ ξ_65 ≠ ξ_87 (NIST SP 800-185 §3.3).
// We can't compare ξ directly (it's internal), so we compare a derived
// signature instead: with deterministic signing and a fixed (msg, ctx),
// the signature is a function of (sk, msg, ctx). If sk differs, the
// signature differs.
//
// Why this matters: a single 64-byte BIP-32 child seed is the typical
// HD-wallet input. If the customization strings collided, the validator
// identity (mldsa65) and any future mldsa87-based protocol would derive
// the same private key from the same parent seed — a key-reuse violation
// of FIPS 204's per-scheme key separation.
func TestCrossParameterSet_OversizedSeedDomainSeparation(t *testing.T) {
parentSeed := bytes.Repeat([]byte{0x5C}, 64) // > SeedSize → triggers cSHAKE
msg := []byte("LUX cross-parameter-set domain separation")
ctx := []byte("LUX/test/mldsa-cross")
pk44, sk44, err := mldsa44.NewKeyFromSeed(parentSeed)
if err != nil {
t.Fatalf("mldsa44 NewKeyFromSeed: %v", err)
}
pk65, sk65, err := mldsa65.NewKeyFromSeed(parentSeed)
if err != nil {
t.Fatalf("mldsa65 NewKeyFromSeed: %v", err)
}
pk87, sk87, err := mldsa87.NewKeyFromSeed(parentSeed)
if err != nil {
t.Fatalf("mldsa87 NewKeyFromSeed: %v", err)
}
// Different parameter sets → different public-key sizes, so the
// byte comparison is unambiguous. Public keys MUST differ in length
// (parameter-set sanity) AND in content (domain separation).
if mldsa44.PublicKeySize == mldsa65.PublicKeySize ||
mldsa65.PublicKeySize == mldsa87.PublicKeySize {
t.Fatal("parameter-set public-key sizes collide (build error)")
}
// Sign with each derived sk and confirm the signature verifies
// under its OWN pk but NOT under the others (cross-set keys must
// not interoperate).
sig44, err := mldsa44.Sign(sk44, msg, ctx, false)
if err != nil {
t.Fatalf("mldsa44 Sign: %v", err)
}
sig65, err := mldsa65.Sign(sk65, msg, ctx, false)
if err != nil {
t.Fatalf("mldsa65 Sign: %v", err)
}
sig87, err := mldsa87.Sign(sk87, msg, ctx, false)
if err != nil {
t.Fatalf("mldsa87 Sign: %v", err)
}
if !mldsa44.Verify(pk44, msg, ctx, sig44) {
t.Fatal("mldsa44: derived keypair fails self-verify")
}
if !mldsa65.Verify(pk65, msg, ctx, sig65) {
t.Fatal("mldsa65: derived keypair fails self-verify")
}
if !mldsa87.Verify(pk87, msg, ctx, sig87) {
t.Fatal("mldsa87: derived keypair fails self-verify")
}
// Length sanity: three signatures of three distinct lengths.
if len(sig44) == len(sig65) || len(sig65) == len(sig87) {
t.Fatal("parameter-set signature sizes collide (build error)")
}
}