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.
Files changed (80) hide show
  1. {pybiss-0.2.0 → pybiss-0.3.2}/PKG-INFO +2 -2
  2. {pybiss-0.2.0 → pybiss-0.3.2}/pyproject.toml +2 -2
  3. {pybiss-0.2.0 → pybiss-0.3.2}/src/app.py +18 -102
  4. pybiss-0.3.2/src/card_access.py +66 -0
  5. pybiss-0.3.2/src/card_service.py +158 -0
  6. {pybiss-0.2.0 → pybiss-0.3.2}/src/ccid.py +1 -1
  7. {pybiss-0.2.0 → pybiss-0.3.2}/src/chain_fetch.py +2 -2
  8. {pybiss-0.2.0 → pybiss-0.3.2}/src/dashboard.py +13 -29
  9. {pybiss-0.2.0 → pybiss-0.3.2}/src/fonts.py +12 -11
  10. pybiss-0.3.2/src/mcp.py +340 -0
  11. {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/PKG-INFO +2 -2
  12. {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/SOURCES.txt +5 -0
  13. {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/top_level.txt +3 -0
  14. pybiss-0.3.2/src/tkinter_mods.py +3323 -0
  15. {pybiss-0.2.0 → pybiss-0.3.2}/src/transport.py +10 -6
  16. {pybiss-0.2.0 → pybiss-0.3.2}/src/update_check.py +2 -2
  17. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_app_apdu_backend.py +8 -8
  18. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_app_fidelity.py +4 -5
  19. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_card_fallback.py +16 -16
  20. pybiss-0.3.2/tests/test_card_service.py +180 -0
  21. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_f_ccid.py +1 -1
  22. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_gui.py +37 -17
  23. pybiss-0.3.2/tests/test_mcp.py +85 -0
  24. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sign_flow.py +8 -8
  25. pybiss-0.3.2/tests/test_tkinter_mods.py +1473 -0
  26. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_vpcd.py +1 -1
  27. pybiss-0.2.0/src/tkinter_mods.py +0 -1037
  28. pybiss-0.2.0/tests/test_tkinter_mods.py +0 -247
  29. {pybiss-0.2.0 → pybiss-0.3.2}/Readme.md +0 -0
  30. {pybiss-0.2.0 → pybiss-0.3.2}/setup.cfg +0 -0
  31. {pybiss-0.2.0 → pybiss-0.3.2}/src/__init__.py +0 -0
  32. {pybiss-0.2.0 → pybiss-0.3.2}/src/backends.py +0 -0
  33. {pybiss-0.2.0 → pybiss-0.3.2}/src/cards.py +0 -0
  34. {pybiss-0.2.0 → pybiss-0.3.2}/src/cert_parser.py +0 -0
  35. {pybiss-0.2.0 → pybiss-0.3.2}/src/config.py +0 -0
  36. {pybiss-0.2.0 → pybiss-0.3.2}/src/detector.py +0 -0
  37. {pybiss-0.2.0 → pybiss-0.3.2}/src/download_assets.py +0 -0
  38. {pybiss-0.2.0 → pybiss-0.3.2}/src/hardware.py +0 -0
  39. {pybiss-0.2.0 → pybiss-0.3.2}/src/i18n.py +0 -0
  40. {pybiss-0.2.0 → pybiss-0.3.2}/src/jws.py +0 -0
  41. {pybiss-0.2.0 → pybiss-0.3.2}/src/mscapi.py +0 -0
  42. {pybiss-0.2.0 → pybiss-0.3.2}/src/pkcs11_funcs.py +0 -0
  43. {pybiss-0.2.0 → pybiss-0.3.2}/src/pkcs11_types.py +0 -0
  44. {pybiss-0.2.0 → pybiss-0.3.2}/src/pkcs15.py +0 -0
  45. {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.egg-info/dependency_links.txt +0 -0
  46. {pybiss-0.2.0 → pybiss-0.3.2}/src/pybiss.py +0 -0
  47. {pybiss-0.2.0 → pybiss-0.3.2}/src/sc_apdu.py +0 -0
  48. {pybiss-0.2.0 → pybiss-0.3.2}/src/sc_ffi.py +0 -0
  49. {pybiss-0.2.0 → pybiss-0.3.2}/src/sc_types.py +0 -0
  50. {pybiss-0.2.0 → pybiss-0.3.2}/src/server.py +0 -0
  51. {pybiss-0.2.0 → pybiss-0.3.2}/src/service.py +0 -0
  52. {pybiss-0.2.0 → pybiss-0.3.2}/src/spkac.py +0 -0
  53. {pybiss-0.2.0 → pybiss-0.3.2}/src/tracing.py +0 -0
  54. {pybiss-0.2.0 → pybiss-0.3.2}/src/tui.py +0 -0
  55. {pybiss-0.2.0 → pybiss-0.3.2}/src/ui_bridge.py +0 -0
  56. {pybiss-0.2.0 → pybiss-0.3.2}/src/verifier.py +0 -0
  57. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_backends.py +0 -0
  58. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_btrust_cards.py +0 -0
  59. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_ccid.py +0 -0
  60. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_ccid_transports.py +0 -0
  61. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_cert_parser.py +0 -0
  62. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_chain_fetch.py +0 -0
  63. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_detector.py +0 -0
  64. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_hardware.py +0 -0
  65. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_idprime_minidriver.py +0 -0
  66. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_jws.py +0 -0
  67. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_mscapi.py +0 -0
  68. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pcsc_daemon.py +0 -0
  69. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pkcs11_funcs.py +0 -0
  70. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pkcs11_types.py +0 -0
  71. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_pkcs15.py +0 -0
  72. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sc_apdu.py +0 -0
  73. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sc_types.py +0 -0
  74. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_sc_types_abi.py +0 -0
  75. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_server.py +0 -0
  76. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_service.py +0 -0
  77. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_spkac.py +0 -0
  78. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_tracing.py +0 -0
  79. {pybiss-0.2.0 → pybiss-0.3.2}/tests/test_transport.py +0 -0
  80. {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.0
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.13
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.0"
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.13"
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
- funcs = hardware.load_pkcs11(lib_path)
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
- funcs = hardware.load_pkcs11(lib_path)
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
- funcs = hardware.load_pkcs11(lib_path)
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 (TypeError, ValueError):
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
- funcs = hardware.load_pkcs11(lib_path)
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
- funcs = hardware.load_pkcs11(lib_path)
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 (TypeError, ValueError):
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 (TypeError, ValueError):
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 (OSError, ValueError):
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 (ValueError, IndexError):
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 (urllib.error.URLError, OSError, ValueError):
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 filedialog, font as tkfont
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 = filedialog.askdirectory(title='Save diagnostics into...')
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
- funcs, slot = self._load_card()
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
- funcs, slot = self._load_card()
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
- # Native OS File Dialog
512
- filepath = filedialog.askopenfilename(
513
- title="Select Certificate",
514
- filetypes=(("DER Certificates", "*.cer *.der *.crt"), ("All Files", "*.*"))
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
- funcs, slot = self._load_card()
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
- funcs, slot = self._load_card()
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
- funcs, slot = self._load_card()
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