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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ bootstrap-linux-arm64.sum.txt

# For SKD debugging while building images
spike-sdk-go

# WSL
*:Zone.Identifier
7 changes: 7 additions & 0 deletions app/bootstrap/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/spiffe/spike-sdk-go/config/env"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/security/mem"
"github.com/spiffe/spike/app/bootstrap/internal/state"

"github.com/spiffe/spike/app/bootstrap/internal/lifecycle"
"github.com/spiffe/spike/app/bootstrap/internal/net"
Expand Down Expand Up @@ -104,6 +106,11 @@ func main() {
// Retries verification until successful.
net.VerifyInitialization(ctx, api)

// Clear the seed after use.
state.LockRootKeySeed()
defer state.UnlockRootKeySeed()
mem.ClearRawBytes(state.RootKeySeedNoLock())

// Bootstrap verification is complete. Mark the bootstrap as "done".

// Mark completion in Kubernetes
Expand Down
10 changes: 8 additions & 2 deletions app/bootstrap/internal/net/broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/spiffe/go-spiffe/v2/workloadapi"
spike "github.com/spiffe/spike-sdk-go/api"
"github.com/spiffe/spike-sdk-go/config/env"
"github.com/spiffe/spike-sdk-go/crypto"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
"github.com/spiffe/spike-sdk-go/retry"
Expand Down Expand Up @@ -69,9 +70,12 @@ func BroadcastKeepers(ctx context.Context, api *spike.API) {
return
}

state.LockRootKeySeed()
defer state.UnlockRootKeySeed()
// RootShares() generates the root key and splits it into shares.
// It enforces single-call semantics and will terminate if called again.
rs := state.RootShares()
rks := state.RootKeySeedNoLock()
rs := crypto.RootShares(rks)

timeout := env.BootstrapKeeperTimeoutVal()
maxRetries := env.BootstrapKeeperMaxRetriesVal()
Expand Down Expand Up @@ -174,8 +178,10 @@ func VerifyInitialization(ctx context.Context, api *spike.API) {
}
randomText := hex.EncodeToString(randomBytes)

state.LockRootKeySeed()
// Encrypt the random text with the root key
rootKey := state.RootKey()
rootKey := state.RootKeySeed()
defer state.UnlockRootKeySeed()
block, aesErr := aes.NewCipher(rootKey[:])
if aesErr != nil {
failErr := sdkErrors.ErrCryptoFailedToCreateCipher.Wrap(aesErr)
Expand Down
5 changes: 0 additions & 5 deletions app/bootstrap/internal/state/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,4 @@ var (
rootKeySeed [crypto.AES256KeySize]byte
// rootKeySeedMu provides mutual exclusion for access to the root key seed.
rootKeySeedMu sync.RWMutex

// rootSharesGenerated tracks whether RootShares() has been called.
rootSharesGenerated bool
// rootSharesGeneratedMu protects the rootSharesGenerated flag.
rootSharesGeneratedMu sync.Mutex
)
106 changes: 32 additions & 74 deletions app/bootstrap/internal/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,101 +5,59 @@
package state

import (
"crypto/rand"
"fmt"
"strconv"

"github.com/cloudflare/circl/group"
shamir "github.com/cloudflare/circl/secretsharing"
"github.com/spiffe/spike-sdk-go/config/env"
"github.com/spiffe/spike-sdk-go/crypto"
sdkErrors "github.com/spiffe/spike-sdk-go/errors"
"github.com/spiffe/spike-sdk-go/log"
)

// RootShares generates a set of Shamir secret shares from a cryptographically
// secure random root key. It creates a 32-byte random seed, uses it to generate
// a root secret on the P256 elliptic curve group, and splits it into n shares
// using Shamir's Secret Sharing scheme with threshold t. The threshold t is
// set to (ShamirThreshold - 1), meaning t+1 shares are required for
// reconstruction. A deterministic reader seeded with the root key is used to
// ensure identical share generation across restarts, which is critical for
// synchronization after crashes. The function verifies that the generated
// shares can reconstruct the original secret before returning.
//
// Security behavior:
// The application will crash (via log.FatalErr) if:
// - Called more than once per process (would generate different root keys)
// - Random number generation fails
// - Root secret unmarshaling fails
// - Share reconstruction verification fails
//
// Returns:
// - []shamir.Share: The generated Shamir secret shares
func RootShares() []shamir.Share {
const fName = "rootShares"

// Ensure this function is only called once per process.
rootSharesGeneratedMu.Lock()
if rootSharesGenerated {
failErr := sdkErrors.ErrStateIntegrityCheck.Clone()
failErr.Msg = "RootShares() called more than once"
log.FatalErr(fName, *failErr)
}
rootSharesGenerated = true
rootSharesGeneratedMu.Unlock()

rootKeySeedMu.Lock()
defer rootKeySeedMu.Unlock()

if _, err := rand.Read(rootKeySeed[:]); err != nil {
failErr := sdkErrors.ErrCryptoRandomGenerationFailed.Wrap(err)
log.FatalErr(fName, *failErr)
}

// Initialize parameters
g := group.P256
t := uint(env.ShamirThresholdVal() - 1) // Need t+1 shares to reconstruct
n := uint(env.ShamirSharesVal()) // Total number of shares

// Create a secret from our 32-byte key:
rootSecret := g.NewScalar()
if err := rootSecret.UnmarshalBinary(rootKeySeed[:]); err != nil {
failErr := sdkErrors.ErrDataUnmarshalFailure.Wrap(err)
log.FatalErr(fName, *failErr)
}

// To compute identical shares, we need an identical seed for the random
// reader. Using `finalKey` for seed is secure because Shamir Secret Sharing
// algorithm's security does not depend on the random seed; it depends on
// the shards being securely kept secret.
// If we use `random.Read` instead, then synchronizing shards after Nexus
// crashes will be cumbersome and prone to edge-case failures.
reader := crypto.NewDeterministicReader(rootKeySeed[:])
ss := shamir.New(reader, t, rootSecret)

computedShares := ss.Share(n)

// Verify the generated shares can reconstruct the original secret.
// This crashes via log.FatalErr if reconstruction fails.
crypto.VerifyShamirReconstruction(rootSecret, computedShares)

return computedShares
}

// RootKey returns a pointer to the root key seed used for encryption.
// RootKeySeed returns a pointer to the root key seed used for encryption.
// This key is generated when RootShares() is called and persists in memory
// for the duration of the bootstrap process. This function acquires a read
// lock to ensure thread-safe access to the root key seed.
//
// Returns:
// - *[32]byte: Pointer to the root key seed
func RootKey() *[crypto.AES256KeySize]byte {
func RootKeySeed() *[crypto.AES256KeySize]byte {
rootKeySeedMu.RLock()
defer rootKeySeedMu.RUnlock()
return &rootKeySeed
}

// RootKeySeedNoLock returns a pointer to the root key seed without acquiring
// any lock. The caller must hold the lock via LockRootKeySeed before calling
// this function and release it via UnlockRootKeySeed when done.
//
// This function exists to support patterns where the caller needs to perform
// multiple operations on the root key seed atomically, or when using defer
// in a loop would cause resource leaks.
//
// Returns:
// - *[32]byte: Pointer to the root key seed
func RootKeySeedNoLock() *[crypto.AES256KeySize]byte {
return &rootKeySeed
}

// LockRootKeySeed acquires an exclusive write lock on the root key seed.
// This must be paired with UnlockRootKeySeed to release the lock.
//
// Use this in combination with RootKeySeedNoLock when you need explicit lock
// control, such as avoiding defer in loops or performing multiple operations
// atomically.
func LockRootKeySeed() {
rootKeySeedMu.Lock()
}

// UnlockRootKeySeed releases the exclusive write lock on the root key seed.
// This must be called after LockRootKeySeed to avoid deadlocks.
func UnlockRootKeySeed() {
rootKeySeedMu.Unlock()
}

// KeeperShare finds and returns the secret share corresponding to a specific
// Keeper ID. It searches through the provided root shares to locate the share
// with an ID matching the given keeperID (converted from string to integer).
Expand Down
Loading
Loading