From 64c63b9cd1532160ca32678f1904a55987fa8c57 Mon Sep 17 00:00:00 2001 From: tra371 Date: Mon, 5 Feb 2024 23:59:42 +0630 Subject: [PATCH 1/4] feat: add new format and implement VCDICredFormatHandler (Draft) Signed-off-by: tra371 --- aries_cloudagent/indy/models/cred_abstract.py | 160 ++++++- aries_cloudagent/indy/models/cred_request.py | 103 ++++- .../v2_0/formats/vc_di/__init__.py | 0 .../v2_0/formats/vc_di/handler.py | 437 ++++++++++++++++++ .../issue_credential/v2_0/message_types.py | 8 +- .../v2_0/messages/cred_format.py | 9 + .../v2_0/models/detail/vc_di.py | 135 ++++++ 7 files changed, 847 insertions(+), 5 deletions(-) create mode 100644 aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/__init__.py create mode 100644 aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py create mode 100644 aries_cloudagent/protocols/issue_credential/v2_0/models/detail/vc_di.py diff --git a/aries_cloudagent/indy/models/cred_abstract.py b/aries_cloudagent/indy/models/cred_abstract.py index 9abdbd5bb3..c5e46f233b 100644 --- a/aries_cloudagent/indy/models/cred_abstract.py +++ b/aries_cloudagent/indy/models/cred_abstract.py @@ -1,6 +1,7 @@ """Cred abstract artifacts to attach to RFC 453 messages.""" -from typing import Sequence +from typing import Sequence, Union +from ...vc.vc_ld.models.credential import CredentialSchema, VerifiableCredential from marshmallow import EXCLUDE, fields @@ -152,3 +153,160 @@ class Meta: required=True, metadata={"description": "Key correctness proof"}, ) + + +class AnoncredsLinkSecret(BaseModel): + """Anoncreds Link Secret Model.""" + + class Meta: + """AnoncredsLinkSecret metadata.""" + + schema_class = "AnoncredsLinkSecretSchema" + + def __init__( + self, + nonce: str = None, + cred_def_id: str = None, + key_correctness_proof: str = None, + **kwargs, + ): + """Initialize values for AnoncredsLinkSecret.""" + super().__init__(**kwargs) + self.nonce = nonce + self.cred_def_id = cred_def_id + self.key_correctness_proof = key_correctness_proof + + +class AnoncredsLinkSecretSchema(BaseModelSchema): + """Anoncreds Link Secret Schema.""" + + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential abstract", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + cred_def_id = fields.Str( + required=True, + validate=INDY_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": INDY_CRED_DEF_ID_EXAMPLE, + }, + ) + + key_correctness_proof = fields.Nested( + IndyKeyCorrectnessProofSchema(), + required=True, + metadata={"description": "Key correctness proof"}, + ) + + +class DidcommSignedAttachment(BaseModel): + """Didcomm Signed Attachment Model.""" + + class Meta: + """DidcommSignedAttachment metadata.""" + + schema_class = "DidcommSignedAttachmentSchema" + + def __init__( + self, + algs_supported: Sequence[str] = None, + did_methods_supported: Sequence[str] = None, + nonce: str = None, + **kwargs, + ): + """Initialize values for DidcommSignedAttachment.""" + super().__init__(**kwargs) + self.algs_supported = algs_supported + self.did_methods_supported = did_methods_supported + self.nonce = nonce + + +class DidcommSignedAttachmentSchema(BaseModelSchema): + """Didcomm Signed Attachment Schema.""" + + algs_supported = fields.List(fields.Str(), required=True) + + did_methods_supported = fields.List(fields.Str(), required=True) + + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential abstract", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + +class BindingMethodSchema(BaseModelSchema): + """VCDI Binding Method Schema.""" + + anoncreds_link_secret = fields.Nested(AnoncredsLinkSecretSchema, required=False) + didcomm_signed_attachment = fields.Nested( + DidcommSignedAttachmentSchema, required=True + ) + + +class VCDICredAbstract(BaseModel): + """VCDI Credential Abstract.""" + + class Meta: + """VCDI credential abstract metadata.""" + + schema_class = "VCDICredAbstractSchema" + + def __init__( + self, + data_model_versions_supported: str = None, + binding_required: str = None, + binding_methods: str = None, + credential: Union[dict, VerifiableCredential] = None, + **kwargs, + ): + """Initialize vcdi cred abstract object. + + Args: + data_model_versions_supported: supported versions for data model + binding_required: boolean value + binding_methods: required if binding_required is true + credential: credential object + """ + super().__init__(**kwargs) + self.data_model_versions_supported = data_model_versions_supported + self.binding_required = binding_required + self.binding_methods = binding_methods + self.credential = credential + + +class VCDICredAbstractSchema(BaseModelSchema): + """VCDI Credential Abstract Schema.""" + + class Meta: + """VCDICredAbstractSchema metadata.""" + + model_class = VCDICredAbstract + unknown = EXCLUDE + + data_model_versions_supported = fields.List( + required=True, validate="", metadata={"description": "", "example": ""} + ) + + binding_required = fields.Bool( + required=False, metadata={"description": "", "example": ""} + ) + + binding_method = fields.Nested( + BindingMethodSchema(), + required=binding_required, + metadata={"description": "", "example": ""}, + ) + + credential = fields.Nested( + CredentialSchema(), required=True, metadata={"description": "", "example": ""} + ) diff --git a/aries_cloudagent/indy/models/cred_request.py b/aries_cloudagent/indy/models/cred_request.py index 805cdaa61d..a722170c32 100644 --- a/aries_cloudagent/indy/models/cred_request.py +++ b/aries_cloudagent/indy/models/cred_request.py @@ -1,6 +1,6 @@ """Cred request artifacts to attach to RFC 453 messages.""" -from typing import Mapping +from typing import Mapping, Union from marshmallow import EXCLUDE, fields @@ -79,3 +79,104 @@ class Meta: "example": NUM_STR_WHOLE_EXAMPLE, }, ) + + +class BindingProof(BaseModel): + """Binding proof model.""" + + class Meta: + """VCDI credential request schema metadata.""" + + schema_class = "BindingProofSchema" + + def __init__( + self, + entropy: str = None, + cred_def_id: str = None, + blinded_ms: Mapping = None, + blinded_ms_correctness_proof: Mapping = None, + nonce: str = None, + **kwargs, + ): + """Initialize indy credential request.""" + super().__init__(**kwargs) + self.entropy = entropy + self.cred_def_id = cred_def_id + self.blinded_ms = blinded_ms + self.blinded_ms_correctness_proof = blinded_ms_correctness_proof + self.nonce = nonce + + +class BindingProofSchema(BaseModelSchema): + """VCDI credential request schema.""" + + class Meta: + """VCDI credential request schema metadata.""" + + model_class = BindingProof + unknown = EXCLUDE + + entropy = fields.Str( + required=True, + validate=INDY_DID_VALIDATE, + metadata={"description": "Prover DID", "example": INDY_DID_EXAMPLE}, + ) + cred_def_id = fields.Str( + required=True, + validate=INDY_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": INDY_CRED_DEF_ID_EXAMPLE, + }, + ) + blinded_ms = fields.Dict( + required=True, metadata={"description": "Blinded master secret"} + ) + blinded_ms_correctness_proof = fields.Dict( + required=True, + metadata={"description": "Blinded master secret correctness proof"}, + ) + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential request", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + +class VCDICredRequest(BaseModel): + """VCDI credential request model.""" + + class Meta: + """VCDI credential request metadata.""" + + schema_class = "VCDICredRequestSchema" + + def __init__( + self, + data_model_version: str = None, + binding_proof: Union[dict, BindingProof] = None, + **kwargs, + ): + """Initialize values for VCDICredRequest.""" + super().__init__(**kwargs) + self.data_model_version = data_model_version + self.binding_proof = binding_proof + + +class VCDICredRequestSchema(BaseModelSchema): + """VCDI credential request schema.""" + + class Meta: + """VCDI credential request schema metadata.""" + + model_class = VCDICredRequest + unknown = EXCLUDE + + data_model_version = fields.str( + required=True, metadata={"description": "", "example": ""} + ) + + binding_proof = fields.str(required=True, metadata={"description": "", "example": ""}) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/__init__.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py new file mode 100644 index 0000000000..3a7bb4f0f4 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py @@ -0,0 +1,437 @@ +"""V2.0 issue-credential indy credential format handler.""" + +import json +import logging +from typing import Mapping, Tuple +from aries_cloudagent.protocols.issue_credential.v2_0.models.detail.vc_di import ( + V20CredExRecordVCDI, +) +from aries_cloudagent.vc.vc_ld.models.credential import VerifiableCredentialSchema + +from marshmallow import RAISE + +from ......anoncreds.revocation import AnonCredsRevocation + +from ......anoncreds.registry import AnonCredsRegistry +from ......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ......anoncreds.issuer import ( + AnonCredsIssuer, +) +from ......indy.models.cred import IndyCredentialSchema +from ......indy.models.cred_abstract import IndyCredAbstractSchema, VCDICredAbstractSchema +from ......indy.models.cred_request import IndyCredRequestSchema, VCDICredRequestSchema +from ......cache.base import BaseCache +from ......ledger.base import BaseLedger +from ......ledger.multiple_ledger.ledger_requests_executor import ( + GET_CRED_DEF, + IndyLedgerRequestsExecutor, +) +from ......messaging.credential_definitions.util import ( + CRED_DEF_SENT_RECORD_TYPE, + CredDefQueryStringSchema, +) +from ......messaging.decorators.attach_decorator import AttachDecorator +from ......multitenant.base import BaseMultitenantManager +from ......revocation_anoncreds.models.issuer_cred_rev_record import IssuerCredRevRecord +from ......storage.base import BaseStorage +from ...message_types import ( + ATTACHMENT_FORMAT, + CRED_20_ISSUE, + CRED_20_OFFER, + CRED_20_PROPOSAL, + CRED_20_REQUEST, +) +from ...messages.cred_format import V20CredFormat +from ...messages.cred_issue import V20CredIssue +from ...messages.cred_offer import V20CredOffer +from ...messages.cred_proposal import V20CredProposal +from ...messages.cred_request import V20CredRequest +from ...models.cred_ex_record import V20CredExRecord +from ...models.detail.indy import V20CredExRecordIndy +from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler + +LOGGER = logging.getLogger(__name__) + + +class VCDICredFormatHandler(V20CredFormatHandler): + """VCDI credential format handler.""" + + format = V20CredFormat.Format.VC_DI + + @classmethod + def validate_fields(cls, message_type: str, attachment_data: Mapping): + """Validate attachment data for a specific message type. + + Uses marshmallow schemas to validate if format specific attachment data + is valid for the specified message type. Only does structural and type + checks, does not validate if .e.g. the issuer value is valid. + + + Args: + message_type (str): The message type to validate the attachment data for. + Should be one of the message types as defined in message_types.py + attachment_data (Mapping): [description] + The attachment data to valide + + Raises: + Exception: When the data is not valid. + + """ + mapping = { + CRED_20_PROPOSAL: CredDefQueryStringSchema, + CRED_20_OFFER: VCDICredAbstractSchema, + CRED_20_REQUEST: VCDICredRequestSchema, + CRED_20_ISSUE: VerifiableCredentialSchema, + } + + # Get schema class + Schema = mapping[message_type] + + # Validate, throw if not valid + Schema(unknown=RAISE).load(attachment_data) + + async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordVCDI: + """Retrieve credential exchange detail record by cred_ex_id.""" + + async with self.profile.session() as session: + records = await VCDICredFormatHandler.format.detail.query_by_cred_ex_id( + session, cred_ex_id + ) + + if len(records) > 1: + LOGGER.warning( + "Cred ex id %s has %d %s detail records: should be 1", + cred_ex_id, + len(records), + VCDICredFormatHandler.format.api, + ) + return records[0] if records else None + + async def _check_uniqueness(self, cred_ex_id: str): + """Raise exception on evidence that cred ex already has cred issued to it.""" + async with self.profile.session() as session: + exist = await VCDICredFormatHandler.format.detail.query_by_cred_ex_id( + session, cred_ex_id + ) + if exist: + raise V20CredFormatError( + f"{VCDICredFormatHandler.format.api} detail record already " + f"exists for cred ex id {cred_ex_id}" + ) + + def get_format_identifier(self, message_type: str) -> str: + """Get attachment format identifier for format and message combination. + + Args: + message_type (str): Message type for which to return the format identifier + + Returns: + str: Issue credential attachment format identifier + + """ + return ATTACHMENT_FORMAT[message_type][VCDICredFormatHandler.format.api] + + def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment: + """Get credential format and attachment objects for use in cred ex messages. + + Returns a tuple of both credential format and attachment decorator for use + in credential exchange messages. It looks up the correct format identifier and + encodes the data as a base64 attachment. + + Args: + message_type (str): The message type for which to return the cred format. + Should be one of the message types defined in the message types file + data (dict): The data to include in the attach decorator + + Returns: + CredFormatAttachment: Credential format and attachment data objects + + """ + return ( + V20CredFormat( + attach_id=VCDICredFormatHandler.format.api, + format_=self.get_format_identifier(message_type), + ), + AttachDecorator.data_base64(data, ident=VCDICredFormatHandler.format.api), + ) + + async def _match_sent_cred_def_id(self, tag_query: Mapping[str, str]) -> str: + """Return most recent matching id of cred def that agent sent to ledger.""" + + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + found = await storage.find_all_records( + type_filter=CRED_DEF_SENT_RECORD_TYPE, tag_query=tag_query + ) + if not found: + raise V20CredFormatError( + f"Issuer has no operable cred def for proposal spec {tag_query}" + ) + return max(found, key=lambda r: int(r.tags["epoch"])).tags["cred_def_id"] + + async def create_proposal( + self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] + ) -> Tuple[V20CredFormat, AttachDecorator]: + """Create indy credential proposal.""" + if proposal_data is None: + proposal_data = {} + + return self.get_format_data(CRED_20_PROPOSAL, proposal_data) + + async def receive_proposal( + self, cred_ex_record: V20CredExRecord, cred_proposal_message: V20CredProposal + ) -> None: + """Receive vcdi credential proposal. + + No custom handling is required for this step. + """ + + async def create_offer( + self, cred_proposal_message: V20CredProposal + ) -> CredFormatAttachment: + """Create vcdi credential offer.""" + + issuer = AnonCredsIssuer(self.profile) + ledger = self.profile.inject(BaseLedger) + cache = self.profile.inject_or(BaseCache) + + cred_def_id = await issuer.match_created_credential_definitions( + **cred_proposal_message.attachment(VCDICredFormatHandler.format) + ) + + async def _create(): + # TODO - implement a separate create_credential_offer for vcdi + offer_json = await issuer.create_credential_offer(cred_def_id) + return json.loads(offer_json) + + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + async with ledger: + schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) + schema = await ledger.get_schema(schema_id) + schema_attrs = set(schema["attrNames"]) + preview_attrs = set(cred_proposal_message.credential_preview.attr_dict()) + if preview_attrs != schema_attrs: + raise V20CredFormatError( + f"Preview attributes {preview_attrs} " + f"mismatch corresponding schema attributes {schema_attrs}" + ) + + cred_offer = None + cache_key = f"credential_offer::{cred_def_id}" + + if cache: + async with cache.acquire(cache_key) as entry: + if entry.result: + cred_offer = entry.result + else: + cred_offer = await _create() + await entry.set_result(cred_offer, 3600) + if not cred_offer: + cred_offer = await _create() + + return self.get_format_data(CRED_20_OFFER, cred_offer) + + async def receive_offer( + self, cred_ex_record: V20CredExRecord, cred_offer_message: V20CredOffer + ) -> None: + """Receive indy credential offer.""" + + async def create_request( + self, cred_ex_record: V20CredExRecord, request_data: Mapping = None + ) -> CredFormatAttachment: + """Create indy credential request.""" + if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: + raise V20CredFormatError( + "Indy issue credential format cannot start from credential request" + ) + + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + holder_did = request_data.get("holder_did") if request_data else None + cred_offer = cred_ex_record.cred_offer.attachment(VCDICredFormatHandler.format) + + if "nonce" not in cred_offer: + raise V20CredFormatError("Missing nonce in credential offer") + + nonce = cred_offer["nonce"] + cred_def_id = cred_offer["cred_def_id"] + + async def _create(): + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred_def_id + ) + + holder = AnonCredsHolder(self.profile) + request_json, metadata_json = await holder.create_credential_request( + cred_offer, cred_def_result.credential_definition, holder_did + ) + + return { + "request": json.loads(request_json), + "metadata": json.loads(metadata_json), + } + + cache_key = f"credential_request::{cred_def_id}::{holder_did}::{nonce}" + cred_req_result = None + cache = self.profile.inject_or(BaseCache) + if cache: + async with cache.acquire(cache_key) as entry: + if entry.result: + cred_req_result = entry.result + else: + cred_req_result = await _create() + await entry.set_result(cred_req_result, 3600) + if not cred_req_result: + cred_req_result = await _create() + + detail_record = V20CredExRecordVCDI( + cred_ex_id=cred_ex_record.cred_ex_id, + cred_request_metadata=cred_req_result["metadata"], + ) + + async with self.profile.session() as session: + await detail_record.save(session, reason="create v2.0 credential request") + + return self.get_format_data(CRED_20_REQUEST, cred_req_result["request"]) + + async def receive_request( + self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest + ) -> None: + """Receive indy credential request.""" + if not cred_ex_record.cred_offer: + raise V20CredFormatError( + "Indy issue credential format cannot start from credential request" + ) + + async def issue_credential( + self, cred_ex_record: V20CredExRecord, retries: int = 5 + ) -> CredFormatAttachment: + """Issue indy credential.""" + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + cred_offer = cred_ex_record.cred_offer.attachment(VCDICredFormatHandler.format) + cred_request = cred_ex_record.cred_request.attachment( + VCDICredFormatHandler.format + ) + cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict(decode=False) + + issuer = AnonCredsIssuer(self.profile) + cred_def_id = cred_offer["cred_def_id"] + if await issuer.cred_def_supports_revocation(cred_def_id): + revocation = AnonCredsRevocation(self.profile) + cred_json, cred_rev_id, rev_reg_def_id = await revocation.create_credential( + cred_offer, cred_request, cred_values + ) + else: + # TODO - implement a separate create_credential for vcdi + cred_json = await issuer.create_credential( + cred_offer, cred_request, cred_values + ) + cred_rev_id = None + rev_reg_def_id = None + + result = self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + + async with self._profile.transaction() as txn: + detail_record = V20CredExRecordIndy( + cred_ex_id=cred_ex_record.cred_ex_id, + rev_reg_id=rev_reg_def_id, + cred_rev_id=cred_rev_id, + ) + await detail_record.save(txn, reason="v2.0 issue credential") + + if cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.cred_ex_id, + cred_ex_version=IssuerCredRevRecord.VERSION_2, + rev_reg_id=rev_reg_def_id, + cred_rev_id=cred_rev_id, + ) + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_def_id}, index {cred_rev_id}" + ), + ) + await txn.commit() + + return result + + async def receive_credential( + self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue + ) -> None: + """Receive indy credential. + + Validation is done in the store credential step. + """ + + async def store_credential( + self, cred_ex_record: V20CredExRecord, cred_id: str = None + ) -> None: + """Store indy credential.""" + cred = cred_ex_record.cred_issue.attachment(VCDICredFormatHandler.format) + + rev_reg_def = None + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred["cred_def_id"] + ) + if cred.get("rev_reg_id"): + rev_reg_def_result = ( + await anoncreds_registry.get_revocation_registry_definition( + self.profile, cred["rev_reg_id"] + ) + ) + rev_reg_def = rev_reg_def_result.revocation_registry + + holder = AnonCredsHolder(self.profile) + cred_offer_message = cred_ex_record.cred_offer + mime_types = None + if cred_offer_message and cred_offer_message.credential_preview: + mime_types = cred_offer_message.credential_preview.mime_types() or None + + if rev_reg_def: + revocation = AnonCredsRevocation(self.profile) + await revocation.get_or_fetch_local_tails_path(rev_reg_def) + try: + detail_record = await self.get_detail_record(cred_ex_record.cred_ex_id) + if detail_record is None: + raise V20CredFormatError( + f"No credential exchange {VCDICredFormatHandler.format.aries} " + f"detail record found for cred ex id {cred_ex_record.cred_ex_id}" + ) + cred_id_stored = await holder.store_credential( + cred_def_result.credential_definition.serialize(), + cred, + detail_record.cred_request_metadata, + mime_types, + credential_id=cred_id, + rev_reg_def=rev_reg_def.serialize() if rev_reg_def else None, + ) + + detail_record.cred_id_stored = cred_id_stored + detail_record.rev_reg_id = cred.get("rev_reg_id", None) + detail_record.cred_rev_id = cred.get("cred_rev_id", None) + + async with self.profile.session() as session: + # Store detail record, emit event + await detail_record.save( + session, reason="store credential v2.0", event=True + ) + except AnonCredsHolderError as e: + LOGGER.error(f"Error storing credential: {e.error_code} - {e.message}") + raise e diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py b/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py index 33c7241ae1..b2b9e4325e 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py @@ -22,9 +22,7 @@ MESSAGE_TYPES = DIDCommPrefix.qualify_all( { - CRED_20_PROPOSAL: ( - f"{PROTOCOL_PACKAGE}.messages.cred_proposal.V20CredProposal" - ), + CRED_20_PROPOSAL: (f"{PROTOCOL_PACKAGE}.messages.cred_proposal.V20CredProposal"), CRED_20_OFFER: f"{PROTOCOL_PACKAGE}.messages.cred_offer.V20CredOffer", CRED_20_REQUEST: f"{PROTOCOL_PACKAGE}.messages.cred_request.V20CredRequest", CRED_20_ISSUE: f"{PROTOCOL_PACKAGE}.messages.cred_issue.V20CredIssue", @@ -43,18 +41,22 @@ CRED_20_PROPOSAL: { V20CredFormat.Format.INDY.api: "hlindy/cred-filter@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", + V20CredFormat.Format.VC_DI.api: "hlindy/cred-filter@v2.0", }, CRED_20_OFFER: { V20CredFormat.Format.INDY.api: "hlindy/cred-abstract@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc-offer@v0.1", }, CRED_20_REQUEST: { V20CredFormat.Format.INDY.api: "hlindy/cred-req@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc-request@v0.1", }, CRED_20_ISSUE: { V20CredFormat.Format.INDY.api: "hlindy/cred@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc@v0.1", }, } diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py index 374a188a3a..0974ac2f38 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py @@ -13,6 +13,7 @@ from .....utils.classloader import DeferLoad from ..models.detail.indy import V20CredExRecordIndy from ..models.detail.ld_proof import V20CredExRecordLDProof +from ..models.detail.vc_di import V20CredExRecordVCDI if TYPE_CHECKING: from ..formats.handler import V20CredFormatHandler @@ -60,6 +61,14 @@ class Format(Enum): ".formats.ld_proof.handler.LDProofCredFormatHandler" ), ) + VC_DI = FormatSpec( + "didcomm/", + V20CredExRecordVCDI, + DeferLoad( + "aries_cloudagent.protocols.issue_credential.v2_0" + ".formats.vc_di.handler.VCDICredFormatHandler" + ), + ) @classmethod def get(cls, label: Union[str, "V20CredFormat.Format"]): diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/models/detail/vc_di.py b/aries_cloudagent/protocols/issue_credential/v2_0/models/detail/vc_di.py new file mode 100644 index 0000000000..5020791f87 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/models/detail/vc_di.py @@ -0,0 +1,135 @@ +"""Indy-specific credential exchange information with non-secrets storage.""" + +from typing import Any, Mapping, Sequence + +from marshmallow import EXCLUDE, fields + +from ......core.profile import ProfileSession +from ......messaging.models.base_record import BaseRecord, BaseRecordSchema +from ......messaging.valid import ( + INDY_CRED_REV_ID_EXAMPLE, + INDY_CRED_REV_ID_VALIDATE, + INDY_REV_REG_ID_EXAMPLE, + INDY_REV_REG_ID_VALIDATE, + UUID4_EXAMPLE, +) +from .. import UNENCRYPTED_TAGS + + +class V20CredExRecordVCDI(BaseRecord): + """Credential exchange vc_di detail record.""" + + class Meta: + """V20CredExRecordIndy metadata.""" + + schema_class = "V20CredExRecordVCDISchema" + + RECORD_ID_NAME = "cred_ex_vc_di_id" + RECORD_TYPE = "vc_di_cred_ex_v20" + TAG_NAMES = {"~cred_ex_id"} if UNENCRYPTED_TAGS else {"cred_ex_id"} + RECORD_TOPIC = "issue_credential_v2_0_vc_di" + + def __init__( + self, + cred_ex_vc_di_id: str = None, + *, + cred_ex_id: str = None, + cred_id_stored: str = None, + cred_request_metadata: Mapping = None, + rev_reg_id: str = None, + cred_rev_id: str = None, + **kwargs, + ): + """Initialize indy credential exchange record details.""" + super().__init__(cred_ex_vc_di_id, **kwargs) + + self.cred_ex_id = cred_ex_id + self.cred_id_stored = cred_id_stored + self.cred_request_metadata = cred_request_metadata + self.rev_reg_id = rev_reg_id + self.cred_rev_id = cred_rev_id + + @property + def cred_ex_vc_di_id(self) -> str: + """Accessor for the ID associated with this exchange.""" + return self._id + + @property + def record_value(self) -> dict: + """Accessor for the JSON record value generated for this credential exchange.""" + return { + prop: getattr(self, prop) + for prop in ( + "cred_id_stored", + "cred_request_metadata", + "rev_reg_id", + "cred_rev_id", + ) + } + + @classmethod + async def query_by_cred_ex_id( + cls, + session: ProfileSession, + cred_ex_id: str, + ) -> Sequence["V20CredExRecordVCDI"]: + """Retrieve credential exchange indy detail record(s) by its cred ex id.""" + return await cls.query( + session=session, + tag_filter={"cred_ex_id": cred_ex_id}, + ) + + def __eq__(self, other: Any) -> bool: + """Comparison between records.""" + return super().__eq__(other) + + +class V20CredExRecordVCDISchema(BaseRecordSchema): + """Credential exchange indy detail record detail schema.""" + + class Meta: + """Credential exchange indy detail record schema metadata.""" + + model_class = V20CredExRecordVCDI + unknown = EXCLUDE + + cred_ex_indy_id = fields.Str( + required=False, + metadata={"description": "Record identifier", "example": UUID4_EXAMPLE}, + ) + cred_ex_id = fields.Str( + required=False, + metadata={ + "description": "Corresponding v2.0 credential exchange record identifier", + "example": UUID4_EXAMPLE, + }, + ) + cred_id_stored = fields.Str( + required=False, + metadata={ + "description": "Credential identifier stored in wallet", + "example": UUID4_EXAMPLE, + }, + ) + cred_request_metadata = fields.Dict( + required=False, + metadata={"description": "Credential request metadata for indy holder"}, + ) + rev_reg_id = fields.Str( + required=False, + validate=INDY_REV_REG_ID_VALIDATE, + metadata={ + "description": "Revocation registry identifier", + "example": INDY_REV_REG_ID_EXAMPLE, + }, + ) + cred_rev_id = fields.Str( + required=False, + validate=INDY_CRED_REV_ID_VALIDATE, + metadata={ + "description": ( + "Credential revocation identifier within revocation registry" + ), + "example": INDY_CRED_REV_ID_EXAMPLE, + }, + ) From 9fdc07f076809471f18cd09a62adf75b63119e97 Mon Sep 17 00:00:00 2001 From: tra371 Date: Thu, 15 Feb 2024 00:01:14 +0630 Subject: [PATCH 2/4] feat: implement VCDICredFormatHandler Signed-off-by: tra371 --- aries_cloudagent/anoncreds/issuer.py | 58 +++++++++ aries_cloudagent/indy/models/cred_request.py | 85 +++++++++++- .../v2_0/formats/vc_di/handler.py | 82 ++++++++---- demo/runners/faber.py | 121 +++++++++++++++++- demo/runners/support/agent.py | 57 +++------ 5 files changed, 328 insertions(+), 75 deletions(-) diff --git a/aries_cloudagent/anoncreds/issuer.py b/aries_cloudagent/anoncreds/issuer.py index ad49e11ce9..6c67cf1f2a 100644 --- a/aries_cloudagent/anoncreds/issuer.py +++ b/aries_cloudagent/anoncreds/issuer.py @@ -598,3 +598,61 @@ async def create_credential( raise AnonCredsIssuerError("Error creating credential") from err return credential.to_json() + + +async def create_credential_vc_di( + self, + credential_offer: dict, + credential_request: dict, + credential_values: dict, +) -> str: + """Create Credential.""" + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + schema_id = credential_offer["schema_id"] + schema_result = await anoncreds_registry.get_schema(self.profile, schema_id) + cred_def_id = credential_offer["cred_def_id"] + schema_attributes = schema_result.schema_value.attr_names + + try: + async with self.profile.session() as session: + cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) + cred_def_private = await session.handle.fetch( + CATEGORY_CRED_DEF_PRIVATE, cred_def_id + ) + except AskarError as err: + raise AnonCredsIssuerError("Error retrieving credential definition") from err + + if not cred_def or not cred_def_private: + raise AnonCredsIssuerError( + "Credential definition not found for credential issuance" + ) + + raw_values = {} + for attribute in schema_attributes: + # Ensure every attribute present in schema to be set. + # Extraneous attribute names are ignored. + try: + credential_value = credential_values[attribute] + except KeyError: + raise AnonCredsIssuerError( + "Provided credential values are missing a value " + f"for the schema attribute '{attribute}'" + ) + + raw_values[attribute] = str(credential_value) + + try: + credential = await asyncio.get_event_loop().run_in_executor( + None, + lambda: Credential.create( + cred_def.raw_value, + cred_def_private.raw_value, + credential_offer, + credential_request, + raw_values, + ), + ) + except AnoncredsError as err: + raise AnonCredsIssuerError("Error creating credential") from err + + return credential.to_json() diff --git a/aries_cloudagent/indy/models/cred_request.py b/aries_cloudagent/indy/models/cred_request.py index a722170c32..636e2b0f72 100644 --- a/aries_cloudagent/indy/models/cred_request.py +++ b/aries_cloudagent/indy/models/cred_request.py @@ -81,13 +81,14 @@ class Meta: ) -class BindingProof(BaseModel): +class AnoncredsLinkSecret(BaseModel): """Binding proof model.""" class Meta: """VCDI credential request schema metadata.""" schema_class = "BindingProofSchema" + unknown = EXCLUDE def __init__( self, @@ -107,13 +108,13 @@ def __init__( self.nonce = nonce -class BindingProofSchema(BaseModelSchema): +class AnoncredsLinkSecretSchema(BaseModelSchema): """VCDI credential request schema.""" class Meta: """VCDI credential request schema metadata.""" - model_class = BindingProof + model_class = AnoncredsLinkSecret unknown = EXCLUDE entropy = fields.Str( @@ -146,6 +147,76 @@ class Meta: ) +class DidcommSignedAttachment(BaseModel): + """Didcomm Signed Attachment Model.""" + + class Meta: + """Didcomm signed attachment metadata.""" + + schema_class = "DidcommSignedAttachmentSchema" + unknown = EXCLUDE + + def __init__(self, attachment_id: str = None, **kwargs): + """Initialize DidcommSignedAttachment.""" + super().__init__(**kwargs) + self.attachment_id = attachment_id + + +class DidcommSignedAttachmentSchema(BaseModelSchema): + """Didcomm Signed Attachment Schema.""" + + class Meta: + """Didcomm Signed Attachment schema metadata.""" + + model_class = DidcommSignedAttachment + + attachment_id = fields.str( + required=True, metadata={"description": "", "example": ""} + ) + + +class BindingProof(BaseModel): + """Binding Proof Model.""" + + class Meta: + """Binding proof metadata.""" + + schema_class = "BindingProofSchema" + unknown = EXCLUDE + + def __init__( + self, + anoncreds_link_secret: str = None, + didcomm_signed_attachment: str = None, + **kwargs, + ): + """Initialize binding proof.""" + super().__init__(**kwargs) + self.anoncreds_link_secret = anoncreds_link_secret + self.didcomm_signed_attachment = didcomm_signed_attachment + + +class BindingProofSchema(BaseModelSchema): + """Binding Proof Schema.""" + + class Meta: + """Binding proof schema metadata.""" + + model_class = BindingProof + + anoncreds_link_secret = fields.Nested( + AnoncredsLinkSecretSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) + + didcomm_signed_attachment = fields.Nested( + DidcommSignedAttachmentSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) + + class VCDICredRequest(BaseModel): """VCDI credential request model.""" @@ -175,8 +246,12 @@ class Meta: model_class = VCDICredRequest unknown = EXCLUDE - data_model_version = fields.str( + data_model_version = fields.Str( required=True, metadata={"description": "", "example": ""} ) - binding_proof = fields.str(required=True, metadata={"description": "", "example": ""}) + binding_proof = fields.Nested( + BindingProofSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py index 3a7bb4f0f4..7f37cd8541 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py @@ -3,10 +3,18 @@ import json import logging from typing import Mapping, Tuple +from aries_cloudagent.protocols.issue_credential.v2_0.manager import ( + V20CredManager, + V20CredManagerError, +) from aries_cloudagent.protocols.issue_credential.v2_0.models.detail.vc_di import ( V20CredExRecordVCDI, ) -from aries_cloudagent.vc.vc_ld.models.credential import VerifiableCredentialSchema +from aries_cloudagent.vc.vc_ld.manager import VcLdpManager, VcLdpManagerError +from aries_cloudagent.vc.vc_ld.models.credential import ( + VerifiableCredential, + VerifiableCredentialSchema, +) from marshmallow import RAISE @@ -18,8 +26,17 @@ AnonCredsIssuer, ) from ......indy.models.cred import IndyCredentialSchema -from ......indy.models.cred_abstract import IndyCredAbstractSchema, VCDICredAbstractSchema -from ......indy.models.cred_request import IndyCredRequestSchema, VCDICredRequestSchema +from ......indy.models.cred_abstract import ( + IndyCredAbstractSchema, + VCDICredAbstract, + VCDICredAbstractSchema, +) +from ......indy.models.cred_request import ( + BindingProof, + IndyCredRequestSchema, + VCDICredRequest, + VCDICredRequestSchema, +) from ......cache.base import BaseCache from ......ledger.base import BaseLedger from ......ledger.multiple_ledger.ledger_requests_executor import ( @@ -47,7 +64,6 @@ from ...messages.cred_proposal import V20CredProposal from ...messages.cred_request import V20CredRequest from ...models.cred_ex_record import V20CredExRecord -from ...models.detail.indy import V20CredExRecordIndy from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler LOGGER = logging.getLogger(__name__) @@ -260,11 +276,18 @@ async def create_request( holder_did = request_data.get("holder_did") if request_data else None cred_offer = cred_ex_record.cred_offer.attachment(VCDICredFormatHandler.format) - if "nonce" not in cred_offer: - raise V20CredFormatError("Missing nonce in credential offer") + if ( + "anoncreds_link_secret" in cred_offer["binding_method"] + and "nonce" not in cred_offer["binding_method"]["anoncreds_link_secret"] + ): + raise V20CredFormatError( + "Missing nonce in credential offer with anoncreds link secret binding method" + ) - nonce = cred_offer["nonce"] - cred_def_id = cred_offer["cred_def_id"] + nonce = cred_offer["binding_method"]["anoncreds_link_secret"]["nonce"] + cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"][ + "cred_def_id" + ] async def _create(): anoncreds_registry = self.profile.inject(AnonCredsRegistry) @@ -321,31 +344,36 @@ async def issue_credential( """Issue indy credential.""" await self._check_uniqueness(cred_ex_record.cred_ex_id) - cred_offer = cred_ex_record.cred_offer.attachment(VCDICredFormatHandler.format) - cred_request = cred_ex_record.cred_request.attachment( + attached_credential = cred_ex_record.cred_offer.attachment( VCDICredFormatHandler.format ) - cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict(decode=False) - - issuer = AnonCredsIssuer(self.profile) - cred_def_id = cred_offer["cred_def_id"] - if await issuer.cred_def_supports_revocation(cred_def_id): - revocation = AnonCredsRevocation(self.profile) - cred_json, cred_rev_id, rev_reg_def_id = await revocation.create_credential( - cred_offer, cred_request, cred_values - ) - else: - # TODO - implement a separate create_credential for vcdi - cred_json = await issuer.create_credential( - cred_offer, cred_request, cred_values + detail_credential = VCDICredAbstract.deserialize(attached_credential) + binding_proof = cred_ex_record.cred_request.attachment( + VCDICredFormatHandler.format + ) + detail_proof = VCDICredRequest.deserialize(binding_proof) + manager = self.profile.inject(VcLdpManager) + # TODO - implement a separate create_credential for vcdi + assert detail_credential.credential and isinstance( + detail_credential.credential, VerifiableCredential + ) + assert detail_proof.binding_proof and isinstance( + detail_proof.binding_proof, BindingProof + ) + try: + vc = await manager.issue( + detail_credential.credential, detail_proof.binding_proof ) - cred_rev_id = None - rev_reg_def_id = None + except VcLdpManagerError as err: + raise V20CredFormatError("Failed to issue credential") from err + + result = self.get_format_data(CRED_20_ISSUE, vc.serialize()) - result = self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + cred_rev_id = None + rev_reg_def_id = None async with self._profile.transaction() as txn: - detail_record = V20CredExRecordIndy( + detail_record = V20CredExRecordVCDI( cred_ex_id=cred_ex_record.cred_ex_id, rev_reg_id=rev_reg_def_id, cred_rev_id=cred_rev_id, diff --git a/demo/runners/faber.py b/demo/runners/faber.py index d8b9e4347f..be5a5b0b3c 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -19,6 +19,7 @@ from runners.support.agent import ( # noqa:E402 CRED_FORMAT_INDY, CRED_FORMAT_JSON_LD, + CRED_FORMAT_VC_DI, SIG_TYPE_BLS, ) from runners.support.utils import ( # noqa:E402 @@ -175,6 +176,31 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin } return offer_request + elif cred_type == CRED_FORMAT_VC_DI: + self.cred_attrs[cred_def_id] = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "birthdate_dateint": birth_date.strftime(birth_date_format), + "timestamp": str(int(time.time())), + } + + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} + for (n, v) in self.cred_attrs[cred_def_id].items() + ], + } + offer_request = { + "connection_id": self.connection_id, + "comment": f"Offer on cred def id {cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "filter": {"vc_di": {"cred_def_id": cred_def_id}}, + "trace": exchange_tracing, + } + return offer_request else: raise Exception(f"Error invalid credential type: {self.cred_type}") @@ -314,6 +340,70 @@ def generate_proof_request_web_request( proof_request_web_request["connection_id"] = self.connection_id return proof_request_web_request + elif cred_type == CRED_FORMAT_VC_DI: + req_attrs = [ + { + "name": "name", + "restrictions": [{"schema_name": "degree schema"}], + }, + { + "name": "date", + "restrictions": [{"schema_name": "degree schema"}], + }, + ] + if revocation: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + "non_revoked": {"to": int(time.time() - 1)}, + }, + ) + else: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + } + ) + if SELF_ATTESTED: + # test self-attested claims + req_attrs.append( + {"name": "self_attested_thing"}, + ) + req_preds = [ + # test zero-knowledge proofs + { + "name": "birthdate_dateint", + "p_type": "<=", + "p_value": int(birth_date.strftime(birth_date_format)), + "restrictions": [{"schema_name": "degree schema"}], + } + ] + + vc_di_proof_request = { + "name": "Proof of Education", + "version": "1.0", + "requested_attributes": { + f"0_{req_attr['name']}_uuid": req_attr for req_attr in req_attrs + }, + "requested_predicates": { + f"0_{req_pred['name']}_GE_uuid": req_pred + for req_pred in req_preds + }, + } + + if revocation: + vc_di_proof_request["non_revoked"] = {"to": int(time.time())} + + proof_request_web_request = { + "presentation_request": {"vc_di": vc_di_proof_request}, + "trace": exchange_tracing, + } + if not connectionless: + proof_request_web_request["connection_id"] = self.connection_id + return proof_request_web_request + elif cred_type == CRED_FORMAT_JSON_LD: proof_request_web_request = { "comment": "test proof request for json-ld", @@ -445,7 +535,7 @@ async def main(args): else False ), ) - elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: + elif faber_agent.cred_type == (CRED_FORMAT_JSON_LD or CRED_FORMAT_VC_DI): faber_agent.public_did = True await faber_agent.initialize(the_agent=agent) else: @@ -562,6 +652,14 @@ async def main(args): exchange_tracing, ) + elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + offer_request = faber_agent.agent.generate_credential_offer( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.cred_def_id, + exchange_tracing, + ) + else: raise Exception( f"Error invalid credential type: {faber_agent.cred_type}" @@ -611,6 +709,16 @@ async def main(args): ) ) + elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + ) + ) + else: raise Exception( "Error invalid credential type:" + faber_agent.cred_type @@ -678,6 +786,17 @@ async def main(args): connectionless=True, ) ) + + elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + connectionless=True, + ) + ) else: raise Exception( "Error invalid credential type:" + faber_agent.cred_type diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index a114ef00a3..0dc2a2f1d2 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -72,6 +72,7 @@ CRED_FORMAT_INDY = "indy" CRED_FORMAT_JSON_LD = "json-ld" +CRED_FORMAT_VC_DI = "vc_di" DID_METHOD_SOV = "sov" DID_METHOD_KEY = "key" KEY_TYPE_ED255 = "ed25519" @@ -369,9 +370,7 @@ async def register_schema_and_creddef_indy( log_msg("Schema ID:", schema_id) # Create a cred def for the schema - cred_def_tag = ( - tag if tag else (self.ident + "." + schema_name).replace(" ", "_") - ) + cred_def_tag = tag if tag else (self.ident + "." + schema_name).replace(" ", "_") credential_definition_body = { "schema_id": schema_id, "support_revocation": support_revocation, @@ -401,9 +400,7 @@ async def register_schema_and_creddef_indy( credential_definition_response = await self.admin_GET( "/credential-definitions/created" ) - if 0 == len( - credential_definition_response["credential_definition_ids"] - ): + if 0 == len(credential_definition_response["credential_definition_ids"]): await asyncio.sleep(1.0) attempts = attempts - 1 credential_definition_id = credential_definition_response[ @@ -450,9 +447,7 @@ async def register_schema_and_creddef_anoncreds( log_msg("Schema ID:", schema_id) # Create a cred def for the schema - cred_def_tag = ( - tag if tag else (self.ident + "." + schema_name).replace(" ", "_") - ) + cred_def_tag = tag if tag else (self.ident + "." + schema_name).replace(" ", "_") max_cred_num = revocation_registry_size if revocation_registry_size else 0 credential_definition_body = { "credential_definition": { @@ -488,9 +483,7 @@ async def register_schema_and_creddef_anoncreds( credential_definition_response = await self.admin_GET( "/anoncreds/credential-definitions" ) - if 0 == len( - credential_definition_response["credential_definition_ids"] - ): + if 0 == len(credential_definition_response["credential_definition_ids"]): await asyncio.sleep(1.0) attempts = attempts - 1 credential_definition_id = credential_definition_response[ @@ -707,9 +700,7 @@ async def register_did( nym_info = data else: log_msg("using ledger: " + ledger_url + "/register") - resp = await self.client_session.post( - ledger_url + "/register", json=data - ) + resp = await self.client_session.post(ledger_url + "/register", json=data) if resp.status != 200: raise Exception( f"Error registering DID {data}, response code {resp.status}" @@ -906,9 +897,7 @@ def _process(self, args, env, loop): def get_process_args(self): return list( - flatten( - ([PYTHON, "-m", "aries_cloudagent", "start"], self.get_agent_args()) - ) + flatten(([PYTHON, "-m", "aries_cloudagent", "start"], self.get_agent_args())) ) async def start_process(self, python_path: str = None, wait: bool = True): @@ -1124,9 +1113,7 @@ async def admin_GET( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] response = await self.admin_request( "GET", path, None, text, params, headers=headers ) @@ -1178,9 +1165,7 @@ async def admin_POST( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] response = await self.admin_request( "POST", path, data, text, params, headers=headers ) @@ -1201,9 +1186,7 @@ async def admin_PATCH( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] return await self.admin_request( "PATCH", path, data, text, params, headers=headers ) @@ -1218,9 +1201,7 @@ async def admin_PUT( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] return await self.admin_request( "PUT", path, data, text, params, headers=headers ) @@ -1240,9 +1221,7 @@ async def admin_DELETE( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] response = await self.admin_request( "DELETE", path, data, text, params, headers=headers ) @@ -1261,9 +1240,7 @@ async def admin_GET_FILE(self, path, params=None, headers=None) -> bytes: if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] params = {k: v for (k, v) in (params or {}).items() if v is not None} resp = await self.client_session.request( "GET", self.admin_url + path, params=params, headers=headers @@ -1279,9 +1256,7 @@ async def admin_PUT_FILE(self, files, url, params=None, headers=None) -> bytes: if self.multitenant: if not headers: headers = {} - headers["Authorization"] = ( - "Bearer " + self.managed_wallet_params["token"] - ) + headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] params = {k: v for (k, v) in (params or {}).items() if v is not None} resp = await self.client_session.request( "PUT", url, params=params, data=files, headers=headers @@ -1653,9 +1628,7 @@ async def handle_connections(self, message): # setup endorser meta-data on our connection log_msg("Setup endorser agent meta-data ...") await self.admin_POST( - "/transactions/" - + self.endorser_connection_id - + "/set-endorser-role", + "/transactions/" + self.endorser_connection_id + "/set-endorser-role", params={"transaction_my_job": "TRANSACTION_ENDORSER"}, ) From d66caa422dc2d8bee9d530f157ea23608abde6c8 Mon Sep 17 00:00:00 2001 From: tra371 Date: Tue, 20 Feb 2024 00:11:57 +0630 Subject: [PATCH 3/4] feat: fix schemas on cred_abstract and cred_request, improve on vc_di support on alice-faber demo Signed-off-by: tra371 --- aries_cloudagent/anoncreds/issuer.py | 107 +++++++++--------- aries_cloudagent/indy/models/cred_abstract.py | 46 +++++++- aries_cloudagent/indy/models/cred_request.py | 11 +- .../v2_0/formats/vc_di/handler.py | 22 ++-- .../issue_credential/v2_0/message_types.py | 4 +- demo/runners/agent_container.py | 66 +++++------ demo/runners/faber.py | 2 +- demo/runners/support/agent.py | 4 +- 8 files changed, 145 insertions(+), 117 deletions(-) diff --git a/aries_cloudagent/anoncreds/issuer.py b/aries_cloudagent/anoncreds/issuer.py index 6c67cf1f2a..1e6d1f0bf1 100644 --- a/aries_cloudagent/anoncreds/issuer.py +++ b/aries_cloudagent/anoncreds/issuer.py @@ -517,9 +517,7 @@ async def create_credential_offer(self, credential_definition_id: str) -> str: CATEGORY_CRED_DEF_KEY_PROOF, credential_definition_id ) except AskarError as err: - raise AnonCredsIssuerError( - "Error retrieving credential definition" - ) from err + raise AnonCredsIssuerError("Error retrieving credential definition") from err if not cred_def or not key_proof: raise AnonCredsIssuerError( "Credential definition not found for credential offer" @@ -560,9 +558,7 @@ async def create_credential( CATEGORY_CRED_DEF_PRIVATE, cred_def_id ) except AskarError as err: - raise AnonCredsIssuerError( - "Error retrieving credential definition" - ) from err + raise AnonCredsIssuerError("Error retrieving credential definition") from err if not cred_def or not cred_def_private: raise AnonCredsIssuerError( @@ -599,60 +595,59 @@ async def create_credential( return credential.to_json() + async def create_credential_vc_di( + self, + credential_offer: dict, + credential_request: dict, + credential_values: dict, + ) -> str: + """Create Credential.""" + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + schema_id = credential_offer["schema_id"] + schema_result = await anoncreds_registry.get_schema(self.profile, schema_id) + cred_def_id = credential_offer["cred_def_id"] + schema_attributes = schema_result.schema_value.attr_names + + try: + async with self.profile.session() as session: + cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) + cred_def_private = await session.handle.fetch( + CATEGORY_CRED_DEF_PRIVATE, cred_def_id + ) + except AskarError as err: + raise AnonCredsIssuerError("Error retrieving credential definition") from err -async def create_credential_vc_di( - self, - credential_offer: dict, - credential_request: dict, - credential_values: dict, -) -> str: - """Create Credential.""" - anoncreds_registry = self.profile.inject(AnonCredsRegistry) - schema_id = credential_offer["schema_id"] - schema_result = await anoncreds_registry.get_schema(self.profile, schema_id) - cred_def_id = credential_offer["cred_def_id"] - schema_attributes = schema_result.schema_value.attr_names - - try: - async with self.profile.session() as session: - cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) - cred_def_private = await session.handle.fetch( - CATEGORY_CRED_DEF_PRIVATE, cred_def_id + if not cred_def or not cred_def_private: + raise AnonCredsIssuerError( + "Credential definition not found for credential issuance" ) - except AskarError as err: - raise AnonCredsIssuerError("Error retrieving credential definition") from err - if not cred_def or not cred_def_private: - raise AnonCredsIssuerError( - "Credential definition not found for credential issuance" - ) + raw_values = {} + for attribute in schema_attributes: + # Ensure every attribute present in schema to be set. + # Extraneous attribute names are ignored. + try: + credential_value = credential_values[attribute] + except KeyError: + raise AnonCredsIssuerError( + "Provided credential values are missing a value " + f"for the schema attribute '{attribute}'" + ) + + raw_values[attribute] = str(credential_value) - raw_values = {} - for attribute in schema_attributes: - # Ensure every attribute present in schema to be set. - # Extraneous attribute names are ignored. try: - credential_value = credential_values[attribute] - except KeyError: - raise AnonCredsIssuerError( - "Provided credential values are missing a value " - f"for the schema attribute '{attribute}'" + credential = await asyncio.get_event_loop().run_in_executor( + None, + lambda: Credential.create( + cred_def.raw_value, + cred_def_private.raw_value, + credential_offer, + credential_request, + raw_values, + ), ) + except AnoncredsError as err: + raise AnonCredsIssuerError("Error creating credential") from err - raw_values[attribute] = str(credential_value) - - try: - credential = await asyncio.get_event_loop().run_in_executor( - None, - lambda: Credential.create( - cred_def.raw_value, - cred_def_private.raw_value, - credential_offer, - credential_request, - raw_values, - ), - ) - except AnoncredsError as err: - raise AnonCredsIssuerError("Error creating credential") from err - - return credential.to_json() + return credential.to_json() diff --git a/aries_cloudagent/indy/models/cred_abstract.py b/aries_cloudagent/indy/models/cred_abstract.py index c5e46f233b..999ccb8c2f 100644 --- a/aries_cloudagent/indy/models/cred_abstract.py +++ b/aries_cloudagent/indy/models/cred_abstract.py @@ -180,6 +180,12 @@ def __init__( class AnoncredsLinkSecretSchema(BaseModelSchema): """Anoncreds Link Secret Schema.""" + class Meta: + """AnoncredsLinkSecret schema metadata.""" + + model_class = AnoncredsLinkSecret + unknown = EXCLUDE + nonce = fields.Str( required=True, validate=NUM_STR_WHOLE_VALIDATE, @@ -230,6 +236,12 @@ def __init__( class DidcommSignedAttachmentSchema(BaseModelSchema): """Didcomm Signed Attachment Schema.""" + class Meta: + """Didcomm signed attachment schema metadata.""" + + model_class = DidcommSignedAttachment + unknown = EXCLUDE + algs_supported = fields.List(fields.Str(), required=True) did_methods_supported = fields.List(fields.Str(), required=True) @@ -244,9 +256,35 @@ class DidcommSignedAttachmentSchema(BaseModelSchema): ) +class BindingMethod(BaseModel): + """Binding Method Model.""" + + class Meta: + """Binding method metadata.""" + + schema_class = "BindingMethodSchema" + + def __init__( + self, + anoncreds_link_secret: Union[dict, AnoncredsLinkSecret] = None, + didcomm_signed_attachment: Union[dict, DidcommSignedAttachment] = None, + **kwargs, + ): + """Initialize values for DidcommSignedAttachment.""" + super().__init__(**kwargs) + self.anoncreds_link_secret = anoncreds_link_secret + self.didcomm_signed_attachment = didcomm_signed_attachment + + class BindingMethodSchema(BaseModelSchema): """VCDI Binding Method Schema.""" + class Meta: + """VCDI binding method schema metadata.""" + + model_class = BindingMethod + unknown = EXCLUDE + anoncreds_link_secret = fields.Nested(AnoncredsLinkSecretSchema, required=False) didcomm_signed_attachment = fields.Nested( DidcommSignedAttachmentSchema, required=True @@ -263,7 +301,7 @@ class Meta: def __init__( self, - data_model_versions_supported: str = None, + data_model_versions_supported: Sequence[str] = None, binding_required: str = None, binding_methods: str = None, credential: Union[dict, VerifiableCredential] = None, @@ -294,7 +332,7 @@ class Meta: unknown = EXCLUDE data_model_versions_supported = fields.List( - required=True, validate="", metadata={"description": "", "example": ""} + fields.Str(), required=True, metadata={"description": "", "example": ""} ) binding_required = fields.Bool( @@ -308,5 +346,7 @@ class Meta: ) credential = fields.Nested( - CredentialSchema(), required=True, metadata={"description": "", "example": ""} + CredentialSchema(), + required=True, + metadata={"description": "", "example": ""}, ) diff --git a/aries_cloudagent/indy/models/cred_request.py b/aries_cloudagent/indy/models/cred_request.py index 636e2b0f72..e90fd9e8fc 100644 --- a/aries_cloudagent/indy/models/cred_request.py +++ b/aries_cloudagent/indy/models/cred_request.py @@ -8,6 +8,8 @@ from ...messaging.valid import ( INDY_CRED_DEF_ID_EXAMPLE, INDY_CRED_DEF_ID_VALIDATE, + INDY_DID_EXAMPLE, + INDY_DID_VALIDATE, UUID4_EXAMPLE, NUM_STR_WHOLE_EXAMPLE, NUM_STR_WHOLE_VALIDATE, @@ -88,7 +90,6 @@ class Meta: """VCDI credential request schema metadata.""" schema_class = "BindingProofSchema" - unknown = EXCLUDE def __init__( self, @@ -154,7 +155,6 @@ class Meta: """Didcomm signed attachment metadata.""" schema_class = "DidcommSignedAttachmentSchema" - unknown = EXCLUDE def __init__(self, attachment_id: str = None, **kwargs): """Initialize DidcommSignedAttachment.""" @@ -169,10 +169,9 @@ class Meta: """Didcomm Signed Attachment schema metadata.""" model_class = DidcommSignedAttachment + unknown = EXCLUDE - attachment_id = fields.str( - required=True, metadata={"description": "", "example": ""} - ) + attachment_id = fields.Str(required=True, metadata={"description": "", "example": ""}) class BindingProof(BaseModel): @@ -182,7 +181,6 @@ class Meta: """Binding proof metadata.""" schema_class = "BindingProofSchema" - unknown = EXCLUDE def __init__( self, @@ -203,6 +201,7 @@ class Meta: """Binding proof schema metadata.""" model_class = BindingProof + unknown = EXCLUDE anoncreds_link_secret = fields.Nested( AnoncredsLinkSecretSchema(), diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py index 7f37cd8541..d29095f970 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py @@ -188,7 +188,7 @@ async def _match_sent_cred_def_id(self, tag_query: Mapping[str, str]) -> str: async def create_proposal( self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] ) -> Tuple[V20CredFormat, AttachDecorator]: - """Create indy credential proposal.""" + """Create vc_di credential proposal.""" if proposal_data is None: proposal_data = {} @@ -260,15 +260,15 @@ async def _create(): async def receive_offer( self, cred_ex_record: V20CredExRecord, cred_offer_message: V20CredOffer ) -> None: - """Receive indy credential offer.""" + """Receive vcdi credential offer.""" async def create_request( self, cred_ex_record: V20CredExRecord, request_data: Mapping = None ) -> CredFormatAttachment: - """Create indy credential request.""" + """Create vcdi credential request.""" if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: raise V20CredFormatError( - "Indy issue credential format cannot start from credential request" + "vcdi issue credential format cannot start from credential request" ) await self._check_uniqueness(cred_ex_record.cred_ex_id) @@ -285,9 +285,7 @@ async def create_request( ) nonce = cred_offer["binding_method"]["anoncreds_link_secret"]["nonce"] - cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"][ - "cred_def_id" - ] + cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"]["cred_def_id"] async def _create(): anoncreds_registry = self.profile.inject(AnonCredsRegistry) @@ -332,16 +330,16 @@ async def _create(): async def receive_request( self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest ) -> None: - """Receive indy credential request.""" + """Receive vcdi credential request.""" if not cred_ex_record.cred_offer: raise V20CredFormatError( - "Indy issue credential format cannot start from credential request" + "vcdi issue credential format cannot start from credential request" ) async def issue_credential( self, cred_ex_record: V20CredExRecord, retries: int = 5 ) -> CredFormatAttachment: - """Issue indy credential.""" + """Issue vcdi credential.""" await self._check_uniqueness(cred_ex_record.cred_ex_id) attached_credential = cred_ex_record.cred_offer.attachment( @@ -402,7 +400,7 @@ async def issue_credential( async def receive_credential( self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue ) -> None: - """Receive indy credential. + """Receive vcdi credential. Validation is done in the store credential step. """ @@ -410,7 +408,7 @@ async def receive_credential( async def store_credential( self, cred_ex_record: V20CredExRecord, cred_id: str = None ) -> None: - """Store indy credential.""" + """Store vcdi credential.""" cred = cred_ex_record.cred_issue.attachment(VCDICredFormatHandler.format) rev_reg_def = None diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py b/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py index b2b9e4325e..81cb20a244 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py @@ -22,7 +22,9 @@ MESSAGE_TYPES = DIDCommPrefix.qualify_all( { - CRED_20_PROPOSAL: (f"{PROTOCOL_PACKAGE}.messages.cred_proposal.V20CredProposal"), + CRED_20_PROPOSAL: ( + f"{PROTOCOL_PACKAGE}.messages.cred_proposal.V20CredProposal" + ), CRED_20_OFFER: f"{PROTOCOL_PACKAGE}.messages.cred_offer.V20CredOffer", CRED_20_REQUEST: f"{PROTOCOL_PACKAGE}.messages.cred_request.V20CredRequest", CRED_20_ISSUE: f"{PROTOCOL_PACKAGE}.messages.cred_issue.V20CredIssue", diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index b2db5ed138..ef3a0ac6c5 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -16,6 +16,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from runners.support.agent import ( # noqa:E402 + CRED_FORMAT_VC_DI, DemoAgent, default_genesis_txns, start_mediator_agent, @@ -214,9 +215,7 @@ async def handle_issue_credential(self, message): cred_attrs = self.cred_attrs[message["credential_definition_id"]] cred_preview = { "@type": CRED_PREVIEW_TYPE, - "attributes": [ - {"name": n, "value": v} for (n, v) in cred_attrs.items() - ], + "attributes": [{"name": n, "value": v} for (n, v) in cred_attrs.items()], } try: cred_ex_rec = await self.admin_POST( @@ -423,9 +422,7 @@ async def handle_present_proof_v2_0(self, message): pres_request_indy = ( message["by_format"].get("pres_request", {}).get("indy") ) - pres_request_dif = ( - message["by_format"].get("pres_request", {}).get("dif") - ) + pres_request_dif = message["by_format"].get("pres_request", {}).get("dif") request = {} if not pres_request_dif and not pres_request_indy: @@ -602,9 +599,7 @@ async def generate_invitation( self._connection_ready = asyncio.Future() with log_timer("Generate invitation duration:"): # Generate an invitation - log_status( - "#7 Create a connection to alice and print out the invite details" - ) + log_status("#7 Create a connection to alice and print out the invite details") invi_rec = await self.get_invite( use_did_exchange, auto_accept=auto_accept, @@ -816,9 +811,7 @@ async def initialize( raise Exception("Endorser agent returns None :-(") # set the endorser invite so the agent can auto-connect - self.agent.endorser_invite = ( - self.endorser_agent.endorser_multi_invitation_url - ) + self.agent.endorser_invite = self.endorser_agent.endorser_multi_invitation_url self.agent.endorser_did = self.endorser_agent.endorser_public_did else: self.endorser_agent = None @@ -854,25 +847,17 @@ async def initialize( if self.mediation: # we need to pre-connect the agent to its mediator self.agent.log("Connect wallet to mediator ...") - if not await connect_wallet_to_mediator( - self.agent, self.mediator_agent - ): + if not await connect_wallet_to_mediator(self.agent, self.mediator_agent): raise Exception("Mediation setup FAILED :-(") if self.endorser_agent: self.agent.log("Connect wallet to endorser ...") - if not await connect_wallet_to_endorser( - self.agent, self.endorser_agent - ): + if not await connect_wallet_to_endorser(self.agent, self.endorser_agent): raise Exception("Endorser setup FAILED :-(") if self.taa_accept: await self.agent.taa_accept() # if we are an author, create our public DID here ... - if ( - self.endorser_role - and self.endorser_role == "author" - and self.endorser_agent - ): + if self.endorser_role and self.endorser_role == "author" and self.endorser_agent: if self.public_did and self.cred_type != CRED_FORMAT_JSON_LD: new_did = await self.agent.admin_POST("/wallet/did/create") self.agent.did = new_did["result"]["did"] @@ -905,9 +890,7 @@ async def create_schema_and_cred_def( ): if not self.public_did: raise Exception("Can't create a schema/cred def without a public DID :-(") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # need to redister schema and cred def on the ledger self.cred_def_id = await self.agent.create_schema_and_cred_def( schema_name, @@ -974,6 +957,25 @@ async def issue_credential( return cred_exchange + elif self.cred_type == CRED_FORMAT_VC_DI: + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": cred_attrs, + } + offer_request = { + "connection_id": self.agent.connection_id, + "comment": f"Offer on cred def id {cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "filter": {"vc_di": {"cred_def_id": cred_def_id}}, + "trace": self.exchange_tracing, + } + cred_exchange = await self.agent.admin_POST( + "/issue-credential-2.0/send-offer", offer_request + ) + + return cred_exchange + elif self.cred_type == CRED_FORMAT_JSON_LD: # TODO create and send the json-ld credential offer pass @@ -1015,9 +1017,7 @@ async def receive_credential( async def request_proof(self, proof_request, explicit_revoc_required: bool = False): log_status("#20 Request proof of degree from alice") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: indy_proof_request = { "name": ( proof_request["name"] @@ -1100,9 +1100,7 @@ async def verify_proof(self, proof_request): # log_status(f">>> last proof received: {self.agent.last_proof_received}") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # return verified status return self.agent.last_proof_received["verified"] @@ -1304,9 +1302,7 @@ def arg_parser(ident: str = None, port: int = 8020): metavar=(""), help="API level (10 or 20 (default))", ) - parser.add_argument( - "--timing", action="store_true", help="Enable timing information" - ) + parser.add_argument("--timing", action="store_true", help="Enable timing information") parser.add_argument( "--multitenant", action="store_true", help="Enable multitenancy options" ) diff --git a/demo/runners/faber.py b/demo/runners/faber.py index be5a5b0b3c..2de31cb06d 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -535,7 +535,7 @@ async def main(args): else False ), ) - elif faber_agent.cred_type == (CRED_FORMAT_JSON_LD or CRED_FORMAT_VC_DI): + elif faber_agent.cred_type in [CRED_FORMAT_JSON_LD, CRED_FORMAT_VC_DI]: faber_agent.public_did = True await faber_agent.initialize(the_agent=agent) else: diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 0dc2a2f1d2..1ba14c6554 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -668,9 +668,7 @@ async def register_did( role: str = "TRUST_ANCHOR", cred_type: str = CRED_FORMAT_INDY, ): - if cred_type in [ - CRED_FORMAT_INDY, - ]: + if cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # if registering a did for issuing indy credentials, publish the did on the ledger self.log(f"Registering {self.ident} ...") if not ledger_url: From 80df4c13f2b1e7e7a966f3090ec0ad2cee0e5786 Mon Sep 17 00:00:00 2001 From: tra371 Date: Tue, 20 Feb 2024 00:14:52 +0630 Subject: [PATCH 4/4] fix: reformat code with black Signed-off-by: tra371 --- aries_cloudagent/anoncreds/issuer.py | 12 +++- aries_cloudagent/indy/models/cred_request.py | 4 +- .../v2_0/formats/vc_di/handler.py | 4 +- demo/runners/agent_container.py | 34 ++++++++--- demo/runners/support/agent.py | 56 ++++++++++++++----- 5 files changed, 83 insertions(+), 27 deletions(-) diff --git a/aries_cloudagent/anoncreds/issuer.py b/aries_cloudagent/anoncreds/issuer.py index 1e6d1f0bf1..dcd13da86e 100644 --- a/aries_cloudagent/anoncreds/issuer.py +++ b/aries_cloudagent/anoncreds/issuer.py @@ -517,7 +517,9 @@ async def create_credential_offer(self, credential_definition_id: str) -> str: CATEGORY_CRED_DEF_KEY_PROOF, credential_definition_id ) except AskarError as err: - raise AnonCredsIssuerError("Error retrieving credential definition") from err + raise AnonCredsIssuerError( + "Error retrieving credential definition" + ) from err if not cred_def or not key_proof: raise AnonCredsIssuerError( "Credential definition not found for credential offer" @@ -558,7 +560,9 @@ async def create_credential( CATEGORY_CRED_DEF_PRIVATE, cred_def_id ) except AskarError as err: - raise AnonCredsIssuerError("Error retrieving credential definition") from err + raise AnonCredsIssuerError( + "Error retrieving credential definition" + ) from err if not cred_def or not cred_def_private: raise AnonCredsIssuerError( @@ -615,7 +619,9 @@ async def create_credential_vc_di( CATEGORY_CRED_DEF_PRIVATE, cred_def_id ) except AskarError as err: - raise AnonCredsIssuerError("Error retrieving credential definition") from err + raise AnonCredsIssuerError( + "Error retrieving credential definition" + ) from err if not cred_def or not cred_def_private: raise AnonCredsIssuerError( diff --git a/aries_cloudagent/indy/models/cred_request.py b/aries_cloudagent/indy/models/cred_request.py index e90fd9e8fc..4438a99259 100644 --- a/aries_cloudagent/indy/models/cred_request.py +++ b/aries_cloudagent/indy/models/cred_request.py @@ -171,7 +171,9 @@ class Meta: model_class = DidcommSignedAttachment unknown = EXCLUDE - attachment_id = fields.Str(required=True, metadata={"description": "", "example": ""}) + attachment_id = fields.Str( + required=True, metadata={"description": "", "example": ""} + ) class BindingProof(BaseModel): diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py index d29095f970..50a8e40979 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py @@ -285,7 +285,9 @@ async def create_request( ) nonce = cred_offer["binding_method"]["anoncreds_link_secret"]["nonce"] - cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"]["cred_def_id"] + cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"][ + "cred_def_id" + ] async def _create(): anoncreds_registry = self.profile.inject(AnonCredsRegistry) diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index ef3a0ac6c5..d7f4f3c09d 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -215,7 +215,9 @@ async def handle_issue_credential(self, message): cred_attrs = self.cred_attrs[message["credential_definition_id"]] cred_preview = { "@type": CRED_PREVIEW_TYPE, - "attributes": [{"name": n, "value": v} for (n, v) in cred_attrs.items()], + "attributes": [ + {"name": n, "value": v} for (n, v) in cred_attrs.items() + ], } try: cred_ex_rec = await self.admin_POST( @@ -422,7 +424,9 @@ async def handle_present_proof_v2_0(self, message): pres_request_indy = ( message["by_format"].get("pres_request", {}).get("indy") ) - pres_request_dif = message["by_format"].get("pres_request", {}).get("dif") + pres_request_dif = ( + message["by_format"].get("pres_request", {}).get("dif") + ) request = {} if not pres_request_dif and not pres_request_indy: @@ -599,7 +603,9 @@ async def generate_invitation( self._connection_ready = asyncio.Future() with log_timer("Generate invitation duration:"): # Generate an invitation - log_status("#7 Create a connection to alice and print out the invite details") + log_status( + "#7 Create a connection to alice and print out the invite details" + ) invi_rec = await self.get_invite( use_did_exchange, auto_accept=auto_accept, @@ -811,7 +817,9 @@ async def initialize( raise Exception("Endorser agent returns None :-(") # set the endorser invite so the agent can auto-connect - self.agent.endorser_invite = self.endorser_agent.endorser_multi_invitation_url + self.agent.endorser_invite = ( + self.endorser_agent.endorser_multi_invitation_url + ) self.agent.endorser_did = self.endorser_agent.endorser_public_did else: self.endorser_agent = None @@ -847,17 +855,25 @@ async def initialize( if self.mediation: # we need to pre-connect the agent to its mediator self.agent.log("Connect wallet to mediator ...") - if not await connect_wallet_to_mediator(self.agent, self.mediator_agent): + if not await connect_wallet_to_mediator( + self.agent, self.mediator_agent + ): raise Exception("Mediation setup FAILED :-(") if self.endorser_agent: self.agent.log("Connect wallet to endorser ...") - if not await connect_wallet_to_endorser(self.agent, self.endorser_agent): + if not await connect_wallet_to_endorser( + self.agent, self.endorser_agent + ): raise Exception("Endorser setup FAILED :-(") if self.taa_accept: await self.agent.taa_accept() # if we are an author, create our public DID here ... - if self.endorser_role and self.endorser_role == "author" and self.endorser_agent: + if ( + self.endorser_role + and self.endorser_role == "author" + and self.endorser_agent + ): if self.public_did and self.cred_type != CRED_FORMAT_JSON_LD: new_did = await self.agent.admin_POST("/wallet/did/create") self.agent.did = new_did["result"]["did"] @@ -1302,7 +1318,9 @@ def arg_parser(ident: str = None, port: int = 8020): metavar=(""), help="API level (10 or 20 (default))", ) - parser.add_argument("--timing", action="store_true", help="Enable timing information") + parser.add_argument( + "--timing", action="store_true", help="Enable timing information" + ) parser.add_argument( "--multitenant", action="store_true", help="Enable multitenancy options" ) diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 1ba14c6554..35436478d3 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -370,7 +370,9 @@ async def register_schema_and_creddef_indy( log_msg("Schema ID:", schema_id) # Create a cred def for the schema - cred_def_tag = tag if tag else (self.ident + "." + schema_name).replace(" ", "_") + cred_def_tag = ( + tag if tag else (self.ident + "." + schema_name).replace(" ", "_") + ) credential_definition_body = { "schema_id": schema_id, "support_revocation": support_revocation, @@ -400,7 +402,9 @@ async def register_schema_and_creddef_indy( credential_definition_response = await self.admin_GET( "/credential-definitions/created" ) - if 0 == len(credential_definition_response["credential_definition_ids"]): + if 0 == len( + credential_definition_response["credential_definition_ids"] + ): await asyncio.sleep(1.0) attempts = attempts - 1 credential_definition_id = credential_definition_response[ @@ -447,7 +451,9 @@ async def register_schema_and_creddef_anoncreds( log_msg("Schema ID:", schema_id) # Create a cred def for the schema - cred_def_tag = tag if tag else (self.ident + "." + schema_name).replace(" ", "_") + cred_def_tag = ( + tag if tag else (self.ident + "." + schema_name).replace(" ", "_") + ) max_cred_num = revocation_registry_size if revocation_registry_size else 0 credential_definition_body = { "credential_definition": { @@ -483,7 +489,9 @@ async def register_schema_and_creddef_anoncreds( credential_definition_response = await self.admin_GET( "/anoncreds/credential-definitions" ) - if 0 == len(credential_definition_response["credential_definition_ids"]): + if 0 == len( + credential_definition_response["credential_definition_ids"] + ): await asyncio.sleep(1.0) attempts = attempts - 1 credential_definition_id = credential_definition_response[ @@ -698,7 +706,9 @@ async def register_did( nym_info = data else: log_msg("using ledger: " + ledger_url + "/register") - resp = await self.client_session.post(ledger_url + "/register", json=data) + resp = await self.client_session.post( + ledger_url + "/register", json=data + ) if resp.status != 200: raise Exception( f"Error registering DID {data}, response code {resp.status}" @@ -895,7 +905,9 @@ def _process(self, args, env, loop): def get_process_args(self): return list( - flatten(([PYTHON, "-m", "aries_cloudagent", "start"], self.get_agent_args())) + flatten( + ([PYTHON, "-m", "aries_cloudagent", "start"], self.get_agent_args()) + ) ) async def start_process(self, python_path: str = None, wait: bool = True): @@ -1111,7 +1123,9 @@ async def admin_GET( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) response = await self.admin_request( "GET", path, None, text, params, headers=headers ) @@ -1163,7 +1177,9 @@ async def admin_POST( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) response = await self.admin_request( "POST", path, data, text, params, headers=headers ) @@ -1184,7 +1200,9 @@ async def admin_PATCH( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) return await self.admin_request( "PATCH", path, data, text, params, headers=headers ) @@ -1199,7 +1217,9 @@ async def admin_PUT( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) return await self.admin_request( "PUT", path, data, text, params, headers=headers ) @@ -1219,7 +1239,9 @@ async def admin_DELETE( if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) response = await self.admin_request( "DELETE", path, data, text, params, headers=headers ) @@ -1238,7 +1260,9 @@ async def admin_GET_FILE(self, path, params=None, headers=None) -> bytes: if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) params = {k: v for (k, v) in (params or {}).items() if v is not None} resp = await self.client_session.request( "GET", self.admin_url + path, params=params, headers=headers @@ -1254,7 +1278,9 @@ async def admin_PUT_FILE(self, files, url, params=None, headers=None) -> bytes: if self.multitenant: if not headers: headers = {} - headers["Authorization"] = "Bearer " + self.managed_wallet_params["token"] + headers["Authorization"] = ( + "Bearer " + self.managed_wallet_params["token"] + ) params = {k: v for (k, v) in (params or {}).items() if v is not None} resp = await self.client_session.request( "PUT", url, params=params, data=files, headers=headers @@ -1626,7 +1652,9 @@ async def handle_connections(self, message): # setup endorser meta-data on our connection log_msg("Setup endorser agent meta-data ...") await self.admin_POST( - "/transactions/" + self.endorser_connection_id + "/set-endorser-role", + "/transactions/" + + self.endorser_connection_id + + "/set-endorser-role", params={"transaction_my_job": "TRANSACTION_ENDORSER"}, )