datumesh 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {datumesh-0.2.1/src/datumesh.egg-info → datumesh-0.3.0}/PKG-INFO +8 -8
- {datumesh-0.2.1 → datumesh-0.3.0}/README.md +4 -4
- {datumesh-0.2.1 → datumesh-0.3.0}/pyproject.toml +4 -4
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/__init__.py +3 -3
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_envelope.py +9 -12
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_errors.py +3 -1
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_lexical.py +2 -2
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_unitlist.py +2 -2
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_verify.py +46 -20
- {datumesh-0.2.1 → datumesh-0.3.0/src/datumesh.egg-info}/PKG-INFO +8 -8
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh.egg-info/SOURCES.txt +1 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_api.py +11 -8
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_conformance.py +10 -10
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_envelope.py +6 -5
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_keys.py +3 -3
- datumesh-0.3.0/tests/test_spec_0_3_0.py +274 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_unicode.py +0 -1
- {datumesh-0.2.1 → datumesh-0.3.0}/LICENSE +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/setup.cfg +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_json.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_keys.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_model.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_ucd15.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_unicode.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_units.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/_uri.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh/py.typed +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh.egg-info/dependency_links.txt +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh.egg-info/requires.txt +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/src/datumesh.egg-info/top_level.txt +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_bitflips.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_blob.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_jcs.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_spec_0_2_1.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_units.py +0 -0
- {datumesh-0.2.1 → datumesh-0.3.0}/tests/test_uri.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datumesh
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Verify Datumesh receipts (spec-0.
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Verify Datumesh receipts (spec-0.3.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents.
|
|
5
5
|
Author: Datumesh
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://datumesh.com
|
|
8
8
|
Project-URL: Repository, https://github.com/datumesh-com/datumesh-verify-py
|
|
9
|
-
Project-URL: Specification (spec-0.
|
|
10
|
-
Keywords: datumesh,spec-0.
|
|
9
|
+
Project-URL: Specification (spec-0.3.0), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md
|
|
10
|
+
Keywords: datumesh,spec-0.3.0,receipt,verification,ed25519,rfc8785,jcs,canonical-json
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -27,11 +27,11 @@ Dynamic: license-file
|
|
|
27
27
|
|
|
28
28
|
# datumesh
|
|
29
29
|
|
|
30
|
-
Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.
|
|
30
|
+
Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md) (revision `spec-0.3.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
33
|
pip install datumesh
|
|
34
|
-
git clone --depth 1 --branch spec-0.
|
|
34
|
+
git clone --depth 1 --branch spec-0.3.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```python
|
|
@@ -49,8 +49,8 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
|
|
|
49
49
|
|
|
50
50
|
## API
|
|
51
51
|
|
|
52
|
-
- `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor, the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
|
|
53
|
-
- `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one;
|
|
52
|
+
- `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. One code comes with success: when you pass `recompute` without a `value` because you could not evaluate the function, the other recomputation checks still run, and if they pass the result is ok at level `signed` with `code` `"E_EVAL_UNAVAILABLE"` (SPEC 10.1 step 7.6). It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor (leave it out if you have none; `None` is a JSON null and fails the hash check), the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
|
|
53
|
+
- `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; the receipt's `asof` must be at or before it, for `ok` and `stale` alike). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
|
|
54
54
|
- `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
|
|
55
55
|
- `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
|
|
56
56
|
- `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# datumesh
|
|
2
2
|
|
|
3
|
-
Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.
|
|
3
|
+
Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md) (revision `spec-0.3.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
pip install datumesh
|
|
7
|
-
git clone --depth 1 --branch spec-0.
|
|
7
|
+
git clone --depth 1 --branch spec-0.3.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
```python
|
|
@@ -22,8 +22,8 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
|
|
|
22
22
|
|
|
23
23
|
## API
|
|
24
24
|
|
|
25
|
-
- `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor, the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
|
|
26
|
-
- `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one;
|
|
25
|
+
- `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. One code comes with success: when you pass `recompute` without a `value` because you could not evaluate the function, the other recomputation checks still run, and if they pass the result is ok at level `signed` with `code` `"E_EVAL_UNAVAILABLE"` (SPEC 10.1 step 7.6). It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor (leave it out if you have none; `None` is a JSON null and fails the hash check), the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
|
|
26
|
+
- `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; the receipt's `asof` must be at or before it, for `ok` and `stale` alike). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
|
|
27
27
|
- `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
|
|
28
28
|
- `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
|
|
29
29
|
- `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datumesh"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Verify Datumesh receipts (spec-0.
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Verify Datumesh receipts (spec-0.3.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [{ name = "Datumesh" }]
|
|
14
|
-
keywords = ["datumesh", "spec-0.
|
|
14
|
+
keywords = ["datumesh", "spec-0.3.0", "receipt", "verification", "ed25519", "rfc8785", "jcs", "canonical-json"]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 3 - Alpha",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
@@ -30,7 +30,7 @@ dependencies = ["pynacl>=1.5.0"]
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Homepage = "https://datumesh.com"
|
|
32
32
|
Repository = "https://github.com/datumesh-com/datumesh-verify-py"
|
|
33
|
-
"Specification (spec-0.
|
|
33
|
+
"Specification (spec-0.3.0)" = "https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md"
|
|
34
34
|
|
|
35
35
|
[tool.setuptools.packages.find]
|
|
36
36
|
where = ["src"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""datumesh: verify Datumesh receipts (signed answers for addressable data cells).
|
|
2
2
|
|
|
3
|
-
Implements the Datumesh protocol specification, revision ``spec-0.
|
|
3
|
+
Implements the Datumesh protocol specification, revision ``spec-0.3.0``
|
|
4
4
|
(https://github.com/datumesh-com/datumesh-spec): RFC 8785 canonical JSON,
|
|
5
5
|
SHA-256 hashes, Ed25519 receipt signatures, blob content, units, cell URIs,
|
|
6
6
|
response envelopes and key documents. Only :func:`fetch_keys` touches the network.
|
|
@@ -19,9 +19,9 @@ from ._keys import Key, KeysResult, fetch_keys, key_id, keys_from_response
|
|
|
19
19
|
from ._uri import format_uri, parse_uri, uri_to_https
|
|
20
20
|
from ._verify import VerifyResult, load, payload_bytes, verify
|
|
21
21
|
|
|
22
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.3.0"
|
|
23
23
|
#: The specification revision this package conforms to.
|
|
24
|
-
SPEC_VERSION = "spec-0.
|
|
24
|
+
SPEC_VERSION = "spec-0.3.0"
|
|
25
25
|
#: Every literal unit listed in units.md (index units, ``index:<period>=<value>``, are a pattern and not listed).
|
|
26
26
|
UNITS = tuple(sorted(_units.LITERALS))
|
|
27
27
|
#: The currency codes of units.md 2.1.
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import re
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Any, Dict, Mapping, Optional, Tuple, Union
|
|
8
|
+
from typing import Any, Dict, Mapping, NoReturn, Optional, Tuple, Union
|
|
9
9
|
|
|
10
10
|
from . import _lexical as lx
|
|
11
11
|
from . import _units
|
|
@@ -59,7 +59,7 @@ class EnvelopeResult:
|
|
|
59
59
|
path: Optional[str]
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def _bad(detail: str, path: str) ->
|
|
62
|
+
def _bad(detail: str, path: str) -> NoReturn:
|
|
63
63
|
fail("E_ENVELOPE", detail, path)
|
|
64
64
|
|
|
65
65
|
|
|
@@ -183,8 +183,8 @@ def check_envelope(status: Any, body: Union[str, bytes, bytearray, memoryview],
|
|
|
183
183
|
of 5.5 (``E_ENVELOPE``); the status matches the outcome (``E_ENVELOPE``); an
|
|
184
184
|
embedded receipt is verified through step 6 of 10.1 with ``keys`` (the forms
|
|
185
185
|
:func:`verify` takes) and keeps that procedure's codes; the receipt answers
|
|
186
|
-
the request: same cell id, and when an ``asof`` was requested
|
|
187
|
-
|
|
186
|
+
the request: same cell id, and when an ``asof`` was requested a receipt
|
|
187
|
+
``asof`` at or before it, for ``ok`` and ``stale`` alike (``E_ENVELOPE``); then ``trust`` is checked against the receipt (``primary``
|
|
188
188
|
exactly when the signer's host is the namespace's host and there are no
|
|
189
189
|
inputs) and ``superseded_by`` must not be the receipt's own hash (``E_ENVELOPE``).
|
|
190
190
|
|
|
@@ -214,14 +214,11 @@ def check_envelope(status: Any, body: Union[str, bytes, bytearray, memoryview],
|
|
|
214
214
|
raise DatumeshError(e.code, e.detail, path) from None
|
|
215
215
|
if hashlib.sha256(jcs_bytes(r["cell"])).hexdigest() != want_cell:
|
|
216
216
|
_bad("the receipt's cell is not the cell requested", "$.receipt.cell")
|
|
217
|
-
# ok: the
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if outcome == "ok" and r["asof"] != want_asof:
|
|
223
|
-
_bad(f"the receipt answers as of {r['asof']}, the request asked for {want_asof}",
|
|
224
|
-
"$.receipt.asof")
|
|
217
|
+
# ok and stale alike: the receipt's asof at or before the requested one (D31, spec-0.3.0;
|
|
218
|
+
# canonical instants compare as strings, SPEC 3.3). No requested asof: any.
|
|
219
|
+
if want_asof is not None and r["asof"] > want_asof:
|
|
220
|
+
_bad(f"the {outcome} receipt answers as of {r['asof']}, after the requested {want_asof}",
|
|
221
|
+
"$.receipt.asof")
|
|
225
222
|
trust = env["trust"]
|
|
226
223
|
if (trust == "primary") != _is_primary(r):
|
|
227
224
|
_bad("trust is primary exactly when the signer's host is the namespace's host and there are no "
|
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import NoReturn, Optional
|
|
6
6
|
|
|
7
|
-
#: Every error code SPEC.md 10.2 defines, in the order of its table.
|
|
7
|
+
#: Every error code SPEC.md 10.2 defines, in the order of its table. One, ``E_EVAL_UNAVAILABLE``,
|
|
8
|
+
#: is not a failure: :func:`verify` reports it with success (SPEC 10.1 step 7.6).
|
|
8
9
|
ERROR_CODES: tuple[str, ...] = (
|
|
9
10
|
"E_PARSE_JSON",
|
|
10
11
|
"E_PARSE_DUPLICATE_KEY",
|
|
@@ -45,6 +46,7 @@ ERROR_CODES: tuple[str, ...] = (
|
|
|
45
46
|
"E_INPUT_INVALID",
|
|
46
47
|
"E_INPUT_UNDECLARED",
|
|
47
48
|
"E_VALUE_MISMATCH",
|
|
49
|
+
"E_EVAL_UNAVAILABLE",
|
|
48
50
|
"E_BLOB_MISMATCH",
|
|
49
51
|
"E_ENVELOPE",
|
|
50
52
|
)
|
|
@@ -9,7 +9,7 @@ licences); characters, NFC, then length (strings, SPEC 3.5).
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import re
|
|
12
|
-
from typing import Any, Optional
|
|
12
|
+
from typing import Any, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from ._errors import fail
|
|
15
15
|
from ._unicode import is_assigned, is_nfc
|
|
@@ -152,7 +152,7 @@ def check_blob(s: str, path: Optional[str]) -> None:
|
|
|
152
152
|
f"at most {LIMIT_MEDIA_TYPE} characters>", path)
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
def blob_parts(s: str) ->
|
|
155
|
+
def blob_parts(s: str) -> Tuple[str, int, str]:
|
|
156
156
|
"""(sha256 hex, length, media type) of a blob value already checked with :func:`check_blob`."""
|
|
157
157
|
digest, length, media = s.split(":", 2)
|
|
158
158
|
return digest, int(length), media
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""The unit list of spec/units.md (spec-0.
|
|
1
|
+
"""The unit list of spec/units.md (spec-0.3.0). Generated by tools/gen_units.py; do not edit."""
|
|
2
2
|
|
|
3
|
-
SOURCE_SHA256 = "
|
|
3
|
+
SOURCE_SHA256 = "cf165af78779a95a21e1ad35e235e490f2dd125a562ede7652e97b9a5d4bb5ab"
|
|
4
4
|
|
|
5
5
|
#: ```units-currency``` blocks, in file order (ISO 4217 List One without XXX, then withdrawn codes).
|
|
6
6
|
CURRENCY = (
|
|
@@ -23,12 +23,16 @@ class VerifyResult:
|
|
|
23
23
|
"""The outcome of :func:`verify`.
|
|
24
24
|
|
|
25
25
|
``ok``: the receipt verified. ``code``: ``"OK"`` or the first failing
|
|
26
|
-
error code
|
|
26
|
+
error code; on success it is ``"E_EVAL_UNAVAILABLE"`` instead of ``"OK"``
|
|
27
|
+
when recomputation was asked for without an evaluated ``value`` (SPEC 10.1
|
|
28
|
+
step 7.6: steps 7.1-7.5 passed, nothing was compared, the level stays
|
|
29
|
+
``signed``). ``level``: ``"signed"`` (steps 1-6) or ``"recomputed"`` (step 7
|
|
27
30
|
too) when ok, else None. ``hash``: the receipt hash once the receipt is in
|
|
28
31
|
canonical form (steps 1-3 passed), else None. ``key``: the ``computed_by``
|
|
29
32
|
value when ok. ``detail``: human-readable text. ``path``: where the
|
|
30
|
-
failure is (a JSON path)
|
|
31
|
-
content was supplied and step 7.7 ran
|
|
33
|
+
failure is (a JSON path), ``"$.value"`` with ``E_EVAL_UNAVAILABLE``, or None.
|
|
34
|
+
``blob_checked``: True only when blob content was supplied and step 7.7 ran
|
|
35
|
+
and passed (the level and the code are unchanged).
|
|
32
36
|
"""
|
|
33
37
|
|
|
34
38
|
ok: bool
|
|
@@ -76,13 +80,14 @@ def _signed(receipt: Any, sources: Tuple[_Source, ...]) -> Tuple[Dict[str, Any],
|
|
|
76
80
|
return r, h
|
|
77
81
|
|
|
78
82
|
|
|
79
|
-
def _recompute(r: Dict[str, Any], recompute: Mapping[str, Any], sources: Tuple[_Source, ...]) ->
|
|
80
|
-
"""Step 7. Raises DatumeshError.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
def _recompute(r: Dict[str, Any], recompute: Mapping[str, Any], sources: Tuple[_Source, ...]) -> bool:
|
|
84
|
+
"""Step 7.1-7.6. Raises DatumeshError. Returns False when 7.1-7.5 passed and there is no
|
|
85
|
+
evaluated value to compare (E_EVAL_UNAVAILABLE), True when 7.6 passed."""
|
|
86
|
+
# 7.1 descriptor: only an absent member is "none obtained". Whatever is obtained is a JSON
|
|
87
|
+
# value (None is JSON null); text or bytes are parsed strictly first.
|
|
88
|
+
if "descriptor" not in recompute:
|
|
84
89
|
fail("E_FN_UNAVAILABLE", "no descriptor supplied for cell.fn_hash", "$.cell.fn_hash")
|
|
85
|
-
desc = load(
|
|
90
|
+
desc = load(recompute["descriptor"], "descriptor")
|
|
86
91
|
if hashlib.sha256(jcs_bytes(desc)).hexdigest() != r["cell"]["fn_hash"]:
|
|
87
92
|
fail("E_FN_HASH_MISMATCH", "the descriptor does not hash to cell.fn_hash", "$.cell.fn_hash")
|
|
88
93
|
_model.validate_function(desc, "descriptor")
|
|
@@ -124,16 +129,20 @@ def _recompute(r: Dict[str, Any], recompute: Mapping[str, Any], sources: Tuple[_
|
|
|
124
129
|
fqn = f"{sub['cell']['ns']}/{sub['cell']['fn']}"
|
|
125
130
|
if fqn not in declared_inputs:
|
|
126
131
|
fail("E_INPUT_UNDECLARED", f"input {h} is a cell of {fqn}, which the descriptor does not declare", p)
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
# at or before the receipt's asof (D17, spec-0.3.0); canonical instants compare as strings (3.3)
|
|
133
|
+
if sub["asof"] > r["asof"]:
|
|
134
|
+
fail("E_TIME_ORDER", f"input {h} has asof {sub['asof']}, after the receipt's {r['asof']}", p)
|
|
129
135
|
if latest_tx is None or sub["tx"] > latest_tx:
|
|
130
136
|
latest_tx = sub["tx"]
|
|
131
137
|
if latest_tx is not None and r["tx"] != latest_tx:
|
|
132
138
|
fail("E_TIME_ORDER", f"a computed cell's tx is the latest input tx ({latest_tx})", "$.tx")
|
|
133
|
-
# 7.6 value
|
|
139
|
+
# 7.6 value; without one, evaluation is unavailable (not a failure)
|
|
140
|
+
if "value" not in recompute:
|
|
141
|
+
return False
|
|
134
142
|
v = dict(recompute["value"])
|
|
135
143
|
if not _model.same_scalar(v, {"type": r["type"], "value": r["value"]}):
|
|
136
144
|
fail("E_VALUE_MISMATCH", "the recomputed value differs from the receipt's", "$.value")
|
|
145
|
+
return True
|
|
137
146
|
|
|
138
147
|
|
|
139
148
|
def check_blob_content(r: Dict[str, Any], content: bytes) -> None:
|
|
@@ -149,10 +158,12 @@ def _check_recompute_arg(recompute: Any) -> None:
|
|
|
149
158
|
if recompute is None:
|
|
150
159
|
return
|
|
151
160
|
if not isinstance(recompute, Mapping):
|
|
152
|
-
raise TypeError("recompute must be a mapping with
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
raise TypeError("recompute must be a mapping with optional 'descriptor', 'inputs' and 'value'")
|
|
162
|
+
if "value" in recompute:
|
|
163
|
+
v = recompute["value"]
|
|
164
|
+
if not isinstance(v, Mapping) or "type" not in v or "value" not in v:
|
|
165
|
+
raise TypeError("recompute['value'] must be {'type': ..., 'value': ...}: the evaluated result "
|
|
166
|
+
"(leave it out when the function could not be evaluated)")
|
|
156
167
|
inputs = recompute.get("inputs")
|
|
157
168
|
if inputs is not None and not isinstance(inputs, Mapping):
|
|
158
169
|
raise TypeError("recompute['inputs'] must be a mapping of receipt hash to receipt JSON text")
|
|
@@ -168,11 +179,21 @@ def verify(receipt: Any, keys: Any, recompute: Optional[Mapping[str, Any]] = Non
|
|
|
168
179
|
validity windows), or a list of those; the first source that covers the
|
|
169
180
|
``computed_by`` host is used. ``recompute`` (optional) enables step 7:
|
|
170
181
|
``{"descriptor": ..., "inputs": {hash: receipt text}, "value": {"type", "value"}}``.
|
|
182
|
+
``descriptor`` is the descriptor obtained for ``cell.fn_hash``: a parsed JSON
|
|
183
|
+
value, or JSON text/bytes (parsed strictly); leave it out when none was
|
|
184
|
+
obtained (E_FN_UNAVAILABLE). Any other value, ``None`` (JSON null)
|
|
185
|
+
included, is a descriptor offered and must hash to ``cell.fn_hash``
|
|
186
|
+
(E_FN_HASH_MISMATCH). ``value`` is what evaluating the function gave; leave
|
|
187
|
+
it out when the function could not be evaluated: once steps 7.1-7.5 pass,
|
|
188
|
+
the result is then ok at level ``signed`` with ``code`` ``"E_EVAL_UNAVAILABLE"``
|
|
189
|
+
and ``path`` ``"$.value"`` (SPEC 10.1 step 7.6).
|
|
171
190
|
``blob`` (optional): the content bytes of a blob value. Step 7.7 then runs
|
|
172
191
|
last (after step 7 if ``recompute`` is given, else directly after step 6):
|
|
173
192
|
the receipt's ``type`` must be ``blob`` (E_SCHEMA otherwise) and the content
|
|
174
193
|
must have the blob's length and SHA-256 (E_BLOB_MISMATCH). The level stays
|
|
175
|
-
``signed`` or ``recomputed``; ``blob_checked`` reports that 7.7 passed.
|
|
194
|
+
``signed`` or ``recomputed``; ``blob_checked`` reports that 7.7 passed. 7.7
|
|
195
|
+
also runs after E_EVAL_UNAVAILABLE: if it passes the code is kept, if it
|
|
196
|
+
fails its failure is reported instead.
|
|
176
197
|
|
|
177
198
|
Never raises for an invalid receipt or bad key material: the outcome is in
|
|
178
199
|
the returned :class:`VerifyResult`. Raises TypeError only for arguments of
|
|
@@ -188,9 +209,14 @@ def verify(receipt: Any, keys: Any, recompute: Optional[Mapping[str, Any]] = Non
|
|
|
188
209
|
try:
|
|
189
210
|
r, h = _signed(receipt, sources)
|
|
190
211
|
level, detail = "signed", "signature-valid"
|
|
212
|
+
code, path = "OK", None
|
|
191
213
|
if recompute is not None:
|
|
192
|
-
_recompute(r, recompute, sources)
|
|
193
|
-
|
|
214
|
+
if _recompute(r, recompute, sources):
|
|
215
|
+
level, detail = "recomputed", "fully verified by recomputation"
|
|
216
|
+
else:
|
|
217
|
+
code, path = "E_EVAL_UNAVAILABLE", "$.value"
|
|
218
|
+
detail = ("signature-valid; recomputation checks 7.1-7.5 passed, but the function was not "
|
|
219
|
+
"evaluated (no value supplied)")
|
|
194
220
|
checked = False
|
|
195
221
|
if blob is not None:
|
|
196
222
|
if r["type"] != "blob":
|
|
@@ -198,7 +224,7 @@ def verify(receipt: Any, keys: Any, recompute: Optional[Mapping[str, Any]] = Non
|
|
|
198
224
|
check_blob_content(r, bytes(blob))
|
|
199
225
|
detail += "; blob content matches"
|
|
200
226
|
checked = True
|
|
201
|
-
return VerifyResult(True,
|
|
227
|
+
return VerifyResult(True, code, level, h, r["computed_by"], detail, path, checked)
|
|
202
228
|
except DatumeshError as e:
|
|
203
229
|
h = h or getattr(e, "receipt_hash", None)
|
|
204
230
|
return VerifyResult(False, e.code, None, h, None, e.detail, e.path)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datumesh
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Verify Datumesh receipts (spec-0.
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Verify Datumesh receipts (spec-0.3.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents.
|
|
5
5
|
Author: Datumesh
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://datumesh.com
|
|
8
8
|
Project-URL: Repository, https://github.com/datumesh-com/datumesh-verify-py
|
|
9
|
-
Project-URL: Specification (spec-0.
|
|
10
|
-
Keywords: datumesh,spec-0.
|
|
9
|
+
Project-URL: Specification (spec-0.3.0), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md
|
|
10
|
+
Keywords: datumesh,spec-0.3.0,receipt,verification,ed25519,rfc8785,jcs,canonical-json
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -27,11 +27,11 @@ Dynamic: license-file
|
|
|
27
27
|
|
|
28
28
|
# datumesh
|
|
29
29
|
|
|
30
|
-
Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.
|
|
30
|
+
Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md) (revision `spec-0.3.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
33
|
pip install datumesh
|
|
34
|
-
git clone --depth 1 --branch spec-0.
|
|
34
|
+
git clone --depth 1 --branch spec-0.3.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```python
|
|
@@ -49,8 +49,8 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
|
|
|
49
49
|
|
|
50
50
|
## API
|
|
51
51
|
|
|
52
|
-
- `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor, the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
|
|
53
|
-
- `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one;
|
|
52
|
+
- `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. One code comes with success: when you pass `recompute` without a `value` because you could not evaluate the function, the other recomputation checks still run, and if they pass the result is ok at level `signed` with `code` `"E_EVAL_UNAVAILABLE"` (SPEC 10.1 step 7.6). It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor (leave it out if you have none; `None` is a JSON null and fails the hash check), the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
|
|
53
|
+
- `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; the receipt's `asof` must be at or before it, for `ok` and `stale` alike). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
|
|
54
54
|
- `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
|
|
55
55
|
- `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
|
|
56
56
|
- `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
|
|
@@ -18,11 +18,11 @@ LINEAGE = next(v for v in vectors_of("receipt", True) if v["id"] == "receipt/com
|
|
|
18
18
|
|
|
19
19
|
class ApiTest(unittest.TestCase):
|
|
20
20
|
def test_constants(self) -> None:
|
|
21
|
-
self.assertEqual(datumesh.SPEC_VERSION, "spec-0.
|
|
22
|
-
self.assertEqual(datumesh.__version__, "0.
|
|
23
|
-
self.assertEqual(len(datumesh.ERROR_CODES),
|
|
24
|
-
self.assertEqual(len(set(datumesh.ERROR_CODES)),
|
|
25
|
-
for code in ("E_CANON_UNIT", "E_CANON_BLOB", "E_BLOB_MISMATCH", "E_ENVELOPE"):
|
|
21
|
+
self.assertEqual(datumesh.SPEC_VERSION, "spec-0.3.0")
|
|
22
|
+
self.assertEqual(datumesh.__version__, "0.3.0")
|
|
23
|
+
self.assertEqual(len(datumesh.ERROR_CODES), 42)
|
|
24
|
+
self.assertEqual(len(set(datumesh.ERROR_CODES)), 42)
|
|
25
|
+
for code in ("E_CANON_UNIT", "E_CANON_BLOB", "E_BLOB_MISMATCH", "E_ENVELOPE", "E_EVAL_UNAVAILABLE"):
|
|
26
26
|
self.assertIn(code, datumesh.ERROR_CODES)
|
|
27
27
|
for name in ("resolved_unit", "check_envelope", "EnvelopeResult", "UNITS"):
|
|
28
28
|
self.assertIn(name, datumesh.__all__)
|
|
@@ -88,9 +88,12 @@ class ApiTest(unittest.TestCase):
|
|
|
88
88
|
# descriptor as JSON text
|
|
89
89
|
rc2 = dict(rc, descriptor=json.dumps(rc["descriptor"]))
|
|
90
90
|
self.assertEqual(verify(LINEAGE["input"], keys, rc2).level, "recomputed")
|
|
91
|
-
# descriptor
|
|
92
|
-
self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, descriptor=None)).code, "
|
|
91
|
+
# descriptor absent (E_FN_UNAVAILABLE), or None: a JSON null offered (E_FN_HASH_MISMATCH, spec-0.3.0)
|
|
92
|
+
self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, descriptor=None)).code, "E_FN_HASH_MISMATCH")
|
|
93
93
|
self.assertEqual(verify(LINEAGE["input"], keys, {"value": rc["value"]}).code, "E_FN_UNAVAILABLE")
|
|
94
|
+
# value absent: evaluation unavailable (spec-0.3.0), success at level signed
|
|
95
|
+
res = verify(LINEAGE["input"], keys, {"descriptor": rc["descriptor"], "inputs": rc["inputs"]})
|
|
96
|
+
self.assertEqual((res.ok, res.code, res.level), (True, "E_EVAL_UNAVAILABLE", "signed"))
|
|
94
97
|
# inputs missing, given as parsed objects, or tampered
|
|
95
98
|
self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, inputs={})).code, "E_INPUT_UNAVAILABLE")
|
|
96
99
|
parsed = {h: json.loads(t) for h, t in rc["inputs"].items()}
|
|
@@ -98,7 +101,7 @@ class ApiTest(unittest.TestCase):
|
|
|
98
101
|
# boolean vs string values are different
|
|
99
102
|
self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, value={"type": "number", "value": "108.920"})).code,
|
|
100
103
|
"E_VALUE_MISMATCH")
|
|
101
|
-
for bad in ([],
|
|
104
|
+
for bad in ([], dict(rc, value=None), {"value": "108.92"}, dict(rc, inputs=[])):
|
|
102
105
|
with self.subTest(bad=str(bad)[:40]), self.assertRaises(TypeError):
|
|
103
106
|
verify(LINEAGE["input"], keys, bad) # type: ignore[arg-type]
|
|
104
107
|
|
|
@@ -8,7 +8,7 @@ import subprocess
|
|
|
8
8
|
import sys
|
|
9
9
|
import unittest
|
|
10
10
|
|
|
11
|
-
from util import ROOT, blob_of, env_keys, expected_output, load_vectors, runner, vectors_of
|
|
11
|
+
from util import ROOT, blob_of, env_keys, expected_output, load_vectors, recompute_of, runner, vectors_of
|
|
12
12
|
|
|
13
13
|
import datumesh
|
|
14
14
|
|
|
@@ -16,9 +16,9 @@ import datumesh
|
|
|
16
16
|
class ConformanceTest(unittest.TestCase):
|
|
17
17
|
def test_vector_count(self) -> None:
|
|
18
18
|
counts = collections.Counter(v["kind"] for v in load_vectors())
|
|
19
|
-
self.assertEqual(sum(counts.values()),
|
|
19
|
+
self.assertEqual(sum(counts.values()), 365, counts)
|
|
20
20
|
self.assertEqual(counts["unit"], 13, counts)
|
|
21
|
-
self.assertEqual(counts["envelope"],
|
|
21
|
+
self.assertEqual(counts["envelope"], 60, counts)
|
|
22
22
|
# every vector's id is its path, and every kind is one the runner knows
|
|
23
23
|
for v in load_vectors():
|
|
24
24
|
self.assertIn(v["kind"], ("jcs", "scalar", "cell", "ref", "function", "uri", "keyid", "keys",
|
|
@@ -67,20 +67,20 @@ class ConformanceTest(unittest.TestCase):
|
|
|
67
67
|
def test_receipts_through_verify(self) -> None:
|
|
68
68
|
for v in vectors_of("receipt"):
|
|
69
69
|
with self.subTest(v["id"]):
|
|
70
|
-
|
|
70
|
+
rc = recompute_of(v)
|
|
71
|
+
res = datumesh.verify(v["input"], env_keys(v["keys"]), rc, blob_of(v))
|
|
71
72
|
if v["valid"]:
|
|
72
73
|
self.assertTrue(res.ok, res)
|
|
73
|
-
|
|
74
|
+
# SPEC 10.1 step 7.6 (spec-0.3.0): E_EVAL_UNAVAILABLE is reported with success, at $.value
|
|
75
|
+
self.assertEqual(res.code, v.get("code", "OK"))
|
|
74
76
|
self.assertEqual(res.level, v["level"])
|
|
75
77
|
self.assertEqual(res.hash, v["sha256"])
|
|
76
78
|
self.assertEqual(res.blob_checked, "blob_hex" in v)
|
|
77
79
|
self.assertEqual(res.key, json.loads(v["input"])["computed_by"])
|
|
78
|
-
self.
|
|
80
|
+
self.assertEqual(res.path, "$.value" if "code" in v else None)
|
|
79
81
|
# bytes input and parsed input give the same result
|
|
80
|
-
self.assertEqual(datumesh.verify(v["input"].encode(), env_keys(v["keys"]), v
|
|
81
|
-
|
|
82
|
-
self.assertEqual(datumesh.verify(json.loads(v["input"]), env_keys(v["keys"]), v.get("recompute"),
|
|
83
|
-
blob_of(v)), res)
|
|
82
|
+
self.assertEqual(datumesh.verify(v["input"].encode(), env_keys(v["keys"]), rc, blob_of(v)), res)
|
|
83
|
+
self.assertEqual(datumesh.verify(json.loads(v["input"]), env_keys(v["keys"]), rc, blob_of(v)), res)
|
|
84
84
|
self.assertEqual(datumesh.receipt_hash(v["input"]), v["sha256"])
|
|
85
85
|
self.assertEqual(datumesh.receipt_payload(v["input"]).decode(), v["canonical"])
|
|
86
86
|
else:
|
|
@@ -60,7 +60,7 @@ class EnvelopeVectorTest(unittest.TestCase):
|
|
|
60
60
|
else:
|
|
61
61
|
self.assertEqual((res.ok, res.code), (False, v["error"]))
|
|
62
62
|
n += 1
|
|
63
|
-
self.assertEqual(n,
|
|
63
|
+
self.assertEqual(n, 60)
|
|
64
64
|
|
|
65
65
|
def test_valid_bodies_with_wrong_status(self) -> None:
|
|
66
66
|
total = 0
|
|
@@ -245,7 +245,7 @@ class EnvelopeRulesTest(unittest.TestCase):
|
|
|
245
245
|
|
|
246
246
|
|
|
247
247
|
class RequestBindingTest(unittest.TestCase):
|
|
248
|
-
"""SPEC 5.5 step 5: the receipt answers the request (same cell id;
|
|
248
|
+
"""SPEC 5.5 step 5: the receipt answers the request (same cell id; an asof at or before the one asked for)."""
|
|
249
249
|
|
|
250
250
|
def code(self, body: dict, request: dict, status: int = 200) -> str:
|
|
251
251
|
return check_envelope(status, json.dumps(body), KEYS, request).code
|
|
@@ -254,7 +254,8 @@ class RequestBindingTest(unittest.TestCase):
|
|
|
254
254
|
self.assertEqual(self.code(ok(), {"cell_id": CELL_ID, "asof": R["asof"]}), "OK")
|
|
255
255
|
self.assertEqual(self.code(ok(), {"cell_id": CELL_ID}), "OK") # no asof asked: the mesh's now
|
|
256
256
|
self.assertEqual(self.code(ok(), {"cell_id": "0" * 64}), "E_ENVELOPE")
|
|
257
|
-
|
|
257
|
+
# ok (SPEC 5.5 step 5, spec-0.3.0): like stale, the receipt's asof is at or before the requested asof.
|
|
258
|
+
self.assertEqual(self.code(ok(), {"cell_id": CELL_ID, "asof": "2024-03-17T09:00:00.000000Z"}), "OK")
|
|
258
259
|
self.assertEqual(self.code(stale(), {"cell_id": "0" * 64}), "E_ENVELOPE")
|
|
259
260
|
# stale (SPEC 5.5 step 5, spec 240a830): the receipt's asof is at or before the requested asof.
|
|
260
261
|
self.assertEqual(self.code(stale(), {"cell_id": CELL_ID, "asof": "2024-03-17T09:00:00.000000Z"}), "OK")
|
|
@@ -262,11 +263,11 @@ class RequestBindingTest(unittest.TestCase):
|
|
|
262
263
|
self.assertEqual(self.code(stale(), {"cell_id": CELL_ID}), "OK")
|
|
263
264
|
self.assertEqual(self.code(stale(), {"cell_id": CELL_ID, "asof": "2024-03-16T08:59:59.999999Z"}), "E_ENVELOPE")
|
|
264
265
|
self.assertEqual(self.code(stale(), {"cell_id": CELL_ID, "asof": "2020-01-01T00:00:00.000000Z"}), "E_ENVELOPE")
|
|
265
|
-
# ok
|
|
266
|
+
# ok: a receipt asof after the requested one fails
|
|
266
267
|
self.assertEqual(self.code(ok(), {"cell_id": CELL_ID, "asof": "2024-03-16T08:59:59.999999Z"}), "E_ENVELOPE")
|
|
267
268
|
res = check_envelope(200, json.dumps(ok()), KEYS, {"cell_id": "0" * 64})
|
|
268
269
|
self.assertEqual(res.path, "$.receipt.cell")
|
|
269
|
-
res = check_envelope(200, json.dumps(ok()), KEYS, {"cell_id": CELL_ID, "asof": "2024-03-
|
|
270
|
+
res = check_envelope(200, json.dumps(ok()), KEYS, {"cell_id": CELL_ID, "asof": "2024-03-16T08:59:59.999999Z"})
|
|
270
271
|
self.assertEqual(res.path, "$.receipt.asof")
|
|
271
272
|
|
|
272
273
|
def test_order(self) -> None:
|
|
@@ -13,7 +13,7 @@ import unittest
|
|
|
13
13
|
from typing import Any, Dict, List, Optional
|
|
14
14
|
from unittest import mock
|
|
15
15
|
|
|
16
|
-
from util import FIXTURES, env_keys, vectors_of
|
|
16
|
+
from util import FIXTURES, env_keys, recompute_of, vectors_of
|
|
17
17
|
|
|
18
18
|
import datumesh
|
|
19
19
|
from datumesh import KeysResult, keys_from_response, verify
|
|
@@ -178,7 +178,7 @@ class VerifyKeyHandlingTest(unittest.TestCase):
|
|
|
178
178
|
with self.assertRaises(TypeError):
|
|
179
179
|
verify(5, DOC) # type: ignore[arg-type]
|
|
180
180
|
with self.assertRaises(TypeError):
|
|
181
|
-
verify(VECTOR["input"], DOC, {"descriptor": None})
|
|
181
|
+
verify(VECTOR["input"], DOC, {"descriptor": None, "value": None})
|
|
182
182
|
|
|
183
183
|
|
|
184
184
|
# ---------------------------------------------------------------- fetch_keys
|
|
@@ -305,7 +305,7 @@ class FetchKeysTest(unittest.TestCase):
|
|
|
305
305
|
mock.patch.object(socket, "socket", side_effect=AssertionError("socket opened")), \
|
|
306
306
|
mock.patch.object(socket, "create_connection", side_effect=AssertionError("socket opened")):
|
|
307
307
|
for v in vectors_of("receipt"):
|
|
308
|
-
datumesh.verify(v["input"], env_keys(v["keys"]), v
|
|
308
|
+
datumesh.verify(v["input"], env_keys(v["keys"]), recompute_of(v))
|
|
309
309
|
datumesh.verify(VECTOR["input"], [])
|
|
310
310
|
|
|
311
311
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""The changes of spec-0.3.0, each tested through the API (the vectors are run by test_conformance).
|
|
2
|
+
|
|
3
|
+
- D17 revised (8.2, 10.1 step 7.5): an input receipt's asof is at or before the receipt's.
|
|
4
|
+
- D31 revised (5.5 step 5): for ok as for stale, the receipt's asof is at or before the requested one.
|
|
5
|
+
- E_EVAL_UNAVAILABLE (10.1 step 7.6, 10.2, 11): success at level signed, with the code at $.value.
|
|
6
|
+
- 10.1 step 7.1 / 11 (DEF-17): a null descriptor is a descriptor offered (E_FN_HASH_MISMATCH).
|
|
7
|
+
- 9.3 / 11 (DEF-16(b)): only the integer 200 is a retrieved key document.
|
|
8
|
+
- 3.7: the 128-character media type limit is on the whole media type.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import copy
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import unittest
|
|
17
|
+
from typing import Any, Dict
|
|
18
|
+
|
|
19
|
+
from nacl.signing import SigningKey
|
|
20
|
+
|
|
21
|
+
from util import ROOT, env_keys, recompute_of, runner, vectors_of
|
|
22
|
+
|
|
23
|
+
import datumesh
|
|
24
|
+
from datumesh import DatumeshError, canonicalize, check_envelope, keys_from_response, verify
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def vector(vid: str) -> Dict[str, Any]:
|
|
28
|
+
v: Dict[str, Any] = json.loads((ROOT / "spec" / "vectors" / f"{vid}.json").read_text(encoding="utf-8"))
|
|
29
|
+
return v
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
EARLIER = vector("receipt/computed-inputs-earlier-asof")
|
|
33
|
+
EVAL_STORED = vector("receipt/eval-unavailable-stored")
|
|
34
|
+
EVAL_COMPUTED = vector("receipt/eval-unavailable-computed")
|
|
35
|
+
EVAL_BLOB = vector("receipt/eval-unavailable-blob-checked")
|
|
36
|
+
STORED = vector("receipt/stored-number")
|
|
37
|
+
LINEAGE = vector("receipt/computed-with-lineage")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resign(v: Dict[str, Any], **changes: Any) -> str:
|
|
41
|
+
"""The vector's receipt with ``changes``, signed again with the vector's keypair."""
|
|
42
|
+
r = json.loads(v["input"])
|
|
43
|
+
r.update(changes)
|
|
44
|
+
payload = datumesh.jcs({k: x for k, x in r.items() if k != "sig"})
|
|
45
|
+
r["sig"] = SigningKey(bytes.fromhex(v["keypair"]["seed"])).sign(payload).signature.hex()
|
|
46
|
+
return json.dumps(r)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def without_value(v: Dict[str, Any]) -> Dict[str, Any]:
|
|
50
|
+
rc = copy.deepcopy(v["recompute"])
|
|
51
|
+
rc.pop("value", None)
|
|
52
|
+
return rc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class InputAsofTest(unittest.TestCase):
|
|
56
|
+
"""D17 revised: an input's asof at or before the receipt's; only a later one is E_TIME_ORDER."""
|
|
57
|
+
|
|
58
|
+
def test_boundaries(self) -> None:
|
|
59
|
+
keys = env_keys(EARLIER["keys"])
|
|
60
|
+
rc = EARLIER["recompute"]
|
|
61
|
+
latest_input_asof = max(json.loads(t)["asof"] for t in rc["inputs"].values())
|
|
62
|
+
self.assertEqual(latest_input_asof, "2024-03-16T09:00:00.000000Z")
|
|
63
|
+
# the vector as it is: inputs at earlier asofs
|
|
64
|
+
self.assertEqual(verify(EARLIER["input"], keys, rc).level, "recomputed")
|
|
65
|
+
# the receipt's asof equal to the latest input asof
|
|
66
|
+
res = verify(resign(EARLIER, asof=latest_input_asof), keys, rc)
|
|
67
|
+
self.assertEqual((res.ok, res.code, res.level), (True, "OK", "recomputed"))
|
|
68
|
+
# one microsecond earlier: that input is later than the receipt
|
|
69
|
+
res = verify(resign(EARLIER, asof="2024-03-16T08:59:59.999999Z"), keys, rc)
|
|
70
|
+
self.assertEqual((res.ok, res.code, res.path), (False, "E_TIME_ORDER", "$.inputs[0]"))
|
|
71
|
+
# still reported when evaluation is unavailable: 7.5 comes before 7.6
|
|
72
|
+
res = verify(resign(EARLIER, asof="2024-03-16T08:59:59.999999Z"), keys, without_value(EARLIER))
|
|
73
|
+
self.assertEqual((res.ok, res.code), (False, "E_TIME_ORDER"))
|
|
74
|
+
|
|
75
|
+
def test_tx_is_still_the_latest_input_tx(self) -> None:
|
|
76
|
+
keys = env_keys(EARLIER["keys"])
|
|
77
|
+
res = verify(resign(EARLIER, tx="2024-03-15T16:00:00.000000Z"), keys, EARLIER["recompute"])
|
|
78
|
+
self.assertEqual((res.code, res.path), ("E_TIME_ORDER", "$.tx"))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class EvalUnavailableTest(unittest.TestCase):
|
|
82
|
+
"""E_EVAL_UNAVAILABLE: recompute without value, after 7.1-7.5 pass."""
|
|
83
|
+
|
|
84
|
+
def test_error_code_list(self) -> None:
|
|
85
|
+
codes = datumesh.ERROR_CODES
|
|
86
|
+
self.assertIn("E_EVAL_UNAVAILABLE", codes)
|
|
87
|
+
i = codes.index("E_EVAL_UNAVAILABLE")
|
|
88
|
+
self.assertEqual((codes[i - 1], codes[i + 1]), ("E_VALUE_MISMATCH", "E_BLOB_MISMATCH"))
|
|
89
|
+
self.assertEqual(DatumeshError("E_EVAL_UNAVAILABLE").code, "E_EVAL_UNAVAILABLE")
|
|
90
|
+
|
|
91
|
+
def test_stored_and_computed(self) -> None:
|
|
92
|
+
for v in (EVAL_STORED, EVAL_COMPUTED):
|
|
93
|
+
with self.subTest(v["id"]):
|
|
94
|
+
res = verify(v["input"], env_keys(v["keys"]), v["recompute"])
|
|
95
|
+
self.assertEqual(
|
|
96
|
+
(res.ok, res.code, res.level, res.hash, res.key, res.path, res.blob_checked),
|
|
97
|
+
(True, "E_EVAL_UNAVAILABLE", "signed", v["sha256"], json.loads(v["input"])["computed_by"],
|
|
98
|
+
"$.value", False))
|
|
99
|
+
self.assertIn("not evaluated", res.detail)
|
|
100
|
+
|
|
101
|
+
def test_existing_vectors_without_value(self) -> None:
|
|
102
|
+
# Every valid recomputed vector, with value removed, is signed with E_EVAL_UNAVAILABLE.
|
|
103
|
+
for v in vectors_of("receipt", True):
|
|
104
|
+
if v.get("level") != "recomputed":
|
|
105
|
+
continue
|
|
106
|
+
with self.subTest(v["id"]):
|
|
107
|
+
blob = bytes.fromhex(v["blob_hex"]) if "blob_hex" in v else None
|
|
108
|
+
res = verify(v["input"], env_keys(v["keys"]), without_value(v), blob)
|
|
109
|
+
self.assertEqual((res.ok, res.code, res.level, res.path, res.blob_checked),
|
|
110
|
+
(True, "E_EVAL_UNAVAILABLE", "signed", "$.value", blob is not None))
|
|
111
|
+
|
|
112
|
+
def test_failures_before_7_6_are_reported_instead(self) -> None:
|
|
113
|
+
keys = env_keys(LINEAGE["keys"])
|
|
114
|
+
rc = without_value(LINEAGE)
|
|
115
|
+
self.assertEqual(verify(LINEAGE["input"], keys, {}).code, "E_FN_UNAVAILABLE")
|
|
116
|
+
self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, inputs={})).code, "E_INPUT_UNAVAILABLE")
|
|
117
|
+
self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, descriptor=None)).code, "E_FN_HASH_MISMATCH")
|
|
118
|
+
# a receipt that is not signature-valid fails as before
|
|
119
|
+
r = json.loads(LINEAGE["input"])
|
|
120
|
+
r["summary"] = "changed"
|
|
121
|
+
res = verify(r, keys, rc)
|
|
122
|
+
self.assertEqual((res.ok, res.code, res.level), (False, "E_SIGNATURE", None))
|
|
123
|
+
|
|
124
|
+
def test_blob_after_eval_unavailable(self) -> None:
|
|
125
|
+
keys = env_keys(EVAL_BLOB["keys"])
|
|
126
|
+
content = bytes.fromhex(EVAL_BLOB["blob_hex"])
|
|
127
|
+
rc = EVAL_BLOB["recompute"]
|
|
128
|
+
res = verify(EVAL_BLOB["input"], keys, rc, content)
|
|
129
|
+
self.assertEqual((res.ok, res.code, res.level, res.path, res.blob_checked),
|
|
130
|
+
(True, "E_EVAL_UNAVAILABLE", "signed", "$.value", True))
|
|
131
|
+
# 7.7 fails: an ordinary failure replaces the code
|
|
132
|
+
res = verify(EVAL_BLOB["input"], keys, rc, content + b"x")
|
|
133
|
+
self.assertEqual((res.ok, res.code, res.level, res.blob_checked), (False, "E_BLOB_MISMATCH", None, False))
|
|
134
|
+
# content for a value that is not a blob: E_SCHEMA, also after E_EVAL_UNAVAILABLE
|
|
135
|
+
res = verify(EVAL_STORED["input"], env_keys(EVAL_STORED["keys"]), EVAL_STORED["recompute"], b"x")
|
|
136
|
+
self.assertEqual((res.ok, res.code), (False, "E_SCHEMA"))
|
|
137
|
+
# no content: the code, and blob_checked false
|
|
138
|
+
res = verify(EVAL_BLOB["input"], keys, rc)
|
|
139
|
+
self.assertEqual((res.ok, res.code, res.blob_checked), (True, "E_EVAL_UNAVAILABLE", False))
|
|
140
|
+
|
|
141
|
+
def test_value_none_is_a_caller_error(self) -> None:
|
|
142
|
+
# Only an absent value means unavailable; a value that is not {"type", "value"} is a TypeError.
|
|
143
|
+
for bad in (None, "1.0892", {"type": "number"}):
|
|
144
|
+
with self.subTest(bad=bad), self.assertRaises(TypeError):
|
|
145
|
+
verify(EVAL_STORED["input"], env_keys(EVAL_STORED["keys"]), dict(EVAL_STORED["recompute"], value=bad))
|
|
146
|
+
|
|
147
|
+
def test_runner_reports_code_only_with_it(self) -> None:
|
|
148
|
+
run = runner().run_vector
|
|
149
|
+
for v in vectors_of("receipt"):
|
|
150
|
+
with self.subTest(v["id"]):
|
|
151
|
+
out = run(v)
|
|
152
|
+
if v["valid"] and v.get("code"):
|
|
153
|
+
self.assertEqual(out.get("code"), "E_EVAL_UNAVAILABLE")
|
|
154
|
+
self.assertEqual(out["level"], "signed")
|
|
155
|
+
else:
|
|
156
|
+
self.assertNotIn("code", out)
|
|
157
|
+
|
|
158
|
+
def test_envelope_error_may_carry_it(self) -> None:
|
|
159
|
+
body = json.dumps({"outcome": "error", "error": "E_EVAL_UNAVAILABLE"})
|
|
160
|
+
res = check_envelope(503, body, [], {"cell_id": "0" * 64})
|
|
161
|
+
self.assertEqual((res.ok, res.outcome), (True, "error"))
|
|
162
|
+
body = json.dumps({"outcome": "bad_request", "error": "E_EVAL_UNAVAILABLE"})
|
|
163
|
+
self.assertEqual(check_envelope(400, body, [], {"cell_id": "0" * 64}).code, "E_ENVELOPE")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class DescriptorOfferedTest(unittest.TestCase):
|
|
167
|
+
"""10.1 step 7.1: what is obtained is a JSON value; only obtaining nothing is E_FN_UNAVAILABLE."""
|
|
168
|
+
|
|
169
|
+
def test_forms(self) -> None:
|
|
170
|
+
keys = env_keys(STORED["keys"])
|
|
171
|
+
rc = STORED["recompute"]
|
|
172
|
+
desc = rc["descriptor"]
|
|
173
|
+
cases = [
|
|
174
|
+
({"inputs": {}, "value": rc["value"]}, "E_FN_UNAVAILABLE", "$.cell.fn_hash"), # nothing obtained
|
|
175
|
+
(dict(rc, descriptor=None), "E_FN_HASH_MISMATCH", "$.cell.fn_hash"), # JSON null
|
|
176
|
+
(dict(rc, descriptor=0), "E_FN_HASH_MISMATCH", "$.cell.fn_hash"),
|
|
177
|
+
(dict(rc, descriptor=[]), "E_FN_HASH_MISMATCH", "$.cell.fn_hash"),
|
|
178
|
+
(dict(rc, descriptor="null"), "E_FN_HASH_MISMATCH", "$.cell.fn_hash"), # text of null
|
|
179
|
+
# a JSON string value holding the descriptor's text, given as JSON text: a string
|
|
180
|
+
(dict(rc, descriptor=json.dumps(json.dumps(desc))), "E_FN_HASH_MISMATCH", "$.cell.fn_hash"),
|
|
181
|
+
# bytes or text are parsed strictly first, with the parse codes
|
|
182
|
+
(dict(rc, descriptor=b"{"), "E_PARSE_JSON", None),
|
|
183
|
+
(dict(rc, descriptor='{"a":1,"a":1}'), "E_PARSE_DUPLICATE_KEY", None),
|
|
184
|
+
]
|
|
185
|
+
for recompute, code, path in cases:
|
|
186
|
+
with self.subTest(recompute=str(recompute.get("descriptor", "<absent>"))[:30]):
|
|
187
|
+
res = verify(STORED["input"], keys, recompute)
|
|
188
|
+
self.assertEqual(res.code, code)
|
|
189
|
+
if path is not None:
|
|
190
|
+
self.assertEqual(res.path, path)
|
|
191
|
+
# the descriptor as a parsed value, as text, and as bytes
|
|
192
|
+
for d in (desc, json.dumps(desc), json.dumps(desc).encode()):
|
|
193
|
+
self.assertEqual(verify(STORED["input"], keys, dict(rc, descriptor=d)).level, "recomputed")
|
|
194
|
+
|
|
195
|
+
def test_vectors(self) -> None:
|
|
196
|
+
for vid in ("invalid/receipt-descriptor-null", "invalid/receipt-descriptor-text"):
|
|
197
|
+
v = vector(vid)
|
|
198
|
+
with self.subTest(vid):
|
|
199
|
+
self.assertEqual(verify(v["input"], env_keys(v["keys"]), recompute_of(v)).code, "E_FN_HASH_MISMATCH")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class OkAsofTest(unittest.TestCase):
|
|
203
|
+
"""D31 revised: ok binds to an asof at or before the requested one, like stale."""
|
|
204
|
+
|
|
205
|
+
def test_ok_and_stale(self) -> None:
|
|
206
|
+
v = vector("envelope/served-ok")
|
|
207
|
+
s = vector("envelope/served-stale")
|
|
208
|
+
req = v["request"]
|
|
209
|
+
receipt = json.loads(v["input"])["receipt"]
|
|
210
|
+
keys = env_keys(v["keys"])
|
|
211
|
+
self.assertLess(receipt["asof"], req["asof"])
|
|
212
|
+
for body in (v["input"], s["input"]):
|
|
213
|
+
env = json.loads(body)
|
|
214
|
+
for asof, ok in ((req["asof"], True), (receipt["asof"], True), (None, True),
|
|
215
|
+
("2024-03-16T08:59:59.999999Z", False), ("2020-01-01T00:00:00.000000Z", False)):
|
|
216
|
+
request = {"cell_id": req["cell_id"]} if asof is None else {"cell_id": req["cell_id"], "asof": asof}
|
|
217
|
+
with self.subTest(outcome=env["outcome"], asof=asof):
|
|
218
|
+
res = check_envelope(200, body, keys, request)
|
|
219
|
+
self.assertEqual(res.ok, ok, res)
|
|
220
|
+
if not ok:
|
|
221
|
+
self.assertEqual((res.code, res.path), ("E_ENVELOPE", "$.receipt.asof"))
|
|
222
|
+
|
|
223
|
+
def test_age_seconds_still_tells_them_apart(self) -> None:
|
|
224
|
+
v = vector("envelope/served-ok")
|
|
225
|
+
env = json.loads(v["input"])
|
|
226
|
+
keys = env_keys(v["keys"])
|
|
227
|
+
self.assertEqual(check_envelope(200, json.dumps(dict(env, age_seconds=5)), keys, v["request"]).code,
|
|
228
|
+
"E_ENVELOPE")
|
|
229
|
+
self.assertEqual(check_envelope(200, json.dumps(dict(env, outcome="stale")), keys, v["request"]).code,
|
|
230
|
+
"E_ENVELOPE")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class KeyStatusTest(unittest.TestCase):
|
|
234
|
+
"""9.3 / 11: only the integer 200 is a retrieved document."""
|
|
235
|
+
|
|
236
|
+
def test_status_forms(self) -> None:
|
|
237
|
+
body = json.loads(next(v for v in vectors_of("keys", True))["env"]["https"]["body"])
|
|
238
|
+
host = body["host"]
|
|
239
|
+
text = json.dumps(body)
|
|
240
|
+
self.assertTrue(keys_from_response(host, 200, text).ok)
|
|
241
|
+
for status in ("200", 200.0, True, None, b"200", [200]):
|
|
242
|
+
with self.subTest(status=status):
|
|
243
|
+
res = keys_from_response(host, status, text) # type: ignore[arg-type]
|
|
244
|
+
self.assertEqual((res.ok, res.code), (False, "E_KEY_UNAVAILABLE"))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class MediaTypeLengthTest(unittest.TestCase):
|
|
248
|
+
"""3.7: 128 characters for the whole media type; no per-name limit."""
|
|
249
|
+
|
|
250
|
+
def test_limits(self) -> None:
|
|
251
|
+
h = hashlib.sha256(b"").hexdigest()
|
|
252
|
+
|
|
253
|
+
def code(media: str) -> Any:
|
|
254
|
+
try:
|
|
255
|
+
canonicalize({"type": "blob", "value": f"{h}:0:{media}"}, "scalar")
|
|
256
|
+
return None
|
|
257
|
+
except DatumeshError as e:
|
|
258
|
+
return e.code
|
|
259
|
+
|
|
260
|
+
self.assertIsNone(code("a/" + "b" * 126))
|
|
261
|
+
self.assertEqual(code("a/" + "b" * 127), "E_CANON_BLOB")
|
|
262
|
+
self.assertIsNone(code("a" * 126 + "/b"))
|
|
263
|
+
self.assertEqual(code("a" * 127 + "/b"), "E_CANON_BLOB")
|
|
264
|
+
self.assertIsNone(code("a" * 63 + "/" + "b" * 64))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class VersionTest(unittest.TestCase):
|
|
268
|
+
def test_versions(self) -> None:
|
|
269
|
+
self.assertEqual(datumesh.SPEC_VERSION, "spec-0.3.0")
|
|
270
|
+
self.assertEqual(datumesh.__version__, "0.3.0")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|