diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py index 21f199454e..6da2b6b53b 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py @@ -117,6 +117,8 @@ def __init__(self): B58 = alphabet if isinstance(alphabet, str) else alphabet.decode("ascii") INDY_DID = rf"^(did:sov:)?[{B58}]{{21,22}}$" INDY_SCHEMA_ID = rf"^[{B58}]{{21,22}}:2:.+:[0-9.]+$" + # the schema id can be just a number (this is how the schema_id is referenced in a cred def) + INDY_SCHEMA_TXN_ID = rf"^[0-9.]+$" INDY_CRED_DEF_ID = ( rf"^([{B58}]{{21,22}})" # issuer DID f":3" # cred def id marker @@ -131,7 +133,7 @@ def __init__(self): rf"CL_ACCUM:(.+$)" ) self._supported_identifiers_regex = re.compile( - rf"{INDY_DID}|{INDY_SCHEMA_ID}|{INDY_CRED_DEF_ID}|{INDY_REV_REG_DEF_ID}" + rf"{INDY_DID}|{INDY_SCHEMA_ID}|{INDY_SCHEMA_TXN_ID}|{INDY_CRED_DEF_ID}|{INDY_REV_REG_DEF_ID}" ) @property diff --git a/aries_cloudagent/anoncreds/holder.py b/aries_cloudagent/anoncreds/holder.py index 2c08bbf188..9d3ce5dd0c 100644 --- a/aries_cloudagent/anoncreds/holder.py +++ b/aries_cloudagent/anoncreds/holder.py @@ -3,8 +3,11 @@ import asyncio import json import logging +from marshmallow import INCLUDE import re from typing import Dict, Optional, Sequence, Tuple, Union +from pyld import jsonld +from pyld.jsonld import JsonLdProcessor from anoncreds import ( AnoncredsError, @@ -14,16 +17,25 @@ Presentation, PresentCredentials, W3cCredential, + W3cPresentation, create_link_secret, ) from aries_askar import AskarError, AskarErrorCode from uuid_utils import uuid4 +from ..protocols.issue_credential.v2_0.models.detail.ld_proof import ( + V20CredExRecordLDProof, +) + from ..anoncreds.models.anoncreds_schema import AnonCredsSchema from ..askar.profile_anon import AskarAnoncredsProfile from ..core.error import BaseError from ..core.profile import Profile from ..ledger.base import BaseLedger +from ..storage.vc_holder.base import VCHolder +from ..storage.vc_holder.vc_record import VCRecord +from ..vc.vc_ld import VerifiableCredential +from ..vc.ld_proofs import DocumentLoader from ..wallet.error import WalletNotFoundError from .error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from .models.anoncreds_cred_def import CredDef @@ -307,7 +319,7 @@ async def store_credential_w3c( try: secret = await self.get_master_secret() cred_w3c = W3cCredential.load(credential_data) - await asyncio.get_event_loop().run_in_executor( + cred_w3c_recvd = await asyncio.get_event_loop().run_in_executor( None, cred_w3c.process, credential_request_metadata, @@ -327,7 +339,7 @@ async def store_credential_w3c( except AnoncredsError as err: raise AnonCredsHolderError("Error processing received credential") from err - return await self._finish_store_credential( + credential_id = await self._finish_store_credential( credential_definition, cred_recvd, credential_request_metadata, @@ -336,6 +348,45 @@ async def store_credential_w3c( rev_reg_def, ) + # also store in W3C format + # create VC record for storage + cred_w3c_recvd_dict = cred_w3c_recvd.to_dict() + cred_w3c_recvd_dict["proof"] = cred_w3c_recvd_dict["proof"][0] + cred_w3c_recvd_vc = VerifiableCredential.deserialize( + cred_w3c_recvd_dict, unknown=INCLUDE + ) + + # Saving expanded type as a cred_tag + document_loader = self.profile.inject(DocumentLoader) + expanded = jsonld.expand( + cred_w3c_recvd_dict, options={"documentLoader": document_loader} + ) + types = JsonLdProcessor.get_values( + expanded[0], + "@type", + ) + + vc_record = VCRecord( + contexts=cred_w3c_recvd_vc.context_urls, + expanded_types=types, + issuer_id=cred_w3c_recvd_vc.issuer_id, + subject_ids=cred_w3c_recvd_vc.credential_subject_ids, + schema_ids=[], # Schemas not supported yet + proof_types=[cred_w3c_recvd_vc.proof.type], + cred_value=cred_w3c_recvd_vc.serialize(), + given_id=cred_w3c_recvd_vc.id, + record_id=credential_id, + cred_tags=None, # Tags should be derived from credential values + ) + + # save credential in storage + async with self.profile.session() as session: + vc_holder = session.inject(VCHolder) + + await vc_holder.store_credential(vc_record) + + return credential_id + async def get_credentials(self, start: int, count: int, wql: dict): """Get credentials stored in the wallet. @@ -384,16 +435,13 @@ async def get_credentials_for_presentation_request_by_referent( extra_query: wql query dict """ - if not referents: referents = ( *presentation_request["requested_attributes"], *presentation_request["requested_predicates"], ) extra_query = extra_query or {} - creds = {} - for reft in referents: names = set() if reft in presentation_request["requested_attributes"]: @@ -640,6 +688,65 @@ def get_rev_state(cred_id: str, detail: dict): return presentation.to_json() + async def create_presentation_w3c( + self, + presentation_request: dict, + requested_credentials_w3c: list, + credentials_w3c_metadata: list, + schemas: Dict[str, AnonCredsSchema], + credential_definitions: Dict[str, CredDef], + rev_states: dict = None, + ) -> dict: + """Get credentials stored in the wallet. + + Args: + presentation_request: Valid indy format presentation request + requested_credentials: W3C format requested credentials + schemas: Indy formatted schemas JSON + credential_definitions: Indy formatted credential definitions JSON + rev_states: Indy format revocation states JSON + + """ + # TODO implement this!!!!! prepare and return an anoncreds proof + present_creds = PresentCredentials() + for idx, cred in enumerate(requested_credentials_w3c): + meta = credentials_w3c_metadata[idx] + + # TODO deal with revocation + rev_state = None + timestamp = None + + for attr in meta["proof_attrs"]: + present_creds.add_attributes( + cred, + attr, + reveal=True, + timestamp=timestamp, + rev_state=rev_state, + ) + + for pred in meta["proof_preds"]: + present_creds.add_predicates( + cred, + pred, + timestamp=timestamp, + rev_state=rev_state, + ) + + try: + secret = await self.get_master_secret() + presentation = W3cPresentation.create( + presentation_request, + present_creds, + secret, + schemas, + credential_definitions, + ) + except AnoncredsError as err: + raise AnonCredsHolderError("Error creating presentation") from err + + return presentation.to_dict() + async def create_revocation_state( self, cred_rev_id: str, diff --git a/aries_cloudagent/anoncreds/verifier.py b/aries_cloudagent/anoncreds/verifier.py index f320d82013..b4a6de7f0b 100644 --- a/aries_cloudagent/anoncreds/verifier.py +++ b/aries_cloudagent/anoncreds/verifier.py @@ -6,13 +6,14 @@ from time import time from typing import List, Mapping, Tuple -from anoncreds import AnoncredsError, Presentation +from anoncreds import AnoncredsError, Presentation, W3cPresentation from ..core.profile import Profile from ..indy.models.xform import indy_proof_req2non_revoc_intervals from ..messaging.util import canon, encode from .models.anoncreds_cred_def import GetCredDefResult from .registry import AnonCredsRegistry +from ..vc.vc_ld.validation_result import PresentationVerificationResult LOGGER = logging.getLogger(__name__) @@ -496,3 +497,72 @@ async def verify_presentation( verified = False return (verified, msgs) + + async def verify_presentation_w3c( + self, + pres_req, + pres, + ) -> PresentationVerificationResult: + credentials = pres["verifiableCredential"] + cred_def_ids = [] + for credential in credentials: + cred_def_id = credential["proof"]["verificationMethod"] + cred_def_ids.append(cred_def_id) + + cred_defs = {} + schemas = {} + msgs = [] + + # TODO this should use the process_pres_identifiers() method, which will also fetch the revocation info + for cred_def_id in cred_def_ids: + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + # Build schemas for anoncreds + if cred_def_id not in cred_defs: + cred_def = ( + await anoncreds_registry.get_credential_definition( + self.profile, cred_def_id + ) + ).credential_definition.serialize() + cred_defs[cred_def_id] = cred_def + schema_id = cred_def["schemaId"] + schema = ( + await anoncreds_registry.get_schema(self.profile, schema_id) + ).serialize() + if schema["schema_id"] not in schemas: + schemas[schema["schema_id"]] = schema["schema"] + + # TODO - this should get loaded from process_pres_identifiers() (with schemas and cred defs) + rev_reg_defs = {} + rev_lists = {} + + try: + # TODO not sure why this attr causes an error + del pres["presentation_submission"] + + presentation = W3cPresentation.load(pres) + + verified = await asyncio.get_event_loop().run_in_executor( + None, + presentation.verify, + pres_req, + schemas, + cred_defs, + rev_reg_defs, + [ + rev_list + for timestamp_to_list in rev_lists.values() + for rev_list in timestamp_to_list.values() + ], + ) + except AnoncredsError as err: + s = str(err) + msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}") + LOGGER.exception( + f"Validation of presentation on nonce={pres_req['nonce']} " + "failed with error" + ) + verified = False + + result = PresentationVerificationResult(verified=verified, errors=msgs) + + return result 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 100b44530e..2d1d084ad8 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 @@ -295,9 +295,10 @@ async def _create(): credential=credential, ) - return self.get_format_data( + cred_offer = self.get_format_data( CRED_20_OFFER, json.loads(vcdi_cred_abstract.to_json()) ) + return cred_offer async def receive_offer( self, cred_ex_record: V20CredExRecord, cred_offer_message: V20CredOffer @@ -573,7 +574,9 @@ async def store_credential( except AnonCredsHolderError as e: LOGGER.error(f"Error receiving credential: {e.error_code} - {e.message}") raise e - if rev_reg_id: + # argh even with no revocation id (rev_reg_id = None) the following code gets called and fails + # TODO figure this out :-( (somehow the value is getting set to String "None") + if rev_reg_id and rev_reg_id != "None": rev_reg_def_result = ( await anoncreds_registry.get_revocation_registry_definition( self.profile, rev_reg_id diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index d383de8da5..72d7eef25e 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -37,6 +37,7 @@ def __init__( ldp: Mapping = None, ldp_vc: Mapping = None, ldp_vp: Mapping = None, + di_vc: Mapping = None, ): """Initialize format.""" self.jwt = jwt @@ -45,6 +46,7 @@ def __init__( self.ldp = ldp self.ldp_vc = ldp_vc self.ldp_vp = ldp_vp + self.di_vc = di_vc class ClaimFormatSchema(BaseModelSchema): @@ -62,6 +64,7 @@ class Meta: ldp = fields.Dict(required=False) ldp_vc = fields.Dict(required=False) ldp_vp = fields.Dict(required=False) + di_vc = fields.Dict(required=False) class SubmissionRequirements(BaseModel): diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py index d46415abee..91b5a673a5 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch_handler.py @@ -38,6 +38,7 @@ SECURITY_CONTEXT_BBS_URL, ) from ....vc.vc_ld.prove import create_presentation, derive_credential, sign_presentation +from ....vc.vc_di.prove import create_signed_anoncreds_presentation from ....wallet.base import BaseWallet, DIDInfo from ....wallet.default_verification_key_strategy import BaseVerificationKeyStrategy from ....wallet.error import WalletError, WalletNotFoundError @@ -194,7 +195,10 @@ async def get_sign_key_credential_subject_id( else: reqd_key_type = ED25519 for cred in applicable_creds: - if cred.subject_ids and len(cred.subject_ids) > 0: + if cred.cred_value["proof"]["type"] == "DataIntegrityProof": + filtered_creds_list.append(cred.cred_value) + issuer_id = cred.issuer_id + elif cred.subject_ids and len(cred.subject_ids) > 0: if not issuer_id: for cred_subject_id in cred.subject_ids: if not cred_subject_id.startswith("urn:"): @@ -399,14 +403,18 @@ async def filter_constraints( new_credential_dict = self.reveal_doc( credential_dict=credential_dict, constraints=constraints ) - derive_suite = await self._get_derive_suite() - signed_new_credential_dict = await derive_credential( - credential=credential_dict, - reveal_document=new_credential_dict, - suite=derive_suite, - document_loader=document_loader, - ) - credential = self.create_vcrecord(signed_new_credential_dict) + if credential_dict["proof"]["type"] == "DataIntegrityProof": + # TODO - don't sign + credential = self.create_vcrecord(credential_dict) + else: + derive_suite = await self._get_derive_suite() + signed_new_credential_dict = await derive_credential( + credential=credential_dict, + reveal_document=new_credential_dict, + suite=derive_suite, + document_loader=document_loader, + ) + credential = self.create_vcrecord(signed_new_credential_dict) result.append(credential) return result @@ -1072,6 +1080,7 @@ async def apply_requirements( credentials=credentials, schemas=descriptor.schemas, ) + # Filter credentials based upon path expressions specified in constraints filtered = await self.filter_constraints( constraints=descriptor.constraint, @@ -1198,6 +1207,7 @@ async def create_vp( challenge: str = None, domain: str = None, records_filter: dict = None, + is_holder_override: bool = None, ) -> Union[Sequence[dict], dict]: """Create VerifiablePresentation. @@ -1211,6 +1221,7 @@ async def create_vp( req = await self.make_requirement( srs=pd.submission_requirements, descriptors=pd.input_descriptors ) + result = [] if req.nested_req: for nested_req in req.nested_req: @@ -1247,7 +1258,7 @@ async def create_vp( submission_property = PresentationSubmission( id=str(uuid4()), definition_id=pd.id, descriptor_maps=descriptor_maps ) - if self.is_holder: + if self.is_holder or is_holder_override: ( issuer_id, filtered_creds_list, @@ -1262,7 +1273,8 @@ async def create_vp( result_vp.append(vp) continue else: - vp = await create_presentation(credentials=filtered_creds_list) + applicable_creds_list = filtered_creds_list + vp = await create_presentation(credentials=applicable_creds_list) else: if not self.pres_signing_did: ( @@ -1281,22 +1293,37 @@ async def create_vp( result_vp.append(vp) continue else: - vp = await create_presentation(credentials=filtered_creds_list) + applicable_creds_list = filtered_creds_list + vp = await create_presentation( + credentials=applicable_creds_list + ) else: issuer_id = self.pres_signing_did vp = await create_presentation(credentials=applicable_creds_list) vp["presentation_submission"] = submission_property.serialize() if self.proof_type is BbsBlsSignature2020.signature_type: vp["@context"].append(SECURITY_CONTEXT_BBS_URL) - issue_suite = await self._get_issue_suite( - issuer_id=issuer_id, - ) - signed_vp = await sign_presentation( - presentation=vp, - suite=issue_suite, - challenge=challenge, - document_loader=document_loader, - ) + if self.proof_type == ("anoncreds-2023"): + # TODO create anoncreds proof + (_proof_req, signed_vp, _cred_meta) = ( + await create_signed_anoncreds_presentation( + profile=self.profile, + pres_definition=pd.serialize(), + presentation=vp, + credentials=applicable_creds_list, + challenge=challenge, + ) + ) + else: + issue_suite = await self._get_issue_suite( + issuer_id=issuer_id, + ) + signed_vp = await sign_presentation( + presentation=vp, + suite=issue_suite, + challenge=challenge, + document_loader=document_loader, + ) result_vp.append(signed_vp) if len(result_vp) == 1: return result_vp[0] diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index efee40fd18..af2150bfa8 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -8,6 +8,7 @@ from ......messaging.base_handler import BaseResponder from ......messaging.decorators.attach_decorator import AttachDecorator +from ......anoncreds.holder import AnonCredsHolder from ......storage.error import StorageNotFoundError from ......storage.vc_holder.base import VCHolder from ......storage.vc_holder.vc_record import VCRecord @@ -15,7 +16,9 @@ BbsBlsSignature2020, Ed25519Signature2018, Ed25519Signature2020, + LinkedDataProof, ) +from ......vc.vc_di.manager import VcDiManager from ......vc.vc_ld.manager import VcLdpManager from ......vc.vc_ld.models.options import LDProofVCOptions from ......vc.vc_ld.models.presentation import VerifiablePresentation @@ -145,6 +148,7 @@ async def create_pres( proof_request = pres_ex_record.pres_request.attachment( DIFPresFormatHandler.format ) + pres_definition = None limit_record_ids = None reveal_doc_frame = None @@ -168,7 +172,7 @@ async def create_pres( domain = proof_request["options"].get("domain") if not challenge: challenge = str(uuid4()) - + # TODO handle vc_di format in the future input_descriptors = pres_definition.input_descriptors claim_fmt = pres_definition.fmt dif_handler_proof_type = None @@ -285,12 +289,26 @@ async def create_pres( BbsBlsSignature2020.signature_type ) break + elif claim_fmt.di_vc: + if "proof_type" in claim_fmt.di_vc: + proof_types = claim_fmt.di_vc.get("proof_type") + + proof_type = [ + "DataIntegrityProof" + ] # [LinkedDataProof.signature_type] + dif_handler_proof_type = "anoncreds-2023" + + # TODO check acceptable proof type(s) ("anoncreds-2023") + else: + # TODO di_vc allowed ... raise V20PresFormatHandlerError( - "Currently, only ldp_vp with " + "Currently, only: ldp_vp with " "BbsBlsSignature2020, Ed25519Signature2018 and " - "Ed25519Signature2020 signature types are supported" + "Ed25519Signature2020 signature types; and " + "di_vc with anoncreds-2023 signatures are supported" ) + if one_of_uri_groups: records = [] cred_group_record_ids = set() @@ -316,6 +334,7 @@ async def create_pres( # For now, setting to 1000 max_results = 1000 records = await search.fetch(max_results) + # Avoiding addition of duplicate records ( vcrecord_list, @@ -323,6 +342,7 @@ async def create_pres( ) = await self.process_vcrecords_return_list(records, record_ids) record_ids = vcrecord_ids_set credentials_list = credentials_list + vcrecord_list + except StorageNotFoundError as err: raise V20PresFormatHandlerError(err) except TypeError as err: @@ -345,6 +365,7 @@ async def create_pres( ) return + # TODO check for ldp_vp vs di_vc request and prepare presentation as appropriate dif_handler = DIFPresExchHandler( self._profile, pres_signing_did=issuer_id, @@ -358,6 +379,7 @@ async def create_pres( pd=pres_definition, credentials=credentials_list, records_filter=limit_record_ids, + is_holder_override=True, ) return self.get_format_data(PRES_20, pres) except DIFPresExchError as err: @@ -445,11 +467,15 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: pres_request = pres_ex_record.pres_request.attachment( DIFPresFormatHandler.format ) - manager = VcLdpManager(self.profile) - options = LDProofVCOptions.deserialize(pres_request["options"]) - if not options.challenge: - options.challenge = str(uuid4()) + if dif_proof["proof"]["type"] == "DataIntegrityProof": + manager = VcDiManager(self.profile) + options = pres_request + else: + manager = VcLdpManager(self.profile) + options = LDProofVCOptions.deserialize(pres_request["options"]) + if not options.challenge: + options.challenge = str(uuid4()) pres_ver_result = None if isinstance(dif_proof, Sequence): diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index 943494522d..28cd6fcb3a 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -572,6 +572,7 @@ async def present_proof_credentials_list(request: web.BaseRequest): extra_query, ) ) + except (IndyHolderError, AnonCredsHolderError) as err: if pres_ex_record: async with profile.session() as session: @@ -718,6 +719,13 @@ async def present_proof_credentials_list(request: web.BaseRequest): BbsBlsSignature2020.signature_type ] break + elif claim_fmt.di_vc: + # raise web.HTTPBadRequest( + # reason=( + # "VC DI Not supported for credential fetch" + # ) + # ) + pass else: raise web.HTTPBadRequest( reason=( diff --git a/aries_cloudagent/vc/vc_di/__init__.py b/aries_cloudagent/vc/vc_di/__init__.py new file mode 100644 index 0000000000..e790d5a72e --- /dev/null +++ b/aries_cloudagent/vc/vc_di/__init__.py @@ -0,0 +1,7 @@ +from .verify import verify_signed_anoncredspresentation +from .prove import create_signed_anoncreds_presentation + +__all__ = [ + "verify_signed_anoncredspresentation", + "create_signed_anoncreds_presentation", +] diff --git a/aries_cloudagent/vc/vc_di/manager.py b/aries_cloudagent/vc/vc_di/manager.py new file mode 100644 index 0000000000..e809a71fff --- /dev/null +++ b/aries_cloudagent/vc/vc_di/manager.py @@ -0,0 +1,39 @@ +"""Manager for managing DIF DataIntegrityProof presentations over JSON-LD formatted W3C VCs.""" + +from typing import Dict, List, Optional, Type, Union, cast + +from pyld import jsonld +from pyld.jsonld import JsonLdProcessor + +from ...core.profile import Profile +from ..vc_ld.models.presentation import VerifiablePresentation +from ..vc_ld.validation_result import PresentationVerificationResult +from ..vc_ld.models.options import LDProofVCOptions +from .verify import verify_signed_anoncredspresentation + + +class VcDiManagerError(Exception): + """Generic VcLdpManager Error.""" + + +class VcDiManager: + """Class for managing DIF DataIntegrityProof presentations over JSON-LD formatted W3C VCs.""" + + def __init__(self, profile: Profile): + """Initialize the VC DI Proof Manager.""" + self.profile = profile + + async def verify_presentation( + self, vp: VerifiablePresentation, options: dict + ) -> PresentationVerificationResult: + """Verify a VP with a Linked Data Proof.""" + + if not options["options"]["challenge"]: + raise VcDiManagerError("Challenge is required for verifying a VP") + + return await verify_signed_anoncredspresentation( + profile=self.profile, + presentation=vp.serialize(), + challenge=options["options"]["challenge"], + pres_req=options, + ) diff --git a/aries_cloudagent/vc/vc_di/prove.py b/aries_cloudagent/vc/vc_di/prove.py new file mode 100644 index 0000000000..6071a7f7c2 --- /dev/null +++ b/aries_cloudagent/vc/vc_di/prove.py @@ -0,0 +1,248 @@ +"""Verifiable Credential and Presentation proving methods.""" + +from typing import List +from hashlib import sha256 +import time + +from ..ld_proofs import ( + AuthenticationProofPurpose, + ProofPurpose, + DocumentLoaderMethod, + sign, + LinkedDataProof, + LinkedDataProofException, + derive, +) +from ..ld_proofs.constants import CREDENTIALS_CONTEXT_V1_URL +from ..vc_ld.models.credential import VerifiableCredentialSchema +from ...anoncreds.holder import AnonCredsHolder +from ...anoncreds.verifier import AnonCredsVerifier +from ...core.profile import Profile +from anoncreds import ( + W3cCredential, +) + + +async def create_signed_anoncreds_presentation( + *, + profile: Profile, + pres_definition: dict, + presentation: dict, + credentials: list, + purpose: ProofPurpose = None, + challenge: str = None, + domain: str = None, + holder: bool = True, +) -> (dict, dict, dict): + """Sign the presentation with the passed signature suite. + + Will set a default AuthenticationProofPurpose if no proof purpose is passed. + + Args: + presentation (dict): The presentation to sign + suite (LinkedDataProof): The signature suite to sign the presentation with + document_loader (DocumentLoader): Document loader to use. + purpose (ProofPurpose, optional): Purpose to use. Required if challenge is None + challenge (str, optional): Challenge to use. Required if domain is None. + domain (str, optional): Domain to use. Only used if purpose is None. + + Raises: + LinkedDataProofException: When both purpose and challenge are not provided + And when signing of the presentation fails + + Returns: + dict: A verifiable presentation object + + """ + if not purpose and not challenge: + raise LinkedDataProofException( + 'A "challenge" param is required when not providing a' + ' "purpose" (for AuthenticationProofPurpose).' + ) + if not purpose: + purpose = AuthenticationProofPurpose(challenge=challenge, domain=domain) + + # validate structure of presentation + pres_submission = presentation["presentation_submission"] + descriptor_map = pres_submission["descriptor_map"] + + w3c_creds = [] + w3c_creds_metadata = [] + for credential in credentials: + w3c_cred = W3cCredential.load(credential) + w3c_creds.append(w3c_cred) + w3c_creds_metadata.append({}) + + schema_ids = [] + cred_def_ids = [] + + pres_name = ( + pres_definition.get("name") if pres_definition.get("name") else "Proof request" + ) + hash = sha256(challenge.encode("utf-8")).hexdigest() + nonce = str(int(hash, 16))[:20] + + # assemble the necessary structures and then call AnoncredsHolder.create_presentation_w3c() (new method) + anoncreds_proofrequest = { + "version": "1.0", + "name": pres_name, + "nonce": nonce, + "requested_attributes": {}, + "requested_predicates": {}, + } + + non_revoked = int(time.time()) + non_revoked_interval = {"from": non_revoked, "to": non_revoked} + + for descriptor_map_item in descriptor_map: + descriptor = next( + item + for item in pres_definition["input_descriptors"] + if item["id"] == descriptor_map_item["id"] + ) + + referent = descriptor_map_item["id"] + attribute_referent = f"{referent}_attribute" + predicate_referent_base = f"{referent}_predicate" + predicate_referent_index = 0 + issuer_id = None + + fields = descriptor["constraints"]["fields"] + statuses = descriptor["constraints"]["statuses"] + + # descriptor_map_item['path'] should be something like '$.verifiableCredential[n]', we need to extract 'n' + entry_idx = _extract_cred_idx(descriptor_map_item["path"]) + w3c_cred = w3c_creds[entry_idx] + schema_id = w3c_cred.schema_id + cred_def_id = w3c_cred.cred_def_id + rev_reg_id = w3c_cred.rev_reg_id + rev_reg_index = w3c_cred.rev_reg_index + + requires_revoc_status = "active" in statuses and statuses["active"][ + "directive" + ] in ("allowed", "required") + # TODO check that a revocation id is supplied if required + # if requires_revoc_status and (not rev_reg_id): + # throw some kind of error + + w3c_creds_metadata[entry_idx] = { + "schema_id": schema_id, + "cred_def_id": cred_def_id, + "revoc_status": non_revoked_interval if requires_revoc_status else None, + "proof_attrs": [], + "proof_preds": [], + } + + for field in fields: + path = field["path"][0] + + # check for credential attributes vs other + if path.startswith("$.credentialSubject."): + property_name = path.replace("$.credentialSubject.", "") + if "predicate" in field: + # get predicate info + pred_filter = field["filter"] + (p_type, p_value) = _get_predicate_type_and_value(pred_filter) + pred_request = { + "name": property_name, + "p_type": p_type, + "p_value": p_value, + "restrictions": [{"cred_def_id": cred_def_id}], + "non_revoked": ( + non_revoked_interval if requires_revoc_status else None + ), + } + predicate_referent = ( + f"{predicate_referent_base}_{predicate_referent_index}" + ) + predicate_referent_index = predicate_referent_index + 1 + anoncreds_proofrequest["requested_predicates"][ + predicate_referent + ] = pred_request + w3c_creds_metadata[entry_idx]["proof_preds"].append( + predicate_referent + ) + else: + # no predicate, just a revealed attribute + attr_request = { + "names": [property_name], + "restrictions": [{"cred_def_id": cred_def_id}], + "non_revoked": ( + non_revoked_interval if requires_revoc_status else None + ), + } + # check if we already have this referent ... + if ( + attribute_referent + in anoncreds_proofrequest["requested_attributes"] + ): + anoncreds_proofrequest["requested_attributes"][ + attribute_referent + ]["names"].append(property_name) + else: + anoncreds_proofrequest["requested_attributes"][ + attribute_referent + ] = attr_request + w3c_creds_metadata[entry_idx]["proof_attrs"].append( + attribute_referent + ) + elif path.endswith(".issuer"): + # capture issuer - {'path': ['$.issuer'], 'filter': {'type': 'string', 'const': '569XGicsXvYwi512asJkKB'}} + # TODO prob not a general case + issuer_id = field["filter"]["const"] + else: + print("... skipping:", path) + + anoncreds_verifier = AnonCredsVerifier(profile) + ( + schemas, + cred_defs, + rev_reg_defs, + rev_reg_entries, + ) = await anoncreds_verifier.process_pres_identifiers(w3c_creds_metadata) + + # TODO possibly refactor this into a couple of methods - one to create the proof request and another to sign it + # (the holder flag is a bit of a hack) + if holder: + # TODO match up the parameters with what the function is expecting ... + anoncreds_holder = AnonCredsHolder(profile) + anoncreds_proof = await anoncreds_holder.create_presentation_w3c( + presentation_request=anoncreds_proofrequest, + requested_credentials_w3c=w3c_creds, + credentials_w3c_metadata=w3c_creds_metadata, + schemas=schemas, + credential_definitions=cred_defs, + rev_states=None, + ) + + # TODO any processing to put the returned proof into DIF format + anoncreds_proof["presentation_submission"] = presentation[ + "presentation_submission" + ] + else: + anoncreds_proof = None + + return (anoncreds_proofrequest, anoncreds_proof, w3c_creds_metadata) + + +def _extract_cred_idx(item_path: str) -> int: + # TODO put in some logic here ... + print(">>> TODO need to parse the index from this path:", item_path) + return 0 + + +def _get_predicate_type_and_value(pred_filter: dict) -> (str, str): + supported_properties = { + "exclusiveMinimum": ">", + "exclusiveMaximum": "<", + "minimum": ">=", + "maximum": "<=", + } + + # TODO handle multiple predicates? + for key in pred_filter.keys(): + if key in supported_properties: + return (supported_properties[key], pred_filter[key]) + + # TODO more informative description + raise Exception() diff --git a/aries_cloudagent/vc/vc_di/verify.py b/aries_cloudagent/vc/vc_di/verify.py new file mode 100644 index 0000000000..a3120e2895 --- /dev/null +++ b/aries_cloudagent/vc/vc_di/verify.py @@ -0,0 +1,81 @@ +"""Verifiable Credential and Presentation verification methods.""" + +import asyncio +from typing import List +from pyld.jsonld import JsonLdProcessor + +from ...core.profile import Profile +from ...anoncreds.verifier import AnonCredsVerifier +from ..ld_proofs import ( + LinkedDataProof, + CredentialIssuancePurpose, + DocumentLoaderMethod, + ProofPurpose, + AuthenticationProofPurpose, + verify as ld_proofs_verify, + DocumentVerificationResult, + LinkedDataProofException, +) +from ..vc_ld.models.credential import VerifiableCredentialSchema +from ..vc_ld.validation_result import PresentationVerificationResult +from .prove import create_signed_anoncreds_presentation + + +async def verify_signed_anoncredspresentation( + *, + profile: Profile, + presentation: dict, + purpose: ProofPurpose = None, + challenge: str = None, + domain: str = None, + pres_req: dict = None, +) -> PresentationVerificationResult: + """Verify presentation structure, credentials, proof purpose and signature. + + Args: + presentation (dict): The presentation to verify + suites (List[LinkedDataProof]): The signature suites to verify with + document_loader (DocumentLoader): Document loader used for resolving of documents + purpose (ProofPurpose, optional): Proof purpose to use. + Defaults to AuthenticationProofPurpose + challenge (str, optional): The challenge to use for authentication. + Required if purpose is not passed, not used if purpose is passed + domain (str, optional): Domain to use for the authentication proof purpose. + Not used if purpose is passed + + Returns: + PresentationVerificationResult: The result of the verification. Verified property + indicates whether the verification was successful + + """ + + # TODO: I think we should add some sort of options to authenticate the subject id + # to the presentation verification method controller + anoncreds_verifier = AnonCredsVerifier(profile) + + credentials = presentation["verifiableCredential"] + pres_definition = pres_req["presentation_definition"] + + (anoncreds_pres_req, _signed_vp, cred_metadata) = ( + await create_signed_anoncreds_presentation( + profile=profile, + pres_definition=pres_definition, + presentation=presentation, + credentials=credentials, + challenge=challenge, + domain=domain, + holder=False, + ) + ) + + try: + return await anoncreds_verifier.verify_presentation_w3c( + anoncreds_pres_req, + presentation, + ) + except Exception as e: + raise e + # return PresentationVerificationResult(verified=False, errors=[e]) + + +__all__ = ["verify_presentation", "verify_credential"] diff --git a/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py b/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py index 6787e82be7..c441ff5911 100644 --- a/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py +++ b/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py @@ -92,7 +92,7 @@ class Meta: ) created = fields.Str( - required=True, + required=False, validate=INDY_ISO8601_DATETIME_VALIDATE, metadata={ "description": ( diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 9a84f54aef..4469eb1b5d 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -431,6 +431,8 @@ async def handle_present_proof_v2_0(self, message): pres_ex_id = message["pres_ex_id"] self.log(f"Presentation: state = {state}, pres_ex_id = {pres_ex_id}") + print(f"Presentation: state = {state}, pres_ex_id = {pres_ex_id}") + if state in ["request-received"]: # prover role log_status( @@ -451,6 +453,8 @@ async def handle_present_proof_v2_0(self, message): if not pres_request_dif and not pres_request_indy: raise Exception("Invalid presentation request received") + # TODO for a DIF presentation need to check if it is asking for an anoncreds proof ... + if pres_request_indy: # include self-attested attributes (not included in credentials) creds_by_reft = {} @@ -463,6 +467,7 @@ async def handle_present_proof_v2_0(self, message): creds = await self.admin_GET( f"/present-proof-2.0/records/{pres_ex_id}/credentials" ) + # print(">>> creds:", creds) if creds: # select only indy credentials creds = [x for x in creds if "cred_info" in x] @@ -521,6 +526,7 @@ async def handle_present_proof_v2_0(self, message): creds = await self.admin_GET( f"/present-proof-2.0/records/{pres_ex_id}/credentials" ) + if creds and 0 < len(creds): # select only dif credentials creds = [x for x in creds if "issuanceDate" in x] diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 6de8018c80..f0ddc22542 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -350,47 +350,49 @@ def generate_proof_request_web_request( "domain": "4jt78h47fh47", }, "presentation_definition": { - "id": "32f54163-7166-48f1-93d8-ff217bdb0654", - "submission_requirements": [ - { - "name": "Degree Verification", - "rule": "pick", - "min": 1, - "from": "A", - } - ], + "id": "5591656f-5b5d-40f8-ab5c-9041c8e3a6a0", + "name": "Age Verification", + "purpose": "We need to verify your age before entering a bar", "input_descriptors": [ { - "id": "degree_input_1", - "name": "Degree Certificate", - "group": ["A"], + "id": "age-verification", + "name": "A specific type of VC + Issuer", + "purpose": "We want a VC of this type generated by this issuer", "schema": [ { "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" - }, - { - "uri": "https://w3id.org/citizenship#PermanentResidentCard" - }, + } ], "constraints": { + "statuses": { + "active": {"directive": "required"} + }, "limit_disclosure": "required", "fields": [ + { + "path": ["$.issuer"], + "filter": { + "type": "string", + "const": self.did, + }, + }, + {"path": ["$.credentialSubject.name"]}, { "path": [ - "$.credentialSubject.degree.name" - ], - "purpose": "We need to verify that you have the required degree.", - "filter": {"type": "string"}, + "$.credentialSubject.degree" + ] }, { "path": [ - "$.credentialSubject.birthDate" + "$.credentialSubject.birthdate_dateint" ], - "purpose": "To ensure you meet the age requirement.", + "predicate": "preferred", "filter": { - "type": "string", - "pattern": birth_date.strftime( - birth_date_format + "type": "number", + "maximum": int( + birth_date.strftime( + birth_date_format + ) ), }, }, @@ -398,6 +400,15 @@ def generate_proof_request_web_request( }, } ], + "format": { + "di_vc": { + "proof_type": ["DataIntegrityProof"], + "cryptosuite": [ + "anoncreds-2023", + "eddsa-rdfc-2022", + ], + } + }, }, }, },