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.
@@ -1,23 +1,36 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: verify-proof
3
- Version: 0.2.2
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 :: 3 - Alpha
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>=1.2.0; extra == "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
 
@@ -1,7 +1,9 @@
1
1
  LICENSE
2
2
  README.md
3
+ proofledger_api.py
3
4
  pyproject.toml
4
5
  verify_proof.py
6
+ verify_proof_mcp.py
5
7
  verify_proof.egg-info/PKG-INFO
6
8
  verify_proof.egg-info/SOURCES.txt
7
9
  verify_proof.egg-info/dependency_links.txt
@@ -0,0 +1,3 @@
1
+
2
+ [mcp]
3
+ mcp<2,>=1.2.0
@@ -1,2 +1,3 @@
1
+ proofledger_api
1
2
  verify_proof
2
3
  verify_proof_mcp
@@ -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 ProofAnchor and ProofLedger.
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 (ProofAnchor)
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
- - ProofAnchor (proofanchor.com) — proof of creation for digital content creators
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
- Both services anchor SHA-256 hashes to Bitcoin and Polygon blockchains, creating
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
- ProofAnchor, ProofLedger, and most blockchain timestamp services.
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 ProofAnchor, ProofLedger, and OpenTimestamps proofs."
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
- " ProofAnchor (creators): https://proofanchor.com\n"
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"]
@@ -1,3 +0,0 @@
1
-
2
- [mcp]
3
- mcp>=1.2.0
File without changes
File without changes