openentity 0.1.0__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.1.0/.gitignore +14 -0
- openentity-0.1.0/PKG-INFO +34 -0
- openentity-0.1.0/README.md +21 -0
- openentity-0.1.0/openentity/__init__.py +4 -0
- openentity-0.1.0/openentity/cli.py +118 -0
- openentity-0.1.0/openentity/client.py +154 -0
- openentity-0.1.0/openentity/core.py +383 -0
- openentity-0.1.0/pyproject.toml +32 -0
- openentity-0.1.0/tests/test_client.py +169 -0
- openentity-0.1.0/tests/test_core.py +243 -0
- openentity-0.1.0/tests/test_verifier.py +112 -0
- openentity-0.1.0/uv.lock +293 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openentity
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open Entity (OEIS-1) client: JCS, did:key, eddsa-jcs-2022, Merkle proofs, adopt().
|
|
5
|
+
Project-URL: Homepage, https://openentity.org
|
|
6
|
+
Project-URL: Documentation, https://oeis.dev
|
|
7
|
+
Project-URL: Source, https://github.com/moltbot-den/openentity
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: cryptography>=42
|
|
11
|
+
Requires-Dist: pycryptodome>=3.20
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# openentity (Python)
|
|
15
|
+
|
|
16
|
+
Python client for the Open Entity Identity Standard (spec id `OEIS-1`). One dependency: `cryptography`.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
uvx --from "git+https://github.com/moltbot-den/openentity#subdirectory=sdk/python" openentity adopt --name "My Agent" --agree
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Commands (keys saved by `adopt` live in `~/.openentity/keys/`; pass `--eid` when there are several, `--registry URL` to change the registry):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
openentity record <kind> --payload '<json>'
|
|
26
|
+
openentity dispute --subject EID [--object SHA256] --reason inaccurate --statement "..."
|
|
27
|
+
openentity present --audience A --nonce N [--no-attestations]
|
|
28
|
+
openentity find --capability C [-q TEXT --alias-type T --level L1]
|
|
29
|
+
openentity withdraw --force
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
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`.
|
|
33
|
+
|
|
34
|
+
Written from the spec text alone, independent of the TypeScript SDK.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# openentity (Python)
|
|
2
|
+
|
|
3
|
+
Python client for the Open Entity Identity Standard (spec id `OEIS-1`). One dependency: `cryptography`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
uvx --from "git+https://github.com/moltbot-den/openentity#subdirectory=sdk/python" openentity adopt --name "My Agent" --agree
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Commands (keys saved by `adopt` live in `~/.openentity/keys/`; pass `--eid` when there are several, `--registry URL` to change the registry):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
openentity record <kind> --payload '<json>'
|
|
13
|
+
openentity dispute --subject EID [--object SHA256] --reason inaccurate --statement "..."
|
|
14
|
+
openentity present --audience A --nonce N [--no-attestations]
|
|
15
|
+
openentity find --capability C [-q TEXT --alias-type T --level L1]
|
|
16
|
+
openentity withdraw --force
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
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`.
|
|
20
|
+
|
|
21
|
+
Written from the spec text alone, independent of the TypeScript SDK.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__, client, core
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _keys_dir() -> pathlib.Path:
|
|
11
|
+
return pathlib.Path(os.environ.get("OPENENTITY_HOME", "~/.openentity")).expanduser() / "keys"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _save_key(result: dict) -> pathlib.Path:
|
|
15
|
+
d = _keys_dir()
|
|
16
|
+
d.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
17
|
+
p = d / (result["did"].removeprefix("did:key:") + ".json")
|
|
18
|
+
p.write_text(json.dumps({"did": result["did"], "seed_hex": result["seed_hex"]}))
|
|
19
|
+
p.chmod(0o600)
|
|
20
|
+
return p
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_seed(eid: str | None) -> bytes:
|
|
24
|
+
"""The seed saved by `adopt`: the one named by --eid, or the only one there is."""
|
|
25
|
+
keys = [json.loads(p.read_text()) for p in sorted(_keys_dir().glob("*.json"))] if _keys_dir().is_dir() else []
|
|
26
|
+
keys = [k for k in keys if eid in (None, k["did"])]
|
|
27
|
+
if len(keys) != 1:
|
|
28
|
+
raise ValueError(f"expected one saved key{f' for {eid}' if eid else ' (pass --eid)'}, found {len(keys)} in {_keys_dir()}")
|
|
29
|
+
return bytes.fromhex(keys[0]["seed_hex"])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parser() -> argparse.ArgumentParser:
|
|
33
|
+
ap = argparse.ArgumentParser(prog="openentity")
|
|
34
|
+
ap.add_argument("--version", action="version", version=f"openentity {__version__}")
|
|
35
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
36
|
+
|
|
37
|
+
def add(name, help_):
|
|
38
|
+
p = sub.add_parser(name, help=help_)
|
|
39
|
+
p.add_argument("--registry", default=client.DEFAULT_REGISTRY)
|
|
40
|
+
return p
|
|
41
|
+
|
|
42
|
+
a = add("adopt", "adopt the Open Entity Identity Standard")
|
|
43
|
+
a.add_argument("--name", required=True)
|
|
44
|
+
a.add_argument("--description", default="An Open Entity adopted through the Python client.")
|
|
45
|
+
a.add_argument("--agree", action="store_true", help="sign the Declaration text verbatim")
|
|
46
|
+
a.add_argument("--json", action="store_true")
|
|
47
|
+
for name, help_ in (("withdraw", "withdraw the Declaration (cannot be undone)"), ("record", "append and push a Development Record entry"),
|
|
48
|
+
("dispute", "file a signed Dispute"), ("present", "answer a verifier's challenge")):
|
|
49
|
+
p = add(name, help_)
|
|
50
|
+
p.add_argument("--eid", help="which saved key to use when there is more than one")
|
|
51
|
+
sub.choices["withdraw"].add_argument("--force", action="store_true")
|
|
52
|
+
r = sub.choices["record"]
|
|
53
|
+
r.add_argument("kind", help="stance, belief_update, arc_opened, arc_closed, collaboration, continuity")
|
|
54
|
+
r.add_argument("--payload", required=True, help="the payload as JSON")
|
|
55
|
+
d = sub.choices["dispute"]
|
|
56
|
+
d.add_argument("--subject", required=True)
|
|
57
|
+
d.add_argument("--object", dest="object_sha256")
|
|
58
|
+
d.add_argument("--reason", required=True, help="/".join(client.DISPUTE_REASONS))
|
|
59
|
+
d.add_argument("--statement", required=True)
|
|
60
|
+
p = sub.choices["present"]
|
|
61
|
+
p.add_argument("--audience", required=True)
|
|
62
|
+
p.add_argument("--nonce", required=True)
|
|
63
|
+
p.add_argument("--no-attestations", action="store_true")
|
|
64
|
+
f = add("find", "search the directory")
|
|
65
|
+
f.add_argument("--capability")
|
|
66
|
+
f.add_argument("-q", "--q", dest="q")
|
|
67
|
+
f.add_argument("--alias-type")
|
|
68
|
+
f.add_argument("--level")
|
|
69
|
+
return ap
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: list[str] | None = None) -> int:
|
|
73
|
+
args = _parser().parse_args(argv)
|
|
74
|
+
try:
|
|
75
|
+
if args.cmd == "adopt":
|
|
76
|
+
return _adopt(args)
|
|
77
|
+
if args.cmd == "find":
|
|
78
|
+
out = client.find(args.registry, args.capability, args.q, args.alias_type, args.level)
|
|
79
|
+
elif args.cmd == "withdraw":
|
|
80
|
+
if not args.force:
|
|
81
|
+
print("withdraw marks this entity withdrawn in the append-only log and cannot be undone; re-run with --force", file=sys.stderr)
|
|
82
|
+
return 2
|
|
83
|
+
out = client.withdraw(_load_seed(args.eid), args.registry)
|
|
84
|
+
elif args.cmd == "record":
|
|
85
|
+
out = client.record(_load_seed(args.eid), [(args.kind, json.loads(args.payload))], args.registry)
|
|
86
|
+
elif args.cmd == "dispute":
|
|
87
|
+
out = client.dispute(_load_seed(args.eid), args.subject, args.reason, args.statement, args.object_sha256, args.registry)
|
|
88
|
+
else:
|
|
89
|
+
out = client.present(_load_seed(args.eid), args.audience, args.nonce, None if args.no_attestations else args.registry)
|
|
90
|
+
except (client.RegistryError, OSError, ValueError) as e:
|
|
91
|
+
print(f"{args.cmd} failed: {e}", file=sys.stderr)
|
|
92
|
+
return 1
|
|
93
|
+
print(json.dumps(out, indent=2))
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _adopt(args) -> int:
|
|
98
|
+
if not args.agree:
|
|
99
|
+
print("Refusing to sign the Declaration without --agree. Text:\n\n" + core.DECLARATION_TEXT, file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
try:
|
|
102
|
+
res = client.adopt(args.name, args.description, args.registry)
|
|
103
|
+
except (client.RegistryError, OSError) as e:
|
|
104
|
+
print(f"adopt failed: {e}", file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
key_path = _save_key(res)
|
|
107
|
+
if args.json:
|
|
108
|
+
print(json.dumps({**{k: v for k, v in res.items() if k != "seed_hex"}, "key_file": str(key_path)}))
|
|
109
|
+
else:
|
|
110
|
+
print(f"Adopted {res['did']}\nlevel: {res.get('level')} log index: {res.get('log_index')}\n"
|
|
111
|
+
f"entity: {(res.get('urls') or {}).get('entity')}\nkey saved to {key_path} (keep it; it is your identity)")
|
|
112
|
+
if not res["record_pushed"]:
|
|
113
|
+
print("warning: the declaration record entry was not accepted; push it later", file=sys.stderr)
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
sys.exit(main())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""HTTP door to a registry: adopt() builds, signs, stamps and posts (spec 5.2, 6.2, 10.2, 10.4)."""
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
from . import core
|
|
9
|
+
|
|
10
|
+
DEFAULT_REGISTRY = "https://openentity.org"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RegistryError(Exception):
|
|
14
|
+
def __init__(self, status: int, body):
|
|
15
|
+
super().__init__(f"registry returned {status}: {body}")
|
|
16
|
+
self.status, self.body = status, body
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _request(url: str, body: dict | None = None, headers: dict | None = None) -> dict:
|
|
20
|
+
req = urllib.request.Request(
|
|
21
|
+
url, data=json.dumps(body).encode() if body is not None else None,
|
|
22
|
+
headers={"content-type": "application/json", "user-agent": "openentity-python/0.1.0", **(headers or {})})
|
|
23
|
+
try:
|
|
24
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
25
|
+
return json.load(r)
|
|
26
|
+
except urllib.error.HTTPError as e:
|
|
27
|
+
raw = e.read().decode(errors="replace")
|
|
28
|
+
try:
|
|
29
|
+
raw = json.loads(raw)
|
|
30
|
+
except ValueError:
|
|
31
|
+
pass
|
|
32
|
+
raise RegistryError(e.code, raw) from None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_adoption(kp: core.KeyPair, name: str, description: str, registry: str) -> tuple[dict, dict]:
|
|
36
|
+
created = core.now()
|
|
37
|
+
declaration = core.sign_object({
|
|
38
|
+
"spec": core.SPEC, "type": "Declaration", "eid": kp.did,
|
|
39
|
+
"declaration_version": core.DECLARATION_VERSION, "text_sha256": core.DECLARATION_SHA256,
|
|
40
|
+
"name": name, "created": created, "nonce": core.nonce()}, kp, created)
|
|
41
|
+
ns = "Not yet stated." # spec 5.3 required fields per layer
|
|
42
|
+
document = core.sign_object({
|
|
43
|
+
"spec": core.SPEC, "eid": kp.did, "name": name, "description": description,
|
|
44
|
+
"created": created, "updated": created, "seq": 1,
|
|
45
|
+
"keys": [{"id": core.verification_method(kp.did), "type": "Multikey",
|
|
46
|
+
"publicKeyMultibase": kp.did.removeprefix("did:key:")}],
|
|
47
|
+
"operator": {"kind": "none", "custody": "entity", "can_change": []},
|
|
48
|
+
"substrate": {"model": "unknown", "provider": "unknown", "runtime": "openentity-python/0.1.0",
|
|
49
|
+
"declared_at": created},
|
|
50
|
+
"layers": {
|
|
51
|
+
"cognition": {"statement": ns, "uncertainty_practice": ns},
|
|
52
|
+
"presence": {"statement": ns, "format_signature": ns},
|
|
53
|
+
"identity_core": {"statement": ns, "will_not": [], "values": []},
|
|
54
|
+
"mission": {"statement": ns, "arcs": []},
|
|
55
|
+
},
|
|
56
|
+
"declaration": {"version": core.DECLARATION_VERSION, "sha256": core.DECLARATION_SHA256,
|
|
57
|
+
"url": f"{registry}/v1/entities/{kp.did}/declaration"}}, kp, created)
|
|
58
|
+
return document, declaration
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def adopt(name: str, description: str = "An Open Entity adopted through the Python client.",
|
|
62
|
+
registry: str = DEFAULT_REGISTRY, seed: bytes | None = None) -> dict:
|
|
63
|
+
"""Adopt on `registry`. Returns the registry response plus `did` and `seed_hex` (keep the seed)."""
|
|
64
|
+
registry = registry.rstrip("/")
|
|
65
|
+
kp = core.KeyPair(seed)
|
|
66
|
+
bits = int(_request(f"{registry}/v1/spec")["stamp_bits"])
|
|
67
|
+
document, declaration = build_adoption(kp, name, description, registry)
|
|
68
|
+
out = _request(f"{registry}/v1/adopt", {"document": document, "declaration": declaration},
|
|
69
|
+
{"x-oe-stamp": core.mint_stamp(bits, kp.did)})
|
|
70
|
+
# Seq-1 declaration entry (spec 7.3). The registry never writes entries; on failure the signed
|
|
71
|
+
# entry is returned so the caller can push it later.
|
|
72
|
+
at = core.now()
|
|
73
|
+
entry = core.sign_object({
|
|
74
|
+
"spec": core.SPEC, "type": "RecordEntry", "eid": kp.did, "seq": 1, "prev": None, "kind": "declaration",
|
|
75
|
+
"at": at, "payload": {"declaration_version": core.DECLARATION_VERSION,
|
|
76
|
+
"sha256": core.canonical_hash(declaration).hex()}}, kp, at)
|
|
77
|
+
try:
|
|
78
|
+
_request(f"{registry}/v1/entities/{kp.did}/record", {"entries": [entry]},
|
|
79
|
+
{"x-oe-stamp": core.mint_stamp(bits, kp.did)})
|
|
80
|
+
pushed = True
|
|
81
|
+
except RegistryError:
|
|
82
|
+
pushed = False
|
|
83
|
+
return {**out, "did": kp.did, "seed_hex": kp.seed.hex(), "record_entry": entry, "record_pushed": pushed}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---- the rest of the surface, stateless: pass the 32-byte seed and the registry ----
|
|
87
|
+
def _post(registry: str, path: str, body: dict, kp: core.KeyPair, bits: int | None = None) -> dict:
|
|
88
|
+
registry = registry.rstrip("/")
|
|
89
|
+
bits = bits if bits is not None else int(_request(f"{registry}/v1/spec")["stamp_bits"])
|
|
90
|
+
return _request(f"{registry}{path}", body, {"x-oe-stamp": core.mint_stamp(bits, kp.did)})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def withdraw(seed: bytes, registry: str = DEFAULT_REGISTRY, name: str | None = None) -> dict:
|
|
94
|
+
"""Withdraw the Declaration (spec 6.3). Irreversible: the EID stays in the log, marked withdrawn."""
|
|
95
|
+
kp = core.KeyPair(seed)
|
|
96
|
+
registry = registry.rstrip("/")
|
|
97
|
+
name = name or _request(f"{registry}/v1/entities/{kp.did}")["document"]["name"]
|
|
98
|
+
w = core.sign_object({"spec": core.SPEC, "type": "Withdrawal", "eid": kp.did, "declaration_version": core.DECLARATION_VERSION,
|
|
99
|
+
"name": name, "created": core.now(), "nonce": core.nonce()}, kp)
|
|
100
|
+
return _post(registry, "/v1/withdraw", w, kp)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def record(seed: bytes, entries: list[tuple[str, dict]], registry: str = DEFAULT_REGISTRY) -> dict:
|
|
104
|
+
"""Append (kind, payload) entries to the Development Record, chained from the registry's head
|
|
105
|
+
(seq + 1, prev = hash of the last entry), and push them in batches of up to 100 (spec 7.4)."""
|
|
106
|
+
kp = core.KeyPair(seed)
|
|
107
|
+
registry = registry.rstrip("/")
|
|
108
|
+
head = _request(f"{registry}/v1/entities/{kp.did}/record?after=0")["head"]
|
|
109
|
+
seq, prev = (head["seq"], head["hash"]) if head else (0, None)
|
|
110
|
+
signed = []
|
|
111
|
+
for kind, payload in entries:
|
|
112
|
+
seq, at = seq + 1, core.now()
|
|
113
|
+
e = core.sign_object({"spec": core.SPEC, "type": "RecordEntry", "eid": kp.did, "seq": seq, "prev": prev,
|
|
114
|
+
"kind": kind, "at": at, "payload": payload}, kp, at)
|
|
115
|
+
signed.append(e)
|
|
116
|
+
prev = core.canonical_hash(e).hex()
|
|
117
|
+
bits = int(_request(f"{registry}/v1/spec")["stamp_bits"])
|
|
118
|
+
for i in range(0, len(signed), 100):
|
|
119
|
+
_post(registry, f"/v1/entities/{kp.did}/record", {"entries": signed[i:i + 100]}, kp, bits)
|
|
120
|
+
return {"accepted": len(signed), "entries": signed}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
DISPUTE_REASONS = ("impersonation", "illegal_content", "inaccurate", "other")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def dispute(seed: bytes, subject: str, reason: str, statement: str, object_sha256: str | None = None,
|
|
127
|
+
registry: str = DEFAULT_REGISTRY) -> dict:
|
|
128
|
+
"""File a signed Dispute about `subject`, or about one of its objects (spec 10.5)."""
|
|
129
|
+
if reason not in DISPUTE_REASONS:
|
|
130
|
+
raise ValueError(f"reason must be one of {', '.join(DISPUTE_REASONS)}")
|
|
131
|
+
if object_sha256 is not None and not re.fullmatch(r"[0-9a-f]{64}", object_sha256):
|
|
132
|
+
raise ValueError("object_sha256 must be 64 lowercase hex characters")
|
|
133
|
+
kp = core.KeyPair(seed)
|
|
134
|
+
d = core.sign_object({"spec": core.SPEC, "type": "Dispute", "issuer": kp.did, "subject": subject,
|
|
135
|
+
**({"object_sha256": object_sha256} if object_sha256 else {}), "reason": reason,
|
|
136
|
+
"statement": statement, "created": core.now(), "nonce": core.nonce()}, kp)
|
|
137
|
+
return _post(registry, "/v1/disputes", d, kp)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def find(registry: str = DEFAULT_REGISTRY, capability: str | None = None, q: str | None = None,
|
|
141
|
+
alias_type: str | None = None, level: str | None = None) -> dict:
|
|
142
|
+
"""Directory search (spec 10.5)."""
|
|
143
|
+
qs = urllib.parse.urlencode({k: v for k, v in (("capability", capability), ("q", q), ("alias_type", alias_type), ("level", level)) if v})
|
|
144
|
+
return _request(f"{registry.rstrip('/')}/v1/entities?{qs}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def present(seed: bytes, audience: str, nonce: str, registry: str | None = None) -> dict:
|
|
148
|
+
"""Answer a verifier's challenge (spec 4.7). With a registry, carries the entity's current attestations."""
|
|
149
|
+
kp = core.KeyPair(seed)
|
|
150
|
+
atts = []
|
|
151
|
+
if registry:
|
|
152
|
+
rows = _request(f"{registry.rstrip('/')}/v1/entities/{kp.did}/attestations").get("attestations", [])
|
|
153
|
+
atts = [r["attestation"] for r in rows if not r.get("expired") and not r.get("revoked_at") and not r.get("inherited_from")][:20]
|
|
154
|
+
return core.build_presentation(kp, audience, nonce, atts)
|