Merge pull request 'feature-revoke' (#137) from feature-revoke into release
Reviewed-on: https://gitea.pangea.org/trustchain-oc1-orchestral/IdHub/pulls/137
This commit is contained in:
commit
8dae7ff10e
|
@ -440,6 +440,7 @@ class DID(models.Model):
|
||||||
related_name='dids',
|
related_name='dids',
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
# JSON-serialized DID document
|
||||||
didweb_document = models.TextField()
|
didweb_document = models.TextField()
|
||||||
|
|
||||||
def get_key_material(self, password):
|
def get_key_material(self, password):
|
||||||
|
@ -589,6 +590,7 @@ class VerificableCredential(models.Model):
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='vcredentials',
|
related_name='vcredentials',
|
||||||
)
|
)
|
||||||
|
# revocationBitmapIndex = models.AutoField()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_didweb(self):
|
def is_didweb(self):
|
||||||
|
@ -600,7 +602,7 @@ class VerificableCredential(models.Model):
|
||||||
if not self.data:
|
if not self.data:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
if self.eidas1_did or self.is_didweb:
|
if self.eidas1_did:
|
||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
return self.user.decrypt_data(self.data, password)
|
return self.user.decrypt_data(self.data, password)
|
||||||
|
@ -646,7 +648,7 @@ class VerificableCredential(models.Model):
|
||||||
self.render(domain),
|
self.render(domain),
|
||||||
self.issuer_did.get_key_material(issuer_pass)
|
self.issuer_did.get_key_material(issuer_pass)
|
||||||
)
|
)
|
||||||
if self.eidas1_did or self.is_didweb:
|
if self.eidas1_did:
|
||||||
self.data = data
|
self.data = data
|
||||||
else:
|
else:
|
||||||
self.data = self.user.encrypt_data(data, password)
|
self.data = self.user.encrypt_data(data, password)
|
||||||
|
@ -660,7 +662,7 @@ class VerificableCredential(models.Model):
|
||||||
|
|
||||||
cred_path = 'credentials'
|
cred_path = 'credentials'
|
||||||
sid = self.id
|
sid = self.id
|
||||||
if self.eidas1_did or self.is_didweb:
|
if self.eidas1_did:
|
||||||
cred_path = 'public/credentials'
|
cred_path = 'public/credentials'
|
||||||
sid = self.hash
|
sid = self.hash
|
||||||
|
|
||||||
|
@ -671,6 +673,7 @@ class VerificableCredential(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
'id_credential': str(self.id),
|
||||||
'vc_id': url_id,
|
'vc_id': url_id,
|
||||||
'issuer_did': self.issuer_did.did,
|
'issuer_did': self.issuer_did.did,
|
||||||
'subject_did': self.subject_did and self.subject_did.did or '',
|
'subject_did': self.subject_did and self.subject_did.did or '',
|
||||||
|
@ -691,6 +694,11 @@ class VerificableCredential(models.Model):
|
||||||
tmpl = get_template(template_name)
|
tmpl = get_template(template_name)
|
||||||
d_ordered = ujson.loads(tmpl.render(context))
|
d_ordered = ujson.loads(tmpl.render(context))
|
||||||
d_minimum = self.filter_dict(d_ordered)
|
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)
|
return ujson.dumps(d_minimum)
|
||||||
|
|
||||||
def get_issued_on(self):
|
def get_issued_on(self):
|
||||||
|
|
|
@ -58,6 +58,11 @@
|
||||||
"dateOfAssessment": "{{ dateOfAssessment }}",
|
"dateOfAssessment": "{{ dateOfAssessment }}",
|
||||||
"evidenceAssessment": "{{ evidenceAssessment }}"
|
"evidenceAssessment": "{{ evidenceAssessment }}"
|
||||||
},
|
},
|
||||||
|
"credentialStatus": {
|
||||||
|
"id": "{{ issuer_did }}",
|
||||||
|
"type": "RevocationBitmap2022",
|
||||||
|
"revocationBitmapIndex": "{{ id_credential }}"
|
||||||
|
},
|
||||||
"credentialSchema": {
|
"credentialSchema": {
|
||||||
"id": "https://idhub.pangea.org/vc_schemas/course-credential.json",
|
"id": "https://idhub.pangea.org/vc_schemas/course-credential.json",
|
||||||
"type": "FullJsonSchemaValidator2021"
|
"type": "FullJsonSchemaValidator2021"
|
||||||
|
|
|
@ -55,6 +55,11 @@
|
||||||
"role": "{{ role }}",
|
"role": "{{ role }}",
|
||||||
"email": "{{ email }}"
|
"email": "{{ email }}"
|
||||||
},
|
},
|
||||||
|
"credentialStatus": {
|
||||||
|
"id": "{{ issuer_did }}",
|
||||||
|
"type": "RevocationBitmap2022",
|
||||||
|
"revocationBitmapIndex": "{{ id_credential }}"
|
||||||
|
},
|
||||||
"credentialSchema": {
|
"credentialSchema": {
|
||||||
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
|
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
|
||||||
"type": "FullJsonSchemaValidator2021"
|
"type": "FullJsonSchemaValidator2021"
|
||||||
|
|
|
@ -66,6 +66,11 @@
|
||||||
"evidence": "{{ evidence }}",
|
"evidence": "{{ evidence }}",
|
||||||
"certificationDate": "{{ certificationDate }}"
|
"certificationDate": "{{ certificationDate }}"
|
||||||
},
|
},
|
||||||
|
"credentialStatus": {
|
||||||
|
"id": "{{ issuer_did }}",
|
||||||
|
"type": "RevocationBitmap2022",
|
||||||
|
"revocationBitmapIndex": "{{ id_credential }}"
|
||||||
|
},
|
||||||
"credentialSchema": {
|
"credentialSchema": {
|
||||||
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
|
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
|
||||||
"type": "FullJsonSchemaValidator2021"
|
"type": "FullJsonSchemaValidator2021"
|
||||||
|
|
|
@ -62,6 +62,11 @@
|
||||||
"connectivityOptionList": "{{ connectivityOptionList }}",
|
"connectivityOptionList": "{{ connectivityOptionList }}",
|
||||||
"assessmentDate": "{{ assessmentDate }}"
|
"assessmentDate": "{{ assessmentDate }}"
|
||||||
},
|
},
|
||||||
|
"credentialStatus": {
|
||||||
|
"id": "{{ issuer_did }}",
|
||||||
|
"type": "RevocationBitmap2022",
|
||||||
|
"revocationBitmapIndex": "{{ id_credential }}"
|
||||||
|
},
|
||||||
"credentialSchema": {
|
"credentialSchema": {
|
||||||
"id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json",
|
"id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json",
|
||||||
"type": "FullJsonSchemaValidator2021"
|
"type": "FullJsonSchemaValidator2021"
|
||||||
|
|
|
@ -61,6 +61,11 @@
|
||||||
"affiliatedSince": "{{ affiliatedSince }}",
|
"affiliatedSince": "{{ affiliatedSince }}",
|
||||||
"affiliatedUntil": "{{ affiliatedUntil }}"
|
"affiliatedUntil": "{{ affiliatedUntil }}"
|
||||||
},
|
},
|
||||||
|
"credentialStatus": {
|
||||||
|
"id": "{{ issuer_did }}",
|
||||||
|
"type": "RevocationBitmap2022",
|
||||||
|
"revocationBitmapIndex": "{{ id_credential }}"
|
||||||
|
},
|
||||||
"credentialSchema": {
|
"credentialSchema": {
|
||||||
"id": "https://idhub.pangea.org/vc_schemas/membership-card.json",
|
"id": "https://idhub.pangea.org/vc_schemas/membership-card.json",
|
||||||
"type": "FullJsonSchemaValidator2021"
|
"type": "FullJsonSchemaValidator2021"
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
import pyroaring
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls import reverse_lazy
|
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.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpResponseRedirect, HttpResponse, Http404
|
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 idhub.email.views import NotifyActivateUserByEmail
|
||||||
from trustchain_idhub import settings
|
from trustchain_idhub import settings
|
||||||
|
|
||||||
|
@ -99,9 +103,34 @@ class PasswordResetView(auth_views.PasswordResetView):
|
||||||
|
|
||||||
|
|
||||||
def serve_did(request, did_id):
|
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)
|
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 = HttpResponse(document)
|
||||||
retval.headers["Content-Type"] = "application/json"
|
retval.headers["Content-Type"] = "application/json"
|
||||||
return retval
|
return retval
|
||||||
|
|
|
@ -30,3 +30,4 @@ ujson==5.9.0
|
||||||
openpyxl==3.1.2
|
openpyxl==3.1.2
|
||||||
jsonpath_ng==1.6.1
|
jsonpath_ng==1.6.1
|
||||||
./didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl
|
./didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl
|
||||||
|
pyroaring==0.4.5
|
|
@ -1,11 +1,16 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
|
import zlib
|
||||||
|
from ast import literal_eval
|
||||||
|
|
||||||
import didkit
|
import didkit
|
||||||
import json
|
import json
|
||||||
import urllib
|
import urllib
|
||||||
import jinja2
|
import jinja2
|
||||||
from django.template.backends.django import Template
|
from django.template.backends.django import Template
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
from pyroaring import BitMap
|
||||||
|
|
||||||
from trustchain_idhub import settings
|
from trustchain_idhub import settings
|
||||||
|
|
||||||
|
@ -18,8 +23,11 @@ def keydid_from_controller_key(key):
|
||||||
return didkit.key_to_did("key", key)
|
return didkit.key_to_did("key", key)
|
||||||
|
|
||||||
|
|
||||||
async def resolve_keydid(keydid):
|
def resolve_did(keydid):
|
||||||
return await didkit.resolve_did(keydid, "{}")
|
async def inner():
|
||||||
|
return await didkit.resolve_did(keydid, "{}")
|
||||||
|
|
||||||
|
return asyncio.run(inner())
|
||||||
|
|
||||||
|
|
||||||
def webdid_from_controller_key(key):
|
def webdid_from_controller_key(key):
|
||||||
|
@ -29,7 +37,7 @@ def webdid_from_controller_key(key):
|
||||||
"""
|
"""
|
||||||
keydid = keydid_from_controller_key(key) # "did:key:<...>"
|
keydid = keydid_from_controller_key(key) # "did:key:<...>"
|
||||||
pubkeyid = keydid.rsplit(":")[-1] # <...>
|
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:]
|
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 = f"did:web:{domain}:did-registry:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>"
|
||||||
webdid_url_owner = webdid_url + "#owner"
|
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.
|
If it is false, the VC is invalid and the second argument contains a JSON object with further information.
|
||||||
"""
|
"""
|
||||||
async def inner():
|
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:
|
def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str:
|
||||||
|
|
Loading…
Reference in New Issue