koi-net 1.0.0b19__py3-none-any.whl → 1.1.0b1__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.

Potentially problematic release.


This version of koi-net might be problematic. Click here for more details.

@@ -0,0 +1,23 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class ErrorTypes(StrEnum):
5
+ UnknownNode = "unknown_node"
6
+ InvalidKey = "invalid_key"
7
+ InvalidSignature = "invalid_signature"
8
+ InvalidTarget = "invalid_target"
9
+
10
+ class ProtocolError(Exception):
11
+ error_type: ErrorTypes
12
+
13
+ class UnknownNodeError(ProtocolError):
14
+ error_type = ErrorTypes.UnknownNode
15
+
16
+ class InvalidKeyError(ProtocolError):
17
+ error_type = ErrorTypes.InvalidKey
18
+
19
+ class InvalidSignatureError(ProtocolError):
20
+ error_type = ErrorTypes.InvalidSignature
21
+
22
+ class InvalidTargetError(ProtocolError):
23
+ error_type = ErrorTypes.InvalidTarget
koi_net/protocol/node.py CHANGED
@@ -14,4 +14,5 @@ class NodeProvides(BaseModel):
14
14
  class NodeProfile(BaseModel):
15
15
  base_url: str | None = None
16
16
  node_type: NodeType
17
- provides: NodeProvides = NodeProvides()
17
+ provides: NodeProvides = NodeProvides()
18
+ public_key: str | None = None
@@ -0,0 +1,106 @@
1
+ import logging
2
+ from base64 import urlsafe_b64decode, urlsafe_b64encode
3
+ from cryptography.hazmat.primitives import hashes
4
+ from cryptography.hazmat.primitives.asymmetric import ec
5
+ from cryptography.hazmat.primitives import serialization
6
+ from rid_lib.ext.utils import sha256_hash
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class PrivateKey:
12
+ priv_key: ec.EllipticCurvePrivateKey
13
+
14
+ def __init__(self, priv_key):
15
+ self.priv_key = priv_key
16
+
17
+ @classmethod
18
+ def generate(cls):
19
+ return cls(priv_key=ec.generate_private_key(ec.SECP192R1()))
20
+
21
+ def public_key(self) -> "PublicKey":
22
+ return PublicKey(self.priv_key.public_key())
23
+
24
+ @classmethod
25
+ def from_pem(cls, priv_key_pem: str, password: str):
26
+ return cls(
27
+ priv_key=serialization.load_pem_private_key(
28
+ data=priv_key_pem.encode(),
29
+ password=password.encode()
30
+ )
31
+ )
32
+
33
+ def to_pem(self, password: str) -> str:
34
+ return self.priv_key.private_bytes(
35
+ encoding=serialization.Encoding.PEM,
36
+ format=serialization.PrivateFormat.PKCS8,
37
+ encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
38
+ ).decode()
39
+
40
+ def sign(self, message: bytes) -> str:
41
+ hashed_message = sha256_hash(message.decode())
42
+
43
+ signature = urlsafe_b64encode(
44
+ self.priv_key.sign(
45
+ data=message,
46
+ signature_algorithm=ec.ECDSA(hashes.SHA256())
47
+ )
48
+ ).decode()
49
+
50
+ logger.debug(f"Signing message with [{self.public_key().to_der()}]")
51
+ logger.debug(f"hash: {hashed_message}")
52
+ logger.debug(f"signature: {signature}")
53
+
54
+ return signature
55
+
56
+
57
+ class PublicKey:
58
+ pub_key: ec.EllipticCurvePublicKey
59
+
60
+ def __init__(self, pub_key):
61
+ self.pub_key = pub_key
62
+
63
+ @classmethod
64
+ def from_pem(cls, pub_key_pem: str):
65
+ return cls(
66
+ pub_key=serialization.load_pem_public_key(
67
+ data=pub_key_pem.encode()
68
+ )
69
+ )
70
+
71
+ def to_pem(self) -> str:
72
+ return self.pub_key.public_bytes(
73
+ encoding=serialization.Encoding.PEM,
74
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
75
+ ).decode()
76
+
77
+ @classmethod
78
+ def from_der(cls, pub_key_der: str):
79
+ return cls(
80
+ pub_key=serialization.load_der_public_key(
81
+ data=urlsafe_b64decode(pub_key_der)
82
+ )
83
+ )
84
+
85
+ def to_der(self) -> str:
86
+ return urlsafe_b64encode(
87
+ self.pub_key.public_bytes(
88
+ encoding=serialization.Encoding.DER,
89
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
90
+ )
91
+ ).decode()
92
+
93
+ def verify(self, signature: str, message: bytes) -> bool:
94
+ hashed_message = sha256_hash(message.decode())
95
+
96
+ logger.debug(f"Verifying message with [{self.to_der()}]")
97
+ logger.debug(f"hash: {hashed_message}")
98
+ logger.debug(f"signature: {signature}")
99
+
100
+ # NOTE: throws cryptography.exceptions.InvalidSignature on failure
101
+
102
+ self.pub_key.verify(
103
+ signature=urlsafe_b64decode(signature),
104
+ data=message,
105
+ signature_algorithm=ec.ECDSA(hashes.SHA256())
106
+ )
koi_net/secure.py ADDED
@@ -0,0 +1,117 @@
1
+ import logging
2
+ from functools import wraps
3
+
4
+ import cryptography.exceptions
5
+ from rid_lib.ext import Bundle
6
+ from rid_lib.ext.utils import sha256_hash
7
+ from .identity import NodeIdentity
8
+ from .protocol.envelope import UnsignedEnvelope, SignedEnvelope
9
+ from .protocol.secure import PublicKey
10
+ from .protocol.api_models import EventsPayload
11
+ from .protocol.event import EventType
12
+ from .protocol.node import NodeProfile
13
+ from .protocol.secure import PrivateKey
14
+ from .protocol.errors import (
15
+ UnknownNodeError,
16
+ InvalidKeyError,
17
+ InvalidSignatureError,
18
+ InvalidTargetError
19
+ )
20
+ from .effector import Effector
21
+ from .config import NodeConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class Secure:
27
+ identity: NodeIdentity
28
+ effector: Effector
29
+ config: NodeConfig
30
+ priv_key: PrivateKey
31
+
32
+ def __init__(
33
+ self,
34
+ identity: NodeIdentity,
35
+ effector: Effector,
36
+ config: NodeConfig
37
+ ):
38
+ self.identity = identity
39
+ self.effector = effector
40
+ self.config = config
41
+
42
+ self.priv_key = self._load_priv_key()
43
+
44
+ def _load_priv_key(self) -> PrivateKey:
45
+ with open(self.config.koi_net.private_key_pem_path, "r") as f:
46
+ priv_key_pem = f.read()
47
+
48
+ return PrivateKey.from_pem(
49
+ priv_key_pem=priv_key_pem,
50
+ password=self.config.env.priv_key_password
51
+ )
52
+
53
+ def _handle_unknown_node(self, envelope: SignedEnvelope) -> Bundle | None:
54
+ if type(envelope.payload) != EventsPayload:
55
+ return None
56
+
57
+ for event in envelope.payload.events:
58
+ # must be NEW event for bundle of source node's profile
59
+ if event.rid != envelope.source_node:
60
+ continue
61
+ if event.event_type != EventType.NEW:
62
+ continue
63
+
64
+ return event.bundle
65
+ return None
66
+
67
+ def create_envelope(self, payload, target) -> SignedEnvelope:
68
+ return UnsignedEnvelope(
69
+ payload=payload,
70
+ source_node=self.identity.rid,
71
+ target_node=target
72
+ ).sign_with(self.priv_key)
73
+
74
+ def validate_envelope(self, envelope: SignedEnvelope):
75
+ node_bundle = (
76
+ self.effector.deref(envelope.source_node) or
77
+ self._handle_unknown_node(envelope)
78
+ )
79
+
80
+ if not node_bundle:
81
+ raise UnknownNodeError(f"Couldn't resolve {envelope.source_node}")
82
+
83
+ node_profile = node_bundle.validate_contents(NodeProfile)
84
+
85
+ # check that public key matches source node RID
86
+ if envelope.source_node.hash != sha256_hash(node_profile.public_key):
87
+ raise InvalidKeyError("Invalid public key on new node!")
88
+
89
+ # check envelope signed by validated public key
90
+ pub_key = PublicKey.from_der(node_profile.public_key)
91
+ try:
92
+ envelope.verify_with(pub_key)
93
+ except cryptography.exceptions.InvalidSignature as err:
94
+ raise InvalidSignatureError(f"Signature {envelope.signature} is invalid.")
95
+
96
+ # check that this node is the target of the envelope
97
+ if envelope.target_node != self.identity.rid:
98
+ raise InvalidTargetError(f"Envelope target {envelope.target_node!r} is not me")
99
+
100
+ def envelope_handler(self, func):
101
+ @wraps(func)
102
+ async def wrapper(req: SignedEnvelope, *args, **kwargs) -> SignedEnvelope | None:
103
+ logger.info("Validating envelope")
104
+
105
+ self.validate_envelope(req)
106
+ logger.info("Calling endpoint handler")
107
+
108
+ result = await func(req, *args, **kwargs)
109
+
110
+ if result is not None:
111
+ logger.info("Creating response envelope")
112
+ return self.create_envelope(
113
+ payload=result,
114
+ target=req.source_node
115
+ )
116
+ return wrapper
117
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-net
3
- Version: 1.0.0b19
3
+ Version: 1.1.0b1
4
4
  Summary: Implementation of KOI-net protocol in Python
5
5
  Project-URL: Homepage, https://github.com/BlockScience/koi-net/
6
6
  Author-email: Luke Miller <luke@block.science>
@@ -27,11 +27,12 @@ License: MIT License
27
27
  SOFTWARE.
28
28
  License-File: LICENSE
29
29
  Requires-Python: >=3.10
30
+ Requires-Dist: cryptography>=45.0.3
30
31
  Requires-Dist: httpx>=0.28.1
31
32
  Requires-Dist: networkx>=3.4.2
32
33
  Requires-Dist: pydantic>=2.10.6
33
34
  Requires-Dist: python-dotenv>=1.1.0
34
- Requires-Dist: rid-lib>=3.2.3
35
+ Requires-Dist: rid-lib>=3.2.7
35
36
  Requires-Dist: ruamel-yaml>=0.18.10
36
37
  Provides-Extra: dev
37
38
  Requires-Dist: build; extra == 'dev'
@@ -0,0 +1,35 @@
1
+ koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
2
+ koi_net/config.py,sha256=Zu6iVPQ1yar9OMZQp-Ez0eYRIoCeDq69eTQyYplQqQc,4057
3
+ koi_net/context.py,sha256=JmbpCzusXFq_NCXiUp5Z56N6vpBdYMUK8eOs7ogO68A,1428
4
+ koi_net/core.py,sha256=QIYRlvi5BFxaQKYrbI8CGs2yZxf-U6PeJlYBaG6HsLQ,6474
5
+ koi_net/default_actions.py,sha256=TkQR9oj9CpO37Gb5bZLmFNl-Q8n3OxGiX4dvxQR7SaA,421
6
+ koi_net/effector.py,sha256=gSyZgRxQ91X04UL261e2pXWUfBHnQTGtjSHpc2JufxA,4097
7
+ koi_net/identity.py,sha256=FvIWksGTqwM7HCevIwmo_6l-t-2tnYkaaR4CanZatL4,569
8
+ koi_net/secure.py,sha256=cGNF2assqCaYq0i0fhQBm7aREoAdpY-XVypDsE1ALaU,3970
9
+ koi_net/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ koi_net/network/behavior.py,sha256=NZLvWlrxR0uWriE3ZzCXmocUVccQthy7Xx8E_8KBwsg,1208
11
+ koi_net/network/error_handler.py,sha256=CrmCpBY2oj4nl7uXrIYusUHDKxPZ1HDuQAtiBSZarRI,1623
12
+ koi_net/network/event_queue.py,sha256=QV1dLOe-H85fNJVkc-e0ZRsUpHTkFcMLMbp3WU5gRNg,7225
13
+ koi_net/network/graph.py,sha256=NLstBsPa9By0luxcTjThnqVd3hxfQdFwn8tWgJ6u4l4,4144
14
+ koi_net/network/request_handler.py,sha256=77SHLO92gVHTusUXWo89KgjRnxU9vLG_Qi8HxTFtFBg,6376
15
+ koi_net/network/resolver.py,sha256=coIp4M6k0-8sUfAy4h2NMx_7zCNroWlCHKOj3AXZVhc,5412
16
+ koi_net/network/response_handler.py,sha256=tEfzSZWXKCRwUlXhM8Qp3Y6BKTQ4abfi-MrSaatlITI,1980
17
+ koi_net/processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ koi_net/processor/default_handlers.py,sha256=SX1eP740W9LeG-IEoBOmsNrKJfOEDDuEQUSwnJEG_8w,8954
19
+ koi_net/processor/handler.py,sha256=_loaHjgVGVUxtCQdvAY9dQ0iqiq5co7wB2tK-usuv3Y,2355
20
+ koi_net/processor/interface.py,sha256=ebDwqggznFRfp2PT8-UJPUAvCwX8nZaaQ68FUeWQvmw,3682
21
+ koi_net/processor/knowledge_object.py,sha256=avQnsaeqqiJxy40P1VGljuQMtAGmJB-TBa4pmBXTaIs,3863
22
+ koi_net/processor/knowledge_pipeline.py,sha256=i7FpCFl0UIOwCI5zhP1i8M4PX4A48VN28iV9jruvN5k,9486
23
+ koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ koi_net/protocol/api_models.py,sha256=bHhbLeq8I7nVBuW-AXgKfb1O58_mLogHRG8A_LZt2IE,1188
25
+ koi_net/protocol/consts.py,sha256=bisbVEojPIHlLhkLafBzfIhH25TjNfvTORF1g6YXzIM,243
26
+ koi_net/protocol/edge.py,sha256=dQKtI0_eX2E6tD7kMExv6DeJMkqNo2cY-LxJMJbiK0E,963
27
+ koi_net/protocol/envelope.py,sha256=W-K3rjwqwAL9wCXb2_gpAUwnc2xOVdZ1UWMoDlLrqJY,1690
28
+ koi_net/protocol/errors.py,sha256=A83QiYe_fJdxW2lsNsLCujWxDr5sk1UmYYd3TGbSNJA,601
29
+ koi_net/protocol/event.py,sha256=eGgihEj1gliLoQRk8pVB2q_was0AGo-PbT3Hqnpn3oU,1379
30
+ koi_net/protocol/node.py,sha256=7GQzHORFr9cP4BqJgir6EGSWCskL-yqmvJksIiLfcWU,409
31
+ koi_net/protocol/secure.py,sha256=Reem9Z4le4uWXM9uczNOdmgVBg8p4YQav-7_c3pZ1CQ,3366
32
+ koi_net-1.1.0b1.dist-info/METADATA,sha256=pHwaqM6wXoQpOUINUBlLlmCnSv3nEHF-EbL5x42jf3s,37142
33
+ koi_net-1.1.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
+ koi_net-1.1.0b1.dist-info/licenses/LICENSE,sha256=03mgCL5qth2aD9C3F3qNVs4sFJSpK9kjtYCyOwdSp7s,1069
35
+ koi_net-1.1.0b1.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- from rid_lib.core import RIDType
2
- from rid_lib.ext.bundle import Bundle
3
- from rid_lib.types import KoiNetEdge
4
- from rid_lib.types.koi_net_node import KoiNetNode
5
- from .edge import EdgeProfile, EdgeStatus, EdgeType
6
-
7
- def generate_edge_bundle(
8
- source: KoiNetNode,
9
- target: KoiNetNode,
10
- rid_types: list[RIDType],
11
- edge_type: EdgeType
12
- ) -> Bundle:
13
- edge_rid = KoiNetEdge.generate(source, target)
14
- edge_profile = EdgeProfile(
15
- source=source,
16
- target=target,
17
- rid_types=rid_types,
18
- edge_type=edge_type,
19
- status=EdgeStatus.PROPOSED
20
- )
21
- edge_bundle = Bundle.generate(
22
- edge_rid,
23
- edge_profile.model_dump()
24
- )
25
- return edge_bundle
@@ -1,25 +0,0 @@
1
- koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
2
- koi_net/config.py,sha256=TIKb1kFTcEysqwdHp6yCNpcXeS84dlprcb-f0z2jF0Y,3160
3
- koi_net/core.py,sha256=IO8kqiNMYVeuNzilq7eHBA7IulsxRjrCbWnIAx6_abA,4406
4
- koi_net/identity.py,sha256=muc5vuQ8zUOebhwAB3-ql6W2pgQETiYXXQAFBv8bLyg,1288
5
- koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
6
- koi_net/network/graph.py,sha256=dsfPuHUTkCzlj0QeL0e7dgp7-FR5_AGP7eE8EpBPhC0,4710
7
- koi_net/network/interface.py,sha256=icpl0rzpC5yV86BQBAbjAjK7AWhgCFoyFLm_Fjf1mCI,10451
8
- koi_net/network/request_handler.py,sha256=66gjX2x4UnBWZYwKLjp_3WkhL-ekhR3VAyfGviHTcUs,4790
9
- koi_net/network/response_handler.py,sha256=CAwici2Etj9ESndERXdtYkMlc4gWHz_xc7jHgY2Qjcg,1830
10
- koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
11
- koi_net/processor/default_handlers.py,sha256=dP64lEJ64BJ7H8PhFK-GZI1pv51tVVINV4jAgcOtOhc,8669
12
- koi_net/processor/handler.py,sha256=7X6M6PP8m6-xdtsP1y4QO83g_MN5VSszNNikprITK80,2523
13
- koi_net/processor/interface.py,sha256=Kyw4SQos_1WdcPJJe-j2w4xDIfwtmpF4mfGlkRVRqUI,12876
14
- koi_net/processor/knowledge_object.py,sha256=RCgzkILsWm1Jw_NkSu4jTRYA9Ugga6mJ4jqKWwketQs,4090
15
- koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- koi_net/protocol/api_models.py,sha256=DYDKCRD2Uja633bBAyTsaxyb1oF9pX9yQ9NpNAbkczo,1070
17
- koi_net/protocol/consts.py,sha256=bisbVEojPIHlLhkLafBzfIhH25TjNfvTORF1g6YXzIM,243
18
- koi_net/protocol/edge.py,sha256=CcmvIY4P1HEBdKNJ4wFRDmwYMRMss24Besmbi7ZRFxQ,427
19
- koi_net/protocol/event.py,sha256=eGgihEj1gliLoQRk8pVB2q_was0AGo-PbT3Hqnpn3oU,1379
20
- koi_net/protocol/helpers.py,sha256=8ZkQrjb_G0QEaMIKe9wkFOBonl1bkmemx_pwKMwIiLg,695
21
- koi_net/protocol/node.py,sha256=2HhCh3LdBLlY2Z_kXNmKHzpVLKbP_ODob3HjHayFQtM,375
22
- koi_net-1.0.0b19.dist-info/METADATA,sha256=uVzxDiRiHRncwfjotk7j_frmm_ka6PEpI-kcA278HoE,37107
23
- koi_net-1.0.0b19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
- koi_net-1.0.0b19.dist-info/licenses/LICENSE,sha256=03mgCL5qth2aD9C3F3qNVs4sFJSpK9kjtYCyOwdSp7s,1069
25
- koi_net-1.0.0b19.dist-info/RECORD,,