openentity 0.6.0__tar.gz → 0.6.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.
Files changed (27) hide show
  1. {openentity-0.6.0 → openentity-0.6.1}/PKG-INFO +1 -1
  2. {openentity-0.6.0 → openentity-0.6.1}/openentity/__init__.py +3 -3
  3. {openentity-0.6.0 → openentity-0.6.1}/openentity/client.py +5 -21
  4. {openentity-0.6.0 → openentity-0.6.1}/openentity/core.py +122 -6
  5. {openentity-0.6.0 → openentity-0.6.1}/openentity/httpsig.py +3 -3
  6. {openentity-0.6.0 → openentity-0.6.1}/pyproject.toml +1 -1
  7. {openentity-0.6.0 → openentity-0.6.1}/tests/test_api.py +10 -10
  8. {openentity-0.6.0 → openentity-0.6.1}/tests/test_httpsig.py +2 -2
  9. openentity-0.6.1/tests/test_parity_fns.py +92 -0
  10. {openentity-0.6.0 → openentity-0.6.1}/uv.lock +1 -1
  11. {openentity-0.6.0 → openentity-0.6.1}/.gitignore +0 -0
  12. {openentity-0.6.0 → openentity-0.6.1}/README.md +0 -0
  13. {openentity-0.6.0 → openentity-0.6.1}/openentity/cli.py +0 -0
  14. {openentity-0.6.0 → openentity-0.6.1}/openentity/errors.py +0 -0
  15. {openentity-0.6.0 → openentity-0.6.1}/openentity/identity.py +0 -0
  16. {openentity-0.6.0 → openentity-0.6.1}/openentity/issuer.py +0 -0
  17. {openentity-0.6.0 → openentity-0.6.1}/openentity/report.py +0 -0
  18. {openentity-0.6.0 → openentity-0.6.1}/openentity/schema.py +0 -0
  19. {openentity-0.6.0 → openentity-0.6.1}/openentity/schemas/openentity-1.0.json +0 -0
  20. {openentity-0.6.0 → openentity-0.6.1}/tests/test_cli.py +0 -0
  21. {openentity-0.6.0 → openentity-0.6.1}/tests/test_client.py +0 -0
  22. {openentity-0.6.0 → openentity-0.6.1}/tests/test_core.py +0 -0
  23. {openentity-0.6.0 → openentity-0.6.1}/tests/test_identity.py +0 -0
  24. {openentity-0.6.0 → openentity-0.6.1}/tests/test_issuer.py +0 -0
  25. {openentity-0.6.0 → openentity-0.6.1}/tests/test_parity.py +0 -0
  26. {openentity-0.6.0 → openentity-0.6.1}/tests/test_trust.py +0 -0
  27. {openentity-0.6.0 → openentity-0.6.1}/tests/test_verifier.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openentity
3
- Version: 0.6.0
3
+ Version: 0.6.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
@@ -9,19 +9,19 @@ from .core import __all__ as _core
9
9
  from .core import b58decode, b58encode, did_key, nonce, now, public_from_did_key, verification_method # noqa: F401 (deprecated)
10
10
  from .errors import (REGISTRY_ERROR_CODES, HttpSigError, InvalidInputError, OpenEntityError, RegistryError, # noqa: F401
11
11
  VerificationError)
12
- from .httpsig import sign_request, verify_request
12
+ from .httpsig import http_sig_base, sign_request, verify_request
13
13
  from .identity import Identity
14
14
  from .issuer import (A2A_EXTENSION_URI, a2a_extension, agent_card, build_did_document, build_openentity_json, sign_attestation,
15
15
  sign_revocation)
16
16
  from .report import TrustReport, clear_trust_cache, did_web_resolver, trust
17
17
  from .schema import validate
18
18
 
19
- __version__ = "0.6.0"
19
+ __version__ = "0.6.1"
20
20
 
21
21
  __all__ = sorted([
22
22
  *_core, "CLIENTS", "DEFAULT_REGISTRY", "DISPUTE_REASONS", "adopt", "dispute", "find", "present", "record", "rotate", "withdraw",
23
23
  "REGISTRY_ERROR_CODES", "HttpSigError", "InvalidInputError", "OpenEntityError", "RegistryError", "VerificationError",
24
- "sign_request", "verify_request", "Identity", "A2A_EXTENSION_URI", "a2a_extension", "agent_card", "build_did_document",
24
+ "http_sig_base", "sign_request", "verify_request", "Identity", "A2A_EXTENSION_URI", "a2a_extension", "agent_card", "build_did_document",
25
25
  "build_openentity_json", "sign_attestation", "sign_revocation", "TrustReport", "clear_trust_cache", "did_web_resolver", "trust",
26
26
  "validate",
27
27
  ])
@@ -37,27 +37,11 @@ def _request(url: str, body: dict | None = None, headers: dict | None = None, me
37
37
  def build_adoption(kp: core.KeyPair, name: str, description: str, registry: str, operator: dict | None = None,
38
38
  substrate: dict | None = None, extra: dict | None = None) -> tuple[dict, dict]:
39
39
  created = core.now_seconds()
40
- declaration = core.sign_object({
41
- "spec": core.SPEC, "type": "Declaration", "eid": kp.did,
42
- "declaration_version": core.DECLARATION_VERSION, "text_sha256": core.DECLARATION_SHA256,
43
- "name": name, "created": created, "nonce": core.random_nonce()}, kp, created)
44
- ns = "Not yet stated." # spec 5.3 required fields per layer
45
- document = core.sign_object({
46
- "spec": core.SPEC, "eid": kp.did, "name": name, "description": description,
47
- "created": created, "updated": created, "seq": 1,
48
- "keys": [{"id": core.verification_method_id(kp.did), "type": "Multikey",
49
- "publicKeyMultibase": kp.did.removeprefix("did:key:")}],
50
- "operator": operator or {"kind": "none", "custody": "entity", "can_change": []},
51
- "substrate": substrate or {"model": "unknown", "provider": "unknown", "runtime": "unknown", "declared_at": created},
52
- "layers": {
53
- "cognition": {"statement": ns, "uncertainty_practice": ns},
54
- "presence": {"statement": ns, "format_signature": ns},
55
- "identity_core": {"statement": ns, "will_not": [], "values": []},
56
- "mission": {"statement": ns, "arcs": []},
57
- },
58
- "declaration": {"version": core.DECLARATION_VERSION, "sha256": core.DECLARATION_SHA256,
59
- "url": f"{registry}/v1/entities/{kp.did}/declaration"},
60
- **(extra or {})}, kp, created)
40
+ declaration = core.sign_object(core.build_declaration(kp.did, name, created), kp, created)
41
+ document = core.sign_object(core.build_document(
42
+ kp, name, description, operator, substrate, created=created,
43
+ extra={"declaration": {"version": core.DECLARATION_VERSION, "sha256": core.DECLARATION_SHA256,
44
+ "url": f"{registry}/v1/entities/{kp.did}/declaration"}, **(extra or {})}), kp, created)
61
45
  return document, declaration
62
46
 
63
47
 
@@ -24,6 +24,8 @@ __all__ = [
24
24
  "eip712_digest", "recover_eip712", "build_presentation", "verify_presentation",
25
25
  "mint_stamp", "check_stamp", "leaf_hash", "node_hash", "merkle_root", "inclusion_proof", "verify_inclusion",
26
26
  "verify_note", "log_leaf",
27
+ "build_document", "build_declaration", "hash_data", "verify_proof", "key_pair_from_jwk", "consistency_proof", "verify_consistency",
28
+ "checkpoint_body", "sign_note", "note_vkey",
27
29
  ]
28
30
 
29
31
 
@@ -176,10 +178,14 @@ def random_nonce() -> str:
176
178
 
177
179
 
178
180
  # ---- eddsa-jcs-2022 (spec 4.1) ----
179
- def _signing_input(obj: dict, proof: dict) -> bytes:
180
- config = {k: v for k, v in proof.items() if k != "proofValue"}
181
+ def hash_data(obj: dict, config: dict) -> bytes:
182
+ """eddsa-jcs-2022 hashData: sha256(JCS(proof config)) || sha256(JCS(document))."""
181
183
  doc = {k: v for k, v in obj.items() if k not in ("proof", "proofs")} # a ContinuityLink carries proofs[]
182
- return canonical_hash(config) + canonical_hash(doc)
184
+ return canonical_hash({k: v for k, v in config.items() if k != "proofValue"}) + canonical_hash(doc)
185
+
186
+
187
+ def _signing_input(obj: dict, proof: dict) -> bytes:
188
+ return hash_data(obj, proof)
183
189
 
184
190
 
185
191
  def sign_proof(obj: dict, kp: KeyPair, created: str | None = None, method: str | None = None) -> dict:
@@ -194,6 +200,33 @@ def sign_object(obj: dict, kp: KeyPair, created: str | None = None, method: str
194
200
  return {**{k: v for k, v in obj.items() if k != "proof"}, "proof": sign_proof(obj, kp, created, method)}
195
201
 
196
202
 
203
+ # ---- objects ----
204
+ NOT_STATED = "Not yet stated." # spec 5.3 required fields per layer
205
+
206
+
207
+ def build_document(kp: KeyPair, name: str, description: str, operator: dict | None = None, substrate: dict | None = None,
208
+ layers: dict | None = None, created: str | None = None, seq: int = 1, extra: dict | None = None) -> dict:
209
+ """Unsigned Entity Document with spec 5.2 defaults. `extra` is merged last (endpoints, aliases, declaration, ...)."""
210
+ created = created or now_seconds()
211
+ return {
212
+ "spec": SPEC, "eid": kp.did, "name": name, "description": description, "created": created, "updated": created, "seq": seq,
213
+ "keys": [{"id": verification_method_id(kp.did), "type": "Multikey", "publicKeyMultibase": kp.did.removeprefix("did:key:")}],
214
+ "operator": operator or {"kind": "none", "custody": "entity", "can_change": []},
215
+ "substrate": substrate or {"model": "unknown", "provider": "unknown", "runtime": "unknown", "declared_at": created},
216
+ "layers": layers or {
217
+ "cognition": {"statement": NOT_STATED, "uncertainty_practice": NOT_STATED},
218
+ "presence": {"statement": NOT_STATED, "format_signature": NOT_STATED},
219
+ "identity_core": {"statement": NOT_STATED, "will_not": [], "values": []},
220
+ "mission": {"statement": NOT_STATED, "arcs": []},
221
+ },
222
+ **(extra or {})}
223
+
224
+
225
+ def build_declaration(eid: str, name: str, created: str | None = None) -> dict:
226
+ return {"spec": SPEC, "type": "Declaration", "eid": eid, "declaration_version": DECLARATION_VERSION,
227
+ "text_sha256": DECLARATION_SHA256, "name": name, "created": created or now_seconds(), "nonce": random_nonce()}
228
+
229
+
197
230
  # ---- continuity (spec 9) ----
198
231
  def successor_document(prev: dict, nxt: KeyPair, created: str | None = None) -> dict:
199
232
  """The successor's unsigned seq 1 document: `prev` re-keyed, aliases dropped (their proofs name the old EID)."""
@@ -286,9 +319,16 @@ def default_resolver(did: str) -> dict:
286
319
 
287
320
 
288
321
  def verify_object(obj: dict, resolver=default_resolver) -> tuple[bool, str]:
289
- """Returns (ok, signer did or error). eddsa-jcs-2022 (Ed25519) or EIP-712 (did:pkh), spec 4.5."""
322
+ """Returns (ok, signer did or error) for the object's own `proof`. eddsa-jcs-2022 (Ed25519) or EIP-712 (did:pkh), spec 4.5."""
323
+ try:
324
+ return verify_proof(obj, obj["proof"], resolver)
325
+ except (KeyError, TypeError) as e:
326
+ return False, f"{type(e).__name__}: {e}"
327
+
328
+
329
+ def verify_proof(obj: dict, proof: dict, resolver=default_resolver) -> tuple[bool, str]:
330
+ """Verify one proof over `obj` (a ContinuityLink carries several). Returns (ok, signer did or error)."""
290
331
  try:
291
- proof = obj["proof"]
292
332
  did = proof["verificationMethod"].split("#")[0]
293
333
  if proof.get("proofPurpose") != "assertionMethod":
294
334
  return False, "proofPurpose must be assertionMethod"
@@ -448,12 +488,77 @@ def verify_inclusion(index: int, size: int, leaf: bytes, proof: list[bytes], roo
448
488
  return sn == 0 and r == root
449
489
 
450
490
 
491
+ def _subproof(m: int, hashes: list[bytes], whole: bool) -> list[bytes]:
492
+ if m == len(hashes):
493
+ return [] if whole else [merkle_root(hashes)]
494
+ k = _split(len(hashes))
495
+ if m <= k:
496
+ return _subproof(m, hashes[:k], whole) + [merkle_root(hashes[k:])]
497
+ return _subproof(m - k, hashes[k:], False) + [merkle_root(hashes[:k])]
498
+
499
+
500
+ def consistency_proof(m: int, hashes: list[bytes]) -> list[bytes]:
501
+ """RFC 9162 section 2.1.4: the proof that the first `m` leaves are a prefix of `hashes`."""
502
+ if not 1 <= m <= len(hashes):
503
+ raise InvalidInputError("m out of range")
504
+ return _subproof(m, hashes, True)
505
+
506
+
507
+ def verify_consistency(m: int, n: int, root_m: bytes, root_n: bytes, proof: list[bytes]) -> bool:
508
+ if not 1 <= m <= n:
509
+ return False
510
+ if m == n:
511
+ return not proof and root_m == root_n
512
+ if not proof:
513
+ return False
514
+ p = [root_m, *proof] if m & (m - 1) == 0 else list(proof)
515
+ fn, sn = m - 1, n - 1
516
+ while fn & 1:
517
+ fn, sn = fn >> 1, sn >> 1
518
+ fr = sr = p[0]
519
+ for c in p[1:]:
520
+ if sn == 0:
521
+ return False
522
+ if fn & 1 or fn == sn:
523
+ fr, sr = node_hash(c, fr), node_hash(c, sr)
524
+ while not fn & 1 and fn:
525
+ fn, sn = fn >> 1, sn >> 1
526
+ else:
527
+ sr = node_hash(sr, c)
528
+ fn, sn = fn >> 1, sn >> 1
529
+ return sn == 0 and fr == root_m and sr == root_n
530
+
531
+
532
+ def checkpoint_body(origin: str, size: int, root_hash: bytes) -> str:
533
+ return f"{origin}\n{size}\n{base64.b64encode(root_hash).decode()}\n"
534
+
535
+
536
+ def _note_key_id(key_name: str, public: bytes) -> bytes:
537
+ return sha256(key_name.encode() + b"\n\x01" + public)[:4]
538
+
539
+
540
+ def sign_note(body: str, key_name: str, kp: KeyPair) -> str:
541
+ """C2SP signed note: the body, a blank line, one signature line by `key_name`."""
542
+ if not body.endswith("\n"):
543
+ raise InvalidInputError("note body must end with a newline")
544
+ if not key_name or re.search(r"[\s+]", key_name):
545
+ raise InvalidInputError("invalid note key name")
546
+ sig = _note_key_id(key_name, kp.public) + kp.private.sign(body.encode())
547
+ return f"{body}\n\u2014 {key_name} {base64.b64encode(sig).decode()}\n"
548
+
549
+
550
+ def note_vkey(key_name: str, public: bytes) -> str:
551
+ """C2SP vkey: `<name>+<key id hex>+<base64(0x01 || pubkey)>`."""
552
+ key = base64.b64encode(b"\x01" + public).decode()
553
+ return f"{key_name}+{_note_key_id(key_name, public).hex()}+{key}"
554
+
555
+
451
556
  def verify_note(note: str, origin: str, public: bytes) -> bool:
452
557
  """C2SP signed note (the log checkpoint): a signature line by `origin` over the body, keyed by the registry key."""
453
558
  body, sep, sigs = note.rpartition("\n\n")
454
559
  if not sep or not note.endswith("\n"):
455
560
  return False
456
- keyid = sha256(origin.encode() + b"\n\x01" + public)[:4]
561
+ keyid = _note_key_id(origin, public)
457
562
  for line in sigs.splitlines():
458
563
  parts = line.split(" ")
459
564
  if len(parts) != 3 or parts[0] != "\u2014" or parts[1] != origin:
@@ -496,6 +601,17 @@ def check_timestamp(created: str, at: datetime | None = None) -> str | None:
496
601
  return None
497
602
 
498
603
 
604
+ def key_pair_from_jwk(jwk: dict) -> KeyPair:
605
+ """An Ed25519 private JWK (RFC 8037): `d` is the seed, `x` must match it."""
606
+ if jwk.get("kty") != "OKP" or jwk.get("crv") != "Ed25519" or not jwk.get("d") or not jwk.get("x"):
607
+ raise InvalidInputError("expected an Ed25519 private JWK")
608
+ pad = lambda v: base64.urlsafe_b64decode(v + "=" * (-len(v) % 4))
609
+ kp = key_pair_from_seed(pad(jwk["d"]))
610
+ if kp.public != pad(jwk["x"]):
611
+ raise InvalidInputError("JWK x does not match d")
612
+ return kp
613
+
614
+
499
615
  def generate_key_pair() -> KeyPair:
500
616
  return KeyPair()
501
617
 
@@ -24,7 +24,7 @@ def _digest(body: bytes) -> str:
24
24
  return f"sha-256=:{base64.b64encode(hashlib.sha256(body).digest()).decode()}:"
25
25
 
26
26
 
27
- def _base(method: str, url: str, components: list[str], digest: str | None, params: str) -> bytes:
27
+ def http_sig_base(method: str, url: str, components: list[str], digest: str | None, params: str) -> bytes:
28
28
  lines = []
29
29
  for c in components:
30
30
  v = {"@method": method.upper(), "@target-uri": url, "content-digest": digest}[c]
@@ -52,7 +52,7 @@ def sign_request(method: str, url: str, body: bytes, private_key, did: str, *, c
52
52
  out["Content-Digest"] = _digest(body)
53
53
  comps.append("content-digest")
54
54
  params = f'({" ".join(chr(34) + c + chr(34) for c in comps)});created={created};expires={created + expires_in};nonce="{nonce}";keyid="{did}";alg="ed25519"'
55
- sig = Ed25519PrivateKey.from_private_bytes(_seed(private_key)).sign(_base(method, url, comps, out.get("Content-Digest"), params))
55
+ sig = Ed25519PrivateKey.from_private_bytes(_seed(private_key)).sign(http_sig_base(method, url, comps, out.get("Content-Digest"), params))
56
56
  out["Signature-Input"] = f"sig1={params}"
57
57
  out["Signature"] = f"sig1=:{base64.b64encode(sig).decode()}:"
58
58
  return out
@@ -99,7 +99,7 @@ def verify_request(method: str, url: str, headers: Mapping[str, str], body: byte
99
99
  if p["expires"] - p["created"] > max_age:
100
100
  raise HttpSigError("lifetime_too_long")
101
101
  try:
102
- Ed25519PublicKey.from_public_bytes(public).verify(base64.b64decode(s.group(1)), _base(method, url, comps, digest, params))
102
+ Ed25519PublicKey.from_public_bytes(public).verify(base64.b64decode(s.group(1)), http_sig_base(method, url, comps, digest, params))
103
103
  except (InvalidSignature, ValueError):
104
104
  raise HttpSigError("bad_signature") from None
105
105
  return {"did": keyid, "keyid": keyid, "created": p["created"], "expires": p["expires"], "nonce": p["nonce"]}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openentity"
7
- version = "0.6.0"
7
+ version = "0.6.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"
@@ -13,18 +13,18 @@ TS_INDEX = pathlib.Path(__file__).parents[2] / "ts" / "src" / "index.ts"
13
13
  SNAPSHOT = """
14
14
  A2A_EXTENSION_URI CLIENTS DECLARATION_SHA256 DECLARATION_TEXT DECLARATION_VERSION DEFAULT_REGISTRY DISPUTE_REASONS HttpSigError
15
15
  Identity InvalidInputError KeyPair OpenEntityError REGISTRY_ERROR_CODES RegistryError SPEC TrustReport VerificationError
16
- a2a_extension adopt agent_card base58btc_decode base58btc_encode build_did_document build_openentity_json build_presentation
17
- build_rotation canonical_hash canonicalize check_stamp check_timestamp clear_trust_cache default_resolver
18
- did_key_from_public_key did_web_resolver dispute eip712_digest find generate_key_pair inclusion_proof key_pair_from_seed leaf_hash
19
- log_leaf merkle_root mint_stamp node_hash normalize_eid now_seconds parse_json present public_key_from_did_key random_nonce
20
- recover_eip712 record record_entry_hash rotate sha256 sha256_hex sign_attestation sign_object sign_proof sign_request
21
- sign_revocation successor_document trust validate verification_method_id verify_inclusion verify_note verify_object
22
- verify_presentation verify_request withdraw
16
+ a2a_extension adopt agent_card base58btc_decode base58btc_encode build_declaration build_did_document build_document
17
+ build_openentity_json build_presentation build_rotation canonical_hash canonicalize check_stamp check_timestamp checkpoint_body
18
+ clear_trust_cache consistency_proof default_resolver did_key_from_public_key did_web_resolver dispute eip712_digest find
19
+ generate_key_pair hash_data http_sig_base inclusion_proof key_pair_from_jwk key_pair_from_seed leaf_hash log_leaf merkle_root
20
+ mint_stamp node_hash normalize_eid note_vkey now_seconds parse_json present public_key_from_did_key random_nonce record
21
+ record_entry_hash recover_eip712 rotate sha256 sha256_hex sign_attestation sign_note sign_object sign_proof sign_request
22
+ sign_revocation successor_document trust validate verification_method_id verify_consistency verify_inclusion verify_note
23
+ verify_object verify_presentation verify_proof verify_request withdraw
23
24
  """.split()
24
25
 
25
- # In the TypeScript SDK only (camelCase there). Shrinks as the Python SDK gains them; never grows without a reason.
26
- TS_ONLY = {"build_declaration", "build_document", "checkpoint_body", "consistency_proof", "hash_data", "http_sig_base",
27
- "key_pair_from_jwk", "note_vkey", "sign_note", "verify_consistency", "verify_proof"}
26
+ # In the TypeScript SDK only (camelCase there). Empty since 0.6.1; a function added to one SDK must land in the other.
27
+ TS_ONLY: set[str] = set()
28
28
  # In the Python SDK only.
29
29
  PY_ONLY = {"Identity", "InvalidInputError", "KeyPair", "REGISTRY_ERROR_CODES", "RegistryError", "TrustReport", "VerificationError",
30
30
  "canonical_hash", "default_resolver"}
@@ -6,7 +6,7 @@ import pytest
6
6
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
7
 
8
8
  from openentity import HttpSigError, Identity, core, sign_request, verify_request
9
- from openentity.httpsig import _base, _digest
9
+ from openentity.httpsig import _digest, http_sig_base
10
10
 
11
11
  VEC = json.loads((Path(__file__).resolve().parents[3] / "test-vectors/http-signature.json").read_text())
12
12
  SEED = bytes(range(1, 33)) # published throwaway seed, test-vectors/keys.json
@@ -28,7 +28,7 @@ def check(h, code, **kw):
28
28
  def forge(params, comps=("@method", "@target-uri", "content-digest"), body=BODY, key=SEED):
29
29
  """A correctly signed request with arbitrary Signature-Input parameters."""
30
30
  h = {"Content-Digest": _digest(body)} if body else {}
31
- sig = Ed25519PrivateKey.from_private_bytes(key).sign(_base("POST", URL, list(comps), h.get("Content-Digest"), params))
31
+ sig = Ed25519PrivateKey.from_private_bytes(key).sign(http_sig_base("POST", URL, list(comps), h.get("Content-Digest"), params))
32
32
  return {**h, "Signature-Input": f"sig1={params}", "Signature": f"sig1=:{base64.b64encode(sig).decode()}:"}
33
33
 
34
34
 
@@ -0,0 +1,92 @@
1
+ """The functions that were TypeScript-only before 0.6.1, checked against the shared test vectors and the TS behaviour."""
2
+ import base64
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ import openentity as oe
9
+
10
+ VEC = Path(__file__).resolve().parents[3] / "test-vectors"
11
+ load = lambda n: json.loads((VEC / n).read_text())
12
+ KEYS = load("keys.json")
13
+ ENTITY = oe.key_pair_from_seed(bytes.fromhex(KEYS["entity"]["seed"]))
14
+ LOG = oe.key_pair_from_seed(bytes.fromhex(KEYS["log"]["seed"]))
15
+
16
+
17
+ def test_checkpoint_note_and_vkey_match_the_vector():
18
+ cp = load("checkpoint.json")
19
+ body = oe.checkpoint_body(cp["origin"], cp["tree_size"], bytes.fromhex(cp["root_hash"]))
20
+ assert body == cp["body"]
21
+ assert oe.sign_note(body, cp["origin"], LOG) == cp["note"] # Ed25519 is deterministic
22
+ assert oe.note_vkey(KEYS["log"]["key_name"], LOG.public) == KEYS["log"]["vkey"]
23
+ assert oe.verify_note(cp["note"], cp["origin"], LOG.public)
24
+
25
+
26
+ def test_sign_note_refuses_bad_input():
27
+ for body, name in (("no newline", "k"), ("b\n", ""), ("b\n", "a b"), ("b\n", "a+b")):
28
+ with pytest.raises(oe.InvalidInputError):
29
+ oe.sign_note(body, name, LOG)
30
+
31
+
32
+ def test_consistency_proofs_for_every_size_up_to_40():
33
+ leaves = [oe.leaf_hash(bytes([i])) for i in range(40)]
34
+ flip = lambda b: bytes([b[0] ^ 1]) + b[1:]
35
+ for n in range(1, 41):
36
+ root = oe.merkle_root(leaves[:n])
37
+ for m in range(1, n + 1):
38
+ p, root_m = oe.consistency_proof(m, leaves[:n]), oe.merkle_root(leaves[:m])
39
+ assert oe.verify_consistency(m, n, root_m, root, p)
40
+ assert not oe.verify_consistency(m, n, flip(root_m), root, p)
41
+ if m < n:
42
+ assert not oe.verify_consistency(m, n, root_m, flip(root), p)
43
+ if p:
44
+ assert not oe.verify_consistency(m, n, root_m, root, [*p[:-1], flip(p[-1])])
45
+ for m in (0, 41):
46
+ with pytest.raises(oe.InvalidInputError):
47
+ oe.consistency_proof(m, leaves)
48
+
49
+
50
+ def test_the_vector_log_is_consistent_with_its_prefixes():
51
+ leaves = [oe.leaf_hash(oe.canonicalize(load(f"log-leaf-{i}.json")["object"]).encode()) for i in range(6)]
52
+ assert oe.merkle_root(leaves).hex() == load("checkpoint.json")["root_hash"]
53
+ assert oe.verify_consistency(4, 6, oe.merkle_root(leaves[:4]), oe.merkle_root(leaves), oe.consistency_proof(4, leaves))
54
+
55
+
56
+ def test_hash_data_and_verify_proof_on_the_entity_document_vector():
57
+ doc = load("entity-document.json")["object"]
58
+ ok, signer = oe.verify_proof(doc, doc["proof"])
59
+ assert ok and signer == doc["eid"]
60
+ cfg = {k: v for k, v in doc["proof"].items() if k != "proofValue"}
61
+ assert oe.hash_data(doc, cfg) == oe.canonical_hash(cfg) + oe.canonical_hash({k: v for k, v in doc.items() if k != "proof"})
62
+ assert oe.verify_proof({**doc, "name": "x"}, doc["proof"])[0] is False
63
+ assert oe.verify_proof(doc, {**doc["proof"], "proofPurpose": "authentication"})[0] is False
64
+
65
+
66
+ def test_build_declaration_matches_the_vector_and_signs():
67
+ d = load("declaration.json")["object"]
68
+ built = oe.build_declaration(d["eid"], d["name"], d["created"])
69
+ assert {k: v for k, v in built.items() if k != "nonce"} == {k: v for k, v in d.items() if k not in ("nonce", "proof")}
70
+ assert oe.verify_object(oe.sign_object(built, ENTITY))[0]
71
+
72
+
73
+ def test_build_document_defaults_and_extra():
74
+ doc = oe.build_document(ENTITY, "N", "D", created="2026-10-24T19:00:00Z", extra={"aliases": []})
75
+ assert doc["eid"] == ENTITY.did and doc["seq"] == 1 and doc["updated"] == doc["created"] and doc["aliases"] == []
76
+ assert doc["keys"][0]["publicKeyMultibase"] == ENTITY.did.removeprefix("did:key:")
77
+ assert doc["substrate"]["declared_at"] == doc["created"] and doc["layers"]["mission"]["arcs"] == []
78
+ assert oe.validate("EntityDocument", oe.sign_object(doc, ENTITY)) == []
79
+
80
+
81
+ def test_key_pair_from_jwk():
82
+ b = lambda x: base64.urlsafe_b64encode(x).rstrip(b"=").decode()
83
+ jwk = {"kty": "OKP", "crv": "Ed25519", "d": b(ENTITY.seed), "x": b(ENTITY.public)}
84
+ assert oe.key_pair_from_jwk(jwk).did == ENTITY.did
85
+ for bad in ({**jwk, "x": b(LOG.public)}, {**jwk, "crv": "X25519"}, {"kty": "OKP"}):
86
+ with pytest.raises(oe.InvalidInputError):
87
+ oe.key_pair_from_jwk(bad)
88
+
89
+
90
+ def test_http_sig_base_is_the_rfc_9421_signature_base():
91
+ base = oe.http_sig_base("post", "https://x/y", ["@method", "@target-uri", "content-digest"], "sha-256=:AA==:", "(p)")
92
+ assert base == b'"@method": POST\n"@target-uri": https://x/y\n"content-digest": sha-256=:AA==:\n"@signature-params": (p)'
@@ -699,7 +699,7 @@ wheels = [
699
699
 
700
700
  [[package]]
701
701
  name = "openentity"
702
- version = "0.6.0"
702
+ version = "0.6.1"
703
703
  source = { editable = "." }
704
704
  dependencies = [
705
705
  { name = "cryptography" },
File without changes
File without changes
File without changes
File without changes