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.
Files changed (62) hide show
  1. api/__init__.py +1 -0
  2. api/manager/__init__.py +1 -0
  3. api/manager/v1/__init__.py +78 -0
  4. api/manager/v1/authentication.py +278 -0
  5. api/manager/v1/client.py +111 -0
  6. api/manager/v1/domains.py +64 -0
  7. api/manager/v1/entities.py +97 -0
  8. api/manager/v1/exceptions.py +98 -0
  9. api/manager/v1/identity.py +44 -0
  10. api/manager/v1/models.py +342 -0
  11. api/manager/v1/passports.py +131 -0
  12. api/manager/v1/sessions.py +83 -0
  13. api/manager/v1/transport.py +253 -0
  14. api/manager/v1/tunnels.py +120 -0
  15. cryptography/__init__.py +0 -0
  16. cryptography/certificate.py +390 -0
  17. cryptography/encoding.py +0 -0
  18. cryptography/identity.py +124 -0
  19. cryptography/keys.py +463 -0
  20. cryptography/signatures.py +63 -0
  21. cryptography/utils.py +0 -0
  22. domain/__init__.py +0 -0
  23. domain/domain.py +80 -0
  24. domain/membership.py +21 -0
  25. domain/permissions.py +14 -0
  26. domain/policies.py +2 -0
  27. identity/__init__.py +0 -0
  28. identity/identification.py +64 -0
  29. identity/store.py +150 -0
  30. modules/__init__.py +0 -0
  31. modules/shell/__init__.py +0 -0
  32. modules/shell/client.py +361 -0
  33. modules/shell/models.py +61 -0
  34. modules/shell/protocol.py +249 -0
  35. modules/shell/server.py +511 -0
  36. modules/shell/session.py +339 -0
  37. modules/utils.py +212 -0
  38. openshell_shared-0.1.2.dist-info/METADATA +59 -0
  39. openshell_shared-0.1.2.dist-info/RECORD +62 -0
  40. openshell_shared-0.1.2.dist-info/WHEEL +5 -0
  41. openshell_shared-0.1.2.dist-info/top_level.txt +7 -0
  42. protocols/__init__.py +0 -0
  43. protocols/negotiation/challenge.py +127 -0
  44. protocols/negotiation/models.py +28 -0
  45. standards/__init__.py +0 -0
  46. standards/certificates/__init__.py +0 -0
  47. standards/certificates/status.py +12 -0
  48. standards/certificates/types.py +11 -0
  49. standards/entities/__init__.py +0 -0
  50. standards/entities/types.py +14 -0
  51. standards/events/__init__.py +0 -0
  52. standards/events/schemas/__init__.py +0 -0
  53. standards/events/schemas/entity_registered.py +13 -0
  54. standards/events/types.py +18 -0
  55. standards/passports/__init__.py +0 -0
  56. standards/passports/types.py +5 -0
  57. standards/permissions/__init__.py +0 -0
  58. standards/permissions/types.py +14 -0
  59. standards/roles/__init__.py +0 -0
  60. standards/roles/types.py +8 -0
  61. standards/transports/__init__.py +0 -0
  62. standards/transports/types.py +24 -0
cryptography/keys.py ADDED
@@ -0,0 +1,463 @@
1
+ # shared/cryptography/keyformats.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from typing import Final
7
+
8
+ from cryptography.hazmat.primitives import serialization
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
10
+ Ed25519PrivateKey,
11
+ Ed25519PublicKey,
12
+ )
13
+
14
+
15
+ class Ed25519KeyFormats:
16
+ """
17
+ Ed25519 key format utilities.
18
+
19
+ Canonical internal format:
20
+
21
+ RAW bytes
22
+
23
+ Public key:
24
+ 32 bytes
25
+
26
+ Private key seed:
27
+ 32 bytes
28
+
29
+ Formats:
30
+
31
+ STORAGE:
32
+ RAW bytes
33
+
34
+ PROCESS:
35
+ HEX
36
+
37
+ EXPORT:
38
+ PEM / HEX
39
+ """
40
+
41
+ PUBLIC: Final[str] = "PUBLIC"
42
+ PRIVATE: Final[str] = "PRIVATE"
43
+
44
+ PEM: Final[str] = "PEM"
45
+ HEX: Final[str] = "HEX"
46
+ RAW: Final[str] = "RAW"
47
+
48
+ KEY_SIZE: Final[int] = 32
49
+
50
+
51
+ # =====================================================
52
+ # DETECTION
53
+ # =====================================================
54
+
55
+ @staticmethod
56
+ def is_pem(
57
+ data: str | bytes
58
+ ) -> bool:
59
+
60
+ if isinstance(data, bytes):
61
+
62
+ return (
63
+ b"-----BEGIN PUBLIC KEY-----" in data
64
+ or
65
+ b"-----BEGIN PRIVATE KEY-----" in data
66
+ )
67
+
68
+ if isinstance(data, str):
69
+
70
+ return (
71
+ "-----BEGIN PUBLIC KEY-----" in data
72
+ or
73
+ "-----BEGIN PRIVATE KEY-----" in data
74
+ )
75
+
76
+ return False
77
+
78
+
79
+ @staticmethod
80
+ def is_hex(
81
+ data: str | bytes
82
+ ) -> bool:
83
+
84
+ if isinstance(data, bytes):
85
+
86
+ try:
87
+ data = data.decode("ascii")
88
+
89
+ except UnicodeDecodeError:
90
+ return False
91
+
92
+ if not isinstance(data, str):
93
+ return False
94
+
95
+ data = data.strip()
96
+
97
+ if len(data) != 64:
98
+ return False
99
+
100
+ try:
101
+ bytes.fromhex(data)
102
+ return True
103
+
104
+ except ValueError:
105
+ return False
106
+
107
+
108
+ @staticmethod
109
+ def is_raw(
110
+ data
111
+ ) -> bool:
112
+
113
+ return (
114
+ isinstance(data, bytes)
115
+ and
116
+ len(data) == 32
117
+ )
118
+
119
+
120
+ @classmethod
121
+ def detect_format(
122
+ cls,
123
+ data
124
+ ) -> str | None:
125
+
126
+ if cls.is_pem(data):
127
+ return cls.PEM
128
+
129
+ if cls.is_hex(data):
130
+ return cls.HEX
131
+
132
+ if cls.is_raw(data):
133
+ return cls.RAW
134
+
135
+ return None
136
+
137
+
138
+
139
+ # =====================================================
140
+ # TYPE DETECTION
141
+ # =====================================================
142
+
143
+ @classmethod
144
+ def detect_key_type(
145
+ cls,
146
+ data
147
+ ) -> str | None:
148
+
149
+
150
+ if not cls.is_pem(data):
151
+ return None
152
+
153
+
154
+ if isinstance(data, bytes):
155
+
156
+ if b"BEGIN PUBLIC KEY" in data:
157
+ return cls.PUBLIC
158
+
159
+ if b"BEGIN PRIVATE KEY" in data:
160
+ return cls.PRIVATE
161
+
162
+
163
+ if isinstance(data, str):
164
+
165
+ if "BEGIN PUBLIC KEY" in data:
166
+ return cls.PUBLIC
167
+
168
+ if "BEGIN PRIVATE KEY" in data:
169
+ return cls.PRIVATE
170
+
171
+
172
+ return None
173
+
174
+
175
+
176
+ # =====================================================
177
+ # NORMALIZATION
178
+ # =====================================================
179
+
180
+ @classmethod
181
+ def normalize_public_key(
182
+ cls,
183
+ key
184
+ ) -> bytes:
185
+
186
+
187
+ fmt = cls.detect_format(key)
188
+
189
+
190
+ if fmt == cls.RAW:
191
+
192
+ return key
193
+
194
+
195
+ if fmt == cls.HEX:
196
+
197
+ if isinstance(key, bytes):
198
+ key = key.decode("ascii")
199
+
200
+ raw = bytes.fromhex(key)
201
+
202
+ cls._validate_size(raw)
203
+
204
+ return raw
205
+
206
+
207
+ if fmt == cls.PEM:
208
+
209
+ if isinstance(key, str):
210
+ key = key.encode()
211
+
212
+
213
+ public_key = (
214
+ serialization
215
+ .load_pem_public_key(key)
216
+ )
217
+
218
+
219
+ if not isinstance(
220
+ public_key,
221
+ Ed25519PublicKey
222
+ ):
223
+ raise ValueError(
224
+ "Invalid Ed25519 public key"
225
+ )
226
+
227
+
228
+ return public_key.public_bytes(
229
+ serialization.Encoding.Raw,
230
+ serialization.PublicFormat.Raw
231
+ )
232
+
233
+
234
+ raise ValueError(
235
+ "Unsupported public key format"
236
+ )
237
+
238
+
239
+
240
+ @classmethod
241
+ def normalize_private_key(
242
+ cls,
243
+ key
244
+ ) -> bytes:
245
+
246
+
247
+ fmt = cls.detect_format(key)
248
+
249
+
250
+ if fmt == cls.RAW:
251
+
252
+ return key
253
+
254
+
255
+ if fmt == cls.HEX:
256
+
257
+ if isinstance(key, bytes):
258
+ key = key.decode("ascii")
259
+
260
+ raw = bytes.fromhex(key)
261
+
262
+ cls._validate_size(raw)
263
+
264
+ return raw
265
+
266
+
267
+ if fmt == cls.PEM:
268
+
269
+ if isinstance(key, str):
270
+ key = key.encode()
271
+
272
+
273
+ private_key = (
274
+ serialization
275
+ .load_pem_private_key(
276
+ key,
277
+ password=None
278
+ )
279
+ )
280
+
281
+
282
+ if not isinstance(
283
+ private_key,
284
+ Ed25519PrivateKey
285
+ ):
286
+ raise ValueError(
287
+ "Invalid Ed25519 private key"
288
+ )
289
+
290
+
291
+ return private_key.private_bytes(
292
+ serialization.Encoding.Raw,
293
+ serialization.PrivateFormat.Raw,
294
+ serialization.NoEncryption()
295
+ )
296
+
297
+
298
+ raise ValueError(
299
+ "Unsupported private key format"
300
+ )
301
+
302
+
303
+
304
+ # =====================================================
305
+ # VALIDATION
306
+ # =====================================================
307
+
308
+ @classmethod
309
+ def _validate_size(
310
+ cls,
311
+ raw: bytes
312
+ ):
313
+
314
+ if len(raw) != cls.KEY_SIZE:
315
+
316
+ raise ValueError(
317
+ "Invalid Ed25519 key size"
318
+ )
319
+
320
+
321
+
322
+ @classmethod
323
+ def validate_public_key(
324
+ cls,
325
+ key
326
+ ) -> bool:
327
+
328
+ try:
329
+
330
+ cls.normalize_public_key(key)
331
+
332
+ return True
333
+
334
+ except (ValueError, TypeError):
335
+
336
+ return False
337
+
338
+
339
+
340
+ @classmethod
341
+ def validate_private_key(
342
+ cls,
343
+ key
344
+ ) -> bool:
345
+
346
+ try:
347
+
348
+ cls.normalize_private_key(key)
349
+
350
+ return True
351
+
352
+ except (ValueError, TypeError):
353
+
354
+ return False
355
+
356
+
357
+
358
+ # =====================================================
359
+ # DOMAIN CONVERSION
360
+ # =====================================================
361
+
362
+ @classmethod
363
+ def to_storage(
364
+ cls,
365
+ key,
366
+ private=False
367
+ ) -> bytes:
368
+ """
369
+ Database / internal storage.
370
+
371
+ Returns RAW bytes.
372
+ """
373
+
374
+ if private:
375
+ return cls.normalize_private_key(key)
376
+
377
+ return cls.normalize_public_key(key)
378
+
379
+
380
+
381
+ @classmethod
382
+ def to_process(
383
+ cls,
384
+ key,
385
+ private=False
386
+ ) -> str:
387
+ """
388
+ Internal processing format.
389
+
390
+ Returns HEX.
391
+ """
392
+
393
+ raw = cls.to_storage(
394
+ key,
395
+ private
396
+ )
397
+
398
+ return raw.hex()
399
+
400
+
401
+
402
+ @classmethod
403
+ def to_export(
404
+ cls,
405
+ key,
406
+ private=False
407
+ ) -> str:
408
+ """
409
+ External interchange.
410
+
411
+ Returns PEM.
412
+ """
413
+
414
+ raw = cls.to_storage(
415
+ key,
416
+ private
417
+ )
418
+
419
+
420
+ if private:
421
+
422
+ return (
423
+ Ed25519PrivateKey
424
+ .from_private_bytes(raw)
425
+ .private_bytes(
426
+ serialization.Encoding.PEM,
427
+ serialization.PrivateFormat.PKCS8,
428
+ serialization.NoEncryption()
429
+ )
430
+ .decode()
431
+ )
432
+
433
+
434
+ return (
435
+ Ed25519PublicKey
436
+ .from_public_bytes(raw)
437
+ .public_bytes(
438
+ serialization.Encoding.PEM,
439
+ serialization.PublicFormat.SubjectPublicKeyInfo
440
+ )
441
+ .decode()
442
+ )
443
+
444
+
445
+
446
+ # =====================================================
447
+ # FINGERPRINT
448
+ # =====================================================
449
+
450
+ @classmethod
451
+ def fingerprint(
452
+ cls,
453
+ key
454
+ ) -> str:
455
+ """
456
+ Stable SHA256 identity fingerprint.
457
+ """
458
+
459
+ raw = cls.normalize_public_key(key)
460
+
461
+ return hashlib.sha256(
462
+ raw
463
+ ).hexdigest()
@@ -0,0 +1,63 @@
1
+ # Library import
2
+ import json
3
+ import base64
4
+
5
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
6
+ Ed25519PrivateKey,
7
+ Ed25519PublicKey
8
+ )
9
+
10
+ from cryptography.hazmat.primitives import serialization
11
+
12
+
13
+ # Functions
14
+ def canonicalize(data: dict) -> bytes:
15
+ """
16
+ Convert dictionary to canonical JSON bytes
17
+ """
18
+
19
+ return json.dumps(
20
+ data,
21
+ sort_keys=True,
22
+ separators=(",", ":")
23
+ ).encode()
24
+
25
+
26
+ def sign_data(
27
+ private_key_pem: str,
28
+ data: dict
29
+ ) -> str:
30
+
31
+ private_key = serialization.load_pem_private_key(
32
+ private_key_pem.encode(),
33
+ password=None
34
+ )
35
+
36
+ payload = canonicalize(data)
37
+
38
+ signature = private_key.sign(payload)
39
+
40
+ return base64.b64encode(signature).decode()
41
+
42
+
43
+ def verify_signature(
44
+ public_key_pem: str,
45
+ data: dict,
46
+ signature: str
47
+ ) -> bool:
48
+
49
+ public_key = serialization.load_pem_public_key(
50
+ public_key_pem.encode()
51
+ )
52
+
53
+ payload = canonicalize(data)
54
+
55
+ signature_bytes = base64.b64decode(signature)
56
+
57
+ try:
58
+ public_key.verify(signature_bytes, payload)
59
+
60
+ return True
61
+
62
+ except Exception:
63
+ return False
cryptography/utils.py ADDED
File without changes
domain/__init__.py ADDED
File without changes
domain/domain.py ADDED
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+
3
+ from ..identity.identification import (
4
+ EntityIdentity,
5
+ Identification
6
+ )
7
+
8
+ from ..cryptography.identity import (
9
+ CryptographicIdentity
10
+ )
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Domain:
15
+ identity: EntityIdentity
16
+ description: str | None = None
17
+
18
+ @property
19
+ def uid(self) -> str:
20
+ return self.identity.identification.uid
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ return self.identity.name
25
+
26
+ @property
27
+ def cryptographic_identity(self):
28
+ return self.identity.cryptographic_identity
29
+
30
+ @staticmethod
31
+ def generate(
32
+ name: str,
33
+ description: str | None = None
34
+ ) -> "Domain":
35
+
36
+ return Domain(
37
+ identity=EntityIdentity.generate(
38
+ name=name
39
+ ),
40
+ description=description
41
+ )
42
+
43
+ @staticmethod
44
+ def from_database(
45
+ uid: str,
46
+ name: str,
47
+ pik: str,
48
+ description: str | None = None
49
+ ) -> "Domain":
50
+ """
51
+ Reconstruct domain from database data.
52
+ """
53
+
54
+ return Domain(
55
+ identity=EntityIdentity(
56
+ identification=Identification(
57
+ uid=uid
58
+ ),
59
+
60
+ cryptographic_identity=CryptographicIdentity(
61
+ public_key=pik
62
+ ),
63
+
64
+ name=name
65
+ ),
66
+
67
+ description=description
68
+ )
69
+
70
+ def export_public(self) -> dict:
71
+ return {
72
+ "identity": self.identity.export_public(),
73
+ "description": self.description
74
+ }
75
+
76
+ def to_dict(self) -> dict:
77
+ return {
78
+ "identity": self.identity.to_dict(),
79
+ "description": self.description
80
+ }
domain/membership.py ADDED
@@ -0,0 +1,21 @@
1
+ # Library import
2
+ from uuid import UUID
3
+ from datetime import datetime
4
+
5
+ from .permissions import Permission
6
+
7
+ # Classes
8
+ class Membership:
9
+ def __init__(
10
+ self,
11
+ entity_uid: UUID,
12
+ domain_uid: UUID,
13
+ ):
14
+ self.entity_uid = entity_uid
15
+ self.domain_uid = domain_uid
16
+
17
+ self.permissions: set[Permission] = set()
18
+
19
+ self.created_at = datetime.utcnow()
20
+
21
+ self.revoked = False
domain/permissions.py ADDED
@@ -0,0 +1,14 @@
1
+ # Library import
2
+ from enum import Enum
3
+
4
+ # Classes
5
+ class Permission(Enum):
6
+ # Agents
7
+ AGENT_READ = "agent.read"
8
+ AGENT_EXECUTE = "agent.execute"
9
+
10
+ # Proxys
11
+ PROXY_USE = "proxy.use"
12
+
13
+ # Domains
14
+ DOMAIN_ADMIN = "domain.admin"
domain/policies.py ADDED
@@ -0,0 +1,2 @@
1
+ class Policy:
2
+ def evaluate(self, context) -> bool: return True
identity/__init__.py ADDED
File without changes
@@ -0,0 +1,64 @@
1
+ from dataclasses import dataclass
2
+ from uuid import UUID
3
+ from uuid6 import uuid7
4
+ from ..cryptography.identity import CryptographicIdentity
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Identification:
9
+ uid: str
10
+
11
+ def __post_init__(self):
12
+ UUID(self.uid)
13
+
14
+ def to_string(self) -> str:
15
+ return self.uid
16
+
17
+ def to_dict(self) -> dict:
18
+ return {
19
+ "uid": self.uid
20
+ }
21
+
22
+ @staticmethod
23
+ def generate() -> "Identification":
24
+ return Identification(uid=str(uuid7()))
25
+
26
+ @dataclass(frozen=True)
27
+ class EntityIdentity:
28
+ identification: Identification
29
+ cryptographic_identity: CryptographicIdentity
30
+ name: str
31
+
32
+ @staticmethod
33
+ def generate(name) -> "EntityIdentity":
34
+ return EntityIdentity(
35
+ identification=Identification.generate(),
36
+ cryptographic_identity=CryptographicIdentity.generate(),
37
+ name=name
38
+ )
39
+
40
+ def export_public(self) -> dict:
41
+ return {
42
+ "identification": (
43
+ self.identification.to_dict()
44
+ ),
45
+
46
+ "cryptographic_identity": (
47
+ self.cryptographic_identity.export_public()
48
+ ),
49
+
50
+ "name": self.name
51
+ }
52
+
53
+ def to_dict(self) -> dict:
54
+ return {
55
+ "identification": (
56
+ self.identification.to_dict()
57
+ ),
58
+
59
+ "cryptographic_identity": (
60
+ self.cryptographic_identity.to_dict()
61
+ ),
62
+
63
+ "name": self.name
64
+ }