bluefox-edge 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Format Dynamics, Inc. (DBA BlueFox)
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,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluefox-edge
3
+ Version: 0.0.1
4
+ Summary: BlueFox Edge for Python: today, the published offline receipt checker as an installable module; the API client follows.
5
+ Author-email: BlueFox AI <ethan@bluefoxedge.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://www.bluefoxedge.ai
8
+ Project-URL: Documentation, https://www.bluefoxedge.ai/docs/sdk/python
9
+ Project-URL: Verify a receipt, https://www.bluefoxedge.ai/docs/verify-a-receipt
10
+ Project-URL: Published checker, https://api.bluefoxedge.ai/verify_receipt.py
11
+ Project-URL: Repository, https://github.com/ethanholien/bluefox-edge
12
+ Keywords: bluefox,receipt,verification,ed25519,rfc8785,jcs,offline,agents
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ <!-- DRAFT-FOR-CEO-REVIEW · staged dark by ff115 (SS115/D006) · NOT PUBLISHED · every sentence below is proposed public text -->
26
+ # bluefox-edge (Python)
27
+
28
+ **0.0.1 — a first, small, genuine release.** It installs the published BlueFox Edge
29
+ offline receipt checker so you can pin it with pip instead of fetching a file:
30
+
31
+ ```bash
32
+ pip install bluefox-edge
33
+ bluefox-edge-verify-receipt my-receipt.json
34
+ ```
35
+
36
+ That command runs the exact bytes served at
37
+ <https://api.bluefoxedge.ai/verify_receipt.py> (sha256 `50dd2124…711d8`, 24,801 B —
38
+ compare with <https://api.bluefoxedge.ai/verify_receipt.py.sha256> any time). Same
39
+ steps, same order, same one-sentence verdicts. Exit 0 means the check passed.
40
+
41
+ ## What it checks
42
+
43
+ A BlueFox Edge receipt is a signed record of one API answer: the envelope is
44
+ canonicalized (RFC 8785 / JCS), hashed (SHA-256), chained, and signed (Ed25519) with a
45
+ published key. The checker recomputes every signed field from the file's own bytes and
46
+ refuses — in a sentence, never a traceback — on any disagreement. It reads the key set
47
+ from the published JWKS, or fully offline from a file you saved:
48
+
49
+ ```bash
50
+ curl -sSo pinned-jwks.json https://api.bluefoxedge.ai/.well-known/jwks.json
51
+ bluefox-edge-verify-receipt --jwks pinned-jwks.json my-receipt.json
52
+ ```
53
+
54
+ The receipt never names its own key source; the `--jwks` flag is the operator's hand.
55
+ How to read what a receipt does and does not say: <https://www.bluefoxedge.ai/docs/verify-a-receipt>.
56
+
57
+ ## What is coming
58
+
59
+ The BlueFox Edge API client for Python (`BlueFoxClient`, sync and async) is written and
60
+ tested in the repository at version 0.6.0. It arrives on PyPI under this name once its
61
+ handling of the keyless x402 payment quote (HTTP 402) is cured — until then a 0.0.x
62
+ release does not talk to the API for you. Docs: <https://www.bluefoxedge.ai/docs/sdk/python>.
63
+
64
+ ## Why a 0.0.1 at all
65
+
66
+ Fifteen files in the repository and both SDK READMEs already print
67
+ `pip install bluefox-edge`. Publishing a real, working checker under the name closes
68
+ the gap between what the docs say and what the registry has, without pretending the
69
+ client is here.
70
+
71
+ ## Also on npm
72
+
73
+ The JavaScript twin of the checker is `npx bluefox-verify-receipt`, and `bluefox-edge`
74
+ on npm re-exports it.
75
+
76
+ License: MIT. Author: BlueFox AI (Format Dynamics, Inc.).
@@ -0,0 +1,52 @@
1
+ <!-- DRAFT-FOR-CEO-REVIEW · staged dark by ff115 (SS115/D006) · NOT PUBLISHED · every sentence below is proposed public text -->
2
+ # bluefox-edge (Python)
3
+
4
+ **0.0.1 — a first, small, genuine release.** It installs the published BlueFox Edge
5
+ offline receipt checker so you can pin it with pip instead of fetching a file:
6
+
7
+ ```bash
8
+ pip install bluefox-edge
9
+ bluefox-edge-verify-receipt my-receipt.json
10
+ ```
11
+
12
+ That command runs the exact bytes served at
13
+ <https://api.bluefoxedge.ai/verify_receipt.py> (sha256 `50dd2124…711d8`, 24,801 B —
14
+ compare with <https://api.bluefoxedge.ai/verify_receipt.py.sha256> any time). Same
15
+ steps, same order, same one-sentence verdicts. Exit 0 means the check passed.
16
+
17
+ ## What it checks
18
+
19
+ A BlueFox Edge receipt is a signed record of one API answer: the envelope is
20
+ canonicalized (RFC 8785 / JCS), hashed (SHA-256), chained, and signed (Ed25519) with a
21
+ published key. The checker recomputes every signed field from the file's own bytes and
22
+ refuses — in a sentence, never a traceback — on any disagreement. It reads the key set
23
+ from the published JWKS, or fully offline from a file you saved:
24
+
25
+ ```bash
26
+ curl -sSo pinned-jwks.json https://api.bluefoxedge.ai/.well-known/jwks.json
27
+ bluefox-edge-verify-receipt --jwks pinned-jwks.json my-receipt.json
28
+ ```
29
+
30
+ The receipt never names its own key source; the `--jwks` flag is the operator's hand.
31
+ How to read what a receipt does and does not say: <https://www.bluefoxedge.ai/docs/verify-a-receipt>.
32
+
33
+ ## What is coming
34
+
35
+ The BlueFox Edge API client for Python (`BlueFoxClient`, sync and async) is written and
36
+ tested in the repository at version 0.6.0. It arrives on PyPI under this name once its
37
+ handling of the keyless x402 payment quote (HTTP 402) is cured — until then a 0.0.x
38
+ release does not talk to the API for you. Docs: <https://www.bluefoxedge.ai/docs/sdk/python>.
39
+
40
+ ## Why a 0.0.1 at all
41
+
42
+ Fifteen files in the repository and both SDK READMEs already print
43
+ `pip install bluefox-edge`. Publishing a real, working checker under the name closes
44
+ the gap between what the docs say and what the registry has, without pretending the
45
+ client is here.
46
+
47
+ ## Also on npm
48
+
49
+ The JavaScript twin of the checker is `npx bluefox-verify-receipt`, and `bluefox-edge`
50
+ on npm re-exports it.
51
+
52
+ License: MIT. Author: BlueFox AI (Format Dynamics, Inc.).
@@ -0,0 +1,39 @@
1
+ # DRAFT-FOR-CEO-REVIEW -- staged dark by ff115 (SS115/D006), NOT published. Every public-facing string in this
2
+ # file and in README.md is a proposal for the CEO's review before any claim ceremony.
3
+ [build-system]
4
+ requires = ["setuptools>=68.0", "wheel"]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "bluefox-edge"
9
+ version = "0.0.1"
10
+ description = "BlueFox Edge for Python: today, the published offline receipt checker as an installable module; the API client follows."
11
+ readme = "README.md"
12
+ license = {text = "MIT"}
13
+ requires-python = ">=3.8"
14
+ authors = [{name = "BlueFox AI", email = "ethan@bluefoxedge.ai"}]
15
+ keywords = ["bluefox", "receipt", "verification", "ed25519", "rfc8785", "jcs", "offline", "agents"]
16
+ classifiers = [
17
+ "Development Status :: 2 - Pre-Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Security :: Cryptography",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.urls]
28
+ Homepage = "https://www.bluefoxedge.ai"
29
+ Documentation = "https://www.bluefoxedge.ai/docs/sdk/python"
30
+ "Verify a receipt" = "https://www.bluefoxedge.ai/docs/verify-a-receipt"
31
+ "Published checker" = "https://api.bluefoxedge.ai/verify_receipt.py"
32
+ Repository = "https://github.com/ethanholien/bluefox-edge"
33
+
34
+ [project.scripts]
35
+ bluefox-edge-verify-receipt = "bluefox_edge._cli:main"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+ include = ["bluefox_edge*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,43 @@
1
+ """bluefox-edge 0.0.1 -- DRAFT-FOR-CEO-REVIEW, staged dark, not published.
2
+
3
+ What this release IS: the published BlueFox Edge offline receipt checker
4
+ (https://api.bluefoxedge.ai/verify_receipt.py), shipped byte-for-byte as
5
+ ``bluefox_edge.verify_receipt`` so it can be pip-installed and pinned instead of
6
+ fetched. Run it as ``bluefox-edge-verify-receipt <receipt.json>`` or
7
+ ``python -m bluefox_edge.verify_receipt <receipt.json>``.
8
+
9
+ What this release IS NOT: the API client. That client exists in the BlueFox Edge
10
+ repository (sdk/python, version 0.6.0) and lands here under a later version once
11
+ its keyless-402 handling is cured. Nothing in 0.0.x talks to the API on your
12
+ behalf; the checker only fetches the published key set, and ``--jwks`` makes even
13
+ that offline.
14
+ """
15
+ import hashlib as _hashlib
16
+ import os as _os
17
+
18
+ __version__ = "0.0.1"
19
+
20
+ DOCS_URL = "https://www.bluefoxedge.ai/docs/sdk/python"
21
+ VERIFY_DOCS_URL = "https://www.bluefoxedge.ai/docs/verify-a-receipt"
22
+ PUBLISHED_CHECKER_URL = "https://api.bluefoxedge.ai/verify_receipt.py"
23
+ JWKS_URL = "https://api.bluefoxedge.ai/.well-known/jwks.json"
24
+
25
+ #: sha256 of the checker bytes vendored in this release -- the same bytes the
26
+ #: street served at PUBLISHED_CHECKER_URL on 2026-09-12 (24,801 B).
27
+ CHECKER_SHA256 = "50dd2124e8023cd7e7a3d83bccfab8627b645d83f785e082fa1edf38373711d8"
28
+
29
+
30
+ def checker_path():
31
+ """Absolute path of the vendored checker file."""
32
+ return _os.path.join(_os.path.dirname(__file__), "verify_receipt.py")
33
+
34
+
35
+ def checker_sha256():
36
+ """sha256 of the vendored checker as installed -- compare with CHECKER_SHA256 and
37
+ with https://api.bluefoxedge.ai/verify_receipt.py.sha256 before you trust it."""
38
+ with open(checker_path(), "rb") as f:
39
+ return _hashlib.sha256(f.read()).hexdigest()
40
+
41
+
42
+ __all__ = ["__version__", "DOCS_URL", "VERIFY_DOCS_URL", "PUBLISHED_CHECKER_URL",
43
+ "JWKS_URL", "CHECKER_SHA256", "checker_path", "checker_sha256"]
@@ -0,0 +1,27 @@
1
+ """Console entry for ``bluefox-edge-verify-receipt``.
2
+
3
+ The vendored checker is a script, not a library: it reads ``sys.argv`` at import and
4
+ exits with a one-sentence verdict. Running it through ``runpy`` keeps the served bytes
5
+ untouched (so its sha256 stays comparable with the street's) while giving pip users
6
+ a command on PATH.
7
+ """
8
+ import runpy
9
+ import sys
10
+
11
+
12
+ def main(argv=None):
13
+ if argv is None:
14
+ argv = sys.argv[1:]
15
+ if argv and argv[0] in ("-h", "--help"):
16
+ print("usage: bluefox-edge-verify-receipt [--jwks KEYS.json|https://...] [RECEIPT.json]\n"
17
+ "Checks a BlueFox Edge signed receipt. With no file it checks ./specimen-receipt.json.\n"
18
+ "Exit 0 = the check passes; any other exit prints one sentence saying why not.\n"
19
+ "Same bytes as https://api.bluefoxedge.ai/verify_receipt.py (see bluefox_edge.CHECKER_SHA256).")
20
+ return 0
21
+ sys.argv = ["verify_receipt.py"] + list(argv)
22
+ runpy.run_module("bluefox_edge.verify_receipt", run_name="__main__", alter_sys=True)
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,501 @@
1
+ import base64, hashlib, json, sys, urllib.request
2
+
3
+ # DEPENDENCIES: none. Python 3.8+ standard library only. If the third-party
4
+ # cryptography package happens to be installed, its Ed25519 verifier is used;
5
+ # otherwise the self-contained RFC 8032 verifier further down checks the same
6
+ # bytes and reaches the same answer. Nothing here needs pip.
7
+ #
8
+ # python3 verify_receipt.py checks specimen-receipt.json
9
+ # python3 verify_receipt.py my-receipt.json checks the file you name
10
+ # python3 verify_receipt.py --jwks saved-keys.json my-receipt.json
11
+ # checks fully offline against keys you saved
12
+ #
13
+ # Exit 0 means the check passed. Any non-zero exit means it did not, and the
14
+ # reason is one sentence -- never a stack trace.
15
+ #
16
+ # TWO KEY FAMILIES, AND THEY NEVER CROSS. Receipts are signed by the receipt
17
+ # key. The transparency log signs its checkpoints with a DIFFERENT key. Both are
18
+ # published in the same key set, and every entry declares which family it
19
+ # belongs to in a bfx:purpose member. DO NOT SELECT A KEY BY POSITION: keys[0]
20
+ # is the receipt key, so a checker that indexes the list instead of reading the
21
+ # label is one reordering away from checking the wrong thing. This file checks a
22
+ # RECEIPT, so it requires a purpose of "receipt" and refuses a receipt that
23
+ # names the log's key. Checkpoints are the other script's job, and it makes the
24
+ # mirror-image check: https://api.bluefoxedge.ai/verify_inclusion.py
25
+
26
+ # Pin the key set out of band -- never read a jwks_url out of the receipt.
27
+ # That field is unsigned: a forger can point it at keys they control, and a
28
+ # checker that follows it will bless the forgery. The explicit User-Agent is
29
+ # deliberate too: some edge firewalls refuse the default urllib signature.
30
+ #
31
+ # --jwks moves the pin FURTHER out of band, never less: name a key-set file
32
+ # you saved earlier and the whole check runs offline against those pinned
33
+ # bytes -- nothing is fetched at all -- or name an https address you trust
34
+ # and keys are fetched from there instead of the default below. Either way
35
+ # the choice is the operator's hand, never the receipt's: no field in the
36
+ # file being judged can pick its own judge.
37
+ JWKS_URL = "https://api.bluefoxedge.ai/.well-known/jwks.json"
38
+ PURPOSE_CLAIM = "bfx:purpose"
39
+ RECEIPT_PURPOSE = "receipt"
40
+
41
+ def unb64url(s):
42
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
43
+
44
+ def b64url(raw):
45
+ return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
46
+
47
+ def sha256s(raw):
48
+ return "sha256:" + hashlib.sha256(raw).hexdigest()
49
+
50
+ # RFC 8785 (JCS) canonical JSON, standard library only.
51
+ #
52
+ # Reproduced from BRIDIE's construction -- a stranger who, sent at this estate
53
+ # on 2026-08-15 under a no-install rule, rebuilt this in 25 lines of stdlib and
54
+ # got output BYTE-IDENTICAL to the rfc8785 package on the published specimen
55
+ # (1,324 bytes from both). Her point stands and is the reason this file no
56
+ # longer asks you to install anything: the spec is complete enough to
57
+ # reimplement, so the checker should not need a package to prove it.
58
+ #
59
+ # Three rules do all the work. Object keys sort by UTF-16 code unit -- which is
60
+ # what encoding each key to UTF-16 big-endian and comparing those bytes gives
61
+ # you, surrogate pairs included. There is no insignificant whitespace. And a
62
+ # number that is integral is spelled without its fraction: JCS spells 0.0 as 0,
63
+ # and one character breaks a hash. That trap is live in the flagship artifact --
64
+ # the specimen's confidence_score is 0.0.
65
+ #
66
+ # The number domain this accepts is the one the signer itself enforces before
67
+ # hashing (finite floats, integers within the JCS safe-integer range). Outside
68
+ # that, and in the narrow band where a shortest-round-trip float would have to
69
+ # be spelled with an exponent, this REFUSES rather than guessing -- a wrong
70
+ # canonical form is a wrong hash, and a wrong hash silently accuses an honest
71
+ # receipt. Reject, never normalise.
72
+ def jcs(value):
73
+ if value is True:
74
+ return b"true"
75
+ if value is False:
76
+ return b"false"
77
+ if value is None:
78
+ return b"null"
79
+ if isinstance(value, str):
80
+ try:
81
+ return json.dumps(value, ensure_ascii=False,
82
+ separators=(",", ":")).encode("utf-8")
83
+ except UnicodeEncodeError:
84
+ raise SystemExit("this receipt carries text that is not encodable as "
85
+ "UTF-8 (a lone surrogate) -- it cannot be canonicalised")
86
+ if isinstance(value, int):
87
+ if abs(value) > 2 ** 53 - 1:
88
+ raise SystemExit("this receipt carries an integer outside the JCS "
89
+ "safe-integer range -- refusing rather than hashing a "
90
+ "value two implementations would spell differently")
91
+ return repr(value).encode("ascii")
92
+ if isinstance(value, float):
93
+ if value != value or value in (float("inf"), float("-inf")):
94
+ raise SystemExit("this receipt carries a non-finite number, which has "
95
+ "no JSON canonical form")
96
+ text = repr(value)
97
+ if "e" in text or "E" in text:
98
+ raise SystemExit("this receipt carries a number whose canonical "
99
+ "spelling needs an exponent, which this stdlib "
100
+ "canonicaliser does not attempt -- refusing rather "
101
+ "than risking a wrong hash")
102
+ if value.is_integer():
103
+ return repr(int(value)).encode("ascii")
104
+ return text.encode("ascii")
105
+ if isinstance(value, list):
106
+ return b"[" + b",".join(jcs(item) for item in value) + b"]"
107
+ if isinstance(value, dict):
108
+ pairs = []
109
+ for key in sorted(value, key=lambda k: k.encode("utf-16-be")):
110
+ if not isinstance(key, str):
111
+ raise SystemExit("this receipt has a non-text object key -- it is "
112
+ "not canonicalisable JSON")
113
+ pairs.append(jcs(key) + b":" + jcs(value[key]))
114
+ return b"{" + b",".join(pairs) + b"}"
115
+ raise SystemExit("this receipt carries a value of a kind JSON has no "
116
+ "canonical form for -- it is not a receipt this recipe checks")
117
+
118
+ def copies(node, key): # every value a receipt's own subtree carries under key
119
+ if isinstance(node, dict):
120
+ for k, v in node.items():
121
+ if k == key:
122
+ yield v
123
+ yield from copies(v, key)
124
+ elif isinstance(node, list):
125
+ for item in node:
126
+ yield from copies(item, key)
127
+
128
+ # The file to check: the one you name, or the published specimen if you name
129
+ # nothing. Every way this can go wrong below is a sentence and a non-zero exit,
130
+ # because handing a stranger a traceback for a missing file is not a verdict --
131
+ # it is the vendor's stack frames in place of an answer. Flag parsing is spelled
132
+ # out rather than imported: argparse prints its own usage paragraphs and exits 2,
133
+ # and this file's contract is one sentence and its own exit, every time.
134
+ jwks_source = None
135
+ names = []
136
+ args = list(sys.argv[1:])
137
+ while args:
138
+ arg = args.pop(0)
139
+ if arg == "--jwks" or arg.startswith("--jwks="):
140
+ if jwks_source is not None:
141
+ raise SystemExit("--jwks is named twice -- one verdict means one "
142
+ "pinned key set, so name exactly one")
143
+ if arg == "--jwks":
144
+ value = args.pop(0) if args else ""
145
+ else:
146
+ value = arg[len("--jwks="):]
147
+ if not value:
148
+ raise SystemExit("--jwks needs a value -- the path of a key-set "
149
+ "file you saved earlier, or an https address to "
150
+ "fetch the keys from")
151
+ jwks_source = value
152
+ else:
153
+ names.append(arg)
154
+ if len(names) > 1:
155
+ raise SystemExit("usage: python3 verify_receipt.py [PATH-TO-RECEIPT.json] -- "
156
+ "one file at a time, so one verdict means one receipt")
157
+ path = names[0] if names else "specimen-receipt.json"
158
+ try:
159
+ with open(path, encoding="utf-8") as handle:
160
+ specimen = json.load(handle)
161
+ except FileNotFoundError:
162
+ raise SystemExit("there is no file at " + path + " -- name the receipt you "
163
+ "want checked, or save it as specimen-receipt.json and pass "
164
+ "nothing")
165
+ except IsADirectoryError:
166
+ raise SystemExit(path + " is a directory, not a receipt file")
167
+ except PermissionError:
168
+ raise SystemExit(path + " cannot be read (permission denied)")
169
+ except UnicodeDecodeError:
170
+ raise SystemExit(path + " is not UTF-8 text, so it is not a receipt this "
171
+ "recipe can read")
172
+ except ValueError as exc:
173
+ raise SystemExit(path + " is not valid JSON (" + str(exc) + ")")
174
+ except OSError as exc:
175
+ raise SystemExit(path + " could not be read (" + str(exc) + ")")
176
+ if not isinstance(specimen, dict):
177
+ raise SystemExit(path + " holds a " + type(specimen).__name__ + ", not a JSON "
178
+ "object -- a receipt is an object")
179
+
180
+ # The signed surface is the envelope: the one dict that carries the chain
181
+ # block. Its home differs by door -- data.receipt in an x402 mint response,
182
+ # data.envelope in a read-API response or the published specimen, one per
183
+ # entry of an export bundle -- so this finds it instead of indexing a path.
184
+ #
185
+ # WHICH envelope, when a file offers more than one, is a security question,
186
+ # not a convenience one. Every door here answers with a "data" mapping and
187
+ # puts the envelope inside it, so if the file HAS that mapping, ITS slot
188
+ # decides and nothing found elsewhere in the file can speak for it. That is
189
+ # what stops a doctored receipt at the slot you read from being excused by a
190
+ # genuine envelope tucked somewhere else in the same file. Only a file with no
191
+ # such mapping -- an export bundle, or something you assembled yourself --
192
+ # falls back to searching, and then you are reading whatever the search found:
193
+ # run this once per entry and read each answer. Either way a pass is a
194
+ # statement about ONE envelope, never about the file. And a file that HAS a
195
+ # data block but names no receipt in it is refused outright rather than
196
+ # searched, because that is not a shape any of these doors answers with.
197
+ #
198
+ # Both routes also return the dict CARRYING the envelope -- data, or the entry
199
+ # an envelope was found under -- because that is where the unsigned convenience
200
+ # copies live, and it is the fence that keeps the copy check inside ONE receipt,
201
+ # so a two-receipt bundle never compares entry A's hashes against entry B's.
202
+ # An envelope found directly inside a list has no carrier to name, and then the
203
+ # envelope fences itself: only its own copies are checked.
204
+ def signed(node):
205
+ chain = node.get("chain") if isinstance(node, dict) else None
206
+ return isinstance(chain, dict) and "signature" in chain
207
+
208
+ def slot_in_data(doc):
209
+ data = doc.get("data") if isinstance(doc, dict) else None
210
+ if not isinstance(data, dict):
211
+ return None # no data mapping: search, below
212
+ named = [s for s in ("receipt", "envelope") if s in data]
213
+ if not named:
214
+ # A "data" mapping is how these doors answer, so a file that has one
215
+ # and names no receipt inside it is not a receipt response -- and the
216
+ # rest of the file does not get to volunteer a substitute.
217
+ raise SystemExit("this file has a data block with no receipt in it -- "
218
+ "refusing to hunt elsewhere for something to check")
219
+ # From here the file HAS named a receipt, so that name binds and the
220
+ # search never runs. Naming one and then failing to be one is a refusal,
221
+ # not a licence to go looking for something else in the file to bless.
222
+ if len(named) > 1:
223
+ raise SystemExit("this file fills both data.receipt and data.envelope "
224
+ "-- name one receipt, or check them one at a time")
225
+ slot = named[0]
226
+ if not signed(data[slot]):
227
+ raise SystemExit("data." + slot + " is not a signed envelope -- "
228
+ "refusing to check something else in its place")
229
+ return data, data[slot] # data is the carrier, as below
230
+
231
+ def find_envelope(node, parent=None):
232
+ if isinstance(node, dict):
233
+ if signed(node):
234
+ return parent, node
235
+ for child in node.values():
236
+ hit = find_envelope(child, node)
237
+ if hit is not None:
238
+ return hit
239
+ elif isinstance(node, list):
240
+ for child in node:
241
+ hit = find_envelope(child, None)
242
+ if hit is not None:
243
+ return hit
244
+ return None
245
+
246
+ found = slot_in_data(specimen) or find_envelope(specimen)
247
+ if found is None:
248
+ raise SystemExit("no signed envelope found -- not a signed receipt (yet?)")
249
+ scope, envelope = found
250
+ if scope is None:
251
+ scope = envelope
252
+ chain = envelope["chain"]
253
+
254
+ # Everything the recipe is about to read, present AND of the right kind
255
+ # before it is read. A receipt missing one of these -- or carrying null or a
256
+ # number where text belongs -- is refused in a sentence, never with a
257
+ # traceback: handing a stranger a stack trace is the failure this whole
258
+ # recipe was rewritten to stop. (key_id must only be PRESENT: a keyless
259
+ # receipt carries it as null and reaches its own honest exit further down.)
260
+ def needs(field, home, text=True):
261
+ if field not in home:
262
+ raise SystemExit("this envelope has no " + field + " -- it is not a "
263
+ "receipt of the shape this recipe checks")
264
+ if text and not isinstance(home[field], str):
265
+ raise SystemExit("this envelope's " + field + " is not text -- it is "
266
+ "not a receipt of the shape this recipe checks")
267
+
268
+ needs("chain_prev", chain)
269
+ needs("key_id", chain, text=False)
270
+ needs("schema_version", envelope)
271
+
272
+ # 1. content_digest recomputes from the envelope's own bytes: sha256 over the
273
+ # RFC 8785 canonical form of the envelope with the whole chain block
274
+ # removed. JCS, not json.dumps -- JCS spells 0.0 as 0, and one character
275
+ # breaks a hash.
276
+ body = {k: v for k, v in envelope.items() if k != "chain"}
277
+ digest = sha256s(jcs(body))
278
+
279
+ # 2. chain_self recomputes: sha256 over digest|chain_prev|schema_version,
280
+ # joined with literal pipes, hashed as UTF-8. chain_prev is the previous
281
+ # receipt's chain_self, or the literal "GENESIS" at a chain head.
282
+ head = sha256s("|".join(
283
+ (digest, chain["chain_prev"], body["schema_version"])).encode("utf-8"))
284
+
285
+ # 3. Every copy of the two hashes in THIS receipt's subtree agrees with the
286
+ # recomputation -- the chain block's and the top-level mirrors' alike.
287
+ # Presence is checked before agreement: all() is True over an empty
288
+ # iterator, so a receipt carrying NO copy of a hash would otherwise sail
289
+ # through this step without it ever being compared to anything. A detected
290
+ # forgery is a verdict, so it exits in a sentence, never a stack trace.
291
+ for key, want in (("content_digest", digest), ("chain_self", head)):
292
+ carried = list(copies(scope, key))
293
+ if not carried:
294
+ raise SystemExit(key + " is absent -- there is nothing here to check")
295
+ if not all(got == want for got in carried):
296
+ raise SystemExit(
297
+ key + " does not recompute from the receipt's own bytes")
298
+
299
+ if not chain.get("signature"):
300
+ raise SystemExit("keyless receipt: steps 1-3 hold, but with key_id and "
301
+ "signature null there is no signature to check")
302
+
303
+ # 4. The signing key is published, and its name is a derived fact:
304
+ # kid = sha256 of the raw 32-byte Ed25519 public key. The key is selected by
305
+ # kid and then checked against its declared family -- selecting by kid alone
306
+ # would let a checkpoint key verify a receipt if the two ever crossed, and
307
+ # the whole reason there are two keys is that they must not.
308
+ #
309
+ # With --jwks the same selection runs against the keys YOU pinned. A saved
310
+ # file is read here with no network at all; an https address you named is
311
+ # fetched in place of the default. The kid check below is what makes a
312
+ # pinned file trustworthy to hold: the key's name is derived from the key's
313
+ # own bytes, so a swapped key cannot keep its old name.
314
+ key_set_word = "published" if jwks_source is None else "pinned"
315
+ if jwks_source is None or jwks_source.startswith("https://"):
316
+ key_set_url = JWKS_URL if jwks_source is None else jwks_source
317
+ req = urllib.request.Request(key_set_url, headers={"User-Agent": "bluefox-verify/1"})
318
+ try:
319
+ jwks = json.load(urllib.request.urlopen(req, timeout=30))
320
+ except Exception as exc:
321
+ raise SystemExit("the " + key_set_word + " key set at " + key_set_url +
322
+ " could not be "
323
+ "fetched (" + type(exc).__name__ + ") -- this checker will "
324
+ "not fall back to any address inside the receipt, so it "
325
+ "stops here")
326
+ elif jwks_source.startswith("http://"):
327
+ raise SystemExit("--jwks " + jwks_source + " is plaintext http -- keys "
328
+ "fetched without TLS can be swapped in flight, so save the "
329
+ "key set to a file and name that instead, or use https")
330
+ else:
331
+ try:
332
+ with open(jwks_source, encoding="utf-8") as handle:
333
+ jwks = json.load(handle)
334
+ except FileNotFoundError:
335
+ raise SystemExit("there is no key-set file at " + jwks_source + " -- "
336
+ "save the published keys first (curl -sSo "
337
+ "pinned-jwks.json " + JWKS_URL + ") and name that file")
338
+ except IsADirectoryError:
339
+ raise SystemExit(jwks_source + " is a directory, not a key-set file")
340
+ except PermissionError:
341
+ raise SystemExit(jwks_source + " cannot be read (permission denied)")
342
+ except UnicodeDecodeError:
343
+ raise SystemExit(jwks_source + " is not UTF-8 text, so it is not a key "
344
+ "set this recipe can read")
345
+ except ValueError:
346
+ raise SystemExit(jwks_source + " is not valid JSON, so no key can be "
347
+ "pinned from it")
348
+ except OSError:
349
+ raise SystemExit(jwks_source + " could not be read, so no key can be "
350
+ "pinned from it")
351
+ if not isinstance(jwks, dict) or not isinstance(jwks.get("keys"), list):
352
+ raise SystemExit("the " + key_set_word + " key set has no keys array, so no "
353
+ "key can be pinned")
354
+ key = next((k for k in jwks["keys"]
355
+ if isinstance(k, dict) and k.get("kid") == chain["key_id"]), None)
356
+ if key is None:
357
+ if jwks_source is None:
358
+ raise SystemExit("key_id is not in the published key set")
359
+ raise SystemExit("key_id is not in the pinned key set at " + jwks_source +
360
+ " -- if that copy predates this receipt, refresh it from " +
361
+ JWKS_URL + " and pin the new file")
362
+ purpose = key.get(PURPOSE_CLAIM)
363
+ # ABSENT is tolerated on purpose; only a POSITIVE wrong designation refuses.
364
+ # The published set gained bfx:purpose at the 2026-08-18 FIRE, so every key-set
365
+ # file pinned before that day lacks the member -- and the --jwks door exists
366
+ # precisely for files saved earlier. Same philosophy as the settlement lane's
367
+ # network guard: refuse a wrong answer, never punish an absent one. (H98 read
368
+ # the fail-closed suggestion and refuted it against the repo's own genuine
369
+ # 2026-08-08 pinned fixture; a stricter world needs a worded tolerance-sunset.)
370
+ if isinstance(purpose, str) and purpose != RECEIPT_PURPOSE:
371
+ raise SystemExit("this receipt names the " + key_set_word + " key " + chain["key_id"] +
372
+ ", which is designated " + repr(purpose) + " and not a "
373
+ "receipt signer -- the two key families never cross, so "
374
+ "this is refused rather than verified")
375
+ raw_key = unb64url(key["x"])
376
+ if hashlib.sha256(raw_key).hexdigest() != chain["key_id"]:
377
+ raise SystemExit("kid is not sha256 of the raw public key")
378
+
379
+ # 5. The carried signature string is the one canonical spelling of its bytes
380
+ # (base64url, padding stripped) -- flip even a slack bit and this fails.
381
+ needs("signature", chain)
382
+ try:
383
+ sig = unb64url(chain["signature"])
384
+ except Exception:
385
+ raise SystemExit("this envelope's signature is not base64url text -- it "
386
+ "is not a receipt of the shape this recipe checks")
387
+ if b64url(sig) != chain["signature"]:
388
+ raise SystemExit("signature encoding is not canonical")
389
+
390
+ # Ed25519 verification (RFC 8032), self-contained, so this file runs on a bare
391
+ # Python. You never need to read this block: it is the arithmetic, not the
392
+ # recipe. It is here only so that "no install required" is literally true, and
393
+ # it is skipped entirely when the cryptography package is present.
394
+ P = 2 ** 255 - 19
395
+ L = 2 ** 252 + 27742317777372353535851937790883648493
396
+
397
+ def inv(x):
398
+ return pow(x, P - 2, P)
399
+
400
+ D = (-121665 * inv(121666)) % P
401
+ SQRT_M1 = pow(2, (P - 1) // 4, P)
402
+
403
+ def x_recover(y):
404
+ xx = ((y * y - 1) * inv(D * y * y + 1)) % P
405
+ x = pow(xx, (P + 3) // 8, P)
406
+ if (x * x - xx) % P != 0:
407
+ x = (x * SQRT_M1) % P
408
+ if (x * x - xx) % P != 0:
409
+ return None
410
+ return P - x if x % 2 != 0 else x
411
+
412
+ def point_add(pt1, pt2):
413
+ x1, y1 = pt1
414
+ x2, y2 = pt2
415
+ prod = (D * x1 * x2 * y1 * y2) % P
416
+ return (((x1 * y2 + x2 * y1) * inv(1 + prod)) % P,
417
+ ((y1 * y2 + x1 * x2) * inv(1 - prod)) % P)
418
+
419
+ def scalar_mult(pt, scalar):
420
+ acc = (0, 1)
421
+ while scalar > 0:
422
+ if scalar & 1:
423
+ acc = point_add(acc, pt)
424
+ pt = point_add(pt, pt)
425
+ scalar >>= 1
426
+ return acc
427
+
428
+ BASE_Y = (4 * inv(5)) % P
429
+ BASE = (x_recover(BASE_Y), BASE_Y)
430
+
431
+ def on_curve(pt):
432
+ x, y = pt
433
+ return (-x * x + y * y - 1 - D * x * x * y * y) % P == 0
434
+
435
+ def decode_point(blob):
436
+ value = int.from_bytes(blob, "little")
437
+ y = value & ((1 << 255) - 1)
438
+ sign = value >> 255
439
+ if y >= P:
440
+ return None
441
+ x = x_recover(y)
442
+ if x is None:
443
+ return None
444
+ if x & 1 != sign:
445
+ x = P - x
446
+ pt = (x, y)
447
+ return pt if on_curve(pt) else None
448
+
449
+ def ed25519_verify_pure(public_raw, signature, message):
450
+ if len(public_raw) != 32 or len(signature) != 64:
451
+ return False
452
+ point_a = decode_point(public_raw)
453
+ point_r = decode_point(signature[:32])
454
+ if point_a is None or point_r is None:
455
+ return False
456
+ scalar_s = int.from_bytes(signature[32:], "little")
457
+ if scalar_s >= L:
458
+ return False
459
+ challenge = int.from_bytes(
460
+ hashlib.sha512(signature[:32] + public_raw + message).digest(),
461
+ "little") % L
462
+ return scalar_mult(BASE, scalar_s) == point_add(
463
+ point_r, scalar_mult(point_a, challenge))
464
+
465
+ def ed25519_verify(public_raw, signature, message):
466
+ try:
467
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
468
+ Ed25519PublicKey)
469
+ except ImportError:
470
+ return ed25519_verify_pure(public_raw, signature, message)
471
+ try:
472
+ Ed25519PublicKey.from_public_bytes(public_raw).verify(signature, message)
473
+ return True
474
+ except Exception:
475
+ return False
476
+
477
+ # 6. The Ed25519 signature verifies over the RECOMPUTED chain head -- so the
478
+ # signature commits to every envelope byte hashed in step 1, not merely to
479
+ # a hash string the file happened to carry. A failure HERE is the verdict
480
+ # this whole recipe exists to deliver, so it too is a sentence.
481
+ if not ed25519_verify(raw_key, sig, head.encode("utf-8")):
482
+ raise SystemExit("the signature does not verify over the recomputed chain "
483
+ "head -- these are not the bytes the published key signed")
484
+
485
+ # 7. Unsigned convenience mirrors: the dict carrying the envelope repeats some
486
+ # signed fields for readers, and nothing above checked those copies -- they
487
+ # sit outside the signature. Mirrors spell the exact bytes of the envelope's
488
+ # values (produced_at included: same Z-form, same instant), so a copy that
489
+ # disagrees is a REFUSAL, not a warning. The signature held -- but the file
490
+ # in your hand is showing a reader different bytes than the ones it signed,
491
+ # and a disagreeing mirror is exactly how a doctored copy rides a check
492
+ # that reads only the envelope. A warning here asked you to notice what
493
+ # this script exists to notice for you, so it stops instead.
494
+ if scope is not envelope:
495
+ for key, value in body.items():
496
+ if key in scope and scope[key] != value:
497
+ raise SystemExit("unsigned top-level " + key + " does not match the "
498
+ "signed envelope's " + key + " -- the signature held, "
499
+ "but this file presents altered bytes where a reader "
500
+ "reads, and that is a tampered file, not a footnote")
501
+ print("the check passes: every signed field recomputed from the file's own bytes")
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluefox-edge
3
+ Version: 0.0.1
4
+ Summary: BlueFox Edge for Python: today, the published offline receipt checker as an installable module; the API client follows.
5
+ Author-email: BlueFox AI <ethan@bluefoxedge.ai>
6
+ License: MIT
7
+ Project-URL: Homepage, https://www.bluefoxedge.ai
8
+ Project-URL: Documentation, https://www.bluefoxedge.ai/docs/sdk/python
9
+ Project-URL: Verify a receipt, https://www.bluefoxedge.ai/docs/verify-a-receipt
10
+ Project-URL: Published checker, https://api.bluefoxedge.ai/verify_receipt.py
11
+ Project-URL: Repository, https://github.com/ethanholien/bluefox-edge
12
+ Keywords: bluefox,receipt,verification,ed25519,rfc8785,jcs,offline,agents
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ <!-- DRAFT-FOR-CEO-REVIEW · staged dark by ff115 (SS115/D006) · NOT PUBLISHED · every sentence below is proposed public text -->
26
+ # bluefox-edge (Python)
27
+
28
+ **0.0.1 — a first, small, genuine release.** It installs the published BlueFox Edge
29
+ offline receipt checker so you can pin it with pip instead of fetching a file:
30
+
31
+ ```bash
32
+ pip install bluefox-edge
33
+ bluefox-edge-verify-receipt my-receipt.json
34
+ ```
35
+
36
+ That command runs the exact bytes served at
37
+ <https://api.bluefoxedge.ai/verify_receipt.py> (sha256 `50dd2124…711d8`, 24,801 B —
38
+ compare with <https://api.bluefoxedge.ai/verify_receipt.py.sha256> any time). Same
39
+ steps, same order, same one-sentence verdicts. Exit 0 means the check passed.
40
+
41
+ ## What it checks
42
+
43
+ A BlueFox Edge receipt is a signed record of one API answer: the envelope is
44
+ canonicalized (RFC 8785 / JCS), hashed (SHA-256), chained, and signed (Ed25519) with a
45
+ published key. The checker recomputes every signed field from the file's own bytes and
46
+ refuses — in a sentence, never a traceback — on any disagreement. It reads the key set
47
+ from the published JWKS, or fully offline from a file you saved:
48
+
49
+ ```bash
50
+ curl -sSo pinned-jwks.json https://api.bluefoxedge.ai/.well-known/jwks.json
51
+ bluefox-edge-verify-receipt --jwks pinned-jwks.json my-receipt.json
52
+ ```
53
+
54
+ The receipt never names its own key source; the `--jwks` flag is the operator's hand.
55
+ How to read what a receipt does and does not say: <https://www.bluefoxedge.ai/docs/verify-a-receipt>.
56
+
57
+ ## What is coming
58
+
59
+ The BlueFox Edge API client for Python (`BlueFoxClient`, sync and async) is written and
60
+ tested in the repository at version 0.6.0. It arrives on PyPI under this name once its
61
+ handling of the keyless x402 payment quote (HTTP 402) is cured — until then a 0.0.x
62
+ release does not talk to the API for you. Docs: <https://www.bluefoxedge.ai/docs/sdk/python>.
63
+
64
+ ## Why a 0.0.1 at all
65
+
66
+ Fifteen files in the repository and both SDK READMEs already print
67
+ `pip install bluefox-edge`. Publishing a real, working checker under the name closes
68
+ the gap between what the docs say and what the registry has, without pretending the
69
+ client is here.
70
+
71
+ ## Also on npm
72
+
73
+ The JavaScript twin of the checker is `npx bluefox-verify-receipt`, and `bluefox-edge`
74
+ on npm re-exports it.
75
+
76
+ License: MIT. Author: BlueFox AI (Format Dynamics, Inc.).
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/bluefox_edge/__init__.py
5
+ src/bluefox_edge/_cli.py
6
+ src/bluefox_edge/verify_receipt.py
7
+ src/bluefox_edge.egg-info/PKG-INFO
8
+ src/bluefox_edge.egg-info/SOURCES.txt
9
+ src/bluefox_edge.egg-info/dependency_links.txt
10
+ src/bluefox_edge.egg-info/entry_points.txt
11
+ src/bluefox_edge.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bluefox-edge-verify-receipt = bluefox_edge._cli:main
@@ -0,0 +1 @@
1
+ bluefox_edge