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
15 changes: 11 additions & 4 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ env:
# Keep shared stack versions in one place so compatibility upgrades are
# deliberate and reviewable instead of being buried inside individual steps.
PYTHON_VERSION: '3.12.6'
KERIPY_REF: '1.2.12'
KERIPY_REF: '4ee02c0213770d25a0114fe7ebd7ab4ab5500cde'
KERIA_REF: '9e2461550f373ad7bdbe7eebeaceac689cb15397'
VLEI_REF: '1.0.2'
VLEI_REF: 'f514b9431c5f965b5f7f64a8693e19df2f181564'

jobs:
unit:
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:
signifypy/venv
keria/venv
vLEI/venv
key: ${{ runner.os }}-py${{ env.PYTHON_VERSION }}-live-venvs-${{ hashFiles('signifypy/pyproject.toml', 'signifypy/.github/constraints/integration.txt', 'keripy/requirements.txt', 'keripy/setup.py', 'keria/pyproject.toml', 'vLEI/requirements.txt', 'vLEI/setup.py') }}
key: ${{ runner.os }}-py${{ env.PYTHON_VERSION }}-live-venvs-${{ env.KERIPY_REF }}-${{ env.KERIA_REF }}-${{ env.VLEI_REF }}-${{ hashFiles('signifypy/pyproject.toml', 'signifypy/.github/constraints/integration.txt', 'keripy/requirements.txt', 'keripy/setup.py', 'keria/pyproject.toml', 'vLEI/requirements.txt', 'vLEI/setup.py') }}

- name: Stage 5 - Build live-stack virtualenvs
if: steps.cache-live-venvs.outputs.cache-hit != 'true'
Expand All @@ -175,9 +175,9 @@ jobs:
# SignifyPy's test runner installs local KERIpy so the client library
# and the launched services resolve against the same source version.
./venv/bin/python -m pip install --upgrade pip setuptools wheel
make sync
./venv/bin/python -m pip install -c .github/constraints/integration.txt hio==0.6.14
./venv/bin/python -m pip install -c .github/constraints/integration.txt -e ../keripy
make sync

- name: Stage 7 - Install KERIA runtime
if: steps.cache-live-venvs.outputs.cache-hit != 'true'
Expand All @@ -203,6 +203,13 @@ jobs:

- name: Stage 9 - Run integration tests
working-directory: signifypy
env:
SIGNIFYPY_INTEGRATION_KERIPY_ROOT: ../keripy
SIGNIFYPY_INTEGRATION_KERIPY_REF: ${{ env.KERIPY_REF }}
SIGNIFYPY_INTEGRATION_KERIA_ROOT: ../keria
SIGNIFYPY_INTEGRATION_KERIA_REF: ${{ env.KERIA_REF }}
SIGNIFYPY_INTEGRATION_VLEI_ROOT: ../vLEI
SIGNIFYPY_INTEGRATION_VLEI_REF: ${{ env.VLEI_REF }}
run: |
# Run the full live suite with explicit parallelism so the CI
# contract is visible in the workflow rather than hidden behind a
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,5 @@ dmypy.json
.vscode

# temp files
.tmp
.tmp
.integration-deps/
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
UV ?= uv
UV_ENV = UV_PROJECT_ENVIRONMENT=venv
UV_CACHE = UV_CACHE_DIR=$(CURDIR)/.uv-cache
INTEGRATION_DEPS_ROOT ?= .integration-deps
INTEGRATION_POLL_INTERVAL ?= 0.1
INTEGRATION_HEAVY_POLL_INTERVAL ?= 0.25
INTEGRATION_PORT_POLL_INTERVAL ?= 0.1
INTEGRATION_WORKERS ?= 2
INTEGRATION_DIST ?= loadscope
INTEGRATION_TARGETS ?= tests/integration

.PHONY: help sync test test-fast test-ci test-integration test-integration-ci test-integration-parallel test-integration-parallel-ci build dist-check release-patch release-minor release-major release-bump docs clean guard-clean-worktree
.PHONY: help sync sync-integration-deps sync-integration test test-fast test-ci test-integration test-integration-ci test-integration-parallel test-integration-parallel-ci build dist-check release-patch release-minor release-major release-bump docs clean guard-clean-worktree

help: ## Show available maintainer tasks
@awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_-]+:.*## / {printf "%-18s %s\n", $$1, $$2}' $(MAKEFILE_LIST)

sync: ## Sync the local maintainer environment into ./venv
@$(UV_CACHE) $(UV_ENV) $(UV) sync --group dev

sync-integration-deps: sync ## Sync pinned local source deps for live integration tests
@./venv/bin/python scripts/sync_integration_deps.py --deps-root "$(INTEGRATION_DEPS_ROOT)"

sync-integration: sync-integration-deps ## Alias for syncing the live integration runtime

test: ## Run the fast unit/contract suite
@./venv/bin/python -m pytest -q $(PYTEST_ARGS) tests/app tests/core tests/peer

Expand Down
95 changes: 95 additions & 0 deletions scripts/sync_integration_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python
"""Synchronize pinned source repos and virtualenvs for live integration tests."""

from __future__ import annotations

import argparse
import subprocess
import sys
from pathlib import Path

SIGNIFYPY_ROOT = Path(__file__).resolve().parents[1]
CONSTRAINTS = SIGNIFYPY_ROOT / ".github" / "constraints" / "integration.txt"
SIGNIFYPY_VENV_PYTHON = SIGNIFYPY_ROOT / "venv" / "bin" / "python"

sys.path.insert(0, str(SIGNIFYPY_ROOT))
from tests.integration.dependencies import INTEGRATION_DEPENDENCIES, KERIA, KERIPY, VLEI # noqa: E402


def run(*args: str, cwd: Path | None = None) -> None:
print("+", " ".join(args))
subprocess.run(args, cwd=cwd, check=True)


def sync_repo(root: Path, repo: str, ref: str) -> None:
if not root.exists():
root.parent.mkdir(parents=True, exist_ok=True)
run("git", "clone", repo, str(root))

run("git", "-C", str(root), "fetch", "--tags", "origin")
run("git", "-C", str(root), "fetch", "origin")
run("git", "-C", str(root), "checkout", "--detach", ref)


def ensure_venv(venv: Path) -> Path:
python = venv / "bin" / "python"
if not python.exists():
run(sys.executable, "-m", "venv", str(venv))
return python


def pip_install(python: Path, *args: str) -> None:
run(str(python), "-m", "pip", *args)


def install_signifypy_runtime(deps_root: Path) -> None:
if not SIGNIFYPY_VENV_PYTHON.exists():
raise RuntimeError(
f"SignifyPy venv is missing at {SIGNIFYPY_VENV_PYTHON}. "
"Run `make sync` before `make sync-integration-deps`."
)

keripy_root = deps_root / KERIPY.path_name
pip_install(SIGNIFYPY_VENV_PYTHON, "install", "--upgrade", "pip", "setuptools", "wheel")
pip_install(SIGNIFYPY_VENV_PYTHON, "install", "-c", str(CONSTRAINTS), "hio==0.6.14")
pip_install(SIGNIFYPY_VENV_PYTHON, "install", "-c", str(CONSTRAINTS), "-e", str(keripy_root))


def install_keria_runtime(deps_root: Path) -> None:
python = ensure_venv(deps_root / KERIA.path_name / "venv")
keripy_root = deps_root / KERIPY.path_name
keria_root = deps_root / KERIA.path_name
pip_install(python, "install", "--upgrade", "pip", "setuptools", "wheel")
pip_install(python, "install", "-c", str(CONSTRAINTS), "hio==0.6.14")
pip_install(python, "install", "-c", str(CONSTRAINTS), "-e", str(keripy_root), "-e", str(keria_root))


def install_vlei_runtime(deps_root: Path) -> None:
python = ensure_venv(deps_root / VLEI.path_name / "venv")
keripy_root = deps_root / KERIPY.path_name
vlei_root = deps_root / VLEI.path_name
pip_install(python, "install", "--upgrade", "pip", "setuptools", "wheel")
pip_install(python, "install", "-c", str(CONSTRAINTS), "hio==0.6.14")
pip_install(python, "install", "-c", str(CONSTRAINTS), "-e", str(keripy_root), "-e", str(vlei_root))


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--deps-root",
default=str(SIGNIFYPY_ROOT / ".integration-deps"),
help="directory where pinned integration source repos should be stored",
)
args = parser.parse_args()

deps_root = Path(args.deps_root).expanduser().resolve()
for dependency in INTEGRATION_DEPENDENCIES:
sync_repo(deps_root / dependency.path_name, dependency.repo, dependency.ref)

install_signifypy_runtime(deps_root)
install_keria_runtime(deps_root)
install_vlei_runtime(deps_root)


if __name__ == "__main__":
main()
18 changes: 17 additions & 1 deletion src/signify/app/aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,11 @@ def addEndRole(self, name, *, role=Roles.agent, eid=None, stamp=None):
In OOBI-heavy flows this record must exist before any role-specific OOBI
becomes available.
"""
resolved_eid = self._resolveEndRoleEid(role=role, eid=eid)
hab = self.get(name)
pre = hab["prefix"]

rpy = self.makeEndRole(pre, role, eid, stamp)
rpy = self.makeEndRole(pre, role, resolved_eid, stamp)
keeper = self.client.manager.get(aid=hab)
sigs = keeper.sign(ser=rpy.raw)
rpy_msg = api.ReplyMessage(
Expand All @@ -257,6 +258,21 @@ def addEndRole(self, name, *, role=Roles.agent, eid=None, stamp=None):
res = self.client.post(f"/identifiers/{name}/endroles", json=asdict(rpy_msg))
return rpy, sigs, res.json()

def _resolveEndRoleEid(self, *, role, eid):
"""Resolve the endpoint provider AID for endpoint-role authorization."""
if eid:
return eid

if role == Roles.agent:
agent = getattr(self.client, "agent", None)
agent_pre = getattr(agent, "pre", None)
if agent_pre:
return agent_pre

raise kering.ConfigurationError("agent endpoint role authorization requires a connected agent AID")

raise kering.ConfigurationError(f"endpoint role {role} authorization requires eid")

def addLocScheme(self, name, url, *, eid=None, scheme=None, stamp=None):
"""Publish a location-scheme reply for an identifier-scoped endpoint.

Expand Down
4 changes: 2 additions & 2 deletions src/signify/app/clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
request families documented in the feature guide.
"""
from dataclasses import asdict
from urllib.parse import urlparse, urljoin, urlsplit
from urllib.parse import quote, urlparse, urljoin, urlsplit

import requests
import sseclient
Expand Down Expand Up @@ -491,5 +491,5 @@ def __call__(self, req):

p = urlsplit(req.url)
path = p.path if p.path else "/"
req.headers = self.authn.sign(headers, req.method, path)
req.headers = self.authn.sign(headers, req.method, quote(path))
return req
9 changes: 7 additions & 2 deletions src/signify/app/credentialing.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ def get(self, name, registryName):
Returns:
dict: Decoded registry record returned by KERIA.
"""
res = self.client.get(f"/identifiers/{name}/registries/{registryName}")
res = self.client.get(
f"/identifiers/{name}/registries/{registryName}"
)
return res.json()

def list(self, name):
Expand Down Expand Up @@ -396,7 +398,10 @@ def rename(self, target, registryName, newName):
"""
name = target if isinstance(target, str) else target["name"]
body = dict(name=newName)
resp = self.client.put(path=f"/identifiers/{name}/registries/{registryName}", json=body)
resp = self.client.put(
path=f"/identifiers/{name}/registries/{registryName}",
json=body,
)
return resp.json()


Expand Down
4 changes: 2 additions & 2 deletions src/signify/core/authing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
signify.core.authing module

"""
from urllib.parse import urlparse
from urllib.parse import quote, urlparse

from keri import kering
from keri.app import keeping
Expand Down Expand Up @@ -299,7 +299,7 @@ def verify(self, rep, **kwargs):
raise kering.AuthNError("No valid signature from agent on response.")

resource = rep.headers["SIGNIFY-RESOURCE"]
if resource != self.agent.pre or not self.verifysig(rep.headers, rep.request.method, url.path):
if resource != self.agent.pre or not self.verifysig(rep.headers, rep.request.method, quote(url.path)):
raise kering.AuthNError("No valid signature from agent on response.")

def verifysig(self, headers, method, path):
Expand Down
55 changes: 54 additions & 1 deletion tests/app/test_aiding.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,14 +579,15 @@ def test_aiding_add_end_role():

from signify.app.aiding import Identifiers
ids = Identifiers(client=mock_client) # type: ignore
mock_client.agent = mock({"pre": "agent-pre"}, strict=False) # type: ignore

mock_hab = {'prefix': 'hab prefix', 'name': 'aid1'}
expect(ids, times=1).get('aid1').thenReturn(mock_hab)

from keri.core import serdering
mock_serder = mock({'ked': {'a': 'key event dictionary'}, 'raw': b'serder raw bytes'}, spec=serdering.SerderKERI,
strict=True)
expect(ids, times=1).makeEndRole('hab prefix', 'agent', None, None).thenReturn(mock_serder)
expect(ids, times=1).makeEndRole('hab prefix', 'agent', 'agent-pre', None).thenReturn(mock_serder)

from signify.core import keeping
mock_keeper = mock({'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True)
Expand All @@ -608,6 +609,58 @@ def test_aiding_add_end_role():
unstub()


def test_aiding_add_end_role_preserves_explicit_eid():
from signify.app.clienting import SignifyClient
mock_client = mock(spec=SignifyClient, strict=True)

from signify.core import keeping
mock_manager = mock(spec=keeping.Manager, strict=True)
mock_client.manager = mock_manager # type: ignore
mock_client.agent = mock({"pre": "agent-pre"}, strict=False) # type: ignore

from signify.app.aiding import Identifiers
ids = Identifiers(client=mock_client) # type: ignore

mock_hab = {'prefix': 'hab prefix', 'name': 'aid1'}
expect(ids, times=1).get('aid1').thenReturn(mock_hab)

from keri.core import serdering
mock_serder = mock({'ked': {'a': 'key event dictionary'}, 'raw': b'serder raw bytes'}, spec=serdering.SerderKERI,
strict=True)
expect(ids, times=1).makeEndRole('hab prefix', 'agent', 'explicit-eid', None).thenReturn(mock_serder)

mock_keeper = mock({'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True)
expect(mock_manager, times=1).get(aid=mock_hab).thenReturn(mock_keeper)
expect(mock_keeper, times=1).sign(ser=mock_serder.raw).thenReturn(['a signature'])

from requests import Response
mock_response = mock(spec=Response, strict=True)
expected_data = {'rpy': {'a': 'key event dictionary'}, 'sigs': ['a signature']}
expect(mock_client, times=1).post('/identifiers/aid1/endroles', json=expected_data).thenReturn(mock_response)
expect(mock_response, times=1).json().thenReturn({'success': 'yay'})

_serder, _sig, out = ids.addEndRole('aid1', eid='explicit-eid')
assert out['success'] == 'yay'

verifyNoUnwantedInteractions()
unstub()


def test_aiding_add_end_role_requires_eid_for_non_agent_role():
from signify.app.clienting import SignifyClient
mock_client = mock(spec=SignifyClient, strict=True)

from signify.app.aiding import Identifiers
ids = Identifiers(client=mock_client) # type: ignore

from keri import kering
with pytest.raises(kering.ConfigurationError):
ids.addEndRole('aid1', role='mailbox')

verifyNoUnwantedInteractions()
unstub()


def test_aiding_sign():
from signify.app.clienting import SignifyClient
mock_client = mock(spec=SignifyClient, strict=True)
Expand Down
31 changes: 31 additions & 0 deletions tests/app/test_clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,3 +979,34 @@ def test_signify_auth():

unstub()
verifyNoUnwantedInteractions()


def test_signify_auth_quotes_signed_path():
from signify.core import authing
mock_controller = mock({'pre': 'a prefix'}, spec=authing.Controller, strict=True)
mock_authenticator = mock({'ctrl': mock_controller}, spec=authing.Authenticater, strict=True)

from signify.app.clienting import SignifyAuth
signify_auth = SignifyAuth(mock_authenticator)

import requests
mock_request = mock({
'method': 'GET',
'url': 'http://example.com/identifiers/name/registries/did:webs_designated_aliases:Eaid',
'headers': {},
'body': None,
}, spec=requests.Request, strict=True)

from keri.help import helping
expect(helping).nowIso8601().thenReturn('now ISO8601!')
expect(mock_authenticator, times=1).sign(
{'Signify-Resource': 'a prefix', 'Signify-Timestamp': 'now ISO8601!'},
'GET',
'/identifiers/name/registries/did%3Awebs_designated_aliases%3AEaid',
).thenReturn({'headers': 'modified'})

out = signify_auth.__call__(mock_request)
assert out.headers == {'headers': 'modified'}

unstub()
verifyNoUnwantedInteractions()
Loading
Loading