mirror of
https://github.com/luxfi/crypto.git
synced 2026-10-30 16:59:13 +00:00
Every secret here is a draw — ML-DSA and ML-KEM keys, TLS material, signing nonces, threshold dealer seeds — and each is exactly as unpredictable as the draw that made it. This is the layer that produces those draws and says whether they are worth anything. A source is bytes plus a verdict on them, and the verdict is the part usually left out. A physical source that has drifted, saturated or stuck still emits bytes of the expected length; a key made from them verifies, signs and behaves correctly in every respect except being unpredictable, and nothing downstream can tell. So Read fails closed, and the two continuous tests of SP 800-90B §4.4 run over every sample: repetition count for a source that has stopped changing, adaptive proportion for one still changing but drawing from far fewer values than it claims. Cutoffs come from the device's own min-entropy claim, so a weaker claim is allowed longer runs rather than being refused for being honest. Nothing is released until a startup run has passed. Sources combine so that adding one cannot weaken the result: predicting the output means predicting every contribution, so one unpredictable source carries the whole combination. That is what makes it safe to put an unproven device into a running deployment — at worst it contributes nothing. The platform generator is always among the sources, because a deployment whose only source is a device it cannot audit has moved its trust rather than removed it. Mixing is separated from reading so the property it exists for can be tested directly. It needs to be: with a fixed draw per source the contributions are always the same length, so a test of injectivity through Combine passes with or without the length prefixes and proves nothing. Against mix, ["ab","c"] and ["a","bc"] collide the moment the prefixes are removed. Lives beside mldsa and mlkem rather than in a module of its own. LP-176 names luxfi/qrng, but a separate module means a separate go.mod, CI, and release cadence for one package whose only callers are its neighbours here. Integration is the claim LP-187 makes: mldsa.GenerateKey(src, …) — adopting a hardware source is supplying a different reader. No consensus change and no fork. A key is generated, signed with and verified in the tests, and refused outright when a source is unhealthy. Co-authored-by: Hanzo Dev <[email protected]>
108 lines
3.7 KiB
Go
108 lines
3.7 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package entropy
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
// combineTag separates this use of SHAKE256 from every other one in Lux. Two
|
|
// constructions that hash the same bytes for different purposes must not be
|
|
// able to produce the same output, or a value drawn for one becomes usable in
|
|
// the other.
|
|
const combineTag = "LUX-ENTROPY-COMBINE-V1"
|
|
|
|
// drawPerSource is how many bytes are taken from each source per read. It is
|
|
// the security parameter's worth: a source contributing at least this much
|
|
// unpredictability saturates the output.
|
|
const drawPerSource = 32
|
|
|
|
// Combine reads from every source and mixes what it gets.
|
|
//
|
|
// The mix is a hash of the concatenation, each contribution preceded by its
|
|
// length, under a tag used nowhere else:
|
|
//
|
|
// SHAKE256( tag || LE64(len(s_1)) || s_1 || ... || LE64(len(s_n)) || s_n )
|
|
//
|
|
// Two properties follow, and both are the point rather than decoration.
|
|
//
|
|
// Adding a source cannot weaken the result. Predicting the output means
|
|
// predicting the hash input, which means predicting every contribution — so
|
|
// one unpredictable source is enough regardless of what the others do. This is
|
|
// what allows a new and unproven device to be added to a running deployment:
|
|
// at worst it contributes nothing, and it can never subtract.
|
|
//
|
|
// The lengths keep the contributions distinct. Concatenated raw, different
|
|
// source sets can produce identical inputs — the last bytes of one and the
|
|
// first of the next are indistinguishable from a single longer contribution —
|
|
// and two deployments would silently derive the same value. Length-prefixing
|
|
// makes the encoding injective, so the input determines the contributions.
|
|
//
|
|
// A source that is unhealthy fails the whole read. That is deliberate: a
|
|
// deployment that has been told a source is broken should stop and be fixed,
|
|
// not continue on the remainder while believing it has more sources than it
|
|
// does.
|
|
func Combine(sources ...Source) Source {
|
|
return &combined{sources: sources}
|
|
}
|
|
|
|
type combined struct {
|
|
sources []Source
|
|
}
|
|
|
|
func (c *combined) Name() string { return "combined" }
|
|
|
|
func (c *combined) Health() error {
|
|
if len(c.sources) == 0 {
|
|
return fmt.Errorf("%w: no sources", ErrUnhealthy)
|
|
}
|
|
for i, s := range c.sources {
|
|
if err := s.Health(); err != nil {
|
|
return fmt.Errorf("%s: %w", nameOf(s, i), err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *combined) Read(p []byte) (int, error) {
|
|
if err := c.Health(); err != nil {
|
|
return 0, err
|
|
}
|
|
parts := make([][]byte, len(c.sources))
|
|
for i, s := range c.sources {
|
|
buf := make([]byte, drawPerSource)
|
|
if _, err := io.ReadFull(s, buf); err != nil {
|
|
return 0, fmt.Errorf("entropy: read %s: %w", nameOf(s, i), err)
|
|
}
|
|
parts[i] = buf
|
|
}
|
|
return io.ReadFull(mix(parts), p)
|
|
}
|
|
|
|
// mix is the combining construction itself, separated from reading so that the
|
|
// property it exists for can be checked directly: distinct contribution lists
|
|
// must give distinct output.
|
|
//
|
|
// Each contribution is preceded by its length. Combine happens to draw a fixed
|
|
// number of bytes from every source, which makes the lengths uniform today —
|
|
// but a construction that is only injective while every input happens to be
|
|
// the same size is one bad day from not being injective at all. The prefix is
|
|
// what makes the encoding injective for any contributions, so this stays true
|
|
// if a source ever contributes a different amount.
|
|
func mix(parts [][]byte) io.Reader {
|
|
h := sha3.NewShake256()
|
|
h.Write([]byte(combineTag))
|
|
var length [8]byte
|
|
for _, part := range parts {
|
|
binary.LittleEndian.PutUint64(length[:], uint64(len(part)))
|
|
h.Write(length[:])
|
|
h.Write(part)
|
|
}
|
|
return h
|
|
}
|