obsign-verify 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,27 @@
1
+ """Verify an Obsign receipt: re-derive the number yourself, offline.
2
+
3
+ from obsign_verify import verify, load_receipt
4
+ result = verify(load_receipt(open("receipt.json", encoding="utf-8").read()))
5
+
6
+ Imports NOTHING from the engine that produced the receipt. That independence is
7
+ the point -- if this package imported the producer, "it verifies" would mean "the
8
+ producer agrees with itself".
9
+ """
10
+
11
+ from .canonical import canonical_sha256, claim_of, load_receipt
12
+ from .verify import verify
13
+
14
+ __all__ = ["verify", "load_receipt", "claim_of", "canonical_sha256", "__version__"]
15
+ __version__ = "0.1.0"
16
+
17
+
18
+ def data_path(*parts: str):
19
+ """Absolute path to a file shipped INSIDE the package.
20
+
21
+ The conformance vectors and the challenge bundles are package data, not repo
22
+ files. A `pip install` has to give a stranger everything the README tells them
23
+ to run -- the first cut shipped neither, so `obsign-verify challenge/...` and
24
+ `pytest` both failed for anyone who installed rather than cloned.
25
+ """
26
+ from pathlib import Path
27
+ return Path(__file__).resolve().parent.joinpath("data", *parts)
@@ -0,0 +1,80 @@
1
+ """Canonical form and the claim hash.
2
+
3
+ The whole verifier stands on this file. If canonicalisation is off by one byte, a
4
+ genuine receipt reports as tampered and the product's central promise inverts.
5
+
6
+ THE INT/FLOAT TRAP, which the spec calls out by name
7
+
8
+ Python writes `1.0` for a float and `1` for an int, and the two canonicalise to
9
+ DIFFERENT bytes and therefore different hashes. A reader that erases the
10
+ distinction -- notably JavaScript's `JSON.parse`, where every number is a double --
11
+ will canonicalise `gamma: 0.0` as `0` and report an honest receipt as tampered.
12
+
13
+ Python's `json` preserves it, so this implementation can parse normally. That is
14
+ luck, not design, and `tests/` pins it so a future "cleanup" to a float-normalising
15
+ parser fails loudly instead of silently rejecting every receipt in the field.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ from typing import Any
23
+
24
+ #: Fields the spec excludes from the claim. `env` is informational, `signature`
25
+ #: and `case` are post-hoc, `receipt_sha256` is the hash itself, `_`-prefixed keys
26
+ #: are helpers. A verifier that hashed these would report a genuine receipt as
27
+ #: tampered the moment anyone recorded a different platform.
28
+ NON_CLAIM = ("receipt_sha256", "env", "signature", "case")
29
+
30
+
31
+ def claim_of(receipt: dict) -> dict:
32
+ """The subset of a receipt that its `receipt_sha256` covers."""
33
+ return {k: v for k, v in receipt.items()
34
+ if k not in NON_CLAIM and not k.startswith("_")}
35
+
36
+
37
+ def canonical_bytes(obj: Any) -> bytes:
38
+ """Canonical JSON exactly as docs/SPEC.md defines it.
39
+
40
+ `allow_nan=False` is load-bearing, not tidiness: NaN and Infinity have no JSON
41
+ representation, so permitting them would let two different receipts share a
42
+ canonical form.
43
+ """
44
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"),
45
+ ensure_ascii=True, allow_nan=False).encode("utf-8")
46
+
47
+
48
+ def canonical_sha256(obj: Any) -> str:
49
+ return hashlib.sha256(canonical_bytes(obj)).hexdigest()
50
+
51
+
52
+ def load_receipt(text: str) -> dict:
53
+ """Parse receipt TEXT into a dict, preserving the int/float distinction.
54
+
55
+ Takes text rather than a path so the caller controls I/O, and so a port to
56
+ another language has an obvious seam at the only place the trap can bite.
57
+ """
58
+ obj = json.loads(text)
59
+ if not isinstance(obj, dict):
60
+ raise ValueError("a receipt must be a JSON object")
61
+ return obj
62
+
63
+
64
+ def integrity(receipt: dict) -> tuple[bool, str]:
65
+ """Step 1 of the trust ladder: does `receipt_sha256` recompute from the claim?
66
+
67
+ Returns (ok, detail). Never raises on a hostile receipt -- a verifier that
68
+ crashes on malformed input has failed open in the eyes of whoever supplied it.
69
+ """
70
+ stated = receipt.get("receipt_sha256")
71
+ if not isinstance(stated, str) or not stated:
72
+ return False, "no receipt_sha256 to check against"
73
+ try:
74
+ recomputed = canonical_sha256(claim_of(receipt))
75
+ except (ValueError, TypeError) as exc:
76
+ return False, f"claim is not canonicalisable ({type(exc).__name__}: {exc})"
77
+ if recomputed != stated:
78
+ return False, (f"INTEGRITY FAIL - states {stated[:16]}.., "
79
+ f"recomputes {recomputed[:16]}..")
80
+ return True, "integrity OK"
obsign_verify/cli.py ADDED
@@ -0,0 +1,121 @@
1
+ """`obsign-verify receipt.json` -- the one command PG-1 is about.
2
+
3
+ Exit code is the interface: 0 means every named receipt verified, 1 means at least
4
+ one did not. A script wrapping this must be able to branch on that alone.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from . import __version__
15
+ from .canonical import load_receipt
16
+ from .verify import verify
17
+
18
+
19
+ def _report(path: Path, res: dict, quiet: bool) -> None:
20
+ if quiet:
21
+ return
22
+ mark = "VERIFIED" if res["verified"] else "REFUSED"
23
+ print(f" [{mark:^8}] {path.name}")
24
+ print(f" integrity {'ok' if res['integrity'] else 'FAIL'}")
25
+ print(f" re-derived {'ok' if res['reproduced'] else 'FAIL'}")
26
+ sig = res.get("signature") or {}
27
+ if not sig.get("present"):
28
+ print(" signature absent (integrity and re-derivation still hold)")
29
+ else:
30
+ who = sig.get("attributed_signer")
31
+ # Never print a signer name the signature did not cover. A v1 signature's
32
+ # name can be rewritten by anyone with a text editor and no key.
33
+ ident = (f"attributed to {who!r}" if sig.get("identity_bound")
34
+ else f"NOT identity-bound (file claims {sig.get('claimed_signer')!r})")
35
+ print(f" signature {'ok' if sig.get('valid') else 'FAIL'} - {ident}")
36
+ for note in res["notes"]:
37
+ print(f" - {note}")
38
+
39
+
40
+ def _self_check(quiet: bool) -> int:
41
+ """Run the challenge that ships INSIDE the package.
42
+
43
+ A stranger who `pip install`ed has no `challenge/` directory to point at, so the
44
+ README's "run the forgeries" instruction was unfollowable for exactly the people
45
+ it was written for. This needs no files of their own and no arguments.
46
+
47
+ Exit 0 means every bundle got the verdict it declares -- the honest ones
48
+ verified, and every forgery refused. Refusing the forgeries is the job, so a
49
+ clean run is NOT "9 verified".
50
+ """
51
+ from . import data_path
52
+ bundles = sorted(data_path("challenge", "bundles").glob("*/receipt.json"))
53
+ if not bundles:
54
+ print("no bundles shipped with this package -- the install is incomplete",
55
+ file=sys.stderr)
56
+ return 1
57
+
58
+ wrong = 0
59
+ for path in bundles:
60
+ receipt = load_receipt(path.read_text(encoding="utf-8"))
61
+ declared = receipt.get("_challenge", {}).get("expect_verified")
62
+ got = verify(receipt)["verified"]
63
+ ok = got is declared
64
+ wrong += not ok
65
+ if not quiet:
66
+ print(f" {'ok ' if ok else '!! '}{path.parent.name:<36}"
67
+ f"{'VERIFIED' if got else 'REFUSED':<9}"
68
+ f"(expected {'VERIFIED' if declared else 'REFUSED'})")
69
+ if not quiet:
70
+ n = len(bundles)
71
+ print()
72
+ print(f"{n - wrong}/{n} bundles behaved as declared."
73
+ + ("" if wrong else " Every forgery was refused, including the resealed"
74
+ " one whose signature is intact and whose claim is false."))
75
+ return 1 if wrong else 0
76
+
77
+
78
+ def main(argv: list[str] | None = None) -> int:
79
+ ap = argparse.ArgumentParser(
80
+ prog="obsign-verify",
81
+ description="Re-derive an Obsign receipt's claim on your own machine.")
82
+ ap.add_argument("receipts", nargs="*", type=Path)
83
+ ap.add_argument("--self-check", action="store_true",
84
+ help="verify the bundled challenge: 2 honest receipts and 7 "
85
+ "forgeries that must be refused. Needs no files of your own.")
86
+ ap.add_argument("--json", action="store_true", help="machine-readable output")
87
+ ap.add_argument("--quiet", action="store_true")
88
+ ap.add_argument("--version", action="version", version=f"obsign-verify {__version__}")
89
+ args = ap.parse_args(argv)
90
+
91
+ if args.self_check:
92
+ return _self_check(args.quiet)
93
+ if not args.receipts:
94
+ ap.error("give one or more receipt files, or --self-check")
95
+
96
+ report, failures = [], 0
97
+ for path in args.receipts:
98
+ try:
99
+ receipt = load_receipt(path.read_text(encoding="utf-8"))
100
+ except Exception as exc:
101
+ failures += 1
102
+ report.append({"file": str(path), "verified": False,
103
+ "notes": [f"unreadable: {type(exc).__name__}: {exc}"]})
104
+ if not args.quiet:
105
+ print(f" [ REFUSED ] {path.name}: unreadable ({exc})")
106
+ continue
107
+ res = verify(receipt)
108
+ failures += 0 if res["verified"] else 1
109
+ report.append({"file": str(path), **res})
110
+ _report(path, res, args.quiet)
111
+
112
+ if args.json:
113
+ print(json.dumps(report, indent=2))
114
+ elif not args.quiet:
115
+ total = len(report)
116
+ print(f"\n{total - failures}/{total} receipt(s) verified on THIS machine.")
117
+ return 1 if failures else 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ raise SystemExit(main())
@@ -0,0 +1,48 @@
1
+ # Attestation — I ran the Receipt Challenge
2
+
3
+ Fill in, publish anywhere, link it back if you're willing. A failure to break it
4
+ is as useful as a break; a break is more useful.
5
+
6
+ ## Who
7
+ - Name / handle:
8
+ - Affiliation (optional):
9
+ - Date (UTC):
10
+
11
+ ## Platform
12
+ - OS + version:
13
+ - CPU (and whether ARM / x86):
14
+ - Python version:
15
+ - numpy version:
16
+ - Anything unusual (musl, emulation, BSD, big-endian, PyPy, WASM):
17
+
18
+ ## What I ran
19
+ ```
20
+ python verify_challenge.py
21
+ ```
22
+ Paste the full output:
23
+
24
+ ```
25
+
26
+ ```
27
+
28
+ ## SHA256SUMS I observed
29
+ Confirm the bundle you ran matches what was published:
30
+ ```
31
+ sha256sum -c SHA256SUMS # or: python -c "..."
32
+ ```
33
+ Result:
34
+
35
+ ## Verdict
36
+ - [ ] Every valid receipt re-derived on my machine
37
+ - [ ] Every forgery was refused
38
+ - [ ] I found a forgery that VERIFIED ← the interesting one
39
+ - [ ] The valid receipt did NOT reproduce on my hardware ← also interesting
40
+
41
+ ## Notes / anything you broke or tried to break
42
+
43
+ <!-- Attacks attempted, near-misses, ambiguity in the spec, anything the
44
+ verifier fails to check. Negative findings welcome and wanted. -->
45
+
46
+ ## Independence
47
+ - [ ] I confirmed `verify_challenge.py` imports nothing from the producing project
48
+ - [ ] I read the verifier before running it
@@ -0,0 +1,62 @@
1
+ {
2
+ "_challenge": {
3
+ "expect_verified": true,
4
+ "note": "only `env` changed, which the spec excludes from the claim; this MUST still verify, or the format over-claims what it covers"
5
+ },
6
+ "env": {
7
+ "gpu": "NVIDIA GeForce RTX 5080 Laptop GPU",
8
+ "numpy": "2.3.5",
9
+ "platform": "some-other-os",
10
+ "python": "3.14.3",
11
+ "torch": "2.11.0+cu128"
12
+ },
13
+ "input": {
14
+ "kind": "generated",
15
+ "ref": "params",
16
+ "sha256": null
17
+ },
18
+ "kernel": "tau_field_fixed",
19
+ "output": {
20
+ "dtype": "int64",
21
+ "sha256": "a50bef430f745b12111225eed12cd0e4f86119ccb0fb47a878ed1f61d50b3ea3",
22
+ "shape": [
23
+ 64,
24
+ 64
25
+ ]
26
+ },
27
+ "params": {
28
+ "D": 0.14,
29
+ "dt": 0.15,
30
+ "frac_bits": 24,
31
+ "gamma": 0.02,
32
+ "grid": 64,
33
+ "sources": [
34
+ [
35
+ 0.3,
36
+ 0.3,
37
+ 0.11,
38
+ 0.01
39
+ ],
40
+ [
41
+ 0.7,
42
+ 0.7,
43
+ 0.16,
44
+ 0.008
45
+ ]
46
+ ],
47
+ "steps": 100
48
+ },
49
+ "producer": {
50
+ "name": "obsign",
51
+ "spec": "obsign/spec/1",
52
+ "version": "2.0.0"
53
+ },
54
+ "receipt_sha256": "b8b4f656b10646186ca9fe16d1e34eb106f4f2336d2db8f8f4cbb4a0660a5527",
55
+ "run": {
56
+ "backend": "cpu-fixed",
57
+ "deterministic": true,
58
+ "device": "numpy",
59
+ "dtype": "int64"
60
+ },
61
+ "spec": "obsign/receipt/v1"
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "_challenge": {
3
+ "expect_verified": false,
4
+ "note": "claims the computation ran on data it did not run on"
5
+ },
6
+ "env": {
7
+ "gpu": "NVIDIA GeForce RTX 5080 Laptop GPU",
8
+ "numpy": "2.3.5",
9
+ "platform": "Windows-11-10.0.26200-SP0",
10
+ "python": "3.14.3",
11
+ "torch": "2.11.0+cu128"
12
+ },
13
+ "input": {
14
+ "kind": "generated",
15
+ "ref": "params",
16
+ "sha256": "1111111111111111111111111111111111111111111111111111111111111111"
17
+ },
18
+ "kernel": "tau_field_fixed",
19
+ "output": {
20
+ "dtype": "int64",
21
+ "sha256": "a50bef430f745b12111225eed12cd0e4f86119ccb0fb47a878ed1f61d50b3ea3",
22
+ "shape": [
23
+ 64,
24
+ 64
25
+ ]
26
+ },
27
+ "params": {
28
+ "D": 0.14,
29
+ "dt": 0.15,
30
+ "frac_bits": 24,
31
+ "gamma": 0.02,
32
+ "grid": 64,
33
+ "sources": [
34
+ [
35
+ 0.3,
36
+ 0.3,
37
+ 0.11,
38
+ 0.01
39
+ ],
40
+ [
41
+ 0.7,
42
+ 0.7,
43
+ 0.16,
44
+ 0.008
45
+ ]
46
+ ],
47
+ "steps": 100
48
+ },
49
+ "producer": {
50
+ "name": "obsign",
51
+ "spec": "obsign/spec/1",
52
+ "version": "2.0.0"
53
+ },
54
+ "receipt_sha256": "b8b4f656b10646186ca9fe16d1e34eb106f4f2336d2db8f8f4cbb4a0660a5527",
55
+ "run": {
56
+ "backend": "cpu-fixed",
57
+ "deterministic": true,
58
+ "device": "numpy",
59
+ "dtype": "int64"
60
+ },
61
+ "spec": "obsign/receipt/v1"
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "_challenge": {
3
+ "expect_verified": false,
4
+ "note": "claims a different result than the computation produces"
5
+ },
6
+ "env": {
7
+ "gpu": "NVIDIA GeForce RTX 5080 Laptop GPU",
8
+ "numpy": "2.3.5",
9
+ "platform": "Windows-11-10.0.26200-SP0",
10
+ "python": "3.14.3",
11
+ "torch": "2.11.0+cu128"
12
+ },
13
+ "input": {
14
+ "kind": "generated",
15
+ "ref": "params",
16
+ "sha256": null
17
+ },
18
+ "kernel": "tau_field_fixed",
19
+ "output": {
20
+ "dtype": "int64",
21
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
22
+ "shape": [
23
+ 64,
24
+ 64
25
+ ]
26
+ },
27
+ "params": {
28
+ "D": 0.14,
29
+ "dt": 0.15,
30
+ "frac_bits": 24,
31
+ "gamma": 0.02,
32
+ "grid": 64,
33
+ "sources": [
34
+ [
35
+ 0.3,
36
+ 0.3,
37
+ 0.11,
38
+ 0.01
39
+ ],
40
+ [
41
+ 0.7,
42
+ 0.7,
43
+ 0.16,
44
+ 0.008
45
+ ]
46
+ ],
47
+ "steps": 100
48
+ },
49
+ "producer": {
50
+ "name": "obsign",
51
+ "spec": "obsign/spec/1",
52
+ "version": "2.0.0"
53
+ },
54
+ "receipt_sha256": "b8b4f656b10646186ca9fe16d1e34eb106f4f2336d2db8f8f4cbb4a0660a5527",
55
+ "run": {
56
+ "backend": "cpu-fixed",
57
+ "deterministic": true,
58
+ "device": "numpy",
59
+ "dtype": "int64"
60
+ },
61
+ "spec": "obsign/receipt/v1"
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "_challenge": {
3
+ "expect_verified": false,
4
+ "note": "claim tampered AND re-sealed, so integrity is INTACT; only re-derivation catches it"
5
+ },
6
+ "env": {
7
+ "gpu": "NVIDIA GeForce RTX 5080 Laptop GPU",
8
+ "numpy": "2.3.5",
9
+ "platform": "Windows-11-10.0.26200-SP0",
10
+ "python": "3.14.3",
11
+ "torch": "2.11.0+cu128"
12
+ },
13
+ "input": {
14
+ "kind": "generated",
15
+ "ref": "params",
16
+ "sha256": null
17
+ },
18
+ "kernel": "tau_field_fixed",
19
+ "output": {
20
+ "dtype": "int64",
21
+ "sha256": "a50bef430f745b12111225eed12cd0e4f86119ccb0fb47a878ed1f61d50b3ea3",
22
+ "shape": [
23
+ 64,
24
+ 64
25
+ ]
26
+ },
27
+ "params": {
28
+ "D": 0.14,
29
+ "dt": 0.15,
30
+ "frac_bits": 24,
31
+ "gamma": 0.02,
32
+ "grid": 64,
33
+ "sources": [
34
+ [
35
+ 0.3,
36
+ 0.3,
37
+ 0.11,
38
+ 0.01
39
+ ],
40
+ [
41
+ 0.7,
42
+ 0.7,
43
+ 0.16,
44
+ 0.008
45
+ ]
46
+ ],
47
+ "steps": 120
48
+ },
49
+ "producer": {
50
+ "name": "obsign",
51
+ "spec": "obsign/spec/1",
52
+ "version": "2.0.0"
53
+ },
54
+ "receipt_sha256": "e03f51663e632151341b77a29b32b3967565167ea767517649930c76392ca775",
55
+ "run": {
56
+ "backend": "cpu-fixed",
57
+ "deterministic": true,
58
+ "device": "numpy",
59
+ "dtype": "int64"
60
+ },
61
+ "spec": "obsign/receipt/v1"
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "_challenge": {
3
+ "expect_verified": false,
4
+ "note": "output metadata rewritten under an unchanged hash"
5
+ },
6
+ "env": {
7
+ "gpu": "NVIDIA GeForce RTX 5080 Laptop GPU",
8
+ "numpy": "2.3.5",
9
+ "platform": "Windows-11-10.0.26200-SP0",
10
+ "python": "3.14.3",
11
+ "torch": "2.11.0+cu128"
12
+ },
13
+ "input": {
14
+ "kind": "generated",
15
+ "ref": "params",
16
+ "sha256": null
17
+ },
18
+ "kernel": "tau_field_fixed",
19
+ "output": {
20
+ "dtype": "int32",
21
+ "sha256": "a50bef430f745b12111225eed12cd0e4f86119ccb0fb47a878ed1f61d50b3ea3",
22
+ "shape": [
23
+ 64,
24
+ 64
25
+ ]
26
+ },
27
+ "params": {
28
+ "D": 0.14,
29
+ "dt": 0.15,
30
+ "frac_bits": 24,
31
+ "gamma": 0.02,
32
+ "grid": 64,
33
+ "sources": [
34
+ [
35
+ 0.3,
36
+ 0.3,
37
+ 0.11,
38
+ 0.01
39
+ ],
40
+ [
41
+ 0.7,
42
+ 0.7,
43
+ 0.16,
44
+ 0.008
45
+ ]
46
+ ],
47
+ "steps": 100
48
+ },
49
+ "producer": {
50
+ "name": "obsign",
51
+ "spec": "obsign/spec/1",
52
+ "version": "2.0.0"
53
+ },
54
+ "receipt_sha256": "b8b4f656b10646186ca9fe16d1e34eb106f4f2336d2db8f8f4cbb4a0660a5527",
55
+ "run": {
56
+ "backend": "cpu-fixed",
57
+ "deterministic": true,
58
+ "device": "numpy",
59
+ "dtype": "int64"
60
+ },
61
+ "spec": "obsign/receipt/v1"
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "_challenge": {
3
+ "expect_verified": false,
4
+ "note": "same numbers, different operation"
5
+ },
6
+ "env": {
7
+ "gpu": "NVIDIA GeForce RTX 5080 Laptop GPU",
8
+ "numpy": "2.3.5",
9
+ "platform": "Windows-11-10.0.26200-SP0",
10
+ "python": "3.14.3",
11
+ "torch": "2.11.0+cu128"
12
+ },
13
+ "input": {
14
+ "kind": "generated",
15
+ "ref": "params",
16
+ "sha256": null
17
+ },
18
+ "kernel": "tau_field",
19
+ "output": {
20
+ "dtype": "int64",
21
+ "sha256": "a50bef430f745b12111225eed12cd0e4f86119ccb0fb47a878ed1f61d50b3ea3",
22
+ "shape": [
23
+ 64,
24
+ 64
25
+ ]
26
+ },
27
+ "params": {
28
+ "D": 0.14,
29
+ "dt": 0.15,
30
+ "frac_bits": 24,
31
+ "gamma": 0.02,
32
+ "grid": 64,
33
+ "sources": [
34
+ [
35
+ 0.3,
36
+ 0.3,
37
+ 0.11,
38
+ 0.01
39
+ ],
40
+ [
41
+ 0.7,
42
+ 0.7,
43
+ 0.16,
44
+ 0.008
45
+ ]
46
+ ],
47
+ "steps": 100
48
+ },
49
+ "producer": {
50
+ "name": "obsign",
51
+ "spec": "obsign/spec/1",
52
+ "version": "2.0.0"
53
+ },
54
+ "receipt_sha256": "b8b4f656b10646186ca9fe16d1e34eb106f4f2336d2db8f8f4cbb4a0660a5527",
55
+ "run": {
56
+ "backend": "cpu-fixed",
57
+ "deterministic": true,
58
+ "device": "numpy",
59
+ "dtype": "int64"
60
+ },
61
+ "spec": "obsign/receipt/v1"
62
+ }