fleetmesh-node 0.2.0__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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- sidecar/fleetmesh_ssb.py +603 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Opening an inbound DIDComm envelope: one place, every transport.
|
|
2
|
+
|
|
3
|
+
Four paths carry a JWE into this node - a gift wrap off a relay, an iroh
|
|
4
|
+
stream, an iroh QUIC stream, and a FIPS native flow - and until this module
|
|
5
|
+
each of them opened the envelope its own way. Three of the four were wrong in
|
|
6
|
+
the same direction: they took the plaintext's `from` as the sender's name
|
|
7
|
+
without anything that proved it. A frame that was never encrypted, written by
|
|
8
|
+
anyone who could reach the socket, arrived as a message from whoever it named,
|
|
9
|
+
and on two of the paths it arrived marked authenticated. The transport that
|
|
10
|
+
got it right was the relay one, which is the oldest, and its rule is the rule
|
|
11
|
+
here: the names must agree, and a name nobody proved is not a name.
|
|
12
|
+
|
|
13
|
+
What proves a sender is the ECDH-1PU agreement. didcomm.decrypt only succeeds
|
|
14
|
+
under authcrypt if the static key `skid` names took part in the key
|
|
15
|
+
agreement, so a decrypted authcrypt message was written by the holder of that
|
|
16
|
+
key. The plaintext's `from` is then required to name the same node, and on
|
|
17
|
+
the relay path the seal's signer too. Anoncrypt has no skid and therefore no
|
|
18
|
+
sender: the message is delivered with `authenticated=False` and its `from` is
|
|
19
|
+
recorded as a claim, which is what first contact is.
|
|
20
|
+
|
|
21
|
+
Anything that is not a JWE is refused. There is no plaintext path, not for
|
|
22
|
+
discovery and not for ping: a transport that wants an unauthenticated
|
|
23
|
+
protocol runs it beside DIDComm, not inside the queue that request() waits
|
|
24
|
+
on. The reason strings are the ones node/transport/ has always recorded, so an
|
|
25
|
+
operator who learned to read `dropped` reads the same words from every rung.
|
|
26
|
+
|
|
27
|
+
Replay. A JWE is a fixed string of bytes, and a direct rung (fips flow, iroh
|
|
28
|
+
stream, libp2p stream) carries it as-is: anyone who captured one can write
|
|
29
|
+
it to the socket again, and the agreement proves exactly what it proved the
|
|
30
|
+
first time - the holder of the key wrote this message, once. The relay rung
|
|
31
|
+
has always deduplicated by wrap id; the direct rungs had nothing. So each
|
|
32
|
+
direct rung holds an InboundGuard and hands it to open_inbound, which
|
|
33
|
+
refuses a message id it has already opened and a `created_time` outside the
|
|
34
|
+
skew window the schema tolerates. A message with no `created_time` is still
|
|
35
|
+
accepted (the id check alone covers it); the gift-wrap rung passes no guard
|
|
36
|
+
because a stored wrap is legitimately delivered days late and the wrap id
|
|
37
|
+
is already its replay key.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import collections
|
|
42
|
+
import json
|
|
43
|
+
import threading
|
|
44
|
+
import time
|
|
45
|
+
from typing import Any, Optional
|
|
46
|
+
|
|
47
|
+
from fleetmesh_core.identity import didcomm
|
|
48
|
+
from fleetmesh_core.wire import schema_files
|
|
49
|
+
from fleetmesh_core.wire.wire_types import DIDCommMessage
|
|
50
|
+
from fleetmesh_core.identity.did import node_of_did
|
|
51
|
+
|
|
52
|
+
# A DIDComm message must fit in one wrap fragment set; a transport that has
|
|
53
|
+
# no smaller framing limit of its own still refuses anything past this
|
|
54
|
+
# before it is parsed, so a peer cannot make json.loads build a giant object.
|
|
55
|
+
MAX_ENVELOPE_BYTES: int = 16 * 1024 * 1024
|
|
56
|
+
|
|
57
|
+
# How far a direct-rung message's `created_time` may sit from this node's
|
|
58
|
+
# clock, either way: the schema's one tolerance for judging a peer by time,
|
|
59
|
+
# read through ref like every other constant. (It used to be a path
|
|
60
|
+
# computed from __file__ with a silent 300s fallback; moving this module
|
|
61
|
+
# one directory down turned the window from 60s to 300s and nothing but
|
|
62
|
+
# the test noticed. A constant the schema owns is not read by path.)
|
|
63
|
+
FRESH_WINDOW_SECONDS: int = int(schema_files.load_constants()['grant']['max_clock_skew_seconds'])
|
|
64
|
+
# How many message ids a guard remembers. Past this the oldest is forgotten,
|
|
65
|
+
# which is safe because anything that old is also outside the fresh window.
|
|
66
|
+
SEEN_IDS_MAX: int = 4096
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class InboundGuard:
|
|
70
|
+
"""Per-rung replay and freshness state: a bounded id-LRU and a clock window.
|
|
71
|
+
|
|
72
|
+
One per transport instance (never module-level: two nodes in one
|
|
73
|
+
process must not share what they have seen). `refused` counts by reason
|
|
74
|
+
so an operator can tell a replaying peer from a drifting clock.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, window: int = FRESH_WINDOW_SECONDS, capacity: int = SEEN_IDS_MAX) -> None:
|
|
78
|
+
self.window: int = int(window)
|
|
79
|
+
self.capacity: int = int(capacity)
|
|
80
|
+
self._seen: collections.OrderedDict[str, None] = collections.OrderedDict()
|
|
81
|
+
self._lock = threading.Lock()
|
|
82
|
+
self.refused: collections.Counter[str] = collections.Counter()
|
|
83
|
+
|
|
84
|
+
def stale(self, created_time: Any, now: int) -> Optional[str]:
|
|
85
|
+
"""None if `created_time` is absent or inside the window, else why not."""
|
|
86
|
+
if created_time is None:
|
|
87
|
+
return None
|
|
88
|
+
if isinstance(created_time, bool) or not isinstance(created_time, (int, float)):
|
|
89
|
+
self.refused['created_time'] += 1
|
|
90
|
+
return 'created_time is not a number'
|
|
91
|
+
delta = int(created_time) - int(now)
|
|
92
|
+
if abs(delta) > self.window:
|
|
93
|
+
self.refused['stale'] += 1
|
|
94
|
+
return 'stale: created_time is %ds %s this node\'s clock (window %ds)' % (
|
|
95
|
+
abs(delta), 'ahead of' if delta > 0 else 'behind', self.window)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def first_time(self, message_id: str) -> bool:
|
|
99
|
+
"""True and remembered if this id is new; False if it has been opened before."""
|
|
100
|
+
with self._lock:
|
|
101
|
+
if message_id in self._seen:
|
|
102
|
+
self._seen.move_to_end(message_id)
|
|
103
|
+
self.refused['replay'] += 1
|
|
104
|
+
return False
|
|
105
|
+
self._seen[message_id] = None
|
|
106
|
+
while len(self._seen) > self.capacity:
|
|
107
|
+
self._seen.popitem(last=False)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Inbound:
|
|
112
|
+
"""One delivered message: the plaintext, who sent it, and how it was sealed."""
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
message: DIDCommMessage,
|
|
117
|
+
sender_pk: str,
|
|
118
|
+
authenticated: bool,
|
|
119
|
+
wrap_id: str,
|
|
120
|
+
) -> None:
|
|
121
|
+
self.message: DIDCommMessage = message
|
|
122
|
+
self.sender: str = sender_pk
|
|
123
|
+
self.authenticated: bool = authenticated # False for anoncrypt (first contact)
|
|
124
|
+
self.wrap_id: str = wrap_id
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def thid(self) -> Optional[str]:
|
|
128
|
+
return self.message.get('thid') or self.message.get('id')
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def foreign(payload: Any) -> str:
|
|
132
|
+
"""Why a decrypted payload is not one of ours, in a few words.
|
|
133
|
+
|
|
134
|
+
A protocol that shares kind 21059 says whose message it is the same way
|
|
135
|
+
DIDComm does, in a `type`, so that is what is named when there is one and
|
|
136
|
+
it is the counted half of the reason. Nothing else about the payload is
|
|
137
|
+
reported: it decrypted under this node's key, so it may be somebody's
|
|
138
|
+
private traffic that merely arrived at the wrong daemon.
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(payload, dict):
|
|
141
|
+
kind = payload.get('type')
|
|
142
|
+
if isinstance(kind, str) and kind.strip():
|
|
143
|
+
return 'another protocol: %s' % kind.strip()[:80]
|
|
144
|
+
members = ', '.join(sorted(str(k)[:16] for k in list(payload)[:6]))
|
|
145
|
+
return 'payload is not a JWE: an object carrying %s' % (members or 'nothing')
|
|
146
|
+
return 'payload is not a JWE: %s' % type(payload).__name__
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_jwe(payload: Any) -> bool:
|
|
150
|
+
"""The shape didcomm.decrypt will look at. Anything else is not tried."""
|
|
151
|
+
return isinstance(payload, dict) and 'protected' in payload and 'recipients' in payload
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _pubkey_or_none(name: Any) -> Optional[str]:
|
|
155
|
+
"""did:nostr:<pk>[#frag] -> pk, or None. Never raises: a malformed name
|
|
156
|
+
from the wire is a reason to drop, not a way to end a reader thread."""
|
|
157
|
+
if not isinstance(name, str):
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
return node_of_did(name)
|
|
161
|
+
except (ValueError, AttributeError):
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def open_inbound(
|
|
166
|
+
payload: Any,
|
|
167
|
+
keystore: Any,
|
|
168
|
+
peers: Any,
|
|
169
|
+
wrap_id: str,
|
|
170
|
+
seal_pk: Optional[str] = None,
|
|
171
|
+
guard: Optional[InboundGuard] = None,
|
|
172
|
+
now: Optional[int] = None,
|
|
173
|
+
) -> tuple[Optional[Inbound], Optional[str]]:
|
|
174
|
+
"""Open one inbound envelope. Returns (Inbound, None) or (None, reason).
|
|
175
|
+
|
|
176
|
+
payload: the bytes off the wire, or the object a transport already parsed
|
|
177
|
+
(the relay path parses before it can tell a fragment from a whole).
|
|
178
|
+
seal_pk: on the relay path, the key that signed the seal, which every
|
|
179
|
+
other name must then equal. Transports without a seal pass None and the
|
|
180
|
+
agreement between `skid` and `from` is what is checked.
|
|
181
|
+
guard: a direct rung's InboundGuard. When given, a message id already
|
|
182
|
+
opened through it is refused as a replay, and a `created_time` outside
|
|
183
|
+
the guard's window of `now` (the rung's clock; time.time() if None) is
|
|
184
|
+
refused as stale. The relay rung passes none: its wrap id is the key.
|
|
185
|
+
|
|
186
|
+
Exactly one of the pair is None, so a caller writes
|
|
187
|
+
inbound, why = open_inbound(...)
|
|
188
|
+
if inbound is None: self._drop(why, wrap_id); return
|
|
189
|
+
and the reason it records is the same on every rung.
|
|
190
|
+
"""
|
|
191
|
+
if isinstance(payload, (bytes, bytearray, memoryview)):
|
|
192
|
+
raw = bytes(payload)
|
|
193
|
+
if len(raw) > MAX_ENVELOPE_BYTES:
|
|
194
|
+
return None, 'envelope: %d bytes is larger than this node opens' % len(raw)
|
|
195
|
+
try:
|
|
196
|
+
payload = json.loads(raw.decode('utf-8'))
|
|
197
|
+
except (ValueError, UnicodeDecodeError):
|
|
198
|
+
return None, 'payload is not a JWE: not JSON'
|
|
199
|
+
if not is_jwe(payload):
|
|
200
|
+
# Named apart from malformed traffic when it can be, because on a
|
|
201
|
+
# shared kind this is somebody else's healthy protocol. On a direct
|
|
202
|
+
# transport it is somebody sending plaintext, which is refused for
|
|
203
|
+
# the reason in the module docstring: nothing about it is proven.
|
|
204
|
+
return None, foreign(payload)
|
|
205
|
+
try:
|
|
206
|
+
message, skid = didcomm.decrypt(payload, keystore.kid('agreement'), keystore.agreement_priv,
|
|
207
|
+
resolve_sender=peers.resolve_sender)
|
|
208
|
+
except didcomm.EnvelopeError as e:
|
|
209
|
+
return None, 'envelope: %s' % e
|
|
210
|
+
except Exception as e: # a resolver that raised, a header of the wrong type
|
|
211
|
+
return None, 'envelope: %s: %s' % (type(e).__name__, e)
|
|
212
|
+
if (not isinstance(message, dict) or not isinstance(message.get('type'), str)
|
|
213
|
+
or not isinstance(message.get('id'), str)):
|
|
214
|
+
return None, 'plaintext is not a DIDComm message'
|
|
215
|
+
claimed = _pubkey_or_none(message.get('from', ''))
|
|
216
|
+
if claimed is None:
|
|
217
|
+
return None, 'from is not a did:nostr'
|
|
218
|
+
signer: Optional[str] = None
|
|
219
|
+
if skid is not None:
|
|
220
|
+
signer = _pubkey_or_none(skid)
|
|
221
|
+
if signer is None:
|
|
222
|
+
return None, 'skid is not a did:nostr'
|
|
223
|
+
# The names must agree. With a seal there are three; without one, the
|
|
224
|
+
# skid the agreement proved is the anchor and `from` must match it.
|
|
225
|
+
if seal_pk is not None:
|
|
226
|
+
if claimed != seal_pk:
|
|
227
|
+
return None, 'from %s does not match the seal signer %s' % (claimed[:8], seal_pk[:8])
|
|
228
|
+
if signer is not None and signer != seal_pk:
|
|
229
|
+
return None, 'skid does not match the seal signer'
|
|
230
|
+
sender = seal_pk
|
|
231
|
+
elif signer is not None:
|
|
232
|
+
if claimed != signer:
|
|
233
|
+
return None, 'from %s does not match the skid %s' % (claimed[:8], signer[:8])
|
|
234
|
+
sender = signer
|
|
235
|
+
else:
|
|
236
|
+
# Anoncrypt on a transport with no seal: nobody proved anything, so
|
|
237
|
+
# what is recorded as the sender is the claim, and it says so.
|
|
238
|
+
sender = claimed
|
|
239
|
+
raw_thid: Any = message.get('thid')
|
|
240
|
+
if raw_thid is not None and not isinstance(raw_thid, str):
|
|
241
|
+
return None, 'thid is not a string'
|
|
242
|
+
if guard is not None:
|
|
243
|
+
# Freshness first, then the id: a stale message is refused without
|
|
244
|
+
# being remembered, so a flood of old captures cannot push live ids
|
|
245
|
+
# out of the LRU.
|
|
246
|
+
why = guard.stale(message.get('created_time'), int(time.time()) if now is None else now)
|
|
247
|
+
if why is not None:
|
|
248
|
+
return None, why
|
|
249
|
+
if not guard.first_time(message['id']):
|
|
250
|
+
return None, 'replay: message %s was already opened' % message['id'][:32]
|
|
251
|
+
return Inbound(message, sender, authenticated=signer is not None, wrap_id=wrap_id), None
|
node/transport/dnstxt.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""A minimal DNS TXT lookup, because the standard library has none.
|
|
2
|
+
|
|
3
|
+
The nsite gateway binds a custom domain to a site through a TXT record at
|
|
4
|
+
`_nsite.<domain>`. `socket` can resolve A/AAAA, not TXT, and pulling in a
|
|
5
|
+
DNS library for one record type is not worth a dependency: this is a single
|
|
6
|
+
UDP query (RFC 1035, with the RFC 1123 TCP fallback when the answer is
|
|
7
|
+
truncated), to the resolvers `/etc/resolv.conf` names, following CNAMEs in
|
|
8
|
+
the answer section.
|
|
9
|
+
|
|
10
|
+
Returned strings are the TXT character-strings of one record joined, one
|
|
11
|
+
entry per record, decoded as UTF-8 with errors replaced. The caller decides
|
|
12
|
+
what a value means.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import random
|
|
18
|
+
import socket
|
|
19
|
+
import struct
|
|
20
|
+
from typing import Callable, Optional, Sequence
|
|
21
|
+
|
|
22
|
+
TYPE_TXT = 16
|
|
23
|
+
TYPE_CNAME = 5
|
|
24
|
+
CLASS_IN = 1
|
|
25
|
+
MAX_LABEL = 63
|
|
26
|
+
MAX_NAME = 253
|
|
27
|
+
|
|
28
|
+
Resolver = Callable[[str], list[str]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DnsError(Exception):
|
|
32
|
+
"""The lookup could not be completed (network, malformed answer)."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def system_nameservers(path: str = '/etc/resolv.conf') -> list[str]:
|
|
36
|
+
out: list[str] = []
|
|
37
|
+
try:
|
|
38
|
+
with open(path, encoding='utf-8', errors='replace') as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
parts = line.split()
|
|
41
|
+
if len(parts) >= 2 and parts[0] == 'nameserver':
|
|
42
|
+
out.append(parts[1])
|
|
43
|
+
except OSError:
|
|
44
|
+
pass
|
|
45
|
+
return out or ['127.0.0.53', '1.1.1.1']
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _encode_name(name: str) -> bytes:
|
|
49
|
+
name = name.rstrip('.')
|
|
50
|
+
if len(name) > MAX_NAME:
|
|
51
|
+
raise DnsError('name too long')
|
|
52
|
+
out = b''
|
|
53
|
+
for label in name.split('.'):
|
|
54
|
+
raw = label.encode('idna') if label else b''
|
|
55
|
+
if not raw or len(raw) > MAX_LABEL:
|
|
56
|
+
raise DnsError('bad label in %r' % name)
|
|
57
|
+
out += bytes([len(raw)]) + raw
|
|
58
|
+
return out + b'\x00'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_name(msg: bytes, off: int, depth: int = 0) -> tuple[str, int]:
|
|
62
|
+
labels: list[str] = []
|
|
63
|
+
jumped_to: Optional[int] = None
|
|
64
|
+
while True:
|
|
65
|
+
if off >= len(msg):
|
|
66
|
+
raise DnsError('truncated name')
|
|
67
|
+
ln = msg[off]
|
|
68
|
+
if ln & 0xC0 == 0xC0:
|
|
69
|
+
if depth > 8:
|
|
70
|
+
raise DnsError('pointer loop')
|
|
71
|
+
ptr = struct.unpack('!H', msg[off:off + 2])[0] & 0x3FFF
|
|
72
|
+
name, _ = _read_name(msg, ptr, depth + 1)
|
|
73
|
+
labels.append(name)
|
|
74
|
+
return '.'.join(x for x in labels if x), (jumped_to if jumped_to is not None else off + 2)
|
|
75
|
+
off += 1
|
|
76
|
+
if ln == 0:
|
|
77
|
+
return '.'.join(labels), off
|
|
78
|
+
labels.append(msg[off:off + ln].decode('ascii', errors='replace'))
|
|
79
|
+
off += ln
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _query(name: str, server: str, timeout: float, tcp: bool = False) -> bytes:
|
|
83
|
+
qid = random.randrange(0, 1 << 16)
|
|
84
|
+
header = struct.pack('!HHHHHH', qid, 0x0100, 1, 0, 0, 0) # RD set
|
|
85
|
+
q = header + _encode_name(name) + struct.pack('!HH', TYPE_TXT, CLASS_IN)
|
|
86
|
+
fam = socket.AF_INET6 if ':' in server else socket.AF_INET
|
|
87
|
+
if tcp:
|
|
88
|
+
with socket.socket(fam, socket.SOCK_STREAM) as s:
|
|
89
|
+
s.settimeout(timeout)
|
|
90
|
+
s.connect((server, 53))
|
|
91
|
+
s.sendall(struct.pack('!H', len(q)) + q)
|
|
92
|
+
ln = s.recv(2)
|
|
93
|
+
if len(ln) < 2:
|
|
94
|
+
raise DnsError('short tcp answer')
|
|
95
|
+
want = struct.unpack('!H', ln)[0]
|
|
96
|
+
buf = b''
|
|
97
|
+
while len(buf) < want:
|
|
98
|
+
chunk = s.recv(want - len(buf))
|
|
99
|
+
if not chunk:
|
|
100
|
+
break
|
|
101
|
+
buf += chunk
|
|
102
|
+
resp = buf
|
|
103
|
+
else:
|
|
104
|
+
with socket.socket(fam, socket.SOCK_DGRAM) as s:
|
|
105
|
+
s.settimeout(timeout)
|
|
106
|
+
s.sendto(q, (server, 53))
|
|
107
|
+
resp, _ = s.recvfrom(4096)
|
|
108
|
+
if len(resp) < 12 or struct.unpack('!H', resp[:2])[0] != qid:
|
|
109
|
+
raise DnsError('answer id mismatch')
|
|
110
|
+
return resp
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _parse_txt(resp: bytes, name: str) -> list[str]:
|
|
114
|
+
_, flags, qd, an, _, _ = struct.unpack('!HHHHHH', resp[:12])
|
|
115
|
+
rcode = flags & 0xF
|
|
116
|
+
if rcode == 3:
|
|
117
|
+
return [] # NXDOMAIN: no such name, not an error
|
|
118
|
+
if rcode != 0:
|
|
119
|
+
raise DnsError('rcode %d' % rcode)
|
|
120
|
+
if flags & 0x0200:
|
|
121
|
+
raise _Truncated()
|
|
122
|
+
off = 12
|
|
123
|
+
for _ in range(qd):
|
|
124
|
+
_, off = _read_name(resp, off)
|
|
125
|
+
off += 4
|
|
126
|
+
wanted = {name.rstrip('.').lower()}
|
|
127
|
+
out: list[str] = []
|
|
128
|
+
for _ in range(an):
|
|
129
|
+
rname, off = _read_name(resp, off)
|
|
130
|
+
rtype, _, _, rdlen = struct.unpack('!HHIH', resp[off:off + 10])
|
|
131
|
+
off += 10
|
|
132
|
+
rdata = resp[off:off + rdlen]
|
|
133
|
+
off += rdlen
|
|
134
|
+
if rname.lower() not in wanted:
|
|
135
|
+
continue
|
|
136
|
+
if rtype == TYPE_CNAME:
|
|
137
|
+
target, _ = _read_name(resp, off - rdlen)
|
|
138
|
+
wanted.add(target.lower())
|
|
139
|
+
elif rtype == TYPE_TXT:
|
|
140
|
+
i, parts = 0, []
|
|
141
|
+
while i < len(rdata):
|
|
142
|
+
ln = rdata[i]
|
|
143
|
+
parts.append(rdata[i + 1:i + 1 + ln])
|
|
144
|
+
i += 1 + ln
|
|
145
|
+
out.append(b''.join(parts).decode('utf-8', errors='replace'))
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _Truncated(Exception):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def lookup_txt(name: str, servers: Optional[Sequence[str]] = None, timeout: float = 3.0) -> list[str]:
|
|
154
|
+
"""TXT records at `name`. Empty list = the name has none (incl. NXDOMAIN).
|
|
155
|
+
Raises DnsError when no resolver answered usably."""
|
|
156
|
+
servers = list(servers) if servers else system_nameservers()
|
|
157
|
+
if os.environ.get('FLEETMESH_DNS_SERVERS'):
|
|
158
|
+
servers = os.environ['FLEETMESH_DNS_SERVERS'].split(',')
|
|
159
|
+
last: Optional[Exception] = None
|
|
160
|
+
for server in servers:
|
|
161
|
+
try:
|
|
162
|
+
try:
|
|
163
|
+
return _parse_txt(_query(name, server, timeout), name)
|
|
164
|
+
except _Truncated:
|
|
165
|
+
return _parse_txt(_query(name, server, timeout, tcp=True), name)
|
|
166
|
+
except (OSError, DnsError, struct.error) as e:
|
|
167
|
+
last = e
|
|
168
|
+
continue
|
|
169
|
+
raise DnsError('no resolver answered for %s: %s' % (name, last))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""FIPS Native Datagram API transport rung package.
|
|
2
|
+
|
|
3
|
+
Exports the client, framing codecs, reassembly engine, and listener mixin.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .types import FipsArrivalInfo
|
|
8
|
+
from .core import (
|
|
9
|
+
DEFAULT_MAX_PAYLOAD,
|
|
10
|
+
DEFAULT_SOCKET_PATHS,
|
|
11
|
+
FLAG_ACK_REQUESTED,
|
|
12
|
+
FLAG_COMPRESSED,
|
|
13
|
+
FLAG_DIDCOMM,
|
|
14
|
+
FLAG_FINAL,
|
|
15
|
+
FRAME_MAGIC,
|
|
16
|
+
FRAMING_MAGIC,
|
|
17
|
+
FRAMING_VERSION,
|
|
18
|
+
HEADER_FORMAT,
|
|
19
|
+
HEADER_SIZE,
|
|
20
|
+
MAX_FRAGMENTS,
|
|
21
|
+
PORT_ACCOUNTING,
|
|
22
|
+
PORT_COMPUTE_CANCEL,
|
|
23
|
+
PORT_COMPUTE_MARKET,
|
|
24
|
+
PORT_COMPUTE_STREAM,
|
|
25
|
+
PORT_PEERING,
|
|
26
|
+
PORT_TELEMETRY,
|
|
27
|
+
REASSEMBLY_TIMEOUT_S,
|
|
28
|
+
FipsNativeTransport,
|
|
29
|
+
FipsTransportError,
|
|
30
|
+
Reassembler,
|
|
31
|
+
decode_frame,
|
|
32
|
+
decode_frame_ex,
|
|
33
|
+
encode_frame,
|
|
34
|
+
find_api_socket,
|
|
35
|
+
)
|
|
36
|
+
from .listener import FipsRungMixin
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
'FipsArrivalInfo',
|
|
40
|
+
'FipsNativeTransport',
|
|
41
|
+
'FipsRungMixin',
|
|
42
|
+
'FipsTransportError',
|
|
43
|
+
'Reassembler',
|
|
44
|
+
'encode_frame',
|
|
45
|
+
'decode_frame',
|
|
46
|
+
'decode_frame_ex',
|
|
47
|
+
'find_api_socket',
|
|
48
|
+
'FLAG_FINAL',
|
|
49
|
+
'FLAG_COMPRESSED',
|
|
50
|
+
'FLAG_DIDCOMM',
|
|
51
|
+
'FLAG_ACK_REQUESTED',
|
|
52
|
+
'FRAMING_MAGIC',
|
|
53
|
+
'FRAMING_VERSION',
|
|
54
|
+
'FRAME_MAGIC',
|
|
55
|
+
'HEADER_FORMAT',
|
|
56
|
+
'HEADER_SIZE',
|
|
57
|
+
'PORT_PEERING',
|
|
58
|
+
'PORT_TELEMETRY',
|
|
59
|
+
'PORT_ACCOUNTING',
|
|
60
|
+
'PORT_COMPUTE_MARKET',
|
|
61
|
+
'PORT_COMPUTE_STREAM',
|
|
62
|
+
'PORT_COMPUTE_CANCEL',
|
|
63
|
+
'DEFAULT_SOCKET_PATHS',
|
|
64
|
+
'DEFAULT_MAX_PAYLOAD',
|
|
65
|
+
'MAX_FRAGMENTS',
|
|
66
|
+
'REASSEMBLY_TIMEOUT_S',
|
|
67
|
+
]
|