7474# remain short-lived and are opened separately for each signing operation.
7575_pkcs11_provider_cache = {}
7676_pkcs11_provider_cache_lock = threading .Lock ()
77+ _pkcs11_signing_locks = {}
7778
7879
7980@pipe
@@ -1226,6 +1227,30 @@ def _select_pkcs11_provider(
12261227 return provider
12271228
12281229
1230+ def _sign_pkcs11 (provider , module_path , pin , label , xml , * , reference_id = None , cert_pem = None ):
1231+ """Serialize token logins and release authenticated objects even on errors."""
1232+ # Different selectors can resolve to the same token. Lock its actual slot,
1233+ # rather than the configured label/serial/slot selector or key label.
1234+ token = (os .path .realpath (module_path ), provider .slot_id )
1235+ with _pkcs11_provider_cache_lock :
1236+ lock = _pkcs11_signing_locks .setdefault (token , threading .Lock ())
1237+
1238+ with lock :
1239+ session = signer = ctx = None
1240+ try :
1241+ session = provider .open_session (pin )
1242+ signer = pybergshamra .Pkcs11Signer (session , label , pybergshamra .Algorithm .RSA_SHA256 )
1243+ ctx = pybergshamra .DsigContext (pybergshamra .KeysManager ())
1244+ ctx .set_hsm_signer (signer )
1245+ return pybergshamra .sign_enveloped (ctx , xml , reference_id = reference_id , cert_pem = cert_pem )
1246+ finally :
1247+ # Exception tracebacks retain Python frames. Clear every local
1248+ # owner before unlocking so a failed request cannot retain login.
1249+ ctx = None
1250+ signer = None
1251+ session = None
1252+
1253+
12291254@pipe
12301255def sign (req : Plumbing .Request , * _opts ):
12311256 """
@@ -1255,7 +1280,7 @@ def sign(req: Plumbing.Request, *_opts):
12551280
12561281 The pin parameter can be used to point to an environment variable containing the pin: "env:<ENV variable>".
12571282 By default pin is "env:PYKCS11PIN" which tells sign to use the pin found in the PYKCS11PIN environment
1258- variable. This is also the default for PyKCS11 which is used to communicate with the PKCS#11 module .
1283+ variable. Authentication and signing are serialized per token within the worker process .
12591284
12601285 **Examples**
12611286
@@ -1310,9 +1335,6 @@ def sign(req: Plumbing.Request, *_opts):
13101335 with open (cert_file ) as fd :
13111336 cert_pem = fd .read ()
13121337
1313- mgr = pybergshamra .KeysManager ()
1314- ctx = pybergshamra .DsigContext (mgr )
1315-
13161338 is_pkcs11_key = key_file .startswith ("pkcs11:" )
13171339 if is_pkcs11_key :
13181340 # HSM signing via PKCS#11. The URI form is:
@@ -1334,27 +1356,22 @@ def sign(req: Plumbing.Request, *_opts):
13341356 token_label = req .args .get ('token_label' ),
13351357 token_serial = req .args .get ('token_serial' ),
13361358 )
1337- session = provider .open_session (pin )
1338- signer = pybergshamra .Pkcs11Signer (session , label , pybergshamra .Algorithm .RSA_SHA256 )
1339- ctx .set_hsm_signer (signer )
1359+ # Keep HSM signing on an owned XML string. Authentication and all
1360+ # operation objects must be released before the next token login.
1361+ xml = etree .tostring (relt , encoding = 'unicode' )
1362+ signed = _sign_pkcs11 (provider , module_path , pin , label , xml , reference_id = idattr , cert_pem = cert_pem )
1363+ req .t = root (etree .fromstring (signed ))
1364+ return req .t
13401365 else :
13411366 # On-disk private key path.
1367+ mgr = pybergshamra .KeysManager ()
1368+ ctx = pybergshamra .DsigContext (mgr )
13421369 key = pybergshamra .load_key_file (key_file )
13431370 mgr .add_key (key )
13441371
1345- if not is_pkcs11_key and is_document_root (relt ):
1346- # Sign the working tree in place through the shared native DOM
1347- # (pyuppsala document handle). This avoids serializing the whole
1348- # aggregate, the signer's internal re-parses, and the reparse of the
1349- # signed result -- downstream pipes keep the very same tree, with the
1350- # <ds:Signature> inserted as the root's first child.
1351- #
1352- # PKCS#11 signing deliberately uses the owned-string path below. In
1353- # production, mutating a live pyuppsala aggregate through the native
1354- # document capsule while a vendor PKCS#11 module signs has caused
1355- # worker SIGSEGVs and partially serialized aggregates. Keeping the HSM
1356- # operation on an owned document avoids that unsafe integration
1357- # boundary while preserving the native fast path for software keys.
1372+ if is_document_root (relt ):
1373+ # The document API uses an owned XML handoff and imports the signed
1374+ # result into the existing document, preserving downstream tree identity.
13581375 #
13591376 # Only valid when ``relt`` is the document root. After ``first`` (an
13601377 # MDQ single-entity request) the working document is an
@@ -1366,7 +1383,7 @@ def sign(req: Plumbing.Request, *_opts):
13661383 pybergshamra .sign_enveloped_document (ctx , doc , reference_id = idattr , cert_pem = cert_pem )
13671384 req .t = relt
13681385 else :
1369- # PKCS11 signing, or a subtree of a larger document (small, so the
1386+ # A subtree of a larger document (small, so the
13701387 # string round trip is cheap): serialize the working document, build
13711388 # the enveloped signature, sign, and reparse so downstream pipes see a
13721389 # standalone pyuppsala element.
0 commit comments