@dzhechkov/p-replicator 1.5.7 → 1.5.10
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/README/ru/html/build.js +11 -3
- package/package.json +1 -1
- package/src/commands/remove.js +16 -2
- package/src/commands/update.js +7 -0
- package/templates/.claude/rules/replicate-pipeline.md +16 -0
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-tests.sh +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +1 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +1 -1
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +81 -371
- package/templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md +58 -624
- package/templates/.claude/skills/goap-research-ed25519/references/research-actions.md +61 -506
- package/templates/.claude/skills/goap-research-ed25519/references/source-evaluation.md +42 -543
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +427 -448
- package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +4 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +112 -0
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +1 -1
- package/templates/.claude/skills/pipeline-forge/SKILL.md +5 -1
- package/templates/.claude/skills/problem-solver-enhanced/SKILL.md +3 -0
- package/templates/.claude/skills/requirements-validator/SKILL.md +2 -2
- package/templates/.claude/skills/reverse-engineering-unicorn/SKILL.md +7 -2
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/01-intelligence.md +15 -29
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/02-product-customers.md +4 -4
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md +9 -16
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/04-business-finance.md +4 -3
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/05-growth-engine.md +3 -2
|
@@ -1,658 +1,92 @@
|
|
|
1
1
|
# Ed25519 Verification Reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ed25519 is used here for provenance and tamper-evidence under pinned trusted-issuer keys. It proves that a specific key signed a specific canonical message and that the signed bytes were not altered. It does not prove the claim is true.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Safe Install
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
7
|
+
Prefer an isolated Python environment:
|
|
40
8
|
|
|
9
|
+
```bash
|
|
10
|
+
python3 -m venv .venv
|
|
11
|
+
.venv/bin/pip install cryptography
|
|
41
12
|
```
|
|
42
|
-
SIGNING (by source/researcher):
|
|
43
|
-
content → SHA-512(content) → Ed25519.sign(hash, private_key) → signature
|
|
44
13
|
|
|
45
|
-
|
|
46
|
-
(content, signature, public_key) → Ed25519.verify() → true/false
|
|
47
|
-
```
|
|
14
|
+
`pynacl` is also supported. Avoid mutating system Python; `--break-system-packages` is only a last-resort local workaround when you understand the risk.
|
|
48
15
|
|
|
49
|
-
##
|
|
16
|
+
## Trust Bootstrap
|
|
50
17
|
|
|
51
|
-
|
|
18
|
+
Issuer trust requires an explicit pinned public key:
|
|
52
19
|
|
|
53
20
|
```python
|
|
54
|
-
|
|
55
|
-
|
|
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",
|
|
21
|
+
verifier = Ed25519Verifier(
|
|
22
|
+
trusted_issuers={
|
|
23
|
+
"example.org": {"pubkey_b64": "base64-ed25519-public-key", "status": "active"}
|
|
158
24
|
}
|
|
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)
|
|
25
|
+
)
|
|
435
26
|
```
|
|
436
27
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
```javascript
|
|
440
|
-
/**
|
|
441
|
-
* Ed25519 Verification Module for GOAP Research
|
|
442
|
-
*
|
|
443
|
-
* Requirements:
|
|
444
|
-
* npm install @noble/ed25519
|
|
445
|
-
*/
|
|
28
|
+
The embedded `issuer_pubkey` in a fact is an untrusted hint. For issuer-signed facts it must match the pinned key for the claimed issuer. Unknown issuer, missing pin, revoked key, malformed key, embedded-key mismatch, or invalid signature all reject with confidence `0.0`.
|
|
446
29
|
|
|
447
|
-
|
|
448
|
-
import { sha512 } from '@noble/hashes/sha512';
|
|
449
|
-
import { createHash } from 'crypto';
|
|
30
|
+
## Canonical Message
|
|
450
31
|
|
|
451
|
-
|
|
452
|
-
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
32
|
+
Both signing and verification use deterministic JSON containing:
|
|
453
33
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const publicKey = await ed.getPublicKeyAsync(privateKey);
|
|
461
|
-
return { privateKey, publicKey };
|
|
462
|
-
}
|
|
34
|
+
- `issuer`
|
|
35
|
+
- `source_url`
|
|
36
|
+
- `claim`
|
|
37
|
+
- `source_hash`
|
|
38
|
+
- `timestamp`
|
|
39
|
+
- `research_context`
|
|
463
40
|
|
|
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
|
-
}
|
|
41
|
+
The code signs the raw canonical message bytes. Do not pre-hash with SHA-512; Ed25519 performs its own internal hashing. `source_hash` is a SHA-256 digest of source content and is one field inside the signed message.
|
|
479
42
|
|
|
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
|
-
}
|
|
43
|
+
## Trust Classes and Confidence
|
|
496
44
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
}
|
|
45
|
+
| Trust class | Verification path | Confidence |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `ISSUER_SIGNED` | Signature verifies under active pinned key for `issuer` | capped at `0.95` |
|
|
48
|
+
| `SELF_ATTESTED` | Researcher signature verifies under embedded key | capped at `0.60` |
|
|
49
|
+
| `UNVERIFIED` | Unknown, revoked, mismatched, malformed, or invalid | `0.0` |
|
|
521
50
|
|
|
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
|
-
}
|
|
51
|
+
Invalid signatures are rejected. They are not downgraded to a `0.5` confidence.
|
|
542
52
|
|
|
543
|
-
|
|
544
|
-
generateKeypair,
|
|
545
|
-
signContent,
|
|
546
|
-
verifySignature,
|
|
547
|
-
createSignedFact,
|
|
548
|
-
verifyFact
|
|
549
|
-
};
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
## Trusted Issuer Management
|
|
53
|
+
## Citation Chains
|
|
553
54
|
|
|
554
|
-
|
|
55
|
+
Each child fact stores the actual content hash of the previous fact:
|
|
555
56
|
|
|
556
57
|
```python
|
|
557
|
-
|
|
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"
|
|
58
|
+
child.parent_hash = sha256(canonical_message(parent_fact))
|
|
564
59
|
```
|
|
565
60
|
|
|
566
|
-
|
|
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 |
|
|
61
|
+
`chain_signature` signs the ordered list of fact hashes. Verification recomputes parent hashes and verifies the chain signature. Reordering or substituting facts fails.
|
|
613
62
|
|
|
614
|
-
|
|
63
|
+
## Replay and Revocation Limits
|
|
615
64
|
|
|
616
|
-
|
|
617
|
-
- `fetch_signed_source`: +1 cost (verification time)
|
|
618
|
-
- `verify_claim_cryptographic`: +1 cost
|
|
619
|
-
- `verify_citation_chain`: +2 cost (full chain traversal)
|
|
65
|
+
Revocation is enforced for configured pinned keys through `status: active|revoked`. Online revocation fetching is not implemented.
|
|
620
66
|
|
|
621
|
-
|
|
67
|
+
Replay protection is limited to binding a caller-provided `research_context` or nonce into the signed message. A persistent nonce ledger is not implemented, so replay detection across runs is not guaranteed.
|
|
622
68
|
|
|
623
|
-
##
|
|
69
|
+
## Minimal Usage
|
|
624
70
|
|
|
625
|
-
|
|
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
|
-
```
|
|
71
|
+
```python
|
|
72
|
+
from ed25519_verifier import Ed25519Verifier
|
|
651
73
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
74
|
+
issuer = Ed25519Verifier(auto_generate_keypair=True)
|
|
75
|
+
verifier = Ed25519Verifier(
|
|
76
|
+
trusted_issuers={
|
|
77
|
+
"example.org": {"pubkey_b64": issuer.get_public_key_b64(), "status": "active"}
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
fact = issuer.create_issuer_signed_fact(
|
|
82
|
+
claim="Example claim",
|
|
83
|
+
source_url="https://example.org/source",
|
|
84
|
+
source_content="source bytes",
|
|
85
|
+
issuer="example.org",
|
|
86
|
+
research_context="run-2026-07-06",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
result = verifier.verify_fact(fact)
|
|
90
|
+
assert result.verified
|
|
91
|
+
assert result.confidence == 0.95
|
|
656
92
|
```
|
|
657
|
-
|
|
658
|
-
Verify: `Ed25519.verify(signature, message, public_key) == true`
|