diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index 7c213f5f3b..8656665bab 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -111,7 +111,7 @@ async def load_plugins(self, context: InjectionContext): "aries_cloudagent.messaging.credential_definitions" ) plugin_registry.register_plugin("aries_cloudagent.messaging.schemas") - # plugin_registry.register_plugin("aries_cloudagent.messaging.jsonld") + plugin_registry.register_plugin("aries_cloudagent.messaging.jsonld") plugin_registry.register_plugin("aries_cloudagent.revocation") plugin_registry.register_plugin("aries_cloudagent.resolver") plugin_registry.register_plugin("aries_cloudagent.wallet") diff --git a/aries_cloudagent/messaging/jsonld/create_verify_data.py b/aries_cloudagent/messaging/jsonld/create_verify_data.py index 28c91a640e..0199fa9704 100644 --- a/aries_cloudagent/messaging/jsonld/create_verify_data.py +++ b/aries_cloudagent/messaging/jsonld/create_verify_data.py @@ -38,7 +38,9 @@ def _cannonize_document(doc): class DroppedAttributeException(Exception): """Exception used to track that an attribute was removed.""" - pass + +class VerificationMethodMissing(Exception): + """VerificationMethod is required.""" def create_verify_data(data, signature_options): @@ -48,7 +50,10 @@ def create_verify_data(data, signature_options): signature_options["verificationMethod"] = signature_options["creator"] if not signature_options["verificationMethod"]: - raise Exception("signature_options.verificationMethod is required") + raise VerificationMethodMissing( + "signature_options.verificationMethod " + "is required" + ) if "created" not in signature_options: signature_options["created"] = datetime.datetime.now( diff --git a/aries_cloudagent/messaging/jsonld/credential.py b/aries_cloudagent/messaging/jsonld/credential.py index 20bf334225..454e57d98b 100644 --- a/aries_cloudagent/messaging/jsonld/credential.py +++ b/aries_cloudagent/messaging/jsonld/credential.py @@ -10,15 +10,20 @@ bytes_to_b64, str_to_b64, ) +from ...wallet.base import BaseWallet from .create_verify_data import create_verify_data +class InvalidJWSHeader(Exception): + """Invalid jws header provided.""" + + MULTIBASE_B58_BTC = "z" MULTICODEC_ED25519_PUB = b"\xed" -def did_key(verkey: str) -> str: +def did_key(verkey: str) -> str: # Warning: duplicate function in attach_decorator.py """Qualify verkey into DID key if need be.""" if verkey.startswith(f"did:key:{MULTIBASE_B58_BTC}"): @@ -44,7 +49,7 @@ def create_jws(encoded_header, verify_data): return (encoded_header + ".").encode("utf-8") + verify_data -async def jws_sign(verify_data, verkey, wallet): +async def jws_sign(session, verify_data, verkey): """Sign JWS.""" header = {"alg": "EdDSA", "b64": False, "crit": ["b64"]} @@ -53,6 +58,7 @@ async def jws_sign(verify_data, verkey, wallet): jws_to_sign = create_jws(encoded_header, verify_data) + wallet = session.inject(BaseWallet, required=True) signature = await wallet.sign_message(jws_to_sign, verkey) encoded_signature = bytes_to_b64(signature, urlsafe=True, pad=False) @@ -73,10 +79,12 @@ def verify_jws_header(header): ) and len(header) == 3 ): - raise Exception("Invalid JWS header parameters for Ed25519Signature2018.") + raise InvalidJWSHeader( + "Invalid JWS header parameters for Ed25519Signature2018." + ) -async def jws_verify(verify_data, signature, public_key, wallet): +async def jws_verify(session, verify_data, signature, public_key): """Detatched jws verify handling.""" encoded_header, _, encoded_signature = signature.partition("..") @@ -88,25 +96,25 @@ async def jws_verify(verify_data, signature, public_key, wallet): jws_to_verify = create_jws(encoded_header, verify_data) + wallet = session.inject(BaseWallet, required=True) verified = await wallet.verify_message(jws_to_verify, decoded_signature, public_key) return verified -async def sign_credential(credential, signature_options, verkey, wallet): +async def sign_credential(session, credential, signature_options, verkey): """Sign Credential.""" framed, verify_data_hex_string = create_verify_data(credential, signature_options) verify_data_bytes = bytes.fromhex(verify_data_hex_string) - jws = await jws_sign(verify_data_bytes, verkey, wallet) - document_with_proof = {**credential, "proof": {**signature_options, "jws": jws}} - return document_with_proof + jws = await jws_sign(session, verify_data_bytes, verkey) + return {**credential, "proof": {**signature_options, "jws": jws}} -async def verify_credential(doc, verkey, wallet): +async def verify_credential(session, doc, verkey): """Verify credential.""" framed, verify_data_hex_string = create_verify_data(doc, doc["proof"]) verify_data_bytes = bytes.fromhex(verify_data_hex_string) - valid = await jws_verify(verify_data_bytes, framed["proof"]["jws"], verkey, wallet) + valid = await jws_verify(session, verify_data_bytes, framed["proof"]["jws"], verkey) return valid diff --git a/aries_cloudagent/messaging/jsonld/routes.py b/aries_cloudagent/messaging/jsonld/routes.py index a3bdc3d051..ba149f1740 100644 --- a/aries_cloudagent/messaging/jsonld/routes.py +++ b/aries_cloudagent/messaging/jsonld/routes.py @@ -3,21 +3,25 @@ from aiohttp import web from aiohttp_apispec import docs, request_schema, response_schema -from marshmallow import fields - from ...admin.request_context import AdminRequestContext -from ...wallet.base import BaseWallet - -from ..models.openapi import OpenAPISchema +from ...messaging.models.openapi import OpenAPISchema +from marshmallow import fields +from ...config.base import InjectionError +from ...wallet.error import WalletError +from ...resolver.did_resolver import DIDResolver +from ...resolver.base import ResolverError +from ...resolver.did import DIDError from .credential import sign_credential, verify_credential -class SignRequestSchema(OpenAPISchema): +class JsonLDRequestSchema(OpenAPISchema): """Request schema for signing a jsonld doc.""" - verkey = fields.Str(required=True, description="verkey to use for signing") - doc = fields.Dict(required=True, description="JSON-LD Doc to sign") + verification_method = fields.Str( + required=True, data_key="verificationMethod", description="key" + ) + document = fields.Dict(required=True, description="JSON-LD Doc to sign") class SignResponseSchema(OpenAPISchema): @@ -26,8 +30,14 @@ class SignResponseSchema(OpenAPISchema): signed_doc = fields.Dict(required=True) +class VerifyResponseSchema(OpenAPISchema): + """Response schema for verification result.""" + + valid = fields.Bool(required=True) + + @docs(tags=["jsonld"], summary="Sign a JSON-LD structure and return it") -@request_schema(SignRequestSchema()) +@request_schema(JsonLDRequestSchema()) @response_schema(SignResponseSchema(), 200, description="") async def sign(request: web.BaseRequest): """ @@ -37,45 +47,31 @@ async def sign(request: web.BaseRequest): request: aiohttp request object """ - response = {} + context: AdminRequestContext = request["context"] + session = await context.session() + body = await request.json() + ver_meth = body.get("verificationMethod") + doc = body.get("document") try: - context: AdminRequestContext = request["context"] - body = await request.json() - verkey = body.get("verkey") - doc = body.get("doc") - credential = doc["credential"] - signature_options = doc["options"] - - async with context.session() as session: - wallet = session.inject(BaseWallet, required=False) - if not wallet: - raise web.HTTPForbidden() - document_with_proof = await sign_credential( - credential, signature_options, verkey, wallet - ) - - response["signed_doc"] = document_with_proof - except Exception as e: - response["error"] = str(e) - - return web.json_response(response) - - -class VerifyRequestSchema(OpenAPISchema): - """Request schema for signing a jsonld doc.""" - - verkey = fields.Str(required=True, description="verkey to use for doc verification") - doc = fields.Dict(required=True, description="JSON-LD Doc to verify") - - -class VerifyResponseSchema(OpenAPISchema): - """Response schema for verification result.""" - - valid = fields.Bool(required=True) + resolver = session.inject(DIDResolver) + # TODO: make this work in the wild. + ver_meth_expanded = await resolver.dereference(session, ver_meth) + if ver_meth_expanded is None: + raise ResolverError(f"Verification method {ver_meth} not found.") + verkey = ver_meth_expanded.get("publicKeyBase58") + doc_with_proof = await sign_credential( + session, + doc, + {"verificationMethod": ver_meth}, + verkey + ) + except (DIDError, ResolverError, WalletError, InjectionError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + return web.json_response({"signed_doc": doc_with_proof}) @docs(tags=["jsonld"], summary="Verify a JSON-LD structure.") -@request_schema(VerifyRequestSchema()) +@request_schema(JsonLDRequestSchema()) @response_schema(VerifyResponseSchema(), 200, description="") async def verify(request: web.BaseRequest): """ @@ -85,28 +81,40 @@ async def verify(request: web.BaseRequest): request: aiohttp request object """ - response = {"valid": False} + context: AdminRequestContext = request["context"] + session = await context.session() + body = await request.json() + ver_meth = body.get("verificationMethod") + doc = body.get("document") try: - context: AdminRequestContext = request["context"] - body = await request.json() - verkey = body.get("verkey") - doc = body.get("doc") + resolver = session.inject(DIDResolver) + # TODO: make this work in the wild. + ver_meth_expanded = await resolver.dereference(session, ver_meth) + if ver_meth_expanded is None: + raise ResolverError(f"Verification method {ver_meth} not found.") + verkey = ver_meth_expanded.get("publicKeyBase58") + result = await verify_credential(session, doc, verkey) + except (DIDError, ResolverError, WalletError, InjectionError) as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err + return web.json_response({"valid": result}) - async with context.session() as session: - wallet = session.inject(BaseWallet, required=False) - if not wallet: - raise web.HTTPForbidden() - valid = await verify_credential(doc, verkey, wallet) - response["valid"] = valid - except Exception as e: - response["error"] = str(e) +async def register(app: web.Application): + """Register routes.""" - return web.json_response(response) + app.add_routes([web.post("/jsonld/sign", sign), web.post("/jsonld/verify", verify)]) -async def register(app: web.Application): - """Register routes.""" +def post_process_routes(app: web.Application): + """Amend swagger API.""" - app.add_routes([web.post("/jsonld/sign", sign)]) - app.add_routes([web.post("/jsonld/verify", verify)]) + # Add top-level tags description + if "tags" not in app._state["swagger_dict"]: + app._state["swagger_dict"]["tags"] = [] + app._state["swagger_dict"]["tags"].append( + { + "name": "json-ld sign/verify", + "description": "sign and verify json-ld data.", + "externalDocs": {"description": "Specification"}, # , "url": SPEC_URI}, + } + ) diff --git a/aries_cloudagent/messaging/jsonld/tests/test_credential.py b/aries_cloudagent/messaging/jsonld/tests/test_credential.py new file mode 100644 index 0000000000..d1602f0d00 --- /dev/null +++ b/aries_cloudagent/messaging/jsonld/tests/test_credential.py @@ -0,0 +1,348 @@ +"""Test json-ld credential.""" + +import pytest +import asyncio +from asynctest import mock as async_mock +from itertools import cycle +from ....admin.request_context import AdminRequestContext +from ..credential import verify_credential, sign_credential, did_key, InvalidJWSHeader +from ....core.in_memory import InMemoryProfile +from ....wallet.in_memory import InMemoryWallet +from ....wallet.base import BaseWallet + +TEST_SEED = "testseed000000000000000000000001" +TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" +TEST_VERKEY = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" + +TEST_SIGN_OBJ0 = { + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "id": "http://example.gov/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": ("did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"), + "issuanceDate": "2020-03-10T04:24:12.164Z", + "credentialSubject": { + "id": ("did:key:" "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"), + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + }, + }, + "options": { + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "created": "2020-04-02T18:48:36Z", + "domain": "example.com", + "challenge": "d436f0c8-fbd9-4e48-bbb2-55fc5d0920a8", + }, +} +TEST_SIGN_OBJ1 = { + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "id": "http://example.gov/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:example:123", + "issuanceDate": "2020-03-16T22:37:26.544Z", + "credentialSubject": { + "id": "did:example:123", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + }, + }, + "options": { + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "created": "2020-04-02T18:48:36Z", + "domain": "example.com", + "challenge": "d436f0c8-fbd9-4e48-bbb2-55fc5d0920a8", + }, +} +TEST_SIGN_OBJ2 = { + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "holder": "did:example:123", + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ] + }, + {"id": "http://example.gov/credentials/3732"}, + {"type": ["VerifiableCredential", "UniversityDegreeCredential"]}, + {"issuer": "did:example:123"}, + {"issuanceDate": "2020-03-16T22:37:26.544Z"}, + { + "credentialSubject": { + "id": "did:example:123", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + } + }, + { + "proof": { + "type": "Ed25519Signature2018", + "created": "2020-04-02T18:28:08Z", + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..YtqjEYnFENT7fNW-COD0HAACxeuQxPKAmp4nIl8jYAu__6IH2FpSxv81w-l5PvE1og50tS9tH8WyXMlXyo45CA", + } + }, + ], + "proof": { + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "created": "2020-04-02T18:48:36Z", + "domain": "example.com", + "challenge": "d436f0c8-fbd9-4e48-bbb2-55fc5d0920a8", + "type": "Ed25519Signature2018", + "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..a6dB9OAI9HWc1lDoWzd1---XF_QdArVMu99N2OKnOFT2Ize8MiuVvbJCIkYHpjn3arPle-o0iMlUx3q08ES_Bg", + }, + }, + "options": { + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "created": "2020-04-02T18:48:36Z", + "domain": "example.com", + "challenge": "d436f0c8-fbd9-4e48-bbb2-55fc5d0920a8", + }, +} +TEST_SIGN_OBJS = [ + TEST_SIGN_OBJ0, + TEST_SIGN_OBJ1, + TEST_SIGN_OBJ2, +] + +TEST_VERIFY_OBJ0 = { + "verkey": ("5yKdnU7ToTjAoRNDzfuzVTfWBH38qyhE1b9xh4v8JaWF"), + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "id": "http://example.gov/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": ("did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"), + "issuanceDate": "2020-03-10T04:24:12.164Z", + "credentialSubject": { + "id": ("did:key:" "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"), + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2020-04-10T21:35:35Z", + "verificationMethod": ( + "did:key:" + "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc" + "4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZD" + "VzrJzFrwahc4tXLt9DoHd" + ), + "proofPurpose": "assertionMethod", + "jws": ( + "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaX" + "QiOlsiYjY0Il19..l9d0YHjcFAH2H4dB9xlWFZQLUp" + "ixVCWJk0eOt4CXQe1NXKWZwmhmn9OQp6YxX0a2Lffe" + "gtYESTCJEoGVXLqWAA" + ), + }, + }, +} +TEST_VERIFY_OBJ1 = { + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "id": "http://example.gov/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:example:123", + "issuanceDate": "2020-03-16T22:37:26.544Z", + "credentialSubject": { + "id": "did:example:123", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + }, + "proof": { + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "created": "2020-04-02T18:48:36Z", + "domain": "example.com", + "challenge": "d436f0c8-fbd9-4e48-bbb2-55fc5d0920a8", + "type": "Ed25519Signature2018", + "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..MthZGAH62bEu2e4rZSE6b0XvGr_5z6J3FSXuVJnOOxr6sgdJpUenXJ-113MTtjArwC2JXh0zeolhXithxud_Dw", + }, + }, + "verkey": "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", +} +TEST_VERIFY_OBJ2 = { + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "holder": "did:example:123", + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ] + }, + {"id": "http://example.gov/credentials/3732"}, + {"type": ["VerifiableCredential", "UniversityDegreeCredential"]}, + {"issuer": "did:example:123"}, + {"issuanceDate": "2020-03-16T22:37:26.544Z"}, + { + "credentialSubject": { + "id": "did:example:123", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + } + }, + { + "proof": { + "type": "Ed25519Signature2018", + "created": "2020-04-02T18:28:08Z", + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..YtqjEYnFENT7fNW-COD0HAACxeuQxPKAmp4nIl8jYAu__6IH2FpSxv81w-l5PvE1og50tS9tH8WyXMlXyo45CA", + } + }, + ], + "proof": { + "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "proofPurpose": "assertionMethod", + "created": "2020-04-02T18:48:36Z", + "domain": "example.com", + "challenge": "d436f0c8-fbd9-4e48-bbb2-55fc5d0920a8", + "type": "Ed25519Signature2018", + "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..a6dB9OAI9HWc1lDoWzd1---XF_QdArVMu99N2OKnOFT2Ize8MiuVvbJCIkYHpjn3arPle-o0iMlUx3q08ES_Bg", + }, + }, + "verkey": "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", +} +TEST_VERIFY_OBJS = [ + TEST_VERIFY_OBJ0, + TEST_VERIFY_OBJ1, + TEST_VERIFY_OBJ2, +] +TEST_VERIFY_ERROR = { + "verkey": ("5yKdnU7ToTjAoRNDzfuzVTfWBH38qyhE1b9xh4v8JaWF"), + "doc": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + "id": "http://example.gov/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": ("did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"), + "issuanceDate": "2020-03-10T04:24:12.164Z", + "credentialSubject": { + "id": ("did:key:" "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"), + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2020-04-10T21:35:35Z", + "verificationMethod": ( + "did:key:" + "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc" + "4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZD" + "VzrJzFrwahc4tXLt9DoHd" + ), + "proofPurpose": "assertionMethod", + "jws": ( + "eyJhbGciOiJ0RWRUZXN0RWQiLCJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdfQ..l9d0YHjcFAH2H4dB9xlWFZQLUp" + "ixVCWJk0eOt4CXQe1NXKWZwmhmn9OQp6YxX0a2Lffe" + "gtYESTCJEoGVXLqWAA" + ), + }, + }, +} + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + loop.close() + + +@pytest.fixture(scope="module") +async def wallet(): + profile = InMemoryProfile.test_profile() + wallet = InMemoryWallet(profile) + await wallet.create_signing_key(TEST_SEED) + yield wallet + + +@pytest.fixture(scope="module") +async def mock_session(wallet): + session_inject = {BaseWallet: wallet} + context = AdminRequestContext.test_context(session_inject) + session = await context.session() + yield session + + +@pytest.mark.parametrize("input", TEST_VERIFY_OBJS) +@pytest.mark.asyncio +async def test_verify_credential(input, mock_session): + result = await verify_credential( + mock_session, input.get("doc"), input.get("verkey") + ) + assert result + + +@pytest.mark.parametrize("input", TEST_SIGN_OBJS) +@pytest.mark.asyncio +async def test_sign_credential(input, mock_session): + result = await sign_credential( + mock_session, input.get("doc"), input.get("options"), TEST_VERKEY + ) + assert "proof" in result.keys() + assert "jws" in result.get("proof", {}).keys() + + +@pytest.mark.asyncio +async def test_Invalid_JWS_header(mock_session): + with pytest.raises(InvalidJWSHeader): + await verify_credential( + mock_session, TEST_VERIFY_ERROR.get("doc"), TEST_VERIFY_ERROR.get("verkey") + ) + + +@pytest.mark.parametrize( + "verkey", + ( + "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + "did:key:z3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", + ), +) +def test_did_key(verkey): + assert did_key(verkey).startswith("did:key:z") diff --git a/aries_cloudagent/messaging/jsonld/tests/test_routes.py b/aries_cloudagent/messaging/jsonld/tests/test_routes.py index 5509ec543c..239bb07d8f 100644 --- a/aries_cloudagent/messaging/jsonld/tests/test_routes.py +++ b/aries_cloudagent/messaging/jsonld/tests/test_routes.py @@ -1,129 +1,184 @@ +import pytest +import unittest +import asyncio +import pytest +from asynctest import mock as async_mock +from ....messaging.models.base import BaseModelError +from ....wallet.error import WalletError +from ....config.base import InjectionError +from ....storage.error import StorageError, StorageNotFoundError from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock from ....admin.request_context import AdminRequestContext - +from .. import credential from .. import routes as test_module +from ....resolver.tests.test_routes import did_doc +from ....resolver.did_resolver import DIDResolver +from ....resolver.base import DIDNotFound, DIDMethodNotSupported +from ....resolver.tests.test_diddoc import DOC -# TODO: Add tests -class TestJSONLDRoutes(AsyncTestCase): - async def setUp(self): - self.context = AdminRequestContext.test_context() - self.did_info = await (await self.context.session()).wallet.create_local_did() - self.request_dict = { - "context": self.context, - "outbound_message_router": async_mock.CoroutineMock(), - } - self.request = async_mock.MagicMock( - app={}, - match_info={}, - query={}, - __getitem__=lambda _, k: self.request_dict[k], - ) - - async def test_verify_credential(self): - self.request.json = async_mock.CoroutineMock( - return_value={ # posted json - "verkey": ( - # pulled from the did:key in example - "5yKdnU7ToTjAoRNDzfuzVTfWBH38qyhE1b9xh4v8JaWF" - ), - "doc": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1", - ], - "id": "http://example.gov/credentials/3732", - "type": ["VerifiableCredential", "UniversityDegreeCredential"], - "issuer": ( - "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" +@pytest.fixture +def mock_resolver(): + did_resolver = async_mock.MagicMock() + did_resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) + did_resolver.dereference = async_mock.CoroutineMock( + return_value=DOC["verificationMethod"][1] + ) + yield did_resolver + + +@pytest.fixture +def mock_sign_credential(): + temp = test_module.sign_credential + sign_credential = async_mock.CoroutineMock(return_value="fake_signage") + test_module.sign_credential = sign_credential + yield test_module.sign_credential + test_module.sign_credential = temp + + +@pytest.fixture +def mock_verify_credential(): + temp = test_module.verify_credential + verify_credential = async_mock.CoroutineMock(return_value="fake_verify") + test_module.verify_credential = verify_credential + yield test_module.verify_credential + test_module.verify_credential = temp + + +@pytest.fixture +def mock_sign_request(mock_sign_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, + } + request = async_mock.MagicMock( + match_info={}, + query={}, + json=async_mock.CoroutineMock( + return_value={ + "verkey": "fake_verkey", + "doc": {}, + "options": { + "type": "Ed25519Signature2018", + "created": "2020-04-10T21:35:35Z", + "verificationMethod": ( + "did:key:" + "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#" + "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" ), - "issuanceDate": "2020-03-10T04:24:12.164Z", - "credentialSubject": { - "id": ( - "did:key:" - "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - ), - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts", - }, - }, - "proof": { - "type": "Ed25519Signature2018", - "created": "2020-04-10T21:35:35Z", - "verificationMethod": ( - "did:key:" - "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc" - "4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZD" - "VzrJzFrwahc4tXLt9DoHd" - ), - "proofPurpose": "assertionMethod", - "jws": ( - "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaX" - "QiOlsiYjY0Il19..l9d0YHjcFAH2H4dB9xlWFZQLUp" - "ixVCWJk0eOt4CXQe1NXKWZwmhmn9OQp6YxX0a2Lffe" - "gtYESTCJEoGVXLqWAA" - ), - }, + "proofPurpose": "assertionMethod", }, - } - ) + }, + ), + __getitem__=lambda _, k: request_dict[k], + ) + yield request - with async_mock.patch.object(test_module.web, "json_response") as mock_response: - result = await test_module.verify(self.request) - assert result == mock_response.return_value - mock_response.assert_called_once_with({"valid": True}) # expected response - async def test_sign_credential(self): - self.request.json = async_mock.CoroutineMock( - return_value={ # posted json - "verkey": self.did_info.verkey, +@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, + } + request = async_mock.MagicMock( + match_info={}, + query={}, + json=async_mock.CoroutineMock( + return_value={ "doc": { - "credential": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1", - ], - "id": "http://example.gov/credentials/3732", - "type": [ - "VerifiableCredential", - "UniversityDegreeCredential", - ], - "issuer": ( - "did:key:" - "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - ), - "issuanceDate": "2020-03-10T04:24:12.164Z", - "credentialSubject": { - "id": ( - "did:key:" - "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - ), - "degree": { - "type": "BachelorDegree", - "name": u"Bachelor of Encyclopædic Arts", - }, - }, - }, - "options": { + "@context": "https://www.w3.org/2018/credentials/v1", + "type": "VerifiablePresentation", + "holder": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + "proof": { "type": "Ed25519Signature2018", - "created": "2020-04-10T21:35:35Z", - "verificationMethod": ( - "did:key:" - "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#" - "z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - ), - "proofPurpose": "assertionMethod", + "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", }, - }, + } } - ) - - with async_mock.patch.object(test_module.web, "json_response") as mock_response: - result = await test_module.sign(self.request) - assert result == mock_response.return_value - mock_response.assert_called_once() - assert "signed_doc" in mock_response.call_args[0][0] - assert "error" not in mock_response.call_args[0][0] + ), + __getitem__=lambda _, k: request_dict[k], + ) + yield request + + +@pytest.fixture +def mock_response(): + json_response = async_mock.MagicMock() + temp_value = test_module.web.json_response + test_module.web.json_response = json_response + yield json_response + test_module.web.json_response = temp_value + + +@pytest.mark.asyncio +async def test_sign(mock_sign_request, mock_response): + await test_module.sign(mock_sign_request) + mock_response.assert_called_once_with({"signed_doc": "fake_signage"}) + + +@pytest.mark.parametrize( + "error", [DIDNotFound, DIDMethodNotSupported, WalletError, InjectionError] +) +@pytest.mark.asyncio +async def test_sign_bad_req_error(mock_sign_request, mock_response, error): + test_module.sign_credential = async_mock.CoroutineMock(side_effect=error()) + with pytest.raises(test_module.web.HTTPBadRequest): + await test_module.sign(mock_sign_request) + + +@pytest.mark.asyncio +async def test_sign_bad_ver_meth_deref_req_error( + mock_resolver, mock_sign_request, mock_response +): + mock_resolver.dereference.return_value = None + with pytest.raises(test_module.web.HTTPBadRequest): + await test_module.sign(mock_sign_request) + + +@pytest.mark.asyncio +async def test_verify(mock_verify_request, mock_response): + await test_module.verify(mock_verify_request) + mock_response.assert_called_once_with({"valid": "fake_verify"}) + + +@pytest.mark.parametrize("error", [DIDNotFound, DIDMethodNotSupported, WalletError, InjectionError]) +@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()) + with pytest.raises(test_module.web.HTTPBadRequest): + 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 + with pytest.raises(test_module.web.HTTPBadRequest): + await test_module.verify(mock_verify_request) + + +@pytest.mark.asyncio +async def test_register(): + mock_app = async_mock.MagicMock() + mock_app.add_routes = async_mock.MagicMock() + await test_module.register(mock_app) + mock_app.add_routes.assert_called_once() + + +def test_post_process_routes(): + mock_app = async_mock.MagicMock(_state={"swagger_dict": {}}) + test_module.post_process_routes(mock_app) + assert "tags" in mock_app._state["swagger_dict"] diff --git a/aries_cloudagent/resolver/base.py b/aries_cloudagent/resolver/base.py index 9166dab0a5..346a2d71f2 100644 --- a/aries_cloudagent/resolver/base.py +++ b/aries_cloudagent/resolver/base.py @@ -6,9 +6,10 @@ from .diddoc import ResolvedDIDDoc from ..core.profile import Profile +from ..core.error import BaseError -class ResolverError(Exception): +class ResolverError(BaseError): """Base class for resolver exceptions.""" diff --git a/aries_cloudagent/resolver/did.py b/aries_cloudagent/resolver/did.py index 52e7f80929..704ac83e2a 100644 --- a/aries_cloudagent/resolver/did.py +++ b/aries_cloudagent/resolver/did.py @@ -10,15 +10,20 @@ import re from typing import Dict, Union from urllib.parse import urlparse, parse_qsl, urlencode +from ..core.error import BaseError DID_PATTERN = re.compile("did:([a-z]+):((?:[a-zA-Z0-9._-]*:)*[a-zA-Z0-9._-]+)") -class InvalidDIDError(Exception): +class DIDError(BaseError): + """General did error.""" + + +class InvalidDIDError(DIDError): """Invalid DID.""" -class InvalidDIDUrlError(Exception): +class InvalidDIDUrlError(DIDError): """Invalid DID.""" diff --git a/aries_cloudagent/resolver/did_resolver.py b/aries_cloudagent/resolver/did_resolver.py index 1ef0a67498..d9aaf7c324 100644 --- a/aries_cloudagent/resolver/did_resolver.py +++ b/aries_cloudagent/resolver/did_resolver.py @@ -57,7 +57,7 @@ def _match_did_to_resolver(self, did: DID) -> BaseDIDResolver: ) resolvers = list(chain(native_resolvers, non_native_resolvers)) if not resolvers: - raise DIDMethodNotSupported(f"{did.method} not supported") + raise DIDMethodNotSupported(f"DID method '{did.method}' not supported") return resolvers async def dereference(self, profile: Profile, did_url: str) -> ResolvedDIDDoc: diff --git a/aries_cloudagent/resolver/routes.py b/aries_cloudagent/resolver/routes.py index d155763407..dc4bfd92ef 100644 --- a/aries_cloudagent/resolver/routes.py +++ b/aries_cloudagent/resolver/routes.py @@ -141,7 +141,7 @@ def post_process_routes(app: web.Application): app._state["swagger_dict"]["tags"].append( { "name": "resolver", - "description": "universal resolvers", + "description": "did resolver interface.", "externalDocs": {"description": "Specification"}, # , "url": SPEC_URI}, } )