orphograph 0.1.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.
- orphograph-0.1.0/LICENSE +21 -0
- orphograph-0.1.0/PKG-INFO +161 -0
- orphograph-0.1.0/README.md +117 -0
- orphograph-0.1.0/orphograph/__init__.py +175 -0
- orphograph-0.1.0/orphograph/__main__.py +5 -0
- orphograph-0.1.0/orphograph/_cli.py +129 -0
- orphograph-0.1.0/orphograph/_client.py +148 -0
- orphograph-0.1.0/orphograph/_merkle.py +493 -0
- orphograph-0.1.0/orphograph/py.typed +0 -0
- orphograph-0.1.0/orphograph.egg-info/PKG-INFO +161 -0
- orphograph-0.1.0/orphograph.egg-info/SOURCES.txt +15 -0
- orphograph-0.1.0/orphograph.egg-info/dependency_links.txt +1 -0
- orphograph-0.1.0/orphograph.egg-info/entry_points.txt +2 -0
- orphograph-0.1.0/orphograph.egg-info/top_level.txt +1 -0
- orphograph-0.1.0/pyproject.toml +40 -0
- orphograph-0.1.0/setup.cfg +4 -0
- orphograph-0.1.0/tests/test_sdk_client.py +411 -0
orphograph-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the Orphograph contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orphograph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Anchor folders to Bitcoin via the Orphograph hosted service. File contents stay on the local machine.
|
|
5
|
+
Author: the Orphograph contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 the Orphograph contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://orphograph.com
|
|
29
|
+
Project-URL: Documentation, https://orphograph.com/method/architecture.html
|
|
30
|
+
Keywords: bitcoin,opentimestamps,merkle,notarization,provenance
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Topic :: Security :: Cryptography
|
|
39
|
+
Classifier: Topic :: System :: Archiving
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# orphograph
|
|
46
|
+
|
|
47
|
+
A Python SDK for anchoring a local folder to the Bitcoin chain through the
|
|
48
|
+
[Orphograph](https://orphograph.com) hosted service.
|
|
49
|
+
|
|
50
|
+
- License: MIT
|
|
51
|
+
- Python: 3.9 or newer
|
|
52
|
+
- Runtime dependencies: Python standard library only
|
|
53
|
+
|
|
54
|
+
## Privacy contract
|
|
55
|
+
|
|
56
|
+
The library does not transmit file contents. For each file the SDK reads
|
|
57
|
+
the bytes locally, streams them through SHA-256 in one megabyte chunks,
|
|
58
|
+
and commits the digest into an RFC 6962 Merkle leaf bound to the file's
|
|
59
|
+
POSIX relative path. Only the resulting manifest (paths, per-file digests,
|
|
60
|
+
leaf hashes, and the 32-byte root) is sent across the network. The
|
|
61
|
+
verification path is symmetric: the root is recomputed locally from the
|
|
62
|
+
folder on disk and compared to the root recorded in the receipt.
|
|
63
|
+
|
|
64
|
+
The Merkle module is a verbatim copy of the server's reference
|
|
65
|
+
implementation, carrying the source SHA-256 in its header so divergence
|
|
66
|
+
from the canonical algorithm is immediately visible.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
pip install orphograph
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Anchor a folder
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from orphograph import anchor_folder
|
|
78
|
+
|
|
79
|
+
result = anchor_folder("/path/to/folder")
|
|
80
|
+
# {
|
|
81
|
+
# "receipt_id": "...",
|
|
82
|
+
# "root_hex": "...",
|
|
83
|
+
# "leaf_count": 42,
|
|
84
|
+
# "calendars_ok": 5,
|
|
85
|
+
# "calendars_total": 5,
|
|
86
|
+
# }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Optional arguments:
|
|
90
|
+
|
|
91
|
+
| Argument | Purpose |
|
|
92
|
+
| -------------- | -------------------------------------------------------- |
|
|
93
|
+
| `server_url` | Base URL of the service (default `https://orphograph.com`). |
|
|
94
|
+
| `api_key` | Sent as `X-Orpho-Api-Key` when present. |
|
|
95
|
+
| `client_label` | Short free-form label persisted with the receipt. |
|
|
96
|
+
| `exclude` | Sequence of `fnmatch` patterns. `None` selects the default deny list (OS detritus, editor backups, build caches). Passing `[]` disables exclusion. |
|
|
97
|
+
|
|
98
|
+
## Verify a folder
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from orphograph import verify_folder
|
|
102
|
+
|
|
103
|
+
ok = verify_folder("/path/to/folder", receipt_id="...")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The folder is walked locally, the Merkle root is recomputed, and the SDK
|
|
107
|
+
returns `True` only if the recomputed root equals the root recorded in
|
|
108
|
+
the receipt's manifest.
|
|
109
|
+
|
|
110
|
+
## Inclusion proofs
|
|
111
|
+
|
|
112
|
+
A folder receipt can be queried for a proof that a single file belonged
|
|
113
|
+
to the anchored tree. The proof is verified locally; no further network
|
|
114
|
+
call is required to confirm it.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from orphograph import inclusion_proof, verify_inclusion
|
|
118
|
+
|
|
119
|
+
proof = inclusion_proof(receipt_id="...", path="sub/photo.jpg")
|
|
120
|
+
ok = verify_inclusion(
|
|
121
|
+
file_path="/path/to/sub/photo.jpg",
|
|
122
|
+
rel_path="sub/photo.jpg",
|
|
123
|
+
proof=proof["proof"],
|
|
124
|
+
root_hex=proof["root_hex"],
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Command line
|
|
129
|
+
|
|
130
|
+
The package installs an `orphograph` console script and is also runnable
|
|
131
|
+
as a module.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
python -m orphograph anchor /path/to/folder
|
|
135
|
+
python -m orphograph verify /path/to/folder <receipt_id>
|
|
136
|
+
python -m orphograph inclusion-proof <receipt_id> <posix/rel/path>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Each subcommand writes a single line of JSON to standard output. The
|
|
140
|
+
`verify` subcommand exits with status `0` on a match and `1` on a
|
|
141
|
+
mismatch.
|
|
142
|
+
|
|
143
|
+
Environment variables:
|
|
144
|
+
|
|
145
|
+
| Variable | Purpose |
|
|
146
|
+
| ------------------ | --------------------------------------------- |
|
|
147
|
+
| `ORPHO_SERVER_URL` | Default base URL. |
|
|
148
|
+
| `ORPHO_API_KEY` | Default API key (sent as `X-Orpho-Api-Key`). |
|
|
149
|
+
|
|
150
|
+
## Algorithm
|
|
151
|
+
|
|
152
|
+
The Merkle construction is RFC 6962 with a domain-separated leaf
|
|
153
|
+
(`0x00 || rel_path_utf8 || 0x00 || file_sha256`) and a domain-separated
|
|
154
|
+
internal node (`0x01 || left || right`). Odd-level remainders are
|
|
155
|
+
promoted, never duplicated, to avoid the second-preimage ambiguity of
|
|
156
|
+
the duplicate-last construction. The algorithm tag
|
|
157
|
+
`orphograph-merkle-v1-rfc6962` is embedded in every manifest.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# orphograph
|
|
2
|
+
|
|
3
|
+
A Python SDK for anchoring a local folder to the Bitcoin chain through the
|
|
4
|
+
[Orphograph](https://orphograph.com) hosted service.
|
|
5
|
+
|
|
6
|
+
- License: MIT
|
|
7
|
+
- Python: 3.9 or newer
|
|
8
|
+
- Runtime dependencies: Python standard library only
|
|
9
|
+
|
|
10
|
+
## Privacy contract
|
|
11
|
+
|
|
12
|
+
The library does not transmit file contents. For each file the SDK reads
|
|
13
|
+
the bytes locally, streams them through SHA-256 in one megabyte chunks,
|
|
14
|
+
and commits the digest into an RFC 6962 Merkle leaf bound to the file's
|
|
15
|
+
POSIX relative path. Only the resulting manifest (paths, per-file digests,
|
|
16
|
+
leaf hashes, and the 32-byte root) is sent across the network. The
|
|
17
|
+
verification path is symmetric: the root is recomputed locally from the
|
|
18
|
+
folder on disk and compared to the root recorded in the receipt.
|
|
19
|
+
|
|
20
|
+
The Merkle module is a verbatim copy of the server's reference
|
|
21
|
+
implementation, carrying the source SHA-256 in its header so divergence
|
|
22
|
+
from the canonical algorithm is immediately visible.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pip install orphograph
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Anchor a folder
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from orphograph import anchor_folder
|
|
34
|
+
|
|
35
|
+
result = anchor_folder("/path/to/folder")
|
|
36
|
+
# {
|
|
37
|
+
# "receipt_id": "...",
|
|
38
|
+
# "root_hex": "...",
|
|
39
|
+
# "leaf_count": 42,
|
|
40
|
+
# "calendars_ok": 5,
|
|
41
|
+
# "calendars_total": 5,
|
|
42
|
+
# }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Optional arguments:
|
|
46
|
+
|
|
47
|
+
| Argument | Purpose |
|
|
48
|
+
| -------------- | -------------------------------------------------------- |
|
|
49
|
+
| `server_url` | Base URL of the service (default `https://orphograph.com`). |
|
|
50
|
+
| `api_key` | Sent as `X-Orpho-Api-Key` when present. |
|
|
51
|
+
| `client_label` | Short free-form label persisted with the receipt. |
|
|
52
|
+
| `exclude` | Sequence of `fnmatch` patterns. `None` selects the default deny list (OS detritus, editor backups, build caches). Passing `[]` disables exclusion. |
|
|
53
|
+
|
|
54
|
+
## Verify a folder
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from orphograph import verify_folder
|
|
58
|
+
|
|
59
|
+
ok = verify_folder("/path/to/folder", receipt_id="...")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The folder is walked locally, the Merkle root is recomputed, and the SDK
|
|
63
|
+
returns `True` only if the recomputed root equals the root recorded in
|
|
64
|
+
the receipt's manifest.
|
|
65
|
+
|
|
66
|
+
## Inclusion proofs
|
|
67
|
+
|
|
68
|
+
A folder receipt can be queried for a proof that a single file belonged
|
|
69
|
+
to the anchored tree. The proof is verified locally; no further network
|
|
70
|
+
call is required to confirm it.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from orphograph import inclusion_proof, verify_inclusion
|
|
74
|
+
|
|
75
|
+
proof = inclusion_proof(receipt_id="...", path="sub/photo.jpg")
|
|
76
|
+
ok = verify_inclusion(
|
|
77
|
+
file_path="/path/to/sub/photo.jpg",
|
|
78
|
+
rel_path="sub/photo.jpg",
|
|
79
|
+
proof=proof["proof"],
|
|
80
|
+
root_hex=proof["root_hex"],
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Command line
|
|
85
|
+
|
|
86
|
+
The package installs an `orphograph` console script and is also runnable
|
|
87
|
+
as a module.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
python -m orphograph anchor /path/to/folder
|
|
91
|
+
python -m orphograph verify /path/to/folder <receipt_id>
|
|
92
|
+
python -m orphograph inclusion-proof <receipt_id> <posix/rel/path>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Each subcommand writes a single line of JSON to standard output. The
|
|
96
|
+
`verify` subcommand exits with status `0` on a match and `1` on a
|
|
97
|
+
mismatch.
|
|
98
|
+
|
|
99
|
+
Environment variables:
|
|
100
|
+
|
|
101
|
+
| Variable | Purpose |
|
|
102
|
+
| ------------------ | --------------------------------------------- |
|
|
103
|
+
| `ORPHO_SERVER_URL` | Default base URL. |
|
|
104
|
+
| `ORPHO_API_KEY` | Default API key (sent as `X-Orpho-Api-Key`). |
|
|
105
|
+
|
|
106
|
+
## Algorithm
|
|
107
|
+
|
|
108
|
+
The Merkle construction is RFC 6962 with a domain-separated leaf
|
|
109
|
+
(`0x00 || rel_path_utf8 || 0x00 || file_sha256`) and a domain-separated
|
|
110
|
+
internal node (`0x01 || left || right`). Odd-level remainders are
|
|
111
|
+
promoted, never duplicated, to avoid the second-preimage ambiguity of
|
|
112
|
+
the duplicate-last construction. The algorithm tag
|
|
113
|
+
`orphograph-merkle-v1-rfc6962` is embedded in every manifest.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""orphograph — Python SDK for anchoring folders to Bitcoin.
|
|
2
|
+
|
|
3
|
+
The SDK constructs an RFC 6962-style Merkle tree from a local folder, then
|
|
4
|
+
submits only the manifest (paths, per-file SHA-256 digests, leaf hashes,
|
|
5
|
+
and a 32-byte root) to the Orphograph hosted service. File bodies do not
|
|
6
|
+
cross the network at any point in this module.
|
|
7
|
+
|
|
8
|
+
Public API:
|
|
9
|
+
|
|
10
|
+
anchor_folder(folder_path, server_url=..., api_key=..., client_label=..., exclude=...)
|
|
11
|
+
verify_folder(folder_path, receipt_id, server_url=...)
|
|
12
|
+
inclusion_proof(receipt_id, path, server_url=...)
|
|
13
|
+
verify_inclusion(file_path, rel_path, proof, root_hex)
|
|
14
|
+
|
|
15
|
+
Algorithm tag: ``orphograph-merkle-v1-rfc6962``.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional, Sequence
|
|
22
|
+
|
|
23
|
+
from . import _client
|
|
24
|
+
from ._client import OrphographError
|
|
25
|
+
from ._merkle import ALGORITHM, MerkleTree
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ALGORITHM",
|
|
29
|
+
"OrphographError",
|
|
30
|
+
"anchor_folder",
|
|
31
|
+
"verify_folder",
|
|
32
|
+
"inclusion_proof",
|
|
33
|
+
"verify_inclusion",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def anchor_folder(
|
|
40
|
+
folder_path: str,
|
|
41
|
+
*,
|
|
42
|
+
server_url: str = _client.DEFAULT_SERVER_URL,
|
|
43
|
+
api_key: Optional[str] = None,
|
|
44
|
+
client_label: Optional[str] = None,
|
|
45
|
+
exclude: Optional[Sequence[str]] = None,
|
|
46
|
+
) -> dict:
|
|
47
|
+
"""Anchor a folder to Bitcoin via the Orphograph hosted service.
|
|
48
|
+
|
|
49
|
+
The Merkle tree is constructed locally; only the resulting manifest
|
|
50
|
+
(paths + SHA-256 digests + leaf hashes + root) is transmitted to the
|
|
51
|
+
server. File contents stay on disk.
|
|
52
|
+
|
|
53
|
+
Returns a dict with keys: ``receipt_id``, ``root_hex``, ``leaf_count``,
|
|
54
|
+
``calendars_ok``, ``calendars_total``.
|
|
55
|
+
"""
|
|
56
|
+
root = Path(folder_path)
|
|
57
|
+
if not root.is_dir():
|
|
58
|
+
raise ValueError(f"not a directory: {folder_path}")
|
|
59
|
+
excl_list = list(exclude) if exclude is not None else None
|
|
60
|
+
tree = MerkleTree.from_folder(root, exclude=excl_list)
|
|
61
|
+
manifest = tree.manifest()
|
|
62
|
+
response = _client.post_anchor_folder(
|
|
63
|
+
manifest,
|
|
64
|
+
server_url=server_url,
|
|
65
|
+
api_key=api_key,
|
|
66
|
+
client_label=client_label,
|
|
67
|
+
)
|
|
68
|
+
return {
|
|
69
|
+
"receipt_id": response.get("receipt_id"),
|
|
70
|
+
"root_hex": response.get("root_hex", tree.root_hex()),
|
|
71
|
+
"leaf_count": response.get("leaf_count", len(manifest["leaves"])),
|
|
72
|
+
"calendars_ok": response.get("calendars_ok"),
|
|
73
|
+
"calendars_total": response.get("calendars_total"),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def verify_folder(
|
|
78
|
+
folder_path: str,
|
|
79
|
+
receipt_id: str,
|
|
80
|
+
*,
|
|
81
|
+
server_url: str = _client.DEFAULT_SERVER_URL,
|
|
82
|
+
api_key: Optional[str] = None,
|
|
83
|
+
exclude: Optional[list] = None,
|
|
84
|
+
) -> bool:
|
|
85
|
+
"""Verify a local folder against a previously anchored receipt.
|
|
86
|
+
|
|
87
|
+
Rebuilds the Merkle root locally from the folder on disk and compares
|
|
88
|
+
it byte-for-byte to the root recorded in the receipt's manifest. The
|
|
89
|
+
server is consulted only to fetch the manifest; file contents are not
|
|
90
|
+
transmitted.
|
|
91
|
+
"""
|
|
92
|
+
root = Path(folder_path)
|
|
93
|
+
if not root.is_dir():
|
|
94
|
+
raise ValueError(f"not a directory: {folder_path}")
|
|
95
|
+
response = _client.get_verify_folder(receipt_id, server_url=server_url, api_key=api_key)
|
|
96
|
+
manifest = response.get("manifest") or {}
|
|
97
|
+
server_root = manifest.get("root_hex")
|
|
98
|
+
if not server_root:
|
|
99
|
+
return False
|
|
100
|
+
# The manifest is authoritative for its own scope.
|
|
101
|
+
#
|
|
102
|
+
# Previously this mirrored whatever the CALLER passed, which only moved the
|
|
103
|
+
# problem: a folder anchored with custom excludes still could not verify
|
|
104
|
+
# unless the verifier independently remembered the exact list used at
|
|
105
|
+
# capture, months earlier, possibly on another machine. That is not a
|
|
106
|
+
# property evidence should have.
|
|
107
|
+
#
|
|
108
|
+
# Manifests now record the effective patterns (see merkle.build_scope), so
|
|
109
|
+
# they are read from the manifest when present. The caller's `exclude` is
|
|
110
|
+
# only a fallback for manifests issued before scope existed.
|
|
111
|
+
effective_exclude = exclude
|
|
112
|
+
scope = manifest.get("scope")
|
|
113
|
+
if isinstance(scope, dict) and isinstance(scope.get("exclude"), list):
|
|
114
|
+
effective_exclude = list(scope["exclude"])
|
|
115
|
+
tree = MerkleTree.from_folder(root, exclude=effective_exclude)
|
|
116
|
+
return tree.root_hex() == server_root
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def inclusion_proof(
|
|
120
|
+
receipt_id: str,
|
|
121
|
+
path: str,
|
|
122
|
+
*,
|
|
123
|
+
server_url: str = _client.DEFAULT_SERVER_URL,
|
|
124
|
+
api_key: Optional[str] = None,
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Fetch an inclusion proof for one POSIX-relative path in a folder receipt.
|
|
127
|
+
|
|
128
|
+
Returns the server's JSON payload: ``receipt_id``, ``root_hex``,
|
|
129
|
+
``path``, ``file_sha256_hex``, ``proof`` (list of ``[direction, hex]``).
|
|
130
|
+
"""
|
|
131
|
+
return _client.get_inclusion_proof(
|
|
132
|
+
receipt_id, path, server_url=server_url, api_key=api_key
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def verify_inclusion(
|
|
137
|
+
file_path: str,
|
|
138
|
+
rel_path: str,
|
|
139
|
+
proof: Sequence,
|
|
140
|
+
root_hex: str,
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""Verify locally that a file was included in an anchored folder.
|
|
143
|
+
|
|
144
|
+
Reads the file from disk, computes its SHA-256, and walks the proof
|
|
145
|
+
upward against the supplied root. No network call is made.
|
|
146
|
+
|
|
147
|
+
Raises ``FileNotFoundError`` when ``file_path`` does not exist (or is
|
|
148
|
+
not a regular file). A missing local file is an I/O precondition
|
|
149
|
+
failure, not a "not included" verdict — for a notary, a distinguishable
|
|
150
|
+
error beats a silent ``False`` (audit D7; matches the Node SDK, whose
|
|
151
|
+
``verifyInclusion`` rejects with the filesystem error). A malformed
|
|
152
|
+
proof or root still returns ``False`` per VERIFIER_SPEC §4.1.
|
|
153
|
+
"""
|
|
154
|
+
p = Path(file_path)
|
|
155
|
+
if not p.is_file():
|
|
156
|
+
raise FileNotFoundError(f"no such file: {file_path}")
|
|
157
|
+
h = hashlib.sha256()
|
|
158
|
+
with p.open("rb") as f:
|
|
159
|
+
while True:
|
|
160
|
+
chunk = f.read(1024 * 1024)
|
|
161
|
+
if not chunk:
|
|
162
|
+
break
|
|
163
|
+
h.update(chunk)
|
|
164
|
+
file_hash = h.digest()
|
|
165
|
+
try:
|
|
166
|
+
root = bytes.fromhex(root_hex)
|
|
167
|
+
except (ValueError, TypeError):
|
|
168
|
+
return False
|
|
169
|
+
# Coerce proof entries (which may arrive as lists from JSON) to tuples.
|
|
170
|
+
normalised: list = []
|
|
171
|
+
for step in proof:
|
|
172
|
+
if not isinstance(step, (list, tuple)) or len(step) != 2:
|
|
173
|
+
return False
|
|
174
|
+
normalised.append((step[0], step[1]))
|
|
175
|
+
return MerkleTree.verify_inclusion(file_hash, rel_path, normalised, root)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""orphograph._cli — argparse-based command-line interface.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
anchor <folder> Anchor a folder; prints one line of JSON.
|
|
6
|
+
verify <folder> <receipt_id> Verify a folder against a receipt.
|
|
7
|
+
inclusion-proof <rid> <path> Fetch an inclusion proof; prints JSON.
|
|
8
|
+
|
|
9
|
+
Both ``anchor`` and ``verify`` accept repeatable ``--exclude GLOB`` flags.
|
|
10
|
+
A folder anchored with custom excludes can only re-derive the same Merkle
|
|
11
|
+
root when verified with the SAME excludes (AUDIT_VERIFIER_DRIFT D2). Since
|
|
12
|
+
Wedge 01 the manifest records them in its ``scope`` block and ``verify``
|
|
13
|
+
reads them from there (the manifest is authoritative — VERIFIER_SPEC §4.2);
|
|
14
|
+
the ``--exclude`` flag on ``verify`` only applies to manifests that carry no
|
|
15
|
+
scope block (issued before scope existed).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from typing import List, Optional
|
|
24
|
+
|
|
25
|
+
from . import anchor_folder, inclusion_proof, verify_folder
|
|
26
|
+
from ._client import DEFAULT_SERVER_URL, OrphographError
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _env_server() -> str:
|
|
30
|
+
return os.environ.get("ORPHO_SERVER_URL", DEFAULT_SERVER_URL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _env_api_key() -> Optional[str]:
|
|
34
|
+
val = os.environ.get("ORPHO_API_KEY", "").strip()
|
|
35
|
+
return val or None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="orphograph",
|
|
41
|
+
description="Anchor folders to Bitcoin via the Orphograph service.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--server-url",
|
|
45
|
+
default=None,
|
|
46
|
+
help="Base URL of the Orphograph service (default: ORPHO_SERVER_URL or https://orphograph.com).",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--api-key",
|
|
50
|
+
default=None,
|
|
51
|
+
help="Optional API key (default: ORPHO_API_KEY).",
|
|
52
|
+
)
|
|
53
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
54
|
+
|
|
55
|
+
exclude_help = (
|
|
56
|
+
"Glob pattern to exclude (repeatable). Supplying any --exclude "
|
|
57
|
+
"REPLACES the default deny-list rather than extending it. "
|
|
58
|
+
"On verify: applies only to manifests without a scope block — a "
|
|
59
|
+
"manifest's recorded scope.exclude is authoritative (VERIFIER_SPEC §4.2)."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
p_anchor = sub.add_parser("anchor", help="Anchor a folder.")
|
|
63
|
+
p_anchor.add_argument("folder", help="Local folder to anchor.")
|
|
64
|
+
p_anchor.add_argument("--label", default=None, help="Optional short client label.")
|
|
65
|
+
p_anchor.add_argument(
|
|
66
|
+
"--exclude", action="append", default=None, metavar="GLOB", help=exclude_help
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
p_verify = sub.add_parser("verify", help="Verify a folder against a receipt.")
|
|
70
|
+
p_verify.add_argument("folder", help="Local folder to verify.")
|
|
71
|
+
p_verify.add_argument("receipt_id", help="Receipt id returned at anchor time.")
|
|
72
|
+
p_verify.add_argument(
|
|
73
|
+
"--exclude", action="append", default=None, metavar="GLOB", help=exclude_help
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
p_proof = sub.add_parser("inclusion-proof", help="Fetch an inclusion proof.")
|
|
77
|
+
p_proof.add_argument("receipt_id", help="Folder receipt id.")
|
|
78
|
+
p_proof.add_argument("path", help="POSIX relative path inside the folder.")
|
|
79
|
+
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
84
|
+
parser = _build_parser()
|
|
85
|
+
args = parser.parse_args(argv)
|
|
86
|
+
server_url = args.server_url or _env_server()
|
|
87
|
+
api_key = args.api_key if args.api_key is not None else _env_api_key()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
if args.command == "anchor":
|
|
91
|
+
result = anchor_folder(
|
|
92
|
+
args.folder,
|
|
93
|
+
server_url=server_url,
|
|
94
|
+
api_key=api_key,
|
|
95
|
+
client_label=args.label,
|
|
96
|
+
exclude=args.exclude,
|
|
97
|
+
)
|
|
98
|
+
sys.stdout.write(json.dumps(result) + "\n")
|
|
99
|
+
return 0
|
|
100
|
+
if args.command == "verify":
|
|
101
|
+
ok = verify_folder(
|
|
102
|
+
args.folder,
|
|
103
|
+
args.receipt_id,
|
|
104
|
+
server_url=server_url,
|
|
105
|
+
api_key=api_key,
|
|
106
|
+
exclude=args.exclude,
|
|
107
|
+
)
|
|
108
|
+
sys.stdout.write(json.dumps({"match": bool(ok)}) + "\n")
|
|
109
|
+
return 0 if ok else 1
|
|
110
|
+
if args.command == "inclusion-proof":
|
|
111
|
+
proof = inclusion_proof(
|
|
112
|
+
args.receipt_id,
|
|
113
|
+
args.path,
|
|
114
|
+
server_url=server_url,
|
|
115
|
+
api_key=api_key,
|
|
116
|
+
)
|
|
117
|
+
sys.stdout.write(json.dumps(proof) + "\n")
|
|
118
|
+
return 0
|
|
119
|
+
except OrphographError as e:
|
|
120
|
+
sys.stderr.write(json.dumps({"error": e.message, "status": e.status}) + "\n")
|
|
121
|
+
return 2
|
|
122
|
+
except (OSError, ValueError) as e:
|
|
123
|
+
sys.stderr.write(json.dumps({"error": str(e)}) + "\n")
|
|
124
|
+
return 2
|
|
125
|
+
return 2
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
raise SystemExit(main())
|