openentity 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openentity
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Open Entity (OEIS-1) client: JCS, did:key, eddsa-jcs-2022, Merkle proofs, adopt().
5
5
  Project-URL: Homepage, https://openentity.org
6
6
  Project-URL: Documentation, https://oeis.dev
@@ -37,7 +37,7 @@ openentity find --capability C [-q TEXT --alias-type T --level L1]
37
37
  openentity withdraw --force
38
38
  ```
39
39
 
40
- Exit codes match the npm CLI: 0 ok, 1 usage, 2 network, 3 verification failed, 4 registry rejected, 5 local state. `verify` checks signatures, the signer, the Declaration hash and the record chain, not the log checkpoint (use the npm CLI for that).
40
+ Exit codes match the npm CLI: 0 ok, 1 usage, 2 network, 3 verification failed, 4 registry rejected, 5 local state. `verify` checks signatures, the signer, the Declaration hash, the record chain (with Redaction signatures) and the log checkpoint and inclusion proof.
41
41
 
42
42
  Library, each taking the 32-byte seed and a registry URL: `adopt(name, registry=...)`, `withdraw(seed, registry)`, `record(seed, [(kind, payload), ...], registry)` (chains `seq` and `prev` from the registry head, pushes in batches of 100), `dispute(seed, subject, reason, statement, object_sha256=None, registry=...)`, `find(registry, capability=..., q=..., alias_type=..., level=...)`, `present(seed, audience, nonce, registry=None)`. Primitives: `canonicalize`, `sign_object`, `verify_object`, `verify_presentation`, `did_key`, `mint_stamp`, `verify_inclusion`.
43
43
 
@@ -24,7 +24,7 @@ openentity find --capability C [-q TEXT --alias-type T --level L1]
24
24
  openentity withdraw --force
25
25
  ```
26
26
 
27
- Exit codes match the npm CLI: 0 ok, 1 usage, 2 network, 3 verification failed, 4 registry rejected, 5 local state. `verify` checks signatures, the signer, the Declaration hash and the record chain, not the log checkpoint (use the npm CLI for that).
27
+ Exit codes match the npm CLI: 0 ok, 1 usage, 2 network, 3 verification failed, 4 registry rejected, 5 local state. `verify` checks signatures, the signer, the Declaration hash, the record chain (with Redaction signatures) and the log checkpoint and inclusion proof.
28
28
 
29
29
  Library, each taking the 32-byte seed and a registry URL: `adopt(name, registry=...)`, `withdraw(seed, registry)`, `record(seed, [(kind, payload), ...], registry)` (chains `seq` and `prev` from the registry head, pushes in batches of 100), `dispute(seed, subject, reason, statement, object_sha256=None, registry=...)`, `find(registry, capability=..., q=..., alias_type=..., level=...)`, `present(seed, audience, nonce, registry=None)`. Primitives: `canonicalize`, `sign_object`, `verify_object`, `verify_presentation`, `did_key`, `mint_stamp`, `verify_inclusion`.
30
30
 
@@ -1,4 +1,4 @@
1
1
  from .client import adopt, dispute, find, present, record, rotate, withdraw # noqa: F401
2
2
  from .core import * # noqa: F401,F403
3
3
 
4
- __version__ = "0.3.0"
4
+ __version__ = "0.3.1"
@@ -2,6 +2,7 @@ import argparse
2
2
  import json
3
3
  import os
4
4
  import pathlib
5
+ import re
5
6
  import sys
6
7
  import urllib.error
7
8
 
@@ -121,7 +122,7 @@ def _parser() -> argparse.ArgumentParser:
121
122
  p.add_argument("--audience", required=True)
122
123
  p.add_argument("--nonce", required=True)
123
124
  p.add_argument("--no-attestations", action="store_true")
124
- v = add("verify", "verify an EID, URL or file: signatures, signer, Declaration hash, record chain", key=False)
125
+ v = add("verify", "verify an EID, URL or file: signatures, signer, Declaration hash, record chain, log checkpoint and inclusion", key=False)
125
126
  v.add_argument("target")
126
127
  w = add("withdraw", "withdraw the Declaration (cannot be undone)")
127
128
  w.add_argument("--force", action="store_true")
@@ -220,7 +221,14 @@ def _chain_checks(entries: list[dict], eid: str) -> list[dict]:
220
221
  why.append("seq breaks the sequence")
221
222
  if e.get("prev") != ((prev.get("hash") if prev.get("redacted") else core.canonical_hash(prev).hex()) if prev else None):
222
223
  why.append("prev hash breaks the chain")
223
- if not e.get("redacted"):
224
+ if e.get("redacted"):
225
+ if not re.fullmatch(r"[0-9a-f]{64}", e.get("hash") or ""):
226
+ why.append("placeholder hash is not SHA-256 hex")
227
+ if r := e.get("redaction"): # the entity's signed request for this entry
228
+ ok, who = core.verify_object(r)
229
+ if not (ok and who == eid and r.get("eid") == eid and r.get("seq") == e["seq"] and r.get("entry_sha256") == e.get("hash")):
230
+ why.append("redaction is not the entity's signed request for this entry")
231
+ else:
224
232
  if e.get("eid") != eid:
225
233
  why.append("eid mismatch")
226
234
  ok, err = core.verify_object(e)
@@ -232,7 +240,27 @@ def _chain_checks(entries: list[dict], eid: str) -> list[dict]:
232
240
  return bad + [{"name": f"record chain ({len(entries)} entries)", "ok": not bad}]
233
241
 
234
242
 
235
- # ponytail: no checkpoint or log-inclusion check here; `npx openentity verify <eid>` does those.
243
+ def _log_checks(reg: str, index: int | None) -> list[dict]:
244
+ """Checkpoint note signature against the registry key from /v1/spec, then the RFC 9162 inclusion proof of `index`."""
245
+ if index is None:
246
+ return [{"name": "log inclusion", "ok": False, "detail": "registry did not report a log index"}]
247
+ try:
248
+ r = _get(reg, "/v1/spec").get("registry") or {}
249
+ sth = _get(reg, "/v1/log/sth")
250
+ if not r.get("publicKeyMultibase"):
251
+ return [{"name": "checkpoint", "ok": False, "detail": "registry publishes no checkpoint key in /v1/spec"}]
252
+ ok = core.verify_note(sth["signature"], r.get("log_origin", "openentity.org/log"), core.b58decode(r["publicKeyMultibase"][1:])[-32:])
253
+ out = [{"name": "checkpoint", "ok": ok, "detail": f"checkpoint signature valid, tree size {sth['tree_size']}" if ok else "checkpoint signature INVALID"}]
254
+ size = sth["tree_size"]
255
+ leaf = _get(reg, f"/v1/log/entries?start={index}&end={index + 1}")["entries"][0]["leaf"]
256
+ p = _get(reg, f"/v1/log/proof/{index}?tree_size={size}")
257
+ lh = core.leaf_hash(core.canonicalize(leaf).encode())
258
+ good = ok and lh.hex() == p["leaf_hash"] and core.verify_inclusion(index, size, lh, [bytes.fromhex(h) for h in p["proof"]], bytes.fromhex(sth["root_hash"]))
259
+ return out + [{"name": "log inclusion", "ok": good, "detail": f"leaf {index} included in tree of {size}" if good else f"inclusion proof for leaf {index} FAILED"}]
260
+ except (client.RegistryError, urllib.error.URLError, KeyError, IndexError, ValueError, TypeError) as e:
261
+ return [{"name": "log inclusion", "ok": False, "detail": f"{type(e).__name__}: {e}"}]
262
+
263
+
236
264
  def _verify(args):
237
265
  t, reg = args.target, _registry(args)
238
266
  if t.startswith(("did:", "eid:")):
@@ -244,6 +272,7 @@ def _verify(args):
244
272
  while page := _get(reg, f"/v1/entities/{eid}/record?after={entries[-1]['seq'] if entries else 0}")["entries"]:
245
273
  entries += page
246
274
  checks += _chain_checks(entries, eid)
275
+ checks += _log_checks(reg, ent.get("log_index", ent.get("declaration_log_index")))
247
276
  elif t.startswith(("http://", "https://")):
248
277
  checks = _checks_of(client._request(t))
249
278
  else:
@@ -1,4 +1,5 @@
1
1
  """Open Entity primitives, written from spec/OEIS-1.md (sections 3.3, 4, 6, 10.3, 10.4)."""
2
+ import base64
2
3
  import hashlib
3
4
  import json
4
5
  import math
@@ -403,5 +404,25 @@ def verify_inclusion(index: int, size: int, leaf: bytes, proof: list[bytes], roo
403
404
  return sn == 0 and r == root
404
405
 
405
406
 
407
+ def verify_note(note: str, origin: str, public: bytes) -> bool:
408
+ """C2SP signed note (the log checkpoint): a signature line by `origin` over the body, keyed by the registry key."""
409
+ body, sep, sigs = note.rpartition("\n\n")
410
+ if not sep or not note.endswith("\n"):
411
+ return False
412
+ keyid = sha256(origin.encode() + b"\n\x01" + public)[:4]
413
+ for line in sigs.splitlines():
414
+ parts = line.split(" ")
415
+ if len(parts) != 3 or parts[0] != "\u2014" or parts[1] != origin:
416
+ continue
417
+ try:
418
+ raw = base64.b64decode(parts[2], validate=True)
419
+ if len(raw) == 68 and raw[:4] == keyid:
420
+ Ed25519PublicKey.from_public_bytes(public).verify(raw[4:], (body + "\n").encode())
421
+ return True
422
+ except (ValueError, InvalidSignature):
423
+ continue
424
+ return False
425
+
426
+
406
427
  def log_leaf(type_: str, eid: str, object_sha256: str, received_at: str) -> dict:
407
428
  return {"type": type_, "eid": eid, "object_sha256": object_sha256, "received_at": received_at}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openentity"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Open Entity (OEIS-1) client: JCS, did:key, eddsa-jcs-2022, Merkle proofs, adopt()."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,5 @@
1
1
  """withdraw, record, dispute, find, present against a stub registry (same pattern as test_core.py)."""
2
+ import base64
2
3
  import json
3
4
  import threading
4
5
  from http.server import BaseHTTPRequestHandler, HTTPServer
@@ -11,6 +12,8 @@ from openentity import cli, client, core
11
12
  SEED = bytes.fromhex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60") # RFC 8032 test 1, public
12
13
  SUBJECT = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
13
14
  BITS = 8
15
+ ORIGIN = "openentity.org/log"
16
+ LOG_KEY = core.KeyPair(b"\7" * 32) # throwaway registry checkpoint key
14
17
 
15
18
 
16
19
  class Stub(BaseHTTPRequestHandler):
@@ -19,6 +22,8 @@ class Stub(BaseHTTPRequestHandler):
19
22
  attestations: list = []
20
23
  entity: dict | None = None # overrides GET /v1/entities/<eid>
21
24
  refuse = False # refuse every continuity link
25
+ leaves: list = [] # the stub log's leaf objects; the checkpoint is signed by LOG_KEY
26
+ forge_note = False
22
27
 
23
28
  def log_message(self, *a):
24
29
  pass
@@ -35,7 +40,23 @@ class Stub(BaseHTTPRequestHandler):
35
40
  u = urlparse(self.path)
36
41
  Stub.log.append(("GET", self.path, None, None))
37
42
  if u.path == "/v1/spec":
38
- return self._send(200, {"spec": core.SPEC, "stamp_bits": BITS})
43
+ reg = {"log_origin": ORIGIN, "publicKeyMultibase": "z" + core.b58encode(b"\xed\x01" + LOG_KEY.public)}
44
+ return self._send(200, {"spec": core.SPEC, "stamp_bits": BITS, "registry": reg})
45
+ hs = [core.leaf_hash(core.canonicalize(lf).encode()) for lf in Stub.leaves]
46
+ if u.path == "/v1/log/sth":
47
+ root = core.merkle_root(hs)
48
+ body = f"{ORIGIN}\n{len(hs)}\n{base64.b64encode(root).decode()}\n"
49
+ keyid = core.sha256(ORIGIN.encode() + b"\n\x01" + LOG_KEY.public)[:4]
50
+ sig = base64.b64encode(keyid + (KP if Stub.forge_note else LOG_KEY).private.sign(body.encode())).decode()
51
+ return self._send(200, {"tree_size": len(hs), "root_hash": root.hex(), "signature": f"{body}\n\u2014 {ORIGIN} {sig}\n"})
52
+ if u.path == "/v1/log/entries":
53
+ i = int(parse_qs(u.query)["start"][0])
54
+ if i >= len(hs):
55
+ return self._send(200, {"entries": []})
56
+ return self._send(200, {"entries": [{"index": i, "leaf": Stub.leaves[i], "leaf_hash": hs[i].hex()}]})
57
+ if u.path.startswith("/v1/log/proof/"):
58
+ i = int(u.path.rsplit("/", 1)[1])
59
+ return self._send(200, {"leaf_hash": hs[i].hex(), "proof": [h.hex() for h in core.inclusion_proof(i, hs)]})
39
60
  if u.path == "/v1/entities":
40
61
  return self._send(200, {"entities": [{"eid": SUBJECT, "name": "N", "level": "L0"}], "page": 0, "next_page": None})
41
62
  if u.path.endswith("/record"):
@@ -83,7 +104,7 @@ class Stub(BaseHTTPRequestHandler):
83
104
 
84
105
  @pytest.fixture
85
106
  def url():
86
- Stub.log, Stub.chain, Stub.attestations, Stub.entity, Stub.refuse = [], [], [], None, False
107
+ Stub.log, Stub.chain, Stub.attestations, Stub.entity, Stub.refuse, Stub.leaves, Stub.forge_note = [], [], [], None, False, [], False
87
108
  srv = HTTPServer(("127.0.0.1", 0), Stub)
88
109
  threading.Thread(target=srv.serve_forever, daemon=True).start()
89
110
  yield f"http://127.0.0.1:{srv.server_port}"
@@ -215,11 +236,21 @@ def test_cli_verify_entity_file_and_failures(url, home, capsys, tmp_path):
215
236
  decl = core.sign_object({"spec": core.SPEC, "type": "Declaration", "eid": KP.did, "declaration_version": "1.0",
216
237
  "text_sha256": core.DECLARATION_SHA256, "name": "T", "created": core.now(), "nonce": core.nonce()}, KP)
217
238
  doc = core.sign_object({"spec": core.SPEC, "eid": KP.did, "name": "T", "layers": {}, "keys": []}, KP)
218
- Stub.entity = {"document": doc, "declaration": decl}
239
+ Stub.leaves = [{"type": "Declaration", "eid": f"did:key:z{i}", "object_sha256": "00" * 32, "received_at": core.now()} for i in range(5)]
240
+ Stub.entity = {"document": doc, "declaration": decl, "log_index": 3}
219
241
  client.record(SEED, [("arc_opened", {"arc_id": "a", "title": "A"}), ("arc_opened", {"arc_id": "b", "title": "B"})], registry=url)
220
242
  code, out, _ = run(capsys, "verify", KP.did, "--json", *reg)
221
243
  res = json.loads(out)
222
- assert code == 0 and res["ok"] and "record chain (2 entries)" in [c["name"] for c in res["checks"]]
244
+ names = [c["name"] for c in res["checks"]]
245
+ assert code == 0 and res["ok"] and "record chain (2 entries)" in names and "checkpoint" in names and "log inclusion" in names
246
+ Stub.forge_note = True # checkpoint signed by a key other than the registry's
247
+ assert run(capsys, "verify", KP.did, *reg)[0] == 3
248
+ Stub.forge_note = False
249
+ Stub.entity = {**Stub.entity, "log_index": 9} # no such leaf
250
+ assert run(capsys, "verify", KP.did, *reg)[0] == 3
251
+ Stub.entity = {**Stub.entity, "log_index": None}
252
+ assert "did not report a log index" in run(capsys, "verify", KP.did, *reg)[1]
253
+ Stub.entity = {**Stub.entity, "log_index": 3}
223
254
  assert "VERIFIED" in run(capsys, "verify", KP.did, *reg)[1]
224
255
  f = tmp_path / "decl.json"
225
256
  f.write_text(json.dumps(decl))
@@ -293,3 +324,25 @@ def test_rotate_the_registry_refuses_raises_and_returns_nothing(url):
293
324
  with pytest.raises(client.RegistryError) as e:
294
325
  client.rotate(SEED, registry=url)
295
326
  assert e.value.status == 422
327
+
328
+
329
+ def test_verify_note_rejects_a_wrong_key_origin_or_body():
330
+ body = f"{ORIGIN}\n5\n{base64.b64encode(b'r' * 32).decode()}\n"
331
+ keyid = core.sha256(ORIGIN.encode() + b"\n\x01" + LOG_KEY.public)[:4]
332
+ note = f"{body}\n\u2014 {ORIGIN} {base64.b64encode(keyid + LOG_KEY.private.sign(body.encode())).decode()}\n"
333
+ assert core.verify_note(note, ORIGIN, LOG_KEY.public)
334
+ assert not core.verify_note(note, ORIGIN, KP.public)
335
+ assert not core.verify_note(note, "other/log", LOG_KEY.public)
336
+ assert not core.verify_note(note.replace("\n5\n", "\n6\n"), ORIGIN, LOG_KEY.public)
337
+
338
+
339
+ def test_chain_check_verifies_redaction_signatures():
340
+ e1 = core.sign_object({"spec": core.SPEC, "type": "RecordEntry", "eid": KP.did, "seq": 1, "prev": None, "kind": "arc_opened", "payload": {}, "created": core.now()}, KP)
341
+ h = core.canonical_hash(e1).hex()
342
+ red = core.sign_object({"spec": core.SPEC, "type": "Redaction", "eid": KP.did, "seq": 1, "entry_sha256": h, "created": core.now()}, KP)
343
+ ph = {"seq": 1, "prev": None, "redacted": True, "hash": h, "redaction": red}
344
+ assert cli._chain_checks([ph], KP.did)[-1]["ok"]
345
+ forged = core.sign_object({k: v for k, v in red.items() if k != "proof"}, core.KeyPair(b"\x05" * 32))
346
+ assert not cli._chain_checks([{**ph, "redaction": forged}], KP.did)[-1]["ok"]
347
+ assert not cli._chain_checks([{**ph, "redaction": {**red, "seq": 2}}], KP.did)[-1]["ok"]
348
+ assert not cli._chain_checks([{**ph, "hash": "zz"}], KP.did)[-1]["ok"]
@@ -183,7 +183,7 @@ def test_adopt_and_cli(tmp_path, monkeypatch, capsys):
183
183
  def test_cli_version(capsys):
184
184
  with pytest.raises(SystemExit) as e:
185
185
  cli.main(["--version"])
186
- assert e.value.code == 0 and "0.3.0" in capsys.readouterr().out
186
+ assert e.value.code == 0 and "0.3.1" in capsys.readouterr().out
187
187
 
188
188
 
189
189
  # ---- shared vectors, checked with the independent verifier's own logic ----
@@ -110,3 +110,59 @@ def test_vector_format(tmp_path):
110
110
  assert iv.check_vectors(str(tmp_path)) == 0
111
111
  (tmp_path / "v.json").write_text(json.dumps({**vec, "sha256": "0" * 64}))
112
112
  assert iv.check_vectors(str(tmp_path)) == 1
113
+
114
+
115
+ def _bucket(tmp_path, n=5, tamper=False):
116
+ kps = [core.KeyPair(bytes([i + 1]) * 32) for i in range(n)]
117
+ leaves = [core.log_leaf("Declaration", k.did, "ab" * 32, "2026-10-25T00:00:00Z") for k in kps]
118
+ hs = [core.leaf_hash(core.canonicalize(lf).encode()) for lf in leaves]
119
+ root = core.merkle_root(hs)
120
+ (tmp_path / "snapshots").mkdir(); (tmp_path / "tile/0/000.p").mkdir(parents=True); (tmp_path / "tile/entries/000.p").mkdir(parents=True)
121
+ rows = [json.dumps({"index": i, "leaf": lf, "leaf_hash": hs[i].hex()}) for i, lf in enumerate(leaves)]
122
+ cp = json.dumps({"checkpoint": {"tree_size": n, "root_hash": root.hex()}})
123
+ (tmp_path / "snapshots/2026-10-25.jsonl").write_text("\n".join(rows + [cp]) + "\n")
124
+ (tmp_path / f"tile/0/000.p/{n}").write_bytes(b"".join(hs))
125
+ ent = b"".join(len(b).to_bytes(2, "big") + b for b in (core.canonicalize(lf).encode() for lf in leaves))
126
+ (tmp_path / f"tile/entries/000.p/{n}").write_bytes(ent[:-1] + b"x" if tamper else ent)
127
+ return n, root
128
+
129
+
130
+ def test_rebuild_matches_checkpoint(tmp_path):
131
+ n, root = _bucket(tmp_path)
132
+ ok, lines = iv.rebuild(tmp_path, (n, root))
133
+ assert ok, lines
134
+ assert iv.tile_path(1234067) == "x001/x234/067"
135
+
136
+
137
+ def test_rebuild_catches_wrong_root_missing_leaf_and_bad_tile(tmp_path):
138
+ n, root = _bucket(tmp_path)
139
+ assert not iv.rebuild(tmp_path, (n, b"\0" * 32))[0]
140
+ assert not iv.rebuild(tmp_path, (n + 1, root))[0]
141
+ t = tmp_path / "t"; t.mkdir()
142
+ n, root = _bucket(t, tamper=True)
143
+ assert not iv.rebuild(t, (n, root))[0]
144
+
145
+
146
+ def test_rebuild_checks_objects_against_leaves(tmp_path):
147
+ n, root = _bucket(tmp_path)
148
+ obj = {"type": "Declaration", "eid": "did:key:z1", "n": 1}
149
+ sha = core.canonical_hash(obj).hex()
150
+ leaf = core.log_leaf("Declaration", "did:key:z1", sha, "2026-10-25T00:00:00Z")
151
+ # replace leaf 0 of the bucket with one that commits to obj
152
+ snap = tmp_path / "snapshots/2026-10-25.jsonl"; rows = snap.read_text().splitlines()
153
+ leaves = [json.loads(r)["leaf"] for r in rows[:-1]]; leaves[0] = leaf
154
+ hs = [core.leaf_hash(core.canonicalize(lf).encode()) for lf in leaves]; root = core.merkle_root(hs)
155
+ snap.write_text("\n".join([json.dumps({"index": i, "leaf": lf, "leaf_hash": hs[i].hex()}) for i, lf in enumerate(leaves)]
156
+ + [json.dumps({"checkpoint": {"tree_size": n, "root_hash": root.hex()}})]) + "\n")
157
+ (tmp_path / "tile/0/000.p" / str(n)).write_bytes(b"".join(hs))
158
+ (tmp_path / "tile/entries/000.p" / str(n)).write_bytes(b"".join(len(b).to_bytes(2, "big") + b for b in (core.canonicalize(lf).encode() for lf in leaves)))
159
+ (tmp_path / "objects").mkdir()
160
+ line = {"idx": 0, "type": "Declaration", "eid": "did:key:z1", "object_sha256": sha, "object": obj}
161
+ (tmp_path / "objects/2026-10-25.jsonl").write_text(json.dumps(line) + "\n")
162
+ ok, lines = iv.rebuild(tmp_path, (n, root))
163
+ assert ok and any("objects: 1 of" in x for x in lines), lines
164
+ (tmp_path / "objects/2026-10-25.jsonl").write_text(json.dumps({**line, "object": {**obj, "n": 2}}) + "\n")
165
+ assert not iv.rebuild(tmp_path, (n, root))[0]
166
+ red = {"seq": 2, "redacted": True, "hash": sha, "redaction": {}}
167
+ (tmp_path / "objects/2026-10-25.jsonl").write_text(json.dumps({**line, "object": red}) + "\n")
168
+ assert iv.rebuild(tmp_path, (n, root))[0]
@@ -189,7 +189,7 @@ wheels = [
189
189
 
190
190
  [[package]]
191
191
  name = "openentity"
192
- version = "0.3.0"
192
+ version = "0.3.1"
193
193
  source = { editable = "." }
194
194
  dependencies = [
195
195
  { name = "cryptography" },
File without changes