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.
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()