From 3da6ec4a2b2dc7e5c9363d5dd28903a12403f635 Mon Sep 17 00:00:00 2001 From: Adam Burdett Date: Fri, 27 Oct 2023 15:39:53 -0600 Subject: [PATCH] feat: cred ex record management Signed-off-by: Adam Burdett --- oid4vci/oid4vci/v1_0/cred_ex_record.py | 71 ++++++++++++++++++ oid4vci/oid4vci/v1_0/cred_sup_record.py | 97 +++++++++++++++++++++++++ oid4vci/oid4vci/v1_0/models.py | 49 ------------- oid4vci/oid4vci/v1_0/oid4vci_server.py | 3 +- oid4vci/oid4vci/v1_0/routes.py | 92 ++++++++++++++++++++--- oid4vci/poetry.lock | 2 +- oid4vci/pyproject.toml | 1 + 7 files changed, 252 insertions(+), 63 deletions(-) create mode 100644 oid4vci/oid4vci/v1_0/cred_ex_record.py create mode 100644 oid4vci/oid4vci/v1_0/cred_sup_record.py diff --git a/oid4vci/oid4vci/v1_0/cred_ex_record.py b/oid4vci/oid4vci/v1_0/cred_ex_record.py new file mode 100644 index 000000000..9796044a2 --- /dev/null +++ b/oid4vci/oid4vci/v1_0/cred_ex_record.py @@ -0,0 +1,71 @@ +from typing import Any, Dict, Optional +from aries_cloudagent.messaging.models.base_record import BaseRecord, BaseRecordSchema, BaseExchangeRecord +from marshmallow import fields + +class OID4VCICredentialExchangeRecord(BaseExchangeRecord): + class Meta: + schema_class = "CredExRecordSchema" + + RECORD_ID_NAME = "oid4vci_ex_id" + RECORD_TYPE= "oid4vci" + EVENT_NAMESPACE= "oid4vci" + TAG_NAMES = {"nonce", "pin", "token"} + def __init__( + self, + *, + credential_supported_id=None, + credential_subject: Optional[Dict[str, Any]] = None, + nonce=None, + pin=None, + token=None, + **kwargs, + ): + super().__init__( + None, + state="init", + **kwargs, + ) + self.credential_supported_id = credential_supported_id + self.credential_subject = credential_subject # (received from submit) + self.nonce = nonce # in offer + self.pin = pin # (when relevant) + self.token = token + + @property + def credential_exchange_id(self) -> str: + """Accessor for the ID associated with this exchange.""" + return self._id + + +# TODO: add validation +class CredExRecordSchema(BaseRecordSchema): + class Meta: + model_class = OID4VCICredentialExchangeRecord + + credential_supported_id = fields.Str( + required=True, + metadata={ + "description": "Identifier used to identify credential supported record", + }, + ) + credential_subject = ( + fields.Dict( + required=True, + metadata={ + "description": "desired claim and value in credential", + }, + ), + ) + nonce = ( + fields.Str( + required=False, + ), + ) + pin = ( + fields.Str( + required=False, + ), + ) + token = fields.Str( + required=False, + ) \ No newline at end of file diff --git a/oid4vci/oid4vci/v1_0/cred_sup_record.py b/oid4vci/oid4vci/v1_0/cred_sup_record.py new file mode 100644 index 000000000..5413dad15 --- /dev/null +++ b/oid4vci/oid4vci/v1_0/cred_sup_record.py @@ -0,0 +1,97 @@ +from aries_cloudagent.messaging.models.base_record import BaseRecord, BaseRecordSchema +from marshmallow import fields + + +class OID4VCICredentialSupported(BaseRecord): + class Meta: + schema_class = "CredSupRecordSchema" + + RECORD_ID_NAME = "oid4vci_id" + RECORD_TYPE= "oid4vci" + EVENT_NAMESPACE= "oid4vci" + TAG_NAMES = {"credential_definition_id", "types", "scope"} + + def __init__( + self, + *, + credential_definition_id, + format, + types, + cryptographic_binding_methods_supported, + cryptographic_suites_supported, + display, + credential_subject, + scope, + **kwargs, + ): + super().__init__( + None, + state="init", + **kwargs, + ) + self.credential_definition_id = credential_definition_id + self.format = format + self.types = types + self.cryptographic_binding_methods_supported = ( + cryptographic_binding_methods_supported + ) + self.cryptographic_suites_supported = cryptographic_suites_supported + self.display = display + self.credential_subject = credential_subject + self.scope = scope + + def web_serialize(self) -> dict: + return self.serialize() + + @property + def id(self): + return self._id + + +# TODO: add validation +class CredSupRecordSchema(BaseRecordSchema): + class Meta: + model_class = OID4VCICredentialSupported + + credential_definition_id = fields.Str( + required=True, metadata={"example": "UniversityDegree_JWT"} + ) + format = fields.Str(required=True, metadata={"example": "jwt_vc_json"}) + types = fields.List( + fields.Str(), + metadata={"example": ["VerifiableCredential", "UniversityDegreeCredential"]}, + ) + cryptographic_binding_methods_supported = fields.List( + fields.Str(), metadata={"example": []} + ) + cryptographic_suites_supported = fields.List( + fields.Str(), metadata={"example": ["ES256K"]} + ) + display = fields.List( + fields.Dict(), + metadata={ + "example": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university", + }, + "background_color": "#12107c", + "text_color": "#FFFFFF", + } + ] + }, + ) + credential_subject = fields.Dict( + metadata={ + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Surname", "locale": "en-US"}]}, + "degree": {}, + "gpa": {"display": [{"name": "GPA"}]}, + } + ) + scope = fields.Str( + required=True, + ) diff --git a/oid4vci/oid4vci/v1_0/models.py b/oid4vci/oid4vci/v1_0/models.py index 849bb2a0b..3cb5beb1f 100644 --- a/oid4vci/oid4vci/v1_0/models.py +++ b/oid4vci/oid4vci/v1_0/models.py @@ -2,26 +2,6 @@ from aries_cloudagent.messaging.models.base_record import BaseExchangeRecord, BaseRecord -class OID4VCICredentialExchangeRecord(BaseExchangeRecord): - def __init__( - self, - credential_supported_id=None, - credential_subject: Optional[Dict[str, Any]] = None, - nonce=None, - pin=None, - token=None, - ): - self.credential_supported_id = credential_supported_id - self.credential_subject = credential_subject # (received from submit) - self.nonce = nonce # in offer - self.pin = pin # (when relevant) - self.token = token - - @property - def credential_exchange_id(self) -> str: - """Accessor for the ID associated with this exchange.""" - return self._id - class CredentialOfferRecord(BaseExchangeRecord): # TODO: do we need this? def __init__( @@ -33,32 +13,3 @@ def __init__( self.credential_issuer = credential_issuer self.credentials = credentials self.grants = grants - - -class OID4VCICredentialSupported(BaseRecord): - def __init__( - self, - credential_definition_id, - format, - types, - cryptographic_binding_methods_supported, - cryptographic_suites_supported, - display, - credentialSubject, - scope, - ): - self.credential_definition_id = credential_definition_id - self.format = format - self.types = types - self.cryptographic_binding_methods_supported = ( - cryptographic_binding_methods_supported - ) - self.cryptographic_suites_supported = cryptographic_suites_supported - self.display = display - self.credentialSubject = credentialSubject - self.scope = scope - - TAG_NAMES = {"credential_definition_id", "types", "scope"} - - def web_serialize(self) -> dict: - return self.serialize() diff --git a/oid4vci/oid4vci/v1_0/oid4vci_server.py b/oid4vci/oid4vci/v1_0/oid4vci_server.py index eefea31b7..a22190b99 100644 --- a/oid4vci/oid4vci/v1_0/oid4vci_server.py +++ b/oid4vci/oid4vci/v1_0/oid4vci_server.py @@ -23,8 +23,7 @@ from aries_cloudagent.utils.stats import Collector from aries_cloudagent.wallet.jwt import jwt_verify from marshmallow import fields -from .models import OID4VCICredentialSupported - +from .cred_sup_record import OID4VCICredentialSupported LOGGER = logging.getLogger(__name__) diff --git a/oid4vci/oid4vci/v1_0/routes.py b/oid4vci/oid4vci/v1_0/routes.py index 071150c0a..e8cb8555b 100644 --- a/oid4vci/oid4vci/v1_0/routes.py +++ b/oid4vci/oid4vci/v1_0/routes.py @@ -14,12 +14,10 @@ from aries_cloudagent.core.profile import Profile from aries_cloudagent.messaging.models.openapi import OpenAPISchema from aries_cloudagent.protocols.basicmessage.v1_0.message_types import SPEC_URI -from aries_cloudagent.utils.tracing import AdminAPIMessageTracingSchema from marshmallow import fields -from .models import ( - CredentialOfferRecord, - OID4VCICredentialExchangeRecord, -) +from .models import CredentialOfferRecord +from .cred_sup_record import OID4VCICredentialSupported +from .cred_ex_record import OID4VCICredentialExchangeRecord LOGGER = logging.getLogger(__name__) code_size = 8 # TODO: check @@ -44,9 +42,7 @@ class CredExRecordListQueryStringSchema(OpenAPISchema): ) -class CreateCredExSchema(AdminAPIMessageTracingSchema): - """Filter, auto-remove, comment, trace.""" - +class CreateCredExSchema(OpenAPISchema): credential_supported_id = fields.Str( required=True, metadata={ @@ -76,6 +72,51 @@ class CreateCredExSchema(AdminAPIMessageTracingSchema): ) +class CreateCredSupSchema(OpenAPISchema): + credential_definition_id = fields.Str( + required=True, metadata={"example": "UniversityDegree_JWT"} + ) + format = fields.Str(required=True, metadata={"example": "jwt_vc_json"}) + types = fields.List( + fields.Str(), + metadata={"example": ["VerifiableCredential", "UniversityDegreeCredential"]}, + ) + cryptographic_binding_methods_supported = fields.List( + fields.Str(), metadata={"example": []} + ) + cryptographic_suites_supported = fields.List( + fields.Str(), metadata={"example": ["ES256K"]} + ) + display = fields.List( + fields.Dict(), + metadata={ + "example": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university", + }, + "background_color": "#12107c", + "text_color": "#FFFFFF", + } + ] + }, + ) + credential_subject = fields.Dict( + metadata={ + "given_name": {"display": [{"name": "Given Name", "locale": "en-US"}]}, + "family_name": {"display": [{"name": "Surname", "locale": "en-US"}]}, + "degree": {}, + "gpa": {"display": [{"name": "GPA"}]}, + } + ) + scope = fields.Str( + required=True, + ) + + class CredExIdMatchInfoSchema(OpenAPISchema): """Path parameters and validators for request taking credential exchange id.""" @@ -221,11 +262,40 @@ async def get_cred_offer(request: web.BaseRequest): return web.json_response(record) -@docs(tags=["oid4vci"], summary="Get a credential offer") -@querystring_schema(GetCredentialOfferSchema()) +@docs(tags=["oid4vci"], summary="Register a Oid4vci credential") +@request_schema(CreateCredSupSchema()) async def credential_supported_create(request: web.BaseRequest): - pass + context = request["context"] + profile = context.profile + body = await request.json() + + credential_definition_id = body.get("credential_definition_id") + format = body.get("format") + types = body.get("types") + cryptographic_binding_methods_supported = body.get( + "cryptographic_binding_methods_supported" + ) + cryptographic_suites_supported = body.get("cryptographic_suites_supported") + display = body.get("display") + credential_subject = body.get("credential_subject") + scope = body.get("scope") + + record = OID4VCICredentialSupported( + credential_definition_id= credential_definition_id, + format= format, + types = types, + cryptographic_binding_methods_supported = cryptographic_binding_methods_supported, + cryptographic_suites_supported = cryptographic_suites_supported, + display = display, + credential_subject = credential_subject, + scope = scope, + ) + + async with profile.session() as session: + await record.save(session, reason="Save credential supported record.") + + return web.json_response(record.serialize()) @docs( tags=["oid4vci"], diff --git a/oid4vci/poetry.lock b/oid4vci/poetry.lock index 27e99a16a..00bbd2efe 100644 --- a/oid4vci/poetry.lock +++ b/oid4vci/poetry.lock @@ -2385,4 +2385,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "031fc00e0947693a073fbcf18916ca0e1ad0d9bb07aeef6d3fe79b1fe3cb0273" +content-hash = "100d16d09905469d6f7d5728e0722b63277a564fcd1555a2002b72daf4518afb" diff --git a/oid4vci/pyproject.toml b/oid4vci/pyproject.toml index 67d989710..8ff23f872 100644 --- a/oid4vci/pyproject.toml +++ b/oid4vci/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.9" aiohttp = "^3.8.5" aries-cloudagent = { version = "0.10.3" } aiohttp-cors = "^0.7.0" +marshmallow = "^3.20.1" [tool.poetry.dev-dependencies] ruff="^0.0.285"