diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py index b72c96a289..c1886eaf83 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/ld_proof/handler.py @@ -353,7 +353,9 @@ def _get_proof_purpose( f"Supported proof types are: {SUPPORTED_ISSUANCE_PROOF_PURPOSES}" ) - async def _prepare_detail(self, detail: LDProofVCDetail) -> LDProofVCDetail: + async def _prepare_detail( + self, detail: LDProofVCDetail, holder_did: str = None + ) -> LDProofVCDetail: # Add BBS context if not present yet if ( detail.options.proof_type == BbsBlsSignature2020.signature_type @@ -361,6 +363,10 @@ async def _prepare_detail(self, detail: LDProofVCDetail) -> LDProofVCDetail: ): detail.credential.add_context(SECURITY_CONTEXT_BBS_URL) + # add holder_did as credentialSubject.id (if provided) + if holder_did and holder_did.startswith("did:key"): + detail.credential.credential_subject["id"] = holder_did + return detail async def create_proposal( @@ -410,6 +416,8 @@ async def create_request( self, cred_ex_record: V20CredExRecord, request_data: Mapping = None ) -> CredFormatAttachment: """Create linked data proof credential request.""" + holder_did = request_data.get("holder_did") if request_data else None + if cred_ex_record.cred_offer: request_data = cred_ex_record.cred_offer.attachment( LDProofCredFormatHandler.format @@ -426,7 +434,7 @@ async def create_request( ) detail = LDProofVCDetail.deserialize(request_data) - detail = await self._prepare_detail(detail) + detail = await self._prepare_detail(detail, holder_did=holder_did) return self.get_format_data(CRED_20_REQUEST, detail.serialize()) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py index a93b5fc0d2..05c92a3d93 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py @@ -257,6 +257,12 @@ class V20CredRequestFreeSchema(AdminAPIMessageTracingSchema): required=False, example=False, ) + holder_did = fields.Str( + description="Holder DID to substitute for the credentialSubject.id", + required=False, + allow_none=True, + example="did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", + ) class V20CredSendRequestSchema(V20IssueCredSchemaCore): @@ -326,6 +332,17 @@ class V20CreateFreeOfferResultSchema(OpenAPISchema): ) +class V20CredRequestRequestSchema(OpenAPISchema): + """Request schema for sending credential request message.""" + + holder_did = fields.Str( + description="Holder DID to substitute for the credentialSubject.id", + required=False, + allow_none=True, + example="did:key:ahsdkjahsdkjhaskjdhakjshdkajhsdkjahs", + ) + + class V20CredIssueRequestSchema(OpenAPISchema): """Request schema for sending credential issue admin message.""" @@ -1126,6 +1143,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): raise web.HTTPBadRequest(reason="Missing filter") auto_remove = body.get("auto_remove") trace_msg = body.get("trace") + holder_did = body.get("holder_did") conn_record = None cred_ex_record = None @@ -1158,7 +1176,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): cred_ex_record, cred_request_message = await cred_manager.create_request( cred_ex_record=cred_ex_record, - holder_did=conn_record.my_did, + holder_did=holder_did, comment=comment, ) @@ -1199,6 +1217,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): summary="Send issuer a credential request", ) @match_info_schema(V20CredExIdMatchInfoSchema()) +@request_schema(V20CredRequestRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") async def credential_exchange_send_bound_request(request: web.BaseRequest): """ @@ -1216,6 +1235,12 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): context: AdminRequestContext = request["context"] outbound_handler = request["outbound_message_router"] + try: + body = await request.json() or {} + holder_did = body.get("holder_did") + except JSONDecodeError: + holder_did = None + cred_ex_id = request.match_info["cred_ex_id"] cred_ex_record = None @@ -1238,7 +1263,7 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): cred_manager = V20CredManager(context.profile) cred_ex_record, cred_request_message = await cred_manager.create_request( cred_ex_record, - conn_record.my_did, + holder_did if holder_did else conn_record.my_did, ) result = cred_ex_record.serialize() diff --git a/demo/AliceGetsAPhone.md b/demo/AliceGetsAPhone.md index 25f904c72f..1f4fa15108 100644 --- a/demo/AliceGetsAPhone.md +++ b/demo/AliceGetsAPhone.md @@ -133,9 +133,11 @@ Note that with _Play with Docker_ it can be challenging to capture the informati If you are running in a _local bash shell_, navigate to [The demo directory](/demo) and run: ```bash -TAILS_NETWORK=docker_tails-server LEDGER_URL=http://test.bcovrin.vonx.io ./run_demo faber --revocation --events +TAILS_NETWORK=docker_tails-server LEDGER_URL=http://test.bcovrin.vonx.io ./run_demo faber --aip 10 --revocation --events ``` +(Note that we have to start faber with `--aip 10` for compatibility with mobile clients.) + The `TAILS_NETWORK` parameter lets the demo script know how to connect to the tails server (which should be running in a separate shell on the same machine). #### Running in Play with Docker? @@ -143,7 +145,7 @@ The `TAILS_NETWORK` parameter lets the demo script know how to connect to the ta If you are running in _Play with Docker_, navigate to [The demo directory](/demo) and run: ```bash -PUBLIC_TAILS_URL=https://c4f7fbb85911.ngrok.io LEDGER_URL=http://test.bcovrin.vonx.io ./run_demo faber --revocation --events +PUBLIC_TAILS_URL=https://c4f7fbb85911.ngrok.io LEDGER_URL=http://test.bcovrin.vonx.io ./run_demo faber --aip 10 --revocation --events ``` The `PUBLIC_TAILS_URL` parameter lets the demo script know how to connect to the tails server. This can be running in another PWD session, or even on your local machine - the ngrok endpoint is public and will map to the correct location. diff --git a/demo/AliceWantsAJsonCredential.md b/demo/AliceWantsAJsonCredential.md new file mode 100644 index 0000000000..e38e4b5853 --- /dev/null +++ b/demo/AliceWantsAJsonCredential.md @@ -0,0 +1,560 @@ + +# How to Issue JSON-LD Credentials using Aca-py + +Aca-py has the capability to issue and verify both Indy and JSON-LD (W3C compliant) credentials. + +The JSON-LD support is documented [here](../JsonLdCredentials.md) - this document will provide some additional detail in how to use the demo and admin api to issue and prove JSON-LD credentials. + + +## Setup Agents to Issue JDON-LD Credentials + +Clone this repository to a directory on your local: + +```bash +git clone https://github.com/hyperledger/aries-cloudagent-python.git +cd aries-cloudagent-python/demo +``` + +Open up a second shell (so you have 2 shells open in the `demo` directory) and in one shell: + +```bash +./run_demo faber --did-exchange --aip 20 --cred-type json-ld +``` + +... and in the other: + +```bash +./run_demo alice +``` + +Note that you start the `faber` agent with AIP2.0 options. (When you specify `--cred-type json-ld` faber will set aip to `20` automatically, so the `--aip` option is not strictly required.) + +(Alternately you can run run Alice and Faber agents locally, see the `./faber-local.sh` and `./alice-local.sh` scripts in the `demo` directory.) + +Copy the "invitation" json text from the Faber shell and paste into the Alice shell to establish a connection between the two agents. + +Now open up two browser windows to the [Faber](http://localhost:8021/api/doc) and [Alice](http://localhost:8031/api/doc) admin api swagger pages. + +Using the Faber admin api, you have to create a DID with the appropriate: + +- DID method ("key" or "sov") +- key type "ed25519" or "bls12381g2" (corresponding to signature types "Ed25519Signature2018" or "BbsBlsSignature2020") +- if you use DID method "sov" you must use key type "ed25519" + +Note that "did:sov" must be a public DID (i.e. registered on the ledger) but "did:key" is not. + +For example, in Faber's swagger page call the `/wallet/did/create` endpoint with the following payload: + +``` +{ + "method": "key", + "options": { + "key_type": "bls12381g2" // or ed25519 + } +} +``` + +This will return something like: + +``` +{ + "result": { + "did": "did:key:zUC71KdwBhq1FioWh53VXmyFiGpewNcg8Ld42WrSChpMzzskRWwHZfG9TJ7hPj8wzmKNrek3rW4ZkXNiHAjVchSmTr9aNUQaArK3KSkTySzjEM73FuDV62bjdAHF7EMnZ27poCE", + "verkey": "mV6482Amu6wJH8NeMqH3QyTjh6JU6N58A8GcirMZG7Wx1uyerzrzerA2EjnhUTmjiSLAp6CkNdpkLJ1NTS73dtcra8WUDDBZ3o455EMrkPyAtzst16RdTMsGe3ctyTxxJav", + "posture": "wallet_only", + "key_type": "bls12381g2", + "method": "key" + } +} +``` + +You do *not* create a schema or cred def for a JSON-LD credential (these are only required for "indy" credentials). + +You will need to create a DID as above for Alice as well (`/wallet/did/create` etc ...). + +Congradulations, you are now ready to start issuing JSON-LD credentials! + +- You have two agents with a connection established between the agents - you will need to copy Faber's `connection_id` into the examples below. +- You have created a (non-public) DID for Faber to use to sign/issue the credentials - you will need to copy the DID that you created above into the examples below (as `issuer`). +- You have created a (non-public) DID for Alice to use as her `credentialSubject.id` - this is required for Alice to sign the proof (the `credentialSubject.id` is not required, but then the provided presentation can't be verified). + +To issue a credential, use the `/issue-credential-2.0/send` (or `/issue-credential-2.0/create-offer`) endpoint, you can test with this example payload (just replace the "connection_id", "issuer" key, "credentialSubject.id" and "proofType" with appropriate values: + +``` +{ + "connection_id": "4fba2ce5-b411-4ecf-aa1b-ec66f3f6c903", + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:key:zUC71KdwBhq1FioWh53VXmyFiGpewNcg8Ld42WrSChpMzzskRWwHZfG9TJ7hPj8wzmKNrek3rW4ZkXNiHAjVchSmTr9aNUQaArK3KSkTySzjEM73FuDV62bjdAHF7EMnZ27poCE", + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "id": "did:key:aksdkajshdkajhsdkjahsdkjahsdj", + "givenName": "Sally", + "familyName": "Student", + "degree": { + "type": "BachelorDegree", + "degreeType": "Undergraduate", + "name": "Bachelor of Science and Arts" + }, + "college": "Faber College" + } + }, + "options": { + "proofType": "BbsBlsSignature2020" + } + } + } +} +``` + +Note that if you have the "auto" settings on, this is all you need to do. Otherwise you need to call the `/send-request`, `/store`, etc endpoints to complete the protocol. + +To see the issued credential, call the `/credentials/w3c` endpoint on Alice's admin api - this will return something like: + +``` +{ + "results": [ + { + "contexts": [ + "https://w3id.org/security/bbs/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://www.w3.org/2018/credentials/v1" + ], + "types": [ + "UniversityDegreeCredential", + "VerifiableCredential" + ], + "schema_ids": [], + "issuer_id": "did:key:zUC71KdwBhq1FioWh53VXmyFiGpewNcg8Ld42WrSChpMzzskRWwHZfG9TJ7hPj8wzmKNrek3rW4ZkXNiHAjVchSmTr9aNUQaArK3KSkTySzjEM73FuDV62bjdAHF7EMnZ27poCE", + "subject_ids": [], + "proof_types": [ + "BbsBlsSignature2020" + ], + "cred_value": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://w3id.org/security/bbs/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "issuer": "did:key:zUC71Kd...poCE", + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "id": "did:key:aksdkajshdkajhsdkjahsdkjahsdj", + "givenName": "Sally", + "familyName": "Student", + "degree": { + "type": "BachelorDegree", + "degreeType": "Undergraduate", + "name": "Bachelor of Science and Arts" + }, + "college": "Faber College" + }, + "proof": { + "type": "BbsBlsSignature2020", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:key:zUC71Kd...poCE#zUC71Kd...poCE", + "created": "2021-05-19T16:19:44.458170", + "proofValue": "g0weLyw2Q+niQ4pGfiXB...tL9C9ORhy9Q==" + } + }, + "cred_tags": {}, + "record_id": "365ab87b12f74b2db784fdd4db8419f5" + } + ] +} +``` + + +## Building More Realistic JSON-LD Credentials + +The above example uses the "https://www.w3.org/2018/credentials/examples/v1" context, which should never be used in a real application. + +To build credentials in real life, you first determine which attributes you need and then include the appropriate contexts. + + +### Context schema.org + +You can use attributes defined on [schema.org](https://schema.org). Although this is *NOT RECOMMENDED* (included here for illustrative purposes only) - individual attributes can't be validated (see the comment later on). + +You first include `https://schema.org` in the `@context` block of the credential as follows: + +``` +"@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" +], +``` + +Then you review the [attributes and objects defined by `https://schema.org`](https://schema.org/docs/schemas.html) and decide what you need to include in your credential. + +For example to issue a credetial with [givenName](https://schema.org/givenName), [familyName](https://schema.org/familyName) and [alumniOf](https://schema.org/alumniOf) attributes, submit the following: + +``` +{ + "connection_id": "ad35a4d8-c84b-4a4f-a83f-1afbf134b8b9", + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential", "Person"], + "issuer": "did:key:zUC71pj2gpDLfcZ9DE1bMtjZGWCSLhkQsUCaKjqXtCftGkz27894pEX9VvGNiFsaV67gqv2TEPQ2aDaDDdTDNp42LfDdK1LaWSBCfzsQEyaiR1zjZm1RtoRu1ZM6v6vz4TiqDgU", + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "id": "did:key:aksdkajshdkajhsdkjahsdkjahsdj", + "givenName": "Sally", + "familyName": "Student", + "alumniOf": "Example University" + } + }, + "options": { + "proofType": "BbsBlsSignature2020" + } + } + } +} +``` + +Note that with `https://schema.org`, if you include attributes that aren't defined by *any* context, you will *not* get an error. For example you can try replacing the `credentialSubject` in the above with: + +``` +"credentialSubject": { + "id": "did:key:aksdkajshdkajhsdkjahsdkjahsdj", + "givenName": "Sally", + "familyName": "Student", + "alumniOf": "Example University", + "someUndefinedAttribute": "the value of the attribute" +} +``` + +... and the credential issuance *should* fail, however `https://schema.org` defines a `@vocab` that by default all terms derive from ([see here](https://stackoverflow.com/questions/30945898/what-is-the-use-of-vocab-in-json-ld-and-what-is-the-difference-to-context/30948037#30948037)). + +You can include more complex schemas, for example to use the schema.org [Person](https://schema.org/Person) schema (which includes `givenName` and `familyName`): + +``` +{ + "connection_id": "ad35a4d8-c84b-4a4f-a83f-1afbf134b8b9", + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential", "Person"], + "issuer": "did:key:zUC71pj2gpDLfcZ9DE1bMtjZGWCSLhkQsUCaKjqXtCftGkz27894pEX9VvGNiFsaV67gqv2TEPQ2aDaDDdTDNp42LfDdK1LaWSBCfzsQEyaiR1zjZm1RtoRu1ZM6v6vz4TiqDgU", + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "id": "did:key:aksdkajshdkajhsdkjahsdkjahsdj", + "student": { + "type": "Person", + "givenName": "Sally", + "familyName": "Student", + "alumniOf": "Example University" + } + } + }, + "options": { + "proofType": "BbsBlsSignature2020" + } + } + } +} +``` + + +## Credential-Specific Contexts + +The recommended approach to defining credentials is to define a credential-specific vocaublary (or make use of existing ones). (Note that these can include references to `https://schema.org`, you just shouldn't uste this directly in your credential.) + + +### Credential Issue Example + +The following example uses the W3C citizenship context to issue a PermanentResident credential (replace the `connection_id`, `issuer` and `credentialSubject.id` with your local values): + +``` +{ + "connection_id": "41acd909-9f45-4c69-8641-8146e0444a57", + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1" + ], + "type": [ + "VerifiableCredential", + "PermanentResident" + ], + "id": "https://credential.example.com/residents/1234567890", + "issuer": "did:key:zUC7Dus47jW5Avcne8LLsUvJSdwspmErgehxMWqZZy8eSSNoHZ4x8wgs77sAmQtCADED5RQP1WWhvt7KFNm6GGMxdSGpKu3PX6R9a61G9VoVsiFoRf1yoK6pzhq9jtFP3e2SmU9", + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "type": [ + "PermanentResident" + ], + "id": "did:key:zUC7CXi82AXbkv4SvhxDxoufrLwQSAo79qbKiw7omCQ3c4TyciDdb9s3GTCbMvsDruSLZX6HNsjGxAr2SMLCNCCBRN5scukiZ4JV9FDPg5gccdqE9nfCU2zUcdyqRiUVnn9ZH83", + "givenName": "ALICE", + "familyName": "SMITH", + "gender": "Female", + "birthCountry": "Bahamas", + "birthDate": "1958-07-17" + } + }, + "options": { + "proofType": "BbsBlsSignature2020" + } + } + } +} +``` + +Copy and paste this content into Faber's `/issue-credential-2.0/send-offer` endpoint, and it will kick off the exchange process to issue a W3C credential to Alice. + +In Alice's swagger page, submit the `/credentials/records/w3c` endpoint to see the issued credential. + + +### Request Presentation Example + +To request a proof, submit the following (with appropriate `connection_id`) to Faber's `/request-presentation-2.0/request-proof` endpoint: + +``` +{ + "comment": "string", + "connection_id": "41acd909-9f45-4c69-8641-8146e0444a57", + "presentation_request": { + "dif": { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47" + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "format": { + "ldp_vp": { + "proof_type": [ + "BbsBlsSignature2020" + ] + } + }, + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResident" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.familyName" + ], + "purpose": "The claim must be from one of the specified issuers", + "filter": { + "const": "SMITH" + } + }, + { + "path": [ + "$.credentialSubject.givenName" + ], + "purpose": "The claim must be from one of the specified issuers" + } + ] + } + } + ] + } + } + } +} +``` + +There are several ways that Alice can prespond with a presentation. The simplest will just tell aca-py to put the presentation together and send it to Faber - submit the following to Alice's `/request-presentation-2.0/{pres_ex_id}/send-presentation`: + +``` +{ + "dif": { + } +} +``` + +There are two ways that Alice can provide some constraints to tell aca-py which credential(s) to include in the presentation. + +Firstly, Alice can include the received presentation request in the body to the `/send-presentation` endpoint, and can include additional constraints on the fields: + +``` +{ + "dif": { + "issuer_id": "did:key:zUC7Dus47jW5Avcne8LLsUvJSdwspmErgehxMWqZZy8eSSNoHZ4x8wgs77sAmQtCADED5RQP1WWhvt7KFNm6GGMxdSGpKu3PX6R9a61G9VoVsiFoRf1yoK6pzhq9jtFP3e2SmU9", + "presentation_definition": { + "format": { + "ldp_vp": { + "proof_type": [ + "BbsBlsSignature2020" + ] + } + }, + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "Some kind of citizenship check", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResident" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.familyName" + ], + "purpose": "The claim must be from one of the specified issuers", + "filter": { + "const": "SMITH" + } + }, + { + "path": [ + "$.id" + ], + "purpose": "Specify the id of the credential to present", + "filter": { + "const": "https://credential.example.com/residents/1234567890" + } + } + ] + } + } + ] + } + } +} +``` + +Note the additional constraint on `"path": [ "$.id" ]` - this restricts the presented credential to the one with the matching `credential.id`. Any credential attributes can be used, however this presumes that the issued credentials contain a uniquely identifying attribute. + +Another option is for Alice to specify the credential `record_id` - this is an internal value within aca-py: + +``` +{ + "dif": { + "issuer_id": "did:key:zUC7Dus47jW5Avcne8LLsUvJSdwspmErgehxMWqZZy8eSSNoHZ4x8wgs77sAmQtCADED5RQP1WWhvt7KFNm6GGMxdSGpKu3PX6R9a61G9VoVsiFoRf1yoK6pzhq9jtFP3e2SmU9", + "presentation_definition": { + "format": { + "ldp_vp": { + "proof_type": [ + "BbsBlsSignature2020" + ] + } + }, + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "Some kind of citizenship check", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResident" + } + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.familyName" + ], + "purpose": "The claim must be from one of the specified issuers", + "filter": { + "const": "SMITH" + } + } + ] + } + } + ] + }, + "record_ids": { + "citizenship_input_1": [ "1496316f972e40cf9b46b35971182337" ] + } + } +} +``` + +### Another Credential Issue Example + +TBD the following credential is based on the W3C Vaccination schema: + +``` +{ + "connection_id": "ad35a4d8-c84b-4a4f-a83f-1afbf134b8b9", + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vaccination/v1" + ], + "type": ["VerifiableCredential", "VaccinationCertificate"], + "issuer": "did:key:zUC71pj2gpDLfcZ9DE1bMtjZGWCSLhkQsUCaKjqXtCftGkz27894pEX9VvGNiFsaV67gqv2TEPQ2aDaDDdTDNp42LfDdK1LaWSBCfzsQEyaiR1zjZm1RtoRu1ZM6v6vz4TiqDgU", + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "id": "did:key:aksdkajshdkajhsdkjahsdkjahsdj", + "type": "VaccinationEvent", + "batchNumber": "1183738569", + "administeringCentre": "MoH", + "healthProfessional": "MoH", + "countryOfVaccination": "NZ", + "recipient": { + "type": "VaccineRecipient", + "givenName": "JOHN", + "familyName": "SMITH", + "gender": "Male", + "birthDate": "1958-07-17" + }, + "vaccine": { + "type": "Vaccine", + "disease": "COVID-19", + "atcCode": "J07BX03", + "medicinalProductName": "COVID-19 Vaccine Moderna", + "marketingAuthorizationHolder": "Moderna Biotech" + } + } + }, + "options": { + "proofType": "BbsBlsSignature2020" + } + } + } +} +``` + diff --git a/demo/alice-local.sh b/demo/alice-local.sh new file mode 100755 index 0000000000..d24e50945e --- /dev/null +++ b/demo/alice-local.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# this runs the Faber example as a local instace of instance of aca-py +# you need to run a local von-network (in the von-network directory run "./manage start --logs") +# ... and you need to install the local aca-py python libraries locally ("pip install -r ../requriements.txt -r ../requirements.indy.txt -r ../requirements.bbs.txt") + +# the following will auto-respond on connection and credential requests, but not proof requests +PYTHONPATH=.. ../bin/aca-py start \ + --endpoint http://127.0.0.1:8030 \ + --label alice.agent \ + --inbound-transport http 0.0.0.0 8030 \ + --outbound-transport http \ + --admin 0.0.0.0 8031 \ + --admin-insecure-mode \ + --wallet-type indy \ + --wallet-name alice.agent420695 \ + --wallet-key alice.agent420695 \ + --preserve-exchange-records \ + --auto-provision \ + --genesis-url http://localhost:9000/genesis \ + --trace-target log \ + --trace-tag acapy.events \ + --trace-label alice.agent.trace \ + --auto-ping-connection \ + --auto-respond-messages \ + --auto-accept-invites \ + --auto-accept-requests \ + --auto-respond-credential-proposal \ + --auto-respond-credential-offer \ + --auto-respond-credential-request \ + --auto-store-credential + +# set these for full auto +# --auto-respond-presentation-proposal \ +# --auto-respond-presentation-request \ +# --auto-verify-presentation \ diff --git a/demo/faber-local.sh b/demo/faber-local.sh new file mode 100755 index 0000000000..fcedc69bbf --- /dev/null +++ b/demo/faber-local.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# this runs the Faber example as a local instace of instance of aca-py +# you need to run a local von-network (in the von-network directory run "./manage start --logs") +# ... and you need to install the local aca-py python libraries locally ("pip install -r ../requriements.txt -r ../requirements.indy.txt -r ../requirements.bbs.txt") + +# the following will auto-respond on connection and credential requests, but not proof requests +PYTHONPATH=.. ../bin/aca-py start \ + --endpoint http://127.0.0.1:8020 \ + --label faber.agent \ + --inbound-transport http 0.0.0.0 8020 \ + --outbound-transport http \ + --admin 0.0.0.0 8021 \ + --admin-insecure-mode \ + --wallet-type indy \ + --wallet-name faber.agent916333 \ + --wallet-key faber.agent916333 \ + --preserve-exchange-records \ + --auto-provision \ + --genesis-url http://localhost:9000/genesis \ + --trace-target log \ + --trace-tag acapy.events \ + --trace-label faber.agent.trace \ + --auto-ping-connection \ + --auto-respond-messages \ + --auto-accept-invites \ + --auto-accept-requests \ + --auto-respond-credential-proposal \ + --auto-respond-credential-offer \ + --auto-respond-credential-request \ + --auto-store-credential + +# set these for full auto +# --auto-respond-presentation-proposal \ +# --auto-respond-presentation-request \ +# --auto-verify-presentation \ diff --git a/demo/features/0453-issue-credential.feature b/demo/features/0453-issue-credential.feature index c14caf183d..5e069862ec 100644 --- a/demo/features/0453-issue-credential.feature +++ b/demo/features/0453-issue-credential.feature @@ -14,9 +14,29 @@ Feature: RFC 0453 Aries agent issue credential Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | | --public-did | | driverslicense | Data_DL_NormalizedValues | - | --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | - | --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | - | --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | + #| --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | + #| --public-did --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | + #| --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | + + + @T003.1-RFC0453 @GHA + Scenario Outline: Issue a json-ld credential with the Issuer beginning with an offer + Given we have "2" agents + | name | role | capabilities | + | Acme | issuer | | + | Bob | holder | | + And "Acme" and "Bob" have an existing connection + And "Acme" is ready to issue a json-ld credential for + And "Bob" is ready to receive a json-ld credential + When "Acme" offers "Bob" a json-ld credential with data + Then "Bob" has the json-ld credential issued + + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | + | --public-did --cred-type json-ld | | driverslicense | Data_DL_NormalizedValues | + | --public-did --cred-type json-ld --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | + | --public-did --cred-type json-ld --mediation | --mediation | driverslicense | Data_DL_NormalizedValues | + | --public-did --cred-type json-ld --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | @T004-RFC0453 @GHA @@ -28,7 +48,7 @@ Feature: RFC 0453 Aries agent issue credential And "Acme" and "Bob" have an existing connection And "Bob" has an issued credential from "Acme" Then "Acme" revokes the credential - Then "Bob" has the credential issued + And "Bob" has the credential issued Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | diff --git a/demo/features/0454-present-proof.feature b/demo/features/0454-present-proof.feature index aa60c8639e..2092b46007 100644 --- a/demo/features/0454-present-proof.feature +++ b/demo/features/0454-present-proof.feature @@ -13,9 +13,9 @@ Feature: RFC 0454 Aries agent present proof Then "Faber" has the proof verified Examples: - | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | - | Faber | --public-did | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Faber | --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --public-did --did-exchange | --did-exchange | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T001.1-RFC0454 @@ -32,10 +32,29 @@ Feature: RFC 0454 Aries agent present proof Then "Faber" has the proof verified Examples: - | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | - | Acme | --public-did | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Faber | --public-did | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Acme | --public-did --mediation --multitenant | --mediation --multitenant | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Acme | --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --public-did --mediation --multitenant | --mediation --multitenant | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + + @T001.2-RFC0454 @GHA + Scenario Outline: Present Proof json-ld where the prover does not propose a presentation of the proof and is acknowledged + Given we have "2" agents + | name | role | capabilities | + | Acme | issuer | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued json-ld credential from "" + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request for json-ld proof presentation to "Bob" + Then "Faber" has the proof verified + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Acme | --public-did --cred-type json-ld | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --public-did --cred-type json-ld --did-exchange | --did-exchange | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T002-RFC0454 @GHA @@ -52,9 +71,9 @@ Feature: RFC 0454 Aries agent present proof Then "Faber" has the proof verification fail Examples: - | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | - | Faber | --revocation --public-did | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Faber | --revocation --public-did --did-exchange | --did-exchange | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --revocation --public-did --did-exchange | --did-exchange | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T002.1-RFC0454 @@ -72,8 +91,8 @@ Feature: RFC 0454 Aries agent present proof Then "Faber" has the proof verification fail Examples: - | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | - | Acme | --revocation --public-did | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Faber | --revocation --public-did | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Acme | --revocation --public-did --mediation | | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | - | Acme | --revocation --public-did --multitenant | --multitenant | driverslicense | Data_DL_NormalizedValues | DL_age_over_19 | + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Acme | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Faber | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --mediation | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + | Acme | --revocation --public-did --multitenant | --multitenant | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | diff --git a/demo/features/data/cred_data_schema_driverslicense_v2.json b/demo/features/data/cred_data_schema_driverslicense_v2.json new file mode 100644 index 0000000000..5cc857130f --- /dev/null +++ b/demo/features/data/cred_data_schema_driverslicense_v2.json @@ -0,0 +1,63 @@ +{ + "Data_DL_MaxValues":{ + "cred_name":"Data_DriversLicense_v2_MaxValues", + "schema_name":"Schema_DriversLicense_v2", + "schema_version":"1.0.1", + "attributes":[ + { + "name":"address", + "value":"947 this street, Kingston Ontario Canada, K9O 3R5" + }, + { + "name":"DL_number", + "value":"09385029529385" + }, + { + "name":"expiry", + "value":"10/12/2022" + }, + { + "name":"age", + "value":"30" + } + ], + "filters": { + "indy": { + "cred_def_id": "replace_me", + "issuer_did": "replace_me", + "schema_id": "replace_me", + "schema_issuer_did": "replace_me", + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + }, + "json-ld": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/bbs/v1", + { + "dl": "http://example.com/drivers-license", + "address": "dl:address", + "DL_number": "dl:DL_number", + "expiry": "dl:expiry", + "age": "dl:age" + } + ], + "type": ["VerifiableCredential"], + "issuer": "replace_me", + "issuanceDate": "2010-01-01T19:73:24Z", + "credentialSubject": { + "address": "947 this street, Kingston Ontario Canada, K9O 3R5", + "DL_number": "09385029529385", + "expiry": "10/12/2022", + "age": "30" + } + }, + "options": { + "proofType": "replace_me" + } + + } + } + } +} \ No newline at end of file diff --git a/demo/features/data/presentation_DL_address_v2.json b/demo/features/data/presentation_DL_address_v2.json new file mode 100644 index 0000000000..8bb0953cfb --- /dev/null +++ b/demo/features/data/presentation_DL_address_v2.json @@ -0,0 +1,12 @@ +{ + "presentation": { + "comment": "This is a comment for the send presentation.", + "requested_attributes": { + "address_attrs": { + "cred_type_name": "Schema_DriversLicense_v2", + "revealed": true, + "cred_id": "replace_me" + } + } + } +} \ No newline at end of file diff --git a/demo/features/data/presentation_DL_age_over_19_v2.json b/demo/features/data/presentation_DL_age_over_19_v2.json new file mode 100644 index 0000000000..0de5a834a0 --- /dev/null +++ b/demo/features/data/presentation_DL_age_over_19_v2.json @@ -0,0 +1,19 @@ +{ + "presentation": { + "comment": "This is a comment for the send presentation.", + "requested_attributes": { + "address_attrs": { + "cred_type_name": "Schema_DriversLicense_v2", + "revealed": true, + "cred_id": "replace_me" + } + }, + "requested_predicates": { + "age": { + "cred_type_name": "Schema_DriversLicense_v2", + "cred_id": "replace me" + } + }, + "self_attested_attributes": {} + } +} \ No newline at end of file diff --git a/demo/features/data/proof_request_DL_address_v2.json b/demo/features/data/proof_request_DL_address_v2.json new file mode 100644 index 0000000000..968d4d9a77 --- /dev/null +++ b/demo/features/data/proof_request_DL_address_v2.json @@ -0,0 +1,16 @@ +{ + "presentation_proposal": { + "requested_attributes": { + "address_attrs": { + "name": "address", + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + } + }, + "version": "0.1.0" + } +} \ No newline at end of file diff --git a/demo/features/data/proof_request_DL_age_over_19_v2.json b/demo/features/data/proof_request_DL_age_over_19_v2.json new file mode 100644 index 0000000000..830ad73d30 --- /dev/null +++ b/demo/features/data/proof_request_DL_age_over_19_v2.json @@ -0,0 +1,29 @@ +{ + "presentation_proposal": { + "requested_attributes": { + "address_attrs": { + "name": "address", + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + } + }, + "requested_predicates": { + "age": { + "name": "age", + "p_type": ">", + "p_value": 19, + "restrictions": [ + { + "schema_name": "Schema_DriversLicense_v2", + "schema_version": "1.0.1" + } + ] + } + }, + "version": "0.1.0" + } +} \ No newline at end of file diff --git a/demo/features/data/schema_driverslicense_v2.json b/demo/features/data/schema_driverslicense_v2.json new file mode 100644 index 0000000000..f071dd1c9a --- /dev/null +++ b/demo/features/data/schema_driverslicense_v2.json @@ -0,0 +1,13 @@ +{ + "schema":{ + "schema_name":"Schema_DriversLicense_v2", + "schema_version":"1.0.1", + "attributes":[ + "address", + "DL_number", + "expiry", + "age" + ] + }, + "cred_def_support_revocation":false +} \ No newline at end of file diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index ffebe8b79c..d6b25a9133 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -14,6 +14,15 @@ async_sleep, ) from runners.agent_container import AgentContainer +from runners.support.agent import ( + CRED_FORMAT_INDY, + CRED_FORMAT_JSON_LD, + DID_METHOD_SOV, + DID_METHOD_KEY, + KEY_TYPE_ED255, + KEY_TYPE_BLS, + SIG_TYPE_BLS, +) # This step is defined in another feature file @@ -111,6 +120,134 @@ def step_impl(context, holder): assert False +@given('"{issuer}" is ready to issue a json-ld credential for {schema_name}') +def step_impl(context, issuer, schema_name): + # create a "did:key" to use as issuer + agent = context.active_agents[issuer] + + data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} + new_did = agent_container_POST( + agent["agent"], + "/wallet/did/create", + data=data, + ) + agent["agent"].agent.did = new_did["result"]["did"] + # TODO test for goodness + pass + + +@given('"{holder}" is ready to receive a json-ld credential') +def step_impl(context, holder): + # create a "did:key" to use as holder identity + agent = context.active_agents[holder] + + data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} + new_did = agent_container_POST( + agent["agent"], + "/wallet/did/create", + data=data, + ) + agent["agent"].agent.did = new_did["result"]["did"] + + # TODO test for goodness + pass + + +@when('"{issuer}" offers "{holder}" a json-ld credential with data {credential_data}') +def step_impl(context, issuer, holder, credential_data): + # initiate a cred exchange with a json-ld credential + agent = context.active_agents[issuer] + holder_agent = context.active_agents[holder] + + offer_request = { + "connection_id": agent["agent"].agent.connection_id, + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + ], + "type": [ + "VerifiableCredential", + "PermanentResident", + ], + "id": "https://credential.example.com/residents/1234567890", + "issuer": agent["agent"].agent.did, + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "type": ["PermanentResident"], + # let the holder set this + # "id": holder_agent["agent"].agent.did, + "givenName": "ALICE", + "familyName": "SMITH", + "gender": "Female", + "birthCountry": "Bahamas", + "birthDate": "1958-07-17", + }, + }, + "options": {"proofType": SIG_TYPE_BLS}, + } + }, + } + + agent_container_POST( + agent["agent"], + "/issue-credential-2.0/send-offer", + offer_request, + ) + + # TODO test for goodness + pass + + +@then('"{holder}" has the json-ld credential issued') +def step_impl(context, holder): + # verify the holder has a w3c credential + agent = context.active_agents[holder] + + for i in range(10): + async_sleep(1.0) + w3c_creds = agent_container_POST( + agent["agent"], + "/credentials/w3c", + {}, + ) + if 0 < len(w3c_creds["results"]): + return + + assert False + + +@given( + '"{holder}" has an issued json-ld {schema_name} credential {credential_data} from "{issuer}"' +) +def step_impl(context, holder, schema_name, credential_data, issuer): + context.execute_steps( + u''' + Given "''' + + issuer + + """" is ready to issue a json-ld credential for """ + + schema_name + + ''' + And "''' + + holder + + """" is ready to receive a json-ld credential """ + + ''' + When "''' + + issuer + + '''" offers "''' + + holder + + """" a json-ld credential with data """ + + credential_data + + ''' + Then "''' + + holder + + """" has the json-ld credential issued + """ + ) + + @given( '"{holder}" has an issued {schema_name} credential {credential_data} from "{issuer}"' ) diff --git a/demo/features/steps/0454-present-proof.py b/demo/features/steps/0454-present-proof.py index 7481bfd2e7..03c665e482 100644 --- a/demo/features/steps/0454-present-proof.py +++ b/demo/features/steps/0454-present-proof.py @@ -10,8 +10,18 @@ aries_container_verify_proof, agent_container_GET, agent_container_POST, + async_sleep, ) from runners.agent_container import AgentContainer +from runners.support.agent import ( + CRED_FORMAT_INDY, + CRED_FORMAT_JSON_LD, + DID_METHOD_SOV, + DID_METHOD_KEY, + KEY_TYPE_ED255, + KEY_TYPE_BLS, + SIG_TYPE_BLS, +) # This step is defined in another feature file @@ -41,9 +51,9 @@ def step_impl(context, verifier): # check the received credential status (up to 10 seconds) for i in range(10): + async_sleep(1.0) verified = aries_container_verify_proof(agent["agent"], proof_request) - if verified is not None: - assert verified == "true" + if verified is not None and verified.lower() == "true": return assert False @@ -57,9 +67,78 @@ def step_impl(context, verifier): # check the received credential status (up to 10 seconds) for i in range(10): + async_sleep(1.0) verified = aries_container_verify_proof(agent["agent"], proof_request) - if verified is not None: - assert verified == "false" + if verified is not None and verified.lower() == "false": return assert False + + +@when( + '"{verifier}" sends a request for json-ld proof presentation {request_for_proof} to "{prover}"' +) +def step_impl(context, verifier, request_for_proof, prover): + agent = context.active_agents[verifier] + prover_agent = context.active_agents[prover] + + proof_request_info = { + "comment": "test proof request for json-ld", + "connection_id": agent["agent"].agent.connection_id, + "presentation_request": { + "dif": { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47", + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "format": {"ldp_vp": {"proof_type": [SIG_TYPE_BLS]}}, + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResident" + }, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.credentialSubject.familyName"], + "purpose": "The claim must be from one of the specified person", + "filter": {"const": "SMITH"}, + }, + { + "path": ["$.credentialSubject.givenName"], + "purpose": "The claim must be from one of the specified person", + }, + ], + }, + } + ], + }, + } + }, + } + + proof_exchange = agent_container_POST( + agent["agent"], + "/present-proof-2.0/send-request", + proof_request_info, + ) + + context.proof_request = proof_request_info + context.proof_exchange = proof_exchange + + +@then('"{verifier}" has the json-ld proof verified') +def step_impl(context, verifier): + agent = context.active_agents[verifier] + + pass diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 53c56be850..801d2c6094 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -18,6 +18,13 @@ default_genesis_txns, start_mediator_agent, connect_wallet_to_mediator, + CRED_FORMAT_INDY, + CRED_FORMAT_JSON_LD, + DID_METHOD_SOV, + DID_METHOD_KEY, + KEY_TYPE_ED255, + KEY_TYPE_BLS, + SIG_TYPE_BLS, ) from runners.support.utils import ( # noqa:E402 log_json, @@ -46,6 +53,7 @@ def __init__( admin_port: int, prefix: str = "Aries", no_auto: bool = False, + seed: str = None, **kwargs, ): super().__init__( @@ -53,6 +61,7 @@ def __init__( http_port, admin_port, prefix=prefix, + seed=seed, extra_args=( [] if no_auto @@ -106,6 +115,70 @@ async def handle_connections(self, message): self.log("Connected") self._connection_ready.set_result(True) + async def handle_issue_credential(self, message): + state = message["state"] + credential_exchange_id = message["credential_exchange_id"] + prev_state = self.cred_state.get(credential_exchange_id) + if prev_state == state: + return # ignore + self.cred_state[credential_exchange_id] = state + + self.log( + "Credential: state = {}, credential_exchange_id = {}".format( + state, + credential_exchange_id, + ) + ) + + if state == "offer_received": + log_status("#15 After receiving credential offer, send credential request") + await self.admin_POST( + f"/issue-credential/records/{credential_exchange_id}/send-request" + ) + + elif state == "credential_acked": + cred_id = message["credential_id"] + self.log(f"Stored credential {cred_id} in wallet") + log_status(f"#18.1 Stored credential {cred_id} in wallet") + resp = await self.admin_GET(f"/credential/{cred_id}") + log_json(resp, label="Credential details:") + log_json( + message["credential_request_metadata"], + label="Credential request metadata:", + ) + self.log("credential_id", message["credential_id"]) + self.log("credential_definition_id", message["credential_definition_id"]) + self.log("schema_id", message["schema_id"]) + + elif state == "request_received": + log_status("#17 Issue credential to X") + # issue credentials based on the credential_definition_id + cred_attrs = self.cred_attrs[message["credential_definition_id"]] + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} for (n, v) in cred_attrs.items() + ], + } + try: + cred_ex_rec = await self.admin_POST( + f"/issue-credential/records/{credential_exchange_id}/issue", + { + "comment": ( + f"Issuing credential, exchange {credential_exchange_id}" + ), + "credential_preview": cred_preview, + }, + ) + rev_reg_id = cred_ex_rec.get("revoc_reg_id") + cred_rev_id = cred_ex_rec.get("revocation_id") + if rev_reg_id: + self.log(f"Revocation registry ID: {rev_reg_id}") + if cred_rev_id: + self.log(f"Credential revocation ID: {cred_rev_id}") + except ClientError: + pass + async def handle_issue_credential_v2_0(self, message): state = message["state"] cred_ex_id = message["cred_ex_id"] @@ -115,7 +188,6 @@ async def handle_issue_credential_v2_0(self, message): self.cred_state[cred_ex_id] = state self.log(f"Credential: state = {state}, cred_ex_id = {cred_ex_id}") - print(f"Credential: state = {state}, cred_ex_id = {cred_ex_id}") if state == "request-received": log_status("#17 Issue credential to X") @@ -126,9 +198,19 @@ async def handle_issue_credential_v2_0(self, message): ) elif state == "offer-received": log_status("#15 After receiving credential offer, send credential request") - await self.admin_POST( - f"/issue-credential-2.0/records/{cred_ex_id}/send-request" - ) + if message["by_format"]["cred_offer"].get("indy"): + await self.admin_POST( + f"/issue-credential-2.0/records/{cred_ex_id}/send-request" + ) + elif message["by_format"]["cred_offer"].get("ld_proof"): + holder_did = await self.admin_POST( + f"/wallet/did/create", + {"method": "key", "options": {"key_type": "bls12381g2"}}, + ) + data = {"holder_did": holder_did["result"]["did"]} + await self.admin_POST( + f"/issue-credential-2.0/records/{cred_ex_id}/send-request", data + ) elif state == "done": pass # Logic moved to detail record specific handler @@ -137,10 +219,9 @@ async def handle_issue_credential_v2_0_indy(self, message): rev_reg_id = message.get("rev_reg_id") cred_rev_id = message.get("cred_rev_id") cred_id_stored = message.get("cred_id_stored") - print("\n\n\n", message, "\n\n\n") + if cred_id_stored: cred_id = message["cred_id_stored"] - self.log(f"Stored credential {cred_id} in wallet") log_status(f"#18.1 Stored credential {cred_id} in wallet") cred = await self.admin_GET(f"/credential/{cred_id}") log_json(cred, label="Credential details:") @@ -154,18 +235,25 @@ async def handle_issue_credential_v2_0_indy(self, message): self.log(f"Revocation registry ID: {rev_reg_id}") self.log(f"Credential revocation ID: {cred_rev_id}") + async def handle_issue_credential_v2_0_ld_proof(self, message): + self.log(f"LD Credential: message = {message}") + async def handle_issuer_cred_rev(self, message): pass async def handle_present_proof(self, message): state = message["state"] + presentation_exchange_id = message["presentation_exchange_id"] + presentation_request = message["presentation_request"] self.log( - f"Presentation: state = {state}, presentation_exchange_id = {presentation_exchange_id}" + "Presentation: state =", + state, + ", presentation_exchange_id =", + presentation_exchange_id, ) if state == "request_received": - # prover role log_status( "#24 Query for credentials in the wallet that satisfy the proof request" ) @@ -178,20 +266,15 @@ async def handle_present_proof(self, message): try: # select credentials to provide for the proof - presentation_request = message["presentation_request"] credentials = await self.admin_GET( f"/present-proof/records/{presentation_exchange_id}/credentials" ) if credentials: - if "timestamp" in credentials[0]["cred_info"]["attrs"]: - sorted_creds = sorted( - credentials, - key=lambda c: int(c["cred_info"]["attrs"]["timestamp"]), - reverse=True, - ) - else: - sorted_creds = credentials - for row in sorted_creds: + for row in sorted( + credentials, + key=lambda c: int(c["cred_info"]["attrs"]["timestamp"]), + reverse=True, + ): for referent in row["presentation_referents"]: if referent not in credentials_by_reft: credentials_by_reft[referent] = row @@ -234,13 +317,152 @@ async def handle_present_proof(self, message): pass elif state == "presentation_received": - # verifier role log_status("#27 Process the proof provided by X") log_status("#28 Check if proof is valid") proof = await self.admin_POST( f"/present-proof/records/{presentation_exchange_id}/verify-presentation" ) self.log("Proof =", proof["verified"]) + + async def handle_present_proof_v2_0(self, message): + state = message["state"] + pres_ex_id = message["pres_ex_id"] + self.log(f"Presentation: state = {state}, pres_ex_id = {pres_ex_id}") + + if state == "request-received": + # prover role + log_status( + "#24 Query for credentials in the wallet that satisfy the proof request" + ) + pres_request_indy = message["by_format"].get("pres_request", {}).get("indy") + pres_request_dif = message["by_format"].get("pres_request", {}).get("dif") + + if pres_request_indy: + # include self-attested attributes (not included in credentials) + creds_by_reft = {} + revealed = {} + self_attested = {} + predicates = {} + + try: + # select credentials to provide for the proof + creds = await self.admin_GET( + f"/present-proof-2.0/records/{pres_ex_id}/credentials" + ) + if creds: + if "timestamp" in creds[0]["cred_info"]["attrs"]: + sorted_creds = sorted( + creds, + key=lambda c: int(c["cred_info"]["attrs"]["timestamp"]), + reverse=True, + ) + else: + sorted_creds = creds + for row in sorted_creds: + for referent in row["presentation_referents"]: + if referent not in creds_by_reft: + creds_by_reft[referent] = row + + for referent in pres_request_indy["requested_attributes"]: + if referent in creds_by_reft: + revealed[referent] = { + "cred_id": creds_by_reft[referent]["cred_info"][ + "referent" + ], + "revealed": True, + } + else: + self_attested[referent] = "my self-attested value" + + for referent in pres_request_indy["requested_predicates"]: + if referent in creds_by_reft: + predicates[referent] = { + "cred_id": creds_by_reft[referent]["cred_info"][ + "referent" + ] + } + + log_status("#25 Generate the proof") + request = { + "indy": { + "requested_predicates": predicates, + "requested_attributes": revealed, + "self_attested_attributes": self_attested, + } + } + except ClientError: + pass + + elif pres_request_dif: + try: + # select credentials to provide for the proof + creds = await self.admin_GET( + f"/present-proof-2.0/records/{pres_ex_id}/credentials" + ) + if creds and 0 < len(creds): + creds = sorted( + creds, + key=lambda c: c["issuanceDate"], + reverse=True, + ) + record_id = creds[0]["record_id"] + else: + record_id = None + + log_status("#25 Generate the proof") + request = { + "dif": {}, + } + # specify the record id for each input_descriptor id: + request["dif"]["record_ids"] = {} + for input_descriptor in pres_request_dif["presentation_definition"][ + "input_descriptors" + ]: + request["dif"]["record_ids"][input_descriptor["id"]] = [ + record_id, + ] + log_msg("presenting ld-presentation:", request) + + # NOTE that the holder/prover can also/or specify constraints by including the whole proof request + # and constraining the presented credentials by adding filters, for example: + # + # request = { + # "dif": pres_request_dif, + # } + # request["dif"]["presentation_definition"]["input_descriptors"]["constraints"]["fields"].append( + # { + # "path": [ + # "$.id" + # ], + # "purpose": "Specify the id of the credential to present", + # "filter": { + # "const": "https://credential.example.com/residents/1234567890" + # } + # } + # ) + # + # (NOTE the above assumes the credential contains an "id", which is an optional field) + + except ClientError: + pass + + else: + raise Exception("Invalid presentation request received") + + log_status("#26 Send the proof to X: " + json.dumps(request)) + await self.admin_POST( + f"/present-proof-2.0/records/{pres_ex_id}/send-presentation", + request, + ) + + elif state == "presentation-received": + # verifier role + log_status("#27 Process the proof provided by X") + log_status("#28 Check if proof is valid") + proof = await self.admin_POST( + f"/present-proof-2.0/records/{pres_ex_id}/verify-presentation" + ) + self.log("Proof =", proof["verified"]) self.last_proof_received = proof async def handle_basicmessages(self, message): @@ -325,6 +547,7 @@ def __init__( no_auto: bool = False, revocation: bool = False, tails_server_base_url: str = None, + cred_type: str = CRED_FORMAT_INDY, show_timing: bool = False, multitenant: bool = False, mediation: bool = False, @@ -332,6 +555,7 @@ def __init__( wallet_type: str = None, public_did: bool = True, seed: str = "random", + aip: int = 20, arg_file: str = None, ): # configuration parameters @@ -341,6 +565,7 @@ def __init__( self.no_auto = no_auto self.revocation = revocation self.tails_server_base_url = tails_server_base_url + self.cred_type = cred_type self.show_timing = show_timing self.multitenant = multitenant self.mediation = mediation @@ -348,6 +573,7 @@ def __init__( self.wallet_type = wallet_type self.public_did = public_did self.seed = seed + self.aip = aip self.arg_file = arg_file self.exchange_tracing = False @@ -382,6 +608,7 @@ async def initialize( mediation=self.mediation, wallet_type=self.wallet_type, seed=self.seed, + aip=self.aip, arg_file=self.arg_file, ) else: @@ -389,8 +616,9 @@ async def initialize( await self.agent.listen_webhooks(self.start_port + 2) - if self.public_did: - await self.agent.register_did() + if self.public_did and self.cred_type != CRED_FORMAT_JSON_LD: + await self.agent.register_did(cred_type=self.cred_type) + log_msg("Created public DID") with log_timer("Startup duration:"): await self.agent.start_process() @@ -398,6 +626,13 @@ async def initialize( log_msg("Admin URL is at:", self.agent.admin_url) log_msg("Endpoint URL is at:", self.agent.endpoint) + if self.public_did and self.cred_type == CRED_FORMAT_JSON_LD: + # create did of appropriate type + data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} + new_did = await self.agent.admin_POST("/wallet/did/create", data=data) + self.agent.did = new_did["result"]["did"] + log_msg("Created DID key") + if self.mediation: self.mediator_agent = await start_mediator_agent( self.start_port + 4, self.genesis_txns @@ -435,10 +670,18 @@ async def create_schema_and_cred_def( ): if not self.public_did: raise Exception("Can't create a schema/cred def without a public DID :-(") - self.cred_def_id = await self.agent.create_schema_and_cred_def( - schema_name, schema_attrs, self.revocation, version=version - ) - return self.cred_def_id + if self.cred_type == CRED_FORMAT_INDY: + # need to redister schema and cred def on the ledger + self.cred_def_id = await self.agent.create_schema_and_cred_def( + schema_name, schema_attrs, self.revocation, version=version + ) + return self.cred_def_id + elif self.cred_type == CRED_FORMAT_JSON_LD: + # TODO no schema/cred def required + pass + return None + else: + raise Exception("Invalid credential type:" + self.cred_type) async def issue_credential( self, @@ -447,23 +690,32 @@ async def issue_credential( ): log_status("#13 Issue credential offer to X") - cred_preview = { - "@type": CRED_PREVIEW_TYPE, - "attributes": cred_attrs, - } - offer_request = { - "connection_id": self.agent.connection_id, - "comment": f"Offer on cred def id {cred_def_id}", - "auto_remove": False, - "credential_preview": cred_preview, - "filter": {"indy": {"cred_def_id": cred_def_id}}, - "trace": self.exchange_tracing, - } - cred_exchange = await self.agent.admin_POST( - "/issue-credential-2.0/send-offer", offer_request - ) + if self.cred_type == CRED_FORMAT_INDY: + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": cred_attrs, + } + offer_request = { + "connection_id": self.agent.connection_id, + "comment": f"Offer on cred def id {cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "filter": {"indy": {"cred_def_id": cred_def_id}}, + "trace": self.exchange_tracing, + } + cred_exchange = await self.agent.admin_POST( + "/issue-credential-2.0/send-offer", offer_request + ) + + return cred_exchange + + elif self.cred_type == CRED_FORMAT_JSON_LD: + # TODO create and send the json-ld credential offer + pass + return None - return cred_exchange + else: + raise Exception("Invalid credential type:" + self.cred_type) async def receive_credential( self, @@ -501,29 +753,40 @@ async def receive_credential( async def request_proof(self, proof_request): log_status("#20 Request proof of degree from alice") - indy_proof_request = { - "name": proof_request["name"] - if "name" in proof_request - else "Proof of stuff", - "version": proof_request["version"] - if "version" in proof_request - else "1.0", - "requested_attributes": proof_request["requested_attributes"], - "requested_predicates": proof_request["requested_predicates"], - } - - if self.revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} - proof_request_web_request = { - "connection_id": self.agent.connection_id, - "proof_request": indy_proof_request, - "trace": self.exchange_tracing, - } - proof_exchange = await self.agent.admin_POST( - "/present-proof/send-request", proof_request_web_request - ) + if self.cred_type == CRED_FORMAT_INDY: + indy_proof_request = { + "name": proof_request["name"] + if "name" in proof_request + else "Proof of stuff", + "version": proof_request["version"] + if "version" in proof_request + else "1.0", + "requested_attributes": proof_request["requested_attributes"], + "requested_predicates": proof_request["requested_predicates"], + } + + if self.revocation: + indy_proof_request["non_revoked"] = {"to": int(time.time())} + proof_request_web_request = { + "connection_id": self.agent.connection_id, + "presentation_request": { + "indy": indy_proof_request, + }, + "trace": self.exchange_tracing, + } + proof_exchange = await self.agent.admin_POST( + "/present-proof-2.0/send-request", proof_request_web_request + ) - return proof_exchange + return proof_exchange + + elif self.cred_type == CRED_FORMAT_JSON_LD: + # TODO create and send the json-ld proof request + pass + return None + + else: + raise Exception("Invalid credential type:" + self.cred_type) async def verify_proof(self, proof_request): await asyncio.sleep(1.0) @@ -534,9 +797,18 @@ async def verify_proof(self, proof_request): print("No proof received") return None - # return verified status - print("Received proof:", self.agent.last_proof_received["verified"]) - return self.agent.last_proof_received["verified"] + if self.cred_type == CRED_FORMAT_INDY: + # return verified status + print("Received proof:", self.agent.last_proof_received["verified"]) + return self.agent.last_proof_received["verified"] + + elif self.cred_type == CRED_FORMAT_JSON_LD: + # return verified status + print("Received proof:", self.agent.last_proof_received["verified"]) + return self.agent.last_proof_received["verified"] + + else: + raise Exception("Invalid credential type:" + self.cred_type) async def terminate(self): """Shut down any running agents.""" @@ -576,6 +848,7 @@ async def register_did(self, did, verkey, role): did=did, verkey=verkey, role=role, + cred_type=self.cred_type, ) async def admin_GET(self, path, text=False, params=None) -> dict: @@ -692,7 +965,21 @@ def arg_parser(ident: str = None, port: int = 8020): "--tails-server-base-url", type=str, metavar=(""), - help="Tals server base url", + help="Tails server base url", + ) + parser.add_argument( + "--cred-type", + type=str, + default=CRED_FORMAT_INDY, + metavar=(""), + help="Credential type (indy, json-ld)", + ) + parser.add_argument( + "--aip", + type=str, + default=20, + metavar=(""), + help="API level (10 or 20 (default))", ) parser.add_argument( "--timing", action="store_true", help="Enable timing information" @@ -760,6 +1047,30 @@ async def create_agent_with_args(args, ident: str = None): sys.exit(1) agent_ident = ident if ident else (args.ident if "ident" in args else "Aries") + + if "aip" in args: + aip = int(args.aip) + if not aip in [ + 10, + 20, + ]: + raise Exception("Invalid value for aip, should be 10 or 20") + else: + aip = 20 + + if "cred_type" in args and args.cred_type != CRED_FORMAT_INDY: + public_did = None + aip = 20 + elif "cred_type" in args and args.cred_type == CRED_FORMAT_INDY: + public_did = True + else: + public_did = args.public_did if "public_did" in args else None + + cred_type = args.cred_type if "cred_type" in args else None + log_msg( + f"Initializing demo agent {agent_ident} with AIP {aip} and credential type {cred_type}" + ) + agent = AgentContainer( genesis, agent_ident + ".agent", @@ -770,11 +1081,13 @@ async def create_agent_with_args(args, ident: str = None): show_timing=args.timing, multitenant=args.multitenant, mediation=args.mediation, + cred_type=cred_type, use_did_exchange=args.did_exchange if "did_exchange" in args else False, wallet_type=args.wallet_type, - public_did=args.public_did if "public_did" in args else None, - seed="random" if ("public_did" in args and args.public_did) else None, + public_did=public_did, + seed="random" if public_did else None, arg_file=arg_file, + aip=aip, ) return agent @@ -790,6 +1103,8 @@ async def test_main( mediation: bool = False, use_did_exchange: bool = False, wallet_type: str = None, + cred_type: str = None, + aip: str = 20, ): """Test to startup a couple of agents.""" @@ -811,6 +1126,8 @@ async def test_main( wallet_type=wallet_type, public_did=True, seed="random", + cred_type=cred_type, + aip=aip, ) alice_container = AgentContainer( genesis, @@ -941,6 +1258,8 @@ async def test_main( args.mediation, args.did_exchange, args.wallet_type, + args.cred_type, + args.aip, ) ) except KeyboardInterrupt: diff --git a/demo/runners/alice.py b/demo/runners/alice.py index 5244af85b9..6ee95233c1 100644 --- a/demo/runners/alice.py +++ b/demo/runners/alice.py @@ -83,109 +83,6 @@ async def handle_connections(self, message): self.log("Connected") self._connection_ready.set_result(True) - async def handle_issue_credential_v2_0(self, message): - state = message["state"] - cred_ex_id = message["cred_ex_id"] - prev_state = self.cred_state.get(cred_ex_id) - if prev_state == state: - return # ignore - self.cred_state[cred_ex_id] = state - - self.log(f"Credential: state = {state}, cred_ex_id {cred_ex_id}") - - if state == "offer-received": - log_status("#15 After receiving credential offer, send credential request") - await self.admin_POST( - f"/issue-credential-2.0/records/{cred_ex_id}/send-request" - ) - - elif state == "done": - # Moved to indy detail record handler - pass - - async def handle_issue_credential_v2_0_indy(self, message): - cred_req_metadata = message.get("cred_request_metadata") - if cred_req_metadata: - log_json(cred_req_metadata, label="Credential request metadata:") - - log_json(message, label="indy message:") - cred_id = message.get("cred_id_stored") - if cred_id: - self.log(f"Stored credential {cred_id} in wallet") - log_status(f"#18.1 Stored credential {cred_id} in wallet") - cred = await self.admin_GET(f"/credential/{cred_id}") - log_json(cred, label="Credential details:") - self.log("credential_id", cred_id) - self.log("cred_def_id", cred["cred_def_id"]) - self.log("schema_id", cred["schema_id"]) - - async def handle_present_proof_v2_0(self, message): - state = message["state"] - pres_ex_id = message["pres_ex_id"] - log_msg("Presentation: state =", state, ", pres_ex_id =", pres_ex_id) - - if state == "request-received": - log_status( - "#24 Query for credentials in the wallet that satisfy the proof request" - ) - pres_request = message["by_format"].get("pres_request", {}).get("indy") - - # include self-attested attributes (not included in credentials) - creds_by_reft = {} - revealed = {} - self_attested = {} - predicates = {} - - try: - # select credentials to provide for the proof - creds = await self.admin_GET( - f"/present-proof-2.0/records/{pres_ex_id}/credentials" - ) - if creds: - for row in sorted( - creds, - key=lambda c: int(c["cred_info"]["attrs"]["timestamp"]), - reverse=True, - ): - for referent in row["presentation_referents"]: - if referent not in creds_by_reft: - creds_by_reft[referent] = row - - for referent in pres_request["requested_attributes"]: - if referent in creds_by_reft: - revealed[referent] = { - "cred_id": creds_by_reft[referent]["cred_info"]["referent"], - "revealed": True, - } - else: - self_attested[referent] = "my self-attested value" - - for referent in pres_request["requested_predicates"]: - if referent in creds_by_reft: - predicates[referent] = { - "cred_id": creds_by_reft[referent]["cred_info"]["referent"] - } - - log_status("#25 Generate the proof") - request = { - "indy": { - "requested_predicates": predicates, - "requested_attributes": revealed, - "self_attested_attributes": self_attested, - } - } - - log_status("#26 Send the proof to X") - await self.admin_POST( - f"/present-proof-2.0/records/{pres_ex_id}/send-presentation", - request, - ) - except ClientError: - pass - - async def handle_basicmessages(self, message): - self.log("Received message:", message["content"]) - async def input_invitation(agent): async for details in prompt_loop("Invite details: "): diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 3a970356bf..78b177e836 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -22,6 +22,13 @@ default_genesis_txns, start_mediator_agent, connect_wallet_to_mediator, + CRED_FORMAT_INDY, + CRED_FORMAT_JSON_LD, + DID_METHOD_SOV, + DID_METHOD_KEY, + KEY_TYPE_ED255, + KEY_TYPE_BLS, + SIG_TYPE_BLS, ) from runners.support.utils import ( # noqa:E402 log_msg, @@ -73,71 +80,6 @@ async def detect_connection(self): def connection_ready(self): return self._connection_ready.done() and self._connection_ready.result() - async def handle_oob_invitation(self, message): - pass - - async def handle_connections(self, message): - # a bit of a hack, but for the mediator connection self._connection_ready - # will be None - if not self._connection_ready: - return - - conn_id = message["connection_id"] - if message["state"] == "invitation": - self.connection_id = conn_id - if conn_id == self.connection_id: - if ( - message["rfc23_state"] in ["completed", "response-sent"] - and not self._connection_ready.done() - ): - self.log("Connected") - self._connection_ready.set_result(True) - - async def handle_issue_credential_v2_0(self, message): - state = message["state"] - cred_ex_id = message["cred_ex_id"] - prev_state = self.cred_state.get(cred_ex_id) - if prev_state == state: - return # ignore - self.cred_state[cred_ex_id] = state - - self.log(f"Credential: state = {state}, cred_ex_id = {cred_ex_id}") - - if state == "request-received": - log_status("#17 Issue credential to X") - if not message.get("auto_issue"): - # issue credential based on offer preview in cred ex record - await self.admin_POST( - f"/issue-credential-2.0/records/{cred_ex_id}/issue", - {"comment": f"Issuing credential, exchange {cred_ex_id}"}, - ) - - async def handle_issue_credential_v2_0_indy(self, message): - rev_reg_id = message.get("rev_reg_id") - cred_rev_id = message.get("cred_rev_id") - if rev_reg_id and cred_rev_id: - self.log(f"Revocation registry ID: {rev_reg_id}") - self.log(f"Credential revocation ID: {cred_rev_id}") - - async def handle_issuer_cred_rev(self, message): - pass - - async def handle_present_proof_v2_0(self, message): - state = message["state"] - pres_ex_id = message["pres_ex_id"] - self.log(f"Presentation: state = {state}, pres_ex_id = {pres_ex_id}") - - if state == "presentation-received": - log_status("#27 Process the proof provided by X") - log_status("#28 Check if proof is valid") - proof = await self.admin_POST( - f"/present-proof-2.0/records/{pres_ex_id}/verify-presentation" - ) - self.log("Proof =", proof["verified"]) - - async def handle_basicmessages(self, message): - self.log("Received message:", message["content"]) - async def main(args): faber_agent = await create_agent_with_args(args, ident="faber") @@ -162,16 +104,23 @@ async def main(args): multitenant=faber_agent.multitenant, mediation=faber_agent.mediation, wallet_type=faber_agent.wallet_type, + seed=faber_agent.seed, ) - faber_agent.public_did = True - faber_schema_name = "degree schema" - faber_schema_attrs = ["name", "date", "degree", "age", "timestamp"] - await faber_agent.initialize( - the_agent=agent, - schema_name=faber_schema_name, - schema_attrs=faber_schema_attrs, - ) + if faber_agent.cred_type == CRED_FORMAT_INDY: + faber_agent.public_did = True + faber_schema_name = "degree schema" + faber_schema_attrs = ["name", "date", "degree", "age", "timestamp"] + await faber_agent.initialize( + the_agent=agent, + schema_name=faber_schema_name, + schema_attrs=faber_schema_attrs, + ) + elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: + faber_agent.public_did = True + await faber_agent.initialize(the_agent=agent) + else: + raise Exception("Invalid credential type:" + faber_agent.cred_type) # generate an invitation for Alice await faber_agent.generate_invitation(display_qr=True, wait=True) @@ -237,100 +186,309 @@ async def main(args): elif option == "1": log_status("#13 Issue credential offer to X") - # TODO define attributes to send for credential - faber_agent.agent.cred_attrs[faber_agent.cred_def_id] = { - "name": "Alice Smith", - "date": "2018-05-28", - "degree": "Maths", - "age": "24", - "timestamp": str(int(time.time())), - } - - cred_preview = { - "@type": CRED_PREVIEW_TYPE, - "attributes": [ - {"name": n, "value": v} - for (n, v) in faber_agent.agent.cred_attrs[ - faber_agent.cred_def_id - ].items() - ], - } - offer_request = { - "connection_id": faber_agent.agent.connection_id, - "comment": f"Offer on cred def id {faber_agent.cred_def_id}", - "auto_remove": False, - "credential_preview": cred_preview, - "filter": {"indy": {"cred_def_id": faber_agent.cred_def_id}}, - "trace": exchange_tracing, - } - await faber_agent.agent.admin_POST( - "/issue-credential-2.0/send-offer", offer_request - ) - # TODO issue an additional credential for Student ID + if faber_agent.aip == 10: + # define attributes to send for credential + faber_agent.agent.cred_attrs[faber_agent.cred_def_id] = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "age": "24", + "timestamp": str(int(time.time())), + } + + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} + for (n, v) in faber_agent.agent.cred_attrs[ + faber_agent.cred_def_id + ].items() + ], + } + offer_request = { + "connection_id": faber_agent.agent.connection_id, + "cred_def_id": faber_agent.cred_def_id, + "comment": f"Offer on cred def id {faber_agent.cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "trace": exchange_tracing, + } + await faber_agent.agent.admin_POST( + "/issue-credential/send-offer", offer_request + ) + + elif faber_agent.aip == 20: + if faber_agent.cred_type == CRED_FORMAT_INDY: + faber_agent.agent.cred_attrs[faber_agent.cred_def_id] = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "age": "24", + "timestamp": str(int(time.time())), + } + + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} + for (n, v) in faber_agent.agent.cred_attrs[ + faber_agent.cred_def_id + ].items() + ], + } + offer_request = { + "connection_id": faber_agent.agent.connection_id, + "comment": f"Offer on cred def id {faber_agent.cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "filter": { + "indy": {"cred_def_id": faber_agent.cred_def_id} + }, + "trace": exchange_tracing, + } + + elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: + offer_request = { + "connection_id": faber_agent.agent.connection_id, + "filter": { + "ld_proof": { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + ], + "type": [ + "VerifiableCredential", + "PermanentResident", + ], + "id": "https://credential.example.com/residents/1234567890", + "issuer": faber_agent.agent.did, + "issuanceDate": "2020-01-01T12:00:00Z", + "credentialSubject": { + "type": ["PermanentResident"], + # "id": "", + "givenName": "ALICE", + "familyName": "SMITH", + "gender": "Female", + "birthCountry": "Bahamas", + "birthDate": "1958-07-17", + }, + }, + "options": {"proofType": SIG_TYPE_BLS}, + } + }, + } + + else: + raise Exception( + f"Error invalid credential type: {faber_agent.cred_type}" + ) + + await faber_agent.agent.admin_POST( + "/issue-credential-2.0/send-offer", offer_request + ) + + else: + raise Exception(f"Error invalid AIP level: {faber_agent.aip}") elif option == "2": log_status("#20 Request proof of degree from alice") - req_attrs = [ - { - "name": "name", - "restrictions": [{"schema_name": faber_schema_name}], - }, - { - "name": "date", - "restrictions": [{"schema_name": faber_schema_name}], - }, - ] - if faber_agent.revocation: - req_attrs.append( + if faber_agent.aip == 10: + req_attrs = [ { - "name": "degree", - "restrictions": [{"schema_name": faber_schema_name}], - "non_revoked": {"to": int(time.time() - 1)}, + "name": "name", + "restrictions": [{"schema_name": "degree schema"}], }, - ) - else: - req_attrs.append( { - "name": "degree", - "restrictions": [{"schema_name": faber_schema_name}], + "name": "date", + "restrictions": [{"schema_name": "degree schema"}], + }, + ] + if faber_agent.revocation: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + "non_revoked": {"to": int(time.time() - 1)}, + }, + ) + else: + req_attrs.append( + { + "name": "degree", + "restrictions": [{"schema_name": "degree schema"}], + } + ) + if SELF_ATTESTED: + # test self-attested claims + req_attrs.append( + {"name": "self_attested_thing"}, + ) + req_preds = [ + # test zero-knowledge proofs + { + "name": "age", + "p_type": ">=", + "p_value": 18, + "restrictions": [{"schema_name": "degree schema"}], } + ] + indy_proof_request = { + "name": "Proof of Education", + "version": "1.0", + "requested_attributes": { + f"0_{req_attr['name']}_uuid": req_attr + for req_attr in req_attrs + }, + "requested_predicates": { + f"0_{req_pred['name']}_GE_uuid": req_pred + for req_pred in req_preds + }, + } + + if faber_agent.revocation: + indy_proof_request["non_revoked"] = {"to": int(time.time())} + proof_request_web_request = { + "connection_id": faber_agent.agent.connection_id, + "proof_request": indy_proof_request, + "trace": exchange_tracing, + } + await faber_agent.agent.admin_POST( + "/present-proof/send-request", proof_request_web_request ) - if SELF_ATTESTED: - # test self-attested claims - req_attrs.append( - {"name": "self_attested_thing"}, + pass + + elif faber_agent.aip == 20: + if faber_agent.cred_type == CRED_FORMAT_INDY: + req_attrs = [ + { + "name": "name", + "restrictions": [{"schema_name": faber_schema_name}], + }, + { + "name": "date", + "restrictions": [{"schema_name": faber_schema_name}], + }, + ] + if faber_agent.revocation: + req_attrs.append( + { + "name": "degree", + "restrictions": [ + {"schema_name": faber_schema_name} + ], + "non_revoked": {"to": int(time.time() - 1)}, + }, + ) + else: + req_attrs.append( + { + "name": "degree", + "restrictions": [ + {"schema_name": faber_schema_name} + ], + } + ) + if SELF_ATTESTED: + # test self-attested claims + req_attrs.append( + {"name": "self_attested_thing"}, + ) + req_preds = [ + # test zero-knowledge proofs + { + "name": "age", + "p_type": ">=", + "p_value": 18, + "restrictions": [{"schema_name": faber_schema_name}], + } + ] + indy_proof_request = { + "name": "Proof of Education", + "version": "1.0", + "requested_attributes": { + f"0_{req_attr['name']}_uuid": req_attr + for req_attr in req_attrs + }, + "requested_predicates": { + f"0_{req_pred['name']}_GE_uuid": req_pred + for req_pred in req_preds + }, + } + + if faber_agent.revocation: + indy_proof_request["non_revoked"] = {"to": int(time.time())} + proof_request_web_request = { + "connection_id": faber_agent.agent.connection_id, + "presentation_request": {"indy": indy_proof_request}, + "trace": exchange_tracing, + } + + elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: + proof_request_web_request = { + "comment": "test proof request for json-ld", + "connection_id": faber_agent.agent.connection_id, + "presentation_request": { + "dif": { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47", + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "format": { + "ldp_vp": {"proof_type": [SIG_TYPE_BLS]} + }, + "input_descriptors": [ + { + "id": "citizenship_input_1", + "name": "EU Driver's License", + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResident" + }, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.familyName" + ], + "purpose": "The claim must be from one of the specified person", + "filter": { + "const": "SMITH" + }, + }, + { + "path": [ + "$.credentialSubject.givenName" + ], + "purpose": "The claim must be from one of the specified person", + }, + ], + }, + } + ], + }, + } + }, + } + + else: + raise Exception( + "Error invalid credential type:" + faber_agent.cred_type + ) + + await agent.admin_POST( + "/present-proof-2.0/send-request", proof_request_web_request ) - req_preds = [ - # test zero-knowledge proofs - { - "name": "age", - "p_type": ">=", - "p_value": 18, - "restrictions": [{"schema_name": faber_schema_name}], - } - ] - indy_proof_request = { - "name": "Proof of Education", - "version": "1.0", - "requested_attributes": { - f"0_{req_attr['name']}_uuid": req_attr for req_attr in req_attrs - }, - "requested_predicates": { - f"0_{req_pred['name']}_GE_uuid": req_pred - for req_pred in req_preds - }, - } - - if faber_agent.revocation: - indy_proof_request["non_revoked"] = {"to": int(time.time())} - proof_request_web_request = { - "connection_id": faber_agent.agent.connection_id, - "presentation_request": {"indy": indy_proof_request}, - "trace": exchange_tracing, - } - await agent.admin_POST( - "/present-proof-2.0/send-request", proof_request_web_request - ) + + else: + raise Exception(f"Error invalid AIP level: {faber_agent.aip}") elif option == "3": msg = await prompt("Enter message: ") diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 7c9bf88c53..9eca2718f8 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -62,6 +62,14 @@ DEFAULT_EXTERNAL_HOST = os.getenv("DOCKERHOST") or "host.docker.internal" DEFAULT_PYTHON_PATH = "." +CRED_FORMAT_INDY = "indy" +CRED_FORMAT_JSON_LD = "json-ld" +DID_METHOD_SOV = "sov" +DID_METHOD_KEY = "key" +KEY_TYPE_ED255 = "ed25519" +KEY_TYPE_BLS = "bls12381g2" +SIG_TYPE_BLS = "BbsBlsSignature2020" + class repr_json: def __init__(self, val): @@ -110,7 +118,7 @@ def __init__( internal_host: str = None, external_host: str = None, genesis_data: str = None, - seed: str = "random", + seed: str = None, label: str = None, color: str = None, prefix: str = None, @@ -121,6 +129,7 @@ def __init__( revocation: bool = False, multitenant: bool = False, mediation: bool = False, + aip: int = 10, arg_file: str = None, extra_args=None, **params, @@ -147,6 +156,7 @@ def __init__( self.mediation = mediation self.mediator_connection_id = None self.mediator_request_id = None + self.aip = aip self.arg_file = arg_file self.admin_url = f"http://{self.internal_host}:{admin_port}" @@ -354,30 +364,40 @@ async def register_did( did: str = None, verkey: str = None, role: str = "TRUST_ANCHOR", + cred_type: str = CRED_FORMAT_INDY, ): - self.log(f"Registering {self.ident} ...") - if not ledger_url: - ledger_url = LEDGER_URL - if not ledger_url: - ledger_url = f"http://{self.external_host}:9000" - data = {"alias": alias or self.ident, "role": role} - if did and verkey: - data["did"] = did - data["verkey"] = verkey + if cred_type == CRED_FORMAT_INDY: + # if registering a did for issuing indy credentials, publish the did on the ledger + self.log(f"Registering {self.ident} ...") + if not ledger_url: + ledger_url = LEDGER_URL + if not ledger_url: + ledger_url = f"http://{self.external_host}:9000" + data = {"alias": alias or self.ident, "role": role} + if did and verkey: + data["did"] = did + data["verkey"] = verkey + else: + data["seed"] = self.seed + async with self.client_session.post( + ledger_url + "/register", json=data + ) as resp: + if resp.status != 200: + raise Exception( + f"Error registering DID, response code {resp.status}" + ) + nym_info = await resp.json() + self.did = nym_info["did"] + self.log(f"nym_info: {nym_info}") + if self.multitenant: + if not self.agency_wallet_did: + self.agency_wallet_did = self.did + self.log(f"Registered DID: {self.did}") + elif cred_type == CRED_FORMAT_JSON_LD: + # TODO register a did:key with appropriate signature type + pass else: - data["seed"] = self.seed - async with self.client_session.post( - ledger_url + "/register", json=data - ) as resp: - if resp.status != 200: - raise Exception(f"Error registering DID, response code {resp.status}") - nym_info = await resp.json() - self.did = nym_info["did"] - self.log(f"nym_info: {nym_info}") - if self.multitenant: - if not self.agency_wallet_did: - self.agency_wallet_did = self.did - self.log(f"Registered DID: {self.did}") + raise Exception("Invalid credential type:" + cred_type) async def register_or_switch_wallet( self, @@ -385,6 +405,7 @@ async def register_or_switch_wallet( public_did=False, webhook_port: int = None, mediator_agent=None, + cred_type: str = CRED_FORMAT_INDY, ): if webhook_port is not None: await self.listen_webhooks(webhook_port) @@ -438,13 +459,24 @@ async def register_or_switch_wallet( self.log("New wallet params:", new_wallet) self.managed_wallet_params = new_wallet if public_did: - # assign public did - new_did = await self.admin_POST("/wallet/did/create") - self.did = new_did["result"]["did"] - await self.register_did( - did=new_did["result"]["did"], verkey=new_did["result"]["verkey"] - ) - await self.admin_POST("/wallet/did/public?did=" + self.did) + if cred_type == CRED_FORMAT_INDY: + # assign public did + new_did = await self.admin_POST("/wallet/did/create") + self.did = new_did["result"]["did"] + await self.register_did( + did=new_did["result"]["did"], verkey=new_did["result"]["verkey"] + ) + await self.admin_POST("/wallet/did/public?did=" + self.did) + elif cred_type == CRED_FORMAT_JSON_LD: + # create did of appropriate type + data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} + new_did = await self.admin_POST("/wallet/did/create", data=data) + self.did = new_did["result"]["did"] + + # did:key is not registered as a public did + else: + # todo ignore for now + pass # if mediation, mediate the wallet connections if mediator_agent: