Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: mediation webhook routing keys #153

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
403992f
feat: add wallet_sd_jwt_sign endpoint
cjhowland Aug 14, 2023
c753d55
feat: (WIP) sd_jwt_sign logic using json paths
cjhowland Aug 14, 2023
469b931
test: (WIP) sd-jwt sign
cjhowland Aug 14, 2023
e8ec8c1
fix: super().__init__()
cjhowland Aug 16, 2023
a1496fb
fix: await jwt_sign() call
cjhowland Aug 16, 2023
471a819
feat: create SDJWSCreateSchema
cjhowland Aug 16, 2023
62dfabe
fix: marshmallow error
cjhowland Aug 16, 2023
d4b36e7
fix: flake8
cjhowland Aug 16, 2023
1615b4f
fix: typo
cjhowland Aug 16, 2023
14cee83
fix: typo
cjhowland Aug 17, 2023
04086df
fix: set jwt_sign result on self.serialized_sd_jwt
cjhowland Aug 17, 2023
cc36294
feat: create endpoint for sd-jwt verify
cjhowland Aug 17, 2023
03f7c44
feat: create SDJWTVerifierACAPy class and sd_jwt_verify() method
cjhowland Aug 17, 2023
cea0f15
test: sd_jwt_verify()
cjhowland Aug 17, 2023
0e01399
fix: remove self.jwt
cjhowland Aug 17, 2023
d06286b
feat: create SDJWSVerifyResponseSchema
cjhowland Aug 17, 2023
f1f3fdf
feat: create SDJWTVerifyResult
cjhowland Aug 17, 2023
9583624
feat: sdlist regex (WIP)
cjhowland Aug 21, 2023
212e9ca
feat: sd-jwt regex validation
cjhowland Aug 21, 2023
c87f1b8
feat: remove disclosures validation
cjhowland Aug 21, 2023
f7d5c8d
fix: add disclosures to sd-jwt verify json response
cjhowland Aug 21, 2023
5daf780
fix: validation for SDJWSVerifyResponseSchema
cjhowland Aug 24, 2023
a702859
fix: use inheritance for SDJWTVerifyResult
cjhowland Aug 24, 2023
2f6f49d
fix: remove unnecessary type checking
cjhowland Aug 24, 2023
66558a4
feat: add else for JWS JSON serialization
cjhowland Aug 24, 2023
30c6e5c
test: fixture for payload
cjhowland Aug 24, 2023
786d107
test: payloads with nested structures and array elements
cjhowland Aug 24, 2023
c0608ba
feat: use BaseModel for JWTVerifyResult/SDJWTVerifyResult
cjhowland Aug 24, 2023
d412788
fix: exponential backtracking issue
cjhowland Aug 25, 2023
a46c4fe
fix: regex for SDJSONWebToken
cjhowland Aug 25, 2023
14a6df6
fix: add ~ to regex
cjhowland Aug 25, 2023
28fcd42
feat: invert sd list validation and examples
cjhowland Aug 25, 2023
f721cc3
feat: method to create json paths from payload
cjhowland Aug 25, 2023
5148520
feat: method to handle list splices
cjhowland Aug 25, 2023
aa28ba1
feat: invert sd handling such that sd is default
cjhowland Aug 25, 2023
f0d0644
feat: add list of claims which are always visible
cjhowland Aug 25, 2023
eb645ce
test: adjust tests for sd inversion
cjhowland Aug 25, 2023
677651b
feat: default to empty list for non sd list
cjhowland Aug 25, 2023
aec0ac0
fix: flake8
cjhowland Aug 25, 2023
74bcbe0
fix: use atomic group to fix exponential backtracking issue with regex
cjhowland Aug 28, 2023
d9df449
feat: update sd-jwt repo
cjhowland Aug 28, 2023
6ad057c
fix: type hints
cjhowland Aug 28, 2023
8c9323e
fix: use isinstance
cjhowland Aug 28, 2023
04edc50
refactor: return sd_jwt_issuance from .issue() directly
cjhowland Aug 28, 2023
0d28173
test: include subset of SD claims in presentation
cjhowland Aug 28, 2023
3a4ca88
fix: syntax ?> not supported in the python re module
cjhowland Aug 28, 2023
3fca384
fix: remove Wy prefix from sd-jwt regex
cjhowland Aug 29, 2023
2553a6b
fix: ensure values between ~ delimiters in sd-jwt regex
cjhowland Aug 29, 2023
31b7dc2
fix: include full character set for urlsafe base64 encoded data
cjhowland Aug 29, 2023
95be51c
fix: include full urlsafe b64 encoding character set for jwts
cjhowland Aug 29, 2023
2986a77
fix: remove some special characters from jwt regex
cjhowland Aug 29, 2023
889da11
fix: jwt regex fix
cjhowland Aug 29, 2023
c7214c2
fix: prevent overwriting "typ" key in jwt_sign headers
cjhowland Sep 7, 2023
c84208f
fix: prevent adding always visible claims to sd_jwt
cjhowland Sep 7, 2023
74cec2e
feat: add expected_nonce, expected_aud as arguments
cjhowland Sep 7, 2023
47aabca
feat: redefine _verify_key_binding_jwt
cjhowland Sep 7, 2023
a7993fc
test: key binding implementation
cjhowland Sep 7, 2023
3141e45
feat: add sd-jwt python library to pyproject.toml
cjhowland Sep 7, 2023
9acac83
chore: ruff
cjhowland Sep 8, 2023
1b8bc8b
fix: move fixtures to conftest.py
cjhowland Sep 8, 2023
11684e8
chore: poetry lock
cjhowland Sep 8, 2023
9d3f013
chore: remove requirements.txt
cjhowland Sep 8, 2023
10eac81
docs: sd jwt implementation
cjhowland Sep 13, 2023
3c74952
docs: typo
cjhowland Sep 13, 2023
522a6c1
fix: sd-jwt regex to allow key binding JWT
cjhowland Sep 14, 2023
636ed44
docs: example inputs to admin api endpoints
cjhowland Sep 14, 2023
bc833ca
fix: run tests script copying local env
dbluhm Sep 18, 2023
d705ca2
Merge pull request #2495 from dbluhm/fix/scripts-run-test
swcurran Sep 18, 2023
5d0335f
chore: use alias for jsonpath parse
cjhowland Sep 19, 2023
97f7d69
docs: fix markdown link
cjhowland Sep 19, 2023
d40b6e6
Merge branch 'main' into feat/sd-jwt-implementation
dbluhm Sep 20, 2023
2930ac2
Merge pull request #2487 from Indicio-tech/feat/sd-jwt-implementation
dbluhm Sep 20, 2023
cb4ca9b
Use correct rust log level
loneil Sep 21, 2023
0867ad1
Merge pull request #2499 from loneil/bugfix/dockerfileRustLog
swcurran Sep 21, 2023
08d9344
fix: begin to replace with normalize_with_public_did
anwalker293 Sep 14, 2023
0b4b133
fix: import normalize_from_public_Key
anwalker293 Sep 14, 2023
59ecb16
fix: routing key storage for MediationRecord
anwalker293 Sep 14, 2023
73b5a13
fix: retrievals of route record
anwalker293 Sep 15, 2023
1b8e583
fix: tests hardcoded to expect base58
dbluhm Sep 18, 2023
1e983ff
feat: add upgrade step for routing keys to did:key
dbluhm Sep 21, 2023
9acebaa
test: update routing keys routine
dbluhm Sep 21, 2023
698da0e
test: update_routing_keys called when configured
dbluhm Sep 21, 2023
3f0d1a6
fix: drop old comments
dbluhm Sep 21, 2023
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ test-reports
.python-version
docker
env
.venv
4 changes: 3 additions & 1 deletion aries_cloudagent/commands/default_version_upgrade_config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
v0.11.0:
update_routing_keys: true
v0.8.1:
resave_records:
base_record_path:
Expand All @@ -13,4 +15,4 @@ v0.7.1:
v0.7.0:
update_existing_records: false
v0.6.0:
update_existing_records: false
update_existing_records: false
118 changes: 114 additions & 4 deletions aries_cloudagent/commands/tests/test_upgrade.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import asyncio

from asynctest import mock as async_mock, TestCase as AsyncTestCase
from asynctest import TestCase as AsyncTestCase, mock as async_mock

from ...core.in_memory import InMemoryProfile
from .. import upgrade as test_module
from ...connections.models.conn_record import ConnRecord
from ...core.in_memory import InMemoryProfile
from ...protocols.coordinate_mediation.v1_0.models.mediation_record import (
MediationRecord,
)
from ...protocols.routing.v1_0.models.route_record import RouteRecord
from ...storage.base import BaseStorage
from ...storage.record import StorageRecord
from ...version import __version__

from .. import upgrade as test_module
from ..upgrade import UpgradeError


TEST_BASE58_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"
TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL"


class TestUpgrade(AsyncTestCase):
async def setUp(self):
self.session = InMemoryProfile.test_session()
Expand Down Expand Up @@ -619,3 +626,106 @@ async def test_upgrade_explicit_check(self):
await test_module.upgrade(profile=self.profile)
assert mock_logger.warning.call_count == 1
assert mock_logger.info.call_count == 0

async def test_update_routing_keys(self):
"""Test update routing keys routine."""
routes = [
RouteRecord(
role=RouteRecord.ROLE_SERVER,
recipient_key=TEST_BASE58_VERKEY,
connection_id="dummy connection id",
),
RouteRecord(
role=RouteRecord.ROLE_SERVER,
recipient_key=TEST_BASE58_VERKEY,
connection_id="dummy connection id",
),
RouteRecord(
role=RouteRecord.ROLE_SERVER,
recipient_key=TEST_VERKEY,
connection_id="dummy connection id",
),
]
mediations = [
MediationRecord(
role=MediationRecord.ROLE_CLIENT,
state=MediationRecord.STATE_GRANTED,
connection_id="dummy connection id",
routing_keys=[TEST_BASE58_VERKEY],
),
MediationRecord(
role=MediationRecord.ROLE_CLIENT,
state=MediationRecord.STATE_GRANTED,
connection_id="dummy connection id",
routing_keys=[TEST_BASE58_VERKEY, TEST_BASE58_VERKEY],
),
MediationRecord(
role=MediationRecord.ROLE_CLIENT,
state=MediationRecord.STATE_GRANTED,
connection_id="dummy connection id",
routing_keys=[TEST_VERKEY],
),
MediationRecord(
role=MediationRecord.ROLE_CLIENT,
state=MediationRecord.STATE_GRANTED,
connection_id="dummy connection id",
routing_keys=[TEST_VERKEY, TEST_BASE58_VERKEY],
),
]
for route in routes:
await route.save(self.session)
for mediation in mediations:
await mediation.save(self.session)

await test_module.update_routing_keys(self.profile)

mediation_records = await MediationRecord.query(self.session)
route_records = await RouteRecord.query(self.session)
assert len(mediation_records)
assert len(route_records)

for record in mediation_records:
for routing_key in record.routing_keys:
assert routing_key == TEST_VERKEY

for record in route_records:
assert record.recipient_key == TEST_VERKEY

async def test_update_routing_keys_called_from_executables(self):
version_storage_record = await self.storage.find_record(
type_filter="acapy_version", tag_query={}
)
await self.storage.delete_record(version_storage_record)
with async_mock.patch.object(
test_module,
"wallet_config",
async_mock.CoroutineMock(
return_value=(
self.profile,
async_mock.CoroutineMock(did="public DID", verkey="verkey"),
)
),
), async_mock.patch.object(
test_module.yaml,
"safe_load",
async_mock.MagicMock(
return_value={
"v0.11.0": {
"update_routing_keys": True,
},
}
),
), async_mock.patch.object(
test_module,
"update_routing_keys",
async_mock.CoroutineMock(),
) as mock_update_routing_keys:
test_module.UPGRADE_EXISTING_RECORDS_FUNCTION_MAPPING[
"update_routing_keys"
] = mock_update_routing_keys
await test_module.upgrade(
settings={
"upgrade.from_version": "v0.7.2",
}
)
mock_update_routing_keys.assert_called_once()
70 changes: 68 additions & 2 deletions aries_cloudagent/commands/upgrade.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Upgrade command for handling breaking changes when updating ACA-PY versions."""

import asyncio
import json
import logging
import os
import yaml
Expand All @@ -19,14 +20,22 @@
Tuple,
)

from ..protocols.coordinate_mediation.v1_0.models.mediation_record import (
MediationRecord,
)
from ..protocols.coordinate_mediation.v1_0.normalization import (
normalize_from_public_key,
)
from ..protocols.routing.v1_0.models.route_record import RouteRecord

from ..core.profile import Profile
from ..config import argparse as arg
from ..config.default_context import DefaultContextBuilder
from ..config.base import BaseError, BaseSettings
from ..config.util import common_config
from ..config.wallet import wallet_config
from ..messaging.models.base_record import BaseRecord
from ..storage.base import BaseStorage
from ..storage.base import BaseStorage, BaseStorageSearch
from ..storage.error import StorageNotFoundError
from ..storage.record import StorageRecord
from ..utils.classloader import ClassLoader, ClassNotFoundError
Expand Down Expand Up @@ -341,6 +350,7 @@ async def upgrade(
f"Only BaseRecord can be resaved, found: {str(rec_type)}"
)
async with root_profile.session() as session:
# TODO This should use BaseStorageSearch so we don't risk OOM errors
all_records = await rec_type.query(session)
for record in all_records:
await record.save(
Expand Down Expand Up @@ -394,6 +404,61 @@ async def update_existing_records(profile: Profile):
pass


async def update_routing_keys(profile: Profile):
"""Update routing keys previously stored in wallet.

This update step will transform the stored routing keys into did:key values
from raw base58 encoded values.

Steps:
for each mediation record stored in the wallet:
ensure the stored routing_keys list is formated as did:keys
save the record if modified
for each routing record stored in the wallet:
ensure the stored recipient keys are formated as did:keys
save the record if modified
"""
async with profile.transaction() as txn:
searcher = txn.inject(BaseStorageSearch)
search = searcher.search_records(
MediationRecord.RECORD_TYPE,
)
async for record in search:
try:
value = json.loads(record.value)
record = MediationRecord.from_storage(record.id, value)
original = record.routing_keys
record.routing_keys = [
normalize_from_public_key(key) for key in record.routing_keys
]
if original != record.routing_keys:
await record.save(
txn, reason="Normalize routing keys to did:key", event=False
)
except Exception:
LOGGER.exception("Error normalizing routing keys in mediation record")
await txn.commit()

async with profile.transaction() as txn:
searcher = txn.inject(BaseStorageSearch)
search = searcher.search_records(
RouteRecord.RECORD_TYPE,
)
async for record in search:
try:
value = json.loads(record.value)
record = RouteRecord.from_storage(record.id, value)
original = record.recipient_key
record.recipient_key = normalize_from_public_key(record.recipient_key)
if original != record.recipient_key:
await record.save(
txn, reason="Normalize recipient key to did:key", event=False
)
except Exception:
LOGGER.exception("Error normalizing recipient key in route record")
await txn.commit()


def execute(argv: Sequence[str] = None):
"""Entrypoint."""
parser = arg.create_argument_parser(prog=PROG)
Expand All @@ -413,7 +478,8 @@ def main():


UPGRADE_EXISTING_RECORDS_FUNCTION_MAPPING = {
"update_existing_records": update_existing_records
"update_existing_records": update_existing_records,
"update_routing_keys": update_routing_keys,
}

main()
6 changes: 4 additions & 2 deletions aries_cloudagent/core/in_memory/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from ...config.injection_context import InjectionContext
from ...config.provider import ClassProvider
from ...storage.base import BaseStorage
from ...storage.base import BaseStorage, BaseStorageSearch
from ...storage.vc_holder.base import VCHolder
from ...utils.classloader import DeferLoad
from ...wallet.base import BaseWallet
Expand Down Expand Up @@ -114,7 +114,9 @@ async def _setup(self):

def _init_context(self):
"""Initialize the session context."""
self._context.injector.bind_instance(BaseStorage, STORAGE_CLASS(self.profile))
storage = STORAGE_CLASS(self.profile)
self._context.injector.bind_instance(BaseStorageSearch, storage)
self._context.injector.bind_instance(BaseStorage, storage)
self._context.injector.bind_instance(BaseWallet, WALLET_CLASS(self.profile))

@property
Expand Down
50 changes: 49 additions & 1 deletion aries_cloudagent/messaging/valid.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,26 @@ def __init__(self):
)


class NonSDList(Regexp):
"""Validate NonSD List."""

EXAMPLE = [
"name",
"address",
"address.street_address",
"nationalities[1:3]",
]
PATTERN = r"[a-z0-9:\[\]_\.@?\(\)]"

def __init__(self):
"""Initialize the instance."""

super().__init__(
NonSDList.PATTERN,
error="Value {input} is not a valid NonSDList",
)


class JSONWebToken(Regexp):
"""Validate JSON Web Token."""

Expand All @@ -208,7 +228,7 @@ class JSONWebToken(Regexp):
"eyJhIjogIjAifQ."
"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
)
PATTERN = r"^[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*\.[-_a-zA-Z0-9]*$"
PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$"

def __init__(self):
"""Initialize the instance."""
Expand All @@ -219,6 +239,28 @@ def __init__(self):
)


class SDJSONWebToken(Regexp):
"""Validate SD-JSON Web Token."""

EXAMPLE = (
"eyJhbGciOiJFZERTQSJ9."
"eyJhIjogIjAifQ."
"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
"~WyJEM3BUSFdCYWNRcFdpREc2TWZKLUZnIiwgIkRFIl0"
"~WyJPMTFySVRjRTdHcXExYW9oRkd0aDh3IiwgIlNBIl0"
"~WyJkVmEzX1JlTGNsWTU0R1FHZm5oWlRnIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ"
)
PATTERN = r"^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+(?:~[a-zA-Z0-9._-]+)*~?$"

def __init__(self):
"""Initialize the instance."""

super().__init__(
SDJSONWebToken.PATTERN,
error="Value {input} is not a valid SD-JSON Web token",
)


class DIDKey(Regexp):
"""Validate value against DID key specification."""

Expand Down Expand Up @@ -800,9 +842,15 @@ def __init__(
JWS_HEADER_KID_VALIDATE = JWSHeaderKid()
JWS_HEADER_KID_EXAMPLE = JWSHeaderKid.EXAMPLE

NON_SD_LIST_VALIDATE = NonSDList()
NON_SD_LIST_EXAMPLE = NonSDList().EXAMPLE

JWT_VALIDATE = JSONWebToken()
JWT_EXAMPLE = JSONWebToken.EXAMPLE

SD_JWT_VALIDATE = SDJSONWebToken()
SD_JWT_EXAMPLE = SDJSONWebToken.EXAMPLE

DID_KEY_VALIDATE = DIDKey()
DID_KEY_EXAMPLE = DIDKey.EXAMPLE

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .. import mediation_grant_handler as test_module

TEST_CONN_ID = "conn-id"
TEST_RECORD_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"
TEST_BASE58_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"
TEST_VERKEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL"
TEST_ENDPOINT = "https://example.com"

Expand Down Expand Up @@ -59,7 +59,21 @@ async def test_handler(self):
assert record
assert record.state == MediationRecord.STATE_GRANTED
assert record.endpoint == TEST_ENDPOINT
assert record.routing_keys == [TEST_RECORD_VERKEY]
assert record.routing_keys == [TEST_VERKEY]

async def test_handler_with_base58_received(self):
assert isinstance(self.context.message, MediationGrant)
self.context.message.routing_keys = [TEST_BASE58_VERKEY]
handler, responder = MediationGrantHandler(), MockResponder()
await MediationRecord(connection_id=TEST_CONN_ID).save(self.session)
await handler.handle(self.context, responder)
record = await MediationRecord.retrieve_by_connection_id(
self.session, TEST_CONN_ID
)
assert record
assert record.state == MediationRecord.STATE_GRANTED
assert record.endpoint == TEST_ENDPOINT
assert record.routing_keys == [TEST_VERKEY]

async def test_handler_connection_has_set_to_default_meta(self):
handler, responder = MediationGrantHandler(), MockResponder()
Expand Down
Loading