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") }