Skip to content

Commit

Permalink
Merge pull request #1301 from sicpa-dlab/fix/jsonld-verify-vmethod
Browse files Browse the repository at this point in the history
fix: failure to verify jsonld on non-conformant doc but vaild vmethod
  • Loading branch information
andrewwhitehead authored Aug 16, 2021
2 parents a32b601 + 8574ce5 commit c09ed19
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 73 deletions.
36 changes: 16 additions & 20 deletions aries_cloudagent/messaging/jsonld/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from aiohttp import web
from aiohttp_apispec import docs, request_schema, response_schema
from marshmallow import INCLUDE, Schema, fields
from pydid import VerificationMethod
from pydid.verification_method import (
Ed25519VerificationKey2018,
KnownVerificationMethods,
)

from ...admin.request_context import AdminRequestContext
from ...config.base import InjectionError
Expand All @@ -14,11 +17,12 @@
from .credential import sign_credential, verify_credential
from .error import (
BaseJSONLDMessagingError,
InvalidVerificationMethod,
MissingVerificationMethodError,
)


SUPPORTED_VERIFICATION_METHOD_TYPES = (Ed25519VerificationKey2018,)


class SignatureOptionsSchema(Schema):
"""Schema for LD signature options."""

Expand Down Expand Up @@ -141,30 +145,22 @@ async def verify(request: web.BaseRequest):
async with context.session() as session:
if verkey is None:
resolver = session.inject(DIDResolver)
ver_meth_expanded = await resolver.dereference(
profile, doc["proof"]["verificationMethod"]
vmethod = await resolver.dereference(
profile,
doc["proof"]["verificationMethod"],
cls=KnownVerificationMethods,
)

if ver_meth_expanded is None:
raise MissingVerificationMethodError(
f"Verification method "
f"{doc['proof']['verificationMethod']} not found."
if not isinstance(vmethod, SUPPORTED_VERIFICATION_METHOD_TYPES):
raise web.HTTPBadRequest(
reason="{} is not supported".format(vmethod.type)
)

if not isinstance(ver_meth_expanded, VerificationMethod):
raise InvalidVerificationMethod(
"verificationMethod does not identify a valid verification method"
)

verkey = ver_meth_expanded.material
verkey = vmethod.material

valid = await verify_credential(session, doc, verkey)

response["valid"] = valid
except (
BaseJSONLDMessagingError,
ResolverError,
) as error:
except (BaseJSONLDMessagingError, ResolverError, ValueError) as error:
response["error"] = str(error)
except (WalletError, InjectionError):
raise web.HTTPForbidden(reason="No wallet available")
Expand Down
130 changes: 84 additions & 46 deletions aries_cloudagent/messaging/jsonld/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from aiohttp import web
from asynctest import TestCase as AsyncTestCase
from asynctest import mock as async_mock
from pydid import DIDDocument, Service
from pyld import jsonld
import pytest

Expand All @@ -13,7 +12,6 @@
from ....config.base import InjectionError
from ....resolver.base import DIDMethodNotSupported, DIDNotFound, ResolverError
from ....resolver.did_resolver import DIDResolver
from ....resolver.tests import DOC
from ....vc.ld_proofs.document_loader import DocumentLoader
from ....wallet.base import BaseWallet
from ....wallet.did_method import DIDMethod
Expand All @@ -29,17 +27,40 @@

@pytest.fixture
def did_doc():
yield DIDDocument.deserialize(DOC)
yield {
"@context": "https://w3id.org/did/v1",
"id": "did:example:1234abcd",
"verificationMethod": [
{
"id": "did:example:1234abcd#key-1",
"type": "Ed25519VerificationKey2018",
"controller": "did:example:1234abcd",
"publicKeyBase58": "12345",
},
{
"id": "did:example:1234abcd#key-2",
"type": "RsaVerificationKey2018",
"controller": "did:example:1234abcd",
"publicKeyJwk": {},
},
],
"service": [
{
"id": "did:example:1234abcd#did-communication",
"type": "did-communication",
"priority": 0,
"recipientKeys": ["did:example:1234abcd#4"],
"routingKeys": ["did:example:1234abcd#6"],
"serviceEndpoint": "http://example.com",
}
],
}


@pytest.fixture
def mock_resolver(did_doc):
did_resolver = async_mock.MagicMock()
did_resolver = DIDResolver(async_mock.MagicMock())
did_resolver.resolve = async_mock.CoroutineMock(return_value=did_doc)
url = "did:example:1234abcd#4"
did_resolver.dereference = async_mock.CoroutineMock(
return_value=did_doc.dereference(url)
)
yield did_resolver


Expand Down Expand Up @@ -94,37 +115,43 @@ def mock_sign_request(mock_sign_credential):


@pytest.fixture
def mock_verify_request(mock_verify_credential, mock_resolver):
context = AdminRequestContext.test_context({DIDResolver: mock_resolver})
outbound_message_router = async_mock.CoroutineMock()
request_dict = {
"context": context,
"outbound_message_router": outbound_message_router,
def request_body():
yield {
"doc": {
"@context": "https://www.w3.org/2018/credentials/v1",
"type": "VerifiablePresentation",
"holder": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd",
"proof": {
"type": "Ed25519Signature2018",
"created": "2021-02-16T15:21:38.512Z",
"challenge": "5103d61a-bd26-4b1a-ab62-87a2a71281d3",
"domain": "svip-issuer.ocs-support.com",
"jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..mH_j_Y7MUIu_KXU_1Dy1BjE4w52INieSPaN7FPtKQKZYTRydPYO5jbjeM-uWB5BXpxS9o-obI5Ztx5IXex-9Aw",
"proofPurpose": "authentication",
"verificationMethod": "did:example:1234abcd#key-1",
},
}
}
request = async_mock.MagicMock(
match_info={},
query={},
json=async_mock.CoroutineMock(
return_value={
"doc": {
"@context": "https://www.w3.org/2018/credentials/v1",
"type": "VerifiablePresentation",
"holder": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd",
"proof": {
"type": "Ed25519Signature2018",
"created": "2021-02-16T15:21:38.512Z",
"challenge": "5103d61a-bd26-4b1a-ab62-87a2a71281d3",
"domain": "svip-issuer.ocs-support.com",
"jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..mH_j_Y7MUIu_KXU_1Dy1BjE4w52INieSPaN7FPtKQKZYTRydPYO5jbjeM-uWB5BXpxS9o-obI5Ztx5IXex-9Aw",
"proofPurpose": "authentication",
"verificationMethod": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd",
},
}
}
),
__getitem__=lambda _, k: request_dict[k],
)
yield request


@pytest.fixture
def mock_verify_request(mock_verify_credential, mock_resolver, request_body):
def _mock_verify_request(request_body=request_body):
context = AdminRequestContext.test_context({DIDResolver: mock_resolver})
outbound_message_router = async_mock.CoroutineMock()
request_dict = {
"context": context,
"outbound_message_router": outbound_message_router,
}
request = async_mock.MagicMock(
match_info={},
query={},
json=async_mock.CoroutineMock(return_value=request_body),
__getitem__=lambda _, k: request_dict[k],
)
return request

yield _mock_verify_request


@pytest.fixture
Expand Down Expand Up @@ -163,7 +190,7 @@ async def test_sign_bad_req_http_error(mock_sign_request, mock_response, error):

@pytest.mark.asyncio
async def test_verify(mock_verify_request, mock_response):
await test_module.verify(mock_verify_request)
await test_module.verify(mock_verify_request())
mock_response.assert_called_once_with({"valid": "fake_verify"})


Expand All @@ -180,7 +207,7 @@ async def test_verify(mock_verify_request, mock_response):
@pytest.mark.asyncio
async def test_verify_bad_req_error(mock_verify_request, mock_response, error):
test_module.verify_credential = async_mock.CoroutineMock(side_effect=error())
await test_module.verify(mock_verify_request)
await test_module.verify(mock_verify_request())
assert "error" in mock_response.call_args[0][0]


Expand All @@ -195,27 +222,38 @@ async def test_verify_bad_req_error(mock_verify_request, mock_response, error):
async def test_verify_bad_req_http_error(mock_verify_request, mock_response, error):
test_module.verify_credential = async_mock.CoroutineMock(side_effect=error())
with pytest.raises(web.HTTPForbidden):
await test_module.verify(mock_verify_request)
await test_module.verify(mock_verify_request())


@pytest.mark.asyncio
async def test_verify_bad_ver_meth_deref_req_error(
mock_resolver, mock_verify_request, mock_response
):
mock_resolver.dereference.return_value = None
await test_module.verify(mock_verify_request)
mock_resolver.dereference = async_mock.CoroutineMock(side_effect=ResolverError)
await test_module.verify(mock_verify_request())
assert "error" in mock_response.call_args[0][0]


@pytest.mark.asyncio
async def test_verify_bad_ver_meth_not_ver_meth(
mock_resolver, mock_verify_request, mock_response
mock_resolver, mock_verify_request, mock_response, request_body
):
mock_resolver.dereference.return_value = async_mock.MagicMock(spec=Service)
await test_module.verify(mock_verify_request)
request_body["doc"]["proof"][
"verificationMethod"
] = "did:example:1234abcd#did-communication"
await test_module.verify(mock_verify_request(request_body))
assert "error" in mock_response.call_args[0][0]


@pytest.mark.asyncio
async def test_verify_bad_vmethod_unsupported(
mock_resolver, mock_verify_request, mock_response, request_body
):
request_body["doc"]["proof"]["verificationMethod"] = "did:example:1234abcd#key-2"
with pytest.raises(web.HTTPBadRequest):
await test_module.verify(mock_verify_request(request_body))


@pytest.mark.asyncio
async def test_register():
mock_app = async_mock.MagicMock()
Expand Down
26 changes: 20 additions & 6 deletions aries_cloudagent/resolver/did_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
from datetime import datetime
from itertools import chain
import logging
from typing import Sequence, Tuple, Union
from typing import Sequence, Tuple, Type, TypeVar, Union

from pydid import DID, DIDError, DIDUrl, Resource
import pydid
from pydid import DID, DIDError, DIDUrl, Resource, NonconformantDocument
from pydid.doc.doc import IDNotFoundError

from ..core.profile import Profile
from .base import (
Expand All @@ -27,6 +27,9 @@
LOGGER = logging.getLogger(__name__)


ResourceType = TypeVar("ResourceType", bound=Resource)


class DIDResolver:
"""did resolver singleton."""

Expand Down Expand Up @@ -99,16 +102,27 @@ async def _match_did_to_resolver(
raise DIDMethodNotSupported(f'No resolver supprting DID "{did}" loaded')
return resolvers

async def dereference(self, profile: Profile, did_url: str) -> Resource:
async def dereference(
self, profile: Profile, did_url: str, *, cls: Type[ResourceType] = Resource
) -> ResourceType:
"""Dereference a DID URL to its corresponding DID Doc object."""
# TODO Use cached DID Docs when possible
try:
parsed = DIDUrl.parse(did_url)
if not parsed.did:
raise ValueError("Invalid DID URL")
doc_dict = await self.resolve(profile, parsed.did)
return pydid.deserialize_document(doc_dict).dereference(parsed)
except DIDError as err:
raise ResolverError(
"Failed to parse DID URL from {}".format(did_url)
) from err

doc_dict = await self.resolve(profile, parsed.did)
# Use non-conformant doc as the "least common denominator"
try:
return NonconformantDocument.deserialize(doc_dict).dereference_as(
cls, parsed
)
except IDNotFoundError as error:
raise ResolverError(
"Failed to dereference DID URL: {}".format(error)
) from error
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pyld~=2.0.3
pyyaml~=5.4.0
ConfigArgParse~=1.2.3
pyjwt~=1.7.1
pydid~=0.3.0
pydid~=0.3.2.post1
jsonpath_ng==1.5.2
pytz~=2021.1
python-dateutil~=2.8.1
Expand Down

0 comments on commit c09ed19

Please sign in to comment.