Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions internal/util/random.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,15 @@ func RandomHexWithFallback(n int) string {
return s
}

// RandomBigInt returns a cryptographically random non-negative integer with
// the given bit width. The result is guaranteed to be strictly positive (≥ 1).
// This centralises crypto/rand.Int usage for callers that need a *big.Int
// (e.g. X.509 certificate serial numbers).
func RandomBigInt(bits uint) (*big.Int, error) {
// randomBigIntFromRandFunc returns a cryptographically random non-negative integer
// with the given bit width using the provided rand function. It is the testable
// core of RandomBigInt.
func randomBigIntFromRandFunc(bits uint, randFn func(io.Reader, *big.Int) (*big.Int, error)) (*big.Int, error) {
if bits == 0 {
return nil, fmt.Errorf("failed to generate random big.Int: bits must be > 0")
}
max := new(big.Int).Lsh(big.NewInt(1), bits)
n, err := rand.Int(rand.Reader, max)
n, err := randFn(rand.Reader, max)
if err != nil {
return nil, fmt.Errorf("failed to generate random big.Int(%d bits): %w", bits, err)
}
Expand All @@ -83,3 +82,11 @@ func RandomBigInt(bits uint) (*big.Int, error) {
}
return n, nil
}

// RandomBigInt returns a cryptographically random non-negative integer with
// the given bit width. The result is guaranteed to be strictly positive (≥ 1).
// This centralises crypto/rand.Int usage for callers that need a *big.Int
// (e.g. X.509 certificate serial numbers).
func RandomBigInt(bits uint) (*big.Int, error) {
return randomBigIntFromRandFunc(bits, rand.Int)
}
47 changes: 47 additions & 0 deletions internal/util/random_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"io"
"math/big"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -351,3 +352,49 @@ func TestRandomBigInt(t *testing.T) {
assert.Contains(t, err.Error(), "bits must be > 0")
})
}

// TestRandomBigIntFromRandFunc covers the internal randomBigIntFromRandFunc helper,
// including the rarely-exercised zero-result normalization branch.
func TestRandomBigIntFromRandFunc(t *testing.T) {
t.Run("zero bits returns error", func(t *testing.T) {
called := false
_, err := randomBigIntFromRandFunc(0, func(_ io.Reader, _ *big.Int) (*big.Int, error) {
called = true
return big.NewInt(1), nil
})
require.Error(t, err)
assert.Contains(t, err.Error(), "bits must be > 0")
assert.False(t, called, "rand function should not be called when bits == 0")
})

t.Run("rand function error is wrapped and returned", func(t *testing.T) {
randErr := errors.New("simulated rand failure")
_, err := randomBigIntFromRandFunc(128, func(_ io.Reader, _ *big.Int) (*big.Int, error) {
return nil, randErr
})
require.Error(t, err)
assert.ErrorIs(t, err, randErr)
assert.Contains(t, err.Error(), "128 bits")
})

t.Run("normal positive result is returned unchanged", func(t *testing.T) {
want := big.NewInt(42)
got, err := randomBigIntFromRandFunc(128, func(_ io.Reader, _ *big.Int) (*big.Int, error) {
return new(big.Int).Set(want), nil
})
require.NoError(t, err)
assert.Equal(t, 0, got.Cmp(want), "positive result should be returned as-is")
})

t.Run("zero result is normalized to 1", func(t *testing.T) {
// This covers the astronomically rare defensive branch in RandomBigInt where
// crypto/rand.Int returns zero. We exercise it by injecting a stub that always
// returns zero.
got, err := randomBigIntFromRandFunc(128, func(_ io.Reader, _ *big.Int) (*big.Int, error) {
return big.NewInt(0), nil
})
require.NoError(t, err)
assert.Equal(t, int64(1), got.Int64(), "zero result should be normalized to 1")
assert.Positive(t, got.Sign(), "normalized result should be strictly positive")
})
}
Loading