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.
- {openentity-0.6.0 → openentity-0.6.1}/PKG-INFO +1 -1
- {openentity-0.6.0 → openentity-0.6.1}/openentity/__init__.py +3 -3
- {openentity-0.6.0 → openentity-0.6.1}/openentity/client.py +5 -21
- {openentity-0.6.0 → openentity-0.6.1}/openentity/core.py +122 -6
- {openentity-0.6.0 → openentity-0.6.1}/openentity/httpsig.py +3 -3
- {openentity-0.6.0 → openentity-0.6.1}/pyproject.toml +1 -1
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_api.py +10 -10
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_httpsig.py +2 -2
- openentity-0.6.1/tests/test_parity_fns.py +92 -0
- {openentity-0.6.0 → openentity-0.6.1}/uv.lock +1 -1
- {openentity-0.6.0 → openentity-0.6.1}/.gitignore +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/README.md +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/cli.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/errors.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/identity.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/issuer.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/report.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/schema.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/openentity/schemas/openentity-1.0.json +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_cli.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_client.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_core.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_identity.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_issuer.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_parity.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_trust.py +0 -0
- {openentity-0.6.0 → openentity-0.6.1}/tests/test_verifier.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
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
|
|
180
|
-
|
|
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)
|
|
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 =
|
|
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
|
|
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(
|
|
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)),
|
|
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"]}
|
|
@@ -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
|
|
17
|
-
build_rotation canonical_hash canonicalize check_stamp check_timestamp
|
|
18
|
-
did_key_from_public_key did_web_resolver dispute eip712_digest find
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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).
|
|
26
|
-
TS_ONLY =
|
|
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
|
|
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(
|
|
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)'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|