dynamicfeed-verify 1.0.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,78 @@
1
+ """
2
+ dynamicfeed-verify — verify Dynamic Feed (DF-VERIFY/1) Ed25519-signed responses, independently.
3
+
4
+ A signed response carries a top-level ``signature`` block. This library reproduces the DF-VERIFY/1
5
+ canonical form (JSON, keys sorted recursively, compact separators), fetches the public key published
6
+ at ``<base>/.well-known/keys``, and verifies the detached Ed25519 signature. If it verifies, the
7
+ response provably came from the issuer and has not been altered — checkable by anyone, even against us.
8
+
9
+ Spec: https://dynamicfeed.ai/standard
10
+
11
+ from dynamicfeed_verify import verify, verify_live
12
+ env, result = verify_live() # fetch a fresh signed verdict + verify it
13
+ assert result["ok"]
14
+ verify(my_signed_response) # verify any signed response you already hold
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import base64
19
+ import json
20
+ import urllib.request
21
+
22
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
23
+
24
+ __version__ = "1.0.0"
25
+ DEFAULT_BASE = "https://dynamicfeed.ai"
26
+
27
+
28
+ def _b64d(s: str) -> bytes:
29
+ """Decode base64url, tolerating missing padding."""
30
+ return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
31
+
32
+
33
+ def canonical(payload: dict) -> bytes:
34
+ """DF-VERIFY/1 canonical bytes for ``payload`` (the response WITHOUT its ``signature`` field):
35
+ JSON with object keys sorted recursively and compact separators, encoded UTF-8."""
36
+ return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
37
+
38
+
39
+ def fetch_keys(base: str = DEFAULT_BASE, timeout: float = 20) -> dict:
40
+ """Fetch the JWKS-style public-key map: ``{key_id: base64url(Ed25519 public key)}``."""
41
+ with urllib.request.urlopen(base.rstrip("/") + "/.well-known/keys", timeout=timeout) as r:
42
+ return json.load(r)
43
+
44
+
45
+ def verify(envelope: dict, jwks: dict | None = None, base: str = DEFAULT_BASE) -> dict:
46
+ """Verify a DF-VERIFY/1 signed response.
47
+
48
+ Returns ``{"ok": bool, "key_id"?, "verdict"?, "snapshot_id"?, "ephemeral"?, "error"?}``.
49
+ Pass ``jwks`` to verify fully offline; otherwise the public key is fetched from ``base``.
50
+ """
51
+ sig = (envelope or {}).get("signature") or {}
52
+ kid, sig_b64 = sig.get("key_id"), sig.get("sig")
53
+ if not kid or not sig_b64:
54
+ return {"ok": False, "error": "no signature block (need signature.key_id + signature.sig)"}
55
+ payload = {k: v for k, v in envelope.items() if k != "signature"}
56
+ keys = jwks if jwks is not None else fetch_keys(base)
57
+ if kid not in keys:
58
+ return {"ok": False, "key_id": kid, "error": f"key_id {kid} not in JWKS (rotated or ephemeral)"}
59
+ try:
60
+ Ed25519PublicKey.from_public_bytes(_b64d(keys[kid])).verify(_b64d(sig_b64), canonical(payload))
61
+ except Exception as e: # noqa: BLE001 — any failure means it did not verify
62
+ return {"ok": False, "key_id": kid, "error": f"signature INVALID: {e}"}
63
+ return {
64
+ "ok": True, "key_id": kid, "ephemeral": bool(sig.get("ephemeral_key")),
65
+ "verdict": (envelope.get("verdict") or {}).get("status"),
66
+ "snapshot_id": envelope.get("snapshot_id"),
67
+ }
68
+
69
+
70
+ def verify_live(base: str = DEFAULT_BASE, robot: dict | None = None, location: dict | None = None):
71
+ """Fetch a fresh signed awareness verdict from ``base`` and verify it. Returns ``(envelope, result)``."""
72
+ body = {"robot": robot or {"class": "aerial"}, "location": location or {"lat": 51.5, "lon": -0.12}}
73
+ req = urllib.request.Request(
74
+ base.rstrip("/") + "/v1/awareness",
75
+ data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
76
+ with urllib.request.urlopen(req, timeout=25) as r:
77
+ env = json.load(r)
78
+ return env, verify(env, base=base)
@@ -0,0 +1,41 @@
1
+ """CLI: ``dynamicfeed-verify`` — fetch a live signed verdict and verify it, or verify a saved response."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+
7
+ from . import DEFAULT_BASE, verify, verify_live
8
+
9
+
10
+ def main() -> int:
11
+ args = sys.argv[1:]
12
+ if args and args[0] in ("-h", "--help"):
13
+ print("usage:\n"
14
+ " dynamicfeed-verify [BASE_URL] fetch a live signed verdict and verify it\n"
15
+ " dynamicfeed-verify - < response.json verify a saved signed response (key still fetched)\n"
16
+ " default BASE_URL = https://dynamicfeed.ai · spec: https://dynamicfeed.ai/standard")
17
+ return 0
18
+ if args and args[0] == "-":
19
+ env = json.load(sys.stdin)
20
+ base = args[1].rstrip("/") if len(args) > 1 else DEFAULT_BASE
21
+ res = verify(env, base=base)
22
+ else:
23
+ base = args[0].rstrip("/") if args else DEFAULT_BASE
24
+ print(f"requesting a live signed verdict from {base}/v1/awareness ...")
25
+ _env, res = verify_live(base)
26
+ if res.get("ok"):
27
+ extra = ""
28
+ if res.get("verdict"):
29
+ extra += f" · verdict={res['verdict']}"
30
+ if res.get("snapshot_id"):
31
+ extra += f" · snapshot={res['snapshot_id']}"
32
+ if res.get("ephemeral"):
33
+ extra += " · EPHEMERAL key"
34
+ print(f"✅ VALID — key={res['key_id']}{extra}")
35
+ return 0
36
+ print(f"✗ INVALID — {res.get('error')}")
37
+ return 1
38
+
39
+
40
+ if __name__ == "__main__":
41
+ raise SystemExit(main())
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynamicfeed-verify
3
+ Version: 1.0.0
4
+ Summary: Verify Dynamic Feed (DF-VERIFY/1) Ed25519-signed responses, independently — in one line.
5
+ Project-URL: Homepage, https://dynamicfeed.ai
6
+ Project-URL: Standard, https://dynamicfeed.ai/standard
7
+ Project-URL: Source, https://github.com/dynamicfeed/df-verify
8
+ Author: Dynamic Feed
9
+ License: MIT
10
+ Keywords: agents,ai,attestation,df-verify,dynamic-feed,ed25519,provenance,verification
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Requires-Python: >=3.8
17
+ Requires-Dist: cryptography>=41
18
+ Description-Content-Type: text/markdown
19
+
20
+ # dynamicfeed-verify
21
+
22
+ Verify [Dynamic Feed](https://dynamicfeed.ai) **DF-VERIFY/1** Ed25519-signed responses — independently, in one line. No account, no dependency on Dynamic Feed at runtime beyond fetching the public key. You can verify, even against us.
23
+
24
+ Reference implementation of the [DF-VERIFY/1 standard](https://dynamicfeed.ai/standard).
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install dynamicfeed-verify
30
+ ```
31
+
32
+ ## Use
33
+
34
+ ```python
35
+ from dynamicfeed_verify import verify, verify_live
36
+
37
+ # 1) fetch a fresh signed awareness verdict and verify it
38
+ env, result = verify_live()
39
+ print(result) # {'ok': True, 'key_id': 'df-ed25519-…', 'verdict': 'caution', ...}
40
+
41
+ # 2) verify any signed response you already hold
42
+ result = verify(signed_response)
43
+ if not result["ok"]:
44
+ raise RuntimeError(result["error"])
45
+
46
+ # 3) verify fully offline if you already have the JWKS
47
+ result = verify(signed_response, jwks={"df-ed25519-…": "<base64url public key>"})
48
+ ```
49
+
50
+ ## CLI
51
+
52
+ ```bash
53
+ dynamicfeed-verify # fetch a live verdict + verify
54
+ dynamicfeed-verify - < response.json # verify a saved signed response
55
+ ```
56
+
57
+ ## How it works
58
+
59
+ A signed response carries a `signature` block (`alg`, `key_id`, `canonicalization`, `sig`). Verification:
60
+
61
+ 1. Drop the `signature` field; keep the rest as the payload.
62
+ 2. Canonicalize — JSON, keys sorted recursively, compact separators (`,` `:`), UTF-8. Equivalent to `json.dumps(payload, sort_keys=True, separators=(",", ":"))`.
63
+ 3. Fetch the public key from `https://dynamicfeed.ai/.well-known/keys` and look up `signature.key_id`.
64
+ 4. Verify the Ed25519 signature over the canonical bytes. Change one byte → it fails.
65
+
66
+ Full specification: **https://dynamicfeed.ai/standard**
67
+
68
+ ## License
69
+
70
+ MIT.
@@ -0,0 +1,6 @@
1
+ dynamicfeed_verify/__init__.py,sha256=yHOYCKm9pUvWjj_cOgCDAeInTRmG8nhgLdOLk8zUPY0,3691
2
+ dynamicfeed_verify/__main__.py,sha256=ewUXlpUN2TJkmj3sbNVK75UlOPJ4QqeXDQK7QnWuBRA,1517
3
+ dynamicfeed_verify-1.0.0.dist-info/METADATA,sha256=45jC5KaQJ5QRhk5mbDKcmRJKEDWS7Ga24AnCjxONlak,2512
4
+ dynamicfeed_verify-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ dynamicfeed_verify-1.0.0.dist-info/entry_points.txt,sha256=87e-MAR6LuqBfcdj3qocdeZITAQ8YEZ8vuvTE35t7W0,72
6
+ dynamicfeed_verify-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dynamicfeed-verify = dynamicfeed_verify.__main__:main