interop-verifier 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ """Generalized Sally-style verifier for KERI interop tests."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1 @@
1
+ """CLI package for interop-verifier."""
@@ -0,0 +1,107 @@
1
+ """Command line entrypoint."""
2
+
3
+ import argparse
4
+ import json
5
+
6
+ from keri.app import configing, habbing, keeping
7
+
8
+ import interop_verifier
9
+ from interop_verifier.core import serving
10
+ from interop_verifier.core.policy import VerificationPolicy
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(prog="interop-verifier")
15
+ sub = parser.add_subparsers(dest="command", required=True)
16
+ start = sub.add_parser("start", help="Start the interop verifier service")
17
+ start.add_argument("-p", "--http", default=9723, type=int)
18
+ start.add_argument("-n", "--name", default="interop-verifier")
19
+ start.add_argument("-b", "--base", default="")
20
+ start.add_argument("--head-dir", dest="headDirPath", default=None)
21
+ start.add_argument("-a", "--alias", required=True)
22
+ start.add_argument("-s", "--salt")
23
+ start.add_argument("--passcode", dest="bran", default=None)
24
+ start.add_argument("-i", "--incept-file", dest="inceptFile", default=None)
25
+ start.add_argument("-c", "--config-dir", dest="configDir", default=None)
26
+ start.add_argument("-f", "--config-file", dest="configFile", default=None)
27
+ start.add_argument("-w", "--web-hook", dest="webHook", required=True)
28
+ start.add_argument("--policy", dest="policyFile", default=None)
29
+ start.add_argument("--schema", dest="schemas", action="append", default=[])
30
+ start.add_argument("-r", "--retry-delay", default=3, type=int)
31
+ start.add_argument("-e", "--escrow-timeout", default=10, type=int)
32
+ start.add_argument("--no-clear-escrows", action="store_true", default=False)
33
+ start.set_defaults(handler=start_command)
34
+
35
+ version = sub.add_parser("version", help="Print version")
36
+ version.set_defaults(handler=lambda args: print(interop_verifier.__version__))
37
+
38
+ args = parser.parse_args()
39
+ args.handler(args)
40
+
41
+
42
+ def start_command(args):
43
+ hby = init_habery(
44
+ name=args.name,
45
+ base=args.base,
46
+ bran=args.bran,
47
+ config_file=args.configFile,
48
+ config_dir=args.configDir,
49
+ salt=args.salt,
50
+ head_dir_path=args.headDirPath,
51
+ )
52
+ incept_args = {
53
+ "name": args.name,
54
+ "base": args.base,
55
+ "alias": args.alias,
56
+ "bran": args.bran,
57
+ "incept_file": args.inceptFile,
58
+ "config_dir": args.configDir,
59
+ }
60
+ hab = serving.incept_if_new(hby, args.alias, incept_args)
61
+ serving.load_schemas(hby, args.schemas)
62
+ policy = VerificationPolicy(load_policy(args.policyFile))
63
+ doers = serving.setupDoers(
64
+ hby,
65
+ hab,
66
+ alias=args.alias,
67
+ http_port=args.http,
68
+ hook=args.webHook,
69
+ policy=policy,
70
+ timeout=args.escrow_timeout,
71
+ retry=args.retry_delay,
72
+ clear_escrows=not args.no_clear_escrows,
73
+ head_dir_path=args.headDirPath,
74
+ )
75
+ print(f"Interop verifier {hab.pre} listening on http://127.0.0.1:{args.http}", flush=True)
76
+ serving.run_doers(doers)
77
+
78
+
79
+ def init_habery(name, base, bran, config_file, config_dir, salt, head_dir_path=None):
80
+ ks = keeping.Keeper(name=name, base=base, temp=False, reopen=True, headDirPath=head_dir_path)
81
+ aeid = ks.gbls.get("aeid")
82
+ if aeid is None:
83
+ cf = None
84
+ if config_file is not None:
85
+ cf = configing.Configer(name=config_file, base=base, headDirPath=config_dir, temp=False, reopen=True, clear=False)
86
+ return habbing.Habery(
87
+ name=name,
88
+ base=base,
89
+ bran=bran,
90
+ cf=cf,
91
+ ks=ks,
92
+ salt=salt if salt else None,
93
+ headDirPath=head_dir_path,
94
+ )
95
+
96
+ return habbing.Habery(name=name, base=base, bran=bran, ks=ks, headDirPath=head_dir_path)
97
+
98
+
99
+ def load_policy(path):
100
+ if not path:
101
+ return {}
102
+ with open(path, "r", encoding="utf-8") as fp:
103
+ return json.load(fp)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1 @@
1
+ """Core verifier components."""
@@ -0,0 +1,54 @@
1
+ """Sally-style cue database for verifier communication state."""
2
+
3
+ from keri.core import coring, serdering
4
+ from keri.db import dbing, subing
5
+
6
+
7
+ class CueBaser(dbing.LMDBer):
8
+ """Stores verifier presentation, revocation, delivery, and ack queues."""
9
+
10
+ TailDirPath = "interop-verifier/db"
11
+ AltTailDirPath = ".interop-verifier/db"
12
+ TempPrefix = "interop_verifier_db_"
13
+
14
+ def __init__(self, name="cb", headDirPath=None, reopen=True, **kwa):
15
+ self.snd = None
16
+ self.iss = None
17
+ self.rev = None
18
+ self.recv = None
19
+ self.revk = None
20
+ self.ack = None
21
+ super().__init__(name=name, headDirPath=headDirPath, reopen=reopen, **kwa)
22
+
23
+ def reopen(self, **kwa):
24
+ super().reopen(**kwa)
25
+ self.snd = subing.CesrSuber(db=self, subkey="snd.", klas=coring.Prefixer)
26
+ self.iss = subing.CesrSuber(db=self, subkey="iss.", klas=coring.Dater)
27
+ self.rev = subing.CesrSuber(db=self, subkey="rev.", klas=coring.Dater)
28
+ self.recv = subing.SerderSuber(db=self, subkey="recv", klas=serdering.SerderACDC)
29
+ self.revk = subing.SerderSuber(db=self, subkey="revk", klas=serdering.SerderACDC)
30
+ self.ack = subing.SerderSuber(db=self, subkey="ack", klas=serdering.SerderACDC)
31
+ return self.env
32
+
33
+ def clearEscrows(self):
34
+ self.iss.trim()
35
+ self.rev.trim()
36
+ self.recv.trim()
37
+ self.revk.trim()
38
+ self.ack.trim()
39
+
40
+ @staticmethod
41
+ def items(suber, keys=""):
42
+ if hasattr(suber, "getItemIter"):
43
+ return suber.getItemIter(keys=keys)
44
+ return suber.getTopItemIter(keys=keys)
45
+
46
+ def getCounts(self):
47
+ return {
48
+ "senders": self.snd.cntAll(),
49
+ "iss": self.iss.cntAll(),
50
+ "rev": self.rev.cntAll(),
51
+ "recv": self.recv.cntAll(),
52
+ "revk": self.revk.cntAll(),
53
+ "ack": self.ack.cntAll(),
54
+ }
@@ -0,0 +1,32 @@
1
+ """TEL revocation cue bridge."""
2
+
3
+ from hio.base import doing
4
+ from hio.help import decking
5
+ from keri.core import coring
6
+
7
+
8
+ class TeveryCuery(doing.Doer):
9
+ """Moves KERIpy TEL revocation cues into the verifier cue database."""
10
+
11
+ def __init__(self, cdb, reger, cues=None, **kwa):
12
+ self.cdb = cdb
13
+ self.reger = reger
14
+ self.cues = cues if cues is not None else decking.Deck()
15
+ super().__init__(**kwa)
16
+
17
+ def do(self, tymth, *, tock=0.0, **opts):
18
+ self.wind(tymth)
19
+ self.tock = tock
20
+ yield self.tock
21
+ while True:
22
+ while self.cues:
23
+ cue = self.cues.popleft()
24
+ if cue.get("kin") == "revoked":
25
+ serder = cue["serder"]
26
+ said = serder.ked["i"]
27
+ creder = self.reger.creds.get(said)
28
+ if creder is not None:
29
+ self.cdb.snd.pin(keys=(said,), val=coring.Prefixer(qb64=creder.issuer))
30
+ self.cdb.rev.pin(keys=(said,), val=coring.Dater())
31
+ yield self.tock
32
+ yield self.tock
@@ -0,0 +1,208 @@
1
+ """Generic Sally-style IPEX presentation and webhook handling."""
2
+
3
+ import datetime
4
+ import json
5
+ from base64 import urlsafe_b64encode as encodeB64
6
+ from urllib import parse
7
+
8
+ from hio.base import doing, Doer
9
+ from hio.core import http
10
+ from hio.help import Hict
11
+ from keri import kering
12
+ from keri.core import coring
13
+ from keri.end import ending
14
+ from keri.help import helping
15
+ from keri.peer import exchanging
16
+
17
+ from interop_verifier.core import httping
18
+ from interop_verifier.core.policy import VerificationPolicy, credential_payload, revocation_payload
19
+
20
+
21
+ def loadHandlers(cdb, hby, notifier, parser):
22
+ return [PresentationProofHandler(cdb=cdb, hby=hby, notifier=notifier, parser=parser)]
23
+
24
+
25
+ class PresentationProofHandler(doing.Doer):
26
+ """Processes IPEX grant notifications and parses embedded proof material."""
27
+
28
+ def __init__(self, cdb, hby, notifier, parser, **kwa):
29
+ self.cdb = cdb
30
+ self.hby = hby
31
+ self.notifier = notifier
32
+ self.parser = parser
33
+ super().__init__(**kwa)
34
+
35
+ def processNotes(self):
36
+ for keys, notice in self.cdb.items(self.notifier.noter.notes):
37
+ attrs = notice.attrs
38
+ route = attrs.get("r")
39
+ if route in ("/exn/ipex/grant", "/ipex/grant"):
40
+ said = attrs["d"]
41
+ exn, pathed = exchanging.cloneMessage(self.hby, said=said)
42
+ embeds = exn.ked["e"]
43
+
44
+ for label in ("anc", "iss", "acdc"):
45
+ if label not in embeds:
46
+ continue
47
+ sadder = coring.Sadder(ked=embeds[label])
48
+ attachment = pathed[label] if label in pathed else bytearray()
49
+ self.parser.parseOne(ims=bytearray(sadder.raw) + attachment)
50
+
51
+ acdc = embeds["acdc"]
52
+ credential = acdc["d"]
53
+ sender = acdc["i"]
54
+ self.cdb.snd.pin(keys=(credential,), val=coring.Prefixer(qb64=sender))
55
+ self.cdb.iss.pin(keys=(credential,), val=coring.Dater())
56
+
57
+ self.notifier.noter.notes.rem(keys=keys)
58
+ return False
59
+
60
+ def recur(self, tyme):
61
+ self.processNotes()
62
+ return False
63
+
64
+
65
+ class Communicator(doing.DoDoer):
66
+ """Moves verified credentials to webhook delivery queues and posts callbacks."""
67
+
68
+ def __init__(self, hby, hab, cdb, reger, hook, policy=None, timeout=10, retry=3.0):
69
+ self.hby = hby
70
+ self.hab = hab
71
+ self.cdb = cdb
72
+ self.reger = reger
73
+ self.hook = hook
74
+ self.policy = policy or VerificationPolicy()
75
+ self.timeout = timeout
76
+ self.retry = retry
77
+ self.clients = {}
78
+ super().__init__(doers=[doing.doify(self.escrowDo)])
79
+
80
+ def processPresentations(self):
81
+ for (said,), dater in self.cdb.items(self.cdb.iss):
82
+ now = helping.nowUTC()
83
+ if now - dater.datetime > datetime.timedelta(minutes=self.timeout):
84
+ self.cdb.iss.rem(keys=(said,))
85
+ continue
86
+
87
+ if self.reger.saved.get(keys=(said,)) is None:
88
+ continue
89
+
90
+ creder = self.reger.creds.get(keys=(said,))
91
+ try:
92
+ state = self.reger.tevers[creder.regid].vcState(creder.said)
93
+ if state is None or state.et not in (kering.Ilks.iss, kering.Ilks.bis):
94
+ raise kering.ValidationError(f"credential {creder.said} is not currently issued")
95
+ self.policy.validate(creder)
96
+ except kering.ValidationError:
97
+ self.cdb.iss.rem(keys=(said,))
98
+ else:
99
+ self.cdb.recv.pin(keys=(said, dater.qb64), val=creder)
100
+ self.cdb.iss.rem(keys=(said,))
101
+
102
+ def processRevocations(self):
103
+ for (said,), dater in self.cdb.items(self.cdb.rev):
104
+ now = helping.nowUTC()
105
+ if now - dater.datetime > datetime.timedelta(minutes=self.timeout):
106
+ self.cdb.rev.rem(keys=(said,))
107
+ continue
108
+
109
+ creder = self.reger.creds.get(keys=(said,))
110
+ if creder is None:
111
+ continue
112
+ state = self.reger.tevers[creder.regid].vcState(creder.said)
113
+ if state is None or state.et in (kering.Ilks.iss, kering.Ilks.bis):
114
+ continue
115
+ if state.et in (kering.Ilks.rev, kering.Ilks.brv):
116
+ self.cdb.rev.rem(keys=(said,))
117
+ self.cdb.revk.pin(keys=(said, dater.qb64), val=creder)
118
+
119
+ def processReceived(self, db, action):
120
+ for (said, dates), creder in self.cdb.items(db):
121
+ if said not in self.clients:
122
+ data = (
123
+ credential_payload(self.reger, creder, self.policy)
124
+ if action == "iss"
125
+ else revocation_payload(self.reger, creder, self.policy)
126
+ )
127
+ self.request(creder.said, creder.schema, action, creder.issuer, data)
128
+ continue
129
+
130
+ client, clientDoer = self.clients[said]
131
+ if client.responses:
132
+ response = client.responses.popleft()
133
+ self.remove([clientDoer])
134
+ client.close()
135
+ del self.clients[said]
136
+
137
+ if 200 <= response["status"] < 300:
138
+ db.rem(keys=(said, dates))
139
+ self.cdb.ack.pin(keys=(said,), val=creder)
140
+ else:
141
+ dater = coring.Dater(qb64=dates)
142
+ if helping.nowUTC() - dater.datetime > datetime.timedelta(minutes=self.timeout):
143
+ db.rem(keys=(said, dates))
144
+
145
+ def processAcks(self):
146
+ for (said,), _ in self.cdb.items(self.cdb.ack):
147
+ self.cdb.ack.rem(keys=(said,))
148
+
149
+ def escrowDo(self, tymth, tock=1.0, **opts):
150
+ self.wind(tymth)
151
+ self.tock = tock
152
+ _ = yield self.tock
153
+ while True:
154
+ self.processEscrows()
155
+ yield self.retry
156
+
157
+ def processEscrows(self):
158
+ self.processPresentations()
159
+ self.processRevocations()
160
+ self.processReceived(db=self.cdb.recv, action="iss")
161
+ self.processReceived(db=self.cdb.revk, action="rev")
162
+ self.processAcks()
163
+
164
+ def request(self, said, resource, action, actor, data):
165
+ purl = parse.urlparse(self.hook)
166
+ client = http.clienting.Client(hostname=purl.hostname, port=purl.port or 80)
167
+ clientDoer = http.clienting.ClientDoer(client=client)
168
+ self.extend([clientDoer])
169
+
170
+ raw = json.dumps({"action": action, "actor": actor, "data": data}).encode("utf-8")
171
+ headers = Hict([
172
+ ("Content-Type", "application/json"),
173
+ ("Content-Length", len(raw)),
174
+ ("Connection", "close"),
175
+ ("Sally-Resource", resource),
176
+ ("Sally-Timestamp", helping.nowIso8601()),
177
+ ])
178
+ path = purl.path or "/"
179
+
180
+ keyid = encodeB64(self.hab.kever.serder.verfers[0].raw).decode("utf-8")
181
+ header, unq = httping.siginput(
182
+ self.hab,
183
+ "sig0",
184
+ "POST",
185
+ path,
186
+ headers,
187
+ fields=["Sally-Resource", "@method", "@path", "Sally-Timestamp"],
188
+ alg="ed25519",
189
+ keyid=keyid,
190
+ )
191
+ headers.extend(header)
192
+ signage = ending.Signage(
193
+ markers={"sig0": unq},
194
+ indexed=True,
195
+ signer=self.hab.pre,
196
+ ordinal=None,
197
+ digest=None,
198
+ kind=None,
199
+ )
200
+ headers.extend(ending.signature([signage]))
201
+ client.request(
202
+ method="POST",
203
+ path=path,
204
+ qargs=parse.parse_qs(purl.query),
205
+ headers=headers,
206
+ body=raw,
207
+ )
208
+ self.clients[said] = (client, clientDoer)
@@ -0,0 +1,90 @@
1
+ """HTTP helper functions copied from Sally and kept schema-agnostic."""
2
+
3
+ from base64 import urlsafe_b64encode as encodeB64
4
+ from collections import namedtuple
5
+
6
+ import falcon
7
+ from http_sfv import Dictionary
8
+ from keri.help import helping
9
+
10
+ Inputage = namedtuple("Inputage", "name fields created keyid alg expires nonce context")
11
+
12
+
13
+ def normalize(param):
14
+ return str(param).strip()
15
+
16
+
17
+ def cors_middleware():
18
+ return falcon.CORSMiddleware(
19
+ allow_origins="*",
20
+ allow_credentials="*",
21
+ expose_headers=cesr_headers(),
22
+ )
23
+
24
+
25
+ def cesr_headers():
26
+ return ["cesr-attachment", "cesr-date", "content-type"]
27
+
28
+
29
+ def siginput(hab, name, method, path, headers, fields, expires=None, nonce=None, alg=None, keyid=None, context=None):
30
+ items = []
31
+ ifields = []
32
+ for field in fields:
33
+ if field.startswith("@"):
34
+ if field == "@method":
35
+ items.append(f'"{field}": {method}')
36
+ ifields.append(field)
37
+ elif field == "@path":
38
+ items.append(f'"{field}": {path}')
39
+ ifields.append(field)
40
+ continue
41
+
42
+ lower = field.lower()
43
+ if lower not in headers:
44
+ continue
45
+ ifields.append(lower)
46
+ items.append(f'"{lower}": {normalize(headers[lower])}')
47
+
48
+ sid = Dictionary()
49
+ sid[name] = ifields
50
+ now = helping.nowUTC()
51
+ sid[name].params["created"] = int(now.timestamp())
52
+
53
+ values = [f"({' '.join(ifields)})", f"created={int(now.timestamp())}"]
54
+ if expires is not None:
55
+ values.append(f"expires={expires}")
56
+ sid[name].params["expires"] = expires
57
+ if nonce is not None:
58
+ values.append(f"nonce={nonce}")
59
+ sid[name].params["nonce"] = nonce
60
+ if keyid is not None:
61
+ values.append(f"keyid={keyid}")
62
+ sid[name].params["keyid"] = keyid
63
+ if context is not None:
64
+ values.append(f"context={context}")
65
+ sid[name].params["context"] = context
66
+ if alg is not None:
67
+ values.append(f"alg={alg}")
68
+ sid[name].params["alg"] = alg
69
+
70
+ items.append(f'"@signature-params: {";".join(values)}"')
71
+ ser = "\n".join(items).encode("utf-8")
72
+ sigers = hab.sign(ser=ser, verfers=hab.kever.verfers, indexed=False)
73
+ return {"Signature-Input": f"{sid}"}, Unqualified(raw=sigers[0].raw)
74
+
75
+
76
+ class Unqualified:
77
+ def __init__(self, raw):
78
+ self._raw = raw
79
+
80
+ @property
81
+ def raw(self):
82
+ return self._raw
83
+
84
+ @property
85
+ def qb64(self):
86
+ return self.qb64b.decode("utf-8")
87
+
88
+ @property
89
+ def qb64b(self):
90
+ return encodeB64(self.raw)
@@ -0,0 +1,23 @@
1
+ """Health endpoint."""
2
+
3
+ import falcon
4
+ from keri.help import nowIso8601
5
+
6
+ import interop_verifier
7
+
8
+
9
+ class HealthEnd:
10
+ def __init__(self, cdb=None, hab=None, stats=None):
11
+ self.cdb = cdb
12
+ self.hab = hab
13
+ self.stats = stats
14
+
15
+ def on_get(self, req, resp):
16
+ resp.status = falcon.HTTP_OK
17
+ resp.media = {
18
+ "message": f"Health is okay. Time is {nowIso8601()}",
19
+ "version": interop_verifier.__version__,
20
+ "pre": self.hab.pre if self.hab else None,
21
+ "counts": self.cdb.getCounts() if self.cdb else {},
22
+ "stats": self.stats() if self.stats else {},
23
+ }
@@ -0,0 +1,97 @@
1
+ """Generic credential policy and webhook payload helpers."""
2
+
3
+ from keri import kering
4
+
5
+
6
+ class VerificationPolicy:
7
+ """Schema-agnostic verifier policy with optional config restrictions."""
8
+
9
+ def __init__(self, config=None):
10
+ config = config or {}
11
+ schemas = config.get("schemas") or {}
12
+ if isinstance(schemas, list):
13
+ schemas = {said: {"name": said} for said in schemas}
14
+ self.schemas = schemas
15
+ self.allow_unknown_schemas = bool(config.get("allowUnknownSchemas", True))
16
+ self.issuers = set(config.get("issuers") or [])
17
+
18
+ def validate(self, creder):
19
+ if self.schemas and creder.schema not in self.schemas and not self.allow_unknown_schemas:
20
+ raise kering.ValidationError(f"unsupported credential schema {creder.schema}")
21
+ if self.issuers and creder.issuer not in self.issuers:
22
+ raise kering.ValidationError(f"credential issuer {creder.issuer} is not allowed")
23
+
24
+ def schema_name(self, schema):
25
+ entry = self.schemas.get(schema)
26
+ if isinstance(entry, dict):
27
+ return entry.get("name") or schema
28
+ if isinstance(entry, str):
29
+ return entry
30
+ return schema
31
+
32
+
33
+ def credential_payload(reger, creder, policy):
34
+ attrs = dict(creder.sad.get("a") or {})
35
+ edges = creder.edge or creder.sad.get("e") or {}
36
+ data = {
37
+ "type": policy.schema_name(creder.schema),
38
+ "schema": creder.schema,
39
+ "issuer": creder.issuer,
40
+ "issueTimestamp": attrs.get("dt"),
41
+ "credential": creder.said,
42
+ "recipient": attrs.get("i"),
43
+ "attributes": attrs,
44
+ "edges": edges,
45
+ "rules": creder.sad.get("r"),
46
+ "chain": chain_summary(reger, creder),
47
+ }
48
+ return {key: value for key, value in data.items() if value is not None}
49
+
50
+
51
+ def revocation_payload(reger, creder, policy):
52
+ state = reger.tevers[creder.regi].vcState(creder.said)
53
+ return {
54
+ "type": policy.schema_name(creder.schema),
55
+ "schema": creder.schema,
56
+ "credential": creder.said,
57
+ "issuer": creder.issuer,
58
+ "revocationTimestamp": state.dt if state is not None else None,
59
+ }
60
+
61
+
62
+ def chain_summary(reger, creder):
63
+ seen = set()
64
+ out = []
65
+
66
+ def visit(current, label=None):
67
+ edges = current.edge or current.sad.get("e") or {}
68
+ if not isinstance(edges, dict):
69
+ return
70
+ for edge_label, edge in edges.items():
71
+ if not isinstance(edge, dict):
72
+ continue
73
+ said = edge.get("n")
74
+ if not said or said in seen:
75
+ continue
76
+ seen.add(said)
77
+ source = credential_by_said(reger, said)
78
+ out.append({
79
+ "label": label or edge_label,
80
+ "edge": edge_label,
81
+ "credential": said,
82
+ "schema": source.schema if source is not None else edge.get("s"),
83
+ "issuer": source.issuer if source is not None else None,
84
+ "operator": edge.get("o"),
85
+ })
86
+ if source is not None:
87
+ visit(source, edge_label)
88
+
89
+ visit(creder)
90
+ return out
91
+
92
+
93
+ def credential_by_said(reger, said):
94
+ try:
95
+ return reger.creds.get(keys=(said,))
96
+ except TypeError:
97
+ return reger.creds.get(said)
@@ -0,0 +1,270 @@
1
+ """KERIpy/Hio service setup for the generalized verifier."""
2
+
3
+ import json
4
+ import os
5
+ from importlib import resources
6
+
7
+ import falcon
8
+ from hio.base import doing
9
+ from hio.core import http
10
+ from hio.help import decking
11
+ from keri.app import habbing, indirecting, notifying, oobiing, storing
12
+ from keri.app.habbing import Hab, Habery
13
+ from keri import kering
14
+ from keri.core import eventing, parsing, routing, scheming
15
+ from keri.end import ending
16
+ from keri.peer import exchanging
17
+ from keri.vc import protocoling
18
+ from keri.vdr import verifying
19
+ from keri.vdr.eventing import Tevery
20
+ try:
21
+ from keri.vdr.eventing import Reger
22
+ except ImportError:
23
+ from keri.vdr.viring import Reger
24
+
25
+ from interop_verifier.core import basing, handling, httping, monitoring
26
+ from interop_verifier.core.credentials import TeveryCuery
27
+ from interop_verifier.core.policy import VerificationPolicy
28
+ from interop_verifier.core.verifying import VerificationAgent
29
+
30
+
31
+ def incept_if_new(hby: Habery, alias: str, incept_args: dict):
32
+ hab = hby.habByName(name=alias)
33
+ if hab is not None:
34
+ return hab
35
+ return hby.makeHab(name=alias, **inception_config(**incept_args))
36
+
37
+
38
+ def run_doers(doers, expire=0.0):
39
+ doist = doing.Doist(limit=expire, tock=0.03125, real=True)
40
+ doist.do(doers=doers)
41
+
42
+
43
+ def load_schemas(hby: Habery, paths):
44
+ for path in paths or []:
45
+ with open(path, "rb") as fp:
46
+ schemer = scheming.Schemer(raw=fp.read())
47
+ hby.db.schema.pin(keys=(schemer.said,), val=schemer)
48
+
49
+
50
+ def setupDoers(
51
+ hby: Habery,
52
+ hab: Hab,
53
+ alias: str,
54
+ http_port: int,
55
+ hook: str,
56
+ policy: VerificationPolicy,
57
+ timeout: int = 10,
58
+ retry: int = 3,
59
+ clear_escrows: bool = True,
60
+ head_dir_path: str | None = None,
61
+ ):
62
+ hbyDoer = habbing.HaberyDoer(habery=hby)
63
+ obl = oobiing.Oobiery(hby=hby)
64
+ doers = [hbyDoer, *obl.doers]
65
+ doers += setup(
66
+ hby,
67
+ hab,
68
+ alias=alias,
69
+ httpPort=http_port,
70
+ hook=hook,
71
+ policy=policy,
72
+ timeout=timeout,
73
+ retry=retry,
74
+ clear_escrows=clear_escrows,
75
+ headDirPath=head_dir_path,
76
+ )
77
+ return doers
78
+
79
+
80
+ def setup(
81
+ hby: Habery,
82
+ hab: Hab,
83
+ alias: str,
84
+ httpPort: int,
85
+ hook: str,
86
+ policy,
87
+ timeout=10,
88
+ retry=3,
89
+ clear_escrows=True,
90
+ headDirPath=None,
91
+ ):
92
+ cues = decking.Deck()
93
+ if hab is None:
94
+ raise ValueError(f"Identifier with alias '{alias}' does not exist")
95
+
96
+ app = falcon.App(middleware=httping.cors_middleware())
97
+ server = http.Server(port=httpPort, app=app)
98
+ httpServerDoer = http.ServerDoer(server=server)
99
+
100
+ reger = Reger(name=hab.name, db=hab.db, temp=False, headDirPath=headDirPath)
101
+ verifier = verifying.Verifier(hby=hby, reger=reger)
102
+ mbx = storing.Mailboxer(name=hby.name, headDirPath=headDirPath)
103
+ exc = exchanging.Exchanger(hby=hby, handlers=[])
104
+ rvy = routing.Revery(db=hby.db)
105
+ notifier = notifying.Notifier(hby=hby)
106
+ protocoling.loadHandlers(hby=hby, exc=exc, notifier=notifier)
107
+
108
+ kvy = eventing.Kevery(db=hby.db, lax=True, local=False, rvy=rvy)
109
+ kvy.registerReplyRoutes(router=rvy.rtr)
110
+ tvy = Tevery(reger=verifier.reger, db=hby.db, local=False)
111
+ tvy.registerReplyRoutes(router=rvy.rtr)
112
+
113
+ cdb = basing.CueBaser(name=hby.name, headDirPath=headDirPath)
114
+ if clear_escrows:
115
+ cdb.clearEscrows()
116
+ tc = TeveryCuery(cdb=cdb, reger=reger, cues=tvy.cues)
117
+ parser = parsing.Parser(framed=True, kvy=kvy, tvy=tvy, rvy=rvy, vry=verifier, exc=exc, version=kering.Vrsn_1_0)
118
+ comms = handling.Communicator(
119
+ hby=hby,
120
+ hab=hab,
121
+ cdb=cdb,
122
+ reger=reger,
123
+ hook=hook,
124
+ policy=policy,
125
+ timeout=timeout,
126
+ retry=retry,
127
+ )
128
+
129
+ httpEnd = CountingHttpEnd(rxbs=parser.ims, mbx=mbx)
130
+ app.add_route(
131
+ "/health",
132
+ monitoring.HealthEnd(
133
+ cdb=cdb,
134
+ hab=hab,
135
+ stats=lambda: verifier_stats(
136
+ parser=parser,
137
+ ingress=httpEnd,
138
+ hby=hby,
139
+ reger=reger,
140
+ notifier=notifier,
141
+ ),
142
+ ),
143
+ )
144
+ load_verifier_ends(app, hby=hby, hab=hab, httpPort=httpPort)
145
+ app.add_route("/", httpEnd)
146
+ agent = VerificationAgent(hab=hab, parser=parser, kvy=kvy, tvy=tvy, rvy=rvy, verifier=verifier, exc=exc, cues=cues)
147
+ return [
148
+ httpServerDoer,
149
+ comms,
150
+ tc,
151
+ *handling.loadHandlers(cdb=cdb, hby=hby, notifier=notifier, parser=parser),
152
+ agent,
153
+ ]
154
+
155
+
156
+ def load_verifier_ends(app, hby: Habery, hab: Hab, httpPort: int):
157
+ app.add_route('/end/{aid}/{role}', ending.PointEnd(hby=hby))
158
+ app.add_route('/loc', ending.LocationEnd(hby=hby))
159
+ app.add_route('/admin', ending.AdminEnd(hby=hby))
160
+
161
+ end = VerifierOOBIEnd(hby=hby, hab=hab, httpPort=httpPort)
162
+ app.add_route("/oobi", end)
163
+ app.add_route("/oobi/{aid}", end)
164
+ app.add_route("/oobi/{aid}/{role}", end)
165
+ app.add_route("/oobi/{aid}/{role}/{eid}", end)
166
+
167
+
168
+ class VerifierOOBIEnd(ending.OOBIEnd):
169
+ def __init__(self, hby: Habery, hab: Hab, httpPort: int):
170
+ super().__init__(hby=hby, default=hab.pre)
171
+ self.hab = hab
172
+ self.httpPort = httpPort
173
+
174
+ def on_get(self, req, rep, aid=None, role=None, eid=None):
175
+ aid = aid if aid is not None else self.default
176
+ if aid == self.hab.pre and (role is None or role == kering.Roles.controller) and (eid is None or eid == self.hab.pre):
177
+ if aid not in self.hby.kevers:
178
+ rep.status = falcon.HTTP_NOT_FOUND
179
+ return
180
+
181
+ kever = self.hby.kevers[aid]
182
+ if not self.hby.db.fullyWitnessed(kever.serder):
183
+ rep.status = falcon.HTTP_NOT_FOUND
184
+ return
185
+
186
+ url = f"http://127.0.0.1:{self.httpPort}"
187
+ msgs = bytearray()
188
+ msgs.extend(self.hab.replay(aid))
189
+ msgs.extend(self.hab.makeLocScheme(eid=self.hab.pre, scheme="http", url=url))
190
+ msgs.extend(self.hab.makeEndRole(self.hab.pre, kering.Roles.controller, True))
191
+
192
+ rep.status = falcon.HTTP_200
193
+ rep.set_header(ending.OOBI_AID_HEADER, aid)
194
+ rep.content_type = "application/cesr"
195
+ rep.data = bytes(msgs)
196
+ return
197
+
198
+ return super().on_get(req, rep, aid=aid, role=role, eid=eid)
199
+
200
+
201
+ class CountingHttpEnd(indirecting.HttpEnd):
202
+ def __init__(self, *args, **kwargs):
203
+ self.posts = 0
204
+ self.puts = 0
205
+ self.lastPutSize = 0
206
+ self.lastPostSize = 0
207
+ super().__init__(*args, **kwargs)
208
+
209
+ def on_post(self, req, rep):
210
+ self.posts += 1
211
+ self.lastPostSize = int(req.get_header("Content-Length", required=False) or 0)
212
+ return super().on_post(req, rep)
213
+
214
+ def on_put(self, req, rep):
215
+ self.puts += 1
216
+ body = req.bounded_stream.read()
217
+ self.lastPutSize = len(body)
218
+ self.rxbs.extend(body)
219
+
220
+ rep.set_header('Content-Type', "application/json")
221
+ rep.status = falcon.HTTP_204
222
+
223
+
224
+ def count_suber(suber):
225
+ if suber is None:
226
+ return 0
227
+ if hasattr(suber, "cntAll"):
228
+ return suber.cntAll()
229
+ if hasattr(suber, "getItemIter"):
230
+ return len(list(suber.getItemIter()))
231
+ return len(list(suber.getTopItemIter()))
232
+
233
+
234
+ def verifier_stats(parser, ingress, hby, reger, notifier):
235
+ return {
236
+ "httpPosts": ingress.posts,
237
+ "httpPuts": ingress.puts,
238
+ "lastPostSize": ingress.lastPostSize,
239
+ "lastPutSize": ingress.lastPutSize,
240
+ "parserBacklog": len(parser.ims),
241
+ "exns": count_suber(hby.db.exns),
242
+ "exnEscrows": count_suber(hby.db.epse),
243
+ "notices": count_suber(notifier.noter.notes),
244
+ "savedCredentials": count_suber(reger.saved),
245
+ "missingSchemaEscrows": count_suber(reger.mse),
246
+ "missingRegistryEscrows": count_suber(reger.mre),
247
+ "missingChainEscrows": count_suber(reger.mce),
248
+ }
249
+
250
+
251
+ def inception_config(name=None, base=None, alias=None, bran=None, incept_file=None, config_dir=None):
252
+ if incept_file is None:
253
+ raw = resources.files("interop_verifier.data").joinpath("interop-verifier-incept-no-wits.json").read_text()
254
+ config = json.loads(raw)
255
+ else:
256
+ path = incept_file if config_dir is None else os.path.join(config_dir, incept_file)
257
+ with open(path, "r", encoding="utf-8") as fp:
258
+ config = json.load(fp)
259
+ return {
260
+ "transferable": config.get("transferable", True),
261
+ "wits": config.get("wits", []),
262
+ "toad": config.get("toad", 0),
263
+ "icount": config.get("icount", 1),
264
+ "ncount": config.get("ncount", 1),
265
+ "isith": config.get("isith", "1"),
266
+ "nsith": config.get("nsith", "1"),
267
+ "delpre": config.get("delpre"),
268
+ "estOnly": config.get("estOnly", config.get("est_only", False)),
269
+ "data": config.get("data"),
270
+ }
@@ -0,0 +1,40 @@
1
+ """Direct-mode parser and escrow processing doer."""
2
+
3
+ from hio.base import doing
4
+ from hio.help import decking
5
+
6
+
7
+ class VerificationAgent(doing.DoDoer):
8
+ """Runs KERIpy parser, KEL/TEL/reply/verifier, and exchange escrows."""
9
+
10
+ def __init__(self, hab, parser, kvy, tvy, rvy, verifier, exc, cues=None, **opts):
11
+ self.hab = hab
12
+ self.parser = parser
13
+ self.kvy = kvy
14
+ self.tvy = tvy
15
+ self.rvy = rvy
16
+ self.verifier = verifier
17
+ self.exc = exc
18
+ self.cues = cues if cues is not None else decking.Deck()
19
+ super().__init__(doers=[doing.doify(self.msgDo), doing.doify(self.escrowDo)], **opts)
20
+
21
+ def msgDo(self, tymth=None, tock=0.0, **opts):
22
+ self.wind(tymth)
23
+ self.tock = tock
24
+ _ = yield self.tock
25
+ done = yield from self.parser.parsator(local=False)
26
+ return done
27
+
28
+ def escrowDo(self, tymth=None, tock=0.0, **opts):
29
+ self.wind(tymth)
30
+ self.tock = tock
31
+ _ = yield self.tock
32
+ while True:
33
+ self.kvy.processEscrows()
34
+ self.rvy.processEscrowReply()
35
+ if self.tvy is not None:
36
+ self.tvy.processEscrows()
37
+ if self.verifier is not None:
38
+ self.verifier.processEscrows()
39
+ self.exc.processEscrow()
40
+ yield
@@ -0,0 +1 @@
1
+ """Package data for interop-verifier."""
@@ -0,0 +1,9 @@
1
+ {
2
+ "transferable": true,
3
+ "wits": [],
4
+ "toad": 0,
5
+ "icount": 1,
6
+ "ncount": 1,
7
+ "isith": "1",
8
+ "nsith": "1"
9
+ }
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: interop-verifier
3
+ Version: 1.0.0
4
+ Summary: Generalized Sally-style KERIpy verifier for ACDC interop tests
5
+ Project-URL: Homepage, https://github.com/kentbull/interop-verifier
6
+ Project-URL: Repository, https://github.com/kentbull/interop-verifier
7
+ Project-URL: Issues, https://github.com/kentbull/interop-verifier/issues
8
+ Author-email: Kent Bull <kent@kentbull.com>
9
+ Keywords: acdc,cesr,ipex,keri,verifier
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Requires-Python: >=3.12.6
17
+ Requires-Dist: falcon==4.0.2
18
+ Requires-Dist: hio<0.7.0,>=0.6.14
19
+ Requires-Dist: http-sfv==0.9.9
20
+ Requires-Dist: keri<1.3.0,>=1.2.12
21
+ Description-Content-Type: text/markdown
22
+
23
+ # interop-verifier
24
+
25
+ `interop-verifier` is a generalized Sally-style KERIpy verifier used by the
26
+ `keri-ts` ACDC interop tests. It receives normal CESR/IPEX credential
27
+ presentations, verifies them with KERIpy VDR state, and posts generic webhook
28
+ payloads without hardcoding the vLEI credential chain.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ uv tool install interop-verifier
34
+ ```
35
+
36
+ ## Run from PyPI
37
+
38
+ ```sh
39
+ interop-verifier start \
40
+ --name verifier \
41
+ --base "" \
42
+ --alias verifier \
43
+ --http 9723 \
44
+ --web-hook http://127.0.0.1:9923/
45
+ ```
46
+
47
+ The verifier uses a bundled no-witness inception config by default. Pass
48
+ `--incept-file` to use a custom KERIpy-style inception JSON file.
49
+
50
+ ## Run with Docker
51
+
52
+ ```sh
53
+ docker run --rm -p 9723:9723 kentbull/interop-verifier:1.0.0 start \
54
+ --name verifier \
55
+ --head-dir /data \
56
+ --alias verifier \
57
+ --http 9723 \
58
+ --web-hook http://host.docker.internal:9923/
59
+ ```
60
+
61
+ Mount `/data` to persist verifier key state.
62
+
63
+ ## Interfaces
64
+
65
+ The verifier serves:
66
+
67
+ - `GET /health` for liveness and queue counts.
68
+ - `GET /oobi/{aid}/controller` for KERI controller OOBI resolution.
69
+ - `PUT /` and `POST /` for CESR/KERI ingress.
70
+ - Sally-style signed webhook callbacks for verified credentials.
71
+
72
+ ## Development
73
+
74
+ ```sh
75
+ make sync
76
+ make check
77
+ make build
78
+ make docker-build
79
+ ```
@@ -0,0 +1,18 @@
1
+ interop_verifier/__init__.py,sha256=IH51cY922nVUxZCp0wbn7IZ0Ynattbbdj9wE32PK3N8,86
2
+ interop_verifier/app/__init__.py,sha256=xv9zdJqymAw5KZoy3TU5tiwHRUARg1bfgAimDAs5ZB4,40
3
+ interop_verifier/app/cli.py,sha256=kI97NvlH70mEknvQzzDb_DM7P4j6lBq5fn5ycICw5DE,3805
4
+ interop_verifier/core/__init__.py,sha256=A_3HQyEpftQNnfmvU4KTwqVziB_447iREj_VLOeJlM8,32
5
+ interop_verifier/core/basing.py,sha256=Ra8hdD9t5ZjQJKFE0_qXJ1FCBHpbrpWn_RIA1z0JA9Y,1897
6
+ interop_verifier/core/credentials.py,sha256=s1gPcnrApQOUJ-MyKKLhg9gnG8Pq3lB3BFkD_lHo9Xk,1105
7
+ interop_verifier/core/handling.py,sha256=A8VLGrsHja6z5EnsG_DRGK6Fk2iI0U0n3Sj6oVwHeoQ,7700
8
+ interop_verifier/core/httping.py,sha256=bhMb2Cga7SCev_-mNC_9wmB8denHjR-gSrpkWW-fzCw,2572
9
+ interop_verifier/core/monitoring.py,sha256=BEegMPIOPraNFPutDnRK_SphQqywuJPzw1z2_7ryIKE,643
10
+ interop_verifier/core/policy.py,sha256=ZNrxV8531FCt-_emaIt89rmvQ9yyoJBIR_k3IREQnkg,3295
11
+ interop_verifier/core/serving.py,sha256=QQCjAdrXLQGtPz_bkkhqMN9M8UGff-q1WklOk_HkjXM,8832
12
+ interop_verifier/core/verifying.py,sha256=PtJjUkBn3_LnEvyWVNASIATQ0uvSlsy-ZVrhzp0PRho,1316
13
+ interop_verifier/data/__init__.py,sha256=lrSPAc7jVZLKXQaTv3ZuM1FfvDBE3oatR_ueOTkkXM0,41
14
+ interop_verifier/data/interop-verifier-incept-no-wits.json,sha256=iXEZzfPGPOhVjOSHEjzosUfdkQGvOtGcKOtF_Qb9LoY,116
15
+ interop_verifier-1.0.0.dist-info/METADATA,sha256=uQSkdH1pk1K2xeZdH-AOW6vaDG9cCQekeOACMmIB9Tw,2170
16
+ interop_verifier-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ interop_verifier-1.0.0.dist-info/entry_points.txt,sha256=cDYQJ2GCQvvzpEGVUEH7kXcZ7UPLYeMbJa_s_AY2qfo,67
18
+ interop_verifier-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ interop-verifier = interop_verifier.app.cli:main