weed-cli 0.1.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.
- dht.py +137 -0
- discovery_relay.py +94 -0
- lightning_settle.py +90 -0
- node.py +784 -0
- poc_reputation.py +311 -0
- shell.py +420 -0
- tunnel_relay.py +204 -0
- web_ui.py +444 -0
- weed.py +238 -0
- weed_cli-0.1.0.dist-info/METADATA +870 -0
- weed_cli-0.1.0.dist-info/RECORD +14 -0
- weed_cli-0.1.0.dist-info/WHEEL +5 -0
- weed_cli-0.1.0.dist-info/entry_points.txt +2 -0
- weed_cli-0.1.0.dist-info/top_level.txt +9 -0
poc_reputation.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Persistent local reputation + signed portable attestations — the two
|
|
4
|
+
mechanisms from the last two Slack messages, actually built.
|
|
5
|
+
|
|
6
|
+
1. Local reputation list: a client's own record of "how has peer X
|
|
7
|
+
behaved with me," built from real challenge-response outcomes
|
|
8
|
+
(poc_network_challenge.py) and persisted across runs.
|
|
9
|
+
|
|
10
|
+
2. Signed attestations: a client can sign its own verification outcome
|
|
11
|
+
for a peer with its own Ed25519 key, and hand that signed blob to
|
|
12
|
+
another client — who didn't do the verification themselves, but can
|
|
13
|
+
check the signature and decide how much to trust it, exactly PGP's
|
|
14
|
+
Web of Trust model, applied to possession-verification results
|
|
15
|
+
instead of identity.
|
|
16
|
+
|
|
17
|
+
3. Revocation: a signer can kill their own earlier vouch — for a peer that
|
|
18
|
+
went bad after being vouched for, or a vouch that was simply wrong.
|
|
19
|
+
Only the original signer's key can revoke it (verified by real signature,
|
|
20
|
+
same as the attestation itself); the revoked attestation stays on record
|
|
21
|
+
rather than being deleted, so "X vouched for Y, then revoked it" remains
|
|
22
|
+
an honest, auditable fact instead of quietly disappearing.
|
|
23
|
+
|
|
24
|
+
Demo below runs all real (Ed25519 sign/verify via `cryptography`, not
|
|
25
|
+
faked) and shows: (a) a fresh client with zero direct history can bootstrap
|
|
26
|
+
trust in an unknown peer purely from another client's signed vouching, (b)
|
|
27
|
+
tampering with a signed attestation after the fact is caught, (c) an
|
|
28
|
+
attestation's trust weight decays with age instead of being trusted
|
|
29
|
+
forever, (d) a signer can revoke their own vouch and it actually moves the
|
|
30
|
+
score, (e) nobody else can forge a revocation of someone else's vouch.
|
|
31
|
+
"""
|
|
32
|
+
import hashlib
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import time
|
|
36
|
+
|
|
37
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
38
|
+
from cryptography.hazmat.primitives import serialization
|
|
39
|
+
from cryptography.exceptions import InvalidSignature
|
|
40
|
+
|
|
41
|
+
ATTESTATION_HALF_LIFE_DAYS = 30.0 # trust weight halves every 30 days without a fresh re-verification
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Identity:
|
|
45
|
+
"""An Ed25519 keypair for one client. Real signing keys, generated fresh
|
|
46
|
+
per run here — in a real client this would persist to disk once."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, name):
|
|
49
|
+
self.name = name
|
|
50
|
+
self._priv = Ed25519PrivateKey.generate()
|
|
51
|
+
self.pub = self._priv.public_key()
|
|
52
|
+
|
|
53
|
+
def pubkey_hex(self):
|
|
54
|
+
return self.pub.public_bytes(
|
|
55
|
+
serialization.Encoding.Raw, serialization.PublicFormat.Raw).hex()
|
|
56
|
+
|
|
57
|
+
def sign_attestation(self, peer_pubkey_hex, passes, fails, avg_latency_ms, k, ts=None):
|
|
58
|
+
payload = {
|
|
59
|
+
'signer_pubkey': self.pubkey_hex(),
|
|
60
|
+
'peer_pubkey': peer_pubkey_hex,
|
|
61
|
+
'passes': passes,
|
|
62
|
+
'fails': fails,
|
|
63
|
+
'avg_latency_ms': round(avg_latency_ms, 3),
|
|
64
|
+
'k': k,
|
|
65
|
+
'ts': ts if ts is not None else time.time(),
|
|
66
|
+
}
|
|
67
|
+
payload_bytes = json.dumps(payload, sort_keys=True).encode()
|
|
68
|
+
sig = self._priv.sign(payload_bytes)
|
|
69
|
+
return {'payload': payload, 'signature': sig.hex()}
|
|
70
|
+
|
|
71
|
+
def sign_event(self, event_type, ts=None, **fields):
|
|
72
|
+
"""General-purpose signed event — same shape as attestations/
|
|
73
|
+
revocations, used by discovery_relay.py/poc_discovery.py for
|
|
74
|
+
publish/like/subscribe events. Kept generic rather than one method
|
|
75
|
+
per event type since a relay never needs to know the field schema,
|
|
76
|
+
only that the signature is real."""
|
|
77
|
+
payload = {
|
|
78
|
+
'type': event_type,
|
|
79
|
+
'signer_pubkey': self.pubkey_hex(),
|
|
80
|
+
'ts': ts if ts is not None else time.time(),
|
|
81
|
+
**fields,
|
|
82
|
+
}
|
|
83
|
+
payload_bytes = json.dumps(payload, sort_keys=True).encode()
|
|
84
|
+
sig = self._priv.sign(payload_bytes)
|
|
85
|
+
return {'payload': payload, 'signature': sig.hex()}
|
|
86
|
+
|
|
87
|
+
def sign_revocation(self, target_id, reason='', ts=None):
|
|
88
|
+
payload = {
|
|
89
|
+
'type': 'revocation',
|
|
90
|
+
'signer_pubkey': self.pubkey_hex(),
|
|
91
|
+
'revokes': target_id,
|
|
92
|
+
'reason': reason,
|
|
93
|
+
'ts': ts if ts is not None else time.time(),
|
|
94
|
+
}
|
|
95
|
+
payload_bytes = json.dumps(payload, sort_keys=True).encode()
|
|
96
|
+
sig = self._priv.sign(payload_bytes)
|
|
97
|
+
return {'payload': payload, 'signature': sig.hex()}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def attestation_id(attestation):
|
|
101
|
+
"""Stable content-hash identifier for an attestation, so a revocation can
|
|
102
|
+
reference exactly which vouch it kills without a central sequence number."""
|
|
103
|
+
payload_bytes = json.dumps(attestation['payload'], sort_keys=True).encode()
|
|
104
|
+
return hashlib.sha256(payload_bytes).hexdigest()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def verify_attestation(attestation):
|
|
108
|
+
"""Real Ed25519 verification against the embedded signer pubkey. Returns
|
|
109
|
+
(valid: bool, reason: str)."""
|
|
110
|
+
payload = attestation['payload']
|
|
111
|
+
payload_bytes = json.dumps(payload, sort_keys=True).encode()
|
|
112
|
+
try:
|
|
113
|
+
signer_pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(payload['signer_pubkey']))
|
|
114
|
+
signer_pub.verify(bytes.fromhex(attestation['signature']), payload_bytes)
|
|
115
|
+
return True, 'signature valid'
|
|
116
|
+
except InvalidSignature:
|
|
117
|
+
return False, 'signature invalid — payload was tampered with after signing'
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return False, f'malformed attestation: {e}'
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def attestation_weight(attestation):
|
|
123
|
+
"""Decayed confidence in an attestation based on its age — a six-month-old
|
|
124
|
+
'this peer was good' claim shouldn't count as much as one from today."""
|
|
125
|
+
age_days = (time.time() - attestation['payload']['ts']) / 86400
|
|
126
|
+
return 0.5 ** (age_days / ATTESTATION_HALF_LIFE_DAYS)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ReputationStore:
|
|
130
|
+
"""One client's local view: direct experience (highest confidence,
|
|
131
|
+
never shared) plus signed attestations received from other clients
|
|
132
|
+
(portable, weighted by how much *I* trust the signer)."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, path):
|
|
135
|
+
self.path = path
|
|
136
|
+
self.direct = {} # peer_pubkey -> {passes, fails, avg_latency_ms, last_ts}
|
|
137
|
+
self.attestations = [] # list of received signed attestations
|
|
138
|
+
self.revocations = [] # list of received signed revocations
|
|
139
|
+
self._load()
|
|
140
|
+
|
|
141
|
+
def _load(self):
|
|
142
|
+
if os.path.exists(self.path):
|
|
143
|
+
with open(self.path) as f:
|
|
144
|
+
data = json.load(f)
|
|
145
|
+
self.direct = data.get('direct', {})
|
|
146
|
+
self.attestations = data.get('attestations', [])
|
|
147
|
+
self.revocations = data.get('revocations', [])
|
|
148
|
+
|
|
149
|
+
def save(self):
|
|
150
|
+
with open(self.path, 'w') as f:
|
|
151
|
+
json.dump({'direct': self.direct, 'attestations': self.attestations,
|
|
152
|
+
'revocations': self.revocations}, f, indent=2)
|
|
153
|
+
|
|
154
|
+
def record_direct(self, peer_pubkey, passes, fails, avg_latency_ms):
|
|
155
|
+
self.direct[peer_pubkey] = {
|
|
156
|
+
'passes': passes, 'fails': fails,
|
|
157
|
+
'avg_latency_ms': avg_latency_ms, 'last_ts': time.time(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def add_attestation(self, attestation):
|
|
161
|
+
ok, reason = verify_attestation(attestation)
|
|
162
|
+
if not ok:
|
|
163
|
+
return False, reason
|
|
164
|
+
self.attestations.append(attestation)
|
|
165
|
+
return True, 'accepted'
|
|
166
|
+
|
|
167
|
+
def add_revocation(self, revocation):
|
|
168
|
+
"""Only accepted if it's a real signature AND the revoker is the
|
|
169
|
+
same key that signed the attestation being revoked — otherwise
|
|
170
|
+
anyone could forge a revocation to silently kill someone else's
|
|
171
|
+
legitimate vouch. The original attestation is kept (not deleted):
|
|
172
|
+
'X vouched for Y, then revoked it' stays an honest, auditable fact."""
|
|
173
|
+
ok, reason = verify_attestation(revocation)
|
|
174
|
+
if not ok:
|
|
175
|
+
return False, reason
|
|
176
|
+
target_id = revocation['payload']['revokes']
|
|
177
|
+
target = next((a for a in self.attestations if attestation_id(a) == target_id), None)
|
|
178
|
+
if target is None:
|
|
179
|
+
return False, 'revokes an attestation this store has never seen'
|
|
180
|
+
if target['payload']['signer_pubkey'] != revocation['payload']['signer_pubkey']:
|
|
181
|
+
return False, 'revocation signer does not match the original attestation signer — rejected'
|
|
182
|
+
self.revocations.append(revocation)
|
|
183
|
+
return True, 'accepted'
|
|
184
|
+
|
|
185
|
+
def _revoked_ids(self):
|
|
186
|
+
return {r['payload']['revokes'] for r in self.revocations}
|
|
187
|
+
|
|
188
|
+
def trust_score(self, peer_pubkey, trust_in_signers=None):
|
|
189
|
+
"""Direct experience always dominates when present. Otherwise,
|
|
190
|
+
blend signed, non-revoked attestations weighted by (a) how much I
|
|
191
|
+
trust that signer and (b) how stale the attestation is."""
|
|
192
|
+
trust_in_signers = trust_in_signers or {}
|
|
193
|
+
if peer_pubkey in self.direct:
|
|
194
|
+
rec = self.direct[peer_pubkey]
|
|
195
|
+
total = rec['passes'] + rec['fails']
|
|
196
|
+
return rec['passes'] / total if total else 0.0, 'direct experience'
|
|
197
|
+
|
|
198
|
+
revoked = self._revoked_ids()
|
|
199
|
+
relevant = [a for a in self.attestations
|
|
200
|
+
if a['payload']['peer_pubkey'] == peer_pubkey and attestation_id(a) not in revoked]
|
|
201
|
+
n_revoked = sum(1 for a in self.attestations
|
|
202
|
+
if a['payload']['peer_pubkey'] == peer_pubkey and attestation_id(a) in revoked)
|
|
203
|
+
if not relevant:
|
|
204
|
+
if n_revoked:
|
|
205
|
+
return 0.0, f'all {n_revoked} attestation(s) for this peer were revoked by their own signers'
|
|
206
|
+
return 0.0, 'no direct history, no attestations — unknown peer'
|
|
207
|
+
|
|
208
|
+
num, den = 0.0, 0.0
|
|
209
|
+
for a in relevant:
|
|
210
|
+
signer = a['payload']['signer_pubkey']
|
|
211
|
+
my_trust = trust_in_signers.get(signer, 0.0)
|
|
212
|
+
if my_trust <= 0:
|
|
213
|
+
continue
|
|
214
|
+
w = my_trust * attestation_weight(a)
|
|
215
|
+
p = a['payload']['passes']
|
|
216
|
+
f = a['payload']['fails']
|
|
217
|
+
score = p / (p + f) if (p + f) else 0.0
|
|
218
|
+
num += w * score
|
|
219
|
+
den += w
|
|
220
|
+
if den == 0:
|
|
221
|
+
return 0.0, 'attestations exist but from signers I have zero trust in'
|
|
222
|
+
revoked_note = f', {n_revoked} revoked and excluded' if n_revoked else ''
|
|
223
|
+
return num / den, f'{len(relevant)} attestation(s){revoked_note}, weighted by signer trust + age'
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
for p in ('/tmp/poc_rep_alice.json', '/tmp/poc_rep_bob.json'):
|
|
228
|
+
if os.path.exists(p):
|
|
229
|
+
os.remove(p)
|
|
230
|
+
|
|
231
|
+
print("=== identities (real Ed25519 keypairs) ===")
|
|
232
|
+
alice = Identity('alice') # verifies peers directly, will vouch for them
|
|
233
|
+
bob = Identity('bob') # fresh client, never met "peer_x" directly
|
|
234
|
+
print(f" alice pubkey: {alice.pubkey_hex()[:16]}...")
|
|
235
|
+
print(f" bob pubkey: {bob.pubkey_hex()[:16]}...")
|
|
236
|
+
|
|
237
|
+
peer_x_pubkey = 'peerX_' + os.urandom(8).hex() # stand-in for a real peer identity
|
|
238
|
+
|
|
239
|
+
print("\n=== alice directly verifies peer_x (8/8 real challenge passes, from earlier work) ===")
|
|
240
|
+
alice_store = ReputationStore('/tmp/poc_rep_alice.json')
|
|
241
|
+
alice_store.record_direct(peer_x_pubkey, passes=8, fails=0, avg_latency_ms=0.18)
|
|
242
|
+
score, why = alice_store.trust_score(peer_x_pubkey)
|
|
243
|
+
print(f" alice's own trust score for peer_x: {score:.2f} ({why})")
|
|
244
|
+
|
|
245
|
+
print("\n=== alice signs an attestation and hands it to bob ===")
|
|
246
|
+
attestation = alice.sign_attestation(peer_x_pubkey, passes=8, fails=0, avg_latency_ms=0.18, k=8)
|
|
247
|
+
print(f" signed payload: {json.dumps(attestation['payload'])}")
|
|
248
|
+
print(f" signature: {attestation['signature'][:24]}...")
|
|
249
|
+
|
|
250
|
+
print("\n=== bob (never talked to peer_x) receives it, verifies, has SOME trust in alice ===")
|
|
251
|
+
bob_store = ReputationStore('/tmp/poc_rep_bob.json')
|
|
252
|
+
ok, reason = bob_store.add_attestation(attestation)
|
|
253
|
+
print(f" signature check: {ok} ({reason})")
|
|
254
|
+
bob_trust_in_signers = {alice.pubkey_hex(): 0.8} # bob trusts alice's vouches at 80%
|
|
255
|
+
score, why = bob_store.trust_score(peer_x_pubkey, bob_trust_in_signers)
|
|
256
|
+
print(f" bob's bootstrapped trust score for peer_x: {score:.2f} ({why})")
|
|
257
|
+
print(" -> bob has zero direct history with peer_x, but isn't starting from zero either.")
|
|
258
|
+
|
|
259
|
+
print("\n=== bob receives the SAME attestation from a signer he doesn't trust at all ===")
|
|
260
|
+
mallory = Identity('mallory')
|
|
261
|
+
fake_vouch = mallory.sign_attestation(peer_x_pubkey, passes=8, fails=0, avg_latency_ms=0.18, k=8)
|
|
262
|
+
bob_store.add_attestation(fake_vouch)
|
|
263
|
+
score, why = bob_store.trust_score(peer_x_pubkey, bob_trust_in_signers) # mallory absent from bob's trust map
|
|
264
|
+
print(f" bob's trust score, mallory's vouch included but ignored (0 weight): {score:.2f} ({why})")
|
|
265
|
+
print(" -> an untrusted signer's signature is still cryptographically valid, it just doesn't move the score.")
|
|
266
|
+
|
|
267
|
+
print("\n=== tamper test: mutate the payload after signing ===")
|
|
268
|
+
tampered = json.loads(json.dumps(attestation)) # deep copy
|
|
269
|
+
tampered['payload']['passes'] = 800 # attacker tries to inflate the record
|
|
270
|
+
ok, reason = verify_attestation(tampered)
|
|
271
|
+
print(f" tampered attestation (passes: 8 -> 800): valid={ok} ({reason})")
|
|
272
|
+
|
|
273
|
+
print("\n=== staleness test: same attestation, backdated ===")
|
|
274
|
+
old_attestation = alice.sign_attestation(
|
|
275
|
+
peer_x_pubkey, passes=8, fails=0, avg_latency_ms=0.18, k=8,
|
|
276
|
+
ts=time.time() - 90 * 86400) # 90 days old
|
|
277
|
+
fresh_weight = attestation_weight(attestation)
|
|
278
|
+
old_weight = attestation_weight(old_attestation)
|
|
279
|
+
print(f" fresh attestation weight: {fresh_weight:.3f}")
|
|
280
|
+
print(f" 90-day-old attestation weight: {old_weight:.3f} "
|
|
281
|
+
f"(half-life {ATTESTATION_HALF_LIFE_DAYS:.0f}d, so ~3 half-lives in)")
|
|
282
|
+
|
|
283
|
+
print("\n=== revocation: alice later learns peer_x went bad, revokes her own vouch ===")
|
|
284
|
+
target_id = attestation_id(attestation)
|
|
285
|
+
revocation = alice.sign_revocation(target_id, reason='peer_x failed possession challenges after this vouch')
|
|
286
|
+
ok, reason = bob_store.add_revocation(revocation)
|
|
287
|
+
print(f" bob receives alice's revocation: accepted={ok} ({reason})")
|
|
288
|
+
score, why = bob_store.trust_score(peer_x_pubkey, bob_trust_in_signers)
|
|
289
|
+
print(f" bob's trust score for peer_x now: {score:.2f} ({why})")
|
|
290
|
+
print(" -> dropped without bob ever re-verifying peer_x himself — the original attestation")
|
|
291
|
+
print(" is still on record (attestation_id present in the JSON below), just excluded.")
|
|
292
|
+
|
|
293
|
+
print("\n=== attack test: mallory tries to forge a revocation of ALICE's vouch ===")
|
|
294
|
+
forged = mallory.sign_revocation(target_id, reason='pretending to be alice')
|
|
295
|
+
ok, reason = bob_store.add_revocation(forged)
|
|
296
|
+
print(f" bob receives mallory's forged revocation: accepted={ok} ({reason})")
|
|
297
|
+
print(" -> valid signature (it really is signed by mallory), but signer != original signer, rejected.")
|
|
298
|
+
|
|
299
|
+
print("\n=== integrity test: revocation referencing an attestation nobody's seen ===")
|
|
300
|
+
bogus = alice.sign_revocation('0' * 64, reason='nonexistent target')
|
|
301
|
+
ok, reason = bob_store.add_revocation(bogus)
|
|
302
|
+
print(f" bob receives revocation for an unknown attestation id: accepted={ok} ({reason})")
|
|
303
|
+
|
|
304
|
+
alice_store.save()
|
|
305
|
+
bob_store.save()
|
|
306
|
+
print(f"\npersisted to {alice_store.path} / {bob_store.path} — "
|
|
307
|
+
f"a real client would keep these across restarts, not just this run")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
if __name__ == '__main__':
|
|
311
|
+
main()
|