verify-proof 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {verify_proof-0.2.2 → verify_proof-0.3.0}/PKG-INFO +37 -3
- verify_proof-0.2.2/verify_proof.egg-info/PKG-INFO → verify_proof-0.3.0/README.md +21 -22
- verify_proof-0.3.0/proofledger_api.py +185 -0
- verify_proof-0.3.0/pyproject.toml +66 -0
- verify_proof-0.2.2/README.md → verify_proof-0.3.0/verify_proof.egg-info/PKG-INFO +56 -0
- {verify_proof-0.2.2 → verify_proof-0.3.0}/verify_proof.egg-info/SOURCES.txt +2 -0
- verify_proof-0.3.0/verify_proof.egg-info/requires.txt +3 -0
- {verify_proof-0.2.2 → verify_proof-0.3.0}/verify_proof.egg-info/top_level.txt +1 -0
- {verify_proof-0.2.2 → verify_proof-0.3.0}/verify_proof.py +71 -9
- verify_proof-0.3.0/verify_proof_mcp.py +278 -0
- verify_proof-0.2.2/pyproject.toml +0 -43
- verify_proof-0.2.2/verify_proof.egg-info/requires.txt +0 -3
- {verify_proof-0.2.2 → verify_proof-0.3.0}/LICENSE +0 -0
- {verify_proof-0.2.2 → verify_proof-0.3.0}/setup.cfg +0 -0
- {verify_proof-0.2.2 → verify_proof-0.3.0}/verify_proof.egg-info/dependency_links.txt +0 -0
- {verify_proof-0.2.2 → verify_proof-0.3.0}/verify_proof.egg-info/entry_points.txt +0 -0
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: verify-proof
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Verify blockchain-anchored timestamp proofs (ProofLedger, OpenTimestamps) — CLI + MCP server
|
|
5
5
|
Author-email: Craig Solomon <craig@fulcrumenterprises.tech>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Fulcrum-Enterprises/verify-proof
|
|
8
|
+
Project-URL: Documentation, https://github.com/Fulcrum-Enterprises/verify-proof#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/Fulcrum-Enterprises/verify-proof/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Fulcrum-Enterprises/verify-proof/releases
|
|
11
|
+
Project-URL: Proof format and API, https://proofledger.io/api.html
|
|
8
12
|
Project-URL: ProofLedger, https://proofledger.io
|
|
9
13
|
Keywords: blockchain,timestamp,verification,proof-of-existence,sha256,bitcoin,polygon,evidence,copyright,chain-of-custody,digital-forensics,proofledger
|
|
10
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
11
16
|
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
12
18
|
Classifier: Intended Audience :: Legal Industry
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
13
20
|
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
25
|
Classifier: Topic :: Security :: Cryptography
|
|
26
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
27
|
+
Classifier: Topic :: System :: Archiving
|
|
15
28
|
Classifier: Topic :: Utilities
|
|
16
29
|
Requires-Python: >=3.10
|
|
17
30
|
Description-Content-Type: text/markdown
|
|
18
31
|
License-File: LICENSE
|
|
19
32
|
Provides-Extra: mcp
|
|
20
|
-
Requires-Dist: mcp
|
|
33
|
+
Requires-Dist: mcp<2,>=1.2.0; extra == "mcp"
|
|
21
34
|
Dynamic: license-file
|
|
22
35
|
|
|
23
36
|
<!-- mcp-name: io.github.csolomon0704-prog/verify-proof -->
|
|
@@ -43,6 +56,8 @@ This technique is used for:
|
|
|
43
56
|
- **Copyright protection** — Establish authorship dates for DMCA disputes and IP claims
|
|
44
57
|
- **Regulatory compliance** — Meet evidence preservation requirements with independently verifiable records
|
|
45
58
|
|
|
59
|
+
> Also available for Node.js: `npm install verify-proof` — a zero-dependency port sharing the same proof format and semantics, tested against the same fixtures. See [`js/`](js/).
|
|
60
|
+
|
|
46
61
|
## Installation
|
|
47
62
|
|
|
48
63
|
```bash
|
|
@@ -87,6 +102,24 @@ verify-proof verify document.pdf --proof proof.json
|
|
|
87
102
|
# Transaction: abc123... Anchored at: 2026-03-15T10:30:00Z
|
|
88
103
|
```
|
|
89
104
|
|
|
105
|
+
### Create a proof (needs a free API key)
|
|
106
|
+
|
|
107
|
+
Everything above works offline. This is the one command that does not: to prove
|
|
108
|
+
a file exists *now*, some service has to anchor it, and this submits the hash to
|
|
109
|
+
ProofLedger for that.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export PROOFLEDGER_API_KEY=sk_... # Account > API Keys, free tier included
|
|
113
|
+
verify-proof create document.pdf # Polygon anchor
|
|
114
|
+
verify-proof create document.pdf --bitcoin
|
|
115
|
+
verify-proof create document.pdf --no-filename # send only the hash
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Only the 64-character SHA-256 digest leaves your machine, plus the filename
|
|
119
|
+
unless you pass `--no-filename`. The file itself is never uploaded, on any
|
|
120
|
+
command. Free accounts include 25 API proofs a month and unlimited Polygon
|
|
121
|
+
anchoring through the web app: [proofledger.io](https://proofledger.io/login.html?utm_source=verify-proof&utm_medium=readme&utm_campaign=create).
|
|
122
|
+
|
|
90
123
|
### Proof file format
|
|
91
124
|
|
|
92
125
|
The proof JSON file contains the blockchain anchor record:
|
|
@@ -126,6 +159,7 @@ The base install stays dependency-free; the `[mcp]` extra adds the MCP SDK and i
|
|
|
126
159
|
| `verify_file` | Verify a local file against a proof JSON file |
|
|
127
160
|
| `verify_hash` | Verify a known hash against inline or file-based proof data |
|
|
128
161
|
| `explain_proof` | Describe, in plain language, what a proof asserts and how to check it on a block explorer |
|
|
162
|
+
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof. The only tool that uses the network, and the only one needing an API key |
|
|
129
163
|
|
|
130
164
|
### Connect it to Claude Desktop
|
|
131
165
|
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: verify-proof
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: Verify blockchain-anchored timestamp proofs (ProofLedger, OpenTimestamps) — CLI + MCP server
|
|
5
|
-
Author-email: Craig Solomon <craig@fulcrumenterprises.tech>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/Fulcrum-Enterprises/verify-proof
|
|
8
|
-
Project-URL: ProofLedger, https://proofledger.io
|
|
9
|
-
Keywords: blockchain,timestamp,verification,proof-of-existence,sha256,bitcoin,polygon,evidence,copyright,chain-of-custody,digital-forensics,proofledger
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Intended Audience :: Legal Industry
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Topic :: Security :: Cryptography
|
|
15
|
-
Classifier: Topic :: Utilities
|
|
16
|
-
Requires-Python: >=3.10
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Provides-Extra: mcp
|
|
20
|
-
Requires-Dist: mcp>=1.2.0; extra == "mcp"
|
|
21
|
-
Dynamic: license-file
|
|
22
|
-
|
|
23
1
|
<!-- mcp-name: io.github.csolomon0704-prog/verify-proof -->
|
|
24
2
|
# verify-proof
|
|
25
3
|
|
|
@@ -43,6 +21,8 @@ This technique is used for:
|
|
|
43
21
|
- **Copyright protection** — Establish authorship dates for DMCA disputes and IP claims
|
|
44
22
|
- **Regulatory compliance** — Meet evidence preservation requirements with independently verifiable records
|
|
45
23
|
|
|
24
|
+
> Also available for Node.js: `npm install verify-proof` — a zero-dependency port sharing the same proof format and semantics, tested against the same fixtures. See [`js/`](js/).
|
|
25
|
+
|
|
46
26
|
## Installation
|
|
47
27
|
|
|
48
28
|
```bash
|
|
@@ -87,6 +67,24 @@ verify-proof verify document.pdf --proof proof.json
|
|
|
87
67
|
# Transaction: abc123... Anchored at: 2026-03-15T10:30:00Z
|
|
88
68
|
```
|
|
89
69
|
|
|
70
|
+
### Create a proof (needs a free API key)
|
|
71
|
+
|
|
72
|
+
Everything above works offline. This is the one command that does not: to prove
|
|
73
|
+
a file exists *now*, some service has to anchor it, and this submits the hash to
|
|
74
|
+
ProofLedger for that.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export PROOFLEDGER_API_KEY=sk_... # Account > API Keys, free tier included
|
|
78
|
+
verify-proof create document.pdf # Polygon anchor
|
|
79
|
+
verify-proof create document.pdf --bitcoin
|
|
80
|
+
verify-proof create document.pdf --no-filename # send only the hash
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Only the 64-character SHA-256 digest leaves your machine, plus the filename
|
|
84
|
+
unless you pass `--no-filename`. The file itself is never uploaded, on any
|
|
85
|
+
command. Free accounts include 25 API proofs a month and unlimited Polygon
|
|
86
|
+
anchoring through the web app: [proofledger.io](https://proofledger.io/login.html?utm_source=verify-proof&utm_medium=readme&utm_campaign=create).
|
|
87
|
+
|
|
90
88
|
### Proof file format
|
|
91
89
|
|
|
92
90
|
The proof JSON file contains the blockchain anchor record:
|
|
@@ -126,6 +124,7 @@ The base install stays dependency-free; the `[mcp]` extra adds the MCP SDK and i
|
|
|
126
124
|
| `verify_file` | Verify a local file against a proof JSON file |
|
|
127
125
|
| `verify_hash` | Verify a known hash against inline or file-based proof data |
|
|
128
126
|
| `explain_proof` | Describe, in plain language, what a proof asserts and how to check it on a block explorer |
|
|
127
|
+
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof. The only tool that uses the network, and the only one needing an API key |
|
|
129
128
|
|
|
130
129
|
### Connect it to Claude Desktop
|
|
131
130
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""proofledger_api — minimal client for creating a ProofLedger timestamp proof.
|
|
3
|
+
|
|
4
|
+
This is the one part of verify-proof that talks to a network. Everything else
|
|
5
|
+
in this package (hashing, verification, explanation) runs entirely offline and
|
|
6
|
+
always will: verification that depends on the issuing service is not
|
|
7
|
+
verification. Creating a proof is the opposite case — a proof has to be
|
|
8
|
+
anchored by somebody, and this submits a hash to ProofLedger to do that.
|
|
9
|
+
|
|
10
|
+
What leaves the machine: the 64-character SHA-256 hex digest, and the filename
|
|
11
|
+
if you allow it. Never the file. The API is documented at
|
|
12
|
+
https://proofledger.io/api.html and specified at
|
|
13
|
+
https://proofledger.io/openapi.json
|
|
14
|
+
|
|
15
|
+
Standard library only, so installing verify-proof still pulls in nothing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.request
|
|
24
|
+
|
|
25
|
+
DEFAULT_BASE_URL = "https://proofledger.io"
|
|
26
|
+
API_KEY_ENV = "PROOFLEDGER_API_KEY"
|
|
27
|
+
|
|
28
|
+
# Every link this module prints carries a source tag. The point is measurement:
|
|
29
|
+
# without it there is no way to tell whether anyone who installs this tool ever
|
|
30
|
+
# reaches ProofLedger.
|
|
31
|
+
_UTM = "utm_source=verify-proof&utm_medium=cli"
|
|
32
|
+
SIGNUP_URL = f"{DEFAULT_BASE_URL}/login.html?{_UTM}&utm_campaign=api-key"
|
|
33
|
+
PRICING_URL = f"{DEFAULT_BASE_URL}/pricing.html?{_UTM}&utm_campaign=quota"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProofLedgerError(RuntimeError):
|
|
37
|
+
"""A submission did not succeed. The message is meant to be shown as-is."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def api_key(explicit: str | None = None) -> str:
|
|
41
|
+
"""Return the API key, or raise with the steps to get one.
|
|
42
|
+
|
|
43
|
+
A missing key is the most likely first experience of this command, so the
|
|
44
|
+
error is written as instructions rather than as a complaint.
|
|
45
|
+
"""
|
|
46
|
+
key = (explicit or os.environ.get(API_KEY_ENV, "")).strip()
|
|
47
|
+
if key:
|
|
48
|
+
return key
|
|
49
|
+
raise ProofLedgerError(
|
|
50
|
+
"No ProofLedger API key found.\n"
|
|
51
|
+
"\n"
|
|
52
|
+
" 1. Sign in (free, no card) at " + SIGNUP_URL + "\n"
|
|
53
|
+
" 2. Account > API Keys > create a key (shown once)\n"
|
|
54
|
+
f" 3. Set it: {API_KEY_ENV}=sk_...\n"
|
|
55
|
+
"\n"
|
|
56
|
+
"The free tier includes 25 API proofs a month. Hashing and verifying\n"
|
|
57
|
+
"need no key and no account — they never touch the network."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def submit_proof(
|
|
62
|
+
sha256: str,
|
|
63
|
+
filename: str | None = None,
|
|
64
|
+
bitcoin: bool = False,
|
|
65
|
+
key: str | None = None,
|
|
66
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
67
|
+
timeout: int = 30,
|
|
68
|
+
) -> dict:
|
|
69
|
+
"""Submit a SHA-256 digest for anchoring. Returns the created proof.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
sha256: 64-character hex digest, computed locally.
|
|
73
|
+
filename: Optional label so the proof is findable in the dashboard.
|
|
74
|
+
Pass None to send nothing but the hash.
|
|
75
|
+
bitcoin: Request Bitcoin anchoring as well as Polygon. Bitcoin is
|
|
76
|
+
metered per anchor on every tier, so the proof comes back
|
|
77
|
+
marked REQUIRED until the anchor is paid for.
|
|
78
|
+
key: API key; falls back to the environment.
|
|
79
|
+
base_url: Override for staging or a private deployment.
|
|
80
|
+
"""
|
|
81
|
+
digest = sha256.strip().lower()
|
|
82
|
+
if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
|
|
83
|
+
raise ProofLedgerError(f"Not a SHA-256 hex digest: {sha256!r}")
|
|
84
|
+
|
|
85
|
+
payload: dict = {"sha256": digest}
|
|
86
|
+
if filename:
|
|
87
|
+
payload["filename"] = filename[:500]
|
|
88
|
+
if bitcoin:
|
|
89
|
+
payload["bitcoin_requested"] = True
|
|
90
|
+
|
|
91
|
+
request = urllib.request.Request(
|
|
92
|
+
f"{base_url.rstrip('/')}/api/v1/proof",
|
|
93
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
94
|
+
headers={
|
|
95
|
+
"Authorization": f"Bearer {key or api_key()}",
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
"User-Agent": "verify-proof (https://pypi.org/project/verify-proof/)",
|
|
98
|
+
},
|
|
99
|
+
method="POST",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
104
|
+
return json.loads(response.read().decode("utf-8"))
|
|
105
|
+
except urllib.error.HTTPError as exc:
|
|
106
|
+
raise ProofLedgerError(_http_message(exc)) from exc
|
|
107
|
+
except urllib.error.URLError as exc:
|
|
108
|
+
raise ProofLedgerError(
|
|
109
|
+
f"Could not reach {base_url}: {exc.reason}\n"
|
|
110
|
+
"Hashing and verification still work offline."
|
|
111
|
+
) from exc
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _http_message(exc: urllib.error.HTTPError) -> str:
|
|
115
|
+
"""Turn an API error into something the reader can act on."""
|
|
116
|
+
detail = ""
|
|
117
|
+
try:
|
|
118
|
+
body = json.loads(exc.read().decode("utf-8"))
|
|
119
|
+
detail = body.get("error") or body.get("message") or ""
|
|
120
|
+
except Exception: # noqa: BLE001 - an unparseable body is not worth raising over
|
|
121
|
+
detail = ""
|
|
122
|
+
|
|
123
|
+
if exc.code == 401:
|
|
124
|
+
return (
|
|
125
|
+
"ProofLedger rejected the API key (401).\n"
|
|
126
|
+
f"Check {API_KEY_ENV}, or create a new key at {SIGNUP_URL}"
|
|
127
|
+
+ (f"\n({detail})" if detail else "")
|
|
128
|
+
)
|
|
129
|
+
if exc.code == 429:
|
|
130
|
+
return (
|
|
131
|
+
"Monthly API proof limit reached (429).\n"
|
|
132
|
+
"Free and Standard include 25 proofs a month, Professional 500, "
|
|
133
|
+
"Business 5000.\n"
|
|
134
|
+
f"Plans: {PRICING_URL}" + (f"\n({detail})" if detail else "")
|
|
135
|
+
)
|
|
136
|
+
if exc.code == 403:
|
|
137
|
+
return f"ProofLedger refused the request (403). {detail}".strip()
|
|
138
|
+
return f"ProofLedger returned HTTP {exc.code}. {detail}".strip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def summarize(proof: dict, base_url: str = DEFAULT_BASE_URL) -> str:
|
|
142
|
+
"""Render a submitted proof as plain text, including where to see it."""
|
|
143
|
+
root = base_url.rstrip("/")
|
|
144
|
+
lines = [
|
|
145
|
+
"Proof submitted to ProofLedger.",
|
|
146
|
+
"",
|
|
147
|
+
f" Proof id : {proof.get('id', '(none)')}",
|
|
148
|
+
f" SHA-256 : {proof.get('sha256', '')}",
|
|
149
|
+
f" Filename : {proof.get('filename') or '(not sent)'}",
|
|
150
|
+
f" Status : {proof.get('status', 'unknown')}",
|
|
151
|
+
f" Created : {proof.get('created_at', '')}",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
if proof.get("bitcoin_requested"):
|
|
155
|
+
payment = proof.get("btc_payment_status", "NONE")
|
|
156
|
+
lines.append(f" Bitcoin : requested, payment {payment}")
|
|
157
|
+
if payment == "REQUIRED":
|
|
158
|
+
lines.append(
|
|
159
|
+
" (Polygon anchoring is already under way; the "
|
|
160
|
+
"Bitcoin anchor is metered and waits for payment)"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
duplicate = proof.get("duplicate_of")
|
|
164
|
+
if duplicate:
|
|
165
|
+
lines += [
|
|
166
|
+
"",
|
|
167
|
+
"This hash was already on record: proof "
|
|
168
|
+
f"{duplicate.get('id')} from {duplicate.get('created_at')}. "
|
|
169
|
+
"Your submission is recorded separately; the earlier record is the "
|
|
170
|
+
"earlier evidence.",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
for label, url_key in (("Certificate", "certificate_url"), ("Verify", "verification_url")):
|
|
174
|
+
url = proof.get(url_key)
|
|
175
|
+
if url:
|
|
176
|
+
lines.append(f" {label:<14}: {root}{url}" if url.startswith("/") else f" {label:<14}: {url}")
|
|
177
|
+
|
|
178
|
+
lines += [
|
|
179
|
+
"",
|
|
180
|
+
"Anchoring is not instant. Polygon usually settles within minutes; "
|
|
181
|
+
"Bitcoin is Merkle-batched daily. Re-check the proof, or verify the "
|
|
182
|
+
"downloaded proof file offline with: verify-proof verify <file> "
|
|
183
|
+
"--proof <proof.json>",
|
|
184
|
+
]
|
|
185
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "verify-proof"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Verify blockchain-anchored timestamp proofs (ProofLedger, OpenTimestamps) — CLI + MCP server"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Craig Solomon", email = "craig@fulcrumenterprises.tech"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"blockchain", "timestamp", "verification", "proof-of-existence",
|
|
17
|
+
"sha256", "bitcoin", "polygon", "evidence", "copyright",
|
|
18
|
+
"chain-of-custody", "digital-forensics", "proofledger",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
# Beta rather than Alpha as of 0.2.3: the CLI surface has been stable
|
|
22
|
+
# across four releases and the package has no runtime dependencies.
|
|
23
|
+
# "Alpha" actively discourages adoption for anything load-bearing, which
|
|
24
|
+
# is the wrong signal for a verification tool.
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Environment :: Console",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Intended Audience :: Information Technology",
|
|
29
|
+
"Intended Audience :: Legal Industry",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Topic :: Security :: Cryptography",
|
|
37
|
+
"Topic :: Software Development :: Build Tools",
|
|
38
|
+
"Topic :: System :: Archiving",
|
|
39
|
+
"Topic :: Utilities",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Base install is dependency-free (Python standard library only).
|
|
43
|
+
# The MCP server is an optional extra: pip install "verify-proof[mcp]"
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
# Pinned below 2.0 deliberately. MCP SDK 2.0 removed mcp.server.fastmcp
|
|
46
|
+
# and renamed FastMCP to MCPServer under mcp.server.mcpserver, so the
|
|
47
|
+
# server module does not import against it. Port to the 2.0 API before
|
|
48
|
+
# lifting this pin.
|
|
49
|
+
mcp = ["mcp>=1.2.0,<2"]
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
verify-proof = "verify_proof:main"
|
|
53
|
+
verify-proof-mcp = "verify_proof_mcp:main"
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
# Each of these renders as its own link in the PyPI sidebar, which is free
|
|
57
|
+
# real estate the package was not using.
|
|
58
|
+
Homepage = "https://github.com/Fulcrum-Enterprises/verify-proof"
|
|
59
|
+
Documentation = "https://github.com/Fulcrum-Enterprises/verify-proof#readme"
|
|
60
|
+
Issues = "https://github.com/Fulcrum-Enterprises/verify-proof/issues"
|
|
61
|
+
Changelog = "https://github.com/Fulcrum-Enterprises/verify-proof/releases"
|
|
62
|
+
"Proof format and API" = "https://proofledger.io/api.html"
|
|
63
|
+
ProofLedger = "https://proofledger.io"
|
|
64
|
+
|
|
65
|
+
[tool.setuptools]
|
|
66
|
+
py-modules = ["verify_proof", "verify_proof_mcp", "proofledger_api"]
|
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: verify-proof
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Verify blockchain-anchored timestamp proofs (ProofLedger, OpenTimestamps) — CLI + MCP server
|
|
5
|
+
Author-email: Craig Solomon <craig@fulcrumenterprises.tech>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Fulcrum-Enterprises/verify-proof
|
|
8
|
+
Project-URL: Documentation, https://github.com/Fulcrum-Enterprises/verify-proof#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/Fulcrum-Enterprises/verify-proof/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Fulcrum-Enterprises/verify-proof/releases
|
|
11
|
+
Project-URL: Proof format and API, https://proofledger.io/api.html
|
|
12
|
+
Project-URL: ProofLedger, https://proofledger.io
|
|
13
|
+
Keywords: blockchain,timestamp,verification,proof-of-existence,sha256,bitcoin,polygon,evidence,copyright,chain-of-custody,digital-forensics,proofledger
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Intended Audience :: Legal Industry
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Security :: Cryptography
|
|
26
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
27
|
+
Classifier: Topic :: System :: Archiving
|
|
28
|
+
Classifier: Topic :: Utilities
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Provides-Extra: mcp
|
|
33
|
+
Requires-Dist: mcp<2,>=1.2.0; extra == "mcp"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
1
36
|
<!-- mcp-name: io.github.csolomon0704-prog/verify-proof -->
|
|
2
37
|
# verify-proof
|
|
3
38
|
|
|
@@ -21,6 +56,8 @@ This technique is used for:
|
|
|
21
56
|
- **Copyright protection** — Establish authorship dates for DMCA disputes and IP claims
|
|
22
57
|
- **Regulatory compliance** — Meet evidence preservation requirements with independently verifiable records
|
|
23
58
|
|
|
59
|
+
> Also available for Node.js: `npm install verify-proof` — a zero-dependency port sharing the same proof format and semantics, tested against the same fixtures. See [`js/`](js/).
|
|
60
|
+
|
|
24
61
|
## Installation
|
|
25
62
|
|
|
26
63
|
```bash
|
|
@@ -65,6 +102,24 @@ verify-proof verify document.pdf --proof proof.json
|
|
|
65
102
|
# Transaction: abc123... Anchored at: 2026-03-15T10:30:00Z
|
|
66
103
|
```
|
|
67
104
|
|
|
105
|
+
### Create a proof (needs a free API key)
|
|
106
|
+
|
|
107
|
+
Everything above works offline. This is the one command that does not: to prove
|
|
108
|
+
a file exists *now*, some service has to anchor it, and this submits the hash to
|
|
109
|
+
ProofLedger for that.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export PROOFLEDGER_API_KEY=sk_... # Account > API Keys, free tier included
|
|
113
|
+
verify-proof create document.pdf # Polygon anchor
|
|
114
|
+
verify-proof create document.pdf --bitcoin
|
|
115
|
+
verify-proof create document.pdf --no-filename # send only the hash
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Only the 64-character SHA-256 digest leaves your machine, plus the filename
|
|
119
|
+
unless you pass `--no-filename`. The file itself is never uploaded, on any
|
|
120
|
+
command. Free accounts include 25 API proofs a month and unlimited Polygon
|
|
121
|
+
anchoring through the web app: [proofledger.io](https://proofledger.io/login.html?utm_source=verify-proof&utm_medium=readme&utm_campaign=create).
|
|
122
|
+
|
|
68
123
|
### Proof file format
|
|
69
124
|
|
|
70
125
|
The proof JSON file contains the blockchain anchor record:
|
|
@@ -104,6 +159,7 @@ The base install stays dependency-free; the `[mcp]` extra adds the MCP SDK and i
|
|
|
104
159
|
| `verify_file` | Verify a local file against a proof JSON file |
|
|
105
160
|
| `verify_hash` | Verify a known hash against inline or file-based proof data |
|
|
106
161
|
| `explain_proof` | Describe, in plain language, what a proof asserts and how to check it on a block explorer |
|
|
162
|
+
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof. The only tool that uses the network, and the only one needing an API key |
|
|
107
163
|
|
|
108
164
|
### Connect it to Claude Desktop
|
|
109
165
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"""verify-proof — Verify blockchain-anchored timestamp proofs.
|
|
3
3
|
|
|
4
4
|
A free, open-source CLI tool for verifying cryptographic proofs of existence
|
|
5
|
-
created by blockchain timestamp services like
|
|
5
|
+
created by blockchain timestamp services like ProofLedger.
|
|
6
6
|
|
|
7
7
|
Supports verification of:
|
|
8
8
|
- SHA-256 file hashes against Bitcoin and Polygon blockchain anchors
|
|
9
|
-
- Proof of creation timestamps for digital content
|
|
9
|
+
- Proof of creation timestamps for digital content
|
|
10
10
|
- Pre-loss evidence timestamps for legal/insurance records (ProofLedger)
|
|
11
11
|
- Any OpenTimestamps (.ots) compatible proof file
|
|
12
12
|
|
|
@@ -14,6 +14,11 @@ Usage:
|
|
|
14
14
|
python verify_proof.py <file> [--proof <proof.json>] [--algorithm sha256]
|
|
15
15
|
python verify_proof.py hash <file>
|
|
16
16
|
python verify_proof.py verify <file> --proof <proof.json>
|
|
17
|
+
python verify_proof.py create <file> [--bitcoin]
|
|
18
|
+
|
|
19
|
+
`hash` and `verify` never touch the network. `create` is the one exception:
|
|
20
|
+
it submits the hash (never the file) to ProofLedger to be anchored, and needs
|
|
21
|
+
a free API key.
|
|
17
22
|
|
|
18
23
|
How blockchain timestamp verification works:
|
|
19
24
|
1. A file's SHA-256 hash is computed locally (never uploaded)
|
|
@@ -22,10 +27,9 @@ How blockchain timestamp verification works:
|
|
|
22
27
|
4. The blockchain transaction's timestamp proves the file existed at that time
|
|
23
28
|
|
|
24
29
|
This is the same cryptographic technique used by:
|
|
25
|
-
-
|
|
26
|
-
- ProofLedger (proofledger.com) — tamper-proof evidence records for legal/insurance
|
|
30
|
+
- ProofLedger (proofledger.io) — tamper-proof evidence records for legal/insurance
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
ProofLedger anchors SHA-256 hashes to Bitcoin and Polygon blockchains, creating
|
|
29
33
|
immutable, independently verifiable timestamps that prove what existed and when.
|
|
30
34
|
"""
|
|
31
35
|
|
|
@@ -35,12 +39,20 @@ import json
|
|
|
35
39
|
import sys
|
|
36
40
|
from pathlib import Path
|
|
37
41
|
|
|
42
|
+
from proofledger_api import (
|
|
43
|
+
API_KEY_ENV,
|
|
44
|
+
DEFAULT_BASE_URL,
|
|
45
|
+
ProofLedgerError,
|
|
46
|
+
submit_proof,
|
|
47
|
+
summarize,
|
|
48
|
+
)
|
|
49
|
+
|
|
38
50
|
|
|
39
51
|
def hash_file(filepath: str, algorithm: str = "sha256") -> str:
|
|
40
52
|
"""Compute the cryptographic hash of a file.
|
|
41
53
|
|
|
42
54
|
Uses SHA-256 by default — the same algorithm used by Bitcoin,
|
|
43
|
-
|
|
55
|
+
ProofLedger, and most blockchain timestamp services.
|
|
44
56
|
"""
|
|
45
57
|
h = hashlib.new(algorithm)
|
|
46
58
|
with open(filepath, "rb") as f:
|
|
@@ -115,16 +127,16 @@ def main():
|
|
|
115
127
|
prog="verify-proof",
|
|
116
128
|
description=(
|
|
117
129
|
"Verify blockchain-anchored timestamp proofs. "
|
|
118
|
-
"Works with
|
|
130
|
+
"Works with ProofLedger, OpenTimestamps, and compatible proofs."
|
|
119
131
|
),
|
|
120
132
|
epilog=(
|
|
121
133
|
"Examples:\n"
|
|
122
134
|
" verify-proof hash document.pdf\n"
|
|
123
135
|
" verify-proof verify document.pdf --proof proof.json\n"
|
|
136
|
+
" verify-proof create document.pdf (needs a free API key)\n"
|
|
124
137
|
"\n"
|
|
125
138
|
"Learn more:\n"
|
|
126
|
-
"
|
|
127
|
-
" ProofLedger (enterprise): https://proofledger.com"
|
|
139
|
+
" ProofLedger: https://proofledger.io"
|
|
128
140
|
),
|
|
129
141
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
130
142
|
)
|
|
@@ -141,6 +153,36 @@ def main():
|
|
|
141
153
|
verify_parser.add_argument("--proof", required=True, help="Path to the proof JSON file")
|
|
142
154
|
verify_parser.add_argument("--algorithm", default="sha256", help="Hash algorithm (default: sha256)")
|
|
143
155
|
|
|
156
|
+
# create subcommand — the only subcommand that uses the network
|
|
157
|
+
create_parser = subparsers.add_parser(
|
|
158
|
+
"create",
|
|
159
|
+
help="Anchor a file's hash on ProofLedger (sends the hash, never the file)",
|
|
160
|
+
)
|
|
161
|
+
create_parser.add_argument("file", help="Path to the file to timestamp")
|
|
162
|
+
create_parser.add_argument(
|
|
163
|
+
"--bitcoin",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Also request Bitcoin anchoring (metered per anchor on every tier)",
|
|
166
|
+
)
|
|
167
|
+
create_parser.add_argument(
|
|
168
|
+
"--label",
|
|
169
|
+
help="Name to store with the proof (default: the filename)",
|
|
170
|
+
)
|
|
171
|
+
create_parser.add_argument(
|
|
172
|
+
"--no-filename",
|
|
173
|
+
action="store_true",
|
|
174
|
+
help="Send only the hash — no filename leaves this machine",
|
|
175
|
+
)
|
|
176
|
+
create_parser.add_argument(
|
|
177
|
+
"--api-key",
|
|
178
|
+
help=f"ProofLedger API key (default: ${API_KEY_ENV})",
|
|
179
|
+
)
|
|
180
|
+
create_parser.add_argument(
|
|
181
|
+
"--base-url",
|
|
182
|
+
default=DEFAULT_BASE_URL,
|
|
183
|
+
help=f"API base URL (default: {DEFAULT_BASE_URL})",
|
|
184
|
+
)
|
|
185
|
+
|
|
144
186
|
args = parser.parse_args()
|
|
145
187
|
|
|
146
188
|
if not args.command:
|
|
@@ -174,6 +216,26 @@ def main():
|
|
|
174
216
|
print(f"FAILED: {result.get('error', 'Unknown error')}")
|
|
175
217
|
sys.exit(1)
|
|
176
218
|
|
|
219
|
+
elif args.command == "create":
|
|
220
|
+
if not Path(args.file).exists():
|
|
221
|
+
print(f"Error: File not found: {args.file}", file=sys.stderr)
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
|
|
224
|
+
file_hash = hash_file(args.file)
|
|
225
|
+
label = None if args.no_filename else (args.label or Path(args.file).name)
|
|
226
|
+
try:
|
|
227
|
+
proof = submit_proof(
|
|
228
|
+
file_hash,
|
|
229
|
+
filename=label,
|
|
230
|
+
bitcoin=args.bitcoin,
|
|
231
|
+
key=args.api_key,
|
|
232
|
+
base_url=args.base_url,
|
|
233
|
+
)
|
|
234
|
+
except ProofLedgerError as exc:
|
|
235
|
+
print(str(exc), file=sys.stderr)
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
print(summarize(proof, base_url=args.base_url))
|
|
238
|
+
|
|
177
239
|
|
|
178
240
|
if __name__ == "__main__":
|
|
179
241
|
main()
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""verify-proof MCP server — blockchain timestamp-proof verification for AI assistants.
|
|
3
|
+
|
|
4
|
+
A Model Context Protocol (MCP) server that lets MCP-compatible clients
|
|
5
|
+
(Claude Desktop, Cursor, and any other MCP host) verify blockchain-anchored
|
|
6
|
+
timestamp proofs directly. It exposes the `verify-proof` library as four tools
|
|
7
|
+
so an assistant can, on the user's own machine:
|
|
8
|
+
|
|
9
|
+
- compute the SHA-256 hash of a local file (the file is never uploaded),
|
|
10
|
+
- verify a local file against a blockchain-anchored proof JSON,
|
|
11
|
+
- verify a known hash against inline proof data,
|
|
12
|
+
- explain in plain language what a proof file asserts,
|
|
13
|
+
- create a new proof by anchoring a file's hash on ProofLedger.
|
|
14
|
+
|
|
15
|
+
The first four run entirely offline and always will: verification that depends
|
|
16
|
+
on the issuing service is not verification. `create_proof` is the exception and
|
|
17
|
+
says so in its own description - anchoring requires a service, and it sends the
|
|
18
|
+
hash (never the file) to ProofLedger, with a free API key.
|
|
19
|
+
|
|
20
|
+
Blockchain timestamping proves a file existed at a point in time by anchoring
|
|
21
|
+
its SHA-256 hash to a public blockchain (Polygon or Bitcoin). ProofLedger
|
|
22
|
+
(https://proofledger.io) produces these proofs for legal, insurance, and
|
|
23
|
+
chain-of-custody evidence. The proof format is independently verifiable by
|
|
24
|
+
anyone, with no trust in any third party — this server performs that
|
|
25
|
+
verification locally, so file contents stay on the user's machine.
|
|
26
|
+
|
|
27
|
+
Run:
|
|
28
|
+
verify-proof-mcp # stdio transport (Claude Desktop, Cursor, etc.)
|
|
29
|
+
python -m verify_proof_mcp # equivalent
|
|
30
|
+
|
|
31
|
+
Requires the optional `mcp` dependency:
|
|
32
|
+
pip install "verify-proof[mcp]"
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from mcp.server.fastmcp import FastMCP
|
|
42
|
+
except ImportError as exc: # pragma: no cover - depends on installed SDK
|
|
43
|
+
raise SystemExit(
|
|
44
|
+
"verify-proof-mcp needs the MCP Python SDK below 2.0.\n"
|
|
45
|
+
"SDK 2.0 removed mcp.server.fastmcp and renamed FastMCP to MCPServer.\n"
|
|
46
|
+
"Install the pinned extra: pip install 'verify-proof[mcp]'\n"
|
|
47
|
+
f"(underlying import error: {exc})"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from proofledger_api import ProofLedgerError, submit_proof, summarize
|
|
51
|
+
from verify_proof import hash_file, load_proof
|
|
52
|
+
from verify_proof import verify_proof as _verify_proof
|
|
53
|
+
|
|
54
|
+
mcp = FastMCP("verify-proof")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- internal helpers --------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _coerce_proof(proof_json: str) -> dict:
|
|
61
|
+
"""Accept a proof as either a JSON string or a path to a proof JSON file."""
|
|
62
|
+
text = proof_json.strip()
|
|
63
|
+
if text.startswith("{"):
|
|
64
|
+
return json.loads(text)
|
|
65
|
+
path = Path(text)
|
|
66
|
+
if path.exists() and path.is_file():
|
|
67
|
+
return load_proof(str(path))
|
|
68
|
+
# Last resort: try parsing as JSON anyway so the error is a clear JSON error.
|
|
69
|
+
return json.loads(text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _explorer_url(blockchain: str, tx_id: str) -> str:
|
|
73
|
+
"""Best-effort public block-explorer link for independent verification."""
|
|
74
|
+
b = (blockchain or "").lower()
|
|
75
|
+
if b in ("polygon", "matic"):
|
|
76
|
+
return f"https://polygonscan.com/tx/{tx_id}"
|
|
77
|
+
if b in ("bitcoin", "btc"):
|
|
78
|
+
return f"https://mempool.space/tx/{tx_id}"
|
|
79
|
+
if b in ("ethereum", "eth"):
|
|
80
|
+
return f"https://etherscan.io/tx/{tx_id}"
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _format_result(result: dict) -> str:
|
|
85
|
+
"""Human-readable headline + full structured result for the model to reason over."""
|
|
86
|
+
if result.get("verified"):
|
|
87
|
+
headline = "VERIFIED ✓ — " + result.get(
|
|
88
|
+
"message", "File hash matches the blockchain anchor."
|
|
89
|
+
)
|
|
90
|
+
explorer = _explorer_url(result.get("blockchain", ""), result.get("tx_id", ""))
|
|
91
|
+
if explorer:
|
|
92
|
+
headline += f"\nIndependently check the transaction: {explorer}"
|
|
93
|
+
else:
|
|
94
|
+
headline = "NOT VERIFIED ✗ — " + result.get(
|
|
95
|
+
"error", "Verification failed."
|
|
96
|
+
)
|
|
97
|
+
return headline + "\n\n" + json.dumps(result, indent=2)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- MCP tools ---------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@mcp.tool()
|
|
104
|
+
def compute_file_hash(file_path: str, algorithm: str = "sha256") -> str:
|
|
105
|
+
"""Compute the cryptographic hash of a local file.
|
|
106
|
+
|
|
107
|
+
The file is read and hashed locally; its contents are never uploaded or
|
|
108
|
+
transmitted. SHA-256 (the default) is the algorithm used by Bitcoin,
|
|
109
|
+
Polygon, and blockchain timestamp services such as ProofLedger. Use this to
|
|
110
|
+
obtain the fingerprint that a timestamp proof anchors, or to confirm a file
|
|
111
|
+
has not changed.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
file_path: Path to the file on the local machine.
|
|
115
|
+
algorithm: Hash algorithm (default "sha256"); any algorithm supported by
|
|
116
|
+
Python's hashlib (sha256, sha512, sha1, md5).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The hex-encoded hash digest.
|
|
120
|
+
"""
|
|
121
|
+
path = Path(file_path)
|
|
122
|
+
if not path.exists():
|
|
123
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
raise ValueError(f"Not a file: {file_path}")
|
|
126
|
+
return hash_file(str(path), algorithm)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@mcp.tool()
|
|
130
|
+
def verify_file(file_path: str, proof_path: str, algorithm: str = "sha256") -> str:
|
|
131
|
+
"""Verify a local file against a blockchain-anchored timestamp proof.
|
|
132
|
+
|
|
133
|
+
Recomputes the file's hash locally and checks it against the hash recorded
|
|
134
|
+
in the proof JSON. If a Merkle path is present, it recomputes the Merkle
|
|
135
|
+
root. Confirms a blockchain transaction reference is present. A passing
|
|
136
|
+
result means the file is byte-for-byte identical to the file that was
|
|
137
|
+
timestamped, and the proof points to a public transaction (on Polygon or
|
|
138
|
+
Bitcoin) that anyone can check on a block explorer.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
file_path: Path to the local file to verify.
|
|
142
|
+
proof_path: Path to the proof JSON file (as produced by ProofLedger or
|
|
143
|
+
any compatible blockchain timestamp service).
|
|
144
|
+
algorithm: Hash algorithm (default "sha256").
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A human-readable verification summary followed by the full structured
|
|
148
|
+
result as JSON.
|
|
149
|
+
"""
|
|
150
|
+
fp = Path(file_path)
|
|
151
|
+
pp = Path(proof_path)
|
|
152
|
+
if not fp.exists():
|
|
153
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
154
|
+
if not pp.exists():
|
|
155
|
+
raise FileNotFoundError(f"Proof file not found: {proof_path}")
|
|
156
|
+
file_hash = hash_file(str(fp), algorithm)
|
|
157
|
+
proof = load_proof(str(pp))
|
|
158
|
+
return _format_result(_verify_proof(file_hash, proof))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@mcp.tool()
|
|
162
|
+
def verify_hash(file_hash: str, proof_json: str) -> str:
|
|
163
|
+
"""Verify a known file hash against inline proof data.
|
|
164
|
+
|
|
165
|
+
Use this when you already have a file's SHA-256 hash and the proof content
|
|
166
|
+
(for example, pasted by the user) and do not need to read a file from disk.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
file_hash: The hex-encoded SHA-256 hash of the file.
|
|
170
|
+
proof_json: The proof as a JSON string, OR a path to a proof JSON file.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
A human-readable verification summary followed by the full structured
|
|
174
|
+
result as JSON.
|
|
175
|
+
"""
|
|
176
|
+
proof = _coerce_proof(proof_json)
|
|
177
|
+
return _format_result(_verify_proof(file_hash.strip(), proof))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@mcp.tool()
|
|
181
|
+
def explain_proof(proof_json: str) -> str:
|
|
182
|
+
"""Explain, in plain language, what a blockchain timestamp proof contains.
|
|
183
|
+
|
|
184
|
+
Does not require the original file. Reads the proof's metadata (blockchain,
|
|
185
|
+
transaction id, anchoring time, issuing service, whether a Merkle path is
|
|
186
|
+
present) and describes what it asserts and how to independently verify it on
|
|
187
|
+
a public block explorer.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
proof_json: The proof as a JSON string, OR a path to a proof JSON file.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A plain-language description of the proof.
|
|
194
|
+
"""
|
|
195
|
+
proof = _coerce_proof(proof_json)
|
|
196
|
+
blockchain = proof.get("blockchain", "unknown")
|
|
197
|
+
tx_id = proof.get("tx_id", "")
|
|
198
|
+
anchored_at = proof.get("anchored_at", "")
|
|
199
|
+
service = proof.get("service", "unknown")
|
|
200
|
+
proof_hash = proof.get("hash", "")
|
|
201
|
+
algorithm = proof.get("algorithm", "sha256")
|
|
202
|
+
has_merkle = bool(proof.get("merkle_path"))
|
|
203
|
+
|
|
204
|
+
lines = [
|
|
205
|
+
"This is a blockchain timestamp proof (proof of existence).",
|
|
206
|
+
"",
|
|
207
|
+
f" Recorded hash : {proof_hash or '(missing)'} ({algorithm})",
|
|
208
|
+
f" Blockchain : {blockchain}",
|
|
209
|
+
f" Transaction : {tx_id or '(missing — cannot verify anchor)'}",
|
|
210
|
+
f" Anchored at : {anchored_at or '(not stated)'}",
|
|
211
|
+
f" Issued by : {service}",
|
|
212
|
+
" Merkle path : "
|
|
213
|
+
+ (
|
|
214
|
+
"present — the hash is a leaf in a batched Merkle tree"
|
|
215
|
+
if has_merkle
|
|
216
|
+
else "none — the hash is anchored directly"
|
|
217
|
+
),
|
|
218
|
+
"",
|
|
219
|
+
"What it asserts: a file with the recorded hash existed no later than "
|
|
220
|
+
f"the time the {blockchain} transaction was mined. Any change to the "
|
|
221
|
+
"file produces a different hash, so the proof only matches the exact "
|
|
222
|
+
"original bytes.",
|
|
223
|
+
]
|
|
224
|
+
explorer = _explorer_url(blockchain, tx_id)
|
|
225
|
+
if explorer:
|
|
226
|
+
lines += ["", f"Independently verify the transaction: {explorer}"]
|
|
227
|
+
return "\n".join(lines)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@mcp.tool()
|
|
231
|
+
def create_proof(file_path: str, bitcoin: bool = False, send_filename: bool = True) -> str:
|
|
232
|
+
"""Create a blockchain timestamp proof for a local file, via ProofLedger.
|
|
233
|
+
|
|
234
|
+
This is the only tool here that uses the network, and the only one that
|
|
235
|
+
needs an account. Use it when the user wants to PROVE a file exists as of
|
|
236
|
+
now, rather than check an existing proof. The file's SHA-256 is computed
|
|
237
|
+
locally and only that digest is sent - the file itself never leaves the
|
|
238
|
+
machine. The hash is anchored on Polygon (included on every plan, free
|
|
239
|
+
tier included); Bitcoin anchoring is metered per anchor.
|
|
240
|
+
|
|
241
|
+
Requires a ProofLedger API key in the PROOFLEDGER_API_KEY environment
|
|
242
|
+
variable. If it is missing, this returns the steps to get a free one -
|
|
243
|
+
show them to the user rather than treating it as a failure.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
file_path: Path to the file to timestamp.
|
|
247
|
+
bitcoin: Also request Bitcoin anchoring. Metered per anchor, so the
|
|
248
|
+
proof returns marked REQUIRED until that anchor is paid for.
|
|
249
|
+
send_filename: Send the filename as a label so the proof is findable
|
|
250
|
+
in the dashboard. Set false to send only the hash.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
A plain-text summary of the created proof: its id, status, and the
|
|
254
|
+
URLs for its certificate and public verification page.
|
|
255
|
+
"""
|
|
256
|
+
path = Path(file_path)
|
|
257
|
+
if not path.exists() or not path.is_file():
|
|
258
|
+
return f"File not found: {file_path}"
|
|
259
|
+
|
|
260
|
+
digest = hash_file(str(path))
|
|
261
|
+
try:
|
|
262
|
+
proof = submit_proof(
|
|
263
|
+
digest,
|
|
264
|
+
filename=path.name if send_filename else None,
|
|
265
|
+
bitcoin=bitcoin,
|
|
266
|
+
)
|
|
267
|
+
except ProofLedgerError as exc:
|
|
268
|
+
return str(exc)
|
|
269
|
+
return summarize(proof)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def main() -> None:
|
|
273
|
+
"""Entry point — run the server over stdio (the transport MCP hosts use)."""
|
|
274
|
+
mcp.run()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=68.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "verify-proof"
|
|
7
|
-
version = "0.2.2"
|
|
8
|
-
description = "Verify blockchain-anchored timestamp proofs (ProofLedger, OpenTimestamps) — CLI + MCP server"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
license = "MIT"
|
|
11
|
-
requires-python = ">=3.10"
|
|
12
|
-
authors = [
|
|
13
|
-
{name = "Craig Solomon", email = "craig@fulcrumenterprises.tech"},
|
|
14
|
-
]
|
|
15
|
-
keywords = [
|
|
16
|
-
"blockchain", "timestamp", "verification", "proof-of-existence",
|
|
17
|
-
"sha256", "bitcoin", "polygon", "evidence", "copyright",
|
|
18
|
-
"chain-of-custody", "digital-forensics", "proofledger",
|
|
19
|
-
]
|
|
20
|
-
classifiers = [
|
|
21
|
-
"Development Status :: 3 - Alpha",
|
|
22
|
-
"Intended Audience :: Developers",
|
|
23
|
-
"Intended Audience :: Legal Industry",
|
|
24
|
-
"Programming Language :: Python :: 3",
|
|
25
|
-
"Topic :: Security :: Cryptography",
|
|
26
|
-
"Topic :: Utilities",
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
# Base install is dependency-free (Python standard library only).
|
|
30
|
-
# The MCP server is an optional extra: pip install "verify-proof[mcp]"
|
|
31
|
-
[project.optional-dependencies]
|
|
32
|
-
mcp = ["mcp>=1.2.0"]
|
|
33
|
-
|
|
34
|
-
[project.scripts]
|
|
35
|
-
verify-proof = "verify_proof:main"
|
|
36
|
-
verify-proof-mcp = "verify_proof_mcp:main"
|
|
37
|
-
|
|
38
|
-
[project.urls]
|
|
39
|
-
Homepage = "https://github.com/Fulcrum-Enterprises/verify-proof"
|
|
40
|
-
ProofLedger = "https://proofledger.io"
|
|
41
|
-
|
|
42
|
-
[tool.setuptools]
|
|
43
|
-
py-modules = ["verify_proof", "verify_proof_mcp"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|