diff --git a/JsonLdCredentials.md b/JsonLdCredentials.md index 314dabdcde..728608ee5f 100644 --- a/JsonLdCredentials.md +++ b/JsonLdCredentials.md @@ -16,6 +16,7 @@ By design Hyperledger Aries is credential format agnostic. This means you can us - [Issuing Credentials](#issuing-credentials) - [Retrieving Issued Credentials](#retrieving-issued-credentials) - [Present Proof](#present-proof) +- [VC-API Endpoints](#vc-api) ## General Concept @@ -210,3 +211,18 @@ Call the `/credentials/w3c` endpoint to retrieve all JSON-LD credentials in your ## Present Proof > ⚠️ TODO: https://github.com/hyperledger/aries-cloudagent-python/pull/1125 + +## VC-API + +In order to support these functionalities outside of the respective DIDComm protocols, a set of endpoints conforming to the [vc-api](https://w3c-ccg.github.io/vc-api) specification are available. These endpoints should be used by a controller when building an identity platform. + +These endpoints include: +- `GET /vc/credentials` -> returns a list of all stored json-ld credentials +- `GET /vc/credentials/{id}` -> returns a json-ld credential based on it's ID +- `POST /vc/credentials/issue` -> signs a credential +- `POST /vc/credentials/verify` -> verifies a credential +- `POST /vc/credentials/store` -> stores an issued credential +- `POST /vc/presentations/prove` -> proves a presentation +- `POST /vc/presentations/verify` -> verifies a presentation + +To learn more about using these endpoints, please refer to the available [postman collection](./demo/AriesPostmanDemo.md#experimenting-with-the-vc-api-endpoints). diff --git a/README.md b/README.md index aab7293411..74f29fd3fd 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,10 @@ ACA-Py supports a Transaction Endorsement protocol, for agents that don't have w ACA-Py supports deployments in scaled environments such as in Kubernetes environments where ACA-Py and its storage components can be horizontally scaled as needed to handle the load. +### VC-API Endpoints + +A set of endpoints conforming to the vc-api specification are included to manage w3c credentials and presentations. They are documented [here](JsonLdCredentials.md#vc-api) and a postman demo is available [here](./demo/AriesPostmanDemo.md#vc-api). + ## Example Uses The business logic you use with ACA-Py is limited only by your imagination. Possible applications include: diff --git a/aries_cloudagent/messaging/valid.py b/aries_cloudagent/messaging/valid.py index 96522cadfc..c2266197ff 100644 --- a/aries_cloudagent/messaging/valid.py +++ b/aries_cloudagent/messaging/valid.py @@ -776,6 +776,27 @@ def __call__(self, value): return value +class PresentationType(Validator): + """Presentation Type.""" + + PRESENTATIONL_TYPE = "VerifiablePresentation" + EXAMPLE = [PRESENTATIONL_TYPE] + + def __init__(self) -> None: + """Initialize the instance.""" + super().__init__() + + def __call__(self, value): + """Validate input value.""" + length = len(value) + if length < 1 or PresentationType.PRESENTATIONL_TYPE not in value: + raise ValidationError( + f"type must include {PresentationType.PRESENTATIONL_TYPE}" + ) + + return value + + class CredentialContext(Validator): """Credential Context.""" @@ -827,6 +848,28 @@ def __call__(self, value): return value +class CredentialStatus(Validator): + """Credential status.""" + + EXAMPLE = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": "94567", + "statusListCredential": "https://example.com/credentials/status/3", + } + + def __init__(self) -> None: + """Initialize the instance.""" + super().__init__() + + def __call__(self, value): + """Validate input value.""" + # TODO write some tests + + return value + + class IndyOrKeyDID(Regexp): """Indy or Key DID class.""" @@ -967,5 +1010,11 @@ def __init__( CREDENTIAL_SUBJECT_VALIDATE = CredentialSubject() CREDENTIAL_SUBJECT_EXAMPLE = CredentialSubject.EXAMPLE +CREDENTIAL_STATUS_VALIDATE = CredentialStatus() +CREDENTIAL_STATUS_EXAMPLE = CredentialStatus.EXAMPLE + +PRESENTATION_TYPE_VALIDATE = PresentationType() +PRESENTATION_TYPE_EXAMPLE = PresentationType.EXAMPLE + INDY_OR_KEY_DID_VALIDATE = IndyOrKeyDID() INDY_OR_KEY_DID_EXAMPLE = IndyOrKeyDID.EXAMPLE diff --git a/aries_cloudagent/vc/routes.py b/aries_cloudagent/vc/routes.py index 74bdee581d..0676fe9f92 100644 --- a/aries_cloudagent/vc/routes.py +++ b/aries_cloudagent/vc/routes.py @@ -1,124 +1,247 @@ -"""VC Routes.""" +"""VC-API Routes.""" from aiohttp import web from aiohttp_apispec import docs, request_schema, response_schema - -from marshmallow import ValidationError, fields, validates_schema - -from aries_cloudagent.vc.vc_ld.validation_result import ( - PresentationVerificationResultSchema, -) - -from .vc_ld.models.credential import ( - CredentialSchema, - VerifiableCredential, - VerifiableCredentialSchema, -) -from .vc_ld.models.options import LDProofVCOptions, LDProofVCOptionsSchema -from .vc_ld.manager import VcLdpManager, VcLdpManagerError +from marshmallow.exceptions import ValidationError +import uuid from ..admin.request_context import AdminRequestContext +from ..storage.error import StorageError, StorageNotFoundError, StorageDuplicateError +from ..wallet.error import WalletError +from ..wallet.base import BaseWallet from ..config.base import InjectionError from ..resolver.base import ResolverError -from ..wallet.error import WalletError -from ..messaging.models.openapi import OpenAPISchema +from ..storage.vc_holder.base import VCHolder +from .vc_ld.models import web_schemas +from .vc_ld.manager import VcLdpManager +from .vc_ld.manager import VcLdpManagerError +from .vc_ld.models.credential import ( + VerifiableCredential, +) +from .vc_ld.models.presentation import ( + VerifiablePresentation, +) -class LdpIssueRequestSchema(OpenAPISchema): - """Request schema for signing an ldb_vc.""" +from .vc_ld.models.options import LDProofVCOptions - credential = fields.Nested(CredentialSchema) - options = fields.Nested(LDProofVCOptionsSchema) +@docs(tags=["vc-api"], summary="List credentials") +@response_schema(web_schemas.ListCredentialsResponse(), 200, description="") +async def list_credentials_route(request: web.BaseRequest): + """Request handler for issuing a credential. -class LdpIssueResponseSchema(OpenAPISchema): - """Request schema for signing an ldb_vc.""" + Args: + request: aiohttp request object - vc = fields.Nested(VerifiableCredentialSchema) + """ + context: AdminRequestContext = request["context"] + holder = context.inject(VCHolder) + try: + search = holder.search_credentials() + records = [record.serialize()["cred_value"] for record in await search.fetch()] + return web.json_response(records, status=200) + except (StorageError, StorageNotFoundError) as err: + return web.json_response({"message": err.roll_up}, status=400) -@docs(tags=["ldp_vc"], summary="Sign an LDP VC.") -@request_schema(LdpIssueRequestSchema()) -@response_schema(LdpIssueResponseSchema(), 200, description="") -async def ldp_issue(request: web.BaseRequest): - """Request handler for signing a jsonld doc. +@docs(tags=["vc-api"], summary="Fetch credential by ID") +@response_schema(web_schemas.FetchCredentialResponse(), 200, description="") +async def fetch_credential_route(request: web.BaseRequest): + """Request handler for issuing a credential. Args: request: aiohttp request object """ context: AdminRequestContext = request["context"] - body = await request.json() - credential = VerifiableCredential.deserialize(body["credential"]) - options = LDProofVCOptions.deserialize(body["options"]) + holder = context.inject(VCHolder) + try: + credential_id = request.match_info["credential_id"].strip('"') + record = await holder.retrieve_credential_by_id(record_id=credential_id) + return web.json_response(record.serialize()["cred_value"], status=200) + except (StorageError, StorageNotFoundError) as err: + return web.json_response({"message": err.roll_up}, status=400) + + +@docs(tags=["vc-api"], summary="Issue a credential") +@request_schema(web_schemas.IssueCredentialRequest()) +@response_schema(web_schemas.IssueCredentialResponse(), 200, description="") +async def issue_credential_route(request: web.BaseRequest): + """Request handler for issuing a credential. + Args: + request: aiohttp request object + + """ + body = await request.json() + context: AdminRequestContext = request["context"] + manager = context.inject(VcLdpManager) try: - manager = context.inject(VcLdpManager) + credential = body["credential"] + options = {} if "options" not in body else body["options"] + + # We derive the proofType from the issuer DID if not provided in options + if not options.get("type", None) and not options.get("proofType", None): + issuer = credential["issuer"] + did = issuer if isinstance(issuer, str) else issuer["id"] + async with context.session() as session: + wallet: BaseWallet | None = session.inject_or(BaseWallet) + info = await wallet.get_local_did(did) + key_type = info.key_type.key_type + + if key_type == "ed25519": + options["proofType"] = "Ed25519Signature2020" + elif key_type == "bls12381g2": + options["proofType"] = "BbsBlsSignature2020" + else: + options["proofType"] = ( + options.pop("type") if "type" in options else options["proofType"] + ) + + credential = VerifiableCredential.deserialize(credential) + options = LDProofVCOptions.deserialize(options) + vc = await manager.issue(credential, options) - except VcLdpManagerError as err: - return web.json_response({"error": str(err)}, status=400) - except (WalletError, InjectionError): - raise web.HTTPForbidden(reason="No wallet available") - return web.json_response({"vc": vc.serialize()}) + response = {"verifiableCredential": vc.serialize()} + return web.json_response(response, status=201) + except (ValidationError, VcLdpManagerError, WalletError, InjectionError) as err: + return web.json_response({"message": str(err)}, status=400) -class LdpVerifyRequestSchema(OpenAPISchema): - """Request schema for verifying an LDP VP.""" +@docs(tags=["vc-api"], summary="Verify a credential") +@request_schema(web_schemas.VerifyCredentialRequest()) +@response_schema(web_schemas.VerifyCredentialResponse(), 200, description="") +async def verify_credential_route(request: web.BaseRequest): + """Request handler for verifying a credential. + + Args: + request: aiohttp request object + + """ + body = await request.json() + context: AdminRequestContext = request["context"] + manager = context.inject(VcLdpManager) + try: + vc = VerifiableCredential.deserialize(body["verifiableCredential"]) + result = await manager.verify_credential(vc) + result = result.serialize() + return web.json_response(result) + except ( + ValidationError, + VcLdpManagerError, + ResolverError, + ValueError, + WalletError, + InjectionError, + ) as err: + return web.json_response({"message": str(err)}, status=400) + + +@docs(tags=["vc-api"], summary="Store a credential") +async def store_credential_route(request: web.BaseRequest): + """Request handler for storing a credential. - vp = fields.Nested(VerifiableCredentialSchema, required=False) - vc = fields.Nested(VerifiableCredentialSchema, required=False) - options = fields.Nested(LDProofVCOptionsSchema) + Args: + request: aiohttp request object - @validates_schema - def validate_fields(self, data, **kwargs): - """Validate schema fields. + """ + body = await request.json() + context: AdminRequestContext = request["context"] + manager = context.inject(VcLdpManager) - Args: - data: The data to validate + try: + vc = body["verifiableCredential"] + cred_id = vc["id"] if "id" in vc else f"urn:uuid:{str(uuid.uuid4())}" + options = {} if "options" not in body else body["options"] - Raises: - ValidationError: if data has neither indy nor ld_proof + vc = VerifiableCredential.deserialize(vc) + options = LDProofVCOptions.deserialize(options) - """ - if not data.get("vp") and not data.get("vc"): - raise ValidationError("Field vp or vc must be present") - if data.get("vp") and data.get("vc"): - raise ValidationError("Field vp or vc must be present, not both") + await manager.verify_credential(vc) + await manager.store_credential(vc, options, cred_id) + return web.json_response({"credentialId": cred_id}, status=200) -class LdpVerifyResponseSchema(PresentationVerificationResultSchema): - """Request schema for verifying an LDP VP.""" + except ( + ValidationError, + VcLdpManagerError, + WalletError, + InjectionError, + StorageDuplicateError, + ) as err: + return web.json_response({"message": str(err)}, status=400) -@docs(tags=["ldp_vc"], summary="Verify an LDP VC or VP.") -@request_schema(LdpVerifyRequestSchema()) -@response_schema(LdpVerifyResponseSchema(), 200, description="") -async def ldp_verify(request: web.BaseRequest): - """Request handler for signing a jsonld doc. +@docs(tags=["vc-api"], summary="Prove a presentation") +@request_schema(web_schemas.ProvePresentationRequest()) +@response_schema(web_schemas.ProvePresentationResponse(), 200, description="") +async def prove_presentation_route(request: web.BaseRequest): + """Request handler for proving a presentation. Args: request: aiohttp request object """ context: AdminRequestContext = request["context"] + manager = context.inject(VcLdpManager) body = await request.json() - vp = body.get("vp") - vc = body.get("vc") try: - manager = context.inject(VcLdpManager) - if vp: - vp = VerifiableCredential.deserialize(vp) - options = LDProofVCOptions.deserialize(body["options"]) - result = await manager.verify_presentation(vp, options) - elif vc: - vc = VerifiableCredential.deserialize(vc) - result = await manager.verify_credential(vc) + presentation = body["presentation"] + options = {} if "options" not in body else body["options"] + + # We derive the proofType from the holder DID if not provided in options + if not options.get("type", None): + holder = presentation["holder"] + did = holder if isinstance(holder, str) else holder["id"] + async with context.session() as session: + wallet: BaseWallet | None = session.inject_or(BaseWallet) + info = await wallet.get_local_did(did) + key_type = info.key_type.key_type + + if key_type == "ed25519": + options["proofType"] = "Ed25519Signature2020" + elif key_type == "bls12381g2": + options["proofType"] = "BbsBlsSignature2020" else: - raise web.HTTPBadRequest(reason="vp or vc must be present") - return web.json_response(result.serialize()) - except (VcLdpManagerError, ResolverError, ValueError) as error: - raise web.HTTPBadRequest(reason=str(error)) - except (WalletError, InjectionError): - raise web.HTTPForbidden(reason="No wallet available") + options["proofType"] = options.pop("type") + + presentation = VerifiablePresentation.deserialize(presentation) + options = LDProofVCOptions.deserialize(options) + vp = await manager.prove(presentation, options) + return web.json_response({"verifiablePresentation": vp.serialize()}, status=201) + + except (ValidationError, VcLdpManagerError, WalletError, InjectionError) as err: + return web.json_response({"message": str(err)}, status=400) + + +@docs(tags=["vc-api"], summary="Verify a Presentation") +@request_schema(web_schemas.VerifyPresentationRequest()) +@response_schema(web_schemas.VerifyPresentationResponse(), 200, description="") +async def verify_presentation_route(request: web.BaseRequest): + """Request handler for verifying a presentation. + + Args: + request: aiohttp request object + + """ + context: AdminRequestContext = request["context"] + manager = context.inject(VcLdpManager) + body = await request.json() + try: + vp = VerifiablePresentation.deserialize(body["verifiablePresentation"]) + options = {} if "options" not in body else body["options"] + options = LDProofVCOptions.deserialize(options) + verified = await manager.verify_presentation(vp, options) + return web.json_response(verified.serialize(), status=200) + except ( + ValidationError, + WalletError, + InjectionError, + VcLdpManagerError, + ResolverError, + ValueError, + ) as err: + return web.json_response({"message": str(err)}, status=400) async def register(app: web.Application): @@ -126,8 +249,17 @@ async def register(app: web.Application): app.add_routes( [ - web.post("/vc/ldp/issue", ldp_issue), - web.post("/vc/ldp/verify", ldp_verify), + web.get("/vc/credentials", list_credentials_route, allow_head=False), + web.get( + "/vc/credentials/{credential_id}", + fetch_credential_route, + allow_head=False, + ), + web.post("/vc/credentials/issue", issue_credential_route), + web.post("/vc/credentials/store", store_credential_route), + web.post("/vc/credentials/verify", verify_credential_route), + web.post("/vc/presentations/prove", prove_presentation_route), + web.post("/vc/presentations/verify", verify_presentation_route), ] ) @@ -139,11 +271,11 @@ def post_process_routes(app: web.Application): app._state["swagger_dict"]["tags"] = [] app._state["swagger_dict"]["tags"].append( { - "name": "ldp-vc", - "description": "Issue and verify LDP VCs and VPs", + "name": "vc-api", + "description": "Endpoints for managing w3c credentials and presentations", "externalDocs": { "description": "Specification", - "url": "https://www.w3.org/TR/vc-data-model/", + "url": "https://w3c-ccg.github.io/vc-api/", }, } ) diff --git a/aries_cloudagent/vc/vc_ld/manager.py b/aries_cloudagent/vc/vc_ld/manager.py index d37a2da47e..8a306cceea 100644 --- a/aries_cloudagent/vc/vc_ld/manager.py +++ b/aries_cloudagent/vc/vc_ld/manager.py @@ -1,6 +1,8 @@ """Manager for performing Linked Data Proof signatures 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 ...wallet.base import BaseWallet @@ -8,6 +10,8 @@ from ...wallet.did_info import DIDInfo from ...wallet.error import WalletNotFoundError from ...wallet.key_type import BLS12381G2, ED25519, KeyType +from ...storage.vc_holder.base import VCHolder +from ...storage.vc_holder.vc_record import VCRecord from ..ld_proofs.constants import ( SECURITY_CONTEXT_BBS_URL, SECURITY_CONTEXT_ED25519_2020_URL, @@ -26,6 +30,7 @@ from ..vc_ld.models.presentation import VerifiablePresentation from ..vc_ld.validation_result import PresentationVerificationResult from .issue import issue as ldp_issue +from .prove import sign_presentation from .models.credential import VerifiableCredential from .models.linked_data_proof import LDProof from .models.options import LDProofVCOptions @@ -271,10 +276,37 @@ async def prepare_credential( return credential - async def _get_suite_for_credential( - self, credential: VerifiableCredential, options: LDProofVCOptions + async def prepare_presentation( + self, + presentation: VerifiablePresentation, + options: LDProofVCOptions, + ) -> VerifiableCredential: + """Prepare a presentation for issuance.""" + # Add BBS context if not present yet + if ( + options.proof_type == BbsBlsSignature2020.signature_type + and SECURITY_CONTEXT_BBS_URL not in presentation.context_urls + ): + presentation.add_context(SECURITY_CONTEXT_BBS_URL) + # Add ED25519-2020 context if not present yet + elif ( + options.proof_type == Ed25519Signature2020.signature_type + and SECURITY_CONTEXT_ED25519_2020_URL not in presentation.context_urls + ): + presentation.add_context(SECURITY_CONTEXT_ED25519_2020_URL) + + return presentation + + async def _get_suite_for_document( + self, + document: Union[VerifiableCredential, VerifiablePresentation], + options: LDProofVCOptions, ) -> LinkedDataProof: - issuer_id = credential.issuer_id + if isinstance(document, VerifiableCredential): + issuer_id = document.issuer_id + elif isinstance(document, VerifiablePresentation): + issuer_id = document.holder_id + proof_type = options.proof_type if not issuer_id: @@ -342,7 +374,7 @@ async def issue( credential = await self.prepare_credential(credential, options) # Get signature suite, proof purpose and document loader - suite = await self._get_suite_for_credential(credential, options) + suite = await self._get_suite_for_document(credential, options) proof_purpose = self._get_proof_purpose( proof_purpose=options.proof_purpose, challenge=options.challenge, @@ -358,10 +390,76 @@ async def issue( ) return VerifiableCredential.deserialize(vc) + async def store_credential( + self, vc: VerifiableCredential, options: LDProofVCOptions, cred_id: str = None + ) -> VerifiableCredential: + """Store a verifiable credential.""" + + # Saving expanded type as a cred_tag + document_loader = self.profile.inject(DocumentLoader) + expanded = jsonld.expand( + vc.serialize(), options={"documentLoader": document_loader} + ) + types = JsonLdProcessor.get_values( + expanded[0], + "@type", + ) + vc_record = VCRecord( + contexts=vc.context_urls, + expanded_types=types, + issuer_id=vc.issuer_id, + subject_ids=vc.credential_subject_ids, + schema_ids=[], # Schemas not supported yet + proof_types=[vc.proof.type], + cred_value=vc.serialize(), + given_id=vc.id, + record_id=cred_id, + cred_tags=None, # Tags should be derived from credential values + ) + + async with self.profile.session() as session: + vc_holder = session.inject(VCHolder) + + await vc_holder.store_credential(vc_record) + + async def verify_credential( + self, vc: VerifiableCredential + ) -> DocumentVerificationResult: + """Verify a VC with a Linked Data Proof.""" + return await verify_credential( + credential=vc.serialize(), + suites=await self._get_all_proof_suites(), + document_loader=self.profile.inject(DocumentLoader), + ) + + async def prove( + self, presentation: VerifiablePresentation, options: LDProofVCOptions + ) -> VerifiablePresentation: + """Sign a VP with a Linked Data Proof.""" + presentation = await self.prepare_presentation(presentation, options) + + # Get signature suite, proof purpose and document loader + suite = await self._get_suite_for_document(presentation, options) + proof_purpose = self._get_proof_purpose( + proof_purpose=options.proof_purpose, + challenge=options.challenge, + domain=options.domain, + ) + document_loader = self.profile.inject(DocumentLoader) + + vp = await sign_presentation( + presentation=presentation.serialize(), + suite=suite, + document_loader=document_loader, + purpose=proof_purpose, + ) + return VerifiablePresentation.deserialize(vp) + async def verify_presentation( self, vp: VerifiablePresentation, options: LDProofVCOptions ) -> PresentationVerificationResult: """Verify a VP with a Linked Data Proof.""" + if not options.challenge: raise VcLdpManagerError("Challenge is required for verifying a VP") @@ -371,13 +469,3 @@ async def verify_presentation( document_loader=self.profile.inject(DocumentLoader), challenge=options.challenge, ) - - async def verify_credential( - self, vc: VerifiableCredential - ) -> DocumentVerificationResult: - """Verify a VC with a Linked Data Proof.""" - return await verify_credential( - credential=vc.serialize(), - suites=await self._get_all_proof_suites(), - document_loader=self.profile.inject(DocumentLoader), - ) diff --git a/aries_cloudagent/vc/vc_ld/models/credential.py b/aries_cloudagent/vc/vc_ld/models/credential.py index 101568a687..6a63df6b7d 100644 --- a/aries_cloudagent/vc/vc_ld/models/credential.py +++ b/aries_cloudagent/vc/vc_ld/models/credential.py @@ -13,6 +13,8 @@ CREDENTIAL_CONTEXT_VALIDATE, CREDENTIAL_SUBJECT_EXAMPLE, CREDENTIAL_SUBJECT_VALIDATE, + CREDENTIAL_STATUS_EXAMPLE, + CREDENTIAL_STATUS_VALIDATE, CREDENTIAL_TYPE_EXAMPLE, CREDENTIAL_TYPE_VALIDATE, RFC3339_DATETIME_EXAMPLE, @@ -47,6 +49,7 @@ def __init__( issuance_date: Optional[str] = None, expiration_date: Optional[str] = None, credential_subject: Optional[Union[dict, List[dict]]] = None, + credential_status: Optional[Union[dict, List[dict]]] = None, proof: Optional[Union[dict, LDProof]] = None, **kwargs, ) -> None: @@ -56,6 +59,7 @@ def __init__( self._type = type or [VERIFIABLE_CREDENTIAL_TYPE] self._issuer = issuer self._credential_subject = credential_subject + self._credential_status = credential_status # TODO: proper date parsing self._issuance_date = issuance_date @@ -232,6 +236,11 @@ def credential_subject(self, credential_subject: Union[dict, List[dict]]): self._credential_subject = credential_subject + @property + def credential_status(self): + """Getter for credential status.""" + return self._credential_status + @property def proof(self): """Getter for proof.""" @@ -253,6 +262,7 @@ def __eq__(self, o: object) -> bool: and self.issuance_date == o.issuance_date and self.expiration_date == o.expiration_date and self.credential_subject == o.credential_subject + and self.credential_status == o.credential_status and self.proof == o.proof and self.extra == o.extra ) @@ -341,6 +351,13 @@ class Meta: metadata={"example": CREDENTIAL_SUBJECT_EXAMPLE}, ) + credential_status = DictOrDictListField( + required=False, + data_key="credentialStatus", + validate=CREDENTIAL_STATUS_VALIDATE, + metadata={"example": CREDENTIAL_STATUS_EXAMPLE}, + ) + proof = fields.Nested( LinkedDataProofSchema(), required=False, 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 81091b7977..40e5a2b7db 100644 --- a/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py +++ b/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py @@ -105,12 +105,14 @@ class Meta: domain = fields.Str( required=False, + # TODO the domain can be more than a Uri, provide a less restrictive validation + # https://www.w3.org/TR/vc-data-integrity/#defn-domain validate=Uri(), metadata={ "description": ( "A string value specifying the restricted domain of the signature." ), - "example": "example.com", + "example": "https://example.com", }, ) diff --git a/aries_cloudagent/vc/vc_ld/models/presentation.py b/aries_cloudagent/vc/vc_ld/models/presentation.py index 0e467c9fa5..4ceebd92a2 100644 --- a/aries_cloudagent/vc/vc_ld/models/presentation.py +++ b/aries_cloudagent/vc/vc_ld/models/presentation.py @@ -1,11 +1,23 @@ """Verifiable Presentation model.""" -from typing import Optional, Sequence, Union +from typing import List, Optional, Union -from marshmallow import INCLUDE, fields +from marshmallow import INCLUDE, ValidationError, fields, post_dump from ....messaging.models.base import BaseModel, BaseModelSchema -from ....messaging.valid import UUID4_EXAMPLE, UUID4_VALIDATE, StrOrDictField -from .linked_data_proof import LinkedDataProofSchema +from ....messaging.valid import ( + CREDENTIAL_CONTEXT_VALIDATE, + PRESENTATION_TYPE_EXAMPLE, + PRESENTATION_TYPE_VALIDATE, + DIDKey, + StrOrDictField, + Uri, + UriOrDictField, +) +from ...ld_proofs.constants import ( + CREDENTIALS_CONTEXT_V1_URL, + VERIFIABLE_PRESENTATION_TYPE, +) +from .linked_data_proof import LDProof, LinkedDataProofSchema class VerifiablePresentation(BaseModel): @@ -14,52 +26,275 @@ class VerifiablePresentation(BaseModel): class Meta: """VerifiablePresentation metadata.""" - schema_class = "VerifiablePresentationSchema" - unknown = INCLUDE + schema_class = "PresentationSchema" def __init__( self, - *, + context: Optional[List[Union[str, dict]]] = None, id: Optional[str] = None, - contexts: Optional[Sequence[Union[str, dict]]] = None, - types: Optional[Sequence[str]] = None, - credentials: Optional[Sequence[dict]] = None, - proof: Optional[Sequence[dict]] = None, + type: Optional[List[str]] = None, + holder: Optional[Union[dict, str]] = None, + verifiable_credential: Optional[List[dict]] = None, + proof: Optional[Union[dict, LDProof]] = None, **kwargs, - ): + ) -> None: """Initialize VerifiablePresentation.""" - super().__init__() - self.id = id - self.contexts = contexts - self.types = types - self.credentials = credentials - self.proof = proof + self._context = context or [CREDENTIALS_CONTEXT_V1_URL] + self._id = id + self._holder = holder + self._type = type or [VERIFIABLE_PRESENTATION_TYPE] + self._verifiable_credential = verifiable_credential + self._proof = proof -class VerifiablePresentationSchema(BaseModelSchema): - """Single Verifiable Presentation Schema.""" + self.extra = kwargs + + @property + def context(self): + """Getter for context.""" + return self._context + + @context.setter + def context(self, context: List[Union[str, dict]]): + """Setter for context. + + First item must be credentials v1 url + """ + assert context[0] == CREDENTIALS_CONTEXT_V1_URL + + self._context = context + + def add_context(self, context: Union[str, dict]): + """Add a context to this presentation.""" + self._context.append(context) + + @property + def context_urls(self) -> List[str]: + """Getter for context urls.""" + return [context for context in self.context if isinstance(context, str)] + + @property + def type(self) -> List[str]: + """Getter for type.""" + return self._type + + @type.setter + def type(self, type: List[str]): + """Setter for type. + + First item must be VerifiablePresentation + """ + assert VERIFIABLE_PRESENTATION_TYPE in type + + self._type = type + + def add_type(self, type: str): + """Add a type to this presentation.""" + self._type.append(type) + + @property + def id(self): + """Getter for id.""" + return self._id + + @id.setter + def id(self, id: Union[str, None]): + """Setter for id.""" + if id: + uri_validator = Uri() + uri_validator(id) + + self._id = id + + @property + def holder_id(self) -> Optional[str]: + """Getter for holder id.""" + if not self._holder: + return None + elif isinstance(self._holder, str): + return self._holder + + return self._holder.get("id") + + @holder_id.setter + def holder_id(self, holder_id: str): + """Setter for holder id.""" + uri_validator = Uri() + uri_validator(holder_id) + + # Use simple string variant if possible + if not self._holder or isinstance(self._holder, str): + self._holder = holder_id + else: + self._holder["id"] = holder_id + + @property + def holder(self): + """Getter for holder.""" + return self._holder + + @holder.setter + def holder(self, holder: Union[str, dict]): + """Setter for holder.""" + uri_validator = Uri() + + holder_id = holder if isinstance(holder, str) else holder.get("id") + + if not holder_id: + raise ValidationError("Holder id is required") + uri_validator(holder_id) + + self._holder = holder + + @property + def verifiable_credential(self): + """Getter for verifiable credential.""" + return self._verifiable_credential + + @verifiable_credential.setter + def verifiable_credential(self, verifiable_credential: List[dict]): + """Setter for verifiable credential.""" + + self._verifiable_credential = verifiable_credential + + @property + def proof(self): + """Getter for proof.""" + return self._proof + + @proof.setter + def proof(self, proof: LDProof): + """Setter for proof.""" + self._proof = proof + + def __eq__(self, o: object) -> bool: + """Check equalness.""" + if isinstance(o, VerifiablePresentation): + return ( + self.context == o.context + and self.id == o.id + and self.type == o.type + and self.holder == o.holder + and self.verifiable_credentials == o.verifiable_credentials + and self.proof == o.proof + and self.extra == o.extra + ) + + return False + + +class PresentationSchema(BaseModelSchema): + """Linked data presentation schema. + + Based on https://www.w3.org/TR/vc-data-model + + """ class Meta: - """VerifiablePresentationSchema metadata.""" + """Accept parameter overload.""" - model_class = VerifiablePresentation unknown = INCLUDE + model_class = VerifiablePresentation + + context = fields.List( + UriOrDictField(required=True), + data_key="@context", + required=True, + validate=CREDENTIAL_CONTEXT_VALIDATE, + metadata={ + "description": "The JSON-LD context of the presentation", + "example": [CREDENTIALS_CONTEXT_V1_URL], + }, + ) id = fields.Str( required=False, - validate=UUID4_VALIDATE, - metadata={"description": "ID", "example": UUID4_EXAMPLE}, + validate=Uri(), + metadata={ + "desscription": "The ID of the presentation", + "example": "http://example.edu/presentations/1872", + }, ) - contexts = fields.List(StrOrDictField(), data_key="@context") - types = fields.List( - fields.Str(required=False, metadata={"description": "Types"}), data_key="type" + + type = fields.List( + fields.Str(required=True), + required=True, + validate=PRESENTATION_TYPE_VALIDATE, + metadata={ + "description": "The JSON-LD type of the presentation", + "example": PRESENTATION_TYPE_EXAMPLE, + }, + ) + + holder = StrOrDictField( + required=False, + metadata={ + "description": ( + "The JSON-LD Verifiable Credential Holder. Either string of object with" + " id field." + ), + "example": DIDKey.EXAMPLE, + }, ) - credentials = fields.List( - fields.Dict(required=False, metadata={"description": "Credentials"}), + + # TODO how to validate VCs in list + verifiable_credential = fields.List( + fields.Dict(required=True), + required=False, data_key="verifiableCredential", + metadata={}, + ) + + proof = fields.Nested( + LinkedDataProofSchema(), + required=False, + metadata={ + "description": "The proof of the presentation", + "example": { + "type": "Ed25519Signature2018", + "verificationMethod": ( + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38Ee" + "fXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + ), + "created": "2019-12-11T03:50:55", + "proofPurpose": "assertionMethod", + "jws": ( + "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0JiNjQiXX0..lKJU0Df_k" + "eblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY5qBQ" + ), + }, + }, ) + + @post_dump(pass_original=True) + def add_unknown_properties(self, data: dict, original, **kwargs): + """Add back unknown properties before outputting.""" + + data.update(original.extra) + + return data + + +class VerifiablePresentationSchema(PresentationSchema): + """Single Verifiable Presentation Schema.""" + proof = fields.Nested( LinkedDataProofSchema(), required=True, - metadata={"description": "The proof of the credential"}, + metadata={ + "description": "The proof of the presentation", + "example": { + "type": "Ed25519Signature2018", + "verificationMethod": ( + "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38Ee" + "fXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" + ), + "created": "2019-12-11T03:50:55", + "proofPurpose": "assertionMethod", + "jws": ( + "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0JiNjQiXX0..lKJU0Df_k" + "eblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY5qBQ" + ), + }, + }, ) diff --git a/aries_cloudagent/vc/vc_ld/models/web_schemas.py b/aries_cloudagent/vc/vc_ld/models/web_schemas.py new file mode 100644 index 0000000000..3349ad2c4e --- /dev/null +++ b/aries_cloudagent/vc/vc_ld/models/web_schemas.py @@ -0,0 +1,100 @@ +"""VC-API routes web requests schemas.""" + +from marshmallow import fields, Schema +from ....messaging.models.openapi import OpenAPISchema + +from ....messaging.valid import ( + RFC3339_DATETIME_EXAMPLE, + UUID4_EXAMPLE, +) +from ..validation_result import ( + PresentationVerificationResultSchema, +) +from .options import LDProofVCOptionsSchema +from .credential import ( + CredentialSchema, + VerifiableCredentialSchema, +) +from .presentation import ( + PresentationSchema, + VerifiablePresentationSchema, +) + + +class IssuanceOptionsSchema(Schema): + """Linked data proof verifiable credential options schema.""" + + type = fields.Str(required=False, metadata={"example": "Ed25519Signature2020"}) + created = fields.Str(required=False, metadata={"example": RFC3339_DATETIME_EXAMPLE}) + domain = fields.Str(required=False, metadata={"example": "website.example"}) + challenge = fields.Str(required=False, metadata={"example": UUID4_EXAMPLE}) + # TODO, implement status list publication through a plugin + # credential_status = fields.Dict( + # data_key="credentialStatus", + # required=False, + # metadata={"example": {"type": "StatusList2021"}}, + # ) + + +class ListCredentialsResponse(OpenAPISchema): + """Response schema for listing credentials.""" + + results = [fields.Nested(VerifiableCredentialSchema)] + + +class FetchCredentialResponse(OpenAPISchema): + """Response schema for fetching a credential.""" + + results = fields.Nested(VerifiableCredentialSchema) + + +class IssueCredentialRequest(OpenAPISchema): + """Request schema for issuing a credential.""" + + credential = fields.Nested(CredentialSchema) + options = fields.Nested(IssuanceOptionsSchema) + + +class IssueCredentialResponse(OpenAPISchema): + """Request schema for issuing a credential.""" + + verifiableCredential = fields.Nested(VerifiableCredentialSchema) + + +class VerifyCredentialRequest(OpenAPISchema): + """Request schema for verifying a credential.""" + + verifiableCredential = fields.Nested(VerifiableCredentialSchema) + options = fields.Nested(LDProofVCOptionsSchema) + + +class VerifyCredentialResponse(OpenAPISchema): + """Request schema for verifying an LDP VP.""" + + results = fields.Nested(PresentationVerificationResultSchema) + + +class ProvePresentationRequest(OpenAPISchema): + """Request schema for proving a presentation.""" + + presentation = fields.Nested(PresentationSchema) + options = fields.Nested(IssuanceOptionsSchema) + + +class ProvePresentationResponse(OpenAPISchema): + """Request schema for proving a presentation.""" + + verifiablePresentation = fields.Nested(VerifiablePresentationSchema) + + +class VerifyPresentationRequest(OpenAPISchema): + """Request schema for verifying a credential.""" + + verifiablePresentation = fields.Nested(VerifiablePresentationSchema) + options = fields.Nested(LDProofVCOptionsSchema) + + +class VerifyPresentationResponse(OpenAPISchema): + """Request schema for verifying an LDP VP.""" + + results = fields.Nested(PresentationVerificationResultSchema) diff --git a/aries_cloudagent/vc/vc_ld/tests/test_manager.py b/aries_cloudagent/vc/vc_ld/tests/test_manager.py index 9ac728b5a8..7fed188ba8 100644 --- a/aries_cloudagent/vc/vc_ld/tests/test_manager.py +++ b/aries_cloudagent/vc/vc_ld/tests/test_manager.py @@ -10,6 +10,7 @@ from ....resolver.default.key import KeyDIDResolver from ....resolver.did_resolver import DIDResolver from ....wallet.base import BaseWallet +from ....storage.vc_holder.base import VCHolder from ....wallet.default_verification_key_strategy import ( BaseVerificationKeyStrategy, DefaultVerificationKeyStrategy, @@ -38,6 +39,7 @@ TEST_DID_SOV = "did:sov:LjgpST2rjsoxYegQDRm7EL" TEST_DID_KEY = "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL" +TEST_UUID = "urn:uuid:dc86e95c-dc85-4f91-b563-82657d095c44" VC = { "credential": { "@context": [ @@ -172,7 +174,7 @@ async def test_get_did_info_for_did_sov( @pytest.mark.asyncio -async def test_get_suite_for_credential(manager: VcLdpManager): +async def test_get_suite_for_document(manager: VcLdpManager): vc = VerifiableCredential.deserialize(VC["credential"]) options = LDProofVCOptions.deserialize(VC["options"]) @@ -185,7 +187,7 @@ async def test_get_suite_for_credential(manager: VcLdpManager): "_did_info_for_did", mock.CoroutineMock(), ) as mock_did_info: - suite = await manager._get_suite_for_credential(vc, options) + suite = await manager._get_suite_for_document(vc, options) assert suite.signature_type == options.proof_type assert isinstance(suite, Ed25519Signature2018) @@ -367,3 +369,26 @@ async def test_get_all_suites(manager: VcLdpManager): ) for suite in suites: assert isinstance(suite, types) + + +@pytest.mark.asyncio +async def test_store( + profile: Profile, + manager: VcLdpManager, + vc: VerifiableCredential, + options: LDProofVCOptions, +): + async with profile.session() as session: + wallet = session.inject(BaseWallet) + did = await wallet.create_local_did( + method=KEY, + key_type=ED25519, + ) + vc.issuer = did.did + options.proof_type = Ed25519Signature2018.signature_type + cred = await manager.issue(vc, options) + await manager.store_credential(cred, options, TEST_UUID) + async with profile.session() as session: + holder = session.inject(VCHolder) + record = await holder.retrieve_credential_by_id(record_id=TEST_UUID) + assert record diff --git a/demo/AriesPostmanDemo.md b/demo/AriesPostmanDemo.md new file mode 100644 index 0000000000..46d296e9f8 --- /dev/null +++ b/demo/AriesPostmanDemo.md @@ -0,0 +1,96 @@ +# Aries Postman Demo + +In these demos we will use Postman as our controller client. + +# Contents + +- [Getting Started](#getting-started) + - [Installing Postman](#install-postman) + - [Creating a workspace](#create-workspace) + - [Importing the environment](#import-environment) + - [Importing the collections](#import-collections) + - [Postman basics](#postman-basics) +- [Experimenting with the vc-api endpoints](#vc-api) + - [Register new dids](#register-dids) + - [Issue credentials](#issue-credentials) + - [Store and retrieve credentials](#store-credentials) + - [Verify credentials](#verify-credentials) + - [Prove a presentation](#prove-presentation) + - [Verify a presentation](#verify-presentation) + +## Getting Started +Welcome to the Postman demo. This is an addition to the available OpenAPI demo, providing a set of collections to test and demonstrate various aca-py funcitonalities. + +### Installing Postman +Download, install and launch [postman](https://www.postman.com/downloads/). + +### Creating a workspace +Create a new postman workspace labeled "acapy-demo". + +### Importing the enviroment +In the environment tab from the left, click the import button. You can paste this [link](https://raw.githubusercontent.com/hyperledger/aries-cloudagent-python/main/demo/postman/environment.json) or copy the json from the [environment file](./postman/environment.json). + +Make sure you have the environment set as your active environment. + +### Importing the collections +In the collections tab from the left, click the import button. + +The following collections are available: +- [vc-api](https://raw.githubusercontent.com/hyperledger/aries-cloudagent-python/main/demo/postman/collections/vc-api.json) + +### Postman basics +Once you are setup, you will be ready to run postman requests. The order of the request is important, since some values are saved dynamically as environment variables for subsequent calls. + +You have your envrionment where you define variables to be accessed by your collections. + +Each collection consists of a series of requests which can be configured independently. + +## Experimenting with the vc-api endpoints + +Make sure you have a demo agent available. You can use the following command to deploy one: +```bash +LEDGER_URL=http://dev.greenlight.bcovrin.vonx.io ./run_demo faber --bg +``` + +When running for the first time, please allow some time for the images to build. + +### Register new dids +The first 2 requests for this collection will create 2 did:keys. We will use those in subsequent calls to issue `Ed25519Signature2020` and `BbsBlsSignature2020` credentials. +Run the 2 did creation requests. These requests will use the `/wallet/did/create` endpoint. + +### Issue credentials +For issuing, you must input a [w3c compliant json-ld credential](https://www.w3.org/TR/vc-data-model/) and [issuance options](https://w3c-ccg.github.io/vc-api/#issue-credential) in your request body. The issuer field must be a registered did from the agent's wallet. The suite will be derived from the did method. +```json +{ + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiableCredential" + ], + "issuer": "did:example:123", + "issuanceDate": "2022-05-01T00:00:00Z", + "credentialSubject": { + "id": "did:example:123" + } + }, + "options": {} +} +``` + +Some examples have been pre-configured in the collection. Run the requests and inspect the results. Experiment with different credentials. + +### Store and retrieve credentials +Your last issued credential will be stored as an environment variable for subsequent calls, such as storing, verifying and including in a presentation. + +Try running the store credential request, then retrieve the credential with the list and fetch requests. Try going back and forth between the issuance endpoints and the storage endpoints to store multiple different credentials. + +### Verify credentials +You can verify your last issued credential with this endpoint or any issued credential you provide to it. + +### Prove a presentation +Proving a presentation is an action where a holder will prove ownership of a credential by signing or demonstrating authority over the document. + +### Verify a presentation +The final request is to verify a presentation. diff --git a/demo/postman/collections/vc-api.json b/demo/postman/collections/vc-api.json new file mode 100644 index 0000000000..f3a49dc228 --- /dev/null +++ b/demo/postman/collections/vc-api.json @@ -0,0 +1,402 @@ +{ + "info": { + "_postman_id": "f6a6fe37-a18c-47ef-a235-424d30ab043d", + "name": "vc-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "31640062" + }, + "item": [ + { + "name": "Create DID Ed25519Signature2020", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const { result } = pm.response.json();\r", + "pm.globals.set(\"did_ed\", result.did);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"method\": \"key\",\r\n \"options\": {\r\n \"key_type\": \"ed25519\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/wallet/did/create", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "wallet", + "did", + "create" + ] + } + }, + "response": [] + }, + { + "name": "Create DID BbsBlsSignature2020", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const { result } = pm.response.json();\r", + "pm.globals.set(\"did_bls\", result.did);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"method\": \"key\",\r\n \"options\": {\r\n \"key_type\": \"bls12381g2\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/wallet/did/create", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "wallet", + "did", + "create" + ] + } + }, + "response": [] + }, + { + "name": "Issue VC Ed25519Signature2020", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const {verifiableCredential} = pm.response.json();\r", + "pm.collectionVariables.set(\"verifiableCredential\", JSON.stringify(verifiableCredential));\r", + "pm.collectionVariables.set(\"credential_id\", verifiableCredential.id);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"credential\": {\r\n \"@context\": [\r\n \"https://www.w3.org/2018/credentials/v1\"\r\n ],\r\n \"credentialSubject\": {\r\n \"id\": \"did:example:ebfeb1f712ebc6f1c276e12ec21\"\r\n },\r\n \"id\": \"urn:uuid:{{$randomUUID}}\",\r\n \"issuanceDate\": \"2010-01-01T19:23:24Z\",\r\n \"issuer\": \"{{did_ed}}\",\r\n \"type\": [\r\n \"VerifiableCredential\"\r\n ]\r\n },\r\n \"options\": {\r\n \"type\": \"Ed25519Signature2020\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/issue", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "issue" + ] + } + }, + "response": [] + }, + { + "name": "Issue VC BbsBlsSignature2020", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const {verifiableCredential} = pm.response.json();\r", + "pm.collectionVariables.set(\"verifiableCredential\", JSON.stringify(verifiableCredential));\r", + "pm.collectionVariables.set(\"credential_id\", verifiableCredential.id);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"credential\": {\r\n \"@context\": [\r\n \"https://www.w3.org/2018/credentials/v1\"\r\n ],\r\n \"credentialSubject\": {\r\n \"id\": \"did:example:ebfeb1f712ebc6f1c276e12ec21\"\r\n },\r\n \"id\": \"urn:uuid:{{$randomUUID}}\",\r\n \"issuanceDate\": \"2010-01-01T19:23:24Z\",\r\n \"issuer\": \"{{did_bls}}\",\r\n \"type\": [\r\n \"VerifiableCredential\"\r\n ]\r\n },\r\n \"options\": {\r\n \"type\": \"BbsBlsSignature2020\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/issue", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "issue" + ] + } + }, + "response": [] + }, + { + "name": "Verify Credential", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {},\r\n \"verifiableCredential\": {{verifiableCredential}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/verify", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "verify" + ] + } + }, + "response": [] + }, + { + "name": "Store Credential", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {},\r\n \"verifiableCredential\": {{verifiableCredential}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/store", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "store" + ] + } + }, + "response": [] + }, + { + "name": "List Credentials", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials" + ] + } + }, + "response": [] + }, + { + "name": "Fetch Credential", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/{{credential_id}}", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "{{credential_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Prove presentation", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const {verifiablePresentation} = pm.response.json();\r", + "pm.collectionVariables.set(\"verifiablePresentation\", JSON.stringify(verifiablePresentation));\r", + "pm.collectionVariables.set(\"challenge\", verifiablePresentation.proof.challenge);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"presentation\": {\r\n \"@context\": [\r\n \"https://www.w3.org/2018/credentials/v1\"\r\n ],\r\n \"type\": [\r\n \"VerifiablePresentation\"\r\n ],\r\n \"holder\": \"{{did_ed}}\",\r\n \"verifiableCredential\": [{{verifiableCredential}}]\r\n },\r\n \"options\": {\r\n \"type\": \"Ed25519Signature2018\",\r\n \"challenge\": \"{{$randomUUID}}\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/presentations/prove", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "presentations", + "prove" + ] + } + }, + "response": [] + }, + { + "name": "Verify Presentation", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {\r\n \"challenge\": \"{{challenge}}\"\r\n },\r\n \"verifiablePresentation\": {{verifiablePresentation}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/presentations/verify", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "presentations", + "verify" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "verifiableCredential", + "value": "" + }, + { + "key": "credential_id", + "value": "" + }, + { + "key": "verifiablePresentation", + "value": "" + }, + { + "key": "challenge", + "value": "" + } + ] +} \ No newline at end of file diff --git a/demo/postman/environment.json b/demo/postman/environment.json new file mode 100644 index 0000000000..3cf464c40e --- /dev/null +++ b/demo/postman/environment.json @@ -0,0 +1,15 @@ +{ + "id": "af2c8f79-d6b5-4009-b9b9-20781d0849ff", + "name": "aca-py", + "values": [ + { + "key": "ADMIN_ENDPOINT", + "value": "http://localhost:8021", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2024-01-24T20:22:38.205Z", + "_postman_exported_using": "Postman/10.22.1" +} \ No newline at end of file diff --git a/demo/postman/vc-api.collection.json b/demo/postman/vc-api.collection.json new file mode 100644 index 0000000000..060454052e --- /dev/null +++ b/demo/postman/vc-api.collection.json @@ -0,0 +1,373 @@ +{ + "info": { + "_postman_id": "f6a6fe37-a18c-47ef-a235-424d30ab043d", + "name": "vc-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "31640062" + }, + "item": [ + { + "name": "Create DID Ed25519Signature2020", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const { result } = pm.response.json();\r", + "pm.globals.set(\"did\", result.did);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"method\": \"key\",\r\n \"options\": {\r\n \"key_type\": \"ed25519\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/wallet/did/create", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "wallet", + "did", + "create" + ] + } + }, + "response": [] + }, + { + "name": "Create DID BbsBlsSignature2020", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const { result } = pm.response.json();\r", + "pm.globals.set(\"did\", result.did);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"method\": \"key\",\r\n \"options\": {\r\n \"key_type\": \"bls12381g2\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/wallet/did/create", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "wallet", + "did", + "create" + ] + } + }, + "response": [] + }, + { + "name": "Issue Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const {verifiableCredential} = pm.response.json();\r", + "pm.collectionVariables.set(\"verifiableCredential\", JSON.stringify(verifiableCredential));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"credential\": {\r\n \"@context\": [\r\n \"https://www.w3.org/2018/credentials/v1\"\r\n ],\r\n \"credentialSubject\": {\r\n \"id\": \"did:example:ebfeb1f712ebc6f1c276e12ec21\"\r\n },\r\n \"id\": \"urn:uuid:{{$randomUUID}}\",\r\n \"issuanceDate\": \"2010-01-01T19:23:24Z\",\r\n \"issuer\": \"{{did}}\",\r\n \"type\": [\r\n \"VerifiableCredential\"\r\n ]\r\n },\r\n \"options\": {}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/issue", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "issue" + ] + } + }, + "response": [] + }, + { + "name": "Verify Credential", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {},\r\n \"verifiableCredential\": {{verifiableCredential}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/verify", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "verify" + ] + } + }, + "response": [] + }, + { + "name": "Store Credential", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const {credentialId} = pm.response.json();\r", + "pm.collectionVariables.set(\"credentialId\", credentialId);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {},\r\n \"verifiableCredential\": {{verifiableCredential}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/store", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "store" + ] + } + }, + "response": [] + }, + { + "name": "List Credentials", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials" + ] + } + }, + "response": [] + }, + { + "name": "Fetch Credential", + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/credentials/{{credentialId}}", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "credentials", + "{{credentialId}}" + ] + } + }, + "response": [] + }, + { + "name": "Prove presentation", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const {verifiablePresentation} = pm.response.json();\r", + "pm.collectionVariables.set(\"verifiablePresentation\", JSON.stringify(verifiablePresentation));\r", + "pm.collectionVariables.set(\"challenge\", verifiablePresentation.proof.challenge);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"presentation\": {\r\n \"@context\": [\r\n \"https://www.w3.org/2018/credentials/v1\"\r\n ],\r\n \"type\": [\r\n \"VerifiablePresentation\"\r\n ],\r\n \"holder\": \"{{did}}\",\r\n \"verifiableCredential\": [{{verifiableCredential}}]\r\n },\r\n \"options\": {\r\n \"domain\": \"https://example.com\",\r\n \"challenge\": \"{{$randomUUID}}\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/presentations/prove", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "presentations", + "prove" + ] + } + }, + "response": [] + }, + { + "name": "Verify Presentation", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"options\": {\r\n \"domain\": \"example.com\",\r\n \"challenge\": \"{{challenge}}\"\r\n },\r\n \"verifiablePresentation\": {{verifiablePresentation}}\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ADMIN_ENDPOINT}}/vc/presentations/verify", + "host": [ + "{{ADMIN_ENDPOINT}}" + ], + "path": [ + "vc", + "presentations", + "verify" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "verifiableCredential", + "value": "" + }, + { + "key": "credential_id", + "value": "" + }, + { + "key": "verifiablePresentation", + "value": "" + }, + { + "key": "challenge", + "value": "" + }, + { + "key": "credentialId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/demo/run_demo b/demo/run_demo index b451cb72fe..45fc7d813d 100755 --- a/demo/run_demo +++ b/demo/run_demo @@ -156,8 +156,8 @@ if [ ! -z "$AGENT_PORT_OVERRIDE" ]; then fi echo "Preparing agent image..." -docker build -q -t acapy-base -f ../docker/Dockerfile .. || exit 1 -docker build -q -t faber-alice-demo -f ../docker/Dockerfile.demo --build-arg from_image=acapy-base .. || exit 1 +docker build -t acapy-base -f ../docker/Dockerfile .. || exit 1 +docker build -t faber-alice-demo -f ../docker/Dockerfile.demo --build-arg from_image=acapy-base .. || exit 1 if [ ! -z "$DOCKERHOST" ]; then # provided via APPLICATION_URL environment variable diff --git a/open-api/openapi.json b/open-api/openapi.json index ca52e07cf6..5840bb0df0 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -96,6 +96,13 @@ "url" : "https://www.w3.org/TR/vc-data-model/" }, "name" : "ldp-vc" + }, { + "description" : "Manage credentials and presentations", + "externalDocs" : { + "description" : "Specification", + "url" : "https://w3c-ccg.github.io/vc-api" + }, + "name" : "vc-api" }, { "description" : "Interaction with ledger", "externalDocs" : { diff --git a/open-api/swagger.json b/open-api/swagger.json index ed3cd58693..06fe49b5ec 100644 --- a/open-api/swagger.json +++ b/open-api/swagger.json @@ -90,6 +90,13 @@ "description" : "Specification", "url" : "https://www.w3.org/TR/vc-data-model/" } + }, { + "description" : "Manage credentials and presentations", + "externalDocs" : { + "description" : "Specification", + "url" : "https://w3c-ccg.github.io/vc-api" + }, + "name" : "vc-api" }, { "name" : "ledger", "description" : "Interaction with ledger",