diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1582147..2de80c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.25.5'] + go-version: ['1.26.1'] cgo: ['0', '1'] steps: @@ -84,6 +84,20 @@ jobs: echo "Running integration tests" go test -v . -count=1 || true + - name: Run secret package tests + env: + CGO_ENABLED: ${{ matrix.cgo }} + run: | + echo "Testing secret package (stub mode)" + go test -v ./secret/... -count=1 + + - name: Run encryption/HPKE tests + env: + CGO_ENABLED: ${{ matrix.cgo }} + run: | + echo "Testing HPKE encryption (Go 1.26 stdlib)" + go test -v ./encryption/... -count=1 + - name: Run benchmarks if: matrix.cgo == '1' env: @@ -92,6 +106,30 @@ jobs: echo "Running performance benchmarks" go test -bench=. -benchmem ./... || true + test-runtimesecret: + name: Test with GOEXPERIMENT=runtimesecret + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.26.1' + + - name: Build with runtimesecret + env: + GOEXPERIMENT: runtimesecret + CGO_ENABLED: '0' + run: go build ./... + + - name: Test with runtimesecret + env: + GOEXPERIMENT: runtimesecret + CGO_ENABLED: '0' + run: go test -v -count=1 ./secret/... ./encryption/... . + security: name: Security Scan runs-on: ubuntu-latest @@ -103,7 +141,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.25.5' + go-version: '1.26.1' - name: Run Gosec Security Scanner uses: securego/gosec@master @@ -123,7 +161,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go-version: ['1.25.5'] + go-version: ['1.26.1'] steps: - name: Checkout code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2dc665..0523759 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,28 +16,28 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.25.5'] - + go-version: ['1.26.1'] + steps: - uses: actions/checkout@v4 - + - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true cache-dependency-path: crypto/go.sum - + - name: Install dependencies run: | cd crypto go mod download - + - name: Run tests run: | cd crypto go test -v -short -coverprofile=coverage.out -covermode=atomic ./... - + - name: Upload coverage uses: codecov/codecov-action@v3 with: diff --git a/bls/bls.go b/bls/bls.go index 0ac10ce..5cb7996 100644 --- a/bls/bls.go +++ b/bls/bls.go @@ -11,6 +11,7 @@ import ( "github.com/cloudflare/circl/ecc/bls12381" blssign "github.com/cloudflare/circl/sign/bls" + "github.com/luxfi/crypto/secret" ) // Domain separation tags - must match the CGO version (blst) exactly @@ -37,17 +38,21 @@ type ( ) func NewSecretKey() (*SecretKey, error) { - ikm := make([]byte, 32) - if _, err := rand.Read(ikm); err != nil { - return nil, err - } - defer clear(ikm) + var result *SecretKey + var keyErr error + secret.Do(func() { + ikm := make([]byte, 32) + rand.Read(ikm) + defer clear(ikm) - sk, err := blssign.KeyGen[blssign.KeyG1SigG2](ikm, nil, nil) - if err != nil { - return nil, err - } - return &SecretKey{sk: sk}, nil + sk, err := blssign.KeyGen[blssign.KeyG1SigG2](ikm, nil, nil) + if err != nil { + keyErr = err + return + } + result = &SecretKey{sk: sk} + }) + return result, keyErr } func SecretKeyToBytes(sk *SecretKey) []byte { @@ -76,11 +81,17 @@ func SecretKeyFromBytes(skBytes []byte) (*SecretKey, error) { if len(skBytes) != SecretKeyLen { return nil, ErrFailedSecretKeyDeserialize } - sk := new(blssign.PrivateKey[blssign.KeyG1SigG2]) - if err := sk.UnmarshalBinary(skBytes); err != nil { - return nil, ErrFailedSecretKeyDeserialize - } - return &SecretKey{sk: sk}, nil + var result *SecretKey + var keyErr error + secret.Do(func() { + sk := new(blssign.PrivateKey[blssign.KeyG1SigG2]) + if err := sk.UnmarshalBinary(skBytes); err != nil { + keyErr = ErrFailedSecretKeyDeserialize + return + } + result = &SecretKey{sk: sk} + }) + return result, keyErr } func (sk *SecretKey) PublicKey() *PublicKey { diff --git a/bls/bls_c.go b/bls/bls_c.go index ce557ec..045ea0b 100644 --- a/bls/bls_c.go +++ b/bls/bls_c.go @@ -10,6 +10,7 @@ import ( "errors" blst "github.com/supranational/blst/bindings/go" + "github.com/luxfi/crypto/secret" ) // Domain separation tags @@ -39,20 +40,14 @@ type ( // NewSecretKey generates a new secret key from the local source of // cryptographically secure randomness. func NewSecretKey() (*SecretKey, error) { - ikm := make([]byte, 32) - _, err := rand.Read(ikm) - if err != nil { - return nil, err - } - - sk := blst.KeyGen(ikm) - - // Clear the ikm - for i := range ikm { - ikm[i] = 0 - } - - return &SecretKey{sk: sk}, nil + var result *SecretKey + secret.Do(func() { + ikm := make([]byte, 32) + rand.Read(ikm) + defer clear(ikm) + result = &SecretKey{sk: blst.KeyGen(ikm)} + }) + return result, nil } // SecretKeyToBytes returns the big-endian format of the secret key. @@ -91,9 +86,13 @@ func SecretKeyFromBytes(skBytes []byte) (*SecretKey, error) { if allZero { return nil, ErrFailedSecretKeyDeserialize } - sk := new(blst.SecretKey) - sk.Deserialize(skBytes) - return &SecretKey{sk: sk}, nil + var result *SecretKey + secret.Do(func() { + sk := new(blst.SecretKey) + sk.Deserialize(skBytes) + result = &SecretKey{sk: sk} + }) + return result, nil } // PublicKey returns the public key associated with the secret key. diff --git a/crypto.go b/crypto.go index 376e9ba..a966aeb 100644 --- a/crypto.go +++ b/crypto.go @@ -31,6 +31,7 @@ import ( "github.com/luxfi/crypto/common" "github.com/luxfi/crypto/rlp" + "github.com/luxfi/crypto/secret" ) // Re-export common types at the package level for convenience @@ -181,7 +182,12 @@ func HexToECDSA(hexkey string) (*ecdsa.PrivateKey, error) { } else if err != nil { return nil, errors.New("invalid hex data for private key") } - return ToECDSA(b) + var key *ecdsa.PrivateKey + secret.Do(func() { + key, err = ToECDSA(b) + clear(b) + }) + return key, err } // LoadECDSA loads a secp256k1 private key from the given file. @@ -204,7 +210,12 @@ func LoadECDSA(file string) (*ecdsa.PrivateKey, error) { return nil, err } - return HexToECDSA(string(buf)) + var key *ecdsa.PrivateKey + secret.Do(func() { + key, err = HexToECDSA(string(buf)) + clear(buf) + }) + return key, err } // readASCII reads into 'buf', stopping when the buffer is full or diff --git a/encryption/hpke.go b/encryption/hpke.go new file mode 100644 index 0000000..2847007 --- /dev/null +++ b/encryption/hpke.go @@ -0,0 +1,164 @@ +// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package encryption + +import ( + "crypto/ecdh" + "crypto/hpke" + "errors" + "fmt" + + "github.com/luxfi/crypto/secret" +) + +// HPKEKeyPair holds an HPKE key pair for X25519-based encryption. +type HPKEKeyPair struct { + private hpke.PrivateKey + public hpke.PublicKey +} + +var ( + // defaultKEM is X25519-based DHKEM, widely deployed and fast. + defaultKEM = hpke.DHKEM(ecdh.X25519()) + // defaultKDF is HKDF-SHA256 per RFC 9180. + defaultKDF = hpke.HKDFSHA256() + // defaultAEAD is ChaCha20-Poly1305, fast on platforms without AES-NI. + defaultAEAD = hpke.ChaCha20Poly1305() +) + +// GenerateHPKEKeyPair generates a new X25519 HPKE key pair. +func GenerateHPKEKeyPair() (*HPKEKeyPair, error) { + priv, err := defaultKEM.GenerateKey() + if err != nil { + return nil, fmt.Errorf("hpke: generate key: %w", err) + } + return &HPKEKeyPair{ + private: priv, + public: priv.PublicKey(), + }, nil +} + +// HPKEKeyPairFromBytes reconstructs an HPKE key pair from a serialized private key. +func HPKEKeyPairFromBytes(privBytes []byte) (*HPKEKeyPair, error) { + priv, err := defaultKEM.NewPrivateKey(privBytes) + if err != nil { + return nil, fmt.Errorf("hpke: parse private key: %w", err) + } + return &HPKEKeyPair{ + private: priv, + public: priv.PublicKey(), + }, nil +} + +// PublicKeyBytes returns the serialized public key. +func (kp *HPKEKeyPair) PublicKeyBytes() []byte { + return kp.public.Bytes() +} + +// PrivateKeyBytes returns the serialized private key. +// The caller should clear the returned slice when done. +func (kp *HPKEKeyPair) PrivateKeyBytes() ([]byte, error) { + return kp.private.Bytes() +} + +// HPKESeal encrypts plaintext to a recipient's public key using HPKE (RFC 9180). +// +// Returns the concatenation of the encapsulated key and ciphertext. +// The info parameter provides context binding (can be nil). +func HPKESeal(recipientPubKeyBytes []byte, info, plaintext []byte) ([]byte, error) { + pub, err := defaultKEM.NewPublicKey(recipientPubKeyBytes) + if err != nil { + return nil, fmt.Errorf("hpke seal: parse public key: %w", err) + } + return hpke.Seal(pub, defaultKDF, defaultAEAD, info, plaintext) +} + +// HPKEOpen decrypts ciphertext using the recipient's private key. +// +// The ciphertext must be the concatenation of the encapsulated key and the +// actual ciphertext, as returned by HPKESeal. +// The info parameter must match what was used during encryption. +func HPKEOpen(kp *HPKEKeyPair, info, ciphertext []byte) ([]byte, error) { + if kp == nil { + return nil, errors.New("hpke open: nil key pair") + } + var plaintext []byte + var openErr error + secret.Do(func() { + plaintext, openErr = hpke.Open(kp.private, defaultKDF, defaultAEAD, info, ciphertext) + }) + return plaintext, openErr +} + +// HPKEPublicKeyFromBytes deserializes an HPKE public key from bytes. +func HPKEPublicKeyFromBytes(pubBytes []byte) (hpke.PublicKey, error) { + return defaultKEM.NewPublicKey(pubBytes) +} + +// Hybrid HPKE: ML-KEM-768 + X25519 (post-quantum hybrid) + +// HybridHPKEKeyPair holds a hybrid ML-KEM-768 + X25519 HPKE key pair. +type HybridHPKEKeyPair struct { + private hpke.PrivateKey + public hpke.PublicKey +} + +var hybridKEM = hpke.MLKEM768X25519() + +// GenerateHybridHPKEKeyPair generates a new hybrid ML-KEM-768+X25519 HPKE key pair. +// This provides post-quantum security combined with classical X25519. +func GenerateHybridHPKEKeyPair() (*HybridHPKEKeyPair, error) { + priv, err := hybridKEM.GenerateKey() + if err != nil { + return nil, fmt.Errorf("hybrid hpke: generate key: %w", err) + } + return &HybridHPKEKeyPair{ + private: priv, + public: priv.PublicKey(), + }, nil +} + +// HybridHPKEKeyPairFromBytes reconstructs a hybrid HPKE key pair from serialized bytes. +func HybridHPKEKeyPairFromBytes(privBytes []byte) (*HybridHPKEKeyPair, error) { + priv, err := hybridKEM.NewPrivateKey(privBytes) + if err != nil { + return nil, fmt.Errorf("hybrid hpke: parse private key: %w", err) + } + return &HybridHPKEKeyPair{ + private: priv, + public: priv.PublicKey(), + }, nil +} + +// PublicKeyBytes returns the serialized public key. +func (kp *HybridHPKEKeyPair) PublicKeyBytes() []byte { + return kp.public.Bytes() +} + +// PrivateKeyBytes returns the serialized private key. +func (kp *HybridHPKEKeyPair) PrivateKeyBytes() ([]byte, error) { + return kp.private.Bytes() +} + +// HybridHPKESeal encrypts plaintext using hybrid ML-KEM-768+X25519 HPKE. +func HybridHPKESeal(recipientPubKeyBytes []byte, info, plaintext []byte) ([]byte, error) { + pub, err := hybridKEM.NewPublicKey(recipientPubKeyBytes) + if err != nil { + return nil, fmt.Errorf("hybrid hpke seal: parse public key: %w", err) + } + return hpke.Seal(pub, defaultKDF, defaultAEAD, info, plaintext) +} + +// HybridHPKEOpen decrypts ciphertext using hybrid ML-KEM-768+X25519 HPKE. +func HybridHPKEOpen(kp *HybridHPKEKeyPair, info, ciphertext []byte) ([]byte, error) { + if kp == nil { + return nil, errors.New("hybrid hpke open: nil key pair") + } + var plaintext []byte + var openErr error + secret.Do(func() { + plaintext, openErr = hpke.Open(kp.private, defaultKDF, defaultAEAD, info, ciphertext) + }) + return plaintext, openErr +} diff --git a/encryption/hpke_test.go b/encryption/hpke_test.go new file mode 100644 index 0000000..ae4a58f --- /dev/null +++ b/encryption/hpke_test.go @@ -0,0 +1,289 @@ +// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package encryption + +import ( + "bytes" + "testing" +) + +func TestHPKESealOpen(t *testing.T) { + kp, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + + plaintext := []byte("hello from Lux HPKE") + info := []byte("test-context") + + ciphertext, err := HPKESeal(kp.PublicKeyBytes(), info, plaintext) + if err != nil { + t.Fatalf("HPKESeal: %v", err) + } + + if bytes.Equal(plaintext, ciphertext) { + t.Fatal("ciphertext must differ from plaintext") + } + + decrypted, err := HPKEOpen(kp, info, ciphertext) + if err != nil { + t.Fatalf("HPKEOpen: %v", err) + } + + if !bytes.Equal(plaintext, decrypted) { + t.Fatalf("decrypted mismatch: got %q, want %q", decrypted, plaintext) + } +} + +func TestHPKEWrongKey(t *testing.T) { + kp1, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + kp2, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + + plaintext := []byte("secret message") + ciphertext, err := HPKESeal(kp1.PublicKeyBytes(), nil, plaintext) + if err != nil { + t.Fatalf("HPKESeal: %v", err) + } + + _, err = HPKEOpen(kp2, nil, ciphertext) + if err == nil { + t.Fatal("expected error decrypting with wrong key") + } +} + +func TestHPKEWrongInfo(t *testing.T) { + kp, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + + plaintext := []byte("context-bound message") + ciphertext, err := HPKESeal(kp.PublicKeyBytes(), []byte("info-a"), plaintext) + if err != nil { + t.Fatalf("HPKESeal: %v", err) + } + + _, err = HPKEOpen(kp, []byte("info-b"), ciphertext) + if err == nil { + t.Fatal("expected error decrypting with wrong info") + } +} + +func TestHPKEEmptyPlaintext(t *testing.T) { + kp, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + + ciphertext, err := HPKESeal(kp.PublicKeyBytes(), nil, []byte{}) + if err != nil { + t.Fatalf("HPKESeal: %v", err) + } + + decrypted, err := HPKEOpen(kp, nil, ciphertext) + if err != nil { + t.Fatalf("HPKEOpen: %v", err) + } + + if len(decrypted) != 0 { + t.Fatalf("expected empty plaintext, got %d bytes", len(decrypted)) + } +} + +func TestHPKEKeyPairRoundTrip(t *testing.T) { + kp, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + + privBytes, err := kp.PrivateKeyBytes() + if err != nil { + t.Fatalf("PrivateKeyBytes: %v", err) + } + defer clear(privBytes) + + kp2, err := HPKEKeyPairFromBytes(privBytes) + if err != nil { + t.Fatalf("HPKEKeyPairFromBytes: %v", err) + } + + if !bytes.Equal(kp.PublicKeyBytes(), kp2.PublicKeyBytes()) { + t.Fatal("public keys do not match after round-trip") + } + + // Verify decryption works with reconstructed key + plaintext := []byte("round-trip test") + ciphertext, err := HPKESeal(kp.PublicKeyBytes(), nil, plaintext) + if err != nil { + t.Fatalf("HPKESeal: %v", err) + } + + decrypted, err := HPKEOpen(kp2, nil, ciphertext) + if err != nil { + t.Fatalf("HPKEOpen with reconstructed key: %v", err) + } + + if !bytes.Equal(plaintext, decrypted) { + t.Fatal("decrypted mismatch with reconstructed key") + } +} + +func TestHPKEOpenNilKeyPair(t *testing.T) { + _, err := HPKEOpen(nil, nil, []byte("data")) + if err == nil { + t.Fatal("expected error with nil key pair") + } +} + +func TestHPKELargeData(t *testing.T) { + kp, err := GenerateHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHPKEKeyPair: %v", err) + } + + // 1MB of data + plaintext := make([]byte, 1024*1024) + for i := range plaintext { + plaintext[i] = byte(i % 256) + } + + ciphertext, err := HPKESeal(kp.PublicKeyBytes(), nil, plaintext) + if err != nil { + t.Fatalf("HPKESeal: %v", err) + } + + decrypted, err := HPKEOpen(kp, nil, ciphertext) + if err != nil { + t.Fatalf("HPKEOpen: %v", err) + } + + if !bytes.Equal(plaintext, decrypted) { + t.Fatal("large data decryption mismatch") + } +} + +func TestHybridHPKESealOpen(t *testing.T) { + kp, err := GenerateHybridHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHybridHPKEKeyPair: %v", err) + } + + plaintext := []byte("post-quantum hybrid encryption") + info := []byte("hybrid-test") + + ciphertext, err := HybridHPKESeal(kp.PublicKeyBytes(), info, plaintext) + if err != nil { + t.Fatalf("HybridHPKESeal: %v", err) + } + + decrypted, err := HybridHPKEOpen(kp, info, ciphertext) + if err != nil { + t.Fatalf("HybridHPKEOpen: %v", err) + } + + if !bytes.Equal(plaintext, decrypted) { + t.Fatalf("hybrid decrypted mismatch: got %q, want %q", decrypted, plaintext) + } +} + +func TestHybridHPKEWrongKey(t *testing.T) { + kp1, err := GenerateHybridHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHybridHPKEKeyPair: %v", err) + } + kp2, err := GenerateHybridHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHybridHPKEKeyPair: %v", err) + } + + ciphertext, err := HybridHPKESeal(kp1.PublicKeyBytes(), nil, []byte("secret")) + if err != nil { + t.Fatalf("HybridHPKESeal: %v", err) + } + + _, err = HybridHPKEOpen(kp2, nil, ciphertext) + if err == nil { + t.Fatal("expected error decrypting with wrong hybrid key") + } +} + +func TestHybridHPKEKeyPairRoundTrip(t *testing.T) { + kp, err := GenerateHybridHPKEKeyPair() + if err != nil { + t.Fatalf("GenerateHybridHPKEKeyPair: %v", err) + } + + privBytes, err := kp.PrivateKeyBytes() + if err != nil { + t.Fatalf("PrivateKeyBytes: %v", err) + } + defer clear(privBytes) + + kp2, err := HybridHPKEKeyPairFromBytes(privBytes) + if err != nil { + t.Fatalf("HybridHPKEKeyPairFromBytes: %v", err) + } + + if !bytes.Equal(kp.PublicKeyBytes(), kp2.PublicKeyBytes()) { + t.Fatal("hybrid public keys do not match after round-trip") + } + + plaintext := []byte("hybrid round-trip") + ciphertext, err := HybridHPKESeal(kp.PublicKeyBytes(), nil, plaintext) + if err != nil { + t.Fatalf("HybridHPKESeal: %v", err) + } + + decrypted, err := HybridHPKEOpen(kp2, nil, ciphertext) + if err != nil { + t.Fatalf("HybridHPKEOpen with reconstructed key: %v", err) + } + + if !bytes.Equal(plaintext, decrypted) { + t.Fatal("hybrid decrypted mismatch with reconstructed key") + } +} + +func TestHybridHPKEOpenNilKeyPair(t *testing.T) { + _, err := HybridHPKEOpen(nil, nil, []byte("data")) + if err == nil { + t.Fatal("expected error with nil hybrid key pair") + } +} + +func BenchmarkHPKESealOpen(b *testing.B) { + kp, err := GenerateHPKEKeyPair() + if err != nil { + b.Fatalf("GenerateHPKEKeyPair: %v", err) + } + plaintext := []byte("benchmark data for HPKE seal/open") + pubBytes := kp.PublicKeyBytes() + + b.ResetTimer() + for b.Loop() { + ct, _ := HPKESeal(pubBytes, nil, plaintext) + HPKEOpen(kp, nil, ct) + } +} + +func BenchmarkHybridHPKESealOpen(b *testing.B) { + kp, err := GenerateHybridHPKEKeyPair() + if err != nil { + b.Fatalf("GenerateHybridHPKEKeyPair: %v", err) + } + plaintext := []byte("benchmark data for hybrid HPKE seal/open") + pubBytes := kp.PublicKeyBytes() + + b.ResetTimer() + for b.Loop() { + ct, _ := HybridHPKESeal(pubBytes, nil, plaintext) + HybridHPKEOpen(kp, nil, ct) + } +} diff --git a/go.mod b/go.mod index 982afc5..f056d55 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.26.1 exclude github.com/luxfi/geth v1.16.1 require ( - filippo.io/age v1.2.1 - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f + filippo.io/age v1.3.1 + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 github.com/btcsuite/btcd/btcutil v1.1.6 - github.com/cloudflare/circl v1.6.2 + github.com/cloudflare/circl v1.6.3 github.com/consensys/gnark-crypto v0.19.2 - github.com/crate-crypto/go-eth-kzg v1.4.0 + github.com/crate-crypto/go-eth-kzg v1.5.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/ethereum/c-kzg-4844/v2 v2.1.5 github.com/google/btree v1.1.3 @@ -26,12 +26,13 @@ require ( github.com/supranational/blst v0.3.16 github.com/zeebo/blake3 v0.2.4 go.uber.org/mock v0.6.0 - golang.org/x/crypto v0.46.0 + golang.org/x/crypto v0.48.0 golang.org/x/sync v0.19.0 - golang.org/x/sys v0.39.0 + golang.org/x/sys v0.41.0 ) require ( + filippo.io/hpke v0.4.0 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index 7e5958b..0388273 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= -c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= +c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M= +c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -39,12 +39,14 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= -filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= +filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0= +filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4= +filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= +filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f h1:a5PUgHGinaD6XrLmIDLQmGHocjIjBsBAcR5gALjZvMU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -82,8 +84,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= -github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -92,8 +94,8 @@ github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPx github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -371,8 +373,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -534,8 +536,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/random.go b/random.go index f4c6e9f..65557e1 100644 --- a/random.go +++ b/random.go @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved. +// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved. // See the file LICENSE for licensing terms. package crypto @@ -7,10 +7,10 @@ import ( "crypto/rand" ) -// RandomBytes returns a slice of n random bytes +// RandomBytes returns a slice of n random bytes from crypto/rand. +// In Go 1.26+, crypto/rand.Read always succeeds or panics. func RandomBytes(n int) []byte { bytes := make([]byte, n) - // #nosec G404 - we don't need cryptographic randomness for test data - _, _ = rand.Read(bytes) + rand.Read(bytes) return bytes } diff --git a/secret/secret.go b/secret/secret.go new file mode 100644 index 0000000..af617ce --- /dev/null +++ b/secret/secret.go @@ -0,0 +1,38 @@ +// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build goexperiment.runtimesecret + +// Package secret provides helpers for secure handling of cryptographic key material. +// +// When built with GOEXPERIMENT=runtimesecret, secret key bytes are processed +// inside runtime/secret.Do(), which ensures the memory holding the key material +// is zeroed by the runtime after use and is not visible to core dumps or +// debuggers. +// +// Without the experiment flag, the package provides identical API with no-op +// wrappers so callers do not need build tags. +package secret + +import ( + "runtime/secret" +) + +// Do executes f inside a runtime/secret context, ensuring any stack-allocated +// secret material in f is securely erased after f returns. +// +// Use this to wrap operations that handle raw private key bytes: +// +// secret.Do(func() { +// key := generateKeyBytes() +// defer clear(key) +// useKey(key) +// }) +func Do(f func()) { + secret.Do(f) +} + +// Enabled reports whether the runtime secret erasure support is active. +func Enabled() bool { + return secret.Enabled() +} diff --git a/secret/secret_stub.go b/secret/secret_stub.go new file mode 100644 index 0000000..afb95e1 --- /dev/null +++ b/secret/secret_stub.go @@ -0,0 +1,20 @@ +// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +//go:build !goexperiment.runtimesecret + +package secret + +// Do executes f directly. Without GOEXPERIMENT=runtimesecret, this is a +// pass-through with no runtime secret memory protection. +// +// Callers should still clear(key) sensitive byte slices manually. +func Do(f func()) { + f() +} + +// Enabled reports whether the runtime secret erasure support is active. +// Returns false when built without GOEXPERIMENT=runtimesecret. +func Enabled() bool { + return false +} diff --git a/secret/secret_test.go b/secret/secret_test.go new file mode 100644 index 0000000..2d1e8f9 --- /dev/null +++ b/secret/secret_test.go @@ -0,0 +1,72 @@ +// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package secret + +import ( + "testing" +) + +func TestDo(t *testing.T) { + called := false + Do(func() { + called = true + }) + if !called { + t.Fatal("Do did not execute the function") + } +} + +func TestDoWithKeyMaterial(t *testing.T) { + var keyBytes []byte + Do(func() { + keyBytes = make([]byte, 32) + for i := range keyBytes { + keyBytes[i] = byte(i) + } + // Simulate using the key then clearing it + clear(keyBytes) + }) + // After Do, keyBytes should be zeroed + for _, b := range keyBytes { + if b != 0 { + t.Fatal("key material was not cleared inside Do") + } + } +} + +func TestEnabled(t *testing.T) { + // This test works with both build tags. + // With goexperiment.runtimesecret: Enabled() == true + // Without: Enabled() == false + // We just verify it returns a bool without panicking. + _ = Enabled() +} + +func TestDoNested(t *testing.T) { + outerCalled := false + innerCalled := false + Do(func() { + outerCalled = true + Do(func() { + innerCalled = true + }) + }) + if !outerCalled { + t.Fatal("outer Do not called") + } + if !innerCalled { + t.Fatal("inner Do not called") + } +} + +func TestDoWithPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic to propagate through Do") + } + }() + Do(func() { + panic("test panic") + }) +}