From e7f6153c6285e83fadca5cddb35f9d55c8806b20 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Wed, 31 Jan 2024 10:54:40 +0100 Subject: [PATCH 1/7] Initial support for revocation of Verifiable Credentials --- idhub/models.py | 2 ++ idhub/views.py | 18 ++++++++++++++++-- utils/idhub_ssikit/__init__.py | 5 ++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index fea0156..4541817 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -440,6 +440,7 @@ class DID(models.Model): related_name='dids', null=True, ) + # JSON-serialized DID document didweb_document = models.TextField() def get_key_material(self, password): @@ -589,6 +590,7 @@ class VerificableCredential(models.Model): on_delete=models.CASCADE, related_name='vcredentials', ) + revocationBitmapIndex = models.AutoField() def get_data(self, password): if not self.data: diff --git a/idhub/views.py b/idhub/views.py index 9c2d595..774ad49 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,3 +1,4 @@ +import json import uuid from django.conf import settings @@ -11,7 +12,7 @@ from django.shortcuts import get_object_or_404, redirect from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect, HttpResponse, Http404 -from idhub.models import DID +from idhub.models import DID, VerificableCredential from idhub.email.views import NotifyActivateUserByEmail from trustchain_idhub import settings @@ -79,7 +80,20 @@ class PasswordResetConfirmView(auth_views.PasswordResetConfirmView): def serve_did(request, did_id): id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}' did = get_object_or_404(DID, did=id_did) - document = did.didweb_document + # Deserialize the base DID from JSON storage + document = json.loads(did.didweb_document) + revoked_credentials = did.verificablecredential_set.filter(status=VerificableCredential.Status.REVOKED) + revoked_credential_indexes = [] + for credential in revoked_credentials: + revoked_credential_indexes.append(credential.revocationBitmapIndex) + encoded_revocation_bitmap = None # TODO + revocation_service = [{ + "id": f"{id_did}#revocation", + "type": "RevocationBitmap2022", + "serviceEndpoint": f"data:application/octet-stream;base64,{encoded_revocation_bitmap}" + }] + # Serialize the DID + Revocation list in preparation for sending + document = json.dumps(document) retval = HttpResponse(document) retval.headers["Content-Type"] = "application/json" return retval diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 85e6e2f..6116ad6 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -101,7 +101,10 @@ def verify_credential(vc): async def inner(): return await didkit.verify_credential(vc, '{"proofFormat": "ldp"}') - return asyncio.run(inner()) + valid, reason = asyncio.run(inner()) + if not valid: + return valid, reason + # Credential passes basic signature verification. Now check it against its schema. def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str: From 19183b9f861679b97b3cd21f8365fba64862613c Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Thu, 1 Feb 2024 21:19:07 +0100 Subject: [PATCH 2/7] =?UTF-8?q?Soporte=20para=20revocaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Falta de alguna manera especificar cuando se crean los did:web, que son prerequisito para revocar credenciales --- idhub/views.py | 10 +++++++++- requirements.txt | 1 + utils/idhub_ssikit/__init__.py | 35 +++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/idhub/views.py b/idhub/views.py index 774ad49..dd1a9a4 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -1,6 +1,9 @@ +import base64 import json import uuid +import zlib +import pyroaring from django.conf import settings from django.core.cache import cache from django.urls import reverse_lazy @@ -82,16 +85,21 @@ def serve_did(request, did_id): did = get_object_or_404(DID, did=id_did) # Deserialize the base DID from JSON storage document = json.loads(did.didweb_document) + # Has this DID issued any Verifiable Credentials? If so, we need to add a Revocation List "service" + # entry to the DID document. revoked_credentials = did.verificablecredential_set.filter(status=VerificableCredential.Status.REVOKED) revoked_credential_indexes = [] for credential in revoked_credentials: revoked_credential_indexes.append(credential.revocationBitmapIndex) - encoded_revocation_bitmap = None # TODO + # TODO: Conditionally add "service" to DID document only if the DID has issued any VC + revocation_bitmap = pyroaring.BitMap(revoked_credential_indexes) + encoded_revocation_bitmap = base64.b64encode(zlib.compress(revocation_bitmap.serialize())) revocation_service = [{ "id": f"{id_did}#revocation", "type": "RevocationBitmap2022", "serviceEndpoint": f"data:application/octet-stream;base64,{encoded_revocation_bitmap}" }] + document["service"] = revocation_service # Serialize the DID + Revocation list in preparation for sending document = json.dumps(document) retval = HttpResponse(document) diff --git a/requirements.txt b/requirements.txt index 27dd8c6..6f858ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ fontTools==4.47.0 weasyprint==60.2 ujson==5.9.0 ./didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl +pyroaring==0.4.5 \ No newline at end of file diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 6116ad6..fb97158 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -1,11 +1,15 @@ import asyncio +import base64 import datetime +import zlib + import didkit import json import urllib import jinja2 from django.template.backends.django import Template from django.template.loader import get_template +from pyroaring import BitMap from trustchain_idhub import settings @@ -18,8 +22,11 @@ def keydid_from_controller_key(key): return didkit.key_to_did("key", key) -async def resolve_keydid(keydid): - return await didkit.resolve_did(keydid, "{}") +def resolve_did(keydid): + async def inner(): + return await didkit.resolve_did(keydid, "{}") + + return asyncio.run(inner()) def webdid_from_controller_key(key): @@ -29,7 +36,7 @@ def webdid_from_controller_key(key): """ keydid = keydid_from_controller_key(key) # "did:key:<...>" pubkeyid = keydid.rsplit(":")[-1] # <...> - document = json.loads(asyncio.run(resolve_keydid(keydid))) # Documento DID en terminos "key" + document = json.loads(resolve_did(keydid)) # Documento DID en terminos "key" domain = urllib.parse.urlencode({"domain": settings.DOMAIN})[7:] webdid_url = f"did:web:{domain}:did-registry:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>" webdid_url_owner = webdid_url + "#owner" @@ -105,6 +112,28 @@ def verify_credential(vc): if not valid: return valid, reason # Credential passes basic signature verification. Now check it against its schema. + # TODO: check agasint schema + pass + # Credential verifies against its schema. Now check revocation status. + vc = json.loads(vc) + revocation_index = int(vc["credentialStatus"]["revocationBitmapIndex"]) # NOTE: THIS FIELD SHOULD BE SERIALIZED AS AN INTEGER, BUT IOTA DOCUMENTAITON SERIALIZES IT AS A STRING. DEFENSIVE CAST ADDED JUST IN CASE. + vc_issuer = vc["issuer"]["id"] # This is a DID + issuer_did_document = json.loads(resolve_did(vc_issuer)) # TODO: implement a caching layer so we don't have to fetch the DID (and thus the revocation list) every time a VC is validated. + issuer_revocation_list = issuer_did_document["service"][0] + assert issuer_revocation_list["type"] == "RevocationBitmap2022" + revocation_bitmap = BitMap.deserialize( + zlib.decompress( + base64.b64decode( + issuer_revocation_list["serviceEndpoint"].rsplit(",")[1] + ) + ) + ) + if revocation_index in revocation_bitmap: + return False, "Credential has been revoked by the issuer" + # Fallthrough means all is good. + return True, "" + + def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str: From 9f40c8c88dc9dca439614943b2548809892c2b2c Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 5 Feb 2024 19:44:54 +0100 Subject: [PATCH 3/7] Complete support for revocation --- idhub/views.py | 8 ++++--- utils/idhub_ssikit/__init__.py | 38 +++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/idhub/views.py b/idhub/views.py index dd1a9a4..7807bee 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -81,20 +81,22 @@ class PasswordResetConfirmView(auth_views.PasswordResetConfirmView): def serve_did(request, did_id): - id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}' + import urllib.parse + domain = urllib.parse.urlencode({"domain": settings.DOMAIN})[7:] + id_did = f'did:web:{domain}:did-registry:{did_id}' did = get_object_or_404(DID, did=id_did) # Deserialize the base DID from JSON storage document = json.loads(did.didweb_document) # Has this DID issued any Verifiable Credentials? If so, we need to add a Revocation List "service" # entry to the DID document. - revoked_credentials = did.verificablecredential_set.filter(status=VerificableCredential.Status.REVOKED) + revoked_credentials = did.vcredentials.filter(status=VerificableCredential.Status.REVOKED) revoked_credential_indexes = [] for credential in revoked_credentials: revoked_credential_indexes.append(credential.revocationBitmapIndex) # TODO: Conditionally add "service" to DID document only if the DID has issued any VC revocation_bitmap = pyroaring.BitMap(revoked_credential_indexes) encoded_revocation_bitmap = base64.b64encode(zlib.compress(revocation_bitmap.serialize())) - revocation_service = [{ + revocation_service = [{ # This is an object within a list. "id": f"{id_did}#revocation", "type": "RevocationBitmap2022", "serviceEndpoint": f"data:application/octet-stream;base64,{encoded_revocation_bitmap}" diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index fb97158..81a2599 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -2,6 +2,7 @@ import asyncio import base64 import datetime import zlib +from ast import literal_eval import didkit import json @@ -106,7 +107,10 @@ def verify_credential(vc): If it is false, the VC is invalid and the second argument contains a JSON object with further information. """ async def inner(): - return await didkit.verify_credential(vc, '{"proofFormat": "ldp"}') + str_res = await didkit.verify_credential(vc, '{"proofFormat": "ldp"}') + res = literal_eval(str_res) + ok = res["warnings"] == [] and res["errors"] == [] + return ok, str_res valid, reason = asyncio.run(inner()) if not valid: @@ -116,24 +120,24 @@ def verify_credential(vc): pass # Credential verifies against its schema. Now check revocation status. vc = json.loads(vc) - revocation_index = int(vc["credentialStatus"]["revocationBitmapIndex"]) # NOTE: THIS FIELD SHOULD BE SERIALIZED AS AN INTEGER, BUT IOTA DOCUMENTAITON SERIALIZES IT AS A STRING. DEFENSIVE CAST ADDED JUST IN CASE. - vc_issuer = vc["issuer"]["id"] # This is a DID - issuer_did_document = json.loads(resolve_did(vc_issuer)) # TODO: implement a caching layer so we don't have to fetch the DID (and thus the revocation list) every time a VC is validated. - issuer_revocation_list = issuer_did_document["service"][0] - assert issuer_revocation_list["type"] == "RevocationBitmap2022" - revocation_bitmap = BitMap.deserialize( - zlib.decompress( - base64.b64decode( - issuer_revocation_list["serviceEndpoint"].rsplit(",")[1] + if "credentialStatus" in vc: + revocation_index = int(vc["credentialStatus"]["revocationBitmapIndex"]) # NOTE: THIS FIELD SHOULD BE SERIALIZED AS AN INTEGER, BUT IOTA DOCUMENTAITON SERIALIZES IT AS A STRING. DEFENSIVE CAST ADDED JUST IN CASE. + vc_issuer = vc["issuer"]["id"] # This is a DID + if vc_issuer[:7] == "did:web": # Only DID:WEB can revoke + issuer_did_document = json.loads(resolve_did(vc_issuer)) # TODO: implement a caching layer so we don't have to fetch the DID (and thus the revocation list) every time a VC is validated. + issuer_revocation_list = issuer_did_document["service"][0] + assert issuer_revocation_list["type"] == "RevocationBitmap2022" + revocation_bitmap = BitMap.deserialize( + zlib.decompress( + base64.b64decode( + issuer_revocation_list["serviceEndpoint"].rsplit(",")[1] + ) + ) ) - ) - ) - if revocation_index in revocation_bitmap: - return False, "Credential has been revoked by the issuer" + if revocation_index in revocation_bitmap: + return False, "Credential has been revoked by the issuer" # Fallthrough means all is good. - return True, "" - - + return True, "Credential passes all checks" def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str: From 8149c2411c66b1f5a1980e4f42274e13a5ca88bc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 13 Feb 2024 10:23:13 +0100 Subject: [PATCH 4/7] use id of credentials for du a index of revoke --- idhub/models.py | 2 +- idhub/views.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index 83f30fb..3758183 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -590,7 +590,7 @@ class VerificableCredential(models.Model): on_delete=models.CASCADE, related_name='vcredentials', ) - revocationBitmapIndex = models.AutoField() + # revocationBitmapIndex = models.AutoField() def get_data(self, password): if not self.data: diff --git a/idhub/views.py b/idhub/views.py index bea0712..e5b90ce 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -114,7 +114,8 @@ def serve_did(request, did_id): revoked_credentials = did.vcredentials.filter(status=VerificableCredential.Status.REVOKED) revoked_credential_indexes = [] for credential in revoked_credentials: - revoked_credential_indexes.append(credential.revocationBitmapIndex) + revoked_credential_indexes.append(credential.id) + # revoked_credential_indexes.append(credential.revocationBitmapIndex) # TODO: Conditionally add "service" to DID document only if the DID has issued any VC revocation_bitmap = pyroaring.BitMap(revoked_credential_indexes) encoded_revocation_bitmap = base64.b64encode(zlib.compress(revocation_bitmap.serialize())) From d2a49b7f20a8631e5386775f06333de4bde796ed Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 14 Feb 2024 17:08:41 +0100 Subject: [PATCH 5/7] add credentialStatus to templates --- idhub/models.py | 1 + idhub/templates/credentials/course-credential.json | 5 +++++ idhub/templates/credentials/e-operator-claim.json | 5 +++++ idhub/templates/credentials/federation-membership.json | 5 +++++ idhub/templates/credentials/financial-vulnerability.json | 5 +++++ idhub/templates/credentials/membership-card.json | 5 +++++ 6 files changed, 26 insertions(+) diff --git a/idhub/models.py b/idhub/models.py index 3758183..4121691 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -666,6 +666,7 @@ class VerificableCredential(models.Model): ) context = { + 'id_credential': self.id, 'vc_id': url_id, 'issuer_did': self.issuer_did.did, 'subject_did': self.subject_did and self.subject_did.did or '', diff --git a/idhub/templates/credentials/course-credential.json b/idhub/templates/credentials/course-credential.json index d4a97cb..6ba46ef 100644 --- a/idhub/templates/credentials/course-credential.json +++ b/idhub/templates/credentials/course-credential.json @@ -58,6 +58,11 @@ "dateOfAssessment": "{{ dateOfAssessment }}", "evidenceAssessment": "{{ evidenceAssessment }}" }, + "credentialStatus": { + "id": "{{ issuer_did }}", + "type": "RevocationBitmap2022", + "revocationBitmapIndex": "{{ id_credential }}" + }, "credentialSchema": { "id": "https://idhub.pangea.org/vc_schemas/course-credential.json", "type": "FullJsonSchemaValidator2021" diff --git a/idhub/templates/credentials/e-operator-claim.json b/idhub/templates/credentials/e-operator-claim.json index 1b85bbe..902a940 100644 --- a/idhub/templates/credentials/e-operator-claim.json +++ b/idhub/templates/credentials/e-operator-claim.json @@ -55,6 +55,11 @@ "role": "{{ role }}", "email": "{{ email }}" }, + "credentialStatus": { + "id": "{{ issuer_did }}", + "type": "RevocationBitmap2022", + "revocationBitmapIndex": "{{ id_credential }}" + }, "credentialSchema": { "id": "https://idhub.pangea.org/vc_schemas/federation-membership.json", "type": "FullJsonSchemaValidator2021" diff --git a/idhub/templates/credentials/federation-membership.json b/idhub/templates/credentials/federation-membership.json index e060b55..ca35e70 100644 --- a/idhub/templates/credentials/federation-membership.json +++ b/idhub/templates/credentials/federation-membership.json @@ -66,6 +66,11 @@ "evidence": "{{ evidence }}", "certificationDate": "{{ certificationDate }}" }, + "credentialStatus": { + "id": "{{ issuer_did }}", + "type": "RevocationBitmap2022", + "revocationBitmapIndex": "{{ id_credential }}" + }, "credentialSchema": { "id": "https://idhub.pangea.org/vc_schemas/federation-membership.json", "type": "FullJsonSchemaValidator2021" diff --git a/idhub/templates/credentials/financial-vulnerability.json b/idhub/templates/credentials/financial-vulnerability.json index 5b38937..859b161 100644 --- a/idhub/templates/credentials/financial-vulnerability.json +++ b/idhub/templates/credentials/financial-vulnerability.json @@ -62,6 +62,11 @@ "connectivityOptionList": "{{ connectivityOptionList }}", "assessmentDate": "{{ assessmentDate }}" }, + "credentialStatus": { + "id": "{{ issuer_did }}", + "type": "RevocationBitmap2022", + "revocationBitmapIndex": "{{ id_credential }}" + }, "credentialSchema": { "id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json", "type": "FullJsonSchemaValidator2021" diff --git a/idhub/templates/credentials/membership-card.json b/idhub/templates/credentials/membership-card.json index cd10786..d227bdc 100644 --- a/idhub/templates/credentials/membership-card.json +++ b/idhub/templates/credentials/membership-card.json @@ -61,6 +61,11 @@ "affiliatedSince": "{{ affiliatedSince }}", "affiliatedUntil": "{{ affiliatedUntil }}" }, + "credentialStatus": { + "id": "{{ issuer_did }}", + "type": "RevocationBitmap2022", + "revocationBitmapIndex": "{{ id_credential }}" + }, "credentialSchema": { "id": "https://idhub.pangea.org/vc_schemas/membership-card.json", "type": "FullJsonSchemaValidator2021" From 0fed4b914d471ae0ef70c2fee8d54dc2ee6876c3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 14 Feb 2024 17:33:24 +0100 Subject: [PATCH 6/7] revoke only didwebs --- idhub/models.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index dc1cc12..4b351a0 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -602,7 +602,7 @@ class VerificableCredential(models.Model): if not self.data: return "" - if self.eidas1_did or self.is_didweb: + if self.eidas1_did: return self.data return self.user.decrypt_data(self.data, password) @@ -648,7 +648,7 @@ class VerificableCredential(models.Model): self.render(domain), self.issuer_did.get_key_material(issuer_pass) ) - if self.eidas1_did or self.is_didweb: + if self.eidas1_did: self.data = data else: self.data = self.user.encrypt_data(data, password) @@ -662,7 +662,7 @@ class VerificableCredential(models.Model): cred_path = 'credentials' sid = self.id - if self.eidas1_did or self.is_didweb: + if self.eidas1_did: cred_path = 'public/credentials' sid = self.hash @@ -673,7 +673,7 @@ class VerificableCredential(models.Model): ) context = { - 'id_credential': self.id, + 'id_credential': str(self.id), 'vc_id': url_id, 'issuer_did': self.issuer_did.did, 'subject_did': self.subject_did and self.subject_did.did or '', @@ -694,6 +694,11 @@ class VerificableCredential(models.Model): tmpl = get_template(template_name) d_ordered = ujson.loads(tmpl.render(context)) d_minimum = self.filter_dict(d_ordered) + + # You can revoke only didweb + if not self.is_didweb: + d_minimum.pop("credentialStatus", None) + return ujson.dumps(d_minimum) def get_issued_on(self): From 764d1e5f0370c520d4459184b43bd0407e0f051f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 14 Feb 2024 18:35:34 +0100 Subject: [PATCH 7/7] fix encode decode base64 --- idhub/views.py | 6 +++++- utils/idhub_ssikit/__init__.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/idhub/views.py b/idhub/views.py index e5b90ce..0d0ee68 100644 --- a/idhub/views.py +++ b/idhub/views.py @@ -118,7 +118,11 @@ def serve_did(request, did_id): # revoked_credential_indexes.append(credential.revocationBitmapIndex) # TODO: Conditionally add "service" to DID document only if the DID has issued any VC revocation_bitmap = pyroaring.BitMap(revoked_credential_indexes) - encoded_revocation_bitmap = base64.b64encode(zlib.compress(revocation_bitmap.serialize())) + encoded_revocation_bitmap = base64.b64encode( + zlib.compress( + revocation_bitmap.serialize() + ) + ).decode('utf-8') revocation_service = [{ # This is an object within a list. "id": f"{id_did}#revocation", "type": "RevocationBitmap2022", diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 81a2599..d97848b 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -117,7 +117,7 @@ def verify_credential(vc): return valid, reason # Credential passes basic signature verification. Now check it against its schema. # TODO: check agasint schema - pass + # pass # Credential verifies against its schema. Now check revocation status. vc = json.loads(vc) if "credentialStatus" in vc: @@ -130,7 +130,7 @@ def verify_credential(vc): revocation_bitmap = BitMap.deserialize( zlib.decompress( base64.b64decode( - issuer_revocation_list["serviceEndpoint"].rsplit(",")[1] + issuer_revocation_list["serviceEndpoint"].rsplit(",")[1].encode('utf-8') ) ) )