Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/signify/app/clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,27 @@ def __init__(self, passcode, url=None, boot_url=None, tier=Tiers.low, extern_mod
self.agent = None
self.authn = None
self.base = None
self._booted_agent = None

self.ctrl = authing.Controller(bran=self.bran, tier=self.tier)
self.url = url
self.boot_url = boot_url

def _cache_booted_agent(self, state):
"""Cache the agent state returned by ``/boot`` for first-connect checks."""
try:
self._booted_agent = authing.Agent(state=state)
except (KeyError, kering.ValidationError) as ex:
raise kering.AuthNError(f"invalid agent state from boot response: {ex}") from ex

def _require_booted_agent_match(self):
"""Ensure first-connect approval targets the agent returned by ``/boot``."""
if self._booted_agent is None:
return

if self.agent.pre != self._booted_agent.pre or self.agent.said != self._booted_agent.said:
raise kering.ConfigurationError("booted agent does not match connected agent state")

def boot(self) -> dict:
"""Create the remote cloud agent delegated to this controller AID."""
evt, siger = self.ctrl.event()
Expand All @@ -93,6 +109,7 @@ def boot(self) -> dict:
body = res.json()
except requests.exceptions.JSONDecodeError as ex:
raise kering.AuthNError(f"invalid response from server: {ex}") from ex
self._cache_booted_agent(body)
return body

def connect(self, url=None):
Expand Down Expand Up @@ -124,7 +141,9 @@ def connect(self, url=None):
raise kering.ConfigurationError("commitment to controller AID missing in agent inception event")

if self.ctrl.serder.sn == 0:
self._require_booted_agent_match()
self.approveDelegation()
self._booted_agent = None

self.authn = authing.Authenticater(agent=self.agent, ctrl=self.ctrl)
self.session.auth = SignifyAuth(self.authn)
Expand Down
81 changes: 78 additions & 3 deletions tests/app/test_clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
from mockito import mock, patch, unstub, verify, verifyNoUnwantedInteractions, expect, ANY


def make_agent_state(pre="agent_prefix", said=None, delpre="a prefix"):
said = pre if said is None else said
return {
"i": pre,
"s": "0",
"d": said,
"di": delpre,
"k": ["DMZh_y-H5C3cSbZZST-fqnsmdNTReZxIh0t2xSTOJQ8a"],
}


def test_signify_client_defaults(make_signify_client):
from signify.app.clienting import SignifyClient
patch(SignifyClient, 'connect', lambda: None)
Expand Down Expand Up @@ -41,13 +52,35 @@ def test_signify_client_bad_passcode_length():
from signify.app.clienting import SignifyClient
SignifyClient(passcode='too short')


def test_signify_client_boot_caches_booted_agent(make_signify_client):
import requests

client = make_signify_client(boot_url='http://boot.example')

body = make_agent_state(pre="booted_agent", said="booted_said", delpre=client.controller)
response = mock({'status_code': requests.codes.accepted}, spec=requests.Response, strict=True)
expect(response, times=1).json().thenReturn(body)
expect(requests, times=1).post(url='http://boot.example/boot', json=ANY).thenReturn(response)

out = client.boot()

assert out == body
assert client._booted_agent is not None
assert client._booted_agent.pre == body["i"]
assert client._booted_agent.said == body["d"]

verifyNoUnwantedInteractions()
unstub()

def test_signify_client_connect_no_delegation(make_signify_client, make_mock_session):
from signify.core import authing
from keri.core.coring import Tiers
mock_init_controller = mock(spec=authing.Controller, strict=True)
expect(authing, times=1).Controller(bran='abcdefghijklmnop01234', tier=Tiers.low).thenReturn(mock_init_controller)

client = make_signify_client()
client._booted_agent = authing.Agent(make_agent_state(pre="stale_boot_agent", said="stale_boot_said"))

import requests
mock_session = make_mock_session()
Expand All @@ -58,7 +91,7 @@ def test_signify_client_connect_no_delegation(make_signify_client, make_mock_ses
expect(client, times=1).states().thenReturn(mock_state)

from signify.core import authing
mock_agent = mock({'delpre': 'a prefix'}, spec=authing.Agent, strict=True)
mock_agent = mock({'delpre': 'a prefix', 'pre': 'connected_agent', 'said': 'connected_said'}, spec=authing.Agent, strict=True)
expect(authing, times=1).Agent(state=mock_state.agent).thenReturn(mock_agent)

from keri.core import serdering
Expand All @@ -83,6 +116,7 @@ def test_signify_client_connect_no_delegation(make_signify_client, make_mock_ses
client.connect('http://example.com')

assert client.pidx == mock_state.pidx
assert client._booted_agent.pre == "stale_boot_agent"
assert client.session.auth == mock_signify_auth #type: ignore
assert client.session.hooks == {'response': mock_authenticator.verify} #type: ignore

Expand All @@ -97,6 +131,7 @@ def test_signify_client_connect_delegation(make_signify_client, make_mock_sessio
expect(authing, times=1).Controller(bran='abcdefghijklmnop01234', tier=Tiers.low).thenReturn(mock_init_controller)

client = make_signify_client()
client._booted_agent = authing.Agent(make_agent_state(pre="booted_agent", said="booted_said"))

# setup for client.connect()
import requests
Expand All @@ -108,7 +143,7 @@ def test_signify_client_connect_delegation(make_signify_client, make_mock_sessio
expect(client, times=1).states().thenReturn(mock_state)

from signify.core import authing
mock_agent = mock({'delpre': 'a prefix'}, spec=authing.Agent, strict=True)
mock_agent = mock({'delpre': 'a prefix', 'pre': 'booted_agent', 'said': 'booted_said'}, spec=authing.Agent, strict=True)
expect(authing, times=1).Agent(state=mock_state.agent).thenReturn(mock_agent)

from keri.core import serdering
Expand All @@ -134,6 +169,7 @@ def test_signify_client_connect_delegation(make_signify_client, make_mock_sessio
expect(clienting, times=1).SignifyAuth(mock_authenticator).thenReturn(mock_signify_auth)

client.connect('http://example.com')
assert client._booted_agent is None

verifyNoUnwantedInteractions()
unstub()
Expand Down Expand Up @@ -163,7 +199,7 @@ def test_signify_client_connect_bad_delegation():
expect(client, times=1).states().thenReturn(mock_state)

from signify.core import authing
mock_agent = mock({'delpre': 'a prefix'}, spec=authing.Agent, strict=True)
mock_agent = mock({'delpre': 'a prefix', 'pre': 'connected_agent', 'said': 'connected_said'}, spec=authing.Agent, strict=True)
expect(authing, times=1).Agent(state=mock_state.agent).thenReturn(mock_agent)

from keri.core import serdering
Expand All @@ -184,6 +220,45 @@ def test_signify_client_connect_bad_delegation():
verifyNoUnwantedInteractions()
unstub()


def test_signify_client_connect_rejects_mismatched_booted_agent(make_signify_client, make_mock_session):
from signify.core import authing
from keri.core.coring import Tiers
mock_init_controller = mock(spec=authing.Controller, strict=True)
expect(authing, times=1).Controller(bran='abcdefghijklmnop01234', tier=Tiers.low).thenReturn(mock_init_controller)

client = make_signify_client()
client._booted_agent = authing.Agent(make_agent_state(pre="booted_agent", said="booted_said"))

import requests
mock_session = make_mock_session()
expect(requests, times=1).Session().thenReturn(mock_session)

from signify.signifying import SignifyState
mock_state = mock({'pidx': 0, 'agent': 'agent info', 'controller': 'controller info'}, spec=SignifyState, strict=True)
expect(client, times=1).states().thenReturn(mock_state)

mock_agent = mock({'delpre': 'a prefix', 'pre': 'connected_agent', 'said': 'connected_said'}, spec=authing.Agent, strict=True)
expect(authing, times=1).Agent(state=mock_state.agent).thenReturn(mock_agent)

from keri.core import serdering
mock_serder = mock({'sn': 0}, spec=serdering.Serder, strict=True)
from keri.core import signing
mock_salter = mock(spec=signing.Salter, strict=True)
mock_controller = mock({'pre': 'a prefix', 'salter': mock_salter, 'serder': mock_serder}, spec=authing.Controller, strict=True)
expect(authing, times=1).Controller(bran='abcdefghijklmnop01234', tier=Tiers.low, state=mock_state.controller).thenReturn(mock_controller)

from signify.core import keeping
mock_manager = mock(spec=keeping.Manager, strict=True)
expect(keeping, times=1).Manager(salter=mock_salter, extern_modules=None).thenReturn(mock_manager)

from keri.kering import ConfigurationError
with pytest.raises(ConfigurationError, match='booted agent does not match connected agent state'):
client.connect('http://example.com')

verifyNoUnwantedInteractions()
unstub()

def test_signify_client_approve_delegation():
from signify.core import authing
from keri.core.coring import Tiers
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def boot_client_manually(client: SignifyClient, live_stack) -> dict:
timeout=30,
)
response.raise_for_status()
return response.json()
body = response.json()
client._cache_booted_agent(body)
return body


def connect_client(
Expand Down Expand Up @@ -109,6 +111,7 @@ def connect_client(
else:
raise ValueError(f"unsupported boot_mode={boot_mode}")
assert isinstance(body, dict)
client._integration_boot_response = body
client.connect()
assert client.agent is not None
client._integration_live_stack = live_stack
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/test_provisioning_and_identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def test_provision_agent_and_connect(client_factory):
assert client.agent.pre
assert client.agent.pre != client.controller
assert client.agent.delpre == client.controller
assert client._integration_boot_response["i"] == client.agent.pre
assert client._integration_boot_response["d"] == client.agent.said
assert client.session is not None
assert client.session.auth is not None

Expand All @@ -58,6 +60,8 @@ def test_manual_agent_boot_and_connect(client_factory):
assert client.controller == client.ctrl.pre
assert client.agent.pre
assert client.agent.delpre == client.controller
assert client._integration_boot_response["i"] == client.agent.pre
assert client._integration_boot_response["d"] == client.agent.said
assert client.session is not None


Expand Down
Loading