From 7e99c0ca9f0ce3cf288993c8ee15793280aa72a7 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 05:34:09 +0100 Subject: [PATCH 1/5] Testing branches --- utils/idhub_ssikit/README.md | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/utils/idhub_ssikit/README.md b/utils/idhub_ssikit/README.md index e69de29..8443717 100644 --- a/utils/idhub_ssikit/README.md +++ b/utils/idhub_ssikit/README.md @@ -0,0 +1,73 @@ +# Helper routines to manage DIDs/VC/VPs + +This module is a wrapper around the functions exported by SpruceID's `DIDKit` framework. + +## DID generation and storage + +For now DIDs are of the kind `did:key`, with planned support for `did:web` in the near future. + +Creation of a DID involves two steps: +* Generate a unique DID controller key +* Derive a `did:key` type from the key + +Both must be stored in the IdHub database and linked to a `User` for later retrieval. + +```python +# Use case: generate and link a new DID for an existing user +user = request.user # ... + +controller_key = idhub_ssikit.generate_did_controller_key() +did_string = idhub_ssikit.keydid_from_controller_key(controller_key) + + +did = idhub.models.DID( + did = did_string, + user = user +) +did_controller_key = idhub.models.DIDControllerKey( + key_material = controller_key, + owner_did = did +) + +did.save() +did_controller_key.save() +``` + +## Verifiable Credential issuance + +Verifiable Credential templates are stored as Jinja2 (TBD) templates in `/schemas` folder. Please examine each template to see what data must be passed to it in order to render. + +The data passed to the template must at a minimum include: +* issuer_did +* subject_did +* vc_id + +For example, in order to render `/schemas/member-credential.json`: + +```python +from jinja2 import Environment, FileSystemLoader, select_autoescape +import idhub_ssikit + +env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() +) +unsigned_vc_template = env.get_template("member-credential.json") + +issuer_user = request.user +issuer_did = user.dids[0] # TODO: Django ORM pseudocode +issuer_did_controller_key = did.keys[0] # TODO: Django ORM pseudocode + +data = { + "vc_id": "http://pangea.org/credentials/3731", + "issuer_did": issuer_did, + "subject_did": "did:web:[...]", + "issuance_date": "2020-08-19T21:41:50Z", + "subject_is_member_of": "Pangea" +} +signed_credential = idhub_ssikit.render_and_sign_credential( + unsigned_vc_template, + issuer_did_controller_key, + data +) +``` \ No newline at end of file From fa5a2d172eef55c65af54cb8dc7763c3c13af44a Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 05:36:58 +0100 Subject: [PATCH 2/5] Added some VC templates --- idhub/templates/credentials/exo.json | 30 +++++++++++++++++ idhub/templates/credentials/openarms.json | 33 +++++++++++++++++++ idhub/templates/credentials/paremanel.json | 30 +++++++++++++++++ .../credentials/verifiable_presentation.json | 11 +++++++ 4 files changed, 104 insertions(+) create mode 100644 idhub/templates/credentials/exo.json create mode 100644 idhub/templates/credentials/openarms.json create mode 100644 idhub/templates/credentials/paremanel.json create mode 100644 idhub/templates/credentials/verifiable_presentation.json diff --git a/idhub/templates/credentials/exo.json b/idhub/templates/credentials/exo.json new file mode 100644 index 0000000..1aee10b --- /dev/null +++ b/idhub/templates/credentials/exo.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "street_address": "https://schema.org/streetAddress", + "connectivity_option_list": "https://schema.org/connectivityOptionList", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "id": "{{ vc_id }}", + "type": ["VerifiableCredential", "HomeConnectivitySurveyCredential"], + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "street_address": "{{ street_address }}", + "connectivity_option_list": "{{ connectivity_option_list }}", + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json" + } + } +} diff --git a/idhub/templates/credentials/openarms.json b/idhub/templates/credentials/openarms.json new file mode 100644 index 0000000..e2ecbc6 --- /dev/null +++ b/idhub/templates/credentials/openarms.json @@ -0,0 +1,33 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "destination_country": "https://schema.org/destinationCountry", + "offboarding_date": "https://schema.org/offboardingDate", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "id": "{{ vc_id }}", + "type": ["VerifiableCredential", "MigrantRescueCredential"], + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "name": "{{ name }}", + "country_of_origin": "{{ country_of_origin }}", + "rescue_date": "{{ rescue_date }}", + "destination_country": "{{ destination_country }}", + "offboarding_date": "{{ offboarding_date }}", + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json" + } + } +} diff --git a/idhub/templates/credentials/paremanel.json b/idhub/templates/credentials/paremanel.json new file mode 100644 index 0000000..a40396b --- /dev/null +++ b/idhub/templates/credentials/paremanel.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "street_address": "https://schema.org/streetAddress", + "financial_vulnerability_score": "https://schema.org/financialVulnerabilityScore", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "id": "{{ vc_id }}", + "type": ["VerifiableCredential", "FinancialSituationCredential"], + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "street_address": "{{ street_address }}", + "financial_vulnerability_score": "{{ financial_vulnerability_score }}", + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json" + } + } +} diff --git a/idhub/templates/credentials/verifiable_presentation.json b/idhub/templates/credentials/verifiable_presentation.json new file mode 100644 index 0000000..752affb --- /dev/null +++ b/idhub/templates/credentials/verifiable_presentation.json @@ -0,0 +1,11 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "http://example.org/presentations/3731", + "type": [ + "VerifiablePresentation" + ], + "holder": "{{ holder_did }}", + "verifiableCredential": {{ verifiable_credential_list }} +} From 737d2a7dceb93e885775db96c2bfd318b8019ba8 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:04:30 +0100 Subject: [PATCH 3/5] Verifier portal backchannel endpoint --- idhub/urls.py | 4 +++ idhub/verification_portal/__init__.py | 0 idhub/verification_portal/models.py | 21 +++++++++++++ idhub/verification_portal/views.py | 43 +++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 69 insertions(+) create mode 100644 idhub/verification_portal/__init__.py create mode 100644 idhub/verification_portal/models.py create mode 100644 idhub/verification_portal/views.py diff --git a/idhub/urls.py b/idhub/urls.py index 785f4d1..80dd103 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy from .views import LoginView from .admin import views as views_admin from .user import views as views_user +from .verification_portal import views as views_verification_portal app_name = 'idhub' @@ -171,4 +172,7 @@ urlpatterns = [ name='admin_import'), path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), + + path('verification_portal/verify/', views_verification_portal.verify, + name="verification_portal_verify") ] diff --git a/idhub/verification_portal/__init__.py b/idhub/verification_portal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py new file mode 100644 index 0000000..c8e1615 --- /dev/null +++ b/idhub/verification_portal/models.py @@ -0,0 +1,21 @@ +from django.db import models + + +class VPVerifyRequest(models.Model): + """ + `nonce` is an opaque random string used to lookup verification requests + `expected_credentials` is a JSON list of credential types that must be present in this VP. + Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] + `expected_contents` is a JSON object that places optional constraints on the contents of the + returned VP. + Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] + `action` is (for now) a JSON object describing the next steps to take if this verification + is successful. For example "send mail to with and " + Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification + """ + nonce = models.CharField(max_length=50) + expected_credentials = models.CharField(max_length=255) + expected_contents = models.TextField() + action = models.TextField() + submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py new file mode 100644 index 0000000..afc922b --- /dev/null +++ b/idhub/verification_portal/views.py @@ -0,0 +1,43 @@ +import json + +from django.core.mail import send_mail +from django.http import HttpResponse +from .models import VPVerifyRequest +from django.shortcuts import get_object_or_404 +from more_itertools import flatten, unique_everseen + + +def verify(request): + assert request.method == "POST" + # TODO: use request.POST["presentation_submission"] + vp = json.loads(request.POST["vp_token"]) + nonce = vp["nonce"] + # "vr" = verification_request + vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 + # Get a list of all included verifiable credential types + included_credential_types = unique_everseen(flatten([ + vc["type"] for vc in vp["verifiableCredential"] + ])) + # Check that it matches what we requested + for requested_vc_type in json.loads(vr.expected_credentials): + if requested_vc_type not in included_credential_types: + raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error + # Perform whatever action we have to do + action = json.loads(vr.action) + if action["action"] == "send_mail": + subject = action["params"]["subject"] + to_email = action["params"]["to"] + from_email = "noreply@verifier-portal" + body = request.POST["vp-token"] + send_mail( + subject, + body, + from_email, + [to_email] + ) + elif action["action"] == "something-else": + pass + else: + raise Exception("Unknown action!") + return HttpResponse("OK! Your verifiable presentation was successfully presented.") + diff --git a/requirements.txt b/requirements.txt index 745c483..83aea96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 +more-itertools==10.1.0 From 2c4dca40b7639665159a0e3bac0cb6d554be0a38 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:11:39 +0100 Subject: [PATCH 4/5] minor changes to verifier --- idhub/verification_portal/models.py | 5 ++++- idhub/verification_portal/views.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py index c8e1615..0bd203a 100644 --- a/idhub/verification_portal/models.py +++ b/idhub/verification_portal/models.py @@ -3,7 +3,8 @@ from django.db import models class VPVerifyRequest(models.Model): """ - `nonce` is an opaque random string used to lookup verification requests + `nonce` is an opaque random string used to lookup verification requests. URL-safe. + Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" `expected_credentials` is a JSON list of credential types that must be present in this VP. Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] `expected_contents` is a JSON object that places optional constraints on the contents of the @@ -12,10 +13,12 @@ class VPVerifyRequest(models.Model): `action` is (for now) a JSON object describing the next steps to take if this verification is successful. For example "send mail to with and " Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `response` is a URL that the user's wallet will redirect the user to. `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification """ nonce = models.CharField(max_length=50) expected_credentials = models.CharField(max_length=255) expected_contents = models.TextField() action = models.TextField() + response_or_redirect = models.CharField(max_length=255) submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py index afc922b..df7cab2 100644 --- a/idhub/verification_portal/views.py +++ b/idhub/verification_portal/views.py @@ -1,7 +1,7 @@ import json from django.core.mail import send_mail -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from .models import VPVerifyRequest from django.shortcuts import get_object_or_404 from more_itertools import flatten, unique_everseen @@ -39,5 +39,6 @@ def verify(request): pass else: raise Exception("Unknown action!") - return HttpResponse("OK! Your verifiable presentation was successfully presented.") + # OK! Your verifiable presentation was successfully presented. + return HttpResponseRedirect(vr.response_or_redirect) From 8566098f2c36a919d32b7e430f4bc59d0533cd34 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:42:12 +0100 Subject: [PATCH 5/5] Added verify_presentation bindings and use them in verification_portal backend --- idhub/verification_portal/views.py | 7 +++++- utils/idhub_ssikit/__init__.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py index df7cab2..486f4f7 100644 --- a/idhub/verification_portal/views.py +++ b/idhub/verification_portal/views.py @@ -2,6 +2,8 @@ import json from django.core.mail import send_mail from django.http import HttpResponse, HttpResponseRedirect + +from utils.idhub_ssikit import verify_presentation from .models import VPVerifyRequest from django.shortcuts import get_object_or_404 from more_itertools import flatten, unique_everseen @@ -9,7 +11,10 @@ from more_itertools import flatten, unique_everseen def verify(request): assert request.method == "POST" - # TODO: use request.POST["presentation_submission"] + # TODO: incorporate request.POST["presentation_submission"] as schema definition + (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) + if not presentation_valid: + raise Exception("Failed to verify signature on the given Verifiable Presentation.") vp = json.loads(request.POST["vp_token"]) nonce = vp["nonce"] # "vr" = verification_request diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 18a5ff2..c4ac0e3 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -72,3 +72,40 @@ def verify_credential(vc, proof_options): return didkit.verify_credential(vc, proof_options) return asyncio.run(inner()) + + +def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str: + async def inner(): + unsigned_vp = unsigned_vp_template.render(data) + signed_vp = await didkit.issue_presentation( + unsigned_vp, + '{"proofFormat": "ldp"}', + jwk_holder + ) + return signed_vp + + # TODO: convert from Jinja2 -> django-templates + env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() + ) + unsigned_vp_template = env.get_template("verifiable_presentation.json") + data = { + "holder_did": holder_did, + "verifiable_credential_list": "[" + ",".join(vc_list) + "]" + } + + return asyncio.run(inner()) + + +def verify_presentation(vp): + """ + Returns a (bool, str) tuple indicating whether the credential is valid. + If the boolean is true, the credential is valid and the second argument can be ignored. + If it is false, the VC is invalid and the second argument contains a JSON object with further information. + """ + async def inner(): + proof_options = '{"proofFormat": "ldp"}' + return didkit.verify_presentation(vp, proof_options) + + return asyncio.run(inner())