koi-net 1.2.4__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.

Files changed (59) hide show
  1. koi_net/__init__.py +1 -0
  2. koi_net/behaviors/handshaker.py +68 -0
  3. koi_net/behaviors/profile_monitor.py +23 -0
  4. koi_net/behaviors/sync_manager.py +68 -0
  5. koi_net/build/artifact.py +209 -0
  6. koi_net/build/assembler.py +60 -0
  7. koi_net/build/comp_order.py +6 -0
  8. koi_net/build/comp_type.py +7 -0
  9. koi_net/build/consts.py +18 -0
  10. koi_net/build/container.py +46 -0
  11. koi_net/cache.py +81 -0
  12. koi_net/config/core.py +113 -0
  13. koi_net/config/full_node.py +45 -0
  14. koi_net/config/loader.py +60 -0
  15. koi_net/config/partial_node.py +26 -0
  16. koi_net/config/proxy.py +20 -0
  17. koi_net/core.py +78 -0
  18. koi_net/effector.py +147 -0
  19. koi_net/entrypoints/__init__.py +2 -0
  20. koi_net/entrypoints/base.py +8 -0
  21. koi_net/entrypoints/poller.py +43 -0
  22. koi_net/entrypoints/server.py +85 -0
  23. koi_net/exceptions.py +107 -0
  24. koi_net/identity.py +20 -0
  25. koi_net/log_system.py +133 -0
  26. koi_net/network/__init__.py +0 -0
  27. koi_net/network/error_handler.py +63 -0
  28. koi_net/network/event_buffer.py +91 -0
  29. koi_net/network/event_queue.py +31 -0
  30. koi_net/network/graph.py +123 -0
  31. koi_net/network/request_handler.py +244 -0
  32. koi_net/network/resolver.py +152 -0
  33. koi_net/network/response_handler.py +130 -0
  34. koi_net/processor/__init__.py +0 -0
  35. koi_net/processor/context.py +36 -0
  36. koi_net/processor/handler.py +61 -0
  37. koi_net/processor/knowledge_handlers.py +302 -0
  38. koi_net/processor/knowledge_object.py +135 -0
  39. koi_net/processor/kobj_queue.py +51 -0
  40. koi_net/processor/pipeline.py +222 -0
  41. koi_net/protocol/__init__.py +0 -0
  42. koi_net/protocol/api_models.py +67 -0
  43. koi_net/protocol/consts.py +7 -0
  44. koi_net/protocol/edge.py +50 -0
  45. koi_net/protocol/envelope.py +65 -0
  46. koi_net/protocol/errors.py +24 -0
  47. koi_net/protocol/event.py +51 -0
  48. koi_net/protocol/model_map.py +62 -0
  49. koi_net/protocol/node.py +18 -0
  50. koi_net/protocol/secure.py +167 -0
  51. koi_net/secure_manager.py +115 -0
  52. koi_net/workers/__init__.py +2 -0
  53. koi_net/workers/base.py +26 -0
  54. koi_net/workers/event_worker.py +111 -0
  55. koi_net/workers/kobj_worker.py +51 -0
  56. koi_net-1.2.4.dist-info/METADATA +485 -0
  57. koi_net-1.2.4.dist-info/RECORD +59 -0
  58. koi_net-1.2.4.dist-info/WHEEL +4 -0
  59. koi_net-1.2.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,167 @@
1
+ from rid_lib.types import KoiNetNode
2
+ import structlog
3
+ from base64 import b64decode, b64encode
4
+ from cryptography.hazmat.primitives import hashes
5
+ from cryptography.hazmat.primitives.asymmetric import ec
6
+ from cryptography.hazmat.primitives import serialization
7
+ from rid_lib.ext.utils import sha256_hash
8
+ from cryptography.hazmat.primitives.asymmetric.utils import (
9
+ decode_dss_signature,
10
+ encode_dss_signature
11
+ )
12
+
13
+ log = structlog.stdlib.get_logger()
14
+
15
+
16
+ def der_to_raw_signature(der_signature: bytes, curve=ec.SECP256R1()) -> bytes:
17
+ """Converts a DER-encoded signature to raw r||s format."""
18
+
19
+ # Decode the DER signature to get r and s
20
+ r, s = decode_dss_signature(der_signature)
21
+
22
+ # Determine byte length based on curve bit size
23
+ byte_length = (curve.key_size + 7) // 8
24
+
25
+ # Convert r and s to big-endian byte arrays of fixed length
26
+ r_bytes = r.to_bytes(byte_length, byteorder='big')
27
+ s_bytes = s.to_bytes(byte_length, byteorder='big')
28
+
29
+ # Concatenate r and s
30
+ return r_bytes + s_bytes
31
+
32
+
33
+ def raw_to_der_signature(raw_signature: bytes, curve=ec.SECP256R1()) -> bytes:
34
+ """Converts a raw r||s signature to DER format."""
35
+
36
+ # Determine byte length based on curve bit size
37
+ byte_length = (curve.key_size + 7) // 8
38
+
39
+ # Split the raw signature into r and s components
40
+ if len(raw_signature) != 2 * byte_length:
41
+ raise ValueError(f"Raw signature must be {2 * byte_length} bytes for {curve.name}")
42
+
43
+ r_bytes = raw_signature[:byte_length]
44
+ s_bytes = raw_signature[byte_length:]
45
+
46
+ # Convert bytes to integers
47
+ r = int.from_bytes(r_bytes, byteorder='big')
48
+ s = int.from_bytes(s_bytes, byteorder='big')
49
+
50
+ # Encode as DER
51
+ return encode_dss_signature(r, s)
52
+
53
+
54
+ class PrivateKey:
55
+ priv_key: ec.EllipticCurvePrivateKey
56
+
57
+ def __init__(self, priv_key):
58
+ self.priv_key = priv_key
59
+
60
+ @classmethod
61
+ def generate(cls):
62
+ """Generates a new `Private Key`."""
63
+ return cls(priv_key=ec.generate_private_key(ec.SECP256R1()))
64
+
65
+ def public_key(self) -> "PublicKey":
66
+ """Returns instance of `PublicKey` dervied from this private key."""
67
+ return PublicKey(self.priv_key.public_key())
68
+
69
+ @classmethod
70
+ def from_pem(cls, priv_key_pem: str, password: str):
71
+ """Loads `PrivateKey` from encrypted PEM string."""
72
+ return cls(
73
+ priv_key=serialization.load_pem_private_key(
74
+ data=priv_key_pem.encode(),
75
+ password=password.encode()
76
+ )
77
+ )
78
+
79
+ def to_pem(self, password: str) -> str:
80
+ """Saves `PrivateKey` to encrypted PEM string."""
81
+ return self.priv_key.private_bytes(
82
+ encoding=serialization.Encoding.PEM,
83
+ format=serialization.PrivateFormat.PKCS8,
84
+ encryption_algorithm=serialization.BestAvailableEncryption(password.encode())
85
+ ).decode()
86
+
87
+ def sign(self, message: bytes) -> str:
88
+ """Returns base64 encoded raw signature bytes of the form r||s."""
89
+ hashed_message = sha256_hash(message.decode())
90
+
91
+ der_signature_bytes = self.priv_key.sign(
92
+ data=message,
93
+ signature_algorithm=ec.ECDSA(hashes.SHA256())
94
+ )
95
+
96
+ raw_signature_bytes = der_to_raw_signature(der_signature_bytes)
97
+
98
+ signature = b64encode(raw_signature_bytes).decode()
99
+
100
+ log.debug(f"Signing message with [{self.public_key().to_der()}]")
101
+ log.debug(f"hash: {hashed_message}")
102
+ log.debug(f"signature: {signature}")
103
+
104
+ return signature
105
+
106
+
107
+ class PublicKey:
108
+ pub_key: ec.EllipticCurvePublicKey
109
+
110
+ def __init__(self, pub_key):
111
+ self.pub_key = pub_key
112
+
113
+ @classmethod
114
+ def from_pem(cls, pub_key_pem: str):
115
+ """Loads `PublicKey` from PEM string."""
116
+ return cls(
117
+ pub_key=serialization.load_pem_public_key(
118
+ data=pub_key_pem.encode()
119
+ )
120
+ )
121
+
122
+ def to_pem(self) -> str:
123
+ """Saves `PublicKey` to PEM string."""
124
+ return self.pub_key.public_bytes(
125
+ encoding=serialization.Encoding.PEM,
126
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
127
+ ).decode()
128
+
129
+ @classmethod
130
+ def from_der(cls, pub_key_der: str):
131
+ """Loads `PublicKey` from base64 encoded DER string."""
132
+ return cls(
133
+ pub_key=serialization.load_der_public_key(
134
+ data=b64decode(pub_key_der)
135
+ )
136
+ )
137
+
138
+ def to_der(self) -> str:
139
+ """Saves `PublicKey` to base64 encoded DER string."""
140
+ return b64encode(
141
+ self.pub_key.public_bytes(
142
+ encoding=serialization.Encoding.DER,
143
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
144
+ )
145
+ ).decode()
146
+
147
+ def to_node_rid(self, name) -> KoiNetNode:
148
+ """Returns an orn:koi-net.node RID from hashed DER string."""
149
+ return KoiNetNode(
150
+ name=name,
151
+ hash=sha256_hash(self.to_der())
152
+ )
153
+
154
+ def verify(self, signature: str, message: bytes):
155
+ """Verifies a signature for a message.
156
+
157
+ Raises `cryptography.exceptions.InvalidSignature` on failure.
158
+ """
159
+
160
+ raw_signature_bytes = b64decode(signature)
161
+ der_signature_bytes = raw_to_der_signature(raw_signature_bytes)
162
+
163
+ self.pub_key.verify(
164
+ signature=der_signature_bytes,
165
+ data=message,
166
+ signature_algorithm=ec.ECDSA(hashes.SHA256())
167
+ )
@@ -0,0 +1,115 @@
1
+ import structlog
2
+ import cryptography.exceptions
3
+ from rid_lib.ext import Bundle, Cache
4
+ from rid_lib.ext.utils import sha256_hash
5
+ from rid_lib.types import KoiNetNode
6
+ from .identity import NodeIdentity
7
+ from .protocol.envelope import UnsignedEnvelope, SignedEnvelope
8
+ from .protocol.secure import PublicKey
9
+ from .protocol.api_models import ApiModels, EventsPayload
10
+ from .protocol.event import EventType
11
+ from .protocol.node import NodeProfile
12
+ from .protocol.secure import PrivateKey
13
+ from .exceptions import (
14
+ UnknownNodeError,
15
+ InvalidKeyError,
16
+ InvalidSignatureError,
17
+ InvalidTargetError
18
+ )
19
+ from .config.core import NodeConfig
20
+
21
+ log = structlog.stdlib.get_logger()
22
+
23
+
24
+ class SecureManager:
25
+ """Subsystem handling secure protocol logic."""
26
+ identity: NodeIdentity
27
+ cache: Cache
28
+ config: NodeConfig
29
+ priv_key: PrivateKey
30
+
31
+ def __init__(
32
+ self,
33
+ identity: NodeIdentity,
34
+ cache: Cache,
35
+ config: NodeConfig
36
+ ):
37
+ self.identity = identity
38
+ self.cache = cache
39
+ self.config = config
40
+
41
+ def start(self):
42
+ self.load_priv_key()
43
+
44
+ def load_priv_key(self) -> PrivateKey:
45
+ """Loads private key from PEM file path in config."""
46
+
47
+ # TODO: handle missing private key
48
+ with open(self.config.koi_net.private_key_pem_path, "r") as f:
49
+ priv_key_pem = f.read()
50
+
51
+ self.priv_key = PrivateKey.from_pem(
52
+ priv_key_pem=priv_key_pem,
53
+ password=self.config.env.priv_key_password
54
+ )
55
+
56
+ def handle_unknown_node(self, envelope: SignedEnvelope) -> Bundle | None:
57
+ """Attempts to find node profile in proided envelope.
58
+
59
+ If an unknown node sends an envelope, it may still be able to be
60
+ validated if that envelope contains their node profile. This is
61
+ essential for allowing unknown nodes to handshake and introduce
62
+ themselves. Only an `EventsPayload` contain a `NEW` event for a
63
+ node profile for the source node is permissible.
64
+ """
65
+ if type(envelope.payload) != EventsPayload:
66
+ return None
67
+
68
+ for event in envelope.payload.events:
69
+ # must be NEW event for bundle of source node's profile
70
+ if event.rid != envelope.source_node:
71
+ continue
72
+ if event.event_type != EventType.NEW:
73
+ continue
74
+
75
+ return event.bundle
76
+ return None
77
+
78
+ def create_envelope(
79
+ self, payload: ApiModels, target: KoiNetNode
80
+ ) -> SignedEnvelope:
81
+ """Returns signed envelope to target from provided payload."""
82
+ return UnsignedEnvelope(
83
+ payload=payload,
84
+ source_node=self.identity.rid,
85
+ target_node=target
86
+ ).sign_with(self.priv_key)
87
+
88
+ def validate_envelope(self, envelope: SignedEnvelope):
89
+ """Validates signed envelope from another node."""
90
+
91
+ node_bundle = (
92
+ self.cache.read(envelope.source_node) or
93
+ self.handle_unknown_node(envelope)
94
+ )
95
+
96
+ if not node_bundle:
97
+ raise UnknownNodeError(f"Couldn't resolve {envelope.source_node}")
98
+
99
+ node_profile = node_bundle.validate_contents(NodeProfile)
100
+
101
+ # check that public key matches source node RID
102
+ if envelope.source_node.hash != sha256_hash(node_profile.public_key):
103
+ raise InvalidKeyError("Invalid public key on new node!")
104
+
105
+ # check envelope signed by validated public key
106
+ pub_key = PublicKey.from_der(node_profile.public_key)
107
+ try:
108
+ envelope.verify_with(pub_key)
109
+ except cryptography.exceptions.InvalidSignature:
110
+ raise InvalidSignatureError(f"Signature {envelope.signature} is invalid.")
111
+
112
+ # check that this node is the target of the envelope
113
+ if envelope.target_node != self.identity.rid:
114
+ raise InvalidTargetError(f"Envelope target {envelope.target_node!r} is not me")
115
+
@@ -0,0 +1,2 @@
1
+ from .event_worker import EventProcessingWorker
2
+ from .kobj_worker import KnowledgeProcessingWorker
@@ -0,0 +1,26 @@
1
+ import threading
2
+
3
+ from ..build import comp_order
4
+
5
+
6
+ class End:
7
+ """Class for STOP_WORKER sentinel pushed to worker queues."""
8
+ pass
9
+
10
+ STOP_WORKER = End()
11
+
12
+ @comp_order.worker
13
+ class ThreadWorker:
14
+ """Base class for thread workers."""
15
+
16
+ thread: threading.Thread
17
+
18
+ def __init__(self):
19
+ self.thread = threading.Thread(target=self.run)
20
+
21
+ def start(self):
22
+ self.thread.start()
23
+
24
+ def run(self):
25
+ """Processing loop for thread."""
26
+ pass
@@ -0,0 +1,111 @@
1
+ import queue
2
+ import traceback
3
+ import time
4
+ import structlog
5
+
6
+ from rid_lib.ext import Cache
7
+ from rid_lib.types import KoiNetNode
8
+
9
+ from ..config.core import NodeConfig
10
+ from ..network.event_queue import EventQueue
11
+ from ..network.request_handler import RequestHandler
12
+ from ..network.event_buffer import EventBuffer
13
+ from ..protocol.node import NodeProfile, NodeType
14
+ from ..exceptions import RequestError
15
+ from .base import ThreadWorker, STOP_WORKER
16
+
17
+ log = structlog.stdlib.get_logger()
18
+
19
+
20
+ class EventProcessingWorker(ThreadWorker):
21
+ """Thread worker that processes the `event_queue`."""
22
+
23
+ def __init__(
24
+ self,
25
+ config: NodeConfig,
26
+ cache: Cache,
27
+ event_queue: EventQueue,
28
+ request_handler: RequestHandler,
29
+ poll_event_buf: EventBuffer,
30
+ broadcast_event_buf: EventBuffer
31
+ ):
32
+ self.event_queue = event_queue
33
+ self.request_handler = request_handler
34
+
35
+ self.config = config
36
+ self.cache = cache
37
+ self.poll_event_buf = poll_event_buf
38
+ self.broadcast_event_buf = broadcast_event_buf
39
+
40
+ super().__init__()
41
+
42
+ def flush_and_broadcast(self, target: KoiNetNode, force_flush: bool = False):
43
+ """Broadcasts all events to target in event buffer."""
44
+
45
+ # TODO: deal with automated retries when unreachable node's buffer is full
46
+ try:
47
+ with self.broadcast_event_buf.safe_flush(target, force_flush) as events:
48
+ self.request_handler.broadcast_events(target, events=events)
49
+ except RequestError:
50
+ log.warning("Failed to reach target, event buffer reset")
51
+ pass
52
+
53
+ def stop(self):
54
+ self.event_queue.q.put(STOP_WORKER)
55
+
56
+ def run(self):
57
+ while True:
58
+ try:
59
+ item = self.event_queue.q.get(
60
+ timeout=self.config.koi_net.event_worker.queue_timeout)
61
+
62
+ try:
63
+ if item is STOP_WORKER:
64
+ log.info(f"Received 'STOP_WORKER' signal, flushing all buffers...")
65
+ for target in list(self.broadcast_event_buf.buffers.keys()):
66
+ self.flush_and_broadcast(target, force_flush=True)
67
+ return
68
+
69
+ log.info(f"Dequeued {item.event!r} -> {item.target!r}")
70
+
71
+ # determines which buffer to push event to based on target node type
72
+ node_bundle = self.cache.read(item.target)
73
+ if node_bundle:
74
+ node_profile = node_bundle.validate_contents(NodeProfile)
75
+
76
+ if node_profile.node_type == NodeType.FULL:
77
+ self.broadcast_event_buf.push(item.target, item.event)
78
+
79
+ elif node_profile.node_type == NodeType.PARTIAL:
80
+ self.poll_event_buf.push(item.target, item.event)
81
+ continue
82
+
83
+ elif item.target == self.config.koi_net.first_contact.rid:
84
+ self.broadcast_event_buf.push(item.target, item.event)
85
+
86
+ else:
87
+ log.warning(f"Couldn't handle event {item.event!r} in queue, node {item.target!r} unknown to me")
88
+ continue
89
+
90
+ buf_len = self.broadcast_event_buf.buf_len(item.target)
91
+ if buf_len > self.config.koi_net.event_worker.max_buf_len:
92
+ self.flush_and_broadcast(target)
93
+
94
+ finally:
95
+ self.event_queue.q.task_done()
96
+
97
+ except queue.Empty:
98
+ # On timeout, check all buffers for max wait time
99
+ for target in list(self.broadcast_event_buf.buffers):
100
+ start_time = self.broadcast_event_buf.start_time.get(target)
101
+
102
+ if (start_time is None) or (self.broadcast_event_buf.buf_len(target) == 0):
103
+ continue
104
+
105
+ now = time.time()
106
+ if (now - start_time) >= self.config.koi_net.event_worker.max_wait_time:
107
+ self.flush_and_broadcast(target)
108
+
109
+ except Exception:
110
+ traceback.print_exc()
111
+ continue
@@ -0,0 +1,51 @@
1
+ import queue
2
+ import traceback
3
+ import structlog
4
+
5
+ from ..config.core import NodeConfig
6
+ from ..processor.pipeline import KnowledgePipeline
7
+ from ..processor.kobj_queue import KobjQueue
8
+ from .base import ThreadWorker, STOP_WORKER
9
+
10
+ log = structlog.stdlib.get_logger()
11
+
12
+
13
+ class KnowledgeProcessingWorker(ThreadWorker):
14
+ """Thread worker that processes the `kobj_queue`."""
15
+
16
+ def __init__(
17
+ self,
18
+ config: NodeConfig,
19
+ kobj_queue: KobjQueue,
20
+ pipeline: KnowledgePipeline
21
+ ):
22
+ self.config = config
23
+ self.kobj_queue = kobj_queue
24
+ self.pipeline = pipeline
25
+
26
+ super().__init__()
27
+
28
+ def stop(self):
29
+ self.kobj_queue.q.put(STOP_WORKER)
30
+
31
+ def run(self):
32
+ while True:
33
+ try:
34
+ item = self.kobj_queue.q.get(timeout=self.config.koi_net.kobj_worker.queue_timeout)
35
+ try:
36
+ if item is STOP_WORKER:
37
+ log.info("Received 'STOP_WORKER' signal, shutting down...")
38
+ return
39
+
40
+ log.info(f"Dequeued {item!r}")
41
+
42
+ self.pipeline.process(item)
43
+ finally:
44
+ self.kobj_queue.q.task_done()
45
+
46
+ except queue.Empty:
47
+ pass
48
+
49
+ except Exception:
50
+ traceback.print_exc()
51
+ continue