@dzhechkov/p-replicator 1.5.8 → 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.
Files changed (23) hide show
  1. package/README/ru/html/build.js +11 -3
  2. package/package.json +1 -1
  3. package/src/commands/remove.js +16 -2
  4. package/src/commands/update.js +7 -0
  5. package/templates/.claude/skills/brutal-honesty-review/scripts/assess-tests.sh +2 -2
  6. package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +1 -1
  7. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +1 -1
  8. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +78 -371
  9. package/templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md +58 -624
  10. package/templates/.claude/skills/goap-research-ed25519/references/research-actions.md +61 -506
  11. package/templates/.claude/skills/goap-research-ed25519/references/source-evaluation.md +42 -543
  12. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +427 -448
  13. package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +4 -4
  14. package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +112 -0
  15. package/templates/.claude/skills/knowledge-extractor/SKILL.md +1 -1
  16. package/templates/.claude/skills/pipeline-forge/SKILL.md +5 -1
  17. package/templates/.claude/skills/requirements-validator/SKILL.md +2 -2
  18. package/templates/.claude/skills/reverse-engineering-unicorn/SKILL.md +4 -2
  19. package/templates/.claude/skills/reverse-engineering-unicorn/modules/01-intelligence.md +15 -29
  20. package/templates/.claude/skills/reverse-engineering-unicorn/modules/02-product-customers.md +4 -4
  21. package/templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md +9 -16
  22. package/templates/.claude/skills/reverse-engineering-unicorn/modules/04-business-finance.md +4 -3
  23. package/templates/.claude/skills/reverse-engineering-unicorn/modules/05-growth-engine.md +3 -2
@@ -1,50 +1,66 @@
1
1
  #!/usr/bin/env python3
2
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
3
+ Ed25519 provenance verifier for GOAP research.
4
+
5
+ Ed25519 provides provenance and tamper-evidence under pinned trusted-issuer
6
+ keys. It proves who signed the canonical message and that signed bytes were not
7
+ altered. It does not prove that a claim is true.
8
+
9
+ Install a backend in an isolated environment:
10
+ python3 -m venv .venv
11
+ .venv/bin/pip install cryptography
12
+ # or: .venv/bin/pip install pynacl
13
+
14
+ Avoid mutating system Python. Use --break-system-packages only as a last-resort
15
+ local workaround when you understand the system-integrity risk.
17
16
  """
18
17
 
18
+ import base64
19
19
  import hashlib
20
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
21
  import os
22
+ from dataclasses import asdict, dataclass, field
23
+ from datetime import datetime
24
+ from typing import Any, Dict, List, Optional, Tuple
26
25
 
27
- # Try to import cryptography library (preferred)
28
26
  try:
27
+ from cryptography.exceptions import InvalidSignature
28
+ from cryptography.hazmat.primitives import serialization
29
29
  from cryptography.hazmat.primitives.asymmetric.ed25519 import (
30
- Ed25519PrivateKey, Ed25519PublicKey
30
+ Ed25519PrivateKey,
31
+ Ed25519PublicKey,
31
32
  )
32
- from cryptography.hazmat.primitives import serialization
33
- from cryptography.exceptions import InvalidSignature
33
+
34
34
  CRYPTO_BACKEND = "cryptography"
35
35
  except ImportError:
36
36
  try:
37
- import nacl.signing
38
- import nacl.encoding
39
37
  import nacl.exceptions
38
+ import nacl.signing
39
+
40
40
  CRYPTO_BACKEND = "pynacl"
41
41
  except ImportError:
42
42
  CRYPTO_BACKEND = None
43
43
 
44
44
 
45
+ TRUST_CLASS_ISSUER_SIGNED = "ISSUER_SIGNED"
46
+ TRUST_CLASS_SELF_ATTESTED = "SELF_ATTESTED"
47
+ TRUST_CLASS_UNVERIFIED = "UNVERIFIED"
48
+
49
+
50
+ @dataclass
51
+ class PinnedKey:
52
+ """Pinned issuer key. Only active pins can grant issuer-grade trust."""
53
+
54
+ pubkey_b64: str
55
+ status: str = "active"
56
+ added_at: Optional[str] = None
57
+ not_after: Optional[str] = None
58
+
59
+
45
60
  @dataclass
46
61
  class VerificationResult:
47
62
  """Result of a verification operation."""
63
+
48
64
  verified: bool
49
65
  content_hash: str
50
66
  signature: str
@@ -52,15 +68,17 @@ class VerificationResult:
52
68
  issuer_pubkey: str
53
69
  timestamp: str
54
70
  confidence: float
71
+ trust_class: str = TRUST_CLASS_UNVERIFIED
55
72
  error: Optional[str] = None
56
-
57
- def to_dict(self) -> Dict:
73
+
74
+ def to_dict(self) -> Dict[str, Any]:
58
75
  return asdict(self)
59
76
 
60
77
 
61
78
  @dataclass
62
79
  class SignedFact:
63
- """A fact with cryptographic signature."""
80
+ """A signed fact. Signatures cover issuer, source URL, claim, hash, time, context."""
81
+
64
82
  claim: str
65
83
  source_url: str
66
84
  source_hash: str
@@ -69,594 +87,555 @@ class SignedFact:
69
87
  signature: str
70
88
  timestamp: str
71
89
  parent_citation: Optional[str] = None
90
+ parent_hash: Optional[str] = None
72
91
  confidence: float = 0.0
92
+ trust_class: str = TRUST_CLASS_SELF_ATTESTED
93
+ research_context: Optional[str] = None
73
94
  metadata: Dict[str, Any] = field(default_factory=dict)
74
-
75
- def to_dict(self) -> Dict:
95
+
96
+ def to_dict(self) -> Dict[str, Any]:
76
97
  return asdict(self)
77
-
98
+
78
99
  def to_json(self) -> str:
79
- return json.dumps(self.to_dict(), indent=2)
80
-
100
+ return json.dumps(self.to_dict(), indent=2, sort_keys=True)
101
+
81
102
  @classmethod
82
- def from_dict(cls, data: Dict) -> 'SignedFact':
83
- return cls(**data)
84
-
103
+ def from_dict(cls, data: Dict[str, Any]) -> "SignedFact":
104
+ allowed = {field_name for field_name in cls.__dataclass_fields__}
105
+ cleaned = {k: v for k, v in data.items() if k in allowed}
106
+ cleaned.setdefault("trust_class", TRUST_CLASS_SELF_ATTESTED)
107
+ cleaned.setdefault("metadata", {})
108
+ return cls(**cleaned)
109
+
85
110
  @classmethod
86
- def from_json(cls, json_str: str) -> 'SignedFact':
111
+ def from_json(cls, json_str: str) -> "SignedFact":
87
112
  return cls.from_dict(json.loads(json_str))
88
113
 
89
114
 
90
115
  @dataclass
91
116
  class CitationChain:
92
- """Chain of signed citations with integrity verification."""
117
+ """Chain of signed citations with hash links and a verifiable chain signature."""
118
+
93
119
  chain_id: str
94
120
  facts: List[SignedFact] = field(default_factory=list)
95
121
  chain_signature: Optional[str] = None
96
122
  chain_hash: Optional[str] = None
97
123
  integrity_verified: bool = False
98
124
  created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat() + "Z")
99
-
125
+
100
126
  def add_fact(self, fact: SignedFact) -> None:
101
- """Add fact to chain with automatic parent linking."""
127
+ """Add fact to chain with automatic parent hash linking."""
102
128
  if self.facts:
103
- fact.parent_citation = f"chain:{self.chain_id}:fact:{len(self.facts)-1}"
129
+ fact.parent_hash = fact_content_hash(self.facts[-1])
130
+ fact.parent_citation = f"chain:{self.chain_id}:fact:{len(self.facts) - 1}"
104
131
  self.facts.append(fact)
105
- self.chain_hash = None # Invalidate cached hash
106
-
132
+ self.chain_hash = None
133
+
134
+ def ordered_hashes(self) -> List[str]:
135
+ return [fact_content_hash(fact) for fact in self.facts]
136
+
107
137
  def get_chain_hash(self) -> str:
108
- """Calculate hash of entire chain for signing."""
109
138
  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()
139
+ self.chain_hash = hashlib.sha256(
140
+ canonical_json({"chain_id": self.chain_id, "hashes": self.ordered_hashes()}).encode("utf-8")
141
+ ).hexdigest()
115
142
  return self.chain_hash
116
-
117
- def to_dict(self) -> Dict:
143
+
144
+ def to_dict(self) -> Dict[str, Any]:
118
145
  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
146
+ "chain_id": self.chain_id,
147
+ "facts": [f.to_dict() for f in self.facts],
148
+ "chain_signature": self.chain_signature,
149
+ "chain_hash": self.get_chain_hash(),
150
+ "integrity_verified": self.integrity_verified,
151
+ "created_at": self.created_at,
152
+ }
153
+
154
+
155
+ def canonical_json(data: Dict[str, Any]) -> str:
156
+ """Return deterministic JSON for signatures and hashes."""
157
+ return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
158
+
159
+
160
+ def canonical_fact_message(fact: SignedFact) -> str:
161
+ """Canonical signed message for a fact."""
162
+ return canonical_json(
163
+ {
164
+ "claim": fact.claim,
165
+ "issuer": fact.issuer,
166
+ "research_context": fact.research_context,
167
+ "source_hash": fact.source_hash,
168
+ "source_url": fact.source_url,
169
+ "timestamp": fact.timestamp,
125
170
  }
171
+ )
172
+
173
+
174
+ def fact_content_hash(fact: SignedFact) -> str:
175
+ """Stable hash for chain linkage. Excludes parent links and chain position."""
176
+ return hashlib.sha256(canonical_fact_message(fact).encode("utf-8")).hexdigest()
177
+
178
+
179
+ class PinnedIssuerRegistry:
180
+ """Issuer -> pinned Ed25519 public key registry."""
181
+
182
+ def __init__(self, pins: Optional[Dict[str, Any]] = None):
183
+ self._pins: Dict[str, PinnedKey] = {}
184
+ for issuer, value in (pins or {}).items():
185
+ self.add(issuer, value)
186
+
187
+ def add(self, issuer: str, value: Any, status: str = "active") -> None:
188
+ if isinstance(value, PinnedKey):
189
+ pin = value
190
+ elif isinstance(value, str):
191
+ pin = PinnedKey(pubkey_b64=strip_ed25519_prefix(value), status=status)
192
+ elif isinstance(value, dict):
193
+ pubkey = value.get("pubkey_b64") or value.get("public_key") or value.get("pubkey")
194
+ if pubkey is None:
195
+ raise ValueError(f"Missing pubkey for issuer {issuer}")
196
+ pin = PinnedKey(
197
+ pubkey_b64=strip_ed25519_prefix(pubkey),
198
+ status=value.get("status", status),
199
+ added_at=value.get("added_at"),
200
+ not_after=value.get("not_after"),
201
+ )
202
+ else:
203
+ raise ValueError(f"Issuer {issuer} must pin an Ed25519 public key")
204
+ self._pins[issuer] = pin
205
+
206
+ def remove(self, issuer: str) -> None:
207
+ self._pins.pop(issuer, None)
208
+
209
+ def get(self, issuer: str) -> Optional[PinnedKey]:
210
+ return self._pins.get(issuer)
211
+
212
+ def is_active(self, issuer: str) -> bool:
213
+ pin = self.get(issuer)
214
+ return bool(pin and pin.status == "active")
215
+
216
+ def to_dict(self) -> Dict[str, Dict[str, Any]]:
217
+ return {issuer: asdict(pin) for issuer, pin in self._pins.items()}
218
+
219
+
220
+ def strip_ed25519_prefix(pubkey: str) -> str:
221
+ return pubkey[8:] if pubkey.startswith("ed25519:") else pubkey
222
+
223
+
224
+ def decode_pubkey_b64(pubkey_b64: str) -> bytes:
225
+ return base64.b64decode(strip_ed25519_prefix(pubkey_b64))
126
226
 
127
227
 
128
228
  class Ed25519Verifier:
129
229
  """
130
230
  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
231
+
232
+ DEFAULT_TRUSTED_ISSUERS is intentionally empty. Issuer-grade trust requires
233
+ explicit pinned public keys supplied by the user or calling template.
138
234
  """
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
-
235
+
236
+ DEFAULT_TRUSTED_ISSUERS: Dict[str, Dict[str, str]] = {}
237
+
166
238
  def __init__(
167
239
  self,
168
- trusted_issuers: Optional[Dict[str, Optional[str]]] = None,
240
+ trusted_issuers: Optional[Dict[str, Any]] = None,
169
241
  verification_threshold: float = 0.85,
170
- auto_generate_keypair: bool = False
242
+ auto_generate_keypair: bool = False,
171
243
  ):
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
244
  if CRYPTO_BACKEND is None:
181
245
  raise RuntimeError(
182
- "No cryptographic backend available. Install cryptography or pynacl:\n"
183
- " pip install cryptography --break-system-packages"
246
+ "No cryptographic backend available. Prefer an isolated venv:\n"
247
+ " python3 -m venv .venv && .venv/bin/pip install cryptography\n"
248
+ "or install pynacl in the same venv."
184
249
  )
185
-
186
- self.trusted_issuers = trusted_issuers or self.DEFAULT_TRUSTED_ISSUERS.copy()
250
+
251
+ self.registry = PinnedIssuerRegistry(trusted_issuers or self.DEFAULT_TRUSTED_ISSUERS)
252
+ self.trusted_issuers = self.registry.to_dict()
187
253
  self.verification_threshold = verification_threshold
188
254
  self.verification_ledger: List[VerificationResult] = []
189
255
  self._private_key: Optional[bytes] = None
190
256
  self._public_key: Optional[bytes] = None
191
-
257
+
192
258
  if auto_generate_keypair:
193
259
  self.generate_keypair()
194
-
260
+
195
261
  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
262
  if CRYPTO_BACKEND == "cryptography":
203
263
  private_key = Ed25519PrivateKey.generate()
204
264
  public_key = private_key.public_key()
205
-
206
265
  private_bytes = private_key.private_bytes(
207
266
  encoding=serialization.Encoding.Raw,
208
267
  format=serialization.PrivateFormat.Raw,
209
- encryption_algorithm=serialization.NoEncryption()
268
+ encryption_algorithm=serialization.NoEncryption(),
210
269
  )
211
270
  public_bytes = public_key.public_bytes(
212
271
  encoding=serialization.Encoding.Raw,
213
- format=serialization.PublicFormat.Raw
272
+ format=serialization.PublicFormat.Raw,
214
273
  )
215
- else: # pynacl
274
+ else:
216
275
  signing_key = nacl.signing.SigningKey.generate()
217
276
  private_bytes = bytes(signing_key)
218
277
  public_bytes = bytes(signing_key.verify_key)
219
-
278
+
220
279
  self._private_key = private_bytes
221
280
  self._public_key = public_bytes
222
-
223
281
  return private_bytes, public_bytes
224
-
282
+
225
283
  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
284
  self._private_key = private_key
234
-
235
285
  if public_key is not None:
236
286
  self._public_key = public_key
287
+ elif CRYPTO_BACKEND == "cryptography":
288
+ pk = Ed25519PrivateKey.from_private_bytes(private_key)
289
+ self._public_key = pk.public_key().public_bytes(
290
+ encoding=serialization.Encoding.Raw,
291
+ format=serialization.PublicFormat.Raw,
292
+ )
237
293
  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
-
294
+ signing_key = nacl.signing.SigningKey(private_key)
295
+ self._public_key = bytes(signing_key.verify_key)
296
+
249
297
  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:
298
+ with open(private_path, "rb") as f:
252
299
  private_key = f.read()
253
-
254
300
  public_key = None
255
301
  if public_path:
256
- with open(public_path, 'rb') as f:
302
+ with open(public_path, "rb") as f:
257
303
  public_key = f.read()
258
-
259
304
  self.load_keypair(private_key, public_key)
260
-
305
+
261
306
  def save_keypair_to_files(self, private_path: str, public_path: str) -> None:
262
- """Save keypair to files."""
263
307
  if self._private_key is None or self._public_key is None:
264
308
  raise ValueError("No keypair to save. Generate or load one first.")
265
-
266
- with open(private_path, 'wb') as f:
309
+ with open(private_path, "wb") as f:
267
310
  f.write(self._private_key)
268
- os.chmod(private_path, 0o600) # Restrict private key permissions
269
-
270
- with open(public_path, 'wb') as f:
311
+ os.chmod(private_path, 0o600)
312
+ with open(public_path, "wb") as f:
271
313
  f.write(self._public_key)
272
-
314
+
273
315
  def get_public_key_b64(self) -> str:
274
- """Get public key as base64 string."""
275
316
  if self._public_key is None:
276
317
  raise ValueError("No public key available.")
277
- return base64.b64encode(self._public_key).decode('ascii')
278
-
318
+ return base64.b64encode(self._public_key).decode("ascii")
319
+
279
320
  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
321
  if self._private_key is None:
290
322
  raise ValueError("No private key loaded. Call generate_keypair() first.")
291
-
292
- content_bytes = content.encode('utf-8')
323
+ content_bytes = content.encode("utf-8")
293
324
  content_hash = hashlib.sha256(content_bytes).hexdigest()
294
-
295
325
  if CRYPTO_BACKEND == "cryptography":
296
326
  private_key = Ed25519PrivateKey.from_private_bytes(self._private_key)
297
327
  signature = private_key.sign(content_bytes)
298
328
  else:
299
329
  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
- """
330
+ signature = signing_key.sign(content_bytes).signature
331
+ return base64.b64encode(signature).decode("ascii"), content_hash
332
+
333
+ def verify_signature(self, content: str, signature_b64: str, public_key: bytes) -> bool:
323
334
  try:
324
- content_bytes = content.encode('utf-8')
335
+ content_bytes = content.encode("utf-8")
325
336
  signature = base64.b64decode(signature_b64)
326
-
327
337
  if CRYPTO_BACKEND == "cryptography":
328
- pub_key = Ed25519PublicKey.from_public_bytes(public_key)
329
- pub_key.verify(signature, content_bytes)
330
- return True
338
+ Ed25519PublicKey.from_public_bytes(public_key).verify(signature, content_bytes)
331
339
  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
340
+ nacl.signing.VerifyKey(public_key).verify(content_bytes, signature)
341
+ return True
337
342
  except Exception:
338
343
  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
-
344
+
345
+ def add_trusted_issuer(self, domain: str, public_key_b64: str, status: str = "active") -> None:
346
+ """Pin an issuer public key. A key is required; None is never trusted."""
347
+ self.registry.add(domain, public_key_b64, status=status)
348
+ self.trusted_issuers = self.registry.to_dict()
349
+
362
350
  def remove_trusted_issuer(self, domain: str) -> None:
363
- """Remove domain from trusted issuers."""
364
- self.trusted_issuers.pop(domain, None)
365
-
351
+ self.registry.remove(domain)
352
+ self.trusted_issuers = self.registry.to_dict()
353
+
354
+ def check_key_status(self, issuer: str) -> str:
355
+ pin = self.registry.get(issuer)
356
+ return pin.status if pin else "unknown"
357
+
358
+ def is_trusted_issuer(self, domain: str) -> bool:
359
+ """True only when a domain has an active pinned key."""
360
+ return self.registry.is_active(domain)
361
+
366
362
  def create_signed_fact(
367
363
  self,
368
364
  claim: str,
369
365
  source_url: str,
370
366
  source_content: str,
371
367
  issuer: str,
372
- metadata: Optional[Dict] = None
368
+ metadata: Optional[Dict[str, Any]] = None,
369
+ research_context: Optional[str] = None,
373
370
  ) -> 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()
371
+ """Create a researcher self-attested fact. This never grants issuer trust."""
372
+ if self._private_key is None or self._public_key is None:
373
+ raise ValueError("No keypair loaded.")
374
+
375
+ source_hash = hashlib.sha256(source_content.encode("utf-8")).hexdigest()
391
376
  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(
377
+ public_key_b64 = self.get_public_key_b64()
378
+ fact = SignedFact(
405
379
  claim=claim,
406
380
  source_url=source_url,
407
381
  source_hash=source_hash,
408
382
  issuer=issuer,
409
383
  issuer_pubkey=f"ed25519:{public_key_b64}",
410
- signature=signature,
384
+ signature="",
411
385
  timestamp=timestamp,
412
- confidence=base_confidence,
413
- metadata=metadata or {}
386
+ confidence=0.60,
387
+ trust_class=TRUST_CLASS_SELF_ATTESTED,
388
+ research_context=research_context,
389
+ metadata=metadata or {},
414
390
  )
415
-
391
+ fact.signature, _ = self.sign_content(canonical_fact_message(fact))
392
+ return fact
393
+
394
+ def create_issuer_signed_fact(
395
+ self,
396
+ claim: str,
397
+ source_url: str,
398
+ source_content: str,
399
+ issuer: str,
400
+ metadata: Optional[Dict[str, Any]] = None,
401
+ research_context: Optional[str] = None,
402
+ ) -> SignedFact:
403
+ """Create a fact intended to verify against the active pinned key for issuer."""
404
+ if self._public_key is None:
405
+ raise ValueError("No keypair loaded.")
406
+ source_hash = hashlib.sha256(source_content.encode("utf-8")).hexdigest()
407
+ timestamp = datetime.utcnow().isoformat() + "Z"
408
+ fact = SignedFact(
409
+ claim=claim,
410
+ source_url=source_url,
411
+ source_hash=source_hash,
412
+ issuer=issuer,
413
+ issuer_pubkey=f"ed25519:{self.get_public_key_b64()}",
414
+ signature="",
415
+ timestamp=timestamp,
416
+ confidence=0.95,
417
+ trust_class=TRUST_CLASS_ISSUER_SIGNED,
418
+ research_context=research_context,
419
+ metadata=metadata or {},
420
+ )
421
+ fact.signature, _ = self.sign_content(canonical_fact_message(fact))
422
+ return fact
423
+
424
+ def _result(
425
+ self,
426
+ fact: SignedFact,
427
+ verified: bool,
428
+ confidence: float,
429
+ trust_class: str,
430
+ error: Optional[str],
431
+ ) -> VerificationResult:
432
+ result = VerificationResult(
433
+ verified=verified,
434
+ content_hash=fact.source_hash,
435
+ signature=fact.signature,
436
+ issuer=fact.issuer,
437
+ issuer_pubkey=fact.issuer_pubkey,
438
+ timestamp=fact.timestamp,
439
+ confidence=confidence,
440
+ trust_class=trust_class,
441
+ error=error,
442
+ )
443
+ self.verification_ledger.append(result)
444
+ return result
445
+
416
446
  def verify_fact(self, fact: SignedFact) -> VerificationResult:
417
447
  """
418
448
  Verify a signed fact.
419
-
420
- Args:
421
- fact: SignedFact to verify
422
-
423
- Returns:
424
- VerificationResult with status and confidence
449
+
450
+ ISSUER_SIGNED verifies against the pinned active key for fact.issuer, not
451
+ the embedded key. SELF_ATTESTED verifies against the embedded researcher
452
+ key but is capped at 0.60 and never promoted to issuer trust.
425
453
  """
426
- # Extract public key from fact
427
454
  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
-
455
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Invalid public key format")
456
+
457
+ embedded_pubkey_b64 = strip_ed25519_prefix(fact.issuer_pubkey)
458
+
459
+ if fact.trust_class == TRUST_CLASS_SELF_ATTESTED:
460
+ try:
461
+ public_key = decode_pubkey_b64(embedded_pubkey_b64)
462
+ except Exception:
463
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Invalid embedded public key")
464
+ if self.verify_signature(canonical_fact_message(fact), fact.signature, public_key):
465
+ return self._result(fact, True, min(fact.confidence or 0.60, 0.60), TRUST_CLASS_SELF_ATTESTED, None)
466
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Self-attestation signature failed")
467
+
468
+ pin = self.registry.get(fact.issuer)
469
+ if pin is None:
470
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Unknown issuer or missing pinned key")
471
+ if pin.status != "active":
472
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, f"Issuer key status is {pin.status}")
473
+ if embedded_pubkey_b64 != strip_ed25519_prefix(pin.pubkey_b64):
474
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Embedded public key does not match pinned key")
475
+
441
476
  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
- """
477
+ public_key = decode_pubkey_b64(pin.pubkey_b64)
478
+ except Exception:
479
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Pinned public key is malformed")
480
+
481
+ verified = self.verify_signature(canonical_fact_message(fact), fact.signature, public_key)
482
+ if not verified:
483
+ return self._result(fact, False, 0.0, TRUST_CLASS_UNVERIFIED, "Signature verification failed")
484
+ return self._result(fact, True, min(fact.confidence or 0.95, 0.95), TRUST_CLASS_ISSUER_SIGNED, None)
485
+
486
+ def chain_message(self, chain: CitationChain) -> str:
487
+ return canonical_json({"chain_id": chain.chain_id, "hashes": chain.ordered_hashes()})
488
+
489
+ def sign_chain(self, chain: CitationChain) -> str:
490
+ """Sign the ordered list of fact content hashes."""
491
+ signature, _ = self.sign_content(self.chain_message(chain))
492
+ chain.chain_signature = signature
493
+ chain.chain_hash = chain.get_chain_hash()
494
+ return signature
495
+
496
+ def verify_chain_signature(self, chain: CitationChain, public_key_b64: Optional[str] = None) -> bool:
497
+ if not chain.chain_signature:
498
+ return False
499
+ if public_key_b64 is None:
500
+ if self._public_key is None:
501
+ return False
502
+ public_key = self._public_key
503
+ else:
504
+ try:
505
+ public_key = decode_pubkey_b64(public_key_b64)
506
+ except Exception:
507
+ return False
508
+ return self.verify_signature(self.chain_message(chain), chain.chain_signature, public_key)
509
+
510
+ def verify_citation_chain(
511
+ self,
512
+ chain: CitationChain,
513
+ chain_signer_pubkey_b64: Optional[str] = None,
514
+ ) -> Tuple[bool, float, Optional[str]]:
495
515
  if not chain.facts:
496
516
  return False, 0.0, "Empty chain"
497
-
517
+
498
518
  all_verified = True
499
519
  total_confidence = 0.0
500
- errors = []
501
-
520
+ errors: List[str] = []
521
+
502
522
  for i, fact in enumerate(chain.facts):
503
523
  result = self.verify_fact(fact)
504
-
524
+ total_confidence += result.confidence
505
525
  if not result.verified:
506
526
  all_verified = False
507
527
  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:
528
+ if i == 0:
529
+ if fact.parent_hash:
530
+ all_verified = False
531
+ errors.append("Fact 0: Root fact must not have a parent hash")
532
+ else:
533
+ expected_parent_hash = fact_content_hash(chain.facts[i - 1])
534
+ if fact.parent_hash != expected_parent_hash:
515
535
  all_verified = False
516
- errors.append(f"Fact {i}: Invalid parent citation")
517
-
536
+ errors.append(f"Fact {i}: Invalid parent hash")
537
+
538
+ if not self.verify_chain_signature(chain, chain_signer_pubkey_b64):
539
+ all_verified = False
540
+ errors.append("Invalid or missing chain signature")
541
+
518
542
  aggregate_confidence = total_confidence / len(chain.facts)
519
543
  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."""
544
+ return all_verified, aggregate_confidence, "; ".join(errors) if errors else None
545
+
546
+ def get_verification_ledger(self) -> List[Dict[str, Any]]:
533
547
  return [r.to_dict() for r in self.verification_ledger]
534
-
548
+
535
549
  def sign_ledger(self) -> str:
536
- """Sign the entire verification ledger."""
537
- ledger_json = json.dumps(self.get_verification_ledger(), sort_keys=True)
550
+ ledger_json = canonical_json({"ledger": self.get_verification_ledger()})
538
551
  signature, _ = self.sign_content(ledger_json)
539
552
  return signature
540
-
553
+
541
554
  def export_ledger(self, filepath: str) -> None:
542
- """Export verification ledger to file."""
543
555
  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
556
+ "ledger": self.get_verification_ledger(),
557
+ "signature": self.sign_ledger(),
558
+ "exported_at": datetime.utcnow().isoformat() + "Z",
559
+ "signer_pubkey": f"ed25519:{self.get_public_key_b64()}" if self._public_key else None,
548
560
  }
549
- with open(filepath, 'w') as f:
550
- json.dump(data, f, indent=2)
551
-
561
+ with open(filepath, "w") as f:
562
+ json.dump(data, f, indent=2, sort_keys=True)
563
+
552
564
  def clear_ledger(self) -> None:
553
- """Clear the verification ledger."""
554
565
  self.verification_ledger.clear()
555
566
 
556
567
 
557
- # Convenience functions
558
568
  def generate_keypair_b64() -> Tuple[str, str]:
559
- """Generate keypair and return as base64 strings."""
560
569
  verifier = Ed25519Verifier()
561
570
  private_bytes, public_bytes = verifier.generate_keypair()
562
- return (
563
- base64.b64encode(private_bytes).decode('ascii'),
564
- base64.b64encode(public_bytes).decode('ascii')
565
- )
571
+ return base64.b64encode(private_bytes).decode("ascii"), base64.b64encode(public_bytes).decode("ascii")
566
572
 
567
573
 
568
574
  def quick_sign(content: str, private_key_b64: str) -> str:
569
- """Quick sign content with base64-encoded private key."""
570
575
  verifier = Ed25519Verifier()
571
- private_bytes = base64.b64decode(private_key_b64)
572
- verifier.load_keypair(private_bytes)
576
+ verifier.load_keypair(base64.b64decode(private_key_b64))
573
577
  signature, _ = verifier.sign_content(content)
574
578
  return signature
575
579
 
576
580
 
577
581
  def quick_verify(content: str, signature_b64: str, public_key_b64: str) -> bool:
578
- """Quick verify content signature."""
579
582
  verifier = Ed25519Verifier()
580
- public_bytes = base64.b64decode(public_key_b64)
581
- return verifier.verify_signature(content, signature_b64, public_bytes)
583
+ return verifier.verify_signature(content, signature_b64, base64.b64decode(public_key_b64))
582
584
 
583
585
 
584
- # Demo and testing
585
586
  if __name__ == "__main__":
586
- print("Ed25519 Verification Module Demo")
587
+ print("Ed25519 Provenance Verifier Demo")
587
588
  print("=" * 60)
588
589
  print(f"Crypto Backend: {CRYPTO_BACKEND}")
589
590
  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(
591
+
592
+ issuer = Ed25519Verifier(auto_generate_keypair=True)
593
+ issuer_pubkey = issuer.get_public_key_b64()
594
+ verifier = Ed25519Verifier(trusted_issuers={"nature.com": {"pubkey_b64": issuer_pubkey, "status": "active"}})
595
+ issuer.load_keypair(issuer._private_key, issuer._public_key)
596
+
597
+ fact = issuer.create_issuer_signed_fact(
603
598
  claim="The study found a 25% improvement in efficiency",
604
599
  source_url="https://nature.com/articles/example",
605
600
  source_content="Full article content here...",
606
- issuer="nature.com"
601
+ issuer="nature.com",
602
+ research_context="demo-run",
607
603
  )
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
604
  result = verifier.verify_fact(fact)
605
+ print("[1] Pinned issuer fact")
618
606
  print(f"Verified: {result.verified}")
619
607
  print(f"Confidence: {result.confidence}")
620
- print(f"Error: {result.error}")
608
+ print(f"Trust class: {result.trust_class}")
621
609
  print()
622
-
623
- # Create citation chain
624
- print("[3] Building citation chain...")
610
+
611
+ attacker = Ed25519Verifier(auto_generate_keypair=True)
612
+ forged = attacker.create_issuer_signed_fact(
613
+ claim="Fabricated claim",
614
+ source_url="https://nature.com/articles/example",
615
+ source_content="Fake content",
616
+ issuer="nature.com",
617
+ )
618
+ forged_result = verifier.verify_fact(forged)
619
+ print("[2] Attacker self-signed trusted string")
620
+ print(f"Verified: {forged_result.verified}")
621
+ print(f"Confidence: {forged_result.confidence}")
622
+ print(f"Error: {forged_result.error}")
623
+ print()
624
+
625
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]
626
+ for i in range(2):
627
+ chain.add_fact(
628
+ issuer.create_issuer_signed_fact(
629
+ claim=f"Claim {i + 1}",
630
+ source_url=f"https://nature.com/articles/{i + 1}",
631
+ source_content=f"Source content {i + 1}",
632
+ issuer="nature.com",
633
+ research_context="demo-run",
634
+ )
633
635
  )
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%}")
636
+ issuer.sign_chain(chain)
637
+ all_verified, confidence, error = verifier.verify_citation_chain(chain, issuer_pubkey)
638
+ print("[3] Citation chain")
639
+ print(f"All verified: {all_verified}")
640
+ print(f"Aggregate confidence: {confidence:.2%}")
646
641
  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!")