Merge branch 'release' into bugfix/136-upload-bad-credentials

This commit is contained in:
Cayo Puigdefabregas 2024-02-15 19:00:05 +01:00
commit 55b19c6371
10 changed files with 114 additions and 9 deletions

View File

@ -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):
@ -611,6 +612,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):
@ -694,6 +696,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 '',
@ -714,6 +717,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):

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -17,7 +17,13 @@
<a href="https://laweb.pangea.org/politica-de-proteccio-de-dades/acces-als-formularis-arco/" target="_blank" type="button" class="btn btn-green-user me-3">{% trans 'ARCO Forms' %}</a> <a href="https://laweb.pangea.org/politica-de-proteccio-de-dades/acces-als-formularis-arco/" target="_blank" type="button" class="btn btn-green-user me-3">{% trans 'ARCO Forms' %}</a>
{% endif %} {% endif %}
<a href="javascript:void()" type="button" class="btn btn-green-user">{% trans 'Notice of Privacy' %}</a> <a href=
{% if lang == 'es' %}
"https://laweb.pangea.org/es/politica-de-privacidad/"
{% else %}
"https://laweb.pangea.org/politica-de-privacitat/"
{% endif %}
type="button" class="btn btn-green-user">{% trans 'Privacy Policy' %}</a>
</div> </div>
</div> </div>
{% load django_bootstrap5 %} {% load django_bootstrap5 %}

View File

@ -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

View File

@ -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

View File

@ -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,9 +23,12 @@ 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):
async def inner():
return await didkit.resolve_did(keydid, "{}") 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: