pybiss 0.0.1__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.0.1/PKG-INFO +6 -0
- pybiss-0.0.1/Readme.md +0 -0
- pybiss-0.0.1/pyproject.toml +13 -0
- pybiss-0.0.1/setup.cfg +4 -0
- pybiss-0.0.1/src/app.py +286 -0
- pybiss-0.0.1/src/cert_parser.py +193 -0
- pybiss-0.0.1/src/config.py +68 -0
- pybiss-0.0.1/src/desktop_ui.py +444 -0
- pybiss-0.0.1/src/detector.py +191 -0
- pybiss-0.0.1/src/hardware.py +522 -0
- pybiss-0.0.1/src/locale.py +117 -0
- pybiss-0.0.1/src/pkcs11_funcs.py +99 -0
- pybiss-0.0.1/src/pkcs11_types.py +102 -0
- pybiss-0.0.1/src/pybiss.egg-info/PKG-INFO +6 -0
- pybiss-0.0.1/src/pybiss.egg-info/SOURCES.txt +31 -0
- pybiss-0.0.1/src/pybiss.egg-info/dependency_links.txt +1 -0
- pybiss-0.0.1/src/pybiss.egg-info/top_level.txt +16 -0
- pybiss-0.0.1/src/server.py +160 -0
- pybiss-0.0.1/src/server_ui.py +206 -0
- pybiss-0.0.1/src/service.py +87 -0
- pybiss-0.0.1/src/spkac.py +68 -0
- pybiss-0.0.1/src/tkinter_mods.py +438 -0
- pybiss-0.0.1/src/tui.py +285 -0
- pybiss-0.0.1/src/verifier.py +41 -0
- pybiss-0.0.1/tests/test_cert_parser.py +128 -0
- pybiss-0.0.1/tests/test_detector.py +140 -0
- pybiss-0.0.1/tests/test_hardware.py +304 -0
- pybiss-0.0.1/tests/test_pkcs11_funcs.py +66 -0
- pybiss-0.0.1/tests/test_pkcs11_types.py +83 -0
- pybiss-0.0.1/tests/test_server.py +218 -0
- pybiss-0.0.1/tests/test_service.py +158 -0
- pybiss-0.0.1/tests/test_spkac.py +86 -0
- pybiss-0.0.1/tests/test_verifier.py +127 -0
pybiss-0.0.1/PKG-INFO
ADDED
pybiss-0.0.1/Readme.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pybiss"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Unofficial BISS implementation in Python. Fast, modern, self contained"
|
|
5
|
+
readme = "Readme.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
[project.scripts]
|
|
11
|
+
# pybiss-server = "src.app:app.run"
|
|
12
|
+
# pybiss-tui = "src.tui:run_tui"
|
|
13
|
+
# pybiss-gui = "src.desktop_ui:main"
|
pybiss-0.0.1/setup.cfg
ADDED
pybiss-0.0.1/src/app.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import base64, sys
|
|
2
|
+
|
|
3
|
+
sys.path.append(__file__.rsplit('/', 1)[0])
|
|
4
|
+
|
|
5
|
+
from src.server import MiniServer
|
|
6
|
+
import src.detector as detector
|
|
7
|
+
import src.hardware as hardware
|
|
8
|
+
import src.service as service
|
|
9
|
+
import src.server_ui as ui
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# -- Endpoints
|
|
13
|
+
|
|
14
|
+
app = MiniServer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.get('/version')
|
|
18
|
+
def get_version(req):
|
|
19
|
+
return {
|
|
20
|
+
'version': 'pybiss-1.0.0',
|
|
21
|
+
'httpMethods': 'GET, POST',
|
|
22
|
+
'contentTypes': 'data, digest',
|
|
23
|
+
'signatureTypes': 'signature',
|
|
24
|
+
'selectorAvailable': True,
|
|
25
|
+
'hashAlgorithms': 'SHA1, SHA256, SHA384, SHA512'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.get('/status')
|
|
30
|
+
def get_status(req):
|
|
31
|
+
atrs = detector.get_connected_atrs()
|
|
32
|
+
lib_path = detector.auto_detect_library()
|
|
33
|
+
return {
|
|
34
|
+
'status': 'ok' if atrs else 'no_cards_detected',
|
|
35
|
+
'readers': atrs,
|
|
36
|
+
'driver': lib_path
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.post('/getsigner')
|
|
41
|
+
def get_signer(req):
|
|
42
|
+
data = req.json
|
|
43
|
+
selector = data.get('selector')
|
|
44
|
+
show_valid_certs = data.get('showValidCerts', True)
|
|
45
|
+
|
|
46
|
+
lib_path = detector.auto_detect_library()
|
|
47
|
+
if not lib_path:
|
|
48
|
+
raise Exception('No supported smart cards detected')
|
|
49
|
+
|
|
50
|
+
funcs = hardware.load_pkcs11(lib_path)
|
|
51
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
52
|
+
if not slots:
|
|
53
|
+
raise Exception('No token present in smart card reader')
|
|
54
|
+
|
|
55
|
+
ui_provider = ui.get_ui_provider()
|
|
56
|
+
pin = ui_provider.prompt_pin()
|
|
57
|
+
if not pin:
|
|
58
|
+
return {'chain': [], 'status': 'error', 'reasonCode': 401, 'reasonText': 'PIN canceled'}
|
|
59
|
+
|
|
60
|
+
all_certs = []
|
|
61
|
+
for slot_id in slots:
|
|
62
|
+
try:
|
|
63
|
+
certs = hardware.get_certificates(funcs, slot_id, pin)
|
|
64
|
+
for c in certs:
|
|
65
|
+
c['slot_id'] = slot_id
|
|
66
|
+
c['pin'] = pin
|
|
67
|
+
all_certs.append(c)
|
|
68
|
+
except Exception:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
filtered_certs = service.filter_certificates(all_certs, selector=selector, show_valid_certs=show_valid_certs)
|
|
72
|
+
if not filtered_certs:
|
|
73
|
+
return {'chain': [], 'status': 'error', 'reasonCode': 404, 'reasonText': 'No matching certificates'}
|
|
74
|
+
|
|
75
|
+
choice_idx = ui_provider.choose_certificate(filtered_certs)
|
|
76
|
+
if choice_idx == -1:
|
|
77
|
+
return {'chain': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'Canceled'}
|
|
78
|
+
|
|
79
|
+
selected_cert = filtered_certs[choice_idx]
|
|
80
|
+
cert_b64 = base64.b64encode(selected_cert['der']).decode('utf-8')
|
|
81
|
+
|
|
82
|
+
return {'chain': [cert_b64], 'status': 'ok', 'reasonCode': 200, 'reasonText': 'Success'}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.post('/sign')
|
|
86
|
+
def sign(req):
|
|
87
|
+
|
|
88
|
+
data = req.json
|
|
89
|
+
|
|
90
|
+
# Extract fields from the raw JSON payload
|
|
91
|
+
contents = data.get('contents', [])
|
|
92
|
+
signed_contents = data.get('signedContents', [])
|
|
93
|
+
signed_contents_cert = data.get('signedContentsCert', [])
|
|
94
|
+
hash_algorithm = data.get('hashAlgorithm', 'SHA256')
|
|
95
|
+
confirm_text = data.get('confirmText', [])
|
|
96
|
+
signer_cert_b64 = data.get('signerCertificateB64')
|
|
97
|
+
|
|
98
|
+
# Verification of Server Signature
|
|
99
|
+
if len(contents) != len(signed_contents) or not contents:
|
|
100
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'Contents and signature length mismatch/empty'}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
if not signed_contents_cert:
|
|
104
|
+
raise ValueError('Server certificate missing')
|
|
105
|
+
server_cert_der = base64.b64decode(signed_contents_cert[0])
|
|
106
|
+
except Exception:
|
|
107
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'Invalid server certificate encoding'}
|
|
108
|
+
|
|
109
|
+
# Verify each content piece against the server signature
|
|
110
|
+
for i in range(len(contents)):
|
|
111
|
+
try:
|
|
112
|
+
payload = base64.b64decode(contents[i])
|
|
113
|
+
srv_sig = base64.b64decode(signed_contents[i])
|
|
114
|
+
except Exception:
|
|
115
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'Invalid payload/signature base64 encoding'}
|
|
116
|
+
|
|
117
|
+
verified = service.verify_server_signature(
|
|
118
|
+
payload=payload,
|
|
119
|
+
signature=srv_sig,
|
|
120
|
+
server_cert_der=server_cert_der,
|
|
121
|
+
hash_alg=hash_algorithm
|
|
122
|
+
)
|
|
123
|
+
if not verified:
|
|
124
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 403, 'reasonText': 'Server signature verification failed'}
|
|
125
|
+
|
|
126
|
+
# Confirm signature request with the user
|
|
127
|
+
ui_provider = ui.get_ui_provider()
|
|
128
|
+
confirm_msg = confirm_text[0] if confirm_text else 'Authorize signature request from banking portal?'
|
|
129
|
+
|
|
130
|
+
confirmed = ui_provider.confirm_sign(confirm_msg)
|
|
131
|
+
if not confirmed:
|
|
132
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'User rejected signature request'}
|
|
133
|
+
|
|
134
|
+
# Locate card PKCS11 library
|
|
135
|
+
lib_path = detector.auto_detect_library()
|
|
136
|
+
if not lib_path:
|
|
137
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'No supported smart cards detected'}
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
funcs = hardware.load_pkcs11(lib_path)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 500, 'reasonText': f'Failed to load PKCS11 driver: {e}'}
|
|
143
|
+
|
|
144
|
+
# Search and identify which slot contains the requested signer certificate
|
|
145
|
+
try:
|
|
146
|
+
if not signer_cert_b64:
|
|
147
|
+
raise ValueError('Signer certificate missing')
|
|
148
|
+
signer_cert_der = base64.b64decode(signer_cert_b64)
|
|
149
|
+
except Exception:
|
|
150
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 400, 'reasonText': 'Invalid signer certificate encoding'}
|
|
151
|
+
|
|
152
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
153
|
+
target_slot = None
|
|
154
|
+
target_cert_id = None
|
|
155
|
+
target_pin = None
|
|
156
|
+
|
|
157
|
+
# Prompt user for PIN to search card
|
|
158
|
+
pin = ui_provider.prompt_pin()
|
|
159
|
+
if not pin:
|
|
160
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 401, 'reasonText': 'PIN required'}
|
|
161
|
+
|
|
162
|
+
for slot_id in slots:
|
|
163
|
+
try:
|
|
164
|
+
certs = hardware.get_certificates(funcs, slot_id, pin)
|
|
165
|
+
for c in certs:
|
|
166
|
+
if c['der'] == signer_cert_der:
|
|
167
|
+
target_slot = slot_id
|
|
168
|
+
target_cert_id = c['id']
|
|
169
|
+
target_pin = pin
|
|
170
|
+
break
|
|
171
|
+
if target_slot is not None:
|
|
172
|
+
break
|
|
173
|
+
except Exception:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
if target_slot is None:
|
|
177
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 404, 'reasonText': 'Requested signing certificate not found on smart card'}
|
|
178
|
+
|
|
179
|
+
# Perform PKCS#11 signing
|
|
180
|
+
generated_signatures = []
|
|
181
|
+
for payload_b64 in contents:
|
|
182
|
+
try:
|
|
183
|
+
payload_bytes = base64.b64decode(payload_b64)
|
|
184
|
+
sig = hardware.sign_payload(
|
|
185
|
+
funcs=funcs,
|
|
186
|
+
slot_id=target_slot,
|
|
187
|
+
pin=target_pin,
|
|
188
|
+
payload=payload_bytes,
|
|
189
|
+
key_id=target_cert_id
|
|
190
|
+
)
|
|
191
|
+
generated_signatures.append(base64.b64encode(sig).decode('utf-8'))
|
|
192
|
+
except Exception as e:
|
|
193
|
+
return {'signatures': [], 'status': 'error', 'reasonCode': 500, 'reasonText': f'Signing failed: {e}'}
|
|
194
|
+
|
|
195
|
+
return {'signatures': generated_signatures, 'status': 'ok', 'reasonCode': 200, 'reasonText': 'Success'}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.get('/getSerialNumber')
|
|
199
|
+
def get_serial_number(req):
|
|
200
|
+
|
|
201
|
+
lib_path = detector.auto_detect_library()
|
|
202
|
+
if not lib_path:
|
|
203
|
+
return {'status': 'error', 'reasonCode': 400, 'reasonText': 'No supported smart cards detected'}
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
funcs = hardware.load_pkcs11(lib_path)
|
|
207
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
208
|
+
if not slots:
|
|
209
|
+
return {'status': 'error', 'reasonCode': 404, 'reasonText': 'No token present'}
|
|
210
|
+
|
|
211
|
+
serial = hardware.get_token_serial_number(funcs, slots[0])
|
|
212
|
+
# The Java app returns keysize and vendor too, but serial is the most critical
|
|
213
|
+
return {'serialNumber': serial, 'status': 'ok', 'reasonCode': 200, 'reasonText': 'Success'}
|
|
214
|
+
except Exception as e:
|
|
215
|
+
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.post('/setPIN')
|
|
219
|
+
def set_pin(req):
|
|
220
|
+
|
|
221
|
+
data = req.json
|
|
222
|
+
old_pin = data.get('oldPIN')
|
|
223
|
+
new_pin = data.get('newPIN')
|
|
224
|
+
|
|
225
|
+
if not old_pin or not new_pin:
|
|
226
|
+
return {'status': 'error', 'reasonCode': 400, 'reasonText': 'Missing PIN data'}
|
|
227
|
+
|
|
228
|
+
lib_path = detector.auto_detect_library()
|
|
229
|
+
try:
|
|
230
|
+
funcs = hardware.load_pkcs11(lib_path)
|
|
231
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
232
|
+
hardware.change_card_pin(funcs, slots[0], old_pin, new_pin)
|
|
233
|
+
return {'status': 'ok', 'reasonCode': 200, 'reasonText': 'PIN successfully changed'}
|
|
234
|
+
except Exception as e:
|
|
235
|
+
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.post('/unblockPIN')
|
|
239
|
+
def unblock_pin(req):
|
|
240
|
+
|
|
241
|
+
data = req.json
|
|
242
|
+
puk = data.get('puk')
|
|
243
|
+
new_pin = data.get('newPIN')
|
|
244
|
+
|
|
245
|
+
if not puk or not new_pin:
|
|
246
|
+
return {'status': 'error', 'reasonCode': 400, 'reasonText': 'Missing PUK or new PIN'}
|
|
247
|
+
|
|
248
|
+
lib_path = detector.auto_detect_library()
|
|
249
|
+
try:
|
|
250
|
+
funcs = hardware.load_pkcs11(lib_path)
|
|
251
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
252
|
+
hardware.unblock_card_pin(funcs, slots[0], puk, new_pin)
|
|
253
|
+
return {'status': 'ok', 'reasonCode': 200, 'reasonText': 'PIN successfully unblocked'}
|
|
254
|
+
except Exception as e:
|
|
255
|
+
return {'status': 'error', 'reasonCode': 403, 'reasonText': str(e)}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.post('/writeCertSC')
|
|
259
|
+
def write_cert_sc(req):
|
|
260
|
+
|
|
261
|
+
data = req.json
|
|
262
|
+
cert_b64 = data.get('certificate')
|
|
263
|
+
|
|
264
|
+
if not cert_b64:
|
|
265
|
+
return {'status': 'error', 'reasonCode': 400, 'reasonText': 'Missing certificate data'}
|
|
266
|
+
|
|
267
|
+
ui_provider = ui.get_ui_provider()
|
|
268
|
+
pin = ui_provider.prompt_pin()
|
|
269
|
+
if not pin:
|
|
270
|
+
return {'status': 'error', 'reasonCode': 401, 'reasonText': 'PIN canceled'}
|
|
271
|
+
|
|
272
|
+
lib_path = detector.auto_detect_library()
|
|
273
|
+
try:
|
|
274
|
+
cert_der = base64.b64decode(cert_b64)
|
|
275
|
+
funcs = hardware.load_pkcs11(lib_path)
|
|
276
|
+
slots = hardware.get_slots(funcs, token_present=True)
|
|
277
|
+
|
|
278
|
+
hardware.write_certificate(funcs, slots[0], pin, cert_der, label="B-Trust Certificate")
|
|
279
|
+
return {'status': 'ok', 'reasonCode': 200, 'reasonText': 'Certificate successfully written to card'}
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return {'status': 'error', 'reasonCode': 500, 'reasonText': str(e)}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == '__main__':
|
|
285
|
+
|
|
286
|
+
app.run(host='127.0.0.1', port=4843)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
class DerReader:
|
|
4
|
+
|
|
5
|
+
def __init__(self, data: bytes):
|
|
6
|
+
self.data = data
|
|
7
|
+
self.pos = 0
|
|
8
|
+
|
|
9
|
+
def is_empty(self) -> bool:
|
|
10
|
+
return self.pos >= len(self.data)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def read_seq(self):
|
|
14
|
+
''' Expects a sequence and returns a new reader for its contents '''
|
|
15
|
+
|
|
16
|
+
tag, val = self.read_tlv()
|
|
17
|
+
if tag != 0x30:
|
|
18
|
+
raise ValueError(f'Expected SEQUENCE (0x30), got {hex(tag)}')
|
|
19
|
+
return DerReader(val)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_tlv(self) -> tuple[int, bytes]:
|
|
23
|
+
''' Reads a Tag-Length-Value block and advances the pointer '''
|
|
24
|
+
|
|
25
|
+
if self.pos >= len(self.data):
|
|
26
|
+
raise ValueError("Unexpected EOF reading tag")
|
|
27
|
+
|
|
28
|
+
tag = self.data[self.pos]
|
|
29
|
+
self.pos += 1
|
|
30
|
+
|
|
31
|
+
if self.pos >= len(self.data):
|
|
32
|
+
raise ValueError("Unexpected EOF reading length")
|
|
33
|
+
|
|
34
|
+
length = self.data[self.pos]
|
|
35
|
+
self.pos += 1
|
|
36
|
+
|
|
37
|
+
# Handle multi-byte lengths (e.g. > 127 bytes)
|
|
38
|
+
if length & 0x80:
|
|
39
|
+
num_bytes = length & 0x7F
|
|
40
|
+
if self.pos + num_bytes > len(self.data):
|
|
41
|
+
raise ValueError("Unexpected EOF reading multi-byte length")
|
|
42
|
+
|
|
43
|
+
length = int.from_bytes(self.data[self.pos:self.pos + num_bytes], 'big')
|
|
44
|
+
self.pos += num_bytes
|
|
45
|
+
|
|
46
|
+
# Strict bounds check before slicing
|
|
47
|
+
if self.pos + length > len(self.data):
|
|
48
|
+
raise ValueError(f"Unexpected EOF reading value. Needs {length} bytes, but only {len(self.data) - self.pos} left.")
|
|
49
|
+
|
|
50
|
+
val = self.data[self.pos:self.pos + length]
|
|
51
|
+
self.pos += length
|
|
52
|
+
|
|
53
|
+
return tag, val
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _decode_string(tag: int, val: bytes) -> str:
|
|
57
|
+
|
|
58
|
+
if tag == 0x1E: # BMPString (Microsoft AD CS often uses this)
|
|
59
|
+
return val.decode('utf-16-be')
|
|
60
|
+
return val.decode('utf-8', errors='ignore')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_name(name_der: bytes) -> str:
|
|
64
|
+
''' Extracts the X.509 distinguished name (DN) into a standard format '''
|
|
65
|
+
|
|
66
|
+
parts = []
|
|
67
|
+
outer = DerReader(name_der)
|
|
68
|
+
while not outer.is_empty():
|
|
69
|
+
_, set_val = outer.read_tlv()
|
|
70
|
+
set_r = DerReader(set_val)
|
|
71
|
+
while not set_r.is_empty():
|
|
72
|
+
seq_r = DerReader(set_r.read_tlv()[1])
|
|
73
|
+
oid = seq_r.read_tlv()[1]
|
|
74
|
+
str_tag, str_val = seq_r.read_tlv()
|
|
75
|
+
|
|
76
|
+
label = {
|
|
77
|
+
b'\x55\x04\x03': 'CN',
|
|
78
|
+
b'\x55\x04\x06': 'C',
|
|
79
|
+
b'\x55\x04\x0a': 'O',
|
|
80
|
+
b'\x55\x04\x0b': 'OU'
|
|
81
|
+
}.get(oid, 'Unknown')
|
|
82
|
+
|
|
83
|
+
parts.append(f'{label}={_decode_string(str_tag, str_val)}')
|
|
84
|
+
|
|
85
|
+
parts.reverse() # Reverse to match RFC4514 representation
|
|
86
|
+
return ','.join(parts)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_time(tag: int, val: bytes) -> int:
|
|
91
|
+
|
|
92
|
+
s = val.decode('ascii')
|
|
93
|
+
if tag == 0x17: # UTCTime (YYMMDDHHMMSSZ)
|
|
94
|
+
y = int(s[:2])
|
|
95
|
+
y += 2000 if y < 50 else 1900
|
|
96
|
+
s = f'{y}{s[2:]}'
|
|
97
|
+
|
|
98
|
+
# GeneralizedTime is already YYYYMMDDHHMMSSZ
|
|
99
|
+
dt = datetime.strptime(s[:14], '%Y%m%d%H%M%S')
|
|
100
|
+
|
|
101
|
+
return int(dt.replace(tzinfo=timezone.utc).timestamp())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_certificate(der_bytes: bytes) -> dict:
|
|
105
|
+
''' Parses an X.509 DER certificate to extract B-Trust metadata '''
|
|
106
|
+
|
|
107
|
+
cert = DerReader(der_bytes).read_seq()
|
|
108
|
+
tbs = cert.read_seq()
|
|
109
|
+
|
|
110
|
+
# Version (Optional [0])
|
|
111
|
+
tag, val = tbs.read_tlv()
|
|
112
|
+
if tag == 0xA0:
|
|
113
|
+
tag, val = tbs.read_tlv() # Move to Serial
|
|
114
|
+
|
|
115
|
+
# Serial Number
|
|
116
|
+
serial = str(int.from_bytes(val, 'big'))
|
|
117
|
+
|
|
118
|
+
# Signature Algorithm (Skip)
|
|
119
|
+
tbs.read_tlv()
|
|
120
|
+
|
|
121
|
+
# Issuer Name
|
|
122
|
+
_, issuer_val = tbs.read_tlv()
|
|
123
|
+
issuer = _parse_name(issuer_val)
|
|
124
|
+
|
|
125
|
+
# Validity Timestamps
|
|
126
|
+
validity = DerReader(tbs.read_tlv()[1])
|
|
127
|
+
not_before = _parse_time(*validity.read_tlv())
|
|
128
|
+
not_after = _parse_time(*validity.read_tlv())
|
|
129
|
+
|
|
130
|
+
# Subject Name
|
|
131
|
+
_, subject_val = tbs.read_tlv()
|
|
132
|
+
subject = _parse_name(subject_val)
|
|
133
|
+
|
|
134
|
+
# SubjectPublicKeyInfo (SPKI) -> RSA Modulus and Exponent
|
|
135
|
+
_, spki_val = tbs.read_tlv()
|
|
136
|
+
spki = DerReader(spki_val)
|
|
137
|
+
spki.read_tlv() # Skip AlgorithmIdentifier
|
|
138
|
+
bit_string = spki.read_tlv()[1]
|
|
139
|
+
|
|
140
|
+
pkcs1 = DerReader(bit_string[1:]).read_seq()
|
|
141
|
+
n = int.from_bytes(pkcs1.read_tlv()[1], 'big')
|
|
142
|
+
e = int.from_bytes(pkcs1.read_tlv()[1], 'big')
|
|
143
|
+
|
|
144
|
+
# Extract Key Usages and AKI from Extensions
|
|
145
|
+
usages, aki = [], ''
|
|
146
|
+
while not tbs.is_empty():
|
|
147
|
+
ext_tag, ext_val = tbs.read_tlv()
|
|
148
|
+
if ext_tag == 0xA3: # Extensions [3]
|
|
149
|
+
ext_seq = DerReader(ext_val).read_seq()
|
|
150
|
+
while not ext_seq.is_empty():
|
|
151
|
+
ext = DerReader(ext_seq.read_tlv()[1])
|
|
152
|
+
oid = ext.read_tlv()[1]
|
|
153
|
+
|
|
154
|
+
e_tag, e_val = ext.read_tlv()
|
|
155
|
+
if e_tag == 0x01: # Skip Boolean CRITICAL flag
|
|
156
|
+
e_tag, e_val = ext.read_tlv()
|
|
157
|
+
|
|
158
|
+
if oid == b'\x55\x1d\x0f': # Key Usage
|
|
159
|
+
bits = DerReader(e_val).read_tlv()[1]
|
|
160
|
+
if len(bits) > 1:
|
|
161
|
+
b = bits[1]
|
|
162
|
+
if b & 0x80: usages.append('digitalSignature')
|
|
163
|
+
if b & 0x40: usages.append('nonRepudiation')
|
|
164
|
+
if b & 0x20: usages.append('keyEncipherment')
|
|
165
|
+
|
|
166
|
+
elif oid == b'\x55\x1d\x23': # Authority Key Identifier (AKI)
|
|
167
|
+
inner = DerReader(e_val).read_seq()
|
|
168
|
+
if not inner.is_empty():
|
|
169
|
+
a_tag, a_val = inner.read_tlv()
|
|
170
|
+
if a_tag == 0x80: # [0] KeyIdentifier
|
|
171
|
+
aki = a_val.hex().upper()
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
'serial': serial, 'issuer': issuer, 'subject': subject,
|
|
175
|
+
'not_before': not_before, 'not_after': not_after,
|
|
176
|
+
'key_usages': usages, 'aki': aki, 'n': n, 'e': e
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_x509_subject(der_bytes: bytes) -> str:
|
|
181
|
+
subject_str = parse_certificate(der_bytes)['subject']
|
|
182
|
+
for part in subject_str.split(','):
|
|
183
|
+
if part.startswith('CN='): return part[3:]
|
|
184
|
+
return 'Unknown Subject'
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_x509_metadata(der_bytes: bytes) -> dict:
|
|
188
|
+
return parse_certificate(der_bytes)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_rsa_public_key(der_bytes: bytes) -> tuple[int, int]:
|
|
192
|
+
cert = parse_certificate(der_bytes)
|
|
193
|
+
return cert['n'], cert['e']
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os, sys, configparser
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigManager:
|
|
6
|
+
''' Manages persistent user settings in a local .ini file '''
|
|
7
|
+
|
|
8
|
+
def __init__(self, app_name='PyBISS', config_file_name='settings.ini'):
|
|
9
|
+
self.config_dir = self._get_config_dir(app_name)
|
|
10
|
+
self.config_file = self.config_dir / config_file_name
|
|
11
|
+
|
|
12
|
+
self.parser = configparser.ConfigParser()
|
|
13
|
+
# Prevent configparser from converting keys to lowercase
|
|
14
|
+
self.parser.optionxform = str
|
|
15
|
+
|
|
16
|
+
# Define the baseline defaults
|
|
17
|
+
self.parser['Settings'] = {
|
|
18
|
+
'signAPI': 'PKCS11',
|
|
19
|
+
'pkcs11Path': '',
|
|
20
|
+
'pfxPath': '',
|
|
21
|
+
'language': 'en',
|
|
22
|
+
'osStarted': 'True',
|
|
23
|
+
'newSacRequired': 'True'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
self.load()
|
|
27
|
+
|
|
28
|
+
def _get_config_dir(self, app_name: str) -> Path:
|
|
29
|
+
if os.name == 'nt':
|
|
30
|
+
base = os.getenv('APPDATA', str(Path.home() / 'AppData' / 'Roaming'))
|
|
31
|
+
return Path(base) / app_name
|
|
32
|
+
elif sys.platform == 'darwin':
|
|
33
|
+
return Path.home() / 'Library' / 'Application Support' / app_name
|
|
34
|
+
else:
|
|
35
|
+
base = os.getenv('XDG_CONFIG_HOME', str(Path.home() / '.config'))
|
|
36
|
+
return Path(base) / app_name
|
|
37
|
+
|
|
38
|
+
def load(self):
|
|
39
|
+
''' Reads the .ini file. Missing keys will fall back to the defaults above '''
|
|
40
|
+
|
|
41
|
+
if self.config_file.exists():
|
|
42
|
+
self.parser.read(self.config_file, encoding='utf-8')
|
|
43
|
+
else:
|
|
44
|
+
self.save()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save(self):
|
|
48
|
+
''' Writes the current state to the .ini file '''
|
|
49
|
+
|
|
50
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
try:
|
|
52
|
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
53
|
+
self.parser.write(f)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f'[!] Error saving config: {e}')
|
|
56
|
+
|
|
57
|
+
def get(self, key: str, fallback=None) -> str:
|
|
58
|
+
return self.parser.get('Settings', key, fallback=fallback)
|
|
59
|
+
|
|
60
|
+
def get_bool(self, key: str, fallback=False) -> bool:
|
|
61
|
+
return self.parser.getboolean('Settings', key, fallback=fallback)
|
|
62
|
+
|
|
63
|
+
def set(self, key: str, value):
|
|
64
|
+
self.parser.set('Settings', key, str(value))
|
|
65
|
+
self.save()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
config = ConfigManager()
|