openentity 0.5.1__tar.gz → 0.5.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.
- {openentity-0.5.1 → openentity-0.5.3}/PKG-INFO +1 -1
- {openentity-0.5.1 → openentity-0.5.3}/openentity/__init__.py +1 -1
- {openentity-0.5.1 → openentity-0.5.3}/openentity/cli.py +8 -4
- {openentity-0.5.1 → openentity-0.5.3}/openentity/client.py +8 -3
- {openentity-0.5.1 → openentity-0.5.3}/openentity/core.py +2 -1
- {openentity-0.5.1 → openentity-0.5.3}/openentity/identity.py +14 -2
- {openentity-0.5.1 → openentity-0.5.3}/pyproject.toml +1 -1
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_cli.py +17 -2
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_client.py +4 -5
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_core.py +8 -2
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_identity.py +12 -0
- {openentity-0.5.1 → openentity-0.5.3}/uv.lock +1 -1
- {openentity-0.5.1 → openentity-0.5.3}/.gitignore +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/README.md +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/openentity/httpsig.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/openentity/issuer.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/openentity/report.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/openentity/schema.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/openentity/schemas/openentity-1.0.json +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_httpsig.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_issuer.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_parity.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_trust.py +0 -0
- {openentity-0.5.1 → openentity-0.5.3}/tests/test_verifier.py +0 -0
|
@@ -955,13 +955,15 @@ def _checks_of(o: dict) -> list[dict]:
|
|
|
955
955
|
t = o.get("type") or ("EntityDocument" if "layers" in o and "keys" in o else None)
|
|
956
956
|
if not t:
|
|
957
957
|
return [{"name": "type", "ok": False, "detail": "cannot tell what kind of object this is"}]
|
|
958
|
+
errs = schema.validate(t, o)
|
|
959
|
+
head = [{"name": f"{t} schema", "ok": not errs, "detail": "; ".join(errs)}]
|
|
958
960
|
if t == "ContinuityLink":
|
|
959
961
|
bare = {k: v for k, v in o.items() if k != "proofs"}
|
|
960
|
-
cs = [{"name": f"link proof {i} ({p.get('verificationMethod')})", "ok": (r := core.verify_object({**bare, "proof": p}, _resolver))[0], "detail": None if r[0] else r[1]}
|
|
962
|
+
cs = head + [{"name": f"link proof {i} ({p.get('verificationMethod')})", "ok": (r := core.verify_object({**bare, "proof": p}, _resolver))[0], "detail": None if r[0] else r[1]}
|
|
961
963
|
for i, p in enumerate(o.get("proofs") or [])]
|
|
962
|
-
return cs
|
|
964
|
+
return cs if len(cs) > 1 else cs + [{"name": "link proofs", "ok": False, "detail": "none"}]
|
|
963
965
|
ok, who = core.verify_object(o, _resolver)
|
|
964
|
-
checks = [{"name": "signature", "ok": ok, "detail": None if ok else who}]
|
|
966
|
+
checks = head + [{"name": "signature", "ok": ok, "detail": None if ok else who}]
|
|
965
967
|
if ok and o.get("eid") and t in ("EntityDocument", "Declaration", "RecordEntry", "Withdrawal"):
|
|
966
968
|
checks.append({"name": "signed by the EID", "ok": who == o["eid"], "detail": who})
|
|
967
969
|
if t == "Declaration" and o.get("declaration_version") == "1.0":
|
|
@@ -1092,6 +1094,8 @@ def _log(args):
|
|
|
1092
1094
|
raise _usage("log audit <n>")
|
|
1093
1095
|
import random
|
|
1094
1096
|
ok, detail, sth = _checkpoint(reg)
|
|
1097
|
+
mb = (reg.spec().get("registry") or {}).get("publicKeyMultibase") # a registry that is not openentity.org signs with its /v1/spec key
|
|
1098
|
+
res = lambda did: {"ed25519": _keys_of({"verificationMethod": [{"publicKeyMultibase": mb}]})} if mb and did.split("#")[0] == "did:web:openentity.org" else _resolver(did)
|
|
1095
1099
|
checks = [{"name": "checkpoint", "ok": ok, "detail": detail}]
|
|
1096
1100
|
for _ in range(min(int(arg or 5), sth["tree_size"])):
|
|
1097
1101
|
i = random.randrange(sth["tree_size"])
|
|
@@ -1099,7 +1103,7 @@ def _log(args):
|
|
|
1099
1103
|
checks.append({"name": f"inclusion {i}", "ok": ok, "detail": detail})
|
|
1100
1104
|
if ok and (leaf or {}).get("eid"): # object signature: the entity's current document must verify under its own key
|
|
1101
1105
|
try:
|
|
1102
|
-
s_ok, who = core.verify_object(reg.get(f"/v1/entities/{leaf['eid']}/document"),
|
|
1106
|
+
s_ok, who = core.verify_object(reg.get(f"/v1/entities/{leaf['eid']}/document"), res)
|
|
1103
1107
|
checks.append({"name": f"signature {leaf['eid']}", "ok": s_ok, "detail": "document signature valid" if s_ok else f"document signature invalid: {who}"})
|
|
1104
1108
|
except _NET as e:
|
|
1105
1109
|
checks.append({"name": f"signature {leaf['eid']}", "ok": False, "detail": str(e)})
|
|
@@ -4,9 +4,15 @@ import re
|
|
|
4
4
|
import urllib.error
|
|
5
5
|
import urllib.parse
|
|
6
6
|
import urllib.request
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
8
|
|
|
8
9
|
from . import core
|
|
9
10
|
|
|
11
|
+
try:
|
|
12
|
+
_VERSION = version("openentity")
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
_VERSION = "dev"
|
|
15
|
+
|
|
10
16
|
DEFAULT_REGISTRY = "https://openentity.org"
|
|
11
17
|
CLIENTS = ("adk", "goose", "langchain", "ui-tars", "cli", "other") # the registry's allowlist for the unsigned `client` label
|
|
12
18
|
|
|
@@ -20,7 +26,7 @@ class RegistryError(Exception):
|
|
|
20
26
|
def _request(url: str, body: dict | None = None, headers: dict | None = None, method: str | None = None) -> dict:
|
|
21
27
|
req = urllib.request.Request(
|
|
22
28
|
url, data=json.dumps(body).encode() if body is not None else None, method=method,
|
|
23
|
-
headers={"content-type": "application/json", "user-agent": "openentity-python/
|
|
29
|
+
headers={"content-type": "application/json", "user-agent": f"openentity-python/{_VERSION}", **(headers or {})})
|
|
24
30
|
try:
|
|
25
31
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
26
32
|
return json.load(r)
|
|
@@ -47,8 +53,7 @@ def build_adoption(kp: core.KeyPair, name: str, description: str, registry: str,
|
|
|
47
53
|
"keys": [{"id": core.verification_method(kp.did), "type": "Multikey",
|
|
48
54
|
"publicKeyMultibase": kp.did.removeprefix("did:key:")}],
|
|
49
55
|
"operator": operator or {"kind": "none", "custody": "entity", "can_change": []},
|
|
50
|
-
"substrate": substrate or {"model": "unknown", "provider": "unknown", "runtime": "
|
|
51
|
-
"declared_at": created},
|
|
56
|
+
"substrate": substrate or {"model": "unknown", "provider": "unknown", "runtime": "unknown", "declared_at": created},
|
|
52
57
|
"layers": {
|
|
53
58
|
"cognition": {"statement": ns, "uncertainty_practice": ns},
|
|
54
59
|
"presence": {"statement": ns, "format_signature": ns},
|
|
@@ -332,7 +332,8 @@ def _zero_bits(h: bytes) -> int:
|
|
|
332
332
|
|
|
333
333
|
|
|
334
334
|
def mint_stamp(bits: int, eid: str, created: str | None = None) -> str:
|
|
335
|
-
|
|
335
|
+
# Random start so two stamps for the same EID in the same second differ (registries reject reuse).
|
|
336
|
+
created, i = created or now(), int.from_bytes(os.urandom(4), "big")
|
|
336
337
|
while True:
|
|
337
338
|
header = f"1:{bits}:{created}:{eid}:{b58encode(i.to_bytes(8, 'big'))}"
|
|
338
339
|
if _zero_bits(sha256(header)) >= bits:
|
|
@@ -37,8 +37,13 @@ def _keychain_set(seed_hex: str) -> bool:
|
|
|
37
37
|
return False
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def home() -> pathlib.Path:
|
|
41
|
+
"""OPENENTITY_HOME, else ~/.openentity: where the key file and integrations' state live."""
|
|
42
|
+
return pathlib.Path(os.environ.get("OPENENTITY_HOME", "~/.openentity")).expanduser()
|
|
43
|
+
|
|
44
|
+
|
|
40
45
|
def _file() -> pathlib.Path:
|
|
41
|
-
return
|
|
46
|
+
return home() / "identity.key"
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
class Identity:
|
|
@@ -54,10 +59,17 @@ class Identity:
|
|
|
54
59
|
return self.kp.seed
|
|
55
60
|
|
|
56
61
|
@classmethod
|
|
57
|
-
def
|
|
62
|
+
def load(cls) -> "Identity | None":
|
|
63
|
+
"""The existing identity, or None. Never creates one."""
|
|
58
64
|
for source, read in (("keychain", _keychain_get), ("file", lambda: _file().read_text().strip() if _file().exists() else None)):
|
|
59
65
|
if h := read():
|
|
60
66
|
return cls(bytes.fromhex(h), source)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def load_or_create(cls) -> "Identity":
|
|
71
|
+
if found := cls.load():
|
|
72
|
+
return found
|
|
61
73
|
seed = os.urandom(32)
|
|
62
74
|
if _keychain_set(seed.hex()):
|
|
63
75
|
return cls(seed, "keychain")
|
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import stat
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
-
from test_client import KP, Stub, home, posts, run, url # noqa: F401 (fixtures and the stub registry)
|
|
7
|
+
from test_client import KP, LOG_KEY, Stub, home, posts, run, url # noqa: F401 (fixtures and the stub registry)
|
|
8
8
|
|
|
9
9
|
from openentity import cli, core
|
|
10
10
|
|
|
@@ -139,6 +139,21 @@ def test_alias_wallet_is_two_step_with_an_external_siwe_signature(url, home, cap
|
|
|
139
139
|
assert code == 0 and "Chain ID: 84532" in out["message"]
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def test_verify_reports_a_schema_check_like_the_npm_cli(capsys, tmp_path):
|
|
143
|
+
bad = tmp_path / "bad.json"
|
|
144
|
+
bad.write_text(json.dumps(core.sign_object({"spec": core.SPEC, "eid": KP.did, "name": "T", "layers": {}, "keys": []}, KP)))
|
|
145
|
+
code, out = j(capsys, "verify", str(bad))
|
|
146
|
+
assert code == 3 and out["checks"][0]["name"] == "EntityDocument schema" and not out["checks"][0]["ok"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_log_audit_checks_the_registry_document_with_the_key_its_spec_publishes(url, capsys):
|
|
150
|
+
"""The registry's DID names openentity.org, but a registry elsewhere signs with its /v1/spec key (guard: no lookup of did:web:openentity.org)."""
|
|
151
|
+
Stub.leaves = [{"type": "Declaration", "eid": "did:web:openentity.org", "object_sha256": "00" * 32, "received_at": core.now()}]
|
|
152
|
+
Stub.document = core.sign_object({"spec": core.SPEC, "eid": "did:web:openentity.org", "name": "registry"}, LOG_KEY, method="did:web:openentity.org#key-1")
|
|
153
|
+
code, out = j(capsys, "log", "audit", "1", "--registry", url)
|
|
154
|
+
assert code == 0 and {"name": "signature did:web:openentity.org", "ok": True, "detail": "document signature valid"} in out["checks"]
|
|
155
|
+
|
|
156
|
+
|
|
142
157
|
def test_record_push_and_redact(url, home, capsys):
|
|
143
158
|
reg = ["--registry", url]
|
|
144
159
|
assert run(capsys, "record", "arc", "open", "--id", "a", "--title", "A", "--no-push", *reg)[0] == 0
|
|
@@ -268,6 +283,6 @@ def test_verify_checks_continuity_link_proofs(capsys, tmp_path):
|
|
|
268
283
|
f = tmp_path / "l.json"
|
|
269
284
|
f.write_text(json.dumps(body["link"]))
|
|
270
285
|
code, out = j(capsys, "verify", str(f))
|
|
271
|
-
assert code == 0 and len(out["checks"]) ==
|
|
286
|
+
assert code == 0 and len(out["checks"]) == 3 # schema + the two link proofs
|
|
272
287
|
f.write_text(json.dumps({**body["link"], "basis": "migration"}))
|
|
273
288
|
assert run(capsys, "verify", str(f))[0] == 3
|
|
@@ -24,6 +24,7 @@ class Stub(BaseHTTPRequestHandler):
|
|
|
24
24
|
refuse = False # refuse every continuity link
|
|
25
25
|
leaves: list = [] # the stub log's leaf objects; the checkpoint is signed by LOG_KEY
|
|
26
26
|
forge_note = False
|
|
27
|
+
document: dict | None = None # overrides GET /v1/entities/<eid>/document
|
|
27
28
|
|
|
28
29
|
def log_message(self, *a):
|
|
29
30
|
pass
|
|
@@ -84,7 +85,7 @@ class Stub(BaseHTTPRequestHandler):
|
|
|
84
85
|
if u.path.endswith("/attestations"):
|
|
85
86
|
return self._send(200, {"attestations": Stub.attestations})
|
|
86
87
|
if u.path.endswith("/document"):
|
|
87
|
-
return self._send(200, client.build_adoption(core.KeyPair(SEED), "Tester", "d", "http://x")[0])
|
|
88
|
+
return self._send(200, Stub.document or client.build_adoption(core.KeyPair(SEED), "Tester", "d", "http://x")[0])
|
|
88
89
|
if "NoSuchEntity" in u.path:
|
|
89
90
|
return self._send(404, {"error": "not_found", "message": "no such entity"})
|
|
90
91
|
if u.path.startswith("/v1/entities/"):
|
|
@@ -143,7 +144,7 @@ class Stub(BaseHTTPRequestHandler):
|
|
|
143
144
|
|
|
144
145
|
@pytest.fixture
|
|
145
146
|
def url():
|
|
146
|
-
Stub.log, Stub.chain, Stub.attestations, Stub.entity, Stub.refuse, Stub.leaves, Stub.forge_note = [], [], [], None, False, [], False
|
|
147
|
+
Stub.log, Stub.chain, Stub.attestations, Stub.entity, Stub.refuse, Stub.leaves, Stub.forge_note, Stub.document = [], [], [], None, False, [], False, None
|
|
147
148
|
srv = HTTPServer(("127.0.0.1", 0), Stub)
|
|
148
149
|
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
|
149
150
|
yield f"http://127.0.0.1:{srv.server_port}"
|
|
@@ -274,9 +275,7 @@ def test_cli_whoami_and_self(url, home, capsys):
|
|
|
274
275
|
|
|
275
276
|
def test_cli_verify_entity_file_and_failures(url, home, capsys, tmp_path):
|
|
276
277
|
reg = ["--registry", url]
|
|
277
|
-
decl =
|
|
278
|
-
"text_sha256": core.DECLARATION_SHA256, "name": "T", "created": core.now(), "nonce": core.nonce()}, KP)
|
|
279
|
-
doc = core.sign_object({"spec": core.SPEC, "eid": KP.did, "name": "T", "layers": {}, "keys": []}, KP)
|
|
278
|
+
doc, decl = client.build_adoption(KP, "T", "d", "http://x") # verify now checks the schemas too
|
|
280
279
|
Stub.leaves = [{"type": "Declaration", "eid": f"did:key:z{i}", "object_sha256": "00" * 32, "received_at": core.now()} for i in range(5)]
|
|
281
280
|
Stub.entity = {"document": doc, "declaration": decl, "log_index": 3}
|
|
282
281
|
client.record(SEED, [("arc_opened", {"arc_id": "a", "title": "A"}), ("arc_opened", {"arc_id": "b", "title": "B"})], registry=url)
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from openentity import cli, core
|
|
10
|
+
from openentity import __version__, cli, core
|
|
11
11
|
|
|
12
12
|
ROOT = Path(__file__).resolve().parents[3]
|
|
13
13
|
VECTORS = ROOT / "test-vectors"
|
|
@@ -90,6 +90,12 @@ def test_declaration_text():
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
# ---- stamp ----
|
|
93
|
+
def test_stamps_for_one_eid_in_one_second_differ():
|
|
94
|
+
"""Registries accept each stamp once, so two quick writes must not mint the same one."""
|
|
95
|
+
eid = core.KeyPair(SEED).did
|
|
96
|
+
assert len({core.mint_stamp(4, eid, "2026-10-28T00:00:00Z") for _ in range(20)}) == 20
|
|
97
|
+
|
|
98
|
+
|
|
93
99
|
def test_stamp():
|
|
94
100
|
eid = core.KeyPair(SEED).did
|
|
95
101
|
h = core.mint_stamp(10, eid)
|
|
@@ -192,7 +198,7 @@ def test_adopt_and_cli(tmp_path, monkeypatch, capsys):
|
|
|
192
198
|
def test_cli_version(capsys):
|
|
193
199
|
with pytest.raises(SystemExit) as e:
|
|
194
200
|
cli.main(["--version"])
|
|
195
|
-
assert e.value.code == 0 and
|
|
201
|
+
assert e.value.code == 0 and __version__ in capsys.readouterr().out
|
|
196
202
|
|
|
197
203
|
|
|
198
204
|
# ---- shared vectors, checked with the independent verifier's own logic ----
|
|
@@ -45,3 +45,15 @@ def test_keychain_failure_falls_back_to_file(monkeypatch):
|
|
|
45
45
|
monkeypatch.delenv("OPENENTITY_KEYCHAIN")
|
|
46
46
|
monkeypatch.setattr(identity, "_run", lambda argv, stdin=None: None) # no `security`, no `secret-tool`
|
|
47
47
|
assert Identity.load_or_create().source == "file"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_load_never_creates_a_key(home):
|
|
51
|
+
"""Read-only callers (a trust gate) must not mint an identity as a side effect."""
|
|
52
|
+
assert Identity.load() is None
|
|
53
|
+
assert not (home / "identity.key").exists()
|
|
54
|
+
a = Identity.load_or_create()
|
|
55
|
+
assert Identity.load().did == a.did
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_home_follows_openentity_home(home):
|
|
59
|
+
assert identity.home() == home
|
|
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
|