pybiss 0.2.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pybiss-0.2.0 → pybiss-0.3.2}/PKG-INFO +2 -2
- {pybiss-0.2.0 → pybiss-0.3.2}/pyproject.toml +2 -2
- {pybiss-0.2.0 → pybiss-0.3.2}/src/app.py +18 -102
- pybiss-0.3.2/src/card_access.py +66 -0
- pybiss-0.3.2/src/card_service.py +158 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/ccid.py +1 -1
- {pybiss-0.2.0 → pybiss-0.3.2}/src/chain_fetch.py +2 -2
- {pybiss-0.2.0 → pybiss-0.3.2}/src/dashboard.py +13 -29
- {pybiss-0.2.0 → pybiss-0.3.2}/src/fonts.py +12 -11
- pybiss-0.3.2/src/mcp.py +340 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/PKG-INFO +2 -2
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/SOURCES.txt +5 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/top_level.txt +3 -0
- pybiss-0.3.2/src/tkinter_mods.py +3323 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/transport.py +10 -6
- {pybiss-0.2.0 → pybiss-0.3.2}/src/update_check.py +2 -2
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_app_apdu_backend.py +8 -8
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_app_fidelity.py +4 -5
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_card_fallback.py +16 -16
- pybiss-0.3.2/tests/test_card_service.py +180 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_f_ccid.py +1 -1
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_gui.py +37 -17
- pybiss-0.3.2/tests/test_mcp.py +85 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sign_flow.py +8 -8
- pybiss-0.3.2/tests/test_tkinter_mods.py +1473 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_vpcd.py +1 -1
- pybiss-0.2.0/src/tkinter_mods.py +0 -1037
- pybiss-0.2.0/tests/test_tkinter_mods.py +0 -247
- {pybiss-0.2.0 → pybiss-0.3.2}/Readme.md +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/setup.cfg +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/__init__.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/backends.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/cards.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/cert_parser.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/config.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/detector.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/download_assets.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/hardware.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/i18n.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/jws.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/mscapi.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pkcs11_funcs.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pkcs11_types.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pkcs15.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/dependency_links.txt +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/sc_apdu.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/sc_ffi.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/sc_types.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/server.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/service.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/spkac.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/tracing.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/tui.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/ui_bridge.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/src/verifier.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_backends.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_btrust_cards.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_ccid.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_ccid_transports.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_cert_parser.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_chain_fetch.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_detector.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_hardware.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_idprime_minidriver.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_jws.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_mscapi.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pcsc_daemon.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pkcs11_funcs.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pkcs11_types.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pkcs15.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sc_apdu.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sc_types.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sc_types_abi.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_server.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_service.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_spkac.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_tracing.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_transport.py +0 -0
- {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_verifier.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybiss
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Unofficial BISS implementation in Python. Fast, modern, self contained
|
|
5
5
|
Project-URL: Homepage, https://github.com/h5rdly/pybiss
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pybiss"
|
|
3
|
-
version = "0.2
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Unofficial BISS implementation in Python. Fast, modern, self contained"
|
|
5
5
|
readme = "Readme.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
7
|
dependencies = []
|
|
8
8
|
|
|
9
9
|
[project.urls]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import base64, functools, hashlib, os, sys
|
|
2
2
|
|
|
3
|
-
from . import detector, hardware, service, jws, transport, backends
|
|
3
|
+
from . import detector, hardware, service, jws, transport, backends, card_service, card_access
|
|
4
4
|
from .cards import BTrustDriver
|
|
5
5
|
from .cert_parser import pem_to_der
|
|
6
6
|
from .config import config
|
|
@@ -95,11 +95,8 @@ def set_pin(req, payload):
|
|
|
95
95
|
if not old_pin or not new_pin:
|
|
96
96
|
return {'status': 'error', 'reasonCode': 401, 'reasonText': 'PIN entry canceled'}
|
|
97
97
|
|
|
98
|
-
lib_path = detector.auto_detect_library()
|
|
99
98
|
try:
|
|
100
|
-
|
|
101
|
-
slots = hardware.get_slots(funcs, token_present=True)
|
|
102
|
-
hardware.change_card_pin(funcs, slots[0], old_pin, new_pin)
|
|
99
|
+
card_service.change_pin(old_pin, new_pin)
|
|
103
100
|
return {'confirm': '0', 'status': 'ok', 'reasonCode': 200, 'reasonText': 'PIN successfully changed'}
|
|
104
101
|
except Exception as e:
|
|
105
102
|
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
@@ -115,11 +112,8 @@ def set_puk(req, payload):
|
|
|
115
112
|
if not old_puk or not new_puk:
|
|
116
113
|
return {'status': 'error', 'reasonCode': 401, 'reasonText': 'PUK entry canceled'}
|
|
117
114
|
|
|
118
|
-
lib_path = detector.auto_detect_library()
|
|
119
115
|
try:
|
|
120
|
-
|
|
121
|
-
slots = hardware.get_slots(funcs, token_present=True)
|
|
122
|
-
hardware.change_card_puk(funcs, slots[0], old_puk, new_puk)
|
|
116
|
+
card_service.change_puk(old_puk, new_puk)
|
|
123
117
|
return {'confirm': '0', 'status': 'ok', 'reasonCode': 200, 'reasonText': 'PUK successfully changed'}
|
|
124
118
|
except Exception as e:
|
|
125
119
|
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
@@ -135,11 +129,8 @@ def unblock_pin(req, payload):
|
|
|
135
129
|
if not puk or not new_pin:
|
|
136
130
|
return {'status': 'error', 'reasonCode': 401, 'reasonText': 'PUK entry canceled'}
|
|
137
131
|
|
|
138
|
-
lib_path = detector.auto_detect_library()
|
|
139
132
|
try:
|
|
140
|
-
|
|
141
|
-
slots = hardware.get_slots(funcs, token_present=True)
|
|
142
|
-
hardware.unblock_card_pin(funcs, slots[0], puk, new_pin)
|
|
133
|
+
card_service.unblock_pin(puk, new_pin)
|
|
143
134
|
return {'confirm': '0', 'status': 'ok', 'reasonCode': 200, 'reasonText': 'PIN successfully unblocked'}
|
|
144
135
|
except Exception as e:
|
|
145
136
|
return {'status': 'error', 'reasonCode': 403, 'reasonText': str(e)}
|
|
@@ -177,7 +168,7 @@ def gen_spkac(req, payload):
|
|
|
177
168
|
|
|
178
169
|
try:
|
|
179
170
|
key_size = int(payload.get('keySize') or 2048)
|
|
180
|
-
except
|
|
171
|
+
except TypeError, ValueError:
|
|
181
172
|
return {'status': 'error', 'reasonCode': 400, 'reasonText': reason('error.request.bad-type')}
|
|
182
173
|
|
|
183
174
|
key_id = (payload.get('serialNum') or 'btrust-renewal').encode('utf-8')[:16]
|
|
@@ -188,7 +179,7 @@ def gen_spkac(req, payload):
|
|
|
188
179
|
return {'status': 'error', 'reasonCode': 401, 'reasonText': reason('error.user-canceled')}
|
|
189
180
|
|
|
190
181
|
try:
|
|
191
|
-
funcs = _load_pkcs11()
|
|
182
|
+
funcs = card_access._load_pkcs11()
|
|
192
183
|
except backends.BackendUnavailable:
|
|
193
184
|
return {'status': 'error', 'reasonCode': 400, 'reasonText': reason('error.no-card-found')}
|
|
194
185
|
|
|
@@ -240,11 +231,8 @@ def write_cert_sc(req, payload):
|
|
|
240
231
|
if not pin:
|
|
241
232
|
return {'confirm': None, 'status': 'error', 'reasonCode': 401, 'reasonText': reason('error.user-canceled')}
|
|
242
233
|
|
|
243
|
-
lib_path = detector.auto_detect_library()
|
|
244
234
|
try:
|
|
245
|
-
|
|
246
|
-
slots = hardware.get_slots(funcs, token_present=True)
|
|
247
|
-
hardware.write_certificate(funcs, slots[0], pin, cert_der)
|
|
235
|
+
card_service.import_certificate(cert_der, pin)
|
|
248
236
|
return {'confirm': '0', 'status': 'ok', 'reasonCode': 200, 'reasonText': reason('response.success')}
|
|
249
237
|
except Exception as e:
|
|
250
238
|
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
@@ -255,17 +243,8 @@ def write_cert_sc(req, payload):
|
|
|
255
243
|
def get_info(req, payload):
|
|
256
244
|
''' Full token spec sheet, in the daemon's flat response shape '''
|
|
257
245
|
|
|
258
|
-
lib_path = detector.auto_detect_library()
|
|
259
|
-
if not lib_path:
|
|
260
|
-
return {'status': 'error', 'reasonCode': 400, 'reasonText': reason('error.no-card-found')}
|
|
261
|
-
|
|
262
246
|
try:
|
|
263
|
-
|
|
264
|
-
slots = hardware.get_slots(funcs, token_present=True)
|
|
265
|
-
if not slots:
|
|
266
|
-
return {'status': 'error', 'reasonCode': 404, 'reasonText': reason('error.token-not-found')}
|
|
267
|
-
|
|
268
|
-
info = hardware.get_token_info_extended(funcs, slots[0])
|
|
247
|
+
info = card_service.token_info()
|
|
269
248
|
provider = next((p for p in map(detector.identify_provider, detector.get_connected_atrs()) if p), '')
|
|
270
249
|
# Flatten to the official C0030ai field names
|
|
271
250
|
return {
|
|
@@ -281,6 +260,8 @@ def get_info(req, payload):
|
|
|
281
260
|
'cardData': [],
|
|
282
261
|
'status': 'ok', 'reasonCode': 200, 'reasonText': reason('response.success'),
|
|
283
262
|
}
|
|
263
|
+
except backends.BackendUnavailable:
|
|
264
|
+
return {'status': 'error', 'reasonCode': 404, 'reasonText': reason('error.token-not-found')}
|
|
284
265
|
except Exception as e:
|
|
285
266
|
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
286
267
|
|
|
@@ -293,7 +274,7 @@ def gen_rsa_pair(req, payload):
|
|
|
293
274
|
key_id = (payload.get('serNum') or payload.get('keyId') or 'btrust-key').encode('utf-8')[:16]
|
|
294
275
|
try:
|
|
295
276
|
key_size = int(payload.get('keySize') or 2048)
|
|
296
|
-
except
|
|
277
|
+
except TypeError, ValueError:
|
|
297
278
|
return {'status': 'error', 'reasonCode': 400, 'reasonText': reason('error.request.bad-type')}
|
|
298
279
|
|
|
299
280
|
ui_provider = get_ui_provider()
|
|
@@ -326,7 +307,7 @@ def renew_cert(req, payload):
|
|
|
326
307
|
|
|
327
308
|
try:
|
|
328
309
|
key_size = int(payload.get('keySize') or 2048)
|
|
329
|
-
except
|
|
310
|
+
except TypeError, ValueError:
|
|
330
311
|
return {'status': 'error', 'reasonCode': 400, 'reasonText': reason('error.request.bad-type')}
|
|
331
312
|
key_id = (payload.get('serNum') or payload.get('keyId') or 'RENEWAL').encode('utf-8')[:16]
|
|
332
313
|
|
|
@@ -405,16 +386,6 @@ def del_cert(req, payload):
|
|
|
405
386
|
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
406
387
|
|
|
407
388
|
|
|
408
|
-
def _detected_atr() -> str:
|
|
409
|
-
''' First connected card ATR (upper hex), or '' - used to pick a backend '''
|
|
410
|
-
|
|
411
|
-
try:
|
|
412
|
-
atrs = detector.get_connected_atrs(ccid_fallback=True)
|
|
413
|
-
return atrs[0].upper() if atrs else ''
|
|
414
|
-
except Exception:
|
|
415
|
-
return ''
|
|
416
|
-
|
|
417
|
-
|
|
418
389
|
def _aia_resolver():
|
|
419
390
|
''' Fetches missing issuer certs over AIA, caching into the user config dir. '''
|
|
420
391
|
|
|
@@ -449,17 +420,8 @@ def _getsigner_cng(selector, show_valid_certs, ui_provider):
|
|
|
449
420
|
mscapi.free_windows_certificate(c['windows_ctx'])
|
|
450
421
|
|
|
451
422
|
|
|
452
|
-
def _load_pkcs11():
|
|
453
|
-
''' Test seam: resolve the vendor library and return its PKCS#11 function table '''
|
|
454
|
-
|
|
455
|
-
lib_path = detector.auto_detect_library()
|
|
456
|
-
if not lib_path:
|
|
457
|
-
raise backends.BackendUnavailable('No PKCS#11 library for the detected card')
|
|
458
|
-
return hardware.load_pkcs11(lib_path)
|
|
459
|
-
|
|
460
|
-
|
|
461
423
|
def _getsigner_pkcs11(selector, show_valid_certs, ui_provider):
|
|
462
|
-
funcs = _load_pkcs11()
|
|
424
|
+
funcs = card_access._load_pkcs11()
|
|
463
425
|
slots = hardware.get_slots(funcs, token_present=True)
|
|
464
426
|
if not slots:
|
|
465
427
|
raise backends.BackendUnavailable('No token present')
|
|
@@ -482,59 +444,13 @@ def _getsigner_pkcs11(selector, show_valid_certs, ui_provider):
|
|
|
482
444
|
return _select_and_build_chain(filtered, [c['der'] for c in all_certs], ui_provider)
|
|
483
445
|
|
|
484
446
|
|
|
485
|
-
def _pcsc_factory():
|
|
486
|
-
from .sc_ffi import SmartCardConnection
|
|
487
|
-
return SmartCardConnection()
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
def _ccid_factory():
|
|
491
|
-
from .ccid import CcidConnection
|
|
492
|
-
return CcidConnection()
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
class CardUnreachable(Exception):
|
|
496
|
-
''' Neither PC/SC nor direct CCID could reach a card; carries both reasons '''
|
|
497
|
-
|
|
498
|
-
def __init__(self, pcsc_error, ccid_error):
|
|
499
|
-
self.pcsc_error, self.ccid_error = pcsc_error, ccid_error
|
|
500
|
-
super().__init__(f'PC/SC: {pcsc_error} | direct CCID: {ccid_error}')
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
class _CardConnection:
|
|
504
|
-
''' Open the card over PC/SC, falling back to direct USB CCID '''
|
|
505
|
-
|
|
506
|
-
def __init__(self, pcsc_factory=_pcsc_factory, ccid_factory=_ccid_factory):
|
|
507
|
-
self._pcsc_factory, self._ccid_factory = pcsc_factory, ccid_factory
|
|
508
|
-
self._inner = None
|
|
509
|
-
|
|
510
|
-
def __enter__(self):
|
|
511
|
-
try:
|
|
512
|
-
inner = self._pcsc_factory()
|
|
513
|
-
conn = inner.__enter__()
|
|
514
|
-
except Exception as pcsc_error:
|
|
515
|
-
try:
|
|
516
|
-
inner = self._ccid_factory()
|
|
517
|
-
conn = inner.__enter__()
|
|
518
|
-
except Exception as ccid_error:
|
|
519
|
-
raise CardUnreachable(pcsc_error, ccid_error) from ccid_error
|
|
520
|
-
self._inner = inner
|
|
521
|
-
return conn
|
|
522
|
-
|
|
523
|
-
def __exit__(self, *exc):
|
|
524
|
-
return self._inner.__exit__(*exc) if self._inner is not None else False
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
def _open_card():
|
|
528
|
-
return _CardConnection()
|
|
529
|
-
|
|
530
|
-
|
|
531
447
|
def _getsigner_apdu(selector, show_valid_certs, ui_provider):
|
|
532
448
|
''' Read-only certificate discovery over raw APDU (no PIN, safe) '''
|
|
533
449
|
|
|
534
450
|
from .cards import BTrustDriver
|
|
535
451
|
|
|
536
452
|
try:
|
|
537
|
-
conn_cm = _open_card()
|
|
453
|
+
conn_cm = card_access._open_card()
|
|
538
454
|
conn = conn_cm.__enter__()
|
|
539
455
|
except Exception as e:
|
|
540
456
|
raise backends.BackendUnavailable(f'No card reachable over PC/SC or direct CCID: {e}')
|
|
@@ -559,7 +475,7 @@ def get_signer(req):
|
|
|
559
475
|
show_valid_certs = data.get('showValidCerts', True)
|
|
560
476
|
ui_provider = get_ui_provider()
|
|
561
477
|
|
|
562
|
-
order = backends.resolve(backends.OP_READ, atr=_detected_atr(), config=config)
|
|
478
|
+
order = backends.resolve(backends.OP_READ, atr=card_access._detected_atr(), config=config)
|
|
563
479
|
for name in order:
|
|
564
480
|
try:
|
|
565
481
|
return _GETSIGNER[name](selector, show_valid_certs, ui_provider)
|
|
@@ -640,7 +556,7 @@ def sign(req):
|
|
|
640
556
|
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'Invalid signer certificate encoding'}
|
|
641
557
|
|
|
642
558
|
# Route to a signing backend per the resolver, falling through on unavailability
|
|
643
|
-
order = backends.resolve(backends.OP_SIGN, atr=_detected_atr(), config=config)
|
|
559
|
+
order = backends.resolve(backends.OP_SIGN, atr=card_access._detected_atr(), config=config)
|
|
644
560
|
for name in order:
|
|
645
561
|
try:
|
|
646
562
|
return _SIGN[name](contents, hash_algorithm, signer_cert_der, ui_provider)
|
|
@@ -678,7 +594,7 @@ def _sign_cng(contents, hash_algorithm, signer_cert_der, ui_provider):
|
|
|
678
594
|
|
|
679
595
|
def _sign_pkcs11(contents, hash_algorithm, signer_cert_der, ui_provider):
|
|
680
596
|
try:
|
|
681
|
-
funcs = _load_pkcs11()
|
|
597
|
+
funcs = card_access._load_pkcs11()
|
|
682
598
|
except backends.BackendUnavailable:
|
|
683
599
|
raise
|
|
684
600
|
except Exception as e:
|
|
@@ -732,7 +648,7 @@ def _sign_apdu(contents, hash_algorithm, signer_cert_der, ui_provider):
|
|
|
732
648
|
hash_name, algo_ref = _APDU_HASH[algo]
|
|
733
649
|
|
|
734
650
|
try:
|
|
735
|
-
conn_cm = _open_card()
|
|
651
|
+
conn_cm = card_access._open_card()
|
|
736
652
|
conn = conn_cm.__enter__()
|
|
737
653
|
except Exception as e:
|
|
738
654
|
raise backends.BackendUnavailable(f'No card reachable over PC/SC or direct CCID: {e}')
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from . import backends, detector, hardware
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _detected_atr() -> str:
|
|
5
|
+
''' First connected card ATR (upper hex), or '' - used to pick a backend '''
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
atrs = detector.get_connected_atrs(ccid_fallback=True)
|
|
9
|
+
return atrs[0].upper() if atrs else ''
|
|
10
|
+
except Exception:
|
|
11
|
+
return ''
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_pkcs11():
|
|
15
|
+
''' Resolve the vendor library and return its PKCS#11 function table. '''
|
|
16
|
+
|
|
17
|
+
lib_path = detector.auto_detect_library()
|
|
18
|
+
if not lib_path:
|
|
19
|
+
raise backends.BackendUnavailable('No PKCS#11 library for the detected card')
|
|
20
|
+
return hardware.load_pkcs11(lib_path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _pcsc_factory():
|
|
24
|
+
from .sc_ffi import SmartCardConnection
|
|
25
|
+
return SmartCardConnection()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ccid_factory():
|
|
29
|
+
from .ccid import CcidConnection
|
|
30
|
+
return CcidConnection()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CardUnreachable(Exception):
|
|
34
|
+
''' Neither PC/SC nor direct CCID could reach a card; carries both reasons '''
|
|
35
|
+
|
|
36
|
+
def __init__(self, pcsc_error, ccid_error):
|
|
37
|
+
self.pcsc_error, self.ccid_error = pcsc_error, ccid_error
|
|
38
|
+
super().__init__(f'PC/SC: {pcsc_error} | direct CCID: {ccid_error}')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _CardConnection:
|
|
42
|
+
''' Open the card over PC/SC, falling back to direct USB CCID '''
|
|
43
|
+
|
|
44
|
+
def __init__(self, pcsc_factory=_pcsc_factory, ccid_factory=_ccid_factory):
|
|
45
|
+
self._pcsc_factory, self._ccid_factory = pcsc_factory, ccid_factory
|
|
46
|
+
self._inner = None
|
|
47
|
+
|
|
48
|
+
def __enter__(self):
|
|
49
|
+
try:
|
|
50
|
+
inner = self._pcsc_factory()
|
|
51
|
+
conn = inner.__enter__()
|
|
52
|
+
except Exception as pcsc_error:
|
|
53
|
+
try:
|
|
54
|
+
inner = self._ccid_factory()
|
|
55
|
+
conn = inner.__enter__()
|
|
56
|
+
except Exception as ccid_error:
|
|
57
|
+
raise CardUnreachable(pcsc_error, ccid_error) from ccid_error
|
|
58
|
+
self._inner = inner
|
|
59
|
+
return conn
|
|
60
|
+
|
|
61
|
+
def __exit__(self, *exc):
|
|
62
|
+
return self._inner.__exit__(*exc) if self._inner is not None else False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _open_card():
|
|
66
|
+
return _CardConnection()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
''' card-management API for the GUI / daemon '''
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from . import backends, detector, hardware
|
|
5
|
+
from .config import config as _config
|
|
6
|
+
from .card_access import _open_card, _detected_atr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# --- engine plumbing --------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
def _pkcs11_session(cfg):
|
|
12
|
+
''' (funcs, slot_id) for the detected token; BackendUnavailable if none. '''
|
|
13
|
+
|
|
14
|
+
lib = ((cfg or _config).get('pkcs11Path') or '') or detector.auto_detect_library()
|
|
15
|
+
if not lib:
|
|
16
|
+
raise backends.BackendUnavailable('No PKCS#11 library for the detected card')
|
|
17
|
+
funcs = hardware.load_pkcs11(lib)
|
|
18
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
19
|
+
if not slots:
|
|
20
|
+
raise backends.BackendUnavailable('No token present in the reader')
|
|
21
|
+
return funcs, slots[0]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _with_apdu_driver(fn):
|
|
25
|
+
''' Open a PC/SC (or direct-CCID) card, run fn(driver), guarantee cleanup. '''
|
|
26
|
+
|
|
27
|
+
from .cards import BTrustDriver
|
|
28
|
+
try:
|
|
29
|
+
cm = _open_card()
|
|
30
|
+
conn = cm.__enter__()
|
|
31
|
+
except Exception as e:
|
|
32
|
+
raise backends.BackendUnavailable(f'No card reachable over PC/SC or CCID: {e}')
|
|
33
|
+
try:
|
|
34
|
+
with conn.transaction():
|
|
35
|
+
return fn(BTrustDriver.create(conn))
|
|
36
|
+
finally:
|
|
37
|
+
cm.__exit__(None, None, None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _dispatch(order, impls, what):
|
|
41
|
+
''' Try each resolved backend in order '''
|
|
42
|
+
|
|
43
|
+
if not order:
|
|
44
|
+
raise backends.BackendUnavailable(f'no backend can {what} for this card')
|
|
45
|
+
last = None
|
|
46
|
+
for name in order:
|
|
47
|
+
impl = impls.get(name)
|
|
48
|
+
if impl is None:
|
|
49
|
+
continue # engine can't do this op (capability filtered)
|
|
50
|
+
try:
|
|
51
|
+
return impl()
|
|
52
|
+
except backends.BackendUnavailable as e:
|
|
53
|
+
last = e # engine unusable - safe to try the next
|
|
54
|
+
continue
|
|
55
|
+
raise last or backends.BackendUnavailable(f'no usable backend to {what}')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _order(op, atr, cfg):
|
|
59
|
+
return backends.resolve(op, atr=(atr or _detected_atr()), config=(cfg or _config))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# --- public API -------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def change_pin(old_pin, new_pin, atr=None, cfg=None):
|
|
65
|
+
''' Change the user PIN via whichever engine the card resolves to. '''
|
|
66
|
+
|
|
67
|
+
return _dispatch(_order(backends.OP_PIN, atr, cfg), {
|
|
68
|
+
'apdu': lambda: _with_apdu_driver(lambda d: d.change_pin(old_pin, new_pin)),
|
|
69
|
+
'pkcs11': lambda: _pkcs11_change_pin(old_pin, new_pin, cfg),
|
|
70
|
+
}, 'change the PIN')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def unblock_pin(puk, new_pin, atr=None, cfg=None):
|
|
74
|
+
''' Unblock the PIN with the PUK via the resolved engine. '''
|
|
75
|
+
|
|
76
|
+
return _dispatch(_order(backends.OP_PIN, atr, cfg), {
|
|
77
|
+
'apdu': lambda: _with_apdu_driver(lambda d: d.unblock_pin(puk, new_pin)),
|
|
78
|
+
'pkcs11': lambda: _pkcs11_unblock(puk, new_pin, cfg),
|
|
79
|
+
}, 'unblock the PIN')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def change_puk(old_puk, new_puk, atr=None, cfg=None):
|
|
83
|
+
''' Change the PUK. PKCS#11 only - the raw-APDU driver has no PUK-change
|
|
84
|
+
(it lacks the 'apdu' impl, so _dispatch routes straight to PKCS#11). '''
|
|
85
|
+
|
|
86
|
+
return _dispatch(_order(backends.OP_PIN, atr, cfg), {
|
|
87
|
+
'pkcs11': lambda: _pkcs11_change_puk(old_puk, new_puk, cfg),
|
|
88
|
+
}, 'change the PUK')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def read_certificates(pin=None, atr=None, cfg=None):
|
|
92
|
+
''' List certificates. APDU discovers public certs (no PIN); PKCS#11 logs in. '''
|
|
93
|
+
|
|
94
|
+
return _dispatch(_order(backends.OP_READ, atr, cfg), {
|
|
95
|
+
'apdu': lambda: _with_apdu_driver(lambda d: d.discover_certificates()),
|
|
96
|
+
'pkcs11': lambda: _pkcs11_certs(pin, cfg),
|
|
97
|
+
}, 'read certificates')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def import_certificate(cert_der, pin, atr=None, cfg=None):
|
|
101
|
+
''' Write a certificate to the card. Only PKCS#11 advertises OP_MANAGE. '''
|
|
102
|
+
|
|
103
|
+
return _dispatch(_order(backends.OP_MANAGE, atr, cfg), {
|
|
104
|
+
'pkcs11': lambda: _pkcs11_write_cert(cert_der, pin, cfg),
|
|
105
|
+
}, 'import a certificate')
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def token_info(atr=None, cfg=None):
|
|
109
|
+
'''
|
|
110
|
+
A dict of token facts. Informational, so it prefers the richer PKCS#11
|
|
111
|
+
source and falls back to the APDU basics - it is not a security operation.
|
|
112
|
+
'''
|
|
113
|
+
|
|
114
|
+
resolved = set(_order(backends.OP_READ, atr, cfg))
|
|
115
|
+
order = [b for b in ('pkcs11', 'apdu') if b in resolved]
|
|
116
|
+
return _dispatch(order, {
|
|
117
|
+
'pkcs11': lambda: _pkcs11_token_info(cfg),
|
|
118
|
+
'apdu': lambda: _apdu_token_info(atr),
|
|
119
|
+
}, 'read token info')
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- per-engine implementations ---------------------------------------------
|
|
123
|
+
|
|
124
|
+
def _pkcs11_change_pin(old, new, cfg):
|
|
125
|
+
funcs, slot = _pkcs11_session(cfg)
|
|
126
|
+
hardware.change_card_pin(funcs, slot_id=slot, old_pin=old, new_pin=new)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _pkcs11_unblock(puk, new, cfg):
|
|
130
|
+
funcs, slot = _pkcs11_session(cfg)
|
|
131
|
+
hardware.unblock_card_pin(funcs, slot_id=slot, puk=puk, new_pin=new)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pkcs11_change_puk(old, new, cfg):
|
|
135
|
+
funcs, slot = _pkcs11_session(cfg)
|
|
136
|
+
hardware.change_card_puk(funcs, slot_id=slot, old_puk=old, new_puk=new)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _pkcs11_certs(pin, cfg):
|
|
140
|
+
funcs, slot = _pkcs11_session(cfg)
|
|
141
|
+
return hardware.get_certificates(funcs, slot_id=slot, pin=pin)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _pkcs11_write_cert(cert_der, pin, cfg):
|
|
145
|
+
funcs, slot = _pkcs11_session(cfg)
|
|
146
|
+
hardware.write_certificate(funcs, slot_id=slot, pin=pin, cert_der=cert_der)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _pkcs11_token_info(cfg):
|
|
150
|
+
funcs, slot = _pkcs11_session(cfg)
|
|
151
|
+
info = dict(hardware.get_token_info_extended(funcs, slot_id=slot))
|
|
152
|
+
info.setdefault('serial', hardware.get_token_serial_number(funcs, slot_id=slot))
|
|
153
|
+
return info
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _apdu_token_info(atr):
|
|
157
|
+
return {'atr': (atr or _detected_atr()), 'engine': 'apdu',
|
|
158
|
+
'note': 'raw-APDU path: detailed token info needs PKCS#11'}
|
|
@@ -189,7 +189,7 @@ def _find_usbfs() -> list[dict]:
|
|
|
189
189
|
info['vid'] = _read_text(dev / 'idVendor') or '????'
|
|
190
190
|
info['pid'] = _read_text(dev / 'idProduct') or '????'
|
|
191
191
|
info['backend'] = 'usbfs'
|
|
192
|
-
except
|
|
192
|
+
except OSError, ValueError:
|
|
193
193
|
continue
|
|
194
194
|
out.append(info)
|
|
195
195
|
return out
|
|
@@ -28,7 +28,7 @@ def ca_issuer_urls(cert_der: bytes) -> list[str]:
|
|
|
28
28
|
tag, location = ad.read_tlv() # accessLocation GeneralName
|
|
29
29
|
if method == _CA_ISSUERS_METHOD and tag == _URI_TAG:
|
|
30
30
|
urls.append(location.decode('latin1'))
|
|
31
|
-
except
|
|
31
|
+
except ValueError, IndexError:
|
|
32
32
|
pass
|
|
33
33
|
return urls
|
|
34
34
|
|
|
@@ -42,7 +42,7 @@ def _http_get(url: str, timeout: float) -> bytes | None:
|
|
|
42
42
|
try:
|
|
43
43
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
44
44
|
return resp.read(1 << 20) # a CA cert is a few KB; cap at 1 MiB
|
|
45
|
-
except
|
|
45
|
+
except urllib.error.URLError, OSError, ValueError:
|
|
46
46
|
return None
|
|
47
47
|
|
|
48
48
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os, sys, logging, subprocess, tkinter as tk, base64, threading, queue
|
|
2
|
-
from tkinter import
|
|
2
|
+
from tkinter import font as tkfont
|
|
3
3
|
|
|
4
|
-
from . import detector, hardware, transport
|
|
4
|
+
from . import detector, hardware, transport, card_service
|
|
5
5
|
from .app import CERTS_DIR
|
|
6
6
|
from .config import config
|
|
7
7
|
from .i18n import _
|
|
@@ -9,7 +9,7 @@ from .app import app as api_server
|
|
|
9
9
|
|
|
10
10
|
from .tkinter_mods import (
|
|
11
11
|
Theme, Window, Button, Frame, Scrollbar, MessageBox, Toggle, Label,
|
|
12
|
-
set_dark_titlebar, Toplevel, Textbox, Listbox, Entry, Font
|
|
12
|
+
set_dark_titlebar, Toplevel, Textbox, Listbox, Entry, Font, FileDialog
|
|
13
13
|
)
|
|
14
14
|
from .ui_bridge import ui_task_queue, set_gui_active
|
|
15
15
|
|
|
@@ -446,7 +446,7 @@ class DesktopDashboard(Window):
|
|
|
446
446
|
self.diag('[-] Run diagnostics first.')
|
|
447
447
|
return
|
|
448
448
|
|
|
449
|
-
target =
|
|
449
|
+
target = FileDialog.ask_directory(self, title='Save diagnostics into...')
|
|
450
450
|
if not target:
|
|
451
451
|
return
|
|
452
452
|
|
|
@@ -463,17 +463,6 @@ class DesktopDashboard(Window):
|
|
|
463
463
|
self.diag(' Drop it in tests/fixtures/ for hardware-free replay tests.')
|
|
464
464
|
|
|
465
465
|
|
|
466
|
-
def _load_card(self):
|
|
467
|
-
''' Loads the PKCS#11 library and returns (funcs, first_slot_id) '''
|
|
468
|
-
|
|
469
|
-
lib_path = config.get('pkcs11Path') or hardware.LIBCVP11_PATH
|
|
470
|
-
funcs = hardware.load_pkcs11(lib_path)
|
|
471
|
-
slots = hardware.get_slots(funcs, token_present=True)
|
|
472
|
-
if not slots:
|
|
473
|
-
raise Exception('No token present in the reader')
|
|
474
|
-
return funcs, slots[0]
|
|
475
|
-
|
|
476
|
-
|
|
477
466
|
def _ui_change_pin(self):
|
|
478
467
|
|
|
479
468
|
# ModalRenderer runs on the Tk main thread; get_ui_provider()'s proxy
|
|
@@ -484,8 +473,7 @@ class DesktopDashboard(Window):
|
|
|
484
473
|
if not new_pin: return
|
|
485
474
|
|
|
486
475
|
try:
|
|
487
|
-
|
|
488
|
-
hardware.change_card_pin(funcs, slot_id=slot, old_pin=old_pin, new_pin=new_pin)
|
|
476
|
+
card_service.change_pin(old_pin, new_pin)
|
|
489
477
|
MessageBox.showinfo("Success", "PIN successfully changed.")
|
|
490
478
|
except Exception as e:
|
|
491
479
|
MessageBox.showinfo("Error", f"Failed to change PIN: {e}")
|
|
@@ -499,8 +487,7 @@ class DesktopDashboard(Window):
|
|
|
499
487
|
if not new_pin: return
|
|
500
488
|
|
|
501
489
|
try:
|
|
502
|
-
|
|
503
|
-
hardware.unblock_card_pin(funcs, slot_id=slot, puk=puk, new_pin=new_pin)
|
|
490
|
+
card_service.unblock_pin(puk, new_pin)
|
|
504
491
|
MessageBox.showinfo("Success", "Card unblocked successfully.")
|
|
505
492
|
except Exception as e:
|
|
506
493
|
MessageBox.showinfo("Error", f"Failed to unblock card: {e}")
|
|
@@ -508,10 +495,10 @@ class DesktopDashboard(Window):
|
|
|
508
495
|
|
|
509
496
|
def _ui_import_cert(self):
|
|
510
497
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
title="Select Certificate",
|
|
514
|
-
filetypes=(("DER Certificates", "*.cer *.der *.crt"), ("All
|
|
498
|
+
filepath = FileDialog.ask_open(
|
|
499
|
+
self,
|
|
500
|
+
title="Select Certificate",
|
|
501
|
+
filetypes=(("DER Certificates", ("*.cer", "*.der", "*.crt")), ("All files", ("*",)))
|
|
515
502
|
)
|
|
516
503
|
if not filepath: return
|
|
517
504
|
|
|
@@ -527,8 +514,7 @@ class DesktopDashboard(Window):
|
|
|
527
514
|
from .cert_parser import pem_to_der
|
|
528
515
|
cert_data = pem_to_der(cert_data)[0]
|
|
529
516
|
|
|
530
|
-
|
|
531
|
-
hardware.write_certificate(funcs, slot_id=slot, pin=pin, cert_der=cert_data)
|
|
517
|
+
card_service.import_certificate(cert_data, pin)
|
|
532
518
|
MessageBox.showinfo("Success", "Certificate imported to smart card.")
|
|
533
519
|
except Exception as e:
|
|
534
520
|
MessageBox.showinfo("Error", f"Import failed: {e}")
|
|
@@ -945,8 +931,7 @@ class DesktopDashboard(Window):
|
|
|
945
931
|
self.log_to_dashboard(f' - {r}')
|
|
946
932
|
|
|
947
933
|
try:
|
|
948
|
-
|
|
949
|
-
serial = hardware.get_token_serial_number(funcs, slot_id=slot)
|
|
934
|
+
serial = card_service.token_info().get('serial', 'n/a')
|
|
950
935
|
self.log_to_dashboard(f'[+] Card Serial Number: {serial}')
|
|
951
936
|
self.status_var.set(_('status_connected'))
|
|
952
937
|
except Exception as e:
|
|
@@ -965,8 +950,7 @@ class DesktopDashboard(Window):
|
|
|
965
950
|
return
|
|
966
951
|
|
|
967
952
|
try:
|
|
968
|
-
|
|
969
|
-
certs = hardware.get_certificates(funcs, slot_id=slot, pin=pin)
|
|
953
|
+
certs = card_service.read_certificates(pin)
|
|
970
954
|
if not certs:
|
|
971
955
|
self.log_to_dashboard('[-] No certificates found on card.')
|
|
972
956
|
return
|