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
@@ -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
+ )
File without changes
@@ -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
+ }