@dzhechkov/p-replicator 1.5.8 → 1.5.12
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/skills/brutal-honesty-review/SKILL.md +18 -1
- package/templates/.claude/skills/brutal-honesty-review/evals/brutal-honesty-review.yaml +61 -0
- package/templates/.claude/skills/brutal-honesty-review/schemas/output.json +291 -0
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-tests.sh +2 -2
- package/templates/.claude/skills/brutal-honesty-review/scripts/validate-config.json +34 -0
- 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 +78 -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/requirements-validator/SKILL.md +2 -2
- package/templates/.claude/skills/reverse-engineering-unicorn/SKILL.md +4 -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,50 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Ed25519
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
30
|
+
Ed25519PrivateKey,
|
|
31
|
+
Ed25519PublicKey,
|
|
31
32
|
)
|
|
32
|
-
|
|
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
|
|
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) ->
|
|
83
|
-
|
|
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) ->
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
183
|
-
" pip install cryptography
|
|
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.
|
|
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:
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
""
|
|
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,
|
|
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)
|
|
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(
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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(
|
|
335
|
+
content_bytes = content.encode("utf-8")
|
|
325
336
|
signature = base64.b64decode(signature_b64)
|
|
326
|
-
|
|
327
337
|
if CRYPTO_BACKEND == "cryptography":
|
|
328
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
|
341
|
-
"""
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
364
|
-
self.trusted_issuers.
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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=
|
|
384
|
+
signature="",
|
|
411
385
|
timestamp=timestamp,
|
|
412
|
-
confidence=
|
|
413
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
return
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if fact.
|
|
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
|
|
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
|
-
|
|
522
|
-
|
|
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
|
-
""
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
587
|
+
print("Ed25519 Provenance Verifier Demo")
|
|
587
588
|
print("=" * 60)
|
|
588
589
|
print(f"Crypto Backend: {CRYPTO_BACKEND}")
|
|
589
590
|
print()
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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"
|
|
608
|
+
print(f"Trust class: {result.trust_class}")
|
|
621
609
|
print()
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
print(
|
|
637
|
-
print(f"
|
|
638
|
-
print(f"
|
|
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!")
|