diff --git a/idhub/models.py b/idhub/models.py
index a9bf727..7e6f847 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):
@@ -611,6 +612,7 @@ class VerificableCredential(models.Model):
on_delete=models.CASCADE,
related_name='vcredentials',
)
+ # revocationBitmapIndex = models.AutoField()
@property
def is_didweb(self):
@@ -694,6 +696,7 @@ class VerificableCredential(models.Model):
)
context = {
+ '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 '',
@@ -714,6 +717,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):
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"
diff --git a/idhub/templates/idhub/user/profile.html b/idhub/templates/idhub/user/profile.html
index cfbc4b9..b7b4c52 100644
--- a/idhub/templates/idhub/user/profile.html
+++ b/idhub/templates/idhub/user/profile.html
@@ -17,7 +17,13 @@
{% trans 'ARCO Forms' %}
{% endif %}
- {% trans 'Notice of Privacy' %}
+ {% trans 'Privacy Policy' %}
{% load django_bootstrap5 %}
diff --git a/idhub/views.py b/idhub/views.py
index 06aeed0..0d0ee68 100644
--- a/idhub/views.py
+++ b/idhub/views.py
@@ -1,6 +1,10 @@
+import base64
+import json
import uuid
import logging
+import zlib
+import pyroaring
from django.conf import settings
from django.core.cache import cache
from django.urls import reverse_lazy
@@ -12,7 +16,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
@@ -99,9 +103,34 @@ class PasswordResetView(auth_views.PasswordResetView):
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)
- document = did.didweb_document
+ # 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.vcredentials.filter(status=VerificableCredential.Status.REVOKED)
+ revoked_credential_indexes = []
+ for credential in revoked_credentials:
+ 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()
+ )
+ ).decode('utf-8')
+ 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}"
+ }]
+ document["service"] = revocation_service
+ # 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/requirements.txt b/requirements.txt
index da0409f..6032a87 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -30,3 +30,4 @@ ujson==5.9.0
openpyxl==3.1.2
jsonpath_ng==1.6.1
./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 85e6e2f..d97848b 100644
--- a/utils/idhub_ssikit/__init__.py
+++ b/utils/idhub_ssikit/__init__.py
@@ -1,11 +1,16 @@
import asyncio
+import base64
import datetime
+import zlib
+from ast import literal_eval
+
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 +23,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 +37,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"
@@ -99,9 +107,37 @@ 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
- 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.
+ # TODO: check agasint schema
+ # pass
+ # Credential verifies against its schema. Now check revocation status.
+ vc = json.loads(vc)
+ 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].encode('utf-8')
+ )
+ )
+ )
+ if revocation_index in revocation_bitmap:
+ return False, "Credential has been revoked by the issuer"
+ # Fallthrough means all is good.
+ return True, "Credential passes all checks"
def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str: