Skip to content

Commit fd7325d

Browse files
authored
Limit PKCS11 session creation (#49)
* Limit PKCS11 session creation The PKCS#11 BCCSP previously fell back to OpenSession unbounded whenever the session cache was empty, allowing the number of concurrently checked out sessions to grow with the number of concurrent callers. Under high sign concurrency this surfaces from HSM/PKCS#11 libraries as CKR_SESSION_COUNT on OpenSession and CKR_DEVICE_ERROR on subsequent operations (see issue #50). Gate concurrent OpenSession calls behind a semaphore.Weighted whose weight equals sessionCacheSize, so the number of outstanding sessions is bounded. Cached sessions in sessPool intentionally do not occupy a slot: getSession reacquires its own slot when pulling one out, and returnSession releases the slot when caching, so a caller blocked on the bound is unblocked the moment another session is returned or closed. Expose the bound via a new public PKCS11Opts.SessionCacheSize field (with json/yaml/mapstructure tags) so it can be configured through the existing BCCSP factory configuration alongside Library/Label/Pin etc., with no environment variable required. A value of 0 selects the default (30); a negative value disables the bound and preserves the original unbounded code path for callers that want it. Update closeSession to release the slot only for sessions that were previously registered, guarding against double-release when an open succeeds but the subsequent Login fails and the partial session is closed before being tracked. Tests in pkcs11_test.go (TestPKCS11GetSession, TestSessionHandleCaching) exercise the bound: they fill the cache, spawn an extra getSession in a goroutine, and assert it blocks until a session is returned. Without this change those tests would fail because the goroutine would proceed straight through to createSession instead of blocking. Signed-off-by: Evan <evanyan@sign.global> * pkcs11: simplify returnSession to a direct send sessPool and sessSem now share cacheSize as capacity, so the channel send in returnSession can never block: the caller still holds an outstanding sem.Acquire(1) from its prior getSession(), which bounds acquired-but-not-returned sessions A >= 1, leaving cached L <= cacheSize-1. The select+default branch was therefore unreachable; remove it so the control flow reflects the actual invariant. Signed-off-by: Evan <evanyan@sign.global> --------- Signed-off-by: Evan <evanyan@sign.global>
1 parent 8fe16c9 commit fd7325d

6 files changed

Lines changed: 385 additions & 117 deletions

File tree

bccsp/pkcs11/conf.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ type PKCS11Opts struct {
2121
Hash string `json:"hash"`
2222

2323
// PKCS11 options
24-
Library string `json:"library"`
25-
Label string `json:"label"`
26-
Pin string `json:"pin"`
27-
SoftwareVerify bool `json:"softwareverify,omitempty"`
28-
Immutable bool `json:"immutable,omitempty"`
29-
AltID string `json:"altid,omitempty"`
30-
KeyIDs []KeyIDMapping `json:"keyids,omitempty" mapstructure:"keyids"`
31-
32-
sessionCacheSize int
24+
Library string `json:"library"`
25+
Label string `json:"label"`
26+
Pin string `json:"pin"`
27+
SoftwareVerify bool `json:"softwareverify,omitempty"`
28+
Immutable bool `json:"immutable,omitempty"`
29+
AltID string `json:"altid,omitempty"`
30+
KeyIDs []KeyIDMapping `json:"keyids,omitempty" mapstructure:"keyids"`
31+
SessionCacheSize uint `json:"session_cache_size,omitempty"`
32+
33+
// createSessionRetries / createSessionRetryDelay are internal overrides
34+
// intended for tests.
3335
createSessionRetries int
3436
createSessionRetryDelay time.Duration
3537
}

bccsp/pkcs11/pkcs11.go

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0
77
package pkcs11
88

99
import (
10+
"context"
1011
"crypto/ecdsa"
1112
"crypto/elliptic"
1213
"crypto/sha256"
@@ -26,6 +27,7 @@ import (
2627
"github.com/miekg/pkcs11"
2728
"github.com/pkg/errors"
2829
"go.uber.org/zap/zapcore"
30+
"golang.org/x/sync/semaphore"
2931
)
3032

3133
var (
@@ -49,6 +51,11 @@ type Provider struct {
4951

5052
sessLock sync.Mutex
5153
sessPool chan pkcs11.SessionHandle
54+
// sessSem bounds the number of concurrently outstanding (checked-out)
55+
// sessions. Cached sessions in sessPool do not hold a slot; the slot is
56+
// released on returnSession (when the session is cached) or on
57+
// closeSession.
58+
sessSem *semaphore.Weighted
5259
sessions map[pkcs11.SessionHandle]struct{}
5360

5461
cacheLock sync.RWMutex
@@ -90,8 +97,9 @@ func New(opts PKCS11Opts, keyStore bccsp.KeyStore, options ...Option) (*Provider
9097
return nil, errors.Wrapf(err, "Failed initializing fallback SW BCCSP")
9198
}
9299

93-
if opts.sessionCacheSize == 0 {
94-
opts.sessionCacheSize = defaultSessionCacheSize
100+
cacheSize := int(opts.SessionCacheSize)
101+
if cacheSize == 0 {
102+
cacheSize = defaultSessionCacheSize
95103
}
96104
if opts.createSessionRetries == 0 {
97105
opts.createSessionRetries = defaultCreateSessionRetries
@@ -100,18 +108,14 @@ func New(opts PKCS11Opts, keyStore bccsp.KeyStore, options ...Option) (*Provider
100108
opts.createSessionRetryDelay = defaultCreateSessionRetryDelay
101109
}
102110

103-
var sessPool chan pkcs11.SessionHandle
104-
if opts.sessionCacheSize > 0 {
105-
sessPool = make(chan pkcs11.SessionHandle, opts.sessionCacheSize)
106-
}
107-
108111
csp := &Provider{
109112
BCCSP: swCSP,
110113
curve: curve,
111114
getKeyIDForSKI: func(ski []byte) []byte { return ski },
112115
createSessionRetries: opts.createSessionRetries,
113116
createSessionRetryDelay: opts.createSessionRetryDelay,
114-
sessPool: sessPool,
117+
sessPool: make(chan pkcs11.SessionHandle, cacheSize),
118+
sessSem: semaphore.NewWeighted(int64(cacheSize)),
115119
sessions: map[pkcs11.SessionHandle]struct{}{},
116120
handleCache: map[string]pkcs11.ObjectHandle{},
117121
keyCache: map[string]bccsp.Key{},
@@ -161,7 +165,7 @@ func (csp *Provider) initialize(opts PKCS11Opts) (*Provider, error) {
161165
csp.ctx = ctx
162166
csp.pin = opts.Pin
163167

164-
session, err := csp.createSession()
168+
session, err := csp.getSession()
165169
if err != nil {
166170
return nil, err
167171
}
@@ -329,16 +333,37 @@ func (csp *Provider) verifyECDSA(k ecdsaPublicKey, signature, digest []byte) (bo
329333
return csp.verifyP11ECDSA(k.ski, digest, r, s, k.pub.Curve.Params().BitSize/8)
330334
}
331335

336+
// getSession returns a session for the caller to use. If a cached session is
337+
// available it is returned; otherwise a new session is opened, gated by the
338+
// sessSem semaphore so the number of concurrently outstanding sessions never
339+
// exceeds SessionCacheSize.
340+
//
341+
// Slot accounting:
342+
// - Acquire one slot up front, before either reusing a cached session or
343+
// opening a new one. The slot represents the resulting outstanding session.
344+
// - returnSession releases the slot when the session is successfully cached.
345+
// - closeSession releases the slot when a known session is closed.
346+
//
347+
// This intentionally keeps cached sessions out of the slot count: a session
348+
// sitting in sessPool is not in-flight and should not block a caller waiting
349+
// for a slot. A caller that subsequently pulls that cached session out will
350+
// reacquire its own slot.
332351
func (csp *Provider) getSession() (session pkcs11.SessionHandle, err error) {
333-
for {
334-
select {
335-
case session = <-csp.sessPool:
336-
return
337-
default:
338-
// cache is empty (or completely in use), create a new session
339-
return csp.createSession()
340-
}
352+
if err = csp.sessSem.Acquire(context.Background(), 1); err != nil {
353+
return 0, errors.Wrap(err, "acquire session slot")
341354
}
355+
356+
select {
357+
case session = <-csp.sessPool:
358+
return session, nil
359+
default:
360+
}
361+
362+
session, err = csp.createSession()
363+
if err != nil {
364+
csp.sessSem.Release(1)
365+
}
366+
return session, err
342367
}
343368

344369
func (csp *Provider) createSession() (pkcs11.SessionHandle, error) {
@@ -379,23 +404,19 @@ func (csp *Provider) closeSession(session pkcs11.SessionHandle) {
379404
}
380405

381406
csp.sessLock.Lock()
382-
defer csp.sessLock.Unlock()
383-
384-
// purge the handle cache if the last session closes
385407
delete(csp.sessions, session)
408+
// purge the handle cache if the last session closes
386409
if len(csp.sessions) == 0 {
387410
csp.clearCaches()
388411
}
412+
csp.sessLock.Unlock()
413+
414+
csp.sessSem.Release(1)
389415
}
390416

391417
func (csp *Provider) returnSession(session pkcs11.SessionHandle) {
392-
select {
393-
case csp.sessPool <- session:
394-
// returned session back to session cache
395-
default:
396-
// have plenty of sessions in cache, dropping
397-
csp.closeSession(session)
398-
}
418+
csp.sessPool <- session
419+
csp.sessSem.Release(1)
399420
}
400421

401422
// Look for an EC key by SKI, stored in CKA_ID

0 commit comments

Comments
 (0)