diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 71e3625..a80fcb4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -120,6 +120,9 @@ jobs:
- name: Run wolftls tests (full config)
run: go test ./wolftls
+ - name: Run sha256 tests (full config)
+ run: go test ./sha256
+
- name: Run wolfx509 tests (full config)
run: go test ./wolfx509
diff --git a/handles/README.md b/handles/README.md
index ba78c4b..867b7df 100644
--- a/handles/README.md
+++ b/handles/README.md
@@ -16,7 +16,8 @@ direct-wrapper layer; opinionated lifecycle/marshal helpers live here.
## Algorithm tagging
-`EccKey` reports `Algorithm()` returning `handles.AlgECDSAP256` so
+`EccKey` reports `Algorithm()` returning `handles.AlgECDSAP256` for P-256
+keys (`handles.AlgUnknown` for any other curve) so
algorithm-polymorphic consumers (e.g. `wolfx509.KeyHandle`) can
dispatch on the key class without type-asserting. The `handles.Algorithm`
enum is the shared algorithm taxonomy across `handles` and `wolfx509`.
diff --git a/handles/aes_handles.go b/handles/aes_handles.go
index 1144c11..eb0b314 100644
--- a/handles/aes_handles.go
+++ b/handles/aes_handles.go
@@ -23,14 +23,22 @@ package handles
import (
"errors"
"fmt"
+ "sync"
wolfSSL "github.com/wolfssl/go-wolfssl"
)
// AesGcmAEAD implements crypto/cipher.AEAD using wolfCrypt AES-256-GCM.
-// Create one with NewAesGcmAEAD.
+// Create one with NewAesGcmAEAD and call Zero when it is no longer needed.
type AesGcmAEAD struct {
- key [wolfSSL.AES_256_KEY_SIZE]byte
+ st *aesGcmState
+}
+
+// aesGcmState is shared by copies of an AesGcmAEAD so Zero reaches them all.
+type aesGcmState struct {
+ mu sync.RWMutex
+ key [wolfSSL.AES_256_KEY_SIZE]byte
+ zeroed bool
}
// errAesGcmAuth is returned from Open for any decrypt/verify failure
@@ -40,7 +48,39 @@ var errAesGcmAuth = errors.New("wolfSSL: AES-GCM authentication failed")
// NewAesGcmAEAD returns an AesGcmAEAD keyed with a 32-byte AES-256 key.
func NewAesGcmAEAD(key [wolfSSL.AES_256_KEY_SIZE]byte) *AesGcmAEAD {
- return &AesGcmAEAD{key: key}
+ return &AesGcmAEAD{st: &aesGcmState{key: key}}
+}
+
+// Zero wipes the key held by a (best effort: the Go runtime may already have
+// copied it). Zero waits for in-flight Seal and Open calls; afterwards Seal
+// panics and Open fails.
+func (a *AesGcmAEAD) Zero() {
+ st := a.st
+ if st == nil {
+ return
+ }
+ st.mu.Lock()
+ defer st.mu.Unlock()
+ zeroBytes(st.key[:])
+ st.zeroed = true
+}
+
+// use read-locks the key state for one operation, or returns nil if the
+// AEAD was not built by NewAesGcmAEAD or has been zeroed.
+func (a *AesGcmAEAD) use() *aesGcmState {
+ st := a.st
+ if st == nil {
+ return nil
+ }
+ st.mu.RLock()
+ if st.zeroed {
+ st.mu.RUnlock()
+ return nil
+ }
+ if testHookAcquired != nil {
+ testHookAcquired()
+ }
+ return st
}
func (a *AesGcmAEAD) NonceSize() int { return wolfSSL.GCM_NONCE_MID_SZ }
@@ -54,18 +94,21 @@ func (a *AesGcmAEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if len(nonce) != a.NonceSize() {
panic("wolfSSL/handles: incorrect nonce length given to AEAD")
}
+ st := a.use()
+ if st == nil {
+ panic("wolfSSL/handles: AEAD used after Zero or without NewAesGcmAEAD")
+ }
+ defer st.mu.RUnlock()
aes := wolfSSL.Wc_AesAllocAligned()
if aes == nil {
panic("wolfSSL/handles: Wc_AesAllocAligned returned nil")
}
- defer func() {
- wolfSSL.Wc_AesFree(aes)
- wolfSSL.Wc_AesFreeAllocAligned(aes)
- }()
+ defer wolfSSL.Wc_AesFreeAllocAligned(aes)
if ret := wolfSSL.Wc_AesInit(aes, nil, wolfSSL.INVALID_DEVID); ret != 0 {
panic(fmt.Sprintf("wolfSSL/handles: wc_AesInit failed: %d", ret))
}
- if ret := wolfSSL.Wc_AesGcmSetKey(aes, a.key[:], wolfSSL.AES_256_KEY_SIZE); ret != 0 {
+ defer wolfSSL.Wc_AesFree(aes)
+ if ret := wolfSSL.Wc_AesGcmSetKey(aes, st.key[:], wolfSSL.AES_256_KEY_SIZE); ret != 0 {
panic(fmt.Sprintf("wolfSSL/handles: wc_AesGcmSetKey failed: %d", ret))
}
@@ -92,18 +135,21 @@ func (a *AesGcmAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte
if len(ciphertext) < a.Overhead() {
return nil, errAesGcmAuth
}
+ st := a.use()
+ if st == nil {
+ return nil, errAesGcmAuth
+ }
+ defer st.mu.RUnlock()
aes := wolfSSL.Wc_AesAllocAligned()
if aes == nil {
return nil, errAesGcmAuth
}
- defer func() {
- wolfSSL.Wc_AesFree(aes)
- wolfSSL.Wc_AesFreeAllocAligned(aes)
- }()
+ defer wolfSSL.Wc_AesFreeAllocAligned(aes)
if ret := wolfSSL.Wc_AesInit(aes, nil, wolfSSL.INVALID_DEVID); ret != 0 {
return nil, errAesGcmAuth
}
- if ret := wolfSSL.Wc_AesGcmSetKey(aes, a.key[:], wolfSSL.AES_256_KEY_SIZE); ret != 0 {
+ defer wolfSSL.Wc_AesFree(aes)
+ if ret := wolfSSL.Wc_AesGcmSetKey(aes, st.key[:], wolfSSL.AES_256_KEY_SIZE); ret != 0 {
return nil, errAesGcmAuth
}
@@ -113,6 +159,7 @@ func (a *AesGcmAEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte
plaintext := make([]byte, ctLen)
if ret := wolfSSL.Wc_AesGcmDecrypt(aes, plaintext, ct, nonce, tag, additionalData); ret != 0 {
+ zeroBytes(plaintext)
return nil, errAesGcmAuth
}
return append(dst, plaintext...), nil
diff --git a/handles/aes_handles_test.go b/handles/aes_handles_test.go
index 1e3030d..87d098d 100644
--- a/handles/aes_handles_test.go
+++ b/handles/aes_handles_test.go
@@ -23,6 +23,8 @@ package handles
import (
"bytes"
"crypto/cipher"
+ "strings"
+ "sync"
"testing"
wolfSSL "github.com/wolfssl/go-wolfssl"
@@ -159,3 +161,139 @@ func TestAesGcmAEAD_DstAppend(t *testing.T) {
t.Fatalf("Open: got %q, want %q", decrypted[len(outPrefix):], plaintext)
}
}
+
+func TestAesGcmAEADOpenFailureWipesPlaintext(t *testing.T) {
+ var key [wolfSSL.AES_256_KEY_SIZE]byte
+ key[2] = 0x5a
+ a := NewAesGcmAEAD(key)
+ nonce := make([]byte, a.NonceSize())
+ plaintext := []byte("plaintext that must not linger after a failed Open")
+ ciphertext := a.Seal(nil, nonce, plaintext, nil)
+ ciphertext[len(ciphertext)-1] ^= 0x01
+
+ wiped := recordZeroBytes(t)
+ if _, err := a.Open(nil, nonce, ciphertext, nil); err == nil {
+ t.Fatal("Open accepted a tampered tag")
+ }
+ for _, b := range wiped() {
+ if len(b) == len(plaintext) && allZero(b) {
+ return
+ }
+ }
+ t.Fatal("decrypt output buffer was not wiped after authentication failure")
+}
+
+func TestAesGcmAEADZeroWipesKey(t *testing.T) {
+ var key [wolfSSL.AES_256_KEY_SIZE]byte
+ for i := range key {
+ key[i] = byte(i + 1)
+ }
+ a := NewAesGcmAEAD(key)
+ nonce := make([]byte, a.NonceSize())
+ ciphertext := a.Seal(nil, nonce, []byte("data"), nil)
+
+ a.Zero()
+ if !allZero(a.st.key[:]) {
+ t.Fatal("Zero left key bytes in the AEAD")
+ }
+ if _, err := a.Open(nil, nonce, ciphertext, nil); err == nil {
+ t.Fatal("Open succeeded after Zero")
+ }
+ defer func() {
+ if recover() == nil {
+ t.Fatal("Seal did not panic after Zero")
+ }
+ }()
+ a.Seal(nil, nonce, []byte("data"), nil)
+}
+
+func TestAesGcmAEADZeroReachesCopies(t *testing.T) {
+ var key [wolfSSL.AES_256_KEY_SIZE]byte
+ key[0] = 0x11
+ a := NewAesGcmAEAD(key)
+ b := *a
+ a.Zero()
+ defer func() {
+ if recover() == nil {
+ t.Fatal("copy of a zeroed AEAD could still Seal")
+ }
+ }()
+ b.Seal(nil, make([]byte, b.NonceSize()), []byte("data"), nil)
+}
+
+func TestAesGcmAEADZeroConcurrentWithUse(t *testing.T) {
+ var key [wolfSSL.AES_256_KEY_SIZE]byte
+ for i := range key {
+ key[i] = byte(0x80 + i)
+ }
+ a := NewAesGcmAEAD(key)
+ zeroKeyAEAD := NewAesGcmAEAD([wolfSSL.AES_256_KEY_SIZE]byte{})
+ nonce := make([]byte, a.NonceSize())
+ msg := []byte("concurrent zero")
+ ct0 := a.Seal(nil, nonce, msg, nil)
+ var ready, wg sync.WaitGroup
+ for i := 0; i < 8; i++ {
+ ready.Add(1)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ signalled := false
+ defer func() {
+ if !signalled {
+ ready.Done()
+ }
+ if r := recover(); r != nil {
+ if s, ok := r.(string); !ok || !strings.Contains(s, "after Zero") {
+ t.Errorf("unexpected panic: %v", r)
+ }
+ }
+ }()
+ for j := 0; j < 50; j++ {
+ ct := a.Seal(nil, nonce, msg, nil)
+ if _, err := zeroKeyAEAD.Open(nil, nonce, ct, nil); err == nil {
+ t.Error("Seal used a wiped key")
+ }
+ _, _ = a.Open(nil, nonce, ct, nil)
+ if !signalled {
+ signalled = true
+ ready.Done()
+ }
+ }
+ }()
+ }
+ ready.Wait()
+ a.Zero()
+ wg.Wait()
+ if _, err := a.Open(nil, nonce, ct0, nil); err == nil {
+ t.Fatal("Open succeeded after Zero")
+ }
+}
+
+func TestAesGcmAEADZeroWaitsForInFlightSeal(t *testing.T) {
+ var key [wolfSSL.AES_256_KEY_SIZE]byte
+ for i := range key {
+ key[i] = byte(0x40 + i)
+ }
+ a := NewAesGcmAEAD(key)
+ nonce := make([]byte, a.NonceSize())
+ msg := []byte("in flight")
+
+ entered, release := holdFirstAcquire(t)
+ sealed := make(chan []byte, 1)
+ go func() { sealed <- a.Seal(nil, nonce, msg, nil) }()
+ <-entered
+ zeroed := make(chan struct{})
+ go func() {
+ a.Zero()
+ close(zeroed)
+ }()
+ assertBlocked(t, zeroed, release, "Zero")
+ ct := <-sealed
+ pt, err := NewAesGcmAEAD(key).Open(nil, nonce, ct, nil)
+ if err != nil || !bytes.Equal(pt, msg) {
+ t.Fatalf("in-flight Seal did not use the original key: %v", err)
+ }
+ if _, err := a.Open(nil, nonce, ct, nil); err == nil {
+ t.Fatal("Open succeeded after Zero")
+ }
+}
diff --git a/handles/ecc_handles.go b/handles/ecc_handles.go
index d3a765a..cd570cf 100644
--- a/handles/ecc_handles.go
+++ b/handles/ecc_handles.go
@@ -21,10 +21,14 @@
package handles
import (
+ "bytes"
"encoding/pem"
"errors"
"fmt"
"runtime"
+ "sort"
+ "sync"
+ "sync/atomic"
"unsafe"
wolfSSL "github.com/wolfssl/go-wolfssl"
@@ -32,6 +36,14 @@ import (
// TODO: Remove "encoding/pem" and replace with wolfSSL ASN parsing
+// zeroBytes wipes temporary buffers holding secrets. Best effort only: the
+// Go runtime may already have copied the bytes elsewhere.
+var zeroBytes = func(b []byte) {
+ for i := range b {
+ b[i] = 0
+ }
+}
+
// Algorithm names a wolfCrypt key class.
type Algorithm int
@@ -49,17 +61,18 @@ const (
// GenerateP256Key, NewEmptyEccKey, or NewEmptyEccPubKey. Callers must
// Free the key when done; freeing twice is safe.
//
-// EccKey is NOT currently thread-safe for use across operations that
-// consume the embedded RNG (SignDigest, GenerateP256Key, and any wolfx509 cert
-// builder call that takes this key as the signer). Either serialize
-// callers or hand each goroutine its own EccKey. Read-only ops
-// (Verify, public-key export) are safe to call concurrently.
+// The helpers in this package lock the key for the duration of each
+// wolfCrypt call, so an EccKey may be shared between goroutines and Free
+// waits for in-flight operations. Code that uses Raw, Rng, CKeyPtr or
+// CRngPtr directly must hold Lock (or LockEccKeys) across that use.
type EccKey struct {
+ mu sync.Mutex
raw wolfSSL.Ecc_key
rng wolfSSL.WC_RNG
- init bool // wc_ecc_init has run
- hasRng bool // wc_InitRng has run (private-key handles only)
- live bool // additionally, key material is populated
+ init bool // wc_ecc_init has run
+ hasRng int32 // atomic; wc_InitRng has run (private-key handles only)
+ live int32 // atomic; additionally, key material is populated
+ alg int32 // atomic; the Algorithm while live
}
// GenerateP256Key returns a fresh P-256 (secp256r1) ECC key pair generated
@@ -74,7 +87,11 @@ func GenerateP256Key() (*EccKey, error) {
k.Free()
return nil, fmt.Errorf("wolfCrypt: wc_ecc_make_key: %d", ret)
}
- k.live = true
+ k.MarkLive()
+ if k.Algorithm() != AlgECDSAP256 {
+ k.Free()
+ return nil, errors.New("handles: generated key is not P-256")
+ }
return k, nil
}
@@ -94,7 +111,7 @@ func NewEmptyEccKey() (*EccKey, error) {
return nil, fmt.Errorf("wolfCrypt: wc_InitRng: %d", ret)
}
k.init = true
- k.hasRng = true
+ atomic.StoreInt32(&k.hasRng, 1)
runtime.SetFinalizer(k, (*EccKey).Free)
return k, nil
}
@@ -113,45 +130,171 @@ func NewEmptyEccPubKey() (*EccKey, error) {
// MarkLive flags the key as fully populated. Higher-level parse helpers call
// this once they have successfully filled the underlying ecc_key from a DER
-// or PEM blob. After MarkLive the key is usable for signing/verification.
-func (k *EccKey) MarkLive() { k.live = true }
+// or PEM blob. After MarkLive the key is usable for verification; signing
+// also needs private key material and the RNG that only NewEmptyEccKey and
+// GenerateP256Key set up. MarkLive also records the key's curve for Algorithm.
+func (k *EccKey) MarkLive() {
+ k.mu.Lock()
+ defer k.mu.Unlock()
+ if !k.init {
+ return
+ }
+ alg, _ := eccKeyAlgorithm(&k.raw)
+ atomic.StoreInt32(&k.alg, int32(alg))
+ atomic.StoreInt32(&k.live, 1)
+}
+
+// p256SPKIPrefix is the DER SubjectPublicKeyInfo header for an uncompressed
+// prime256v1 (P-256) public key.
+var p256SPKIPrefix = []byte{
+ 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
+ 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
+ 0x42, 0x00, 0x04,
+}
+
+// eccPrivateOnlyE is wolfCrypt's ECC_PRIVATEONLY_E: the key has no public point.
+const eccPrivateOnlyE = -246
+
+// eccKeyAlgorithm returns the curve class of key, or AlgUnknown and the
+// negative wolfCrypt error if its public key cannot be exported.
+func eccKeyAlgorithm(key *wolfSSL.Ecc_key) (Algorithm, int) {
+ spki := make([]byte, 160)
+ n := wolfSSL.Wc_EccPublicKeyToDer(key, spki, 1)
+ if n == eccPrivateOnlyE {
+ if ret := wolfSSL.Wc_ecc_make_pub_in_priv(key); ret != 0 {
+ return AlgUnknown, ret
+ }
+ n = wolfSSL.Wc_EccPublicKeyToDer(key, spki, 1)
+ }
+ if n < 0 {
+ return AlgUnknown, n
+ }
+ if n == len(p256SPKIPrefix)+64 && bytes.Equal(spki[:len(p256SPKIPrefix)], p256SPKIPrefix) {
+ return AlgECDSAP256, 0
+ }
+ return AlgUnknown, 0
+}
// IsLive reports whether the key has been generated or successfully parsed
-// (i.e. usable for signing/verification operations) and not yet freed.
-func (k *EccKey) IsLive() bool { return k != nil && k.live }
+// and not yet freed. A live key from NewEmptyEccPubKey has no RNG and
+// cannot sign. The result is stable only while Lock is held.
+func (k *EccKey) IsLive() bool {
+ return k != nil && atomic.LoadInt32(&k.live) == 1
+}
+
+// testHookAcquired, when set by tests, runs while an operation holds a key
+// lock.
+var testHookAcquired func()
+
+// acquire locks a live key and returns its unlock function, or returns nil
+// (with the key unlocked) if k is nil or not live.
+func (k *EccKey) acquire() func() {
+ if k == nil {
+ return nil
+ }
+ k.mu.Lock()
+ if atomic.LoadInt32(&k.live) == 0 {
+ k.mu.Unlock()
+ return nil
+ }
+ if testHookAcquired != nil {
+ testHookAcquired()
+ }
+ return k.mu.Unlock
+}
+
+// Lock serializes direct use of Raw, Rng, CKeyPtr and CRngPtr with the
+// helpers in this package and with Free. While holding it, do not call
+// MarkLive, Free or the package-level helpers on k; they lock k.
+func (k *EccKey) Lock() { k.mu.Lock() }
+
+// Unlock releases the lock taken by Lock.
+func (k *EccKey) Unlock() { k.mu.Unlock() }
+
+// LockEccKeys locks each distinct non-nil key in a fixed (address) order,
+// so callers locking overlapping sets cannot deadlock, and returns a
+// function that unlocks them.
+func LockEccKeys(keys ...*EccKey) (unlock func()) {
+ var locked []*EccKey
+ for _, k := range keys {
+ dup := k == nil
+ for _, l := range locked {
+ if l == k {
+ dup = true
+ }
+ }
+ if !dup {
+ locked = append(locked, k)
+ }
+ }
+ sort.Slice(locked, func(i, j int) bool {
+ return uintptr(unsafe.Pointer(locked[i])) < uintptr(unsafe.Pointer(locked[j]))
+ })
+ for _, k := range locked {
+ k.mu.Lock()
+ }
+ return func() {
+ for i := len(locked) - 1; i >= 0; i-- {
+ locked[i].mu.Unlock()
+ }
+ }
+}
// Free releases the wolfCrypt key and RNG resources. Safe to call multiple
// times and safe to call on a partially-constructed key (NewEmptyEccKey
// or NewEmptyEccPubKey without MarkLive); subsequent calls are no-ops.
func (k *EccKey) Free() {
- if k == nil || !k.init {
+ if k == nil {
return
}
- if k.hasRng {
+ k.mu.Lock()
+ defer k.mu.Unlock()
+ if !k.init {
+ return
+ }
+ if atomic.LoadInt32(&k.hasRng) == 1 {
wolfSSL.Wc_FreeRng(&k.rng)
- k.hasRng = false
+ atomic.StoreInt32(&k.hasRng, 0)
}
+ atomic.StoreInt32(&k.live, 0)
+ atomic.StoreInt32(&k.alg, int32(AlgUnknown))
wolfSSL.Wc_ecc_free(&k.raw)
k.init = false
- k.live = false
// SetFinalizer to nil to avoid double-free
runtime.SetFinalizer(k, nil)
}
// Raw returns the underlying wolfCrypt ecc_key for direct use with low-level
// Wc_* APIs (signing, verifying, cert builder, etc.). The returned pointer
-// is invalid after Free.
+// is invalid after Free; hold Lock while using it.
func (k *EccKey) Raw() *wolfSSL.Ecc_key { return &k.raw }
// Rng returns the wolfCrypt RNG associated with this key. Used by signing
-// operations that take an RNG argument.
+// operations that take an RNG argument. Hold Lock while using it.
func (k *EccKey) Rng() *wolfSSL.WC_RNG { return &k.rng }
-func (k *EccKey) Algorithm() Algorithm { return AlgECDSAP256 }
-func (k *EccKey) CKeyPtr() unsafe.Pointer { return unsafe.Pointer(&k.raw) }
+// Algorithm returns AlgECDSAP256 for a live P-256 key and AlgUnknown for
+// any other curve or for a key that has not been marked live.
+func (k *EccKey) Algorithm() Algorithm {
+ if !k.IsLive() {
+ return AlgUnknown
+ }
+ return Algorithm(atomic.LoadInt32(&k.alg))
+}
+// CKeyPtr returns the C ecc_key, or nil if k is not live. The caller must
+// hold Lock (or LockEccKeys) while calling it and using the pointer.
+func (k *EccKey) CKeyPtr() unsafe.Pointer {
+ if !k.IsLive() {
+ return nil
+ }
+ return unsafe.Pointer(&k.raw)
+}
+
+// CRngPtr returns the key's C WC_RNG, or nil if it has none. The caller
+// must hold Lock (or LockEccKeys) while calling it and using the pointer.
func (k *EccKey) CRngPtr() unsafe.Pointer {
- if k == nil || !k.hasRng {
+ if k == nil || atomic.LoadInt32(&k.hasRng) == 0 {
return nil
}
return unsafe.Pointer(&k.rng)
@@ -167,9 +310,11 @@ func (k *EccKey) CRngPtr() unsafe.Pointer {
// MarshalSEC1DER returns the SEC1 EC-private-key DER encoding of k —
// the body of an "EC PRIVATE KEY" PEM block.
func MarshalSEC1DER(k *EccKey) ([]byte, error) {
- if !k.IsLive() {
+ unlock := k.acquire()
+ if unlock == nil {
return nil, errors.New("handles: EccKey is not live")
}
+ defer unlock()
// 256 bytes comfortably exceeds the worst-case SEC1+pub size for any
// wolfCrypt-supported curve.
buf := make([]byte, 256)
@@ -183,9 +328,11 @@ func MarshalSEC1DER(k *EccKey) ([]byte, error) {
// MarshalPKCS8DER returns the key wrapped in an unencrypted PKCS#8
// PrivateKeyInfo DER blob via wc_EccKeyToPKCS8.
func MarshalPKCS8DER(k *EccKey) ([]byte, error) {
- if !k.IsLive() {
+ unlock := k.acquire()
+ if unlock == nil {
return nil, errors.New("handles: EccKey is not live")
}
+ defer unlock()
out := make([]byte, 384)
outLen := len(out)
if ret := wolfSSL.Wc_EccKeyToPKCS8(k.Raw(), out, &outLen); ret < 0 {
@@ -201,6 +348,7 @@ func MarshalSEC1PEM(k *EccKey) ([]byte, error) {
if err != nil {
return nil, err
}
+ defer zeroBytes(der[:cap(der)])
return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}), nil
}
@@ -211,11 +359,16 @@ func MarshalPKCS8PEM(k *EccKey) ([]byte, error) {
if err != nil {
return nil, err
}
+ defer zeroBytes(der[:cap(der)])
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil
}
+// errNotP256 reports a well-formed EC private key on a curve other than P-256.
+var errNotP256 = errors.New("handles: EC private key is not P-256")
+
// ParseECPrivateKeyDER parses a SEC1 EC private key DER blob (the body of
-// an "EC PRIVATE KEY" PEM) into a fresh EccKey.
+// an "EC PRIVATE KEY" PEM) into a fresh EccKey. Only P-256 keys are
+// accepted.
func ParseECPrivateKeyDER(der []byte) (*EccKey, error) {
if len(der) == 0 {
return nil, errors.New("handles: empty EC private key DER")
@@ -229,6 +382,15 @@ func ParseECPrivateKeyDER(der []byte) (*EccKey, error) {
k.Free()
return nil, fmt.Errorf("wolfCrypt: wc_EccPrivateKeyDecode: %d", ret)
}
+ alg, ret := eccKeyAlgorithm(k.Raw())
+ if ret < 0 {
+ k.Free()
+ return nil, fmt.Errorf("wolfCrypt: wc_EccPublicKeyToDer: %d", ret)
+ }
+ if alg != AlgECDSAP256 {
+ k.Free()
+ return nil, errNotP256
+ }
k.MarkLive()
return k, nil
}
@@ -254,14 +416,16 @@ func ParseECPrivateKeyPEM(p []byte) (*EccKey, error) {
if len(p) == 0 {
return nil, errors.New("handles: empty EC private key PEM")
}
- der := make([]byte, len(p))
- n := wolfSSL.Wc_KeyPemToDer(p, der, "")
+ buf := make([]byte, len(p))
+ defer zeroBytes(buf)
+ n := wolfSSL.Wc_KeyPemToDer(p, buf, "")
if n < 0 {
return nil, fmt.Errorf("wolfCrypt: wc_KeyPemToDer: %d", n)
}
- der = der[:n]
- if k, err := ParseECPrivateKeyDER(der); err == nil {
- return k, nil
+ der := buf[:n]
+ k, err := ParseECPrivateKeyDER(der)
+ if err == nil || err == errNotP256 {
+ return k, err
}
return ParsePKCS8ECPrivateKeyDER(der)
}
@@ -271,9 +435,11 @@ func ParseECPrivateKeyPEM(p []byte) (*EccKey, error) {
// encoders (RFC 7518 §6.2.1) and anywhere else a pure numeric point
// representation is needed.
func PublicRawXY(k *EccKey) (x, y []byte, err error) {
- if !k.IsLive() {
+ unlock := k.acquire()
+ if unlock == nil {
return nil, nil, errors.New("handles: EccKey is not live")
}
+ defer unlock()
x = make([]byte, 32)
y = make([]byte, 32)
xLen := len(x)
@@ -285,15 +451,20 @@ func PublicRawXY(k *EccKey) (x, y []byte, err error) {
}
// SignDigest signs a pre-computed hash digest and returns an ASN.1-DER
-// encoded ECDSA signature (SEQUENCE { r INTEGER, s INTEGER }). For P-256,
-// the caller must supply a 32-byte digest (SHA-256 output).
+// encoded ECDSA signature (SEQUENCE { r INTEGER, s INTEGER }). Any non-empty
+// digest is accepted; callers must supply the digest their protocol requires.
//
-// Not safe to call concurrently with another SignDigest (or any other
-// RNG-consuming operation) on the same EccKey: see the EccKey type doc.
+// k must carry an RNG (GenerateP256Key or NewEmptyEccKey). Concurrent calls
+// on the same EccKey are serialized by the key's lock.
func SignDigest(k *EccKey, digest []byte) ([]byte, error) {
- if !k.IsLive() {
+ unlock := k.acquire()
+ if unlock == nil {
return nil, errors.New("handles: EccKey is not live")
}
+ defer unlock()
+ if atomic.LoadInt32(&k.hasRng) == 0 {
+ return nil, errors.New("handles: SignDigest: EccKey has no RNG")
+ }
if len(digest) == 0 {
return nil, errors.New("handles: SignDigest: empty digest")
}
diff --git a/handles/ecc_handles_test.go b/handles/ecc_handles_test.go
index 70eeea2..9d3c996 100644
--- a/handles/ecc_handles_test.go
+++ b/handles/ecc_handles_test.go
@@ -22,7 +22,12 @@ package handles
import (
"bytes"
+ "encoding/pem"
+ "errors"
+ "strings"
+ "sync"
"testing"
+ "time"
wolfSSL "github.com/wolfssl/go-wolfssl"
)
@@ -95,3 +100,485 @@ func TestEccPrivateKeyToDer(t *testing.T) {
t.Fatalf("round trip mismatch:\n got %x\nwant %x", enc2, enc)
}
}
+
+// eccTestKeyDER generates an ECC key of keySize bytes and returns its SEC1
+// private DER and its SubjectPublicKeyInfo DER. Skips if the curve is not
+// compiled in.
+func eccTestKeyDER(t *testing.T, keySize int) (priv, pub []byte) {
+ t.Helper()
+ var rng wolfSSL.WC_RNG
+ if ret := wolfSSL.Wc_InitRng(&rng); ret != 0 {
+ t.Fatalf("Wc_InitRng: %d", ret)
+ }
+ defer wolfSSL.Wc_FreeRng(&rng)
+ var key wolfSSL.Ecc_key
+ if ret := wolfSSL.Wc_ecc_init(&key); ret != 0 {
+ t.Fatalf("Wc_ecc_init: %d", ret)
+ }
+ defer wolfSSL.Wc_ecc_free(&key)
+ if ret := wolfSSL.Wc_ecc_make_key(&rng, keySize, &key); ret != 0 {
+ t.Skipf("Wc_ecc_make_key(%d): %d (curve not available)", keySize, ret)
+ }
+ priv = make([]byte, 512)
+ n := wolfSSL.Wc_EccKeyToDer(&key, priv)
+ if n <= 0 {
+ t.Fatalf("Wc_EccKeyToDer: %d", n)
+ }
+ pub = make([]byte, 512)
+ m := wolfSSL.Wc_EccPublicKeyToDer(&key, pub, 1)
+ if m <= 0 {
+ t.Fatalf("Wc_EccPublicKeyToDer: %d", m)
+ }
+ return priv[:n], pub[:m]
+}
+
+func TestEccKeyAlgorithmReportsP256Only(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k.Free()
+ if alg := k.Algorithm(); alg != AlgECDSAP256 {
+ t.Fatalf("generated P-256 key Algorithm() = %d, want %d", alg, AlgECDSAP256)
+ }
+
+ der, err := MarshalSEC1DER(k)
+ if err != nil {
+ t.Fatalf("MarshalSEC1DER: %v", err)
+ }
+ k2, err := ParseECPrivateKeyDER(der)
+ if err != nil {
+ t.Fatalf("ParseECPrivateKeyDER(P-256): %v", err)
+ }
+ defer k2.Free()
+ if alg := k2.Algorithm(); alg != AlgECDSAP256 {
+ t.Fatalf("parsed P-256 key Algorithm() = %d, want %d", alg, AlgECDSAP256)
+ }
+
+ _, pub384 := eccTestKeyDER(t, 48)
+ pk, err := NewEmptyEccPubKey()
+ if err != nil {
+ t.Fatalf("NewEmptyEccPubKey: %v", err)
+ }
+ defer pk.Free()
+ idx := 0
+ if ret := wolfSSL.Wc_EccPublicKeyDecode(pub384, &idx, pk.Raw(), len(pub384)); ret != 0 {
+ t.Fatalf("Wc_EccPublicKeyDecode(P-384): %d", ret)
+ }
+ pk.MarkLive()
+ if alg := pk.Algorithm(); alg == AlgECDSAP256 {
+ t.Fatal("P-384 public key reported as AlgECDSAP256")
+ }
+}
+
+// eccTestPKCS8DER generates an ECC key of keySize bytes and returns it as
+// unencrypted PKCS#8 DER. Skips if the curve is not compiled in.
+func eccTestPKCS8DER(t *testing.T, keySize int) []byte {
+ t.Helper()
+ var rng wolfSSL.WC_RNG
+ if ret := wolfSSL.Wc_InitRng(&rng); ret != 0 {
+ t.Fatalf("Wc_InitRng: %d", ret)
+ }
+ defer wolfSSL.Wc_FreeRng(&rng)
+ var key wolfSSL.Ecc_key
+ if ret := wolfSSL.Wc_ecc_init(&key); ret != 0 {
+ t.Fatalf("Wc_ecc_init: %d", ret)
+ }
+ defer wolfSSL.Wc_ecc_free(&key)
+ if ret := wolfSSL.Wc_ecc_make_key(&rng, keySize, &key); ret != 0 {
+ t.Skipf("Wc_ecc_make_key(%d): %d (curve not available)", keySize, ret)
+ }
+ der := make([]byte, 512)
+ derLen := len(der)
+ if ret := wolfSSL.Wc_EccKeyToPKCS8(&key, der, &derLen); ret < 0 {
+ t.Fatalf("Wc_EccKeyToPKCS8: %d", ret)
+ }
+ return der[:derLen]
+}
+
+func TestParseECPrivateKeyPEMReportsNonP256(t *testing.T) {
+ priv384, _ := eccTestKeyDER(t, 48)
+ pkcs8384 := eccTestPKCS8DER(t, 48)
+ blocks := []*pem.Block{
+ {Type: "EC PRIVATE KEY", Bytes: priv384},
+ {Type: "PRIVATE KEY", Bytes: pkcs8384},
+ }
+ for _, b := range blocks {
+ k, err := ParseECPrivateKeyPEM(pem.EncodeToMemory(b))
+ if err == nil {
+ k.Free()
+ t.Fatalf("ParseECPrivateKeyPEM accepted a P-384 %q key", b.Type)
+ }
+ if err != errNotP256 {
+ t.Fatalf("ParseECPrivateKeyPEM(P-384 %q) = %v, want %v", b.Type, err, errNotP256)
+ }
+ }
+}
+
+func TestParseECPrivateKeyDERRejectsNonP256(t *testing.T) {
+ priv384, _ := eccTestKeyDER(t, 48)
+ k, err := ParseECPrivateKeyDER(priv384)
+ if err == nil {
+ k.Free()
+ t.Fatal("ParseECPrivateKeyDER accepted a P-384 key")
+ }
+}
+
+func TestParseECPrivateKeyDERAcceptsP256WithoutPublicKey(t *testing.T) {
+ var rng wolfSSL.WC_RNG
+ if ret := wolfSSL.Wc_InitRng(&rng); ret != 0 {
+ t.Fatalf("Wc_InitRng: %d", ret)
+ }
+ defer wolfSSL.Wc_FreeRng(&rng)
+ var key wolfSSL.Ecc_key
+ if ret := wolfSSL.Wc_ecc_init(&key); ret != 0 {
+ t.Fatalf("Wc_ecc_init: %d", ret)
+ }
+ defer wolfSSL.Wc_ecc_free(&key)
+ if ret := wolfSSL.Wc_ecc_make_key(&rng, 32, &key); ret != 0 {
+ t.Fatalf("Wc_ecc_make_key: %d", ret)
+ }
+ der := make([]byte, 256)
+ n := wolfSSL.Wc_EccPrivateKeyToDer(&key, der)
+ if n <= 0 {
+ t.Skipf("Wc_EccPrivateKeyToDer: %d", n)
+ }
+ k, err := ParseECPrivateKeyDER(der[:n])
+ if err != nil {
+ t.Fatalf("ParseECPrivateKeyDER(private-only P-256): %v", err)
+ }
+ defer k.Free()
+ if alg := k.Algorithm(); alg != AlgECDSAP256 {
+ t.Fatalf("Algorithm() = %d, want %d", alg, AlgECDSAP256)
+ }
+
+ k2, err := NewEmptyEccKey()
+ if err != nil {
+ t.Fatalf("NewEmptyEccKey: %v", err)
+ }
+ defer k2.Free()
+ idx := 0
+ if ret := wolfSSL.Wc_EccPrivateKeyDecode(der[:n], &idx, k2.Raw(), n); ret != 0 {
+ t.Fatalf("Wc_EccPrivateKeyDecode: %d", ret)
+ }
+ k2.MarkLive()
+ if alg := k2.Algorithm(); alg != AlgECDSAP256 {
+ t.Fatalf("MarkLive on private-only P-256: Algorithm() = %d, want %d", alg, AlgECDSAP256)
+ }
+}
+
+func TestEccKeyConcurrentSignAndFree(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ digest := make([]byte, 32)
+ for i := range digest {
+ digest[i] = byte(i + 1)
+ }
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ for i := 0; i < 8; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ for j := 0; j < 25; j++ {
+ sig, err := SignDigest(k, digest)
+ if err == nil && len(sig) == 0 {
+ t.Error("SignDigest returned an empty signature")
+ }
+ }
+ }()
+ }
+ for i := 0; i < 2; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ k.Free()
+ }()
+ }
+ close(start)
+ wg.Wait()
+ if k.IsLive() {
+ t.Fatal("key still live after Free")
+ }
+ if _, err := SignDigest(k, digest); err == nil {
+ t.Fatal("SignDigest succeeded on a freed key")
+ }
+}
+
+func TestLockEccKeysSerializesDirectUse(t *testing.T) {
+ k1, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k1.Free()
+ k2, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k2.Free()
+ digest := make([]byte, 32)
+ for i := range digest {
+ digest[i] = byte(i + 1)
+ }
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ for i := 0; i < 8; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ <-start
+ signer, other := k1, k2
+ if i%2 == 1 {
+ signer, other = k2, k1
+ }
+ for j := 0; j < 10; j++ {
+ unlock := LockEccKeys(signer, other, signer)
+ if signer.CKeyPtr() != nil && other.CKeyPtr() != nil {
+ sig := make([]byte, wolfSSL.ECC_MAX_SIG_SIZE)
+ sigLen := len(sig)
+ if ret := wolfSSL.Wc_ecc_sign_hash(digest, len(digest), sig, &sigLen,
+ signer.Rng(), signer.Raw()); ret != 0 {
+ t.Errorf("Wc_ecc_sign_hash: %d", ret)
+ }
+ }
+ unlock()
+ }
+ }(i)
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ k1.Free()
+ k2.Free()
+ }()
+ close(start)
+ wg.Wait()
+ if k1.CKeyPtr() != nil || k2.CKeyPtr() != nil {
+ t.Fatal("CKeyPtr non-nil after Free")
+ }
+}
+
+// holdFirstAcquire makes the first locked operation wait inside its key lock
+// until release is closed; entered is closed once it is waiting.
+func holdFirstAcquire(t *testing.T) (entered, release chan struct{}) {
+ entered = make(chan struct{})
+ release = make(chan struct{})
+ var once sync.Once
+ testHookAcquired = func() {
+ once.Do(func() {
+ close(entered)
+ <-release
+ })
+ }
+ t.Cleanup(func() { testHookAcquired = nil })
+ return entered, release
+}
+
+// assertBlocked fails if done closes before release is closed, then releases
+// and waits for done.
+func assertBlocked(t *testing.T, done <-chan struct{}, release chan struct{}, what string) {
+ t.Helper()
+ select {
+ case <-done:
+ t.Fatalf("%s returned while an operation held the key", what)
+ case <-time.After(50 * time.Millisecond):
+ }
+ close(release)
+ <-done
+}
+
+func TestEccKeyFreeWaitsForInFlightSign(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k.Free()
+ digest := make([]byte, 32)
+ for i := range digest {
+ digest[i] = byte(i + 1)
+ }
+ entered, release := holdFirstAcquire(t)
+ signErr := make(chan error, 1)
+ go func() {
+ sig, err := SignDigest(k, digest)
+ if err == nil && len(sig) == 0 {
+ err = errors.New("empty signature")
+ }
+ signErr <- err
+ }()
+ <-entered
+ freed := make(chan struct{})
+ go func() {
+ k.Free()
+ close(freed)
+ }()
+ assertBlocked(t, freed, release, "Free")
+ if err := <-signErr; err != nil {
+ t.Fatalf("in-flight SignDigest: %v", err)
+ }
+ if k.IsLive() || k.Algorithm() != AlgUnknown || k.CKeyPtr() != nil {
+ t.Fatal("freed key still reports live state")
+ }
+ var nilKey *EccKey
+ if nilKey.Algorithm() != AlgUnknown || nilKey.IsLive() {
+ t.Fatal("nil key reports live state")
+ }
+}
+
+func TestEccKeyMarkLiveAfterFreeStaysDead(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ k.Free()
+ k.MarkLive()
+ if k.IsLive() || k.CKeyPtr() != nil {
+ t.Fatal("MarkLive revived a freed key")
+ }
+ if _, err := MarshalSEC1DER(k); err == nil {
+ t.Fatal("MarshalSEC1DER accepted a freed key")
+ }
+}
+
+func TestEccKeyAlgorithmDerivesOnlyMissingPublicPoint(t *testing.T) {
+ var empty wolfSSL.Ecc_key
+ if ret := wolfSSL.Wc_ecc_init(&empty); ret != 0 {
+ t.Fatalf("Wc_ecc_init: %d", ret)
+ }
+ defer wolfSSL.Wc_ecc_free(&empty)
+ if alg, ret := eccKeyAlgorithm(&empty); alg != AlgUnknown || ret >= 0 {
+ t.Fatalf("eccKeyAlgorithm(empty key) = (%d, %d), want (AlgUnknown, <0)", alg, ret)
+ }
+
+ _, pub := eccTestKeyDER(t, 32)
+ var pubKey wolfSSL.Ecc_key
+ if ret := wolfSSL.Wc_ecc_init(&pubKey); ret != 0 {
+ t.Fatalf("Wc_ecc_init: %d", ret)
+ }
+ defer wolfSSL.Wc_ecc_free(&pubKey)
+ idx := 0
+ if ret := wolfSSL.Wc_EccPublicKeyDecode(pub, &idx, &pubKey, len(pub)); ret != 0 {
+ t.Fatalf("Wc_EccPublicKeyDecode: %d", ret)
+ }
+ if alg, ret := eccKeyAlgorithm(&pubKey); alg != AlgECDSAP256 || ret != 0 {
+ t.Fatalf("eccKeyAlgorithm(P-256 public key) = (%d, %d), want (AlgECDSAP256, 0)", alg, ret)
+ }
+}
+
+// recordZeroBytes replaces zeroBytes for the duration of the test and returns
+// a function reporting every buffer passed to it.
+func recordZeroBytes(t *testing.T) func() [][]byte {
+ var mu sync.Mutex
+ var wiped [][]byte
+ orig := zeroBytes
+ zeroBytes = func(b []byte) {
+ orig(b)
+ mu.Lock()
+ wiped = append(wiped, b)
+ mu.Unlock()
+ }
+ t.Cleanup(func() { zeroBytes = orig })
+ return func() [][]byte {
+ mu.Lock()
+ defer mu.Unlock()
+ return wiped
+ }
+}
+
+func allZero(b []byte) bool {
+ for _, v := range b {
+ if v != 0 {
+ return false
+ }
+ }
+ return true
+}
+
+func TestParseECPrivateKeyPEMWipesDER(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k.Free()
+ for _, marshal := range []func(*EccKey) ([]byte, error){MarshalSEC1PEM, MarshalPKCS8PEM} {
+ p, err := marshal(k)
+ if err != nil {
+ t.Fatalf("marshal: %v", err)
+ }
+ wiped := recordZeroBytes(t)
+ k2, err := ParseECPrivateKeyPEM(p)
+ if err != nil {
+ t.Fatalf("ParseECPrivateKeyPEM: %v", err)
+ }
+ k2.Free()
+ found := false
+ for _, b := range wiped() {
+ if len(b) == len(p) && allZero(b) {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatal("decoded private-key DER buffer was not wiped")
+ }
+ }
+}
+
+func TestSignDigestRequiresKeyRng(t *testing.T) {
+ priv, _ := eccTestKeyDER(t, 32)
+ k, err := NewEmptyEccPubKey()
+ if err != nil {
+ t.Fatalf("NewEmptyEccPubKey: %v", err)
+ }
+ defer k.Free()
+ idx := 0
+ if ret := wolfSSL.Wc_EccPrivateKeyDecode(priv, &idx, k.Raw(), len(priv)); ret != 0 {
+ t.Fatalf("Wc_EccPrivateKeyDecode: %d", ret)
+ }
+ k.MarkLive()
+ _, err = SignDigest(k, make([]byte, 32))
+ if err == nil {
+ t.Fatal("SignDigest succeeded on a key without an RNG")
+ }
+ if !strings.HasPrefix(err.Error(), "handles:") {
+ t.Fatalf("SignDigest passed a key without an RNG to wolfCrypt: %v", err)
+ }
+}
+
+func TestMarshalPEMWipesDER(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k.Free()
+ cases := []struct {
+ der func(*EccKey) ([]byte, error)
+ pem func(*EccKey) ([]byte, error)
+ }{
+ {MarshalSEC1DER, MarshalSEC1PEM},
+ {MarshalPKCS8DER, MarshalPKCS8PEM},
+ }
+ for _, c := range cases {
+ der, err := c.der(k)
+ if err != nil {
+ t.Fatalf("marshal DER: %v", err)
+ }
+ wiped := recordZeroBytes(t)
+ if _, err := c.pem(k); err != nil {
+ t.Fatalf("marshal PEM: %v", err)
+ }
+ found := false
+ for _, b := range wiped() {
+ if len(b) >= len(der) && allZero(b) {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatal("temporary private-key DER was not wiped")
+ }
+ }
+}
diff --git a/handles/rsa_handles.go b/handles/rsa_handles.go
index 810ea5e..d44554b 100644
--- a/handles/rsa_handles.go
+++ b/handles/rsa_handles.go
@@ -25,6 +25,8 @@ import (
"errors"
"fmt"
"runtime"
+ "sync"
+ "sync/atomic"
wolfSSL "github.com/wolfssl/go-wolfssl"
)
@@ -36,10 +38,16 @@ import (
// This handle currently only carries enough state for verify
// operations. Sign/decrypt callers will need an RNG attached, mirroring
// EccKey.hasRng — out of scope until needed.
+//
+// The verify helpers lock the key for the duration of each wolfCrypt call,
+// so an RsaKey may be shared between goroutines and Free waits for
+// in-flight operations. Direct users of Raw must hold Lock and check
+// IsLive while holding it.
type RsaKey struct {
+ mu sync.Mutex
raw wolfSSL.RsaKey
- init bool // wc_InitRsaKey has run
- live bool // additionally, key material is populated
+ init bool // wc_InitRsaKey has run
+ live int32 // atomic; additionally, key material is populated
}
// NewEmptyRsaKey returns an RsaKey with wc_InitRsaKey called but no key
@@ -58,25 +66,65 @@ func NewEmptyRsaKey() (*RsaKey, error) {
}
// MarkLive flags the key as fully populated.
-func (k *RsaKey) MarkLive() { k.live = true }
+func (k *RsaKey) MarkLive() {
+ k.mu.Lock()
+ defer k.mu.Unlock()
+ if !k.init {
+ return
+ }
+ atomic.StoreInt32(&k.live, 1)
+}
// IsLive reports whether the key has key material loaded and is not yet
-// freed.
-func (k *RsaKey) IsLive() bool { return k != nil && k.live }
+// freed. The result is stable only while Lock is held.
+func (k *RsaKey) IsLive() bool {
+ return k != nil && atomic.LoadInt32(&k.live) == 1
+}
+
+// acquire locks a live key and returns its unlock function, or returns nil
+// (with the key unlocked) if k is nil or not live.
+func (k *RsaKey) acquire() func() {
+ if k == nil {
+ return nil
+ }
+ k.mu.Lock()
+ if atomic.LoadInt32(&k.live) == 0 {
+ k.mu.Unlock()
+ return nil
+ }
+ if testHookAcquired != nil {
+ testHookAcquired()
+ }
+ return k.mu.Unlock
+}
+
+// Lock serializes direct use of Raw with the helpers in this package and
+// with Free. While holding it, do not call MarkLive, Free or the
+// package-level helpers on k; they lock k.
+func (k *RsaKey) Lock() { k.mu.Lock() }
+
+// Unlock releases the lock taken by Lock.
+func (k *RsaKey) Unlock() { k.mu.Unlock() }
// Free releases the wolfCrypt key. Safe to call multiple times.
func (k *RsaKey) Free() {
- if k == nil || !k.init {
+ if k == nil {
+ return
+ }
+ k.mu.Lock()
+ defer k.mu.Unlock()
+ if !k.init {
return
}
wolfSSL.Wc_FreeRsaKey(&k.raw)
k.init = false
- k.live = false
+ atomic.StoreInt32(&k.live, 0)
runtime.SetFinalizer(k, nil)
}
// Raw returns the underlying wolfCrypt RsaKey for direct use with
-// low-level Wc_* APIs. The returned pointer is invalid after Free.
+// low-level Wc_* APIs. The returned pointer is invalid after Free; hold
+// Lock while using it.
func (k *RsaKey) Raw() *wolfSSL.RsaKey { return &k.raw }
// ImportRsaPublicRaw imports an RSA public key from the raw modulus n
@@ -113,12 +161,15 @@ func checkDigestLen(op string, hashType int, digest []byte) error {
}
// RsaVerifyPSS verifies an RSASSA-PSS signature using
-// MGF1 with the same hash family as hashType. saltLen is fixed at
-// hashLen.
+// MGF1 with the same hash family as hashType. wolfCrypt expects a salt of
+// hashLen bytes, except SHA-512 with a 1024-bit key, which uses
+// RSA_PSS_SALT_MAX_SZ.
func RsaVerifyPSS(pub *RsaKey, hashType int, digest, sig []byte) error {
- if !pub.IsLive() {
+ unlock := pub.acquire()
+ if unlock == nil {
return errors.New("handles: RsaVerifyPSS: key is not live")
}
+ defer unlock()
if len(digest) == 0 || len(sig) == 0 {
return errors.New("handles: RsaVerifyPSS: empty digest or sig")
}
@@ -146,9 +197,11 @@ func RsaVerifyPSS(pub *RsaKey, hashType int, digest, sig []byte) error {
// RsaVerifyPKCS1v15 verifies an RSASSA-PKCS1-v1_5 signature
func RsaVerifyPKCS1v15(pub *RsaKey, hashType int, digest, sig []byte) error {
- if !pub.IsLive() {
+ unlock := pub.acquire()
+ if unlock == nil {
return errors.New("handles: RsaVerifyPKCS1v15: key is not live")
}
+ defer unlock()
if len(digest) == 0 || len(sig) == 0 {
return errors.New("handles: RsaVerifyPKCS1v15: empty digest or sig")
}
diff --git a/handles/rsa_handles_test.go b/handles/rsa_handles_test.go
index e617690..c2c28a4 100644
--- a/handles/rsa_handles_test.go
+++ b/handles/rsa_handles_test.go
@@ -21,6 +21,7 @@
package handles
import (
+ "sync"
"testing"
wolfSSL "github.com/wolfssl/go-wolfssl"
@@ -363,3 +364,125 @@ func TestRsaVerifyPSS_Tamper(t *testing.T) {
t.Fatalf("expected verification failure on tampered PSS signature, got nil")
}
}
+
+func TestRsaKeyConcurrentVerifyAndFree(t *testing.T) {
+ priv, n, e, modBytes, cleanup := decodeRsaTestKey(t)
+ defer cleanup()
+ var rng wolfSSL.WC_RNG
+ if ret := wolfSSL.Wc_InitRng(&rng); ret != 0 {
+ t.Fatalf("wc_InitRng: %d", ret)
+ }
+ defer wolfSSL.Wc_FreeRng(&rng)
+ digest := digestForHash(t, wolfSSL.WC_HASH_TYPE_SHA256, []byte("concurrent"))
+ sig := rsaSignDigest(t, priv, wolfSSL.WC_HASH_TYPE_SHA256, digest, modBytes, &rng)
+ pss := rsaSignDigestPSS(t, priv, wolfSSL.WC_HASH_TYPE_SHA256, wolfSSL.WC_MGF1SHA256, digest, modBytes, &rng)
+
+ pub, err := ImportRsaPublicRaw(n, e)
+ if err != nil {
+ t.Fatalf("ImportRsaPublicRaw: %v", err)
+ }
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ for i := 0; i < 8; i++ {
+ wg.Add(1)
+ go func(i int) {
+ defer wg.Done()
+ <-start
+ for j := 0; j < 10; j++ {
+ if i%2 == 0 {
+ _ = RsaVerifyPKCS1v15(pub, wolfSSL.WC_HASH_TYPE_SHA256, digest, sig)
+ } else {
+ _ = RsaVerifyPSS(pub, wolfSSL.WC_HASH_TYPE_SHA256, digest, pss)
+ }
+ }
+ }(i)
+ }
+ for i := 0; i < 2; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ pub.Free()
+ }()
+ }
+ close(start)
+ wg.Wait()
+ if pub.IsLive() {
+ t.Fatal("key still live after Free")
+ }
+ if err := RsaVerifyPKCS1v15(pub, wolfSSL.WC_HASH_TYPE_SHA256, digest, sig); err == nil {
+ t.Fatal("RsaVerifyPKCS1v15 succeeded on a freed key")
+ }
+}
+
+func TestRsaKeyFreeWaitsForInFlightVerify(t *testing.T) {
+ priv, n, e, modBytes, cleanup := decodeRsaTestKey(t)
+ defer cleanup()
+ var rng wolfSSL.WC_RNG
+ if ret := wolfSSL.Wc_InitRng(&rng); ret != 0 {
+ t.Fatalf("wc_InitRng: %d", ret)
+ }
+ defer wolfSSL.Wc_FreeRng(&rng)
+ digest := digestForHash(t, wolfSSL.WC_HASH_TYPE_SHA256, []byte("in flight"))
+ sig := rsaSignDigest(t, priv, wolfSSL.WC_HASH_TYPE_SHA256, digest, modBytes, &rng)
+ pub, err := ImportRsaPublicRaw(n, e)
+ if err != nil {
+ t.Fatalf("ImportRsaPublicRaw: %v", err)
+ }
+ defer pub.Free()
+
+ entered, release := holdFirstAcquire(t)
+ verifyErr := make(chan error, 1)
+ go func() {
+ verifyErr <- RsaVerifyPKCS1v15(pub, wolfSSL.WC_HASH_TYPE_SHA256, digest, sig)
+ }()
+ <-entered
+ freed := make(chan struct{})
+ go func() {
+ pub.Free()
+ close(freed)
+ }()
+ assertBlocked(t, freed, release, "Free")
+ if err := <-verifyErr; err != nil {
+ t.Fatalf("in-flight RsaVerifyPKCS1v15: %v", err)
+ }
+ if pub.IsLive() {
+ t.Fatal("key still live after Free")
+ }
+}
+
+func TestRsaKeyLivenessCheckableUnderLock(t *testing.T) {
+ _, n, e, _, cleanup := decodeRsaTestKey(t)
+ defer cleanup()
+ pub, err := ImportRsaPublicRaw(n, e)
+ if err != nil {
+ t.Fatalf("ImportRsaPublicRaw: %v", err)
+ }
+ pub.Lock()
+ live := pub.IsLive()
+ pub.Unlock()
+ if !live {
+ t.Fatal("imported key not live under Lock")
+ }
+ pub.Free()
+ pub.Lock()
+ live = pub.IsLive()
+ pub.Unlock()
+ if live {
+ t.Fatal("freed key reported live under Lock")
+ }
+}
+
+func TestRsaKeyMarkLiveAfterFreeStaysDead(t *testing.T) {
+ _, n, e, _, cleanup := decodeRsaTestKey(t)
+ defer cleanup()
+ pub, err := ImportRsaPublicRaw(n, e)
+ if err != nil {
+ t.Fatalf("ImportRsaPublicRaw: %v", err)
+ }
+ pub.Free()
+ pub.MarkLive()
+ if pub.IsLive() {
+ t.Fatal("MarkLive revived a freed key")
+ }
+}
diff --git a/sha256/sha256.go b/sha256/sha256.go
index 1e9b8d8..7314d7c 100644
--- a/sha256/sha256.go
+++ b/sha256/sha256.go
@@ -75,13 +75,17 @@ func (d *digest) Size() int { return Size }
// BlockSize returns the hash's underlying block size.
func (d *digest) BlockSize() int { return BlockSize }
-// Write adds more data to the running hash.
+// sha256Update is replaced by tests to simulate a wolfCrypt failure.
+var sha256Update = wolfSSL.Wc_Sha256Update
+
+// Write adds more data to the running hash. It never returns an error, per
+// the hash.Hash contract; a wolfCrypt failure panics.
func (d *digest) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
- if ret := wolfSSL.Wc_Sha256Update(&d.sha, p, len(p)); ret != 0 {
- return 0, fmt.Errorf("wolfSSL/sha256: Wc_Sha256Update: %d", ret)
+ if ret := sha256Update(&d.sha, p, len(p)); ret != 0 {
+ panic(fmt.Sprintf("wolfSSL/sha256: Wc_Sha256Update: %d", ret))
}
return len(p), nil
}
diff --git a/sha256/sha256_test.go b/sha256/sha256_test.go
new file mode 100644
index 0000000..c418815
--- /dev/null
+++ b/sha256/sha256_test.go
@@ -0,0 +1,64 @@
+/* sha256_test.go
+ *
+ * Copyright (C) 2006-2026 wolfSSL Inc.
+ *
+ * This file is part of wolfSSL.
+ *
+ * wolfSSL is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfSSL is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package sha256
+
+import (
+ "bytes"
+ stdsha256 "crypto/sha256"
+ "strings"
+ "testing"
+
+ wolfSSL "github.com/wolfssl/go-wolfssl"
+)
+
+func TestDigestWriteMatchesStdlib(t *testing.T) {
+ h := New()
+ msg := []byte("abc")
+ n, err := h.Write(msg)
+ if n != len(msg) || err != nil {
+ t.Fatalf("Write = (%d, %v), want (%d, nil)", n, err, len(msg))
+ }
+ want := stdsha256.Sum256(msg)
+ if got := h.Sum(nil); !bytes.Equal(got, want[:]) {
+ t.Fatalf("Sum = %x, want %x", got, want)
+ }
+}
+
+func TestDigestWriteFailurePanics(t *testing.T) {
+ orig := sha256Update
+ sha256Update = func(sha *wolfSSL.Wc_Sha256, data []byte, sz int) int {
+ return wolfSSL.BAD_FUNC_ARG
+ }
+ defer func() { sha256Update = orig }()
+
+ d := New().(*digest)
+ defer func() {
+ r := recover()
+ if r == nil {
+ t.Fatal("Write did not panic on a wolfCrypt update failure")
+ }
+ if s, ok := r.(string); !ok || !strings.Contains(s, "Wc_Sha256Update") {
+ t.Fatalf("unexpected panic: %v", r)
+ }
+ }()
+ n, err := d.Write([]byte("abc"))
+ t.Errorf("Write returned (%d, %v) instead of panicking", n, err)
+}
diff --git a/wolfx509/certgen_lock_test.go b/wolfx509/certgen_lock_test.go
new file mode 100644
index 0000000..cd92c78
--- /dev/null
+++ b/wolfx509/certgen_lock_test.go
@@ -0,0 +1,87 @@
+/* certgen_lock_test.go
+ *
+ * Copyright (C) 2006-2026 wolfSSL Inc.
+ *
+ * This file is part of wolfSSL.
+ *
+ * wolfSSL is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfSSL is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package wolfx509
+
+import (
+ "sync"
+ "testing"
+ "time"
+)
+
+func TestCreateCertificateRequestHoldsKeyLock(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ defer k.Free()
+ tmpl := &CertificateRequest{Subject: Name{CommonName: "lock.example"}}
+
+ k.Lock()
+ done := make(chan error, 1)
+ go func() {
+ _, err := CreateCertificateRequest(tmpl, k)
+ done <- err
+ }()
+ select {
+ case err := <-done:
+ k.Unlock()
+ t.Fatalf("CreateCertificateRequest ran while the key was locked (err=%v)", err)
+ case <-time.After(50 * time.Millisecond):
+ }
+ k.Unlock()
+ if err := <-done; err != nil {
+ t.Fatalf("CreateCertificateRequest: %v", err)
+ }
+}
+
+func TestCreateCertificateRequestConcurrentWithKeyFree(t *testing.T) {
+ k, err := GenerateP256Key()
+ if err != nil {
+ t.Fatalf("GenerateP256Key: %v", err)
+ }
+ tmpl := &CertificateRequest{Subject: Name{CommonName: "race.example"}}
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ for i := 0; i < 4; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ for j := 0; j < 10; j++ {
+ der, err := CreateCertificateRequest(tmpl, k)
+ if err == nil && len(der) == 0 {
+ t.Error("CreateCertificateRequest returned empty DER")
+ }
+ }
+ }()
+ }
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ k.Free()
+ }()
+ close(start)
+ wg.Wait()
+ if _, err := CreateCertificateRequest(tmpl, k); err == nil {
+ t.Fatal("CreateCertificateRequest succeeded with a freed key")
+ }
+}
diff --git a/wolfx509/certgen_wolfcrypt.go b/wolfx509/certgen_wolfcrypt.go
index 5b5a0f0..7dd358b 100644
--- a/wolfx509/certgen_wolfcrypt.go
+++ b/wolfx509/certgen_wolfcrypt.go
@@ -156,6 +156,18 @@ func makeAndSignCSR(opts certBuildOpts, key KeyHandle) ([]byte, error) {
return buildAndSignCert(opts, nil, key, key, true)
}
+// lockKeyHandles locks the *handles.EccKey values among keys for the
+// duration of a C-side build and returns the matching unlock function.
+func lockKeyHandles(keys ...KeyHandle) func() {
+ var ecc []*handles.EccKey
+ for _, k := range keys {
+ if e, ok := k.(*handles.EccKey); ok {
+ ecc = append(ecc, e)
+ }
+ }
+ return handles.LockEccKeys(ecc...)
+}
+
// buildAndSignCert dispatches the C-side cert builder on pubKey.Algorithm().
// parentDER, if non-nil, supplies the issuer name. isCSR routes through
// wc_MakeCertReq (no RNG) instead of wc_MakeCert.
@@ -166,6 +178,11 @@ func buildAndSignCert(opts certBuildOpts, parentDER []byte, pubKey, signerKey Ke
if signerKey == nil {
return nil, errors.New("wolfx509: signerKey is nil")
}
+ unlock := lockKeyHandles(pubKey, signerKey)
+ defer unlock()
+ if pubKey.CKeyPtr() == nil || signerKey.CKeyPtr() == nil {
+ return nil, errors.New("wolfx509: key is not live")
+ }
if signerKey.CRngPtr() == nil {
return nil, errors.New("wolfx509: signerKey has no RNG, and cannot sign")
}