mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
feat: enable Go 1.26 runtime/secret for forward secrecy of key shares
Add GOEXPERIMENT=runtimesecret to Makefile, CI, and Dockerfile builds. Wrap key share loading in all 5 signing session types with withSecretErasure() so stack/register state containing raw share bytes is zeroed after parsing. Build-tagged stub ensures everything compiles without the experiment flag.
This commit is contained in:
@@ -9,6 +9,8 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOEXPERIMENT: runtimesecret
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -294,6 +296,8 @@ jobs:
|
||||
build:
|
||||
runs-on: lux-build-linux-amd64
|
||||
needs: [test, lint, codeql-analysis, sbom]
|
||||
env:
|
||||
GOEXPERIMENT: runtimesecret
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -12,6 +12,7 @@ COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
|
||||
ENV GOEXPERIMENT=runtimesecret
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o mpcd ./cmd/mpcd
|
||||
|
||||
FROM alpine:3.21
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
BIN_DIR := bin
|
||||
|
||||
# Go 1.26 experimental features:
|
||||
# runtimesecret - zeroes stack/register state after secret.Do() for forward secrecy
|
||||
# simd - SIMD intrinsics (amd64/arm64)
|
||||
GOEXPERIMENT ?= runtimesecret,simd
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
@@ -10,27 +15,27 @@ build: lux-mpc lux-mpc-cli lux-mpc-bridge
|
||||
|
||||
# Install lux-mpc (builds and places it in $GOBIN or $GOPATH/bin)
|
||||
lux-mpc:
|
||||
GOWORK=off go build -o lux-mpc ./cmd/lux-mpc
|
||||
GOWORK=off GOEXPERIMENT=$(GOEXPERIMENT) go build -o lux-mpc ./cmd/lux-mpc
|
||||
|
||||
# Install lux-mpc-cli
|
||||
lux-mpc-cli:
|
||||
GOWORK=off go build -o lux-mpc-cli ./cmd/lux-mpc-cli
|
||||
GOWORK=off GOEXPERIMENT=$(GOEXPERIMENT) go build -o lux-mpc-cli ./cmd/lux-mpc-cli
|
||||
|
||||
# Install lux-mpc-bridge (bridge compatibility)
|
||||
lux-mpc-bridge:
|
||||
GOWORK=off go build -o lux-mpc-bridge ./cmd/lux-mpc-bridge 2>/dev/null || true
|
||||
GOWORK=off GOEXPERIMENT=$(GOEXPERIMENT) go build -o lux-mpc-bridge ./cmd/lux-mpc-bridge 2>/dev/null || true
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
go test ./...
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) go test ./...
|
||||
|
||||
# Run tests with verbose output
|
||||
test-verbose:
|
||||
go test -v ./...
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) go test -v ./...
|
||||
|
||||
# Run tests with coverage report
|
||||
test-coverage:
|
||||
go test -v -coverprofile=coverage.out ./...
|
||||
GOEXPERIMENT=$(GOEXPERIMENT) go test -v -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# Run E2E integration tests
|
||||
|
||||
@@ -61,16 +61,24 @@ func newBLSSigningSession(
|
||||
identityStore identity.Store,
|
||||
orgID string,
|
||||
) (*blsSigningSession, error) {
|
||||
// Load config from kvstore - BLS keys are stored with bls: prefix
|
||||
// Load and unmarshal key share inside withSecretErasure so that raw
|
||||
// share bytes on the stack are zeroed after parsing completes.
|
||||
blsKey := fmt.Sprintf("bls:%s", walletID)
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, blsKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get BLS key share: %w", err)
|
||||
}
|
||||
|
||||
config, err := UnmarshalBLSConfig(shareBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal BLS key share: %w", err)
|
||||
var config *blsThreshold.Config
|
||||
var loadErr error
|
||||
withSecretErasure(func() {
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, blsKey)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to get BLS key share: %w", err)
|
||||
return
|
||||
}
|
||||
config, err = UnmarshalBLSConfig(shareBytes)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to unmarshal BLS key share: %w", err)
|
||||
}
|
||||
})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
return &blsSigningSession{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build goexperiment.runtimesecret
|
||||
|
||||
package mpc
|
||||
|
||||
import "runtime/secret"
|
||||
|
||||
// withSecretErasure runs f inside runtime/secret.Do, which ensures that any
|
||||
// temporary stack/register state produced by f is zeroed after f returns.
|
||||
// This provides forward secrecy for key material used during MPC signing.
|
||||
func withSecretErasure(f func()) { secret.Do(f) }
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !goexperiment.runtimesecret
|
||||
|
||||
package mpc
|
||||
|
||||
// withSecretErasure is a no-op stub when runtime/secret is not available.
|
||||
// The signing operation runs identically; secret erasure is simply skipped.
|
||||
func withSecretErasure(f func()) { f() }
|
||||
+16
-10
@@ -62,17 +62,23 @@ func newCGGMP21SigningSession(
|
||||
useBroadcast bool,
|
||||
orgID string,
|
||||
) (*cggmp21SigningSession, error) {
|
||||
// Load config from kvstore with org-scoped fallback
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, walletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key share: %w", err)
|
||||
}
|
||||
|
||||
// Create empty config with the correct group - REQUIRED for unmarshalling
|
||||
// Load and unmarshal key share inside withSecretErasure so that raw
|
||||
// share bytes on the stack are zeroed after parsing completes.
|
||||
config := config.EmptyConfig(curve.Secp256k1{})
|
||||
// Use UnmarshalBinary (CBOR) instead of JSON - required for curve.Curve interface
|
||||
if err := config.UnmarshalBinary(shareBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal key share: %w", err)
|
||||
var loadErr error
|
||||
withSecretErasure(func() {
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, walletID)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to get key share: %w", err)
|
||||
return
|
||||
}
|
||||
// Use UnmarshalBinary (CBOR) instead of JSON - required for curve.Curve interface
|
||||
if err := config.UnmarshalBinary(shareBytes); err != nil {
|
||||
loadErr = fmt.Errorf("failed to unmarshal key share: %w", err)
|
||||
}
|
||||
})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
// Create thread pool
|
||||
|
||||
@@ -64,17 +64,25 @@ func newFROSTSigningSession(
|
||||
useBroadcast bool,
|
||||
orgID string,
|
||||
) (*frostSigningSession, error) {
|
||||
// Load config from kvstore - FROST keys are stored with frost: prefix
|
||||
// Load and unmarshal key share inside withSecretErasure so that raw
|
||||
// share bytes on the stack are zeroed after parsing completes.
|
||||
frostKey := fmt.Sprintf("frost:%s", walletID)
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, frostKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get FROST key share: %w", err)
|
||||
}
|
||||
|
||||
// TaprootConfig is stored as CBOR (to properly preserve crypto types)
|
||||
config, err := UnmarshalFROSTConfig(shareBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal FROST key share: %w", err)
|
||||
var config *frost.TaprootConfig
|
||||
var loadErr error
|
||||
withSecretErasure(func() {
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, frostKey)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to get FROST key share: %w", err)
|
||||
return
|
||||
}
|
||||
// TaprootConfig is stored as CBOR (to properly preserve crypto types)
|
||||
config, err = UnmarshalFROSTConfig(shareBytes)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to unmarshal FROST key share: %w", err)
|
||||
}
|
||||
})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
// BIP-340/Taproot requires 32-byte message hash
|
||||
|
||||
@@ -60,16 +60,24 @@ func newLSSSigningSession(
|
||||
useBroadcast bool,
|
||||
orgID string,
|
||||
) (*lssSigningSession, error) {
|
||||
// Load config from kvstore with org-scoped fallback
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, walletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key share: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal config using CBOR (not JSON) to preserve curve types
|
||||
config, err := UnmarshalLSSConfig(shareBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal key share: %w", err)
|
||||
// Load and unmarshal key share inside withSecretErasure so that raw
|
||||
// share bytes on the stack are zeroed after parsing completes.
|
||||
var config *lssConfig.Config
|
||||
var loadErr error
|
||||
withSecretErasure(func() {
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, walletID)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to get key share: %w", err)
|
||||
return
|
||||
}
|
||||
// Unmarshal config using CBOR (not JSON) to preserve curve types
|
||||
config, err = UnmarshalLSSConfig(shareBytes)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to unmarshal key share: %w", err)
|
||||
}
|
||||
})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
// Normalize message hash to 32 bytes (LSS requires exactly 32 bytes)
|
||||
|
||||
@@ -75,17 +75,25 @@ func newSR25519SigningSession(
|
||||
useBroadcast bool,
|
||||
orgID string,
|
||||
) (*sr25519SigningSession, error) {
|
||||
// Load config from kvstore - SR25519 keys are stored with sr25519: prefix
|
||||
// Load and unmarshal key share inside withSecretErasure so that raw
|
||||
// share bytes on the stack are zeroed after parsing completes.
|
||||
sr25519Key := fmt.Sprintf("sr25519:%s", walletID)
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, sr25519Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get SR25519 key share: %w", err)
|
||||
}
|
||||
|
||||
// Config is stored as CBOR (to properly preserve crypto types)
|
||||
config, err := UnmarshalSR25519Config(shareBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal SR25519 key share: %w", err)
|
||||
var config *frost.Config
|
||||
var loadErr error
|
||||
withSecretErasure(func() {
|
||||
shareBytes, err := GetKeyShareWithFallback(kvstore, orgID, sr25519Key)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to get SR25519 key share: %w", err)
|
||||
return
|
||||
}
|
||||
// Config is stored as CBOR (to properly preserve crypto types)
|
||||
config, err = UnmarshalSR25519Config(shareBytes)
|
||||
if err != nil {
|
||||
loadErr = fmt.Errorf("failed to unmarshal SR25519 key share: %w", err)
|
||||
}
|
||||
})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
// Default signing context
|
||||
|
||||
Reference in New Issue
Block a user