Skip to content

Commit

Permalink
Challenge response flow (WebOfTrust#111)
Browse files Browse the repository at this point in the history
* Added necessary endpoints to complete the challenge response flow.

Signed-off-by: pfeairheller <[email protected]>

* Additional test coverage for PR

Signed-off-by: pfeairheller <[email protected]>

---------

Signed-off-by: pfeairheller <[email protected]>
  • Loading branch information
pfeairheller authored Sep 29, 2023
1 parent 42c6288 commit 2acb65b
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 58 deletions.
83 changes: 76 additions & 7 deletions src/keria/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def loadEnds(app, agency, authn):
app.add_route("/challenges", chaEnd)
chaResEnd = ChallengeResourceEnd()
app.add_route("/challenges/{name}", chaResEnd)
chaVerResEnd = ChallengeVerifyResourceEnd()
app.add_route("/challenges/{name}/verify/{source}", chaVerResEnd)

contactColEnd = ContactCollectionEnd()
app.add_route("/contacts", contactColEnd)
Expand Down Expand Up @@ -819,7 +821,7 @@ def on_get(req, rep):
rep: falcon.Response HTTP response
---
summary: Get list of agent identifiers
summary: Get random list of words for a 2 factor auth challenge
description: Get the list of identifiers associated with this agent
tags:
- Challenge/Response
Expand All @@ -832,7 +834,7 @@ def on_get(req, rep):
required: false
responses:
200:
description: An array of Identifier key state information
description: An array of random words
content:
application/json:
schema:
Expand Down Expand Up @@ -919,14 +921,79 @@ def on_post(req, rep, name):

rep.status = falcon.HTTP_202


class ChallengeVerifyResourceEnd:
""" Resource for Challenge/Response Verification Endpoints """

@staticmethod
def on_post(req, rep, name, source):
""" Challenge POST endpoint
Parameters:
req: falcon.Request HTTP request
rep: falcon.Response HTTP response
name: human readable name of identifier to use to sign the challange/response
source: qb64 AID of of source of signed response to verify
---
summary: Sign challange message and forward to peer identfiier
description: Sign a challenge word list received out of bands and send `exn` peer to peer message
to recipient
tags:
- Challenge/Response
parameters:
- in: path
name: name
schema:
type: string
required: true
description: Human readable alias for the identifier to create
requestBody:
required: true
content:
application/json:
schema:
description: Challenge response
properties:
recipient:
type: string
description: human readable alias recipient identifier to send signed challenge to
words:
type: array
description: challenge in form of word list
items:
type: string
responses:
202:
description: Success submission of signed challenge/response
"""
agent = req.context.agent
hab = agent.hby.habByName(name)
if hab is None:
raise falcon.HTTPNotFound(description="no matching Hab for alias {name}")

body = req.get_media()
words = httping.getRequiredParam(body, "words")
if source not in agent.hby.kevers:
raise falcon.HTTPNotFound(description=f"challenge response source={source} not found")

meta = dict(words=words)
op = agent.monitor.submit(source, longrunning.OpTypes.challenge, metadata=meta)
rep.status = falcon.HTTP_202
rep.content_type = "application/json"
rep.data = op.to_json().encode("utf-8")

rep.status = falcon.HTTP_202

@staticmethod
def on_put(req, rep, name):
def on_put(req, rep, name, source):
""" Challenge PUT accept endpoint
Parameters:
req: falcon.Request HTTP request
rep: falcon.Response HTTP response
name: human readable name of identifier to use to sign the challange/response
source: qb64 AID of of source of signed response to verify
---
summary: Mark challenge response exn message as signed
Expand Down Expand Up @@ -962,16 +1029,18 @@ def on_put(req, rep, name):
agent = req.context.agent
hab = agent.hby.habByName(name)
if hab is None:
raise falcon.HTTPBadRequest(description="no matching Hab for alias {name}")
raise falcon.HTTPNotFound(description="no matching Hab for alias {name}")

body = req.get_media()
if "aid" not in body or "said" not in body:
if "said" not in body:
raise falcon.HTTPBadRequest(description="challenge response acceptance requires 'aid' and 'said'")

aid = body["aid"]
if source not in agent.hby.kevers:
raise falcon.HTTPNotFound(description=f"challenge response source={source} not found")

said = body["said"]
saider = coring.Saider(qb64=said)
agent.hby.db.chas.add(keys=(aid,), val=saider)
agent.hby.db.chas.add(keys=(source,), val=saider)

rep.status = falcon.HTTP_202

Expand Down
30 changes: 25 additions & 5 deletions src/keria/core/longrunning.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
from keri.help import helping

# long running operationt types
Typeage = namedtuple("Tierage", 'oobi witness delegation group query registry credential endrole done')
Typeage = namedtuple("Tierage", 'oobi witness delegation group query registry credential endrole challenge done')

OpTypes = Typeage(oobi="oobi", witness='witness', delegation='delegation', group='group', query='query',
registry='registry', credential='credential', endrole='endrole', done='done')
registry='registry', credential='credential', endrole='endrole', challenge='challenge', done='done')


@dataclass_json
Expand Down Expand Up @@ -330,6 +330,29 @@ def status(self, op):
else:
operation.done = False

elif op.type in (OpTypes.challenge,):
if op.oid not in self.hby.kevers:
operation.done = False

if "words" not in op.metadata:
raise kering.ValidationError(
f"invalid long running {op.type} operaiton, metadata missing 'ced' field")

found = False
words = op.metadata["words"]
saiders = self.hby.db.reps.get(keys=(op.oid,))
for saider in saiders:
exn = self.hby.db.exns.get(keys=(saider.qb64,))
if words == exn.ked['a']['words']:
found = True
break

if found:
operation.done = True
operation.response = dict(exn=exn.ked)
else:
operation.done = False

elif op.type in (OpTypes.done, ):
operation.done = True
operation.response = op.metadata["response"]
Expand All @@ -345,9 +368,6 @@ def status(self, op):
class OperationResourceEnd:
""" Single Resource REST endpoint for long running operations
Attributes:
monitor(Monitor): long running operation monitor
"""

@staticmethod
Expand Down
43 changes: 38 additions & 5 deletions tests/app/test_aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,8 @@ def test_challenge_ends(helpers):
app.add_route("/challenges", chaEnd)
chaResEnd = aiding.ChallengeResourceEnd()
app.add_route("/challenges/{name}", chaResEnd)
chaVerResEnd = aiding.ChallengeVerifyResourceEnd()
app.add_route("/challenges/{name}/verify/{source}", chaVerResEnd)

client = testing.TestClient(app)

Expand Down Expand Up @@ -895,19 +897,50 @@ def test_challenge_ends(helpers):
assert result.status == falcon.HTTP_202

data = dict()
data["aid"] = "Eo6MekLECO_ZprzHwfi7wG2ubOt2DWKZQcMZvTbenBNU"
b = json.dumps(data).encode("utf-8")
result = client.simulate_put(path="/challenges/henk", body=b)
assert result.status == falcon.HTTP_400 # Missing recipient
result = client.simulate_put(path="/challenges/henk/verify/Eo6MekLECO_ZprzHwfi7wG2ubOt2DWKZQcMZvTbenBNU", body=b)
assert result.status == falcon.HTTP_404 # Missing recipient

b = json.dumps(data).encode("utf-8")
result = client.simulate_put(path="/challenges/pal", body=b)
result = client.simulate_put(path=f"/challenges/pal/verify/{aid['i']}", body=b)
assert result.status == falcon.HTTP_400 # Missing said

data["said"] = exn.said
b = json.dumps(data).encode("utf-8")
result = client.simulate_put(path="/challenges/pal", body=b)
result = client.simulate_put(path=f"/challenges/pal/verify/EFt8G8gkCJ71e4amQaRUYss0BDK4pUpzKelEIr3yZ1D0",
body=b)
assert result.status == falcon.HTTP_404 # Missing said

result = client.simulate_put(path=f"/challenges/pal/verify/{aid['i']}", body=b)
assert result.status == falcon.HTTP_202

data = dict(
words=words,
said=exn.said
)
b = json.dumps(data).encode("utf-8")
result = client.simulate_post(path=f"/challenges/henk/verify/{aid['i']}", body=b)
assert result.status_code == 404

b = json.dumps(data).encode("utf-8")
result = client.simulate_post(path=f"/challenges/pal/verify/EFt8G8gkCJ71e4amQaRUYss0BDK4pUpzKelEIr3yZ1D0",
body=b)
assert result.status_code == 404

b = json.dumps(data).encode("utf-8")
result = client.simulate_post(path=f"/challenges/pal/verify/{aid['i']}", body=b)
assert result.status == falcon.HTTP_202
op = result.json
assert op["done"] is False

# Set the signed result to True so it verifies
agent.hby.db.reps.add(keys=(aid['i'],), val=exn.saider)
agent.hby.db.exns.pin(keys=(exn.said,), val=exn)

result = client.simulate_post(path=f"/challenges/pal/verify/{aid['i']}", body=b)
assert result.status == falcon.HTTP_202
op = result.json
assert op["done"] is True


def test_contact_ends(helpers):
Expand Down
Loading

0 comments on commit 2acb65b

Please sign in to comment.