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.
- interop_verifier/__init__.py +3 -0
- interop_verifier/app/__init__.py +1 -0
- interop_verifier/app/cli.py +107 -0
- interop_verifier/core/__init__.py +1 -0
- interop_verifier/core/basing.py +54 -0
- interop_verifier/core/credentials.py +32 -0
- interop_verifier/core/handling.py +208 -0
- interop_verifier/core/httping.py +90 -0
- interop_verifier/core/monitoring.py +23 -0
- interop_verifier/core/policy.py +97 -0
- interop_verifier/core/serving.py +270 -0
- interop_verifier/core/verifying.py +40 -0
- interop_verifier/data/__init__.py +1 -0
- interop_verifier/data/interop-verifier-incept-no-wits.json +9 -0
- interop_verifier-1.0.0.dist-info/METADATA +79 -0
- interop_verifier-1.0.0.dist-info/RECORD +18 -0
- interop_verifier-1.0.0.dist-info/WHEEL +4 -0
- interop_verifier-1.0.0.dist-info/entry_points.txt +2 -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,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,,
|