Skip to content

Commit

Permalink
Merge pull request openwallet-foundation#31 from burdettadam/feature/…
Browse files Browse the repository at this point in the history
…json-ld-api

admin api for signing json-ld like structures using aries verkey.
  • Loading branch information
dbluhm authored Feb 19, 2021
2 parents 957cc57 + 3f1e304 commit 336d54a
Show file tree
Hide file tree
Showing 10 changed files with 624 additions and 194 deletions.
2 changes: 1 addition & 1 deletion aries_cloudagent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 7 additions & 2 deletions aries_cloudagent/messaging/jsonld/create_verify_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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(
Expand Down
28 changes: 18 additions & 10 deletions aries_cloudagent/messaging/jsonld/credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"):
Expand All @@ -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"]}
Expand All @@ -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)
Expand All @@ -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("..")
Expand All @@ -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
132 changes: 70 additions & 62 deletions aries_cloudagent/messaging/jsonld/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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},
}
)
Loading

0 comments on commit 336d54a

Please sign in to comment.