datumesh 0.4.2__tar.gz → 0.4.3__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.4.2 → datumesh-0.4.3}/PKG-INFO +15 -10
- {datumesh-0.4.2 → datumesh-0.4.3}/README.md +11 -6
- {datumesh-0.4.2 → datumesh-0.4.3}/pyproject.toml +4 -4
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/__init__.py +25 -6
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_envelope.py +20 -4
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_errors.py +10 -3
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_keys.py +84 -4
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_lexical.py +2 -0
- datumesh-0.4.3/src/datumesh/_licence.py +257 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_unitlist.py +1 -1
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/PKG-INFO +15 -10
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/SOURCES.txt +2 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_api.py +4 -4
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_conformance.py +11 -7
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_envelope.py +1 -1
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_4_1.py +1 -1
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_4_2.py +4 -3
- datumesh-0.4.3/tests/test_spec_0_4_3.py +353 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/LICENSE +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/setup.cfg +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_json.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_mesh.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_model.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_ucd15.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_unicode.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_units.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_uri.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_verify.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/py.typed +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/dependency_links.txt +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/requires.txt +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/top_level.txt +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_bitflips.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_blob.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_jcs.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_keys.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_2_1.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_3_0.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_4_0.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_unicode.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_units.py +0 -0
- {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_uri.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datumesh
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary: Verify Datumesh receipts (spec-0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
|
+
Summary: Verify Datumesh receipts (spec-0.4.3): RFC 8785 canonical JSON, Ed25519 signatures, slice-as-lineage, blobs, units, cell URIs, response envelopes (pending jobs, pins, slice pages, batches), key documents with declarations, mesh documents, licence inheritance and entitlement.
|
|
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.4.
|
|
10
|
-
Keywords: datumesh,spec-0.4.
|
|
9
|
+
Project-URL: Specification (spec-0.4.3), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.4.3/SPEC.md
|
|
10
|
+
Keywords: datumesh,spec-0.4.3,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 slice manifests, 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.4.
|
|
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 slice manifests, 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.4.3/SPEC.md) (revision `spec-0.4.3`) defines. It also checks a mesh's response envelope (including `pending` jobs), resolves the unit of a cell's value, checks a host's mesh document for cross-mesh use, and, separately from verification, checks a computed receipt's licence against the inheritance rule (the entitlement check).
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
33
|
pip install datumesh
|
|
34
|
-
git clone --depth 1 --branch spec-0.4.
|
|
34
|
+
git clone --depth 1 --branch spec-0.4.3 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
```python
|
|
@@ -52,16 +52,21 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
|
|
|
52
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`. Two codes come with success, at level `signed` (SPEC 10.1 step 7.6): `"E_EVAL_UNAVAILABLE"` when you pass `recompute` without a `value` because you could not evaluate the function (the other recomputation checks still run first), and `"E_INPUT_SUPERSEDED"` when your value differs but the evaluation's `lineage` shows that an input was replaced by a later version (the receipt is historical, not wrong). 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 (slice members included), the manifests of slice entries (`slice:<slice id>:<members hash>`), the value you computed and optionally the evaluation's lineage (the library does not evaluate functions, formulas included); `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
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). A `not_covered` envelope may carry a formula evaluation `fault` (exposed as `fault`). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
|
|
54
54
|
- Jobs (spec-0.4.1): a `pending` envelope (HTTP 202) carries `job`, `estimate_seconds` and `poll` (exposed on the result) and echoes your request, which must match. When you poll, pass the original request plus `"job": <the job id>`: the answer is checked against the original cell and `asof`, never the job URL, and only then may it be `bad_request` with `E_JOB_UNKNOWN` (ask again).
|
|
55
|
-
- Pins, trust and the mesh asked (spec-0.4.
|
|
56
|
-
- Slice pages (spec-0.4.
|
|
57
|
-
- `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.
|
|
55
|
+
- Pins, trust and the mesh asked (spec-0.4.3): a request description may carry `fn_hash` (you pinned the function: `parse_uri`/`format_uri` handle `fn_hash=`) and `mesh` (the host you asked). Answers to a pinned request must echo the pin; with `mesh`, `computed` (this mesh evaluated and signed it) is checked against the signer, and `poll` against the host. Without `mesh`, comparing them with the host you asked is your own check.
|
|
56
|
+
- Slice pages (spec-0.4.3): `check_envelope` with `{"slice_id", "asof"?, "fn_hash"?, "mesh"?, "after"?}` checks a page of the slice endpoint: every member's receipt, its membership, `asof`, trust and key order (`result.members`, `result.next`).
|
|
57
|
+
- `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.3): the answer to `POST /cells`, checked as a whole, then item by item against each request (`result.items`); a refusal of the whole batch gives `result.outcome`.
|
|
58
58
|
- `resolve_mesh(host, keys, mesh) -> MeshResult`: checks a host's key document and mesh document (`/.well-known/datumesh-mesh.json`, SPEC 9.6) together, given each response as a `(status, body)` pair; the result has `bindings` and the known `endpoints`, or `E_KEY_UNAVAILABLE`, `E_KEY_DOC` or `E_MESH_DOC`. It uses no network, and verification never needs it.
|
|
59
|
+
- Licences (spec-0.4.3): `inherit_licence(licences, declarations, fn, issued, mesh_licence) -> LicenceResult` applies the inheritance rule of SPEC 8.5 (the strictest input licence wins; a restricted licence only where the function's publisher declared it, open at `issued`) and gives `licence` (None for a refusal, which a mesh answers `unlicensed`) and `reason`. `check_entitlement(receipt, licences, keys) -> EntitlementResult` (SPEC 10.3) says whether a verified computed receipt's licence is the one the rule gives, with one of eight reasons; `keys` is the key document of the publisher's host (the host of `cell.ns`), and another host's is a `ValueError`. `input_licences(receipt, receipts, manifests=None, keys=None)` collects the input licences through the receipts and slice manifests you have. None of this is verification: a receipt that is not entitled is still signature-valid.
|
|
60
|
+
- Key documents may carry `declarations` (spec-0.4.3, SPEC 9.2): `KeysResult.declarations` maps each declared function name to its `Declaration` entries (`licence`, `valid_from`, `valid_to`). A malformed one rejects the whole document (`E_KEY_DOC`); otherwise they change nothing in verification.
|
|
61
|
+
- `E_FOREIGN_UNAVAILABLE` (spec-0.4.3): an `error` envelope at HTTP 502 saying another mesh could not be reached, with `detail` (exposed as `error_detail`): one of `FOREIGN_DETAILS` (`offline`, `network`, `dns`, `tls`, `timeout`, `refused`). `is_unrooted(namespace)` tells names under `.invalid` (SPEC 2.5), whose receipts verify only against a key document supplied out of band.
|
|
59
62
|
- `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
|
|
60
63
|
- `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
|
|
61
64
|
- `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
|
|
62
65
|
- `canonicalize(obj, kind=None) -> bytes`, `cell_hash`, `function_hash`, `receipt_hash`, `receipt_payload`, `jcs`: validation, RFC 8785 canonical bytes and SHA-256 hashes. Kinds `"slice"` and `"manifest"`, with `slice_id`, `members_hash` and `slice_entry`, cover slice objects and manifests (SPEC 4.4, 15.13).
|
|
63
66
|
- `parse_uri(uri, signature)`, `format_uri(ref)`, `uri_to_https(uri, mesh)`: cell URIs (SPEC 5).
|
|
64
|
-
- `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
|
|
67
|
+
- `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `FOREIGN_DETAILS`, `REASONS`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
|
|
68
|
+
|
|
69
|
+
Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
65
70
|
|
|
66
71
|
Requires Python 3.10 or later and PyNaCl. String rules use Unicode 15.1 tables shipped with the package, so results do not depend on the Python version's own Unicode database; the unit list is copied from the specification's `units.md`. Choosing which publishers to trust is up to you: a valid signature only means the key holder made the statement.
|
|
67
72
|
|
|
@@ -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 slice manifests, 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.4.
|
|
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 slice manifests, 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.4.3/SPEC.md) (revision `spec-0.4.3`) defines. It also checks a mesh's response envelope (including `pending` jobs), resolves the unit of a cell's value, checks a host's mesh document for cross-mesh use, and, separately from verification, checks a computed receipt's licence against the inheritance rule (the entitlement check).
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
pip install datumesh
|
|
7
|
-
git clone --depth 1 --branch spec-0.4.
|
|
7
|
+
git clone --depth 1 --branch spec-0.4.3 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
```python
|
|
@@ -25,16 +25,21 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
|
|
|
25
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`. Two codes come with success, at level `signed` (SPEC 10.1 step 7.6): `"E_EVAL_UNAVAILABLE"` when you pass `recompute` without a `value` because you could not evaluate the function (the other recomputation checks still run first), and `"E_INPUT_SUPERSEDED"` when your value differs but the evaluation's `lineage` shows that an input was replaced by a later version (the receipt is historical, not wrong). 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 (slice members included), the manifests of slice entries (`slice:<slice id>:<members hash>`), the value you computed and optionally the evaluation's lineage (the library does not evaluate functions, formulas included); `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
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). A `not_covered` envelope may carry a formula evaluation `fault` (exposed as `fault`). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
|
|
27
27
|
- Jobs (spec-0.4.1): a `pending` envelope (HTTP 202) carries `job`, `estimate_seconds` and `poll` (exposed on the result) and echoes your request, which must match. When you poll, pass the original request plus `"job": <the job id>`: the answer is checked against the original cell and `asof`, never the job URL, and only then may it be `bad_request` with `E_JOB_UNKNOWN` (ask again).
|
|
28
|
-
- Pins, trust and the mesh asked (spec-0.4.
|
|
29
|
-
- Slice pages (spec-0.4.
|
|
30
|
-
- `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.
|
|
28
|
+
- Pins, trust and the mesh asked (spec-0.4.3): a request description may carry `fn_hash` (you pinned the function: `parse_uri`/`format_uri` handle `fn_hash=`) and `mesh` (the host you asked). Answers to a pinned request must echo the pin; with `mesh`, `computed` (this mesh evaluated and signed it) is checked against the signer, and `poll` against the host. Without `mesh`, comparing them with the host you asked is your own check.
|
|
29
|
+
- Slice pages (spec-0.4.3): `check_envelope` with `{"slice_id", "asof"?, "fn_hash"?, "mesh"?, "after"?}` checks a page of the slice endpoint: every member's receipt, its membership, `asof`, trust and key order (`result.members`, `result.next`).
|
|
30
|
+
- `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.3): the answer to `POST /cells`, checked as a whole, then item by item against each request (`result.items`); a refusal of the whole batch gives `result.outcome`.
|
|
31
31
|
- `resolve_mesh(host, keys, mesh) -> MeshResult`: checks a host's key document and mesh document (`/.well-known/datumesh-mesh.json`, SPEC 9.6) together, given each response as a `(status, body)` pair; the result has `bindings` and the known `endpoints`, or `E_KEY_UNAVAILABLE`, `E_KEY_DOC` or `E_MESH_DOC`. It uses no network, and verification never needs it.
|
|
32
|
+
- Licences (spec-0.4.3): `inherit_licence(licences, declarations, fn, issued, mesh_licence) -> LicenceResult` applies the inheritance rule of SPEC 8.5 (the strictest input licence wins; a restricted licence only where the function's publisher declared it, open at `issued`) and gives `licence` (None for a refusal, which a mesh answers `unlicensed`) and `reason`. `check_entitlement(receipt, licences, keys) -> EntitlementResult` (SPEC 10.3) says whether a verified computed receipt's licence is the one the rule gives, with one of eight reasons; `keys` is the key document of the publisher's host (the host of `cell.ns`), and another host's is a `ValueError`. `input_licences(receipt, receipts, manifests=None, keys=None)` collects the input licences through the receipts and slice manifests you have. None of this is verification: a receipt that is not entitled is still signature-valid.
|
|
33
|
+
- Key documents may carry `declarations` (spec-0.4.3, SPEC 9.2): `KeysResult.declarations` maps each declared function name to its `Declaration` entries (`licence`, `valid_from`, `valid_to`). A malformed one rejects the whole document (`E_KEY_DOC`); otherwise they change nothing in verification.
|
|
34
|
+
- `E_FOREIGN_UNAVAILABLE` (spec-0.4.3): an `error` envelope at HTTP 502 saying another mesh could not be reached, with `detail` (exposed as `error_detail`): one of `FOREIGN_DETAILS` (`offline`, `network`, `dns`, `tls`, `timeout`, `refused`). `is_unrooted(namespace)` tells names under `.invalid` (SPEC 2.5), whose receipts verify only against a key document supplied out of band.
|
|
32
35
|
- `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
|
|
33
36
|
- `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
|
|
34
37
|
- `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
|
|
35
38
|
- `canonicalize(obj, kind=None) -> bytes`, `cell_hash`, `function_hash`, `receipt_hash`, `receipt_payload`, `jcs`: validation, RFC 8785 canonical bytes and SHA-256 hashes. Kinds `"slice"` and `"manifest"`, with `slice_id`, `members_hash` and `slice_entry`, cover slice objects and manifests (SPEC 4.4, 15.13).
|
|
36
39
|
- `parse_uri(uri, signature)`, `format_uri(ref)`, `uri_to_https(uri, mesh)`: cell URIs (SPEC 5).
|
|
37
|
-
- `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
|
|
40
|
+
- `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `FOREIGN_DETAILS`, `REASONS`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
|
|
41
|
+
|
|
42
|
+
Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
38
43
|
|
|
39
44
|
Requires Python 3.10 or later and PyNaCl. String rules use Unicode 15.1 tables shipped with the package, so results do not depend on the Python version's own Unicode database; the unit list is copied from the specification's `units.md`. Choosing which publishers to trust is up to you: a valid signature only means the key holder made the statement.
|
|
40
45
|
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datumesh"
|
|
7
|
-
version = "0.4.
|
|
8
|
-
description = "Verify Datumesh receipts (spec-0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
|
+
description = "Verify Datumesh receipts (spec-0.4.3): RFC 8785 canonical JSON, Ed25519 signatures, slice-as-lineage, blobs, units, cell URIs, response envelopes (pending jobs, pins, slice pages, batches), key documents with declarations, mesh documents, licence inheritance and entitlement."
|
|
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.4.
|
|
14
|
+
keywords = ["datumesh", "spec-0.4.3", "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.4.
|
|
33
|
+
"Specification (spec-0.4.3)" = "https://github.com/datumesh-com/datumesh-spec/blob/spec-0.4.3/SPEC.md"
|
|
34
34
|
|
|
35
35
|
[tool.setuptools.packages.find]
|
|
36
36
|
where = ["src"]
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""datumesh: verify Datumesh receipts (signed answers for addressable data cells).
|
|
2
2
|
|
|
3
|
-
Implements the Datumesh protocol specification, revision ``spec-0.4.
|
|
3
|
+
Implements the Datumesh protocol specification, revision ``spec-0.4.3``
|
|
4
4
|
(https://github.com/datumesh-com/datumesh-spec), as a verifier library: RFC 8785
|
|
5
5
|
canonical JSON, SHA-256 hashes, Ed25519 receipt signatures, blob content, units,
|
|
6
6
|
cell URIs (pins included), response envelopes, slice pages and batches, key
|
|
7
7
|
documents, and slice-as-lineage (slice entries,
|
|
8
8
|
slice objects and manifests), and, separately from verification, cross-mesh
|
|
9
|
-
resolution of a host's mesh document (:func:`resolve_mesh`)
|
|
9
|
+
resolution of a host's mesh document (:func:`resolve_mesh`), and since spec-0.4.3 the
|
|
10
|
+
publisher's ``declarations`` in key documents, licence inheritance (:func:`inherit_licence`), the
|
|
11
|
+
licence entitlement check (:func:`check_entitlement`) and unrooted namespaces
|
|
12
|
+
(:func:`is_unrooted`). It evaluates no
|
|
10
13
|
function, formulas included: the evaluated value comes from the caller. Only
|
|
11
14
|
:func:`fetch_keys` touches the network.
|
|
12
15
|
"""
|
|
@@ -18,16 +21,18 @@ from typing import Any, Dict, Optional
|
|
|
18
21
|
|
|
19
22
|
from . import _model, _units
|
|
20
23
|
from ._envelope import TRUST, BatchResult, EnvelopeResult, MemberResult, check_batch, check_envelope
|
|
21
|
-
from ._errors import ERROR_CODES, FAULT_CODES, SUCCESS_CODES, DatumeshError, fail
|
|
24
|
+
from ._errors import ERROR_CODES, FAULT_CODES, FOREIGN_DETAILS, SUCCESS_CODES, DatumeshError, fail
|
|
22
25
|
from ._json import jcs_bytes
|
|
23
|
-
from ._keys import Key, KeysResult, fetch_keys, key_id, keys_from_response
|
|
26
|
+
from ._keys import Declaration, Key, KeysResult, fetch_keys, key_id, keys_from_response
|
|
27
|
+
from ._licence import (ATTRIBUTION, ENTITLED_REASONS, PUBLIC_DOMAIN, REASONS, SHARE_ALIKE, EntitlementResult,
|
|
28
|
+
LicenceResult, check_entitlement, inherit_licence, input_licences, is_unrooted, licence_tier)
|
|
24
29
|
from ._mesh import MESH_ENDPOINTS, MESH_WELL_KNOWN_PATH, MeshResult, resolve_mesh
|
|
25
30
|
from ._uri import format_uri, parse_uri, uri_to_https
|
|
26
31
|
from ._verify import VerifyResult, load, payload_bytes, verify
|
|
27
32
|
|
|
28
|
-
__version__ = "0.4.
|
|
33
|
+
__version__ = "0.4.3"
|
|
29
34
|
#: The specification revision this package conforms to.
|
|
30
|
-
SPEC_VERSION = "spec-0.4.
|
|
35
|
+
SPEC_VERSION = "spec-0.4.3"
|
|
31
36
|
#: Every literal unit listed in units.md (index units, ``index:<period>=<value>``, are a pattern and not listed).
|
|
32
37
|
UNITS = tuple(sorted(_units.LITERALS))
|
|
33
38
|
#: The currency codes of units.md 2.1.
|
|
@@ -39,6 +44,20 @@ KINDS = ("scalar", "cell", "ref", "function", "receipt", "slice", "manifest")
|
|
|
39
44
|
|
|
40
45
|
__all__ = [
|
|
41
46
|
"SPEC_VERSION",
|
|
47
|
+
"ATTRIBUTION",
|
|
48
|
+
"Declaration",
|
|
49
|
+
"ENTITLED_REASONS",
|
|
50
|
+
"EntitlementResult",
|
|
51
|
+
"FOREIGN_DETAILS",
|
|
52
|
+
"LicenceResult",
|
|
53
|
+
"PUBLIC_DOMAIN",
|
|
54
|
+
"REASONS",
|
|
55
|
+
"SHARE_ALIKE",
|
|
56
|
+
"check_entitlement",
|
|
57
|
+
"inherit_licence",
|
|
58
|
+
"input_licences",
|
|
59
|
+
"is_unrooted",
|
|
60
|
+
"licence_tier",
|
|
42
61
|
"BatchResult",
|
|
43
62
|
"MemberResult",
|
|
44
63
|
"TRUST",
|
|
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Mapping, NoReturn, Optional, Sequence, Tuple
|
|
|
11
11
|
from . import _lexical as lx
|
|
12
12
|
from . import _model
|
|
13
13
|
from . import _units
|
|
14
|
-
from ._errors import ERROR_CODES, FAULT_CODES, DatumeshError, fail
|
|
14
|
+
from ._errors import ERROR_CODES, FAULT_CODES, FOREIGN_DETAILS, DatumeshError, fail
|
|
15
15
|
from ._json import jcs_bytes, number_tree, numbers_of, parse_json
|
|
16
16
|
from ._keys import _Source, key_sources
|
|
17
17
|
from ._verify import _signed
|
|
@@ -25,7 +25,7 @@ OUTCOMES: Dict[str, Tuple[range, Tuple[str, ...], Tuple[str, ...]]] = {
|
|
|
25
25
|
"payment_required": (range(402, 403), ("quote",), ("fn_hash",)),
|
|
26
26
|
"rate_limited": (range(429, 430), ("retry_after_seconds",), ("fn_hash",)),
|
|
27
27
|
"bad_request": (range(400, 401), ("error",), ("fn_hash",)),
|
|
28
|
-
"error": (range(500, 600), ("error",), ("fn_hash"
|
|
28
|
+
"error": (range(500, 600), ("error",), ("detail", "fn_hash")), # detail: spec-0.4.3
|
|
29
29
|
"pending": (range(202, 203), ("estimate_seconds", "job", "poll", "request"), ()), # spec-0.4.1
|
|
30
30
|
}
|
|
31
31
|
#: A page of the slice endpoint (SPEC 5.6, spec-0.4.2): an ``ok`` with these members instead.
|
|
@@ -51,6 +51,9 @@ LIMIT_NEXT = 512
|
|
|
51
51
|
#: The trust values (SPEC 5.5); ``computed`` since spec-0.4.2.
|
|
52
52
|
TRUST = ("primary", "verified", "unverified", "computed")
|
|
53
53
|
MAX_SECONDS = 2 ** 53 - 1
|
|
54
|
+
#: spec-0.4.3: a foreign mesh could not be reached; an ``error`` envelope's code, with ``detail``, at HTTP 502.
|
|
55
|
+
FOREIGN = "E_FOREIGN_UNAVAILABLE"
|
|
56
|
+
FOREIGN_STATUS = 502
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
@dataclass(frozen=True)
|
|
@@ -82,6 +85,9 @@ class EnvelopeResult:
|
|
|
82
85
|
echoes (or None); ``error``, the code an ``error``, ``bad_request`` or ``not_covered``
|
|
83
86
|
envelope carries (``E_NAME_UNBOUND`` for the last); and for a page of the slice
|
|
84
87
|
endpoint, ``members`` (a :class:`MemberResult` per member, in order) and ``next``.
|
|
88
|
+
``error_detail`` (spec-0.4.3): with ``E_FOREIGN_UNAVAILABLE``, the envelope's ``detail`` (one of
|
|
89
|
+
``FOREIGN_DETAILS``: why the foreign mesh could not be reached); else None. (``detail`` is this result's
|
|
90
|
+
human-readable text, as before.)
|
|
85
91
|
"""
|
|
86
92
|
|
|
87
93
|
ok: bool
|
|
@@ -99,6 +105,7 @@ class EnvelopeResult:
|
|
|
99
105
|
error: Optional[str] = None
|
|
100
106
|
members: Tuple[MemberResult, ...] = ()
|
|
101
107
|
next: Optional[str] = None
|
|
108
|
+
error_detail: Optional[str] = None
|
|
102
109
|
|
|
103
110
|
|
|
104
111
|
@dataclass(frozen=True)
|
|
@@ -212,10 +219,15 @@ def _members(env: Any, numbers: Dict[str, str], path: str, at_slice: bool, membe
|
|
|
212
219
|
_bad("reason is only present with superseded_by", f"{path}.reason")
|
|
213
220
|
if "fault" in env and "error" in env:
|
|
214
221
|
_bad("not_covered carries fault or error, not both", f"{path}.error")
|
|
222
|
+
if (env.get("error") == FOREIGN) != ("detail" in env): # spec-0.4.3
|
|
223
|
+
_bad("detail is present exactly when error is E_FOREIGN_UNAVAILABLE", f"{path}.detail")
|
|
215
224
|
for name in sorted(env): # JCS order (all names are ASCII)
|
|
216
225
|
v, p = env[name], f"{path}.{name}"
|
|
217
226
|
if name in ("age_seconds", "retry_after_seconds", "estimate_seconds"):
|
|
218
227
|
_seconds(v, numbers.get(name), p)
|
|
228
|
+
elif name == "detail":
|
|
229
|
+
if not isinstance(v, str) or v not in FOREIGN_DETAILS:
|
|
230
|
+
_bad(f"detail is one of {', '.join(FOREIGN_DETAILS)}", p)
|
|
219
231
|
elif name == "error":
|
|
220
232
|
if outcome == "bad_request":
|
|
221
233
|
allowed_codes = BAD_REQUEST_CODES | {"E_JOB_UNKNOWN"}
|
|
@@ -391,6 +403,9 @@ def _check_parsed(status: Any, env_v: Any, tree: Any, sources: Tuple[_Source, ..
|
|
|
391
403
|
_bad("a batch is refused whole only by a refusal outcome", f"{path}.outcome")
|
|
392
404
|
if isinstance(status, bool) or not isinstance(status, int) or status not in OUTCOMES[outcome][0]:
|
|
393
405
|
_bad(f"HTTP status {status!r} does not match outcome {outcome!r}", "status" if path == "$" else path)
|
|
406
|
+
if env.get("error") == FOREIGN and status != FOREIGN_STATUS: # spec-0.4.3
|
|
407
|
+
_bad(f"E_FOREIGN_UNAVAILABLE is answered with HTTP {FOREIGN_STATUS}, not {status}",
|
|
408
|
+
"status" if path == "$" else path)
|
|
394
409
|
# step 3a (SPEC 5.5, a468606): what the request decides without the receipt, in this order
|
|
395
410
|
if env.get("error") == "E_JOB_UNKNOWN" and req.job is None:
|
|
396
411
|
_bad("E_JOB_UNKNOWN answers a job URL only, not a /cell request", f"{path}.error")
|
|
@@ -435,12 +450,13 @@ def _check_parsed(status: Any, env_v: Any, tree: Any, sources: Tuple[_Source, ..
|
|
|
435
450
|
fault, error = env.get("fault"), env.get("error")
|
|
436
451
|
detail = "well-formed envelope" + ("; receipt signature-valid" if h else "") + (
|
|
437
452
|
f"; evaluation fault {fault}" if fault else "") + (f"; job {env['job']}" if outcome == "pending" else "") + (
|
|
438
|
-
f"; a page of {len(members)} members" if page else "")
|
|
453
|
+
f"; a page of {len(members)} members" if page else "") + (
|
|
454
|
+
f"; foreign mesh unavailable ({env['detail']})" if "detail" in env else "")
|
|
439
455
|
if outcome == "pending":
|
|
440
456
|
return EnvelopeResult(True, "OK", outcome, None, None, detail, None, None, env["job"],
|
|
441
457
|
int(numbers_of(tree)["estimate_seconds"]), env["poll"])
|
|
442
458
|
return EnvelopeResult(True, "OK", outcome, trust, h, detail, None, fault, None, None, None, env.get("fn_hash"),
|
|
443
|
-
error, members, env.get("next"))
|
|
459
|
+
error, members, env.get("next"), env.get("detail"))
|
|
444
460
|
|
|
445
461
|
|
|
446
462
|
def _check_page(env: Dict[str, Any], tree: Any, sources: Tuple[_Source, ...], req: _Request,
|
|
@@ -6,9 +6,10 @@ from typing import NoReturn, Optional
|
|
|
6
6
|
|
|
7
7
|
#: Every error code SPEC.md 10.2 defines, in the order of its table. Two, ``E_EVAL_UNAVAILABLE`` and
|
|
8
8
|
#: ``E_INPUT_SUPERSEDED``, are not failures: :func:`verify` reports them with success (SPEC 10.1 step 7.6).
|
|
9
|
-
#: The last
|
|
10
|
-
#: ``E_SLICE_AT_CELL`` and ``E_NAME_UNBOUND`` are a mesh's answers about a request, in an envelope (5.5),
|
|
11
|
-
#: ``E_MESH_DOC`` a result of cross-mesh resolution (9.6)
|
|
9
|
+
#: The last five (spec-0.4.1, spec-0.4.2 and spec-0.4.3) are not verification results: ``E_JOB_UNKNOWN``,
|
|
10
|
+
#: ``E_SLICE_AT_CELL`` and ``E_NAME_UNBOUND`` are a mesh's answers about a request, in an envelope (5.5),
|
|
11
|
+
#: ``E_MESH_DOC`` a result of cross-mesh resolution (9.6), and ``E_FOREIGN_UNAVAILABLE`` a mesh's or resolver's
|
|
12
|
+
#: answer, in an ``error`` envelope with HTTP 502, that a foreign mesh could not be reached (5.5).
|
|
12
13
|
ERROR_CODES: tuple[str, ...] = (
|
|
13
14
|
"E_PARSE_JSON",
|
|
14
15
|
"E_PARSE_DUPLICATE_KEY",
|
|
@@ -58,6 +59,7 @@ ERROR_CODES: tuple[str, ...] = (
|
|
|
58
59
|
"E_MESH_DOC",
|
|
59
60
|
"E_SLICE_AT_CELL",
|
|
60
61
|
"E_NAME_UNBOUND",
|
|
62
|
+
"E_FOREIGN_UNAVAILABLE",
|
|
61
63
|
)
|
|
62
64
|
|
|
63
65
|
|
|
@@ -70,6 +72,11 @@ FAULT_CODES: tuple[str, ...] = ("E_FORMULA_DIV_ZERO", "E_FORMULA_DOMAIN", "E_FOR
|
|
|
70
72
|
"E_FORMULA_ABSENT", "E_FORMULA_FUTURE")
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
#: The ``detail`` an ``error`` envelope carries with ``E_FOREIGN_UNAVAILABLE``, and only with it (SPEC 5.5,
|
|
76
|
+
#: spec-0.4.3): why the foreign mesh could not be reached.
|
|
77
|
+
FOREIGN_DETAILS: tuple[str, ...] = ("offline", "network", "dns", "tls", "timeout", "refused")
|
|
78
|
+
|
|
79
|
+
|
|
73
80
|
class DatumeshError(ValueError):
|
|
74
81
|
"""An input broke a rule of the Datumesh specification.
|
|
75
82
|
|
|
@@ -8,7 +8,8 @@ import ssl
|
|
|
8
8
|
import urllib.error
|
|
9
9
|
import urllib.request
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
|
-
from
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
|
|
12
13
|
|
|
13
14
|
from . import _lexical as lx
|
|
14
15
|
from ._errors import DatumeshError, fail
|
|
@@ -44,6 +45,32 @@ class Key:
|
|
|
44
45
|
return self.valid_to is None or issued < self.valid_to
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Declaration:
|
|
50
|
+
"""One entry of a key document's ``declarations`` (SPEC 9.2, spec-0.4.3): the publisher permits its function
|
|
51
|
+
to be computed over inputs under ``licence`` (a restricted licence, 8.5) from ``valid_from`` until
|
|
52
|
+
``valid_to`` (None: open-ended)."""
|
|
53
|
+
|
|
54
|
+
licence: str
|
|
55
|
+
valid_from: str
|
|
56
|
+
valid_to: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
def open_at(self, issued: str) -> bool:
|
|
59
|
+
"""SPEC 8.5 step 4, as a key's window (9.5): ``valid_from <= issued`` and, if present, ``issued < valid_to``."""
|
|
60
|
+
return self.valid_from <= issued and (self.valid_to is None or issued < self.valid_to)
|
|
61
|
+
|
|
62
|
+
def as_dict(self) -> Dict[str, str]:
|
|
63
|
+
"""The entry as the key document writes it."""
|
|
64
|
+
d = {"licence": self.licence, "valid_from": self.valid_from}
|
|
65
|
+
if self.valid_to is not None:
|
|
66
|
+
d["valid_to"] = self.valid_to
|
|
67
|
+
return d
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#: A key document's declarations: full function name to its entries, in document order.
|
|
71
|
+
Declarations = Mapping[str, Tuple[Declaration, ...]]
|
|
72
|
+
|
|
73
|
+
|
|
47
74
|
@dataclass(frozen=True)
|
|
48
75
|
class KeysResult:
|
|
49
76
|
"""The outcome of resolving a host's keys (SPEC 9.3).
|
|
@@ -51,6 +78,9 @@ class KeysResult:
|
|
|
51
78
|
``ok`` is True and ``code`` is ``"OK"`` when ``keys`` holds the host's
|
|
52
79
|
validated keys; otherwise ``code`` is ``E_KEY_UNAVAILABLE`` or ``E_KEY_DOC``
|
|
53
80
|
and ``keys`` is empty. ``host`` is the host the keys belong to.
|
|
81
|
+
``declarations`` (spec-0.4.3): the document's ``declarations`` (SPEC 9.2), a read-only mapping from full
|
|
82
|
+
function name to a tuple of :class:`Declaration`, or None when the document has none (or is not ok).
|
|
83
|
+
They change nothing in verification.
|
|
54
84
|
"""
|
|
55
85
|
|
|
56
86
|
ok: bool
|
|
@@ -58,21 +88,70 @@ class KeysResult:
|
|
|
58
88
|
host: Optional[str]
|
|
59
89
|
detail: str = ""
|
|
60
90
|
keys: Tuple[Key, ...] = field(default_factory=tuple)
|
|
91
|
+
declarations: Optional[Declarations] = field(default=None, hash=False)
|
|
61
92
|
|
|
62
93
|
@property
|
|
63
94
|
def key_ids(self) -> Tuple[str, ...]:
|
|
64
95
|
return tuple(k.id for k in self.keys)
|
|
65
96
|
|
|
97
|
+
def declarations_json(self) -> Optional[Dict[str, List[Dict[str, str]]]]:
|
|
98
|
+
"""The declarations as the document gives them (a ``keys`` vector's output), or None."""
|
|
99
|
+
if self.declarations is None:
|
|
100
|
+
return None
|
|
101
|
+
return {name: [e.as_dict() for e in entries] for name, entries in self.declarations.items()}
|
|
102
|
+
|
|
66
103
|
|
|
67
104
|
def _doc_error(host: Optional[str], detail: str) -> KeysResult:
|
|
68
105
|
return KeysResult(False, "E_KEY_DOC", host, detail)
|
|
69
106
|
|
|
70
107
|
|
|
108
|
+
def declaration_entries(entries: Any, path: str) -> Tuple[Declaration, ...]:
|
|
109
|
+
"""One declared name's entries (SPEC 9.2): an array of 1 to 16 objects with exactly ``licence`` (8.1),
|
|
110
|
+
``valid_from`` and optionally ``valid_to`` (instants, ``valid_to`` later), all strings. Raises
|
|
111
|
+
DatumeshError (the caller maps it to E_KEY_DOC)."""
|
|
112
|
+
if not isinstance(entries, list) or not 1 <= len(entries) <= lx.LIMIT_DECLARATION_ENTRIES:
|
|
113
|
+
fail("E_SCHEMA", f"a declared name has an array of 1 to {lx.LIMIT_DECLARATION_ENTRIES} entries", path)
|
|
114
|
+
out = []
|
|
115
|
+
for i, e in enumerate(entries):
|
|
116
|
+
p = f"{path}[{i}]"
|
|
117
|
+
require_object(e, p, ("licence", "valid_from"), ("valid_to",))
|
|
118
|
+
for name in ("licence", "valid_from", "valid_to"):
|
|
119
|
+
if name in e and not isinstance(e[name], str):
|
|
120
|
+
fail("E_SCHEMA", f"{name} must be a string", f"{p}.{name}")
|
|
121
|
+
lx.check_licence(e["licence"], f"{p}.licence")
|
|
122
|
+
lx.check_instant(e["valid_from"], f"{p}.valid_from")
|
|
123
|
+
valid_to = e.get("valid_to")
|
|
124
|
+
if valid_to is not None:
|
|
125
|
+
lx.check_instant(valid_to, f"{p}.valid_to")
|
|
126
|
+
if not valid_to > e["valid_from"]:
|
|
127
|
+
fail("E_SCHEMA", "valid_to must be later than valid_from", f"{p}.valid_to")
|
|
128
|
+
out.append(Declaration(e["licence"], e["valid_from"], valid_to))
|
|
129
|
+
return tuple(out)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _declarations(v: Any, doc_host: str) -> Declarations:
|
|
133
|
+
"""SPEC 9.2 (spec-0.4.3): ``declarations`` is an object of 1 to 64 members, each a full function name (2.3)
|
|
134
|
+
whose namespace's host is ``doc_host`` exactly (a subdomain is another host)."""
|
|
135
|
+
if not isinstance(v, dict) or not 1 <= len(v) <= lx.LIMIT_DECLARATIONS:
|
|
136
|
+
fail("E_SCHEMA", f"declarations is an object of 1 to {lx.LIMIT_DECLARATIONS} function names",
|
|
137
|
+
"$.declarations")
|
|
138
|
+
out: Dict[str, Tuple[Declaration, ...]] = {}
|
|
139
|
+
for name, entries in v.items():
|
|
140
|
+
p = f"$.declarations[{name!r}]"
|
|
141
|
+
lx.check_function_name(name, p)
|
|
142
|
+
if lx.host_of(name) != doc_host:
|
|
143
|
+
fail("E_SCHEMA", f"{name!r} is not a function under the document's host {doc_host!r}", p)
|
|
144
|
+
out[name] = declaration_entries(entries, p)
|
|
145
|
+
return MappingProxyType(out)
|
|
146
|
+
|
|
147
|
+
|
|
71
148
|
def validate_key_document(doc: Any, host: Optional[str]) -> KeysResult:
|
|
72
149
|
"""Check a parsed key document against SPEC 9.2. ``host`` is the host it was
|
|
73
|
-
fetched from (None: take the document's own ``host`` member).
|
|
150
|
+
fetched from (None: take the document's own ``host`` member). The optional
|
|
151
|
+
``declarations`` (spec-0.4.3) are checked with the rest: anything malformed
|
|
152
|
+
rejects the whole document (E_KEY_DOC)."""
|
|
74
153
|
try:
|
|
75
|
-
require_object(doc, "$", ("host", "keys", "proto"))
|
|
154
|
+
require_object(doc, "$", ("host", "keys", "proto"), ("declarations",))
|
|
76
155
|
if doc["proto"] != PROTO:
|
|
77
156
|
fail("E_SCHEMA", f"proto must be {PROTO!r}", "$.proto")
|
|
78
157
|
doc_host = doc["host"]
|
|
@@ -110,7 +189,8 @@ def validate_key_document(doc: Any, host: Optional[str]) -> KeysResult:
|
|
|
110
189
|
seen_ids.add(entry["id"])
|
|
111
190
|
seen_public.add(entry["public"])
|
|
112
191
|
out.append(Key(entry["id"], entry["public"], entry["valid_from"], valid_to))
|
|
113
|
-
|
|
192
|
+
decls = _declarations(doc["declarations"], doc_host) if "declarations" in doc else None
|
|
193
|
+
return KeysResult(True, "OK", doc_host, "", tuple(out), decls)
|
|
114
194
|
except DatumeshError as e:
|
|
115
195
|
where = f" at {e.path}" if e.path else ""
|
|
116
196
|
return _doc_error(host if host is not None else (doc.get("host") if isinstance(doc, dict) and isinstance(doc.get("host"), str) else None),
|
|
@@ -31,6 +31,8 @@ LIMIT_LICENCE_URL = 512
|
|
|
31
31
|
LIMIT_KEYS = 64
|
|
32
32
|
LIMIT_EXTENT = 4096 # values in an argument's extent (spec-0.4.0)
|
|
33
33
|
LIMIT_MEMBERS = 1048576 # member receipt hashes in a slice manifest (spec-0.4.0)
|
|
34
|
+
LIMIT_DECLARATIONS = 64 # function names in a key document's declarations (spec-0.4.3)
|
|
35
|
+
LIMIT_DECLARATION_ENTRIES = 16 # entries for one declared name (spec-0.4.3)
|
|
34
36
|
LIMIT_DNS = 253
|
|
35
37
|
LIMIT_BLOB_LENGTH = 2 ** 53 - 1
|
|
36
38
|
LIMIT_MEDIA_TYPE = 128
|