@dzhechkov/skills-presentation-storyteller 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/bin/cli.js +5 -0
- package/package.json +47 -0
- package/sources.json +19 -0
- package/src/cli.js +107 -0
- package/src/commands/doctor.js +340 -0
- package/src/commands/init.js +168 -0
- package/src/commands/list.js +146 -0
- package/src/commands/remove.js +182 -0
- package/src/commands/update.js +170 -0
- package/src/utils.js +149 -0
- package/templates/.claude/commands/presentation-storyteller.md +23 -0
- package/templates/.claude/skills/explore/SKILL.md +218 -0
- package/templates/.claude/skills/explore/references/questioning-techniques.md +151 -0
- package/templates/.claude/skills/explore/references/task-brief-templates.md +355 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +418 -0
- package/templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md +658 -0
- package/templates/.claude/skills/goap-research-ed25519/references/research-actions.md +544 -0
- package/templates/.claude/skills/goap-research-ed25519/references/source-evaluation.md +560 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +662 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +720 -0
- package/templates/.claude/skills/presentation-storyteller/SKILL.md +374 -0
- package/templates/.claude/skills/presentation-storyteller/references/example-presentation.md +273 -0
- package/templates/.claude/skills/presentation-storyteller/references/slide-types.md +426 -0
- package/templates/.claude/skills/presentation-storyteller/references/sources-index-template.md +213 -0
- package/templates/.claude/skills/presentation-storyteller/references/speaker-script-patterns.md +324 -0
- package/templates/.claude/skills/presentation-storyteller/references/storytelling-frameworks.md +270 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Ed25519 Verification Module for GOAP Research
|
|
4
|
+
|
|
5
|
+
Provides cryptographic verification capabilities for research workflows:
|
|
6
|
+
- Keypair generation
|
|
7
|
+
- Content signing
|
|
8
|
+
- Signature verification
|
|
9
|
+
- Citation chain management
|
|
10
|
+
- Trusted issuer whitelist
|
|
11
|
+
- Verification ledger
|
|
12
|
+
|
|
13
|
+
Requirements:
|
|
14
|
+
pip install cryptography --break-system-packages
|
|
15
|
+
# OR
|
|
16
|
+
pip install pynacl --break-system-packages
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import base64
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Optional, Dict, List, Tuple, Any
|
|
24
|
+
from dataclasses import dataclass, field, asdict
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
# Try to import cryptography library (preferred)
|
|
28
|
+
try:
|
|
29
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
30
|
+
Ed25519PrivateKey, Ed25519PublicKey
|
|
31
|
+
)
|
|
32
|
+
from cryptography.hazmat.primitives import serialization
|
|
33
|
+
from cryptography.exceptions import InvalidSignature
|
|
34
|
+
CRYPTO_BACKEND = "cryptography"
|
|
35
|
+
except ImportError:
|
|
36
|
+
try:
|
|
37
|
+
import nacl.signing
|
|
38
|
+
import nacl.encoding
|
|
39
|
+
import nacl.exceptions
|
|
40
|
+
CRYPTO_BACKEND = "pynacl"
|
|
41
|
+
except ImportError:
|
|
42
|
+
CRYPTO_BACKEND = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class VerificationResult:
|
|
47
|
+
"""Result of a verification operation."""
|
|
48
|
+
verified: bool
|
|
49
|
+
content_hash: str
|
|
50
|
+
signature: str
|
|
51
|
+
issuer: str
|
|
52
|
+
issuer_pubkey: str
|
|
53
|
+
timestamp: str
|
|
54
|
+
confidence: float
|
|
55
|
+
error: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> Dict:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SignedFact:
|
|
63
|
+
"""A fact with cryptographic signature."""
|
|
64
|
+
claim: str
|
|
65
|
+
source_url: str
|
|
66
|
+
source_hash: str
|
|
67
|
+
issuer: str
|
|
68
|
+
issuer_pubkey: str
|
|
69
|
+
signature: str
|
|
70
|
+
timestamp: str
|
|
71
|
+
parent_citation: Optional[str] = None
|
|
72
|
+
confidence: float = 0.0
|
|
73
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> Dict:
|
|
76
|
+
return asdict(self)
|
|
77
|
+
|
|
78
|
+
def to_json(self) -> str:
|
|
79
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, data: Dict) -> 'SignedFact':
|
|
83
|
+
return cls(**data)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_json(cls, json_str: str) -> 'SignedFact':
|
|
87
|
+
return cls.from_dict(json.loads(json_str))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class CitationChain:
|
|
92
|
+
"""Chain of signed citations with integrity verification."""
|
|
93
|
+
chain_id: str
|
|
94
|
+
facts: List[SignedFact] = field(default_factory=list)
|
|
95
|
+
chain_signature: Optional[str] = None
|
|
96
|
+
chain_hash: Optional[str] = None
|
|
97
|
+
integrity_verified: bool = False
|
|
98
|
+
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat() + "Z")
|
|
99
|
+
|
|
100
|
+
def add_fact(self, fact: SignedFact) -> None:
|
|
101
|
+
"""Add fact to chain with automatic parent linking."""
|
|
102
|
+
if self.facts:
|
|
103
|
+
fact.parent_citation = f"chain:{self.chain_id}:fact:{len(self.facts)-1}"
|
|
104
|
+
self.facts.append(fact)
|
|
105
|
+
self.chain_hash = None # Invalidate cached hash
|
|
106
|
+
|
|
107
|
+
def get_chain_hash(self) -> str:
|
|
108
|
+
"""Calculate hash of entire chain for signing."""
|
|
109
|
+
if self.chain_hash is None:
|
|
110
|
+
chain_data = json.dumps(
|
|
111
|
+
[f.to_dict() for f in self.facts],
|
|
112
|
+
sort_keys=True
|
|
113
|
+
)
|
|
114
|
+
self.chain_hash = hashlib.sha256(chain_data.encode()).hexdigest()
|
|
115
|
+
return self.chain_hash
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> Dict:
|
|
118
|
+
return {
|
|
119
|
+
'chain_id': self.chain_id,
|
|
120
|
+
'facts': [f.to_dict() for f in self.facts],
|
|
121
|
+
'chain_signature': self.chain_signature,
|
|
122
|
+
'chain_hash': self.get_chain_hash(),
|
|
123
|
+
'integrity_verified': self.integrity_verified,
|
|
124
|
+
'created_at': self.created_at
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Ed25519Verifier:
|
|
129
|
+
"""
|
|
130
|
+
Ed25519 verification system for GOAP research.
|
|
131
|
+
|
|
132
|
+
Provides:
|
|
133
|
+
- Keypair generation and management
|
|
134
|
+
- Content signing and verification
|
|
135
|
+
- Citation chain management
|
|
136
|
+
- Trusted issuer whitelist
|
|
137
|
+
- Verification ledger
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
DEFAULT_TRUSTED_ISSUERS = {
|
|
141
|
+
# News agencies
|
|
142
|
+
"reuters.com": None,
|
|
143
|
+
"ap.org": None,
|
|
144
|
+
"bbc.com": None,
|
|
145
|
+
"nytimes.com": None,
|
|
146
|
+
"wsj.com": None,
|
|
147
|
+
# Academic
|
|
148
|
+
"arxiv.org": None,
|
|
149
|
+
"nature.com": None,
|
|
150
|
+
"science.org": None,
|
|
151
|
+
"sciencedirect.com": None,
|
|
152
|
+
"pubmed.gov": None,
|
|
153
|
+
"ieee.org": None,
|
|
154
|
+
"acm.org": None,
|
|
155
|
+
# Government
|
|
156
|
+
".gov": None, # Suffix match
|
|
157
|
+
".gov.uk": None,
|
|
158
|
+
"europa.eu": None,
|
|
159
|
+
"who.int": None,
|
|
160
|
+
# Financial/Regulatory
|
|
161
|
+
"sec.gov": None,
|
|
162
|
+
"federalreserve.gov": None,
|
|
163
|
+
"ecb.europa.eu": None,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
trusted_issuers: Optional[Dict[str, Optional[str]]] = None,
|
|
169
|
+
verification_threshold: float = 0.85,
|
|
170
|
+
auto_generate_keypair: bool = False
|
|
171
|
+
):
|
|
172
|
+
"""
|
|
173
|
+
Initialize verifier.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
trusted_issuers: Dict of domain -> public_key_b64 (None = trust without signature)
|
|
177
|
+
verification_threshold: Minimum confidence for verified status
|
|
178
|
+
auto_generate_keypair: Generate keypair on init
|
|
179
|
+
"""
|
|
180
|
+
if CRYPTO_BACKEND is None:
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
"No cryptographic backend available. Install cryptography or pynacl:\n"
|
|
183
|
+
" pip install cryptography --break-system-packages"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
self.trusted_issuers = trusted_issuers or self.DEFAULT_TRUSTED_ISSUERS.copy()
|
|
187
|
+
self.verification_threshold = verification_threshold
|
|
188
|
+
self.verification_ledger: List[VerificationResult] = []
|
|
189
|
+
self._private_key: Optional[bytes] = None
|
|
190
|
+
self._public_key: Optional[bytes] = None
|
|
191
|
+
|
|
192
|
+
if auto_generate_keypair:
|
|
193
|
+
self.generate_keypair()
|
|
194
|
+
|
|
195
|
+
def generate_keypair(self) -> Tuple[bytes, bytes]:
|
|
196
|
+
"""
|
|
197
|
+
Generate new Ed25519 keypair.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Tuple of (private_key_bytes, public_key_bytes)
|
|
201
|
+
"""
|
|
202
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
203
|
+
private_key = Ed25519PrivateKey.generate()
|
|
204
|
+
public_key = private_key.public_key()
|
|
205
|
+
|
|
206
|
+
private_bytes = private_key.private_bytes(
|
|
207
|
+
encoding=serialization.Encoding.Raw,
|
|
208
|
+
format=serialization.PrivateFormat.Raw,
|
|
209
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
210
|
+
)
|
|
211
|
+
public_bytes = public_key.public_bytes(
|
|
212
|
+
encoding=serialization.Encoding.Raw,
|
|
213
|
+
format=serialization.PublicFormat.Raw
|
|
214
|
+
)
|
|
215
|
+
else: # pynacl
|
|
216
|
+
signing_key = nacl.signing.SigningKey.generate()
|
|
217
|
+
private_bytes = bytes(signing_key)
|
|
218
|
+
public_bytes = bytes(signing_key.verify_key)
|
|
219
|
+
|
|
220
|
+
self._private_key = private_bytes
|
|
221
|
+
self._public_key = public_bytes
|
|
222
|
+
|
|
223
|
+
return private_bytes, public_bytes
|
|
224
|
+
|
|
225
|
+
def load_keypair(self, private_key: bytes, public_key: Optional[bytes] = None) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Load existing keypair.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
private_key: 32-byte private key (seed)
|
|
231
|
+
public_key: 32-byte public key (optional, can be derived)
|
|
232
|
+
"""
|
|
233
|
+
self._private_key = private_key
|
|
234
|
+
|
|
235
|
+
if public_key is not None:
|
|
236
|
+
self._public_key = public_key
|
|
237
|
+
else:
|
|
238
|
+
# Derive public key from private
|
|
239
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
240
|
+
pk = Ed25519PrivateKey.from_private_bytes(private_key)
|
|
241
|
+
self._public_key = pk.public_key().public_bytes(
|
|
242
|
+
encoding=serialization.Encoding.Raw,
|
|
243
|
+
format=serialization.PublicFormat.Raw
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
signing_key = nacl.signing.SigningKey(private_key)
|
|
247
|
+
self._public_key = bytes(signing_key.verify_key)
|
|
248
|
+
|
|
249
|
+
def load_keypair_from_files(self, private_path: str, public_path: Optional[str] = None) -> None:
|
|
250
|
+
"""Load keypair from files."""
|
|
251
|
+
with open(private_path, 'rb') as f:
|
|
252
|
+
private_key = f.read()
|
|
253
|
+
|
|
254
|
+
public_key = None
|
|
255
|
+
if public_path:
|
|
256
|
+
with open(public_path, 'rb') as f:
|
|
257
|
+
public_key = f.read()
|
|
258
|
+
|
|
259
|
+
self.load_keypair(private_key, public_key)
|
|
260
|
+
|
|
261
|
+
def save_keypair_to_files(self, private_path: str, public_path: str) -> None:
|
|
262
|
+
"""Save keypair to files."""
|
|
263
|
+
if self._private_key is None or self._public_key is None:
|
|
264
|
+
raise ValueError("No keypair to save. Generate or load one first.")
|
|
265
|
+
|
|
266
|
+
with open(private_path, 'wb') as f:
|
|
267
|
+
f.write(self._private_key)
|
|
268
|
+
os.chmod(private_path, 0o600) # Restrict private key permissions
|
|
269
|
+
|
|
270
|
+
with open(public_path, 'wb') as f:
|
|
271
|
+
f.write(self._public_key)
|
|
272
|
+
|
|
273
|
+
def get_public_key_b64(self) -> str:
|
|
274
|
+
"""Get public key as base64 string."""
|
|
275
|
+
if self._public_key is None:
|
|
276
|
+
raise ValueError("No public key available.")
|
|
277
|
+
return base64.b64encode(self._public_key).decode('ascii')
|
|
278
|
+
|
|
279
|
+
def sign_content(self, content: str) -> Tuple[str, str]:
|
|
280
|
+
"""
|
|
281
|
+
Sign content with private key.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
content: String content to sign
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Tuple of (signature_base64, content_sha256_hash)
|
|
288
|
+
"""
|
|
289
|
+
if self._private_key is None:
|
|
290
|
+
raise ValueError("No private key loaded. Call generate_keypair() first.")
|
|
291
|
+
|
|
292
|
+
content_bytes = content.encode('utf-8')
|
|
293
|
+
content_hash = hashlib.sha256(content_bytes).hexdigest()
|
|
294
|
+
|
|
295
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
296
|
+
private_key = Ed25519PrivateKey.from_private_bytes(self._private_key)
|
|
297
|
+
signature = private_key.sign(content_bytes)
|
|
298
|
+
else:
|
|
299
|
+
signing_key = nacl.signing.SigningKey(self._private_key)
|
|
300
|
+
signed = signing_key.sign(content_bytes)
|
|
301
|
+
signature = signed.signature
|
|
302
|
+
|
|
303
|
+
signature_b64 = base64.b64encode(signature).decode('ascii')
|
|
304
|
+
return signature_b64, content_hash
|
|
305
|
+
|
|
306
|
+
def verify_signature(
|
|
307
|
+
self,
|
|
308
|
+
content: str,
|
|
309
|
+
signature_b64: str,
|
|
310
|
+
public_key: bytes
|
|
311
|
+
) -> bool:
|
|
312
|
+
"""
|
|
313
|
+
Verify Ed25519 signature.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
content: Original content
|
|
317
|
+
signature_b64: Base64-encoded signature
|
|
318
|
+
public_key: 32-byte public key
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if valid, False otherwise
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
content_bytes = content.encode('utf-8')
|
|
325
|
+
signature = base64.b64decode(signature_b64)
|
|
326
|
+
|
|
327
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
328
|
+
pub_key = Ed25519PublicKey.from_public_bytes(public_key)
|
|
329
|
+
pub_key.verify(signature, content_bytes)
|
|
330
|
+
return True
|
|
331
|
+
else:
|
|
332
|
+
verify_key = nacl.signing.VerifyKey(public_key)
|
|
333
|
+
verify_key.verify(content_bytes, signature)
|
|
334
|
+
return True
|
|
335
|
+
except (InvalidSignature, nacl.exceptions.BadSignatureError) if CRYPTO_BACKEND == "pynacl" else InvalidSignature:
|
|
336
|
+
return False
|
|
337
|
+
except Exception:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
def is_trusted_issuer(self, domain: str) -> bool:
|
|
341
|
+
"""Check if domain is in trusted issuers whitelist."""
|
|
342
|
+
# Direct match
|
|
343
|
+
if domain in self.trusted_issuers:
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
# Suffix match for patterns like ".gov"
|
|
347
|
+
for trusted in self.trusted_issuers:
|
|
348
|
+
if trusted.startswith('.') and domain.endswith(trusted):
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
# Check if any trusted issuer is a suffix of the domain
|
|
352
|
+
for trusted in self.trusted_issuers:
|
|
353
|
+
if domain.endswith('.' + trusted) or domain == trusted:
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def add_trusted_issuer(self, domain: str, public_key_b64: Optional[str] = None) -> None:
|
|
359
|
+
"""Add domain to trusted issuers."""
|
|
360
|
+
self.trusted_issuers[domain] = public_key_b64
|
|
361
|
+
|
|
362
|
+
def remove_trusted_issuer(self, domain: str) -> None:
|
|
363
|
+
"""Remove domain from trusted issuers."""
|
|
364
|
+
self.trusted_issuers.pop(domain, None)
|
|
365
|
+
|
|
366
|
+
def create_signed_fact(
|
|
367
|
+
self,
|
|
368
|
+
claim: str,
|
|
369
|
+
source_url: str,
|
|
370
|
+
source_content: str,
|
|
371
|
+
issuer: str,
|
|
372
|
+
metadata: Optional[Dict] = None
|
|
373
|
+
) -> SignedFact:
|
|
374
|
+
"""
|
|
375
|
+
Create a signed fact with full verification metadata.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
claim: The factual claim being made
|
|
379
|
+
source_url: URL of the source
|
|
380
|
+
source_content: Content from the source
|
|
381
|
+
issuer: Domain/identity of the issuer
|
|
382
|
+
metadata: Additional metadata
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
SignedFact with Ed25519 signature
|
|
386
|
+
"""
|
|
387
|
+
if self._private_key is None:
|
|
388
|
+
raise ValueError("No private key loaded.")
|
|
389
|
+
|
|
390
|
+
source_hash = hashlib.sha256(source_content.encode()).hexdigest()
|
|
391
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
392
|
+
|
|
393
|
+
# Sign the claim + source_hash + timestamp
|
|
394
|
+
signing_content = f"{claim}|{source_hash}|{timestamp}"
|
|
395
|
+
signature, _ = self.sign_content(signing_content)
|
|
396
|
+
|
|
397
|
+
public_key_b64 = base64.b64encode(self._public_key).decode('ascii')
|
|
398
|
+
|
|
399
|
+
# Calculate confidence
|
|
400
|
+
base_confidence = 0.80 # Base for signed content
|
|
401
|
+
if self.is_trusted_issuer(issuer):
|
|
402
|
+
base_confidence = 0.95
|
|
403
|
+
|
|
404
|
+
return SignedFact(
|
|
405
|
+
claim=claim,
|
|
406
|
+
source_url=source_url,
|
|
407
|
+
source_hash=source_hash,
|
|
408
|
+
issuer=issuer,
|
|
409
|
+
issuer_pubkey=f"ed25519:{public_key_b64}",
|
|
410
|
+
signature=signature,
|
|
411
|
+
timestamp=timestamp,
|
|
412
|
+
confidence=base_confidence,
|
|
413
|
+
metadata=metadata or {}
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def verify_fact(self, fact: SignedFact) -> VerificationResult:
|
|
417
|
+
"""
|
|
418
|
+
Verify a signed fact.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
fact: SignedFact to verify
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
VerificationResult with status and confidence
|
|
425
|
+
"""
|
|
426
|
+
# Extract public key from fact
|
|
427
|
+
if not fact.issuer_pubkey.startswith("ed25519:"):
|
|
428
|
+
result = VerificationResult(
|
|
429
|
+
verified=False,
|
|
430
|
+
content_hash=fact.source_hash,
|
|
431
|
+
signature=fact.signature,
|
|
432
|
+
issuer=fact.issuer,
|
|
433
|
+
issuer_pubkey=fact.issuer_pubkey,
|
|
434
|
+
timestamp=fact.timestamp,
|
|
435
|
+
confidence=0.0,
|
|
436
|
+
error="Invalid public key format (expected 'ed25519:...')"
|
|
437
|
+
)
|
|
438
|
+
self.verification_ledger.append(result)
|
|
439
|
+
return result
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
pubkey_b64 = fact.issuer_pubkey[8:] # Remove "ed25519:" prefix
|
|
443
|
+
public_key = base64.b64decode(pubkey_b64)
|
|
444
|
+
|
|
445
|
+
# Reconstruct signing content
|
|
446
|
+
signing_content = f"{fact.claim}|{fact.source_hash}|{fact.timestamp}"
|
|
447
|
+
|
|
448
|
+
verified = self.verify_signature(signing_content, fact.signature, public_key)
|
|
449
|
+
|
|
450
|
+
# Calculate confidence
|
|
451
|
+
confidence = 0.0
|
|
452
|
+
if verified:
|
|
453
|
+
confidence = 0.80
|
|
454
|
+
if self.is_trusted_issuer(fact.issuer):
|
|
455
|
+
confidence = 0.95
|
|
456
|
+
|
|
457
|
+
result = VerificationResult(
|
|
458
|
+
verified=verified,
|
|
459
|
+
content_hash=fact.source_hash,
|
|
460
|
+
signature=fact.signature,
|
|
461
|
+
issuer=fact.issuer,
|
|
462
|
+
issuer_pubkey=fact.issuer_pubkey,
|
|
463
|
+
timestamp=fact.timestamp,
|
|
464
|
+
confidence=confidence,
|
|
465
|
+
error=None if verified else "Signature verification failed"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
self.verification_ledger.append(result)
|
|
469
|
+
return result
|
|
470
|
+
|
|
471
|
+
except Exception as e:
|
|
472
|
+
result = VerificationResult(
|
|
473
|
+
verified=False,
|
|
474
|
+
content_hash=fact.source_hash,
|
|
475
|
+
signature=fact.signature,
|
|
476
|
+
issuer=fact.issuer,
|
|
477
|
+
issuer_pubkey=fact.issuer_pubkey,
|
|
478
|
+
timestamp=fact.timestamp,
|
|
479
|
+
confidence=0.0,
|
|
480
|
+
error=str(e)
|
|
481
|
+
)
|
|
482
|
+
self.verification_ledger.append(result)
|
|
483
|
+
return result
|
|
484
|
+
|
|
485
|
+
def verify_citation_chain(self, chain: CitationChain) -> Tuple[bool, float, Optional[str]]:
|
|
486
|
+
"""
|
|
487
|
+
Verify entire citation chain integrity.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
chain: CitationChain to verify
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Tuple of (all_verified, aggregate_confidence, error_message)
|
|
494
|
+
"""
|
|
495
|
+
if not chain.facts:
|
|
496
|
+
return False, 0.0, "Empty chain"
|
|
497
|
+
|
|
498
|
+
all_verified = True
|
|
499
|
+
total_confidence = 0.0
|
|
500
|
+
errors = []
|
|
501
|
+
|
|
502
|
+
for i, fact in enumerate(chain.facts):
|
|
503
|
+
result = self.verify_fact(fact)
|
|
504
|
+
|
|
505
|
+
if not result.verified:
|
|
506
|
+
all_verified = False
|
|
507
|
+
errors.append(f"Fact {i}: {result.error}")
|
|
508
|
+
|
|
509
|
+
total_confidence += result.confidence
|
|
510
|
+
|
|
511
|
+
# Verify chain linkage
|
|
512
|
+
if i > 0:
|
|
513
|
+
expected_parent = f"chain:{chain.chain_id}:fact:{i-1}"
|
|
514
|
+
if fact.parent_citation != expected_parent:
|
|
515
|
+
all_verified = False
|
|
516
|
+
errors.append(f"Fact {i}: Invalid parent citation")
|
|
517
|
+
|
|
518
|
+
aggregate_confidence = total_confidence / len(chain.facts)
|
|
519
|
+
chain.integrity_verified = all_verified
|
|
520
|
+
|
|
521
|
+
error_msg = "; ".join(errors) if errors else None
|
|
522
|
+
return all_verified, aggregate_confidence, error_msg
|
|
523
|
+
|
|
524
|
+
def sign_chain(self, chain: CitationChain) -> str:
|
|
525
|
+
"""Sign the entire citation chain."""
|
|
526
|
+
chain_hash = chain.get_chain_hash()
|
|
527
|
+
signature, _ = self.sign_content(chain_hash)
|
|
528
|
+
chain.chain_signature = signature
|
|
529
|
+
return signature
|
|
530
|
+
|
|
531
|
+
def get_verification_ledger(self) -> List[Dict]:
|
|
532
|
+
"""Get verification ledger as list of dicts."""
|
|
533
|
+
return [r.to_dict() for r in self.verification_ledger]
|
|
534
|
+
|
|
535
|
+
def sign_ledger(self) -> str:
|
|
536
|
+
"""Sign the entire verification ledger."""
|
|
537
|
+
ledger_json = json.dumps(self.get_verification_ledger(), sort_keys=True)
|
|
538
|
+
signature, _ = self.sign_content(ledger_json)
|
|
539
|
+
return signature
|
|
540
|
+
|
|
541
|
+
def export_ledger(self, filepath: str) -> None:
|
|
542
|
+
"""Export verification ledger to file."""
|
|
543
|
+
data = {
|
|
544
|
+
'ledger': self.get_verification_ledger(),
|
|
545
|
+
'signature': self.sign_ledger(),
|
|
546
|
+
'exported_at': datetime.utcnow().isoformat() + "Z",
|
|
547
|
+
'signer_pubkey': f"ed25519:{self.get_public_key_b64()}" if self._public_key else None
|
|
548
|
+
}
|
|
549
|
+
with open(filepath, 'w') as f:
|
|
550
|
+
json.dump(data, f, indent=2)
|
|
551
|
+
|
|
552
|
+
def clear_ledger(self) -> None:
|
|
553
|
+
"""Clear the verification ledger."""
|
|
554
|
+
self.verification_ledger.clear()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# Convenience functions
|
|
558
|
+
def generate_keypair_b64() -> Tuple[str, str]:
|
|
559
|
+
"""Generate keypair and return as base64 strings."""
|
|
560
|
+
verifier = Ed25519Verifier()
|
|
561
|
+
private_bytes, public_bytes = verifier.generate_keypair()
|
|
562
|
+
return (
|
|
563
|
+
base64.b64encode(private_bytes).decode('ascii'),
|
|
564
|
+
base64.b64encode(public_bytes).decode('ascii')
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def quick_sign(content: str, private_key_b64: str) -> str:
|
|
569
|
+
"""Quick sign content with base64-encoded private key."""
|
|
570
|
+
verifier = Ed25519Verifier()
|
|
571
|
+
private_bytes = base64.b64decode(private_key_b64)
|
|
572
|
+
verifier.load_keypair(private_bytes)
|
|
573
|
+
signature, _ = verifier.sign_content(content)
|
|
574
|
+
return signature
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def quick_verify(content: str, signature_b64: str, public_key_b64: str) -> bool:
|
|
578
|
+
"""Quick verify content signature."""
|
|
579
|
+
verifier = Ed25519Verifier()
|
|
580
|
+
public_bytes = base64.b64decode(public_key_b64)
|
|
581
|
+
return verifier.verify_signature(content, signature_b64, public_bytes)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# Demo and testing
|
|
585
|
+
if __name__ == "__main__":
|
|
586
|
+
print("Ed25519 Verification Module Demo")
|
|
587
|
+
print("=" * 60)
|
|
588
|
+
print(f"Crypto Backend: {CRYPTO_BACKEND}")
|
|
589
|
+
print()
|
|
590
|
+
|
|
591
|
+
# Initialize verifier
|
|
592
|
+
verifier = Ed25519Verifier(
|
|
593
|
+
verification_threshold=0.85,
|
|
594
|
+
auto_generate_keypair=True
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
print(f"Public Key: ed25519:{verifier.get_public_key_b64()[:32]}...")
|
|
598
|
+
print()
|
|
599
|
+
|
|
600
|
+
# Create and sign a fact
|
|
601
|
+
print("[1] Creating signed fact...")
|
|
602
|
+
fact = verifier.create_signed_fact(
|
|
603
|
+
claim="The study found a 25% improvement in efficiency",
|
|
604
|
+
source_url="https://nature.com/articles/example",
|
|
605
|
+
source_content="Full article content here...",
|
|
606
|
+
issuer="nature.com"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
print(f"Claim: {fact.claim}")
|
|
610
|
+
print(f"Source: {fact.source_url}")
|
|
611
|
+
print(f"Signature: {fact.signature[:32]}...")
|
|
612
|
+
print(f"Confidence: {fact.confidence}")
|
|
613
|
+
print()
|
|
614
|
+
|
|
615
|
+
# Verify the fact
|
|
616
|
+
print("[2] Verifying signed fact...")
|
|
617
|
+
result = verifier.verify_fact(fact)
|
|
618
|
+
print(f"Verified: {result.verified}")
|
|
619
|
+
print(f"Confidence: {result.confidence}")
|
|
620
|
+
print(f"Error: {result.error}")
|
|
621
|
+
print()
|
|
622
|
+
|
|
623
|
+
# Create citation chain
|
|
624
|
+
print("[3] Building citation chain...")
|
|
625
|
+
chain = CitationChain(chain_id="research_001")
|
|
626
|
+
|
|
627
|
+
for i in range(3):
|
|
628
|
+
fact = verifier.create_signed_fact(
|
|
629
|
+
claim=f"Claim {i+1} from the research",
|
|
630
|
+
source_url=f"https://source{i+1}.com/article",
|
|
631
|
+
source_content=f"Source content {i+1}",
|
|
632
|
+
issuer=["reuters.com", "arxiv.org", "example.com"][i]
|
|
633
|
+
)
|
|
634
|
+
chain.add_fact(fact)
|
|
635
|
+
|
|
636
|
+
print(f"Chain ID: {chain.chain_id}")
|
|
637
|
+
print(f"Facts: {len(chain.facts)}")
|
|
638
|
+
print(f"Chain Hash: {chain.get_chain_hash()[:32]}...")
|
|
639
|
+
print()
|
|
640
|
+
|
|
641
|
+
# Verify chain
|
|
642
|
+
print("[4] Verifying citation chain...")
|
|
643
|
+
all_verified, confidence, error = verifier.verify_citation_chain(chain)
|
|
644
|
+
print(f"All Verified: {all_verified}")
|
|
645
|
+
print(f"Aggregate Confidence: {confidence:.2%}")
|
|
646
|
+
print(f"Errors: {error}")
|
|
647
|
+
print()
|
|
648
|
+
|
|
649
|
+
# Sign chain
|
|
650
|
+
chain_sig = verifier.sign_chain(chain)
|
|
651
|
+
print(f"Chain Signature: {chain_sig[:32]}...")
|
|
652
|
+
print()
|
|
653
|
+
|
|
654
|
+
# Verification ledger
|
|
655
|
+
print("[5] Verification Ledger:")
|
|
656
|
+
for i, entry in enumerate(verifier.get_verification_ledger()):
|
|
657
|
+
print(f" {i+1}. {entry['issuer']}: {'✅' if entry['verified'] else '❌'} "
|
|
658
|
+
f"(confidence: {entry['confidence']:.2%})")
|
|
659
|
+
|
|
660
|
+
print()
|
|
661
|
+
print("=" * 60)
|
|
662
|
+
print("Demo complete!")
|