openshell-shared 0.1.2__py3-none-any.whl
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.
- api/__init__.py +1 -0
- api/manager/__init__.py +1 -0
- api/manager/v1/__init__.py +78 -0
- api/manager/v1/authentication.py +278 -0
- api/manager/v1/client.py +111 -0
- api/manager/v1/domains.py +64 -0
- api/manager/v1/entities.py +97 -0
- api/manager/v1/exceptions.py +98 -0
- api/manager/v1/identity.py +44 -0
- api/manager/v1/models.py +342 -0
- api/manager/v1/passports.py +131 -0
- api/manager/v1/sessions.py +83 -0
- api/manager/v1/transport.py +253 -0
- api/manager/v1/tunnels.py +120 -0
- cryptography/__init__.py +0 -0
- cryptography/certificate.py +390 -0
- cryptography/encoding.py +0 -0
- cryptography/identity.py +124 -0
- cryptography/keys.py +463 -0
- cryptography/signatures.py +63 -0
- cryptography/utils.py +0 -0
- domain/__init__.py +0 -0
- domain/domain.py +80 -0
- domain/membership.py +21 -0
- domain/permissions.py +14 -0
- domain/policies.py +2 -0
- identity/__init__.py +0 -0
- identity/identification.py +64 -0
- identity/store.py +150 -0
- modules/__init__.py +0 -0
- modules/shell/__init__.py +0 -0
- modules/shell/client.py +361 -0
- modules/shell/models.py +61 -0
- modules/shell/protocol.py +249 -0
- modules/shell/server.py +511 -0
- modules/shell/session.py +339 -0
- modules/utils.py +212 -0
- openshell_shared-0.1.2.dist-info/METADATA +59 -0
- openshell_shared-0.1.2.dist-info/RECORD +62 -0
- openshell_shared-0.1.2.dist-info/WHEEL +5 -0
- openshell_shared-0.1.2.dist-info/top_level.txt +7 -0
- protocols/__init__.py +0 -0
- protocols/negotiation/challenge.py +127 -0
- protocols/negotiation/models.py +28 -0
- standards/__init__.py +0 -0
- standards/certificates/__init__.py +0 -0
- standards/certificates/status.py +12 -0
- standards/certificates/types.py +11 -0
- standards/entities/__init__.py +0 -0
- standards/entities/types.py +14 -0
- standards/events/__init__.py +0 -0
- standards/events/schemas/__init__.py +0 -0
- standards/events/schemas/entity_registered.py +13 -0
- standards/events/types.py +18 -0
- standards/passports/__init__.py +0 -0
- standards/passports/types.py +5 -0
- standards/permissions/__init__.py +0 -0
- standards/permissions/types.py +14 -0
- standards/roles/__init__.py +0 -0
- standards/roles/types.py +8 -0
- standards/transports/__init__.py +0 -0
- standards/transports/types.py +24 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# shared/cryptography/certificate.py
|
|
2
|
+
|
|
3
|
+
# =========================================================
|
|
4
|
+
# LIBRARY IMPORTS
|
|
5
|
+
# =========================================================
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from uuid6 import uuid7
|
|
13
|
+
from time import time
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
# =========================================================
|
|
17
|
+
# LOCAL IMPORTS
|
|
18
|
+
# =========================================================
|
|
19
|
+
|
|
20
|
+
from .signatures import (
|
|
21
|
+
sign_data,
|
|
22
|
+
verify_signature
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# =========================================================
|
|
26
|
+
# CONSTANTS
|
|
27
|
+
# =========================================================
|
|
28
|
+
|
|
29
|
+
SCHEMA_VERSION: int = 1
|
|
30
|
+
|
|
31
|
+
STATUS_ACTIVE: str = "ACTIVE"
|
|
32
|
+
STATUS_REVOKED: str = "REVOKED"
|
|
33
|
+
STATUS_EXPIRED: str = "EXPIRED"
|
|
34
|
+
STATUS_COMPROMISED: str = "COMPROMISED"
|
|
35
|
+
|
|
36
|
+
VALID_STATUSES = {
|
|
37
|
+
STATUS_ACTIVE,
|
|
38
|
+
STATUS_REVOKED,
|
|
39
|
+
STATUS_EXPIRED,
|
|
40
|
+
STATUS_COMPROMISED
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# =========================================================
|
|
44
|
+
# EXCEPTIONS
|
|
45
|
+
# =========================================================
|
|
46
|
+
|
|
47
|
+
class CertificateError(Exception):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CertificateValidationError(CertificateError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CertificateSignatureError(CertificateError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =========================================================
|
|
60
|
+
# CERTIFICATE
|
|
61
|
+
# =========================================================
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Certificate:
|
|
65
|
+
|
|
66
|
+
# -----------------------------------------------------
|
|
67
|
+
# SCHEMA
|
|
68
|
+
# -----------------------------------------------------
|
|
69
|
+
|
|
70
|
+
schema: int = SCHEMA_VERSION
|
|
71
|
+
|
|
72
|
+
# -----------------------------------------------------
|
|
73
|
+
# IDENTITY
|
|
74
|
+
# -----------------------------------------------------
|
|
75
|
+
|
|
76
|
+
uid: str = field(default_factory=lambda: str(uuid7()))
|
|
77
|
+
|
|
78
|
+
certificate_type: str = ""
|
|
79
|
+
|
|
80
|
+
# -----------------------------------------------------
|
|
81
|
+
# ISSUER
|
|
82
|
+
# -----------------------------------------------------
|
|
83
|
+
|
|
84
|
+
issuer_uid: str = ""
|
|
85
|
+
issuer_public_key: str = ""
|
|
86
|
+
|
|
87
|
+
# -----------------------------------------------------
|
|
88
|
+
# SUBJECT
|
|
89
|
+
# -----------------------------------------------------
|
|
90
|
+
|
|
91
|
+
subject_uid: str = ""
|
|
92
|
+
|
|
93
|
+
# -----------------------------------------------------
|
|
94
|
+
# CONTENT
|
|
95
|
+
# -----------------------------------------------------
|
|
96
|
+
|
|
97
|
+
payload: dict = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
# -----------------------------------------------------
|
|
100
|
+
# TEMPORAL
|
|
101
|
+
# -----------------------------------------------------
|
|
102
|
+
|
|
103
|
+
issued_at: int = field(default_factory=lambda: int(time()))
|
|
104
|
+
expires_at: int = 0
|
|
105
|
+
|
|
106
|
+
# -----------------------------------------------------
|
|
107
|
+
# STATUS
|
|
108
|
+
# -----------------------------------------------------
|
|
109
|
+
|
|
110
|
+
status: str = STATUS_ACTIVE
|
|
111
|
+
|
|
112
|
+
revoked_at: Optional[int] = None
|
|
113
|
+
|
|
114
|
+
# -----------------------------------------------------
|
|
115
|
+
# CRYPTOGRAPHIC
|
|
116
|
+
# -----------------------------------------------------
|
|
117
|
+
|
|
118
|
+
signature: Optional[str] = None
|
|
119
|
+
|
|
120
|
+
# =====================================================
|
|
121
|
+
# FACTORY
|
|
122
|
+
# =====================================================
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def generate(
|
|
126
|
+
certificate_type: str,
|
|
127
|
+
issuer_uid: str,
|
|
128
|
+
issuer_public_key: str,
|
|
129
|
+
subject_uid: str,
|
|
130
|
+
payload: dict,
|
|
131
|
+
expires_at: int
|
|
132
|
+
) -> "Certificate":
|
|
133
|
+
|
|
134
|
+
cert = Certificate(
|
|
135
|
+
certificate_type=certificate_type,
|
|
136
|
+
|
|
137
|
+
issuer_uid=issuer_uid,
|
|
138
|
+
issuer_public_key=issuer_public_key,
|
|
139
|
+
|
|
140
|
+
subject_uid=subject_uid,
|
|
141
|
+
|
|
142
|
+
payload=payload,
|
|
143
|
+
|
|
144
|
+
expires_at=expires_at
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
cert.validate()
|
|
148
|
+
|
|
149
|
+
return cert
|
|
150
|
+
|
|
151
|
+
# =====================================================
|
|
152
|
+
# VALIDATION
|
|
153
|
+
# =====================================================
|
|
154
|
+
|
|
155
|
+
def validate(self):
|
|
156
|
+
|
|
157
|
+
if not isinstance(self.schema, int):
|
|
158
|
+
raise CertificateValidationError(
|
|
159
|
+
"schema must be integer"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if self.schema <= 0:
|
|
163
|
+
raise CertificateValidationError(
|
|
164
|
+
"invalid schema version"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
required_strings = [
|
|
168
|
+
("uid", self.uid),
|
|
169
|
+
("certificate_type", self.certificate_type),
|
|
170
|
+
("issuer_uid", self.issuer_uid),
|
|
171
|
+
("issuer_public_key", self.issuer_public_key),
|
|
172
|
+
("subject_uid", self.subject_uid)
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
for field_name, value in required_strings:
|
|
176
|
+
|
|
177
|
+
if not isinstance(value, str):
|
|
178
|
+
raise CertificateValidationError(
|
|
179
|
+
f"{field_name} must be string"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not value.strip():
|
|
183
|
+
raise CertificateValidationError(
|
|
184
|
+
f"{field_name} cannot be empty"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if not isinstance(self.payload, dict):
|
|
188
|
+
raise CertificateValidationError(
|
|
189
|
+
"payload must be dictionary"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not isinstance(self.issued_at, int):
|
|
193
|
+
raise CertificateValidationError(
|
|
194
|
+
"issued_at must be integer"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if not isinstance(self.expires_at, int):
|
|
198
|
+
raise CertificateValidationError(
|
|
199
|
+
"expires_at must be integer"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if self.expires_at <= self.issued_at:
|
|
203
|
+
raise CertificateValidationError(
|
|
204
|
+
"expires_at must be greater than issued_at"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if self.status not in VALID_STATUSES:
|
|
208
|
+
raise CertificateValidationError(
|
|
209
|
+
f"invalid status: {self.status}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# =====================================================
|
|
213
|
+
# SIGNATURE PAYLOAD
|
|
214
|
+
# =====================================================
|
|
215
|
+
|
|
216
|
+
def signature_payload(self) -> dict:
|
|
217
|
+
"""
|
|
218
|
+
ONLY immutable signed data.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"schema": self.schema,
|
|
223
|
+
|
|
224
|
+
"uid": self.uid,
|
|
225
|
+
"certificate_type": self.certificate_type,
|
|
226
|
+
|
|
227
|
+
"issuer_uid": self.issuer_uid,
|
|
228
|
+
"issuer_public_key": self.issuer_public_key,
|
|
229
|
+
|
|
230
|
+
"subject_uid": self.subject_uid,
|
|
231
|
+
|
|
232
|
+
"payload": self.payload,
|
|
233
|
+
|
|
234
|
+
"issued_at": self.issued_at,
|
|
235
|
+
"expires_at": self.expires_at
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# =====================================================
|
|
239
|
+
# CANONICAL PAYLOAD
|
|
240
|
+
# =====================================================
|
|
241
|
+
|
|
242
|
+
def canonical_payload(self) -> str:
|
|
243
|
+
"""
|
|
244
|
+
Stable deterministic representation.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
return json.dumps(
|
|
248
|
+
self.signature_payload(),
|
|
249
|
+
sort_keys=True,
|
|
250
|
+
separators=(",", ":")
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# =====================================================
|
|
254
|
+
# SIGNING
|
|
255
|
+
# =====================================================
|
|
256
|
+
|
|
257
|
+
def sign(
|
|
258
|
+
self,
|
|
259
|
+
issuer_private_key: str
|
|
260
|
+
):
|
|
261
|
+
|
|
262
|
+
self.validate()
|
|
263
|
+
|
|
264
|
+
self.signature = sign_data(
|
|
265
|
+
issuer_private_key,
|
|
266
|
+
self.canonical_payload()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# =====================================================
|
|
270
|
+
# VERIFICATION
|
|
271
|
+
# =====================================================
|
|
272
|
+
|
|
273
|
+
def verify(self) -> bool:
|
|
274
|
+
|
|
275
|
+
self.validate()
|
|
276
|
+
|
|
277
|
+
if not self.signature:
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
return verify_signature(
|
|
281
|
+
self.issuer_public_key,
|
|
282
|
+
self.canonical_payload(),
|
|
283
|
+
self.signature
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# =====================================================
|
|
287
|
+
# STATUS
|
|
288
|
+
# =====================================================
|
|
289
|
+
|
|
290
|
+
def revoke(self):
|
|
291
|
+
|
|
292
|
+
self.status = STATUS_REVOKED
|
|
293
|
+
self.revoked_at = int(time())
|
|
294
|
+
|
|
295
|
+
def is_expired(self) -> bool:
|
|
296
|
+
return time() > self.expires_at
|
|
297
|
+
|
|
298
|
+
def is_active(self) -> bool:
|
|
299
|
+
|
|
300
|
+
if self.status != STATUS_ACTIVE:
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
if self.is_expired():
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
return self.verify()
|
|
307
|
+
|
|
308
|
+
# =====================================================
|
|
309
|
+
# SERIALIZATION
|
|
310
|
+
# =====================================================
|
|
311
|
+
|
|
312
|
+
def to_dict(self) -> dict:
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"schema": self.schema,
|
|
316
|
+
|
|
317
|
+
"uid": self.uid,
|
|
318
|
+
"certificate_type": self.certificate_type,
|
|
319
|
+
|
|
320
|
+
"issuer_uid": self.issuer_uid,
|
|
321
|
+
"issuer_public_key": self.issuer_public_key,
|
|
322
|
+
|
|
323
|
+
"subject_uid": self.subject_uid,
|
|
324
|
+
|
|
325
|
+
"payload": self.payload,
|
|
326
|
+
|
|
327
|
+
"issued_at": self.issued_at,
|
|
328
|
+
"expires_at": self.expires_at,
|
|
329
|
+
|
|
330
|
+
"status": self.status,
|
|
331
|
+
"revoked_at": self.revoked_at,
|
|
332
|
+
|
|
333
|
+
"signature": self.signature
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
def to_json(self) -> str:
|
|
337
|
+
|
|
338
|
+
return json.dumps(
|
|
339
|
+
self.to_dict(),
|
|
340
|
+
indent=4,
|
|
341
|
+
sort_keys=True
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# =====================================================
|
|
345
|
+
# DESERIALIZATION
|
|
346
|
+
# =====================================================
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def from_dict(data: dict) -> "Certificate":
|
|
350
|
+
|
|
351
|
+
cert = Certificate(
|
|
352
|
+
schema=data["schema"],
|
|
353
|
+
|
|
354
|
+
uid=data["uid"],
|
|
355
|
+
certificate_type=data["certificate_type"],
|
|
356
|
+
|
|
357
|
+
issuer_uid=data["issuer_uid"],
|
|
358
|
+
issuer_public_key=data["issuer_public_key"],
|
|
359
|
+
|
|
360
|
+
subject_uid=data["subject_uid"],
|
|
361
|
+
|
|
362
|
+
payload=data["payload"],
|
|
363
|
+
|
|
364
|
+
issued_at=data["issued_at"],
|
|
365
|
+
expires_at=data["expires_at"],
|
|
366
|
+
|
|
367
|
+
status=data.get(
|
|
368
|
+
"status",
|
|
369
|
+
STATUS_ACTIVE
|
|
370
|
+
),
|
|
371
|
+
|
|
372
|
+
revoked_at=data.get(
|
|
373
|
+
"revoked_at"
|
|
374
|
+
),
|
|
375
|
+
|
|
376
|
+
signature=data.get(
|
|
377
|
+
"signature"
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
cert.validate()
|
|
382
|
+
|
|
383
|
+
return cert
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def from_json(data: str) -> "Certificate":
|
|
387
|
+
|
|
388
|
+
return Certificate.from_dict(
|
|
389
|
+
json.loads(data)
|
|
390
|
+
)
|
cryptography/encoding.py
ADDED
|
File without changes
|
cryptography/identity.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
3
|
+
Ed25519PrivateKey,
|
|
4
|
+
Ed25519PublicKey
|
|
5
|
+
)
|
|
6
|
+
from cryptography.hazmat.primitives import serialization
|
|
7
|
+
from cryptography.hazmat.primitives import hashes
|
|
8
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
9
|
+
load_pem_public_key,
|
|
10
|
+
load_der_public_key
|
|
11
|
+
)
|
|
12
|
+
import base64
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_pik(pik: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Normalize a public key to canonical PEM format.
|
|
18
|
+
|
|
19
|
+
Accepts either:
|
|
20
|
+
- Full PEM (-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----)
|
|
21
|
+
- Bare Base64 SubjectPublicKeyInfo DER (the middle line of a PEM block)
|
|
22
|
+
|
|
23
|
+
Always returns the canonical PEM string so comparisons and
|
|
24
|
+
fingerprints are format-independent.
|
|
25
|
+
"""
|
|
26
|
+
pik = pik.strip()
|
|
27
|
+
|
|
28
|
+
if pik.startswith("-----BEGIN"):
|
|
29
|
+
key = load_pem_public_key(pik.encode())
|
|
30
|
+
else:
|
|
31
|
+
# Bare Base64 → DER → key object
|
|
32
|
+
der_bytes = base64.b64decode(pik)
|
|
33
|
+
key = load_der_public_key(der_bytes)
|
|
34
|
+
|
|
35
|
+
return key.public_bytes(
|
|
36
|
+
encoding=serialization.Encoding.PEM,
|
|
37
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
38
|
+
).decode()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fingerprint_public_key(public_key: str) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Compute a format-independent SHA-256 fingerprint.
|
|
44
|
+
|
|
45
|
+
Normalizes the key to PEM before hashing so PEM input and
|
|
46
|
+
bare-Base64 input of the same key produce the same fingerprint.
|
|
47
|
+
"""
|
|
48
|
+
normalized = normalize_pik(public_key)
|
|
49
|
+
digest = hashes.Hash(hashes.SHA256())
|
|
50
|
+
digest.update(normalized.encode())
|
|
51
|
+
return digest.finalize().hex()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class PublicIdentity:
|
|
56
|
+
public_key: bytes
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class PrivateIdentity:
|
|
61
|
+
private_key: Ed25519PrivateKey
|
|
62
|
+
public_identity: PublicIdentity
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def generate() -> "PrivateIdentity":
|
|
66
|
+
private_key = Ed25519PrivateKey.generate()
|
|
67
|
+
|
|
68
|
+
public_key = private_key.public_key()
|
|
69
|
+
|
|
70
|
+
return PrivateIdentity(
|
|
71
|
+
private_key=private_key,
|
|
72
|
+
public_identity=PublicIdentity(
|
|
73
|
+
public_key=public_key.public_bytes_raw()
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class CryptographicIdentity:
|
|
79
|
+
public_key: str
|
|
80
|
+
private_key: str | None = None
|
|
81
|
+
algorithm: str = "ed25519"
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def generate() -> "CryptographicIdentity":
|
|
85
|
+
# Generate private key
|
|
86
|
+
private_key = Ed25519PrivateKey.generate()
|
|
87
|
+
|
|
88
|
+
# Generate public key
|
|
89
|
+
public_key = private_key.public_key()
|
|
90
|
+
|
|
91
|
+
# Serialize private key
|
|
92
|
+
private_bytes = private_key.private_bytes(
|
|
93
|
+
encoding=serialization.Encoding.PEM,
|
|
94
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
95
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Serialize public key
|
|
99
|
+
public_bytes = public_key.public_bytes(
|
|
100
|
+
encoding=serialization.Encoding.PEM,
|
|
101
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return CryptographicIdentity(
|
|
105
|
+
public_key=public_bytes.decode(),
|
|
106
|
+
private_key=private_bytes.decode()
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> dict:
|
|
110
|
+
return {
|
|
111
|
+
"public_key":self.public_key,
|
|
112
|
+
"private_key":self.private_key,
|
|
113
|
+
"algorithm":self.algorithm
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def fingerprint(self) -> str:
|
|
117
|
+
return fingerprint_public_key(self.public_key)
|
|
118
|
+
|
|
119
|
+
def export_public(self) -> dict:
|
|
120
|
+
return {
|
|
121
|
+
"algorithm": self.algorithm,
|
|
122
|
+
"public_key": self.public_key,
|
|
123
|
+
"fingerprint": self.fingerprint()
|
|
124
|
+
}
|