@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,658 @@
|
|
|
1
|
+
# Ed25519 Verification Reference
|
|
2
|
+
|
|
3
|
+
Comprehensive documentation for Ed25519 cryptographic verification in GOAP research.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Ed25519 is an elliptic curve digital signature algorithm using Curve25519. It provides:
|
|
8
|
+
- **128-bit security level** - resistant to known attacks
|
|
9
|
+
- **Fast verification** - ~71,000 verifications/second on modern hardware
|
|
10
|
+
- **Small signatures** - 64 bytes
|
|
11
|
+
- **Deterministic** - same message + key always produces same signature
|
|
12
|
+
|
|
13
|
+
## Why Ed25519 for Anti-Hallucination?
|
|
14
|
+
|
|
15
|
+
| Problem | Ed25519 Solution |
|
|
16
|
+
|---------|------------------|
|
|
17
|
+
| Source authenticity | Cryptographic proof of origin |
|
|
18
|
+
| Content tampering | Hash-based integrity verification |
|
|
19
|
+
| Citation chain breaks | Linked signature chains |
|
|
20
|
+
| Audit requirements | Signed verification ledger |
|
|
21
|
+
| Trust establishment | Trusted issuer whitelist |
|
|
22
|
+
|
|
23
|
+
## Key Concepts
|
|
24
|
+
|
|
25
|
+
### Keypair Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Private Key (Seed): 32 bytes - KEEP SECRET
|
|
29
|
+
└── Used for signing
|
|
30
|
+
|
|
31
|
+
Public Key: 32 bytes - SHAREABLE
|
|
32
|
+
└── Used for verification
|
|
33
|
+
└── Can be published or fetched from keyserver
|
|
34
|
+
|
|
35
|
+
Signature: 64 bytes
|
|
36
|
+
└── Proves private key holder signed specific content
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Signature Workflow
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
SIGNING (by source/researcher):
|
|
43
|
+
content → SHA-512(content) → Ed25519.sign(hash, private_key) → signature
|
|
44
|
+
|
|
45
|
+
VERIFICATION (by consumer):
|
|
46
|
+
(content, signature, public_key) → Ed25519.verify() → true/false
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Implementation
|
|
50
|
+
|
|
51
|
+
### Python Implementation
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
"""
|
|
55
|
+
Ed25519 Verification Module for GOAP Research
|
|
56
|
+
|
|
57
|
+
Requirements:
|
|
58
|
+
pip install cryptography pynacl --break-system-packages
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
import hashlib
|
|
62
|
+
import json
|
|
63
|
+
import base64
|
|
64
|
+
from datetime import datetime
|
|
65
|
+
from typing import Optional, Dict, List, Tuple
|
|
66
|
+
from dataclasses import dataclass, field, asdict
|
|
67
|
+
|
|
68
|
+
# Use cryptography library (preferred) or pynacl
|
|
69
|
+
try:
|
|
70
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
71
|
+
Ed25519PrivateKey, Ed25519PublicKey
|
|
72
|
+
)
|
|
73
|
+
from cryptography.hazmat.primitives import serialization
|
|
74
|
+
CRYPTO_BACKEND = "cryptography"
|
|
75
|
+
except ImportError:
|
|
76
|
+
import nacl.signing
|
|
77
|
+
import nacl.encoding
|
|
78
|
+
CRYPTO_BACKEND = "pynacl"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class VerificationResult:
|
|
83
|
+
"""Result of a verification operation."""
|
|
84
|
+
verified: bool
|
|
85
|
+
content_hash: str
|
|
86
|
+
signature: str
|
|
87
|
+
issuer: str
|
|
88
|
+
issuer_pubkey: str
|
|
89
|
+
timestamp: str
|
|
90
|
+
confidence: float
|
|
91
|
+
error: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class SignedFact:
|
|
96
|
+
"""A fact with cryptographic signature."""
|
|
97
|
+
claim: str
|
|
98
|
+
source_url: str
|
|
99
|
+
source_hash: str
|
|
100
|
+
issuer: str
|
|
101
|
+
issuer_pubkey: str
|
|
102
|
+
signature: str
|
|
103
|
+
timestamp: str
|
|
104
|
+
parent_citation: Optional[str] = None
|
|
105
|
+
confidence: float = 0.0
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> Dict:
|
|
108
|
+
return asdict(self)
|
|
109
|
+
|
|
110
|
+
def to_json(self) -> str:
|
|
111
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class CitationChain:
|
|
116
|
+
"""Chain of signed citations."""
|
|
117
|
+
chain_id: str
|
|
118
|
+
facts: List[SignedFact] = field(default_factory=list)
|
|
119
|
+
chain_signature: Optional[str] = None
|
|
120
|
+
integrity_verified: bool = False
|
|
121
|
+
|
|
122
|
+
def add_fact(self, fact: SignedFact) -> None:
|
|
123
|
+
if self.facts:
|
|
124
|
+
fact.parent_citation = f"chain:{self.chain_id}:fact:{len(self.facts)-1}"
|
|
125
|
+
self.facts.append(fact)
|
|
126
|
+
|
|
127
|
+
def get_chain_hash(self) -> str:
|
|
128
|
+
"""Calculate hash of entire chain for signing."""
|
|
129
|
+
chain_data = json.dumps([f.to_dict() for f in self.facts], sort_keys=True)
|
|
130
|
+
return hashlib.sha256(chain_data.encode()).hexdigest()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Ed25519Verifier:
|
|
134
|
+
"""
|
|
135
|
+
Ed25519 verification system for GOAP research.
|
|
136
|
+
|
|
137
|
+
Provides:
|
|
138
|
+
- Keypair generation
|
|
139
|
+
- Content signing
|
|
140
|
+
- Signature verification
|
|
141
|
+
- Citation chain management
|
|
142
|
+
- Trusted issuer whitelist
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
DEFAULT_TRUSTED_ISSUERS = {
|
|
146
|
+
# News agencies
|
|
147
|
+
"reuters.com": "ed25519:reuters_pubkey_placeholder",
|
|
148
|
+
"ap.org": "ed25519:ap_pubkey_placeholder",
|
|
149
|
+
"bbc.com": "ed25519:bbc_pubkey_placeholder",
|
|
150
|
+
# Academic
|
|
151
|
+
"arxiv.org": "ed25519:arxiv_pubkey_placeholder",
|
|
152
|
+
"nature.com": "ed25519:nature_pubkey_placeholder",
|
|
153
|
+
"science.org": "ed25519:science_pubkey_placeholder",
|
|
154
|
+
"pubmed.gov": "ed25519:pubmed_pubkey_placeholder",
|
|
155
|
+
# Government
|
|
156
|
+
"sec.gov": "ed25519:sec_pubkey_placeholder",
|
|
157
|
+
"federalreserve.gov": "ed25519:fed_pubkey_placeholder",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
trusted_issuers: Optional[Dict[str, str]] = None,
|
|
163
|
+
verification_threshold: float = 0.85
|
|
164
|
+
):
|
|
165
|
+
self.trusted_issuers = trusted_issuers or self.DEFAULT_TRUSTED_ISSUERS
|
|
166
|
+
self.verification_threshold = verification_threshold
|
|
167
|
+
self.verification_ledger: List[VerificationResult] = []
|
|
168
|
+
self._private_key = None
|
|
169
|
+
self._public_key = None
|
|
170
|
+
|
|
171
|
+
def generate_keypair(self) -> Tuple[bytes, bytes]:
|
|
172
|
+
"""Generate new Ed25519 keypair."""
|
|
173
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
174
|
+
private_key = Ed25519PrivateKey.generate()
|
|
175
|
+
public_key = private_key.public_key()
|
|
176
|
+
|
|
177
|
+
private_bytes = private_key.private_bytes(
|
|
178
|
+
encoding=serialization.Encoding.Raw,
|
|
179
|
+
format=serialization.PrivateFormat.Raw,
|
|
180
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
181
|
+
)
|
|
182
|
+
public_bytes = public_key.public_bytes(
|
|
183
|
+
encoding=serialization.Encoding.Raw,
|
|
184
|
+
format=serialization.PublicFormat.Raw
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
signing_key = nacl.signing.SigningKey.generate()
|
|
188
|
+
private_bytes = bytes(signing_key)
|
|
189
|
+
public_bytes = bytes(signing_key.verify_key)
|
|
190
|
+
|
|
191
|
+
self._private_key = private_bytes
|
|
192
|
+
self._public_key = public_bytes
|
|
193
|
+
|
|
194
|
+
return private_bytes, public_bytes
|
|
195
|
+
|
|
196
|
+
def load_keypair(self, private_key: bytes, public_key: bytes) -> None:
|
|
197
|
+
"""Load existing keypair."""
|
|
198
|
+
self._private_key = private_key
|
|
199
|
+
self._public_key = public_key
|
|
200
|
+
|
|
201
|
+
def sign_content(self, content: str) -> Tuple[str, str]:
|
|
202
|
+
"""
|
|
203
|
+
Sign content with private key.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Tuple of (signature_base64, content_hash)
|
|
207
|
+
"""
|
|
208
|
+
if self._private_key is None:
|
|
209
|
+
raise ValueError("No private key loaded. Call generate_keypair() first.")
|
|
210
|
+
|
|
211
|
+
content_bytes = content.encode('utf-8')
|
|
212
|
+
content_hash = hashlib.sha256(content_bytes).hexdigest()
|
|
213
|
+
|
|
214
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
215
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
216
|
+
private_key = Ed25519PrivateKey.from_private_bytes(self._private_key)
|
|
217
|
+
signature = private_key.sign(content_bytes)
|
|
218
|
+
else:
|
|
219
|
+
signing_key = nacl.signing.SigningKey(self._private_key)
|
|
220
|
+
signed = signing_key.sign(content_bytes)
|
|
221
|
+
signature = signed.signature
|
|
222
|
+
|
|
223
|
+
signature_b64 = base64.b64encode(signature).decode('ascii')
|
|
224
|
+
return signature_b64, content_hash
|
|
225
|
+
|
|
226
|
+
def verify_signature(
|
|
227
|
+
self,
|
|
228
|
+
content: str,
|
|
229
|
+
signature_b64: str,
|
|
230
|
+
public_key: bytes
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""Verify Ed25519 signature."""
|
|
233
|
+
try:
|
|
234
|
+
content_bytes = content.encode('utf-8')
|
|
235
|
+
signature = base64.b64decode(signature_b64)
|
|
236
|
+
|
|
237
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
238
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
239
|
+
pub_key = Ed25519PublicKey.from_public_bytes(public_key)
|
|
240
|
+
pub_key.verify(signature, content_bytes)
|
|
241
|
+
return True
|
|
242
|
+
else:
|
|
243
|
+
verify_key = nacl.signing.VerifyKey(public_key)
|
|
244
|
+
verify_key.verify(content_bytes, signature)
|
|
245
|
+
return True
|
|
246
|
+
except Exception:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def is_trusted_issuer(self, domain: str) -> bool:
|
|
250
|
+
"""Check if domain is in trusted issuers whitelist."""
|
|
251
|
+
# Direct match
|
|
252
|
+
if domain in self.trusted_issuers:
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
# Suffix match for government domains
|
|
256
|
+
for trusted in self.trusted_issuers:
|
|
257
|
+
if trusted.startswith('.') and domain.endswith(trusted):
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def create_signed_fact(
|
|
263
|
+
self,
|
|
264
|
+
claim: str,
|
|
265
|
+
source_url: str,
|
|
266
|
+
source_content: str,
|
|
267
|
+
issuer: str
|
|
268
|
+
) -> SignedFact:
|
|
269
|
+
"""Create a signed fact with full verification metadata."""
|
|
270
|
+
if self._private_key is None:
|
|
271
|
+
raise ValueError("No private key loaded.")
|
|
272
|
+
|
|
273
|
+
source_hash = hashlib.sha256(source_content.encode()).hexdigest()
|
|
274
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
275
|
+
|
|
276
|
+
# Sign the claim + source_hash
|
|
277
|
+
signing_content = f"{claim}|{source_hash}|{timestamp}"
|
|
278
|
+
signature, _ = self.sign_content(signing_content)
|
|
279
|
+
|
|
280
|
+
public_key_b64 = base64.b64encode(self._public_key).decode('ascii')
|
|
281
|
+
|
|
282
|
+
# Calculate confidence
|
|
283
|
+
base_confidence = 0.8 # Base for signed content
|
|
284
|
+
if self.is_trusted_issuer(issuer):
|
|
285
|
+
base_confidence = 0.95
|
|
286
|
+
|
|
287
|
+
return SignedFact(
|
|
288
|
+
claim=claim,
|
|
289
|
+
source_url=source_url,
|
|
290
|
+
source_hash=source_hash,
|
|
291
|
+
issuer=issuer,
|
|
292
|
+
issuer_pubkey=f"ed25519:{public_key_b64}",
|
|
293
|
+
signature=signature,
|
|
294
|
+
timestamp=timestamp,
|
|
295
|
+
confidence=base_confidence
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def verify_fact(self, fact: SignedFact) -> VerificationResult:
|
|
299
|
+
"""Verify a signed fact."""
|
|
300
|
+
# Extract public key from fact
|
|
301
|
+
if not fact.issuer_pubkey.startswith("ed25519:"):
|
|
302
|
+
return VerificationResult(
|
|
303
|
+
verified=False,
|
|
304
|
+
content_hash=fact.source_hash,
|
|
305
|
+
signature=fact.signature,
|
|
306
|
+
issuer=fact.issuer,
|
|
307
|
+
issuer_pubkey=fact.issuer_pubkey,
|
|
308
|
+
timestamp=fact.timestamp,
|
|
309
|
+
confidence=0.0,
|
|
310
|
+
error="Invalid public key format"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
pubkey_b64 = fact.issuer_pubkey[8:] # Remove "ed25519:" prefix
|
|
315
|
+
public_key = base64.b64decode(pubkey_b64)
|
|
316
|
+
|
|
317
|
+
# Reconstruct signing content
|
|
318
|
+
signing_content = f"{fact.claim}|{fact.source_hash}|{fact.timestamp}"
|
|
319
|
+
|
|
320
|
+
verified = self.verify_signature(signing_content, fact.signature, public_key)
|
|
321
|
+
|
|
322
|
+
# Calculate confidence
|
|
323
|
+
confidence = 0.0
|
|
324
|
+
if verified:
|
|
325
|
+
confidence = 0.8
|
|
326
|
+
if self.is_trusted_issuer(fact.issuer):
|
|
327
|
+
confidence = 0.95
|
|
328
|
+
|
|
329
|
+
result = VerificationResult(
|
|
330
|
+
verified=verified,
|
|
331
|
+
content_hash=fact.source_hash,
|
|
332
|
+
signature=fact.signature,
|
|
333
|
+
issuer=fact.issuer,
|
|
334
|
+
issuer_pubkey=fact.issuer_pubkey,
|
|
335
|
+
timestamp=fact.timestamp,
|
|
336
|
+
confidence=confidence,
|
|
337
|
+
error=None if verified else "Signature verification failed"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
self.verification_ledger.append(result)
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
return VerificationResult(
|
|
345
|
+
verified=False,
|
|
346
|
+
content_hash=fact.source_hash,
|
|
347
|
+
signature=fact.signature,
|
|
348
|
+
issuer=fact.issuer,
|
|
349
|
+
issuer_pubkey=fact.issuer_pubkey,
|
|
350
|
+
timestamp=fact.timestamp,
|
|
351
|
+
confidence=0.0,
|
|
352
|
+
error=str(e)
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def verify_citation_chain(self, chain: CitationChain) -> Tuple[bool, float]:
|
|
356
|
+
"""
|
|
357
|
+
Verify entire citation chain integrity.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Tuple of (all_verified, aggregate_confidence)
|
|
361
|
+
"""
|
|
362
|
+
if not chain.facts:
|
|
363
|
+
return False, 0.0
|
|
364
|
+
|
|
365
|
+
all_verified = True
|
|
366
|
+
total_confidence = 0.0
|
|
367
|
+
|
|
368
|
+
for i, fact in enumerate(chain.facts):
|
|
369
|
+
result = self.verify_fact(fact)
|
|
370
|
+
|
|
371
|
+
if not result.verified:
|
|
372
|
+
all_verified = False
|
|
373
|
+
|
|
374
|
+
total_confidence += result.confidence
|
|
375
|
+
|
|
376
|
+
# Verify chain linkage
|
|
377
|
+
if i > 0:
|
|
378
|
+
expected_parent = f"chain:{chain.chain_id}:fact:{i-1}"
|
|
379
|
+
if fact.parent_citation != expected_parent:
|
|
380
|
+
all_verified = False
|
|
381
|
+
|
|
382
|
+
aggregate_confidence = total_confidence / len(chain.facts)
|
|
383
|
+
chain.integrity_verified = all_verified
|
|
384
|
+
|
|
385
|
+
return all_verified, aggregate_confidence
|
|
386
|
+
|
|
387
|
+
def get_verification_ledger(self) -> List[Dict]:
|
|
388
|
+
"""Get verification ledger as list of dicts."""
|
|
389
|
+
return [asdict(r) for r in self.verification_ledger]
|
|
390
|
+
|
|
391
|
+
def sign_ledger(self) -> str:
|
|
392
|
+
"""Sign the entire verification ledger."""
|
|
393
|
+
ledger_json = json.dumps(self.get_verification_ledger(), sort_keys=True)
|
|
394
|
+
signature, _ = self.sign_content(ledger_json)
|
|
395
|
+
return signature
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# Convenience functions for standalone usage
|
|
399
|
+
def generate_keypair() -> Tuple[str, str]:
|
|
400
|
+
"""Generate keypair and return as base64 strings."""
|
|
401
|
+
verifier = Ed25519Verifier()
|
|
402
|
+
private_bytes, public_bytes = verifier.generate_keypair()
|
|
403
|
+
return (
|
|
404
|
+
base64.b64encode(private_bytes).decode('ascii'),
|
|
405
|
+
base64.b64encode(public_bytes).decode('ascii')
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def sign_content(content: str, private_key_b64: str) -> str:
|
|
410
|
+
"""Sign content with base64-encoded private key."""
|
|
411
|
+
verifier = Ed25519Verifier()
|
|
412
|
+
private_bytes = base64.b64decode(private_key_b64)
|
|
413
|
+
# Derive public key from private (first 32 bytes are seed)
|
|
414
|
+
if CRYPTO_BACKEND == "cryptography":
|
|
415
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
416
|
+
pk = Ed25519PrivateKey.from_private_bytes(private_bytes)
|
|
417
|
+
pub_bytes = pk.public_key().public_bytes(
|
|
418
|
+
encoding=serialization.Encoding.Raw,
|
|
419
|
+
format=serialization.PublicFormat.Raw
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
signing_key = nacl.signing.SigningKey(private_bytes)
|
|
423
|
+
pub_bytes = bytes(signing_key.verify_key)
|
|
424
|
+
|
|
425
|
+
verifier.load_keypair(private_bytes, pub_bytes)
|
|
426
|
+
signature, _ = verifier.sign_content(content)
|
|
427
|
+
return signature
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def verify_content(content: str, signature_b64: str, public_key_b64: str) -> bool:
|
|
431
|
+
"""Verify content signature with base64-encoded public key."""
|
|
432
|
+
verifier = Ed25519Verifier()
|
|
433
|
+
public_bytes = base64.b64decode(public_key_b64)
|
|
434
|
+
return verifier.verify_signature(content, signature_b64, public_bytes)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Node.js Implementation
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
/**
|
|
441
|
+
* Ed25519 Verification Module for GOAP Research
|
|
442
|
+
*
|
|
443
|
+
* Requirements:
|
|
444
|
+
* npm install @noble/ed25519
|
|
445
|
+
*/
|
|
446
|
+
|
|
447
|
+
import * as ed from '@noble/ed25519';
|
|
448
|
+
import { sha512 } from '@noble/hashes/sha512';
|
|
449
|
+
import { createHash } from 'crypto';
|
|
450
|
+
|
|
451
|
+
// Enable sync methods
|
|
452
|
+
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Generate Ed25519 keypair
|
|
456
|
+
* @returns {Promise<{privateKey: Uint8Array, publicKey: Uint8Array}>}
|
|
457
|
+
*/
|
|
458
|
+
async function generateKeypair() {
|
|
459
|
+
const privateKey = ed.utils.randomPrivateKey();
|
|
460
|
+
const publicKey = await ed.getPublicKeyAsync(privateKey);
|
|
461
|
+
return { privateKey, publicKey };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Sign content
|
|
466
|
+
* @param {string} content - Content to sign
|
|
467
|
+
* @param {Uint8Array} privateKey - Private key
|
|
468
|
+
* @returns {Promise<{signature: string, contentHash: string}>}
|
|
469
|
+
*/
|
|
470
|
+
async function signContent(content, privateKey) {
|
|
471
|
+
const contentBytes = new TextEncoder().encode(content);
|
|
472
|
+
const contentHash = createHash('sha256').update(contentBytes).digest('hex');
|
|
473
|
+
const signature = await ed.signAsync(contentBytes, privateKey);
|
|
474
|
+
return {
|
|
475
|
+
signature: Buffer.from(signature).toString('base64'),
|
|
476
|
+
contentHash
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Verify signature
|
|
482
|
+
* @param {string} content - Original content
|
|
483
|
+
* @param {string} signatureB64 - Base64 signature
|
|
484
|
+
* @param {Uint8Array} publicKey - Public key
|
|
485
|
+
* @returns {Promise<boolean>}
|
|
486
|
+
*/
|
|
487
|
+
async function verifySignature(content, signatureB64, publicKey) {
|
|
488
|
+
try {
|
|
489
|
+
const contentBytes = new TextEncoder().encode(content);
|
|
490
|
+
const signature = Buffer.from(signatureB64, 'base64');
|
|
491
|
+
return await ed.verifyAsync(signature, contentBytes, publicKey);
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create signed fact
|
|
499
|
+
*/
|
|
500
|
+
async function createSignedFact(claim, sourceUrl, sourceContent, issuer, privateKey, publicKey) {
|
|
501
|
+
const sourceHash = createHash('sha256')
|
|
502
|
+
.update(sourceContent)
|
|
503
|
+
.digest('hex');
|
|
504
|
+
|
|
505
|
+
const timestamp = new Date().toISOString();
|
|
506
|
+
const signingContent = `${claim}|${sourceHash}|${timestamp}`;
|
|
507
|
+
const { signature } = await signContent(signingContent, privateKey);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
claim,
|
|
511
|
+
source_url: sourceUrl,
|
|
512
|
+
source_hash: sourceHash,
|
|
513
|
+
issuer,
|
|
514
|
+
issuer_pubkey: `ed25519:${Buffer.from(publicKey).toString('base64')}`,
|
|
515
|
+
signature,
|
|
516
|
+
timestamp,
|
|
517
|
+
parent_citation: null,
|
|
518
|
+
confidence: 0.8
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Verify signed fact
|
|
524
|
+
*/
|
|
525
|
+
async function verifyFact(fact) {
|
|
526
|
+
const pubkeyB64 = fact.issuer_pubkey.replace('ed25519:', '');
|
|
527
|
+
const publicKey = Buffer.from(pubkeyB64, 'base64');
|
|
528
|
+
|
|
529
|
+
const signingContent = `${fact.claim}|${fact.source_hash}|${fact.timestamp}`;
|
|
530
|
+
const verified = await verifySignature(signingContent, fact.signature, publicKey);
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
verified,
|
|
534
|
+
content_hash: fact.source_hash,
|
|
535
|
+
signature: fact.signature,
|
|
536
|
+
issuer: fact.issuer,
|
|
537
|
+
timestamp: fact.timestamp,
|
|
538
|
+
confidence: verified ? 0.8 : 0.0,
|
|
539
|
+
error: verified ? null : 'Signature verification failed'
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export {
|
|
544
|
+
generateKeypair,
|
|
545
|
+
signContent,
|
|
546
|
+
verifySignature,
|
|
547
|
+
createSignedFact,
|
|
548
|
+
verifyFact
|
|
549
|
+
};
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Trusted Issuer Management
|
|
553
|
+
|
|
554
|
+
### Adding Custom Trusted Issuers
|
|
555
|
+
|
|
556
|
+
```python
|
|
557
|
+
verifier = Ed25519Verifier()
|
|
558
|
+
|
|
559
|
+
# Add organization's public key
|
|
560
|
+
verifier.trusted_issuers["mycompany.com"] = "ed25519:base64_encoded_pubkey"
|
|
561
|
+
|
|
562
|
+
# Add academic institution
|
|
563
|
+
verifier.trusted_issuers["mit.edu"] = "ed25519:mit_pubkey"
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Fetching Public Keys
|
|
567
|
+
|
|
568
|
+
In production, public keys should be fetched from:
|
|
569
|
+
1. **DNS TXT records** - `_ed25519.domain.com`
|
|
570
|
+
2. **Well-known endpoints** - `/.well-known/ed25519-keys.json`
|
|
571
|
+
3. **Keyservers** - Similar to PGP keyservers
|
|
572
|
+
|
|
573
|
+
Example DNS lookup:
|
|
574
|
+
```bash
|
|
575
|
+
dig TXT _ed25519.reuters.com
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Security Considerations
|
|
579
|
+
|
|
580
|
+
### Key Management
|
|
581
|
+
- **Never commit private keys** to repositories
|
|
582
|
+
- Use environment variables or secure vaults
|
|
583
|
+
- Rotate keys periodically
|
|
584
|
+
- Maintain key revocation lists
|
|
585
|
+
|
|
586
|
+
### Verification Limitations
|
|
587
|
+
- Ed25519 proves who signed, not truthfulness
|
|
588
|
+
- Trust still depends on issuer integrity
|
|
589
|
+
- Signatures don't prevent plagiarism
|
|
590
|
+
- Timestamps can be backdated by signer
|
|
591
|
+
|
|
592
|
+
### Attack Vectors
|
|
593
|
+
| Attack | Mitigation |
|
|
594
|
+
|--------|------------|
|
|
595
|
+
| Key compromise | Regular rotation, revocation checks |
|
|
596
|
+
| Replay attacks | Include timestamps, nonces |
|
|
597
|
+
| Man-in-the-middle | TLS + signature verification |
|
|
598
|
+
| Fake issuers | Strict whitelist management |
|
|
599
|
+
|
|
600
|
+
## Integration with GOAP Actions
|
|
601
|
+
|
|
602
|
+
### New Actions Added
|
|
603
|
+
|
|
604
|
+
| Action | Input | Output | Verification |
|
|
605
|
+
|--------|-------|--------|--------------|
|
|
606
|
+
| `configure_trusted_issuers` | issuer list | whitelist_active | None |
|
|
607
|
+
| `fetch_signed_source` | URL + pubkey | signed_content | Ed25519 |
|
|
608
|
+
| `sign_extracted_facts` | facts + privkey | signed_facts | Self-sig |
|
|
609
|
+
| `verify_claim_cryptographic` | claim + sig | verified | Ed25519 |
|
|
610
|
+
| `build_citation_chain` | facts | chain | Chain hash |
|
|
611
|
+
| `verify_citation_chain` | chain | verified_chain | Chain sigs |
|
|
612
|
+
| `generate_signed_report` | report + privkey | signed_report | Ed25519 |
|
|
613
|
+
|
|
614
|
+
### Cost Adjustments
|
|
615
|
+
|
|
616
|
+
Verification adds overhead:
|
|
617
|
+
- `fetch_signed_source`: +1 cost (verification time)
|
|
618
|
+
- `verify_claim_cryptographic`: +1 cost
|
|
619
|
+
- `verify_citation_chain`: +2 cost (full chain traversal)
|
|
620
|
+
|
|
621
|
+
But provides verification bonus in confidence calculation.
|
|
622
|
+
|
|
623
|
+
## CLI Reference
|
|
624
|
+
|
|
625
|
+
### Using with goalie
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
# Verified search
|
|
629
|
+
goalie search "query" --verify --strict-verify
|
|
630
|
+
|
|
631
|
+
# Check issuer trust
|
|
632
|
+
goalie trust check "reuters.com"
|
|
633
|
+
|
|
634
|
+
# Add trusted issuer
|
|
635
|
+
goalie trust add "mycompany.com" --pubkey "ed25519:..."
|
|
636
|
+
|
|
637
|
+
# Verify existing fact
|
|
638
|
+
goalie verify --fact '{"claim":"...", "signature":"..."}'
|
|
639
|
+
|
|
640
|
+
# Generate research keypair
|
|
641
|
+
goalie keys generate --output ./research-keys.json
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
## Appendix: Test Vectors
|
|
645
|
+
|
|
646
|
+
### Test Keypair
|
|
647
|
+
```
|
|
648
|
+
Private (seed): 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
|
|
649
|
+
Public: d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Test Signature
|
|
653
|
+
```
|
|
654
|
+
Message: "Test message for GOAP research"
|
|
655
|
+
Signature: e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Verify: `Ed25519.verify(signature, message, public_key) == true`
|