Merge pull request 'pyvckit' (#1) from pyvckit into release

Reviewed-on: #1
This commit is contained in:
cayop 2024-06-11 15:10:31 +00:00
commit 5b5afa5c4c
25 changed files with 399 additions and 574 deletions

View File

@ -31,23 +31,19 @@ The application's backend is responsible for issuing credentials upun user reque
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
``` ```
3. Install the DIDKit wheel 3. Install the required packages:
```
wget https://gitea.pangea.org/trustchain-oc1-orchestral/ssikit_trustchain/raw/branch/master/didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl
```
4. Install the required packages:
``` ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
5. Run migrations: 4. Run migrations:
``` ```
python manage.py migrate python manage.py migrate
``` ```
6. Optionally you can install a minumum data set: 5. Optionally you can install a minumum data set:
``` ```
python manage.py initial_datas python manage.py initial_datas
``` ```
7. Start the development server: 6. Start the development server:
``` ```
python manage.py runserver python manage.py runserver
``` ```

1
cache_context.json Normal file

File diff suppressed because one or more lines are too long

9
context/base.jsonld Normal file
View File

@ -0,0 +1,9 @@
{
"@context": {
"credentialSchema": "https://idhub.pangea.org/context/#credentialSchema",
"value": "https://idhub.pangea.org/context/#value",
"lang": "https://idhub.pangea.org/context/#lang",
"description": "https://idhub.pangea.org/context/#description",
"name": "https://idhub.pangea.org/context/#name"
}
}

View File

@ -0,0 +1,22 @@
{
"@context": {
"firstName": "https://idhub.pangea.org/context/#firstName",
"lastName": "https://idhub.pangea.org/context/#lastName",
"personalIdentifier": "https://idhub.pangea.org/context/#personalIdentifier",
"issuedDate": "https://idhub.pangea.org/context/#issuedDate",
"modeOfInstruction": "https://idhub.pangea.org/context/#modeOfInstruction",
"courseDuration": "https://idhub.pangea.org/context/#courseDuration",
"courseDays": "https://idhub.pangea.org/context/#courseDays",
"courseName": "https://idhub.pangea.org/context/#courseName",
"courseDescription": "https://idhub.pangea.org/context/#courseDescription",
"gradingScheme": "https://idhub.pangea.org/context/#gradingScheme",
"scoreAwarded": "https://idhub.pangea.org/context/#scoreAwarded",
"qualificationAwarded": "https://idhub.pangea.org/context/#qualificationAwarded",
"courseLevel": "https://idhub.pangea.org/context/#courseLevel",
"courseFramework": "https://idhub.pangea.org/context/#courseFramework",
"courseCredits": "https://idhub.pangea.org/context/#courseCredits",
"dateOfAssessment": "https://idhub.pangea.org/context/#dateOfAssessment",
"evidenceAssessment": "https://idhub.pangea.org/context/#evidenceAssessment",
"email": "https://idhub.pangea.org/context/#email"
}
}

View File

@ -0,0 +1,11 @@
{
"@context": {
"legalName": "https://idhub.pangea.org/context/#legalName",
"accreditedBy": "https://idhub.pangea.org/context/#accreditedBy",
"operatorNumber": "https://idhub.pangea.org/context/#operatorNumber",
"limitJurisdiction": "https://idhub.pangea.org/context/#limitJurisdiction",
"accreditedFor": "https://idhub.pangea.org/context/#accreditedFor",
"role": "https://idhub.pangea.org/context/#role",
"email": "https://idhub.pangea.org/context/#email"
}
}

View File

@ -0,0 +1,22 @@
{
"@context": {
"federation": "https://idhub.pangea.org/context/#federation",
"legalName": "https://idhub.pangea.org/context/#legalName",
"shortName": "https://idhub.pangea.org/context/#shortName",
"registrationIdentifier": "https://idhub.pangea.org/context/#registrationIdentifier",
"publicRegistry": "https://idhub.pangea.org/context/#publicRegistry",
"streetAddress": "https://idhub.pangea.org/context/#streetAddress",
"postCode": "https://idhub.pangea.org/context/#postCode",
"city": "https://idhub.pangea.org/context/#city",
"taxReference": "https://idhub.pangea.org/context/#taxReference",
"membershipType": "https://idhub.pangea.org/context/#membershipType",
"membershipStatus": "https://idhub.pangea.org/context/#membershipStatus",
"membershipId": "https://idhub.pangea.org/context/#membershipId",
"membershipSince": "https://idhub.pangea.org/context/#membershipSince",
"email": "https://idhub.pangea.org/context/#email",
"phone": "https://idhub.pangea.org/context/#phone",
"website": "https://idhub.pangea.org/context/#website",
"evidence": "https://idhub.pangea.org/context/#evidence",
"certificationDate": "https://idhub.pangea.org/context/#certificationDate"
}
}

View File

@ -0,0 +1,17 @@
{
"@context": {
"firstName": "https://idhub.pangea.org/context/#firstName",
"lastName": "https://idhub.pangea.org/context/#lastName",
"email": "https://idhub.pangea.org/context/#email",
"phoneNumber": "https://idhub.pangea.org/context/#phoneNumber",
"identityDocType": "https://idhub.pangea.org/context/#identityDocType",
"identityNumber": "https://idhub.pangea.org/context/#identityNumber",
"streetAddress": "https://idhub.pangea.org/context/#streetAddress",
"socialWorkerName": "https://idhub.pangea.org/context/#socialWorkerName",
"socialWorkerSurname": "https://idhub.pangea.org/context/#socialWorkerSurname",
"financialVulnerabilityScore": "https://idhub.pangea.org/context/#financialVulnerabilityScore",
"amountCoveredByOtherAids": "https://idhub.pangea.org/context/#amountCoveredByOtherAids",
"connectivityOptionList": "https://idhub.pangea.org/context/#connectivityOptionList",
"assessmentDate": "https://idhub.pangea.org/context/#assessmentDate"
}
}

View File

@ -0,0 +1,15 @@
{
"@context": {
"firstName": "https://idhub.pangea.org/context/#firstName",
"lastName": "https://idhub.pangea.org/context/#lastName",
"email": "https://idhub.pangea.org/context/#email",
"organisation": "https://idhub.pangea.org/context/#organisation",
"membershipType": "https://idhub.pangea.org/context/#membershipType",
"membershipId": "https://idhub.pangea.org/context/#membershipId",
"affiliatedSince": "https://idhub.pangea.org/context/#iaffiliatedSince",
"affiliatedUntil": "https://idhub.pangea.org/context/#affiliatedUntil",
"typeOfPerson": "https://idhub.pangea.org/context/#typeOfPerson",
"identityDocType": "https://idhub.pangea.org/context/#identityDocType",
"identityNumber": "https://idhub.pangea.org/context/#identityNumber"
}
}

View File

@ -6,16 +6,17 @@ import datetime
from collections import OrderedDict from collections import OrderedDict
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.urls import reverse
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pyvckit.did import (
from utils.idhub_ssikit import ( generate_keys,
generate_did_controller_key, generate_did,
keydid_from_controller_key, gen_did_document,
sign_credential,
webdid_from_controller_key,
verify_credential,
) )
from pyvckit.sign import sign
from pyvckit.verify import verify_vc
from oidc4vp.models import Organization from oidc4vp.models import Organization
from idhub_auth.models import User from idhub_auth.models import User
@ -95,7 +96,7 @@ class Event(models.Model):
message=msg, message=msg,
user=user user=user
) )
# Is required? # Is required?
@classmethod @classmethod
def set_EV_DATA_UPDATE_REQUESTED_BY_USER(cls, user): def set_EV_DATA_UPDATE_REQUESTED_BY_USER(cls, user):
@ -106,7 +107,7 @@ class Event(models.Model):
type=cls.Types.EV_DATA_UPDATE_REQUESTED_BY_USER, type=cls.Types.EV_DATA_UPDATE_REQUESTED_BY_USER,
message=msg, message=msg,
) )
# Is required? # Is required?
@classmethod @classmethod
def set_EV_DATA_UPDATE_REQUESTED(cls, user): def set_EV_DATA_UPDATE_REQUESTED(cls, user):
@ -117,7 +118,7 @@ class Event(models.Model):
message=msg, message=msg,
user=user user=user
) )
@classmethod @classmethod
def set_EV_USR_UPDATED_BY_ADMIN(cls, user): def set_EV_USR_UPDATED_BY_ADMIN(cls, user):
msg = "The admin has updated the following user 's information: " msg = "The admin has updated the following user 's information: "
@ -144,7 +145,7 @@ class Event(models.Model):
message=msg, message=msg,
user=user user=user
) )
@classmethod @classmethod
def set_EV_USR_DELETED_BY_ADMIN(cls, user): def set_EV_USR_DELETED_BY_ADMIN(cls, user):
msg = _("The admin has deleted the user: username: {username}").format( msg = _("The admin has deleted the user: username: {username}").format(
@ -154,7 +155,7 @@ class Event(models.Model):
type=cls.Types.EV_USR_DELETED_BY_ADMIN, type=cls.Types.EV_USR_DELETED_BY_ADMIN,
message=msg message=msg
) )
@classmethod @classmethod
def set_EV_DID_CREATED_BY_USER(cls, did): def set_EV_DID_CREATED_BY_USER(cls, did):
msg = _("New DID with DID-ID: '{did}' created by user '{username}'").format( msg = _("New DID with DID-ID: '{did}' created by user '{username}'").format(
@ -165,7 +166,7 @@ class Event(models.Model):
type=cls.Types.EV_DID_CREATED_BY_USER, type=cls.Types.EV_DID_CREATED_BY_USER,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_DID_CREATED(cls, did): def set_EV_DID_CREATED(cls, did):
msg = _("New DID with label: '{label}' and DID-ID: '{did}' was created'").format( msg = _("New DID with label: '{label}' and DID-ID: '{did}' was created'").format(
@ -177,10 +178,10 @@ class Event(models.Model):
message=msg, message=msg,
user=did.user user=did.user
) )
@classmethod @classmethod
def set_EV_DID_DELETED(cls, did): def set_EV_DID_DELETED(cls, did):
msg = _("The DID with label '{label}' and DID-ID: '{did}' was deleted from your wallet").format( msg = _("The DID with label '{label}' and DID-ID: '{did}' was deleted from your wallet").format(
label=did.label, label=did.label,
did=did.did did=did.did
) )
@ -189,7 +190,7 @@ class Event(models.Model):
message=msg, message=msg,
user=did.user user=did.user
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_DELETED_BY_ADMIN(cls, cred): def set_EV_CREDENTIAL_DELETED_BY_ADMIN(cls, cred):
msg = _("The credential of type '{type}' and ID: '{id}' was deleted").format( msg = _("The credential of type '{type}' and ID: '{id}' was deleted").format(
@ -200,7 +201,7 @@ class Event(models.Model):
type=cls.Types.EV_CREDENTIAL_DELETED_BY_ADMIN, type=cls.Types.EV_CREDENTIAL_DELETED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_DELETED(cls, cred): def set_EV_CREDENTIAL_DELETED(cls, cred):
msg = _("The credential of type '{type}' and ID: '{id}' was deleted from your wallet").format( msg = _("The credential of type '{type}' and ID: '{id}' was deleted from your wallet").format(
@ -212,7 +213,7 @@ class Event(models.Model):
message=msg, message=msg,
user=cred.user user=cred.user
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_ISSUED_FOR_USER(cls, cred): def set_EV_CREDENTIAL_ISSUED_FOR_USER(cls, cred):
msg = _("The credential of type '{type}' and ID: '{id}' was issued for user {username}").format( msg = _("The credential of type '{type}' and ID: '{id}' was issued for user {username}").format(
@ -224,7 +225,7 @@ class Event(models.Model):
type=cls.Types.EV_CREDENTIAL_ISSUED_FOR_USER, type=cls.Types.EV_CREDENTIAL_ISSUED_FOR_USER,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_ISSUED(cls, cred): def set_EV_CREDENTIAL_ISSUED(cls, cred):
msg = _("The credential of type '{type}' and ID: '{id}' was issued and stored in your wallet").format( msg = _("The credential of type '{type}' and ID: '{id}' was issued and stored in your wallet").format(
@ -236,7 +237,7 @@ class Event(models.Model):
message=msg, message=msg,
user=cred.user user=cred.user
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_PRESENTED_BY_USER(cls, cred, verifier): def set_EV_CREDENTIAL_PRESENTED_BY_USER(cls, cred, verifier):
msg = "The credential of type '{type}' and ID: '{id}' " msg = "The credential of type '{type}' and ID: '{id}' "
@ -251,7 +252,7 @@ class Event(models.Model):
type=cls.Types.EV_CREDENTIAL_PRESENTED_BY_USER, type=cls.Types.EV_CREDENTIAL_PRESENTED_BY_USER,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_PRESENTED(cls, cred, verifier): def set_EV_CREDENTIAL_PRESENTED(cls, cred, verifier):
msg = "The credential of type '{type}' and ID: '{id}' " msg = "The credential of type '{type}' and ID: '{id}' "
@ -266,7 +267,7 @@ class Event(models.Model):
message=msg, message=msg,
user=cred.user user=cred.user
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_ENABLED(cls, cred): def set_EV_CREDENTIAL_ENABLED(cls, cred):
msg = _("The credential of type '{type}' was enabled for user {username}").format( msg = _("The credential of type '{type}' was enabled for user {username}").format(
@ -277,7 +278,7 @@ class Event(models.Model):
type=cls.Types.EV_CREDENTIAL_ENABLED, type=cls.Types.EV_CREDENTIAL_ENABLED,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_CAN_BE_REQUESTED(cls, cred): def set_EV_CREDENTIAL_CAN_BE_REQUESTED(cls, cred):
msg = _("You can request the '{type}' credential").format( msg = _("You can request the '{type}' credential").format(
@ -288,7 +289,7 @@ class Event(models.Model):
message=msg, message=msg,
user=cred.user user=cred.user
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_REVOKED_BY_ADMIN(cls, cred): def set_EV_CREDENTIAL_REVOKED_BY_ADMIN(cls, cred):
msg = _("The credential of type '{type}' and ID: '{id}' was revoked for ").format( msg = _("The credential of type '{type}' and ID: '{id}' was revoked for ").format(
@ -299,7 +300,7 @@ class Event(models.Model):
type=cls.Types.EV_CREDENTIAL_REVOKED_BY_ADMIN, type=cls.Types.EV_CREDENTIAL_REVOKED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_CREDENTIAL_REVOKED(cls, cred): def set_EV_CREDENTIAL_REVOKED(cls, cred):
msg = _("The credential of type '{type}' and ID: '{id}' was revoked by admin").format( msg = _("The credential of type '{type}' and ID: '{id}' was revoked by admin").format(
@ -311,7 +312,7 @@ class Event(models.Model):
message=msg, message=msg,
user=cred.user user=cred.user
) )
@classmethod @classmethod
def set_EV_ROLE_CREATED_BY_ADMIN(cls): def set_EV_ROLE_CREATED_BY_ADMIN(cls):
msg = _('A new role was created by admin') msg = _('A new role was created by admin')
@ -319,7 +320,7 @@ class Event(models.Model):
type=cls.Types.EV_ROLE_CREATED_BY_ADMIN, type=cls.Types.EV_ROLE_CREATED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_ROLE_MODIFIED_BY_ADMIN(cls): def set_EV_ROLE_MODIFIED_BY_ADMIN(cls):
msg = _('The role was modified by admin') msg = _('The role was modified by admin')
@ -327,7 +328,7 @@ class Event(models.Model):
type=cls.Types.EV_ROLE_MODIFIED_BY_ADMIN, type=cls.Types.EV_ROLE_MODIFIED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_ROLE_DELETED_BY_ADMIN(cls): def set_EV_ROLE_DELETED_BY_ADMIN(cls):
msg = _('The role was removed by admin') msg = _('The role was removed by admin')
@ -335,7 +336,7 @@ class Event(models.Model):
type=cls.Types.EV_ROLE_DELETED_BY_ADMIN, type=cls.Types.EV_ROLE_DELETED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_SERVICE_CREATED_BY_ADMIN(cls): def set_EV_SERVICE_CREATED_BY_ADMIN(cls):
msg = _('A new service was created by admin') msg = _('A new service was created by admin')
@ -343,7 +344,7 @@ class Event(models.Model):
type=cls.Types.EV_SERVICE_CREATED_BY_ADMIN, type=cls.Types.EV_SERVICE_CREATED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_SERVICE_MODIFIED_BY_ADMIN(cls): def set_EV_SERVICE_MODIFIED_BY_ADMIN(cls):
msg = _('The service was modified by admin') msg = _('The service was modified by admin')
@ -351,7 +352,7 @@ class Event(models.Model):
type=cls.Types.EV_SERVICE_MODIFIED_BY_ADMIN, type=cls.Types.EV_SERVICE_MODIFIED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_SERVICE_DELETED_BY_ADMIN(cls): def set_EV_SERVICE_DELETED_BY_ADMIN(cls):
msg = _('The service was removed by admin') msg = _('The service was removed by admin')
@ -359,7 +360,7 @@ class Event(models.Model):
type=cls.Types.EV_SERVICE_DELETED_BY_ADMIN, type=cls.Types.EV_SERVICE_DELETED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_ORG_DID_CREATED_BY_ADMIN(cls, did): def set_EV_ORG_DID_CREATED_BY_ADMIN(cls, did):
msg = _("New Organisational DID with label: '{label}' and DID-ID: '{did}' was created").format( msg = _("New Organisational DID with label: '{label}' and DID-ID: '{did}' was created").format(
@ -370,7 +371,7 @@ class Event(models.Model):
type=cls.Types.EV_ORG_DID_CREATED_BY_ADMIN, type=cls.Types.EV_ORG_DID_CREATED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_ORG_DID_DELETED_BY_ADMIN(cls, did): def set_EV_ORG_DID_DELETED_BY_ADMIN(cls, did):
msg = _("Organisational DID with label: '{label}' and DID-ID: '{did}' was removed").format( msg = _("Organisational DID with label: '{label}' and DID-ID: '{did}' was removed").format(
@ -381,7 +382,7 @@ class Event(models.Model):
type=cls.Types.EV_ORG_DID_DELETED_BY_ADMIN, type=cls.Types.EV_ORG_DID_DELETED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_USR_DEACTIVATED_BY_ADMIN(cls, user): def set_EV_USR_DEACTIVATED_BY_ADMIN(cls, user):
msg = "The user '{username}' was temporarily deactivated: " msg = "The user '{username}' was temporarily deactivated: "
@ -395,7 +396,7 @@ class Event(models.Model):
type=cls.Types.EV_USR_DEACTIVATED_BY_ADMIN, type=cls.Types.EV_USR_DEACTIVATED_BY_ADMIN,
message=msg, message=msg,
) )
@classmethod @classmethod
def set_EV_USR_ACTIVATED_BY_ADMIN(cls, user): def set_EV_USR_ACTIVATED_BY_ADMIN(cls, user):
msg = "The user '{username}' was activated: " msg = "The user '{username}' was activated: "
@ -417,7 +418,7 @@ class Event(models.Model):
message=msg, message=msg,
user=user user=user
) )
@classmethod @classmethod
def set_EV_USR_SEND_CREDENTIAL(cls, msg): def set_EV_USR_SEND_CREDENTIAL(cls, msg):
cls.objects.create( cls.objects.create(
@ -467,17 +468,24 @@ class DID(models.Model):
user.set_encrypted_sensitive_data() user.set_encrypted_sensitive_data()
user.save() user.save()
self.key_material = user.encrypt_data(value) self.key_material = user.encrypt_data(value)
def set_did(self): def set_did(self):
new_key_material = generate_did_controller_key() new_key_material = generate_keys()
self.set_key_material(new_key_material) self.set_key_material(new_key_material)
if self.type == self.Types.KEY: if self.type == self.Types.KEY:
self.did = keydid_from_controller_key(new_key_material) self.did = generate_did(new_key_material)
elif self.type == self.Types.WEB: elif self.type == self.Types.WEB:
didurl, document = webdid_from_controller_key(new_key_material, settings.DOMAIN) url = "https://{}".format(settings.DOMAIN)
self.did = didurl path = reverse("idhub:serve_did", args=["a"])
self.didweb_document = document
if path:
path = path.split("/a/did.json")[0]
url = "https://{}/{}".format(settings.DOMAIN, path)
self.did = generate_did(new_key_material, url)
key = json.loads(new_key_material)
url, self.didweb_document = gen_did_document(self.did, key)
def get_key(self): def get_key(self):
return json.loads(self.key_material) return json.loads(self.key_material)
@ -681,15 +689,18 @@ class VerificableCredential(models.Model):
# hash of credential without sign # hash of credential without sign
self.hash = hashlib.sha3_256(self.render(domain).encode()).hexdigest() self.hash = hashlib.sha3_256(self.render(domain).encode()).hexdigest()
data = sign_credential(
self.render(domain), key = self.issuer_did.get_key_material()
self.issuer_did.get_key_material() credential = self.render(domain)
)
valid, reason = verify_credential(data) vc = sign(credential, key, self.issuer_did.did)
vc_str = json.dumps(vc)
valid = verify_vc(vc_str)
if not valid: if not valid:
return return
self.data = self.user.encrypt_data(data) self.data = self.user.encrypt_data(vc_str)
self.status = self.Status.ISSUED self.status = self.Status.ISSUED
@ -761,7 +772,7 @@ class VerificableCredential(models.Model):
tmpl = get_template(template_name) tmpl = get_template(template_name)
d = json.loads(tmpl.render({})) d = json.loads(tmpl.render({}))
self.type = d.get('type')[-1] self.type = d.get('type')[-1]
def filter_dict(self, dic): def filter_dict(self, dic):
new_dict = OrderedDict() new_dict = OrderedDict()
@ -788,7 +799,7 @@ class File_datas(models.Model):
class Membership(models.Model): class Membership(models.Model):
""" """
This model represent the relation of this user with the ecosystem. This model represent the relation of this user with the ecosystem.
""" """
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
BENEFICIARY = 1, _('Beneficiary') BENEFICIARY = 1, _('Beneficiary')
@ -838,7 +849,7 @@ class Service(models.Model):
if self.rol.exists(): if self.rol.exists():
return ", ".join([x.name for x in self.rol.order_by("name")]) return ", ".join([x.name for x in self.rol.order_by("name")])
return _("None") return _("None")
def __str__(self): def __str__(self):
return "{} -> {}".format(self.domain, self.get_roles()) return "{} -> {}".format(self.domain, self.get_roles())

View File

@ -1,71 +1,71 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1", "https://idhub.pangea.org/context/base.jsonld",
"https://idhub.pangea.org/credentials/course-credential/v1" "https://idhub.pangea.org/context/course-credential.jsonld"
], ],
"id": "{{ vc_id }}", "id": "{{ vc_id }}",
"type": [ "type": [
"VerifiableCredential", "VerifiableCredential",
"VerifiableAttestation", "VerifiableAttestation",
"CourseCredential" "CourseCredential"
], ],
"issuer": { "issuer": {
"id": "{{ issuer_did }}", "id": "{{ issuer_did }}",
"name": "{{ organisation }}" "name": "{{ organisation }}"
},
"issuanceDate": "{{ issuance_date }}",
"validFrom": "{{ issuance_date }}",
"validUntil": "{{ validUntil }}",
"name": [
{
"value": "NGO Course Credential for participants",
"lang": "en"
}, },
"issuanceDate": "{{ issuance_date }}", {
"validFrom": "{{ issuance_date }}", "value": "Credencial per participants d'un curs impartit per una ONG",
"validUntil": "{{ validUntil }}", "lang": "ca_ES"
"name": [
{
"value": "NGO Course Credential for participants",
"lang": "en"
},
{
"value": "Credencial per participants d'un curs impartit per una ONG",
"lang": "ca_ES"
},
{
"value": "Credencial para participantes de un curso impartido por una ONG",
"lang": "es"
}
],
"description": [
{
"value": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat",
"lang": "en"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"firstName": "{{ firstName }}",
"lastName": "{{ lastName }}",
"email": "{{ email }}",
"personalIdentifier": "{{ personalIdentifier }}",
"issuedDate": "{{ issuedDate }}",
"modeOfInstruction": "{{ modeOfInstruction }}",
"courseDuration": "{{ courseDuration }}",
"courseDays": "{{ courseDays }}",
"courseName": "{{ courseName }}",
"courseDescription": "{{ courseDescription }}",
"gradingScheme": "{{ gradingScheme }}",
"scoreAwarded": "{{ scoreAwarded }}",
"qualificationAwarded": "{{ qualificationAwarded }}",
"courseLevel": "{{ courseLevel }}",
"courseFramework": "{{ courseFramework }}",
"courseCredits": "{{ courseCredits }}",
"dateOfAssessment": "{{ dateOfAssessment }}",
"evidenceAssessment": "{{ evidenceAssessment }}"
}, },
"credentialStatus": { {
"id": "{{ credential_status_id}}", "value": "Credencial para participantes de un curso impartido por una ONG",
"type": "RevocationBitmap2022", "lang": "es"
"revocationBitmapIndex": "{{ id_credential }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/course-credential.json",
"type": "FullJsonSchemaValidator2021"
} }
} ],
"description": [
{
"value": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat",
"lang": "en"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"firstName": "{{ firstName }}",
"lastName": "{{ lastName }}",
"email": "{{ email }}",
"personalIdentifier": "{{ personalIdentifier }}",
"issuedDate": "{{ issuedDate }}",
"modeOfInstruction": "{{ modeOfInstruction }}",
"courseDuration": "{{ courseDuration }}",
"courseDays": "{{ courseDays }}",
"courseName": "{{ courseName }}",
"courseDescription": "{{ courseDescription }}",
"gradingScheme": "{{ gradingScheme }}",
"scoreAwarded": "{{ scoreAwarded }}",
"qualificationAwarded": "{{ qualificationAwarded }}",
"courseLevel": "{{ courseLevel }}",
"courseFramework": "{{ courseFramework }}",
"courseCredits": "{{ courseCredits }}",
"dateOfAssessment": "{{ dateOfAssessment }}",
"evidenceAssessment": "{{ evidenceAssessment }}"
},
"credentialStatus": {
"id": "{{ credential_status_id}}",
"type": "RevocationBitmap2022",
"revocationBitmapIndex": "{{ id_credential }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/course-credential.json",
"type": "FullJsonSchemaValidator2021"
}
}

View File

@ -1,67 +1,67 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1", "https://idhub.pangea.org/context/base.jsonld",
"https://idhub.pangea.org/credentials/e-operator-claim/v1" "https://idhub.pangea.org/context/e-operator-claim.jsonld"
], ],
"id": "{{ vc_id }}", "id": "{{ vc_id }}",
"type": [ "type": [
"VerifiableCredential", "VerifiableCredential",
"VerifiableAttestation", "VerifiableAttestation",
"EOperatorClaim" "EOperatorClaim"
], ],
"issuer": { "issuer": {
"id": "{{ issuer_did }}", "id": "{{ issuer_did }}",
"name": "{{ organisation }}" "name": "{{ organisation }}"
},
"issuanceDate": "{{ issuance_date }}",
"validFrom": "{{ issuance_date }}",
"validUntil": "{{ validUntil }}",
"name": [
{
"value": "Product and waste electronics operator claim",
"lang": "en"
}, },
"issuanceDate": "{{ issuance_date }}", {
"validFrom": "{{ issuance_date }}", "value": "Declaració d'operador de productes i residus electrònics",
"validUntil": "{{ validUntil }}", "lang": "ca_ES"
"name": [
{
"value": "Product and waste electronics operator claim",
"lang": "en"
},
{
"value": "Declaració d'operador de productes i residus electrònics",
"lang": "ca_ES"
},
{
"value": "Declaración de operador de productos y residuos electrónicos",
"lang": "es"
}
],
"description": [
{
"value": "Credential for e-product and e-waste operator claim",
"lang": "en"
},
{
"value": "Credencial per operador de productes i residus electrònics",
"lang": "ca_ES"
},
{
"value": "Credencial para operador de productos y residuos electrónicos",
"lang": "es"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"legalName": "{{ legalName }}",
"accreditedBy": "{{ accreditedBy }}",
"operatorNumber": "{{ operatorNumber }}",
"limitJurisdiction": "{{ limitJurisdiction }}",
"accreditedFor": "{{ accreditedFor }}",
"role": "{{ role }}",
"email": "{{ email }}"
}, },
"credentialStatus": { {
"id": "{{ credential_status_id}}", "value": "Declaración de operador de productos y residuos electrónicos",
"type": "RevocationBitmap2022", "lang": "es"
"revocationBitmapIndex": "{{ id_credential }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
"type": "FullJsonSchemaValidator2021"
} }
],
"description": [
{
"value": "Credential for e-product and e-waste operator claim",
"lang": "en"
},
{
"value": "Credencial per operador de productes i residus electrònics",
"lang": "ca_ES"
},
{
"value": "Credencial para operador de productos y residuos electrónicos",
"lang": "es"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"legalName": "{{ legalName }}",
"accreditedBy": "{{ accreditedBy }}",
"operatorNumber": "{{ operatorNumber }}",
"limitJurisdiction": "{{ limitJurisdiction }}",
"accreditedFor": "{{ accreditedFor }}",
"role": "{{ role }}",
"email": "{{ email }}"
},
"credentialStatus": {
"id": "{{ credential_status_id}}",
"type": "RevocationBitmap2022",
"revocationBitmapIndex": "{{ id_credential }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
"type": "FullJsonSchemaValidator2021"
} }
}

View File

@ -1,78 +1,78 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1", "https://idhub.pangea.org/context/base.jsonld",
"https://idhub.pangea.org/credentials/federation-membership/v1" "https://idhub.pangea.org/context/federation-membership.jsonld"
], ],
"id": "{{ vc_id }}", "id": "{{ vc_id }}",
"type": [ "type": [
"VerifiableCredential", "VerifiableCredential",
"VerifiableAttestation", "VerifiableAttestation",
"FederationMembership" "FederationMembership"
], ],
"issuer": { "issuer": {
"id": "{{ issuer_did }}", "id": "{{ issuer_did }}",
"name": "{{ organisation }}" "name": "{{ organisation }}"
},
"issuanceDate": "{{ issuance_date }}",
"validFrom": "{{ issuance_date }}",
"validUntil": "{{ validUntil }}",
"name": [
{
"value": "NGO federation membership attestation credential",
"lang": "en"
}, },
"issuanceDate": "{{ issuance_date }}", {
"validFrom": "{{ issuance_date }}", "value": "Credencial d'atestat de pertinença a federació d'ONG",
"validUntil": "{{ validUntil }}", "lang": "ca_ES"
"name": [
{
"value": "NGO federation membership attestation credential",
"lang": "en"
},
{
"value": "Credencial d'atestat de pertinença a federació d'ONG",
"lang": "ca_ES"
},
{
"value": "Credencial de atestado de membresía de Federación de ONG",
"lang": "es"
}
],
"description": [
{
"value": "Credential for NGOs that are members of a NGO federation",
"lang": "en"
},
{
"value": "Credencial para ONG que son miembros de una federación de ONG",
"lang": "es"
},
{
"value": "Credencial per a les ONG que són membres d'una federació d'ONG",
"lang": "ca_ES"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"federation": "{{ federation }}",
"legalName": "{{ legalName }}",
"shortName": "{{ shortName }}",
"registrationIdentifier": "{{ registrationIdentifier }}",
"publicRegistry": "{{ publicRegistry }}",
"streetAddress": "{{ streetAddress }}",
"postCode": "{{ postCode }}",
"city": "{{ city }}",
"taxReference": "{{ taxReference }}",
"membershipType": "{{ membershipType }}",
"membershipStatus": "{{ membershipStatus }}",
"membershipId": "{{ membershipId }}",
"membershipSince": "{{ membershipSince }}",
"email": "{{ email }}",
"phone": "{{ phone }}",
"website": "{{ website }}",
"evidence": "{{ evidence }}",
"certificationDate": "{{ certificationDate }}"
}, },
"credentialStatus": { {
"id": "{{ credential_status_id}}", "value": "Credencial de atestado de membresía de Federación de ONG",
"type": "RevocationBitmap2022", "lang": "es"
"revocationBitmapIndex": "{{ id_credential }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
"type": "FullJsonSchemaValidator2021"
} }
],
"description": [
{
"value": "Credential for NGOs that are members of a NGO federation",
"lang": "en"
},
{
"value": "Credencial para ONG que son miembros de una federación de ONG",
"lang": "es"
},
{
"value": "Credencial per a les ONG que són membres d'una federació d'ONG",
"lang": "ca_ES"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"federation": "{{ federation }}",
"legalName": "{{ legalName }}",
"shortName": "{{ shortName }}",
"registrationIdentifier": "{{ registrationIdentifier }}",
"publicRegistry": "{{ publicRegistry }}",
"streetAddress": "{{ streetAddress }}",
"postCode": "{{ postCode }}",
"city": "{{ city }}",
"taxReference": "{{ taxReference }}",
"membershipType": "{{ membershipType }}",
"membershipStatus": "{{ membershipStatus }}",
"membershipId": "{{ membershipId }}",
"membershipSince": "{{ membershipSince }}",
"email": "{{ email }}",
"phone": "{{ phone }}",
"website": "{{ website }}",
"evidence": "{{ evidence }}",
"certificationDate": "{{ certificationDate }}"
},
"credentialStatus": {
"id": "{{ credential_status_id}}",
"type": "RevocationBitmap2022",
"revocationBitmapIndex": "{{ id_credential }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
"type": "FullJsonSchemaValidator2021"
} }
}

View File

@ -1,8 +1,8 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1", "https://idhub.pangea.org/context/base.jsonld",
"https://idhub.pangea.org/credentials/financial-vulnerability/v1" "https://idhub.pangea.org/context/financial-vulnerability.jsonld"
], ],
"id": "{{ vc_id }}", "id": "{{ vc_id }}",
"type": [ "type": [

View File

@ -1,8 +1,8 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1", "https://idhub.pangea.org/context/base.jsonld",
"https://idhub.pangea.org/credentials/membership-card/v1" "https://idhub.pangea.org/context/membership-card.jsonld"
], ],
"type": [ "type": [
"VerifiableCredential", "VerifiableCredential",

View File

@ -21,7 +21,7 @@ from .views import (
LoginView, LoginView,
PasswordResetView, PasswordResetView,
PasswordResetConfirmView, PasswordResetConfirmView,
serve_did, ServeDidView,
DobleFactorSendView, DobleFactorSendView,
) )
from .admin import views as views_admin from .admin import views as views_admin
@ -183,7 +183,7 @@ urlpatterns = [
name='admin_2fauth'), name='admin_2fauth'),
path('admin/auth/2f/', DobleFactorSendView.as_view(), name='confirm_send_2f'), path('admin/auth/2f/', DobleFactorSendView.as_view(), name='confirm_send_2f'),
path('did-registry/<str:did_id>/did.json', serve_did) path('did-registry/<str:did_id>/did.json', ServeDidView, name="serve_did")
# path('verification_portal/verify/', views_verification_portal.verify, # path('verification_portal/verify/', views_verification_portal.verify,
# name="verification_portal_verify") # name="verification_portal_verify")

View File

@ -90,7 +90,7 @@ class PasswordResetView(auth_views.PasswordResetView):
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)
def serve_did(request, did_id): def ServeDidView(request, did_id):
domain = settings.DOMAIN domain = settings.DOMAIN
id_did = f'did:web:{domain}:did-registry:{did_id}' 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)

View File

@ -1,11 +1,12 @@
import json import json
import uuid
from django import forms from django import forms
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from utils.idhub_ssikit import create_verifiable_presentation from pyvckit.sign import sign
from idhub.models import VerificableCredential from idhub.models import VerificableCredential
@ -72,13 +73,19 @@ class AuthorizeForm(forms.Form):
def get_verificable_presentation(self): def get_verificable_presentation(self):
did = self.subject_did did = self.subject_did
vc_list = [json.loads(x) for x in self.list_credentials]
vp_template = get_template('credentials/verifiable_presentation.json') vp_template = get_template('credentials/verifiable_presentation.json')
vc_list = json.dumps([json.loads(x) for x in self.list_credentials])
context = { context = {
"holder_did": did.did, "holder_did": did.did,
"verifiable_credential_list": vc_list "id": str(uuid.uuid4())
} }
unsigned_vp = vp_template.render(context) unsigned_vp = vp_template.render(context)
vp = json.loads(unsigned_vp)
vp["verifiableCredential"] = vc_list
vp_str = json.dumps(vp)
key_material = did.get_key_material() key_material = did.get_key_material()
self.vp = create_verifiable_presentation(key_material, unsigned_vp) vp = sign(vp_str, key_material, did.did)
self.vp = json.dumps(vp)

View File

@ -12,7 +12,7 @@ from django.http import QueryDict
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from idhub_auth.models import User from idhub_auth.models import User
from django.db import models from django.db import models
from utils.idhub_ssikit import verify_presentation from pyvckit.verify import verify_vp
SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@ -22,7 +22,7 @@ def gen_salt(length: int) -> str:
"""Generate a random string of SALT_CHARS with specified ``length``.""" """Generate a random string of SALT_CHARS with specified ``length``."""
if length <= 0: if length <= 0:
raise ValueError("Salt length must be positive") raise ValueError("Salt length must be positive")
return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
@ -48,7 +48,7 @@ class Organization(models.Model):
For use the packages requests we need use my_client_id For use the packages requests we need use my_client_id
For use in the get or post method of a View, then we need use client_id For use in the get or post method of a View, then we need use client_id
and secret_id. and secret_id.
main is a field which indicates the organization of this idhub main is a field which indicates the organization of this idhub
""" """
name = models.CharField(max_length=250) name = models.CharField(max_length=250)
domain = models.CharField(max_length=250, null=True, default=None) domain = models.CharField(max_length=250, null=True, default=None)
@ -130,7 +130,7 @@ class Organization(models.Model):
sb = secret.SecretBox(sb_key) sb = secret.SecretBox(sb_key)
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = data.encode('utf-8') data = data.encode('utf-8')
return base64.b64encode(sb.encrypt(data)).decode('utf-8') return base64.b64encode(sb.encrypt(data)).decode('utf-8')
def get_salt(self): def get_salt(self):
@ -173,7 +173,7 @@ class Organization(models.Model):
sb = secret.SecretBox(sb_key) sb = secret.SecretBox(sb_key)
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = data.encode('utf-8') data = data.encode('utf-8')
encrypted_data = base64.b64encode(sb.encrypt(data)).decode('utf-8') encrypted_data = base64.b64encode(sb.encrypt(data)).decode('utf-8')
self.encrypted_sensitive_data = encrypted_data self.encrypted_sensitive_data = encrypted_data
@ -261,7 +261,7 @@ class OAuth2VPToken(models.Model):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
code = kwargs.pop("code", None) code = kwargs.pop("code", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.authorization = Authorization.objects.filter(code=code).first() self.authorization = Authorization.objects.filter(code=code).first()
@property @property
@ -271,7 +271,7 @@ class OAuth2VPToken(models.Model):
return self.authorization.code return self.authorization.code
def verifing(self): def verifing(self):
self.result_verify = verify_presentation(self.vp_token) self.result_verify = verify_vp(self.vp_token)
def get_result_verify(self): def get_result_verify(self):
if not self.result_verify: if not self.result_verify:
@ -284,11 +284,10 @@ class OAuth2VPToken(models.Model):
"redirect_uri": "", "redirect_uri": "",
"response": "", "response": "",
} }
verification = json.loads(self.result_verify) if not self.result_verify:
if verification.get('errors') or verification.get('warnings'):
response["verify"] = "Error, {}".format(_("Failed verification")) response["verify"] = "Error, {}".format(_("Failed verification"))
return response return response
response["verify"] = "Ok, {}".format(_("Correct verification")) response["verify"] = "Ok, {}".format(_("Correct verification"))
url = self.get_redirect_url() url = self.get_redirect_url()
if url: if url:

View File

@ -2,10 +2,10 @@
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1" "https://www.w3.org/2018/credentials/v1"
], ],
"id": "http://example.org/presentations/3731", "id": "{{ id }}",
"type": [ "type": [
"VerifiablePresentation" "VerifiablePresentation"
], ],
"holder": "{{ holder_did }}", "holder": "{{ holder_did }}",
"verifiableCredential": {{ verifiable_credential_list|safe }} "verifiableCredential": ""
} }

View File

@ -174,11 +174,7 @@ class VerifyView(View):
""" """
Send a email when a user is activated. Send a email when a user is activated.
""" """
verification = self.vp_token.get_result_verify() if not self.vp_token.result_verify:
if not verification:
return
if verification.get('errors') or verification.get('warnings'):
return return
email = self.get_email(user) email = self.get_email(user)

View File

@ -1,14 +1,6 @@
import json
import requests
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from utils.idhub_ssikit import create_verifiable_presentation
from oidc4vp.models import Organization, Authorization from oidc4vp.models import Organization, Authorization
from promotion.models import Promotion from promotion.models import Promotion
@ -25,7 +17,7 @@ class WalletForm(forms.Form):
self.fields['organization'].choices = [ self.fields['organization'].choices = [
(x.id, x.name) for x in Organization.objects.exclude( (x.id, x.name) for x in Organization.objects.exclude(
domain=settings.DOMAIN domain=settings.DOMAIN
) )
] ]
def save(self, commit=True): def save(self, commit=True):
@ -51,10 +43,10 @@ class WalletForm(forms.Form):
self.promotion.save() self.promotion.save()
return self.authorization.authorize() return self.authorization.authorize()
return
return
class ContractForm(forms.Form): class ContractForm(forms.Form):
nif = forms.CharField() nif = forms.CharField()
name = forms.CharField() name = forms.CharField()
@ -66,4 +58,3 @@ class ContractForm(forms.Form):
birthday = forms.CharField() birthday = forms.CharField()
gen = forms.CharField() gen = forms.CharField()
lang = forms.CharField() lang = forms.CharField()

View File

@ -30,7 +30,7 @@ weasyprint==60.2
ujson==5.9.0 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
pyroaring==0.4.5 pyroaring==0.4.5
coverage==7.4.3 coverage==7.4.3
gunicorn==21.2.0 gunicorn==21.2.0
pyvckit

View File

@ -1,73 +0,0 @@
# 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
)
```

View File

@ -1,15 +0,0 @@
{
"issuerApiUrl": "http://localhost:8080/issuer-api/default",
"issuerClientName": "PANGEA Issuer Portal",
"issuerDid": null,
"issuerUiUrl": "http://localhost:5000",
"wallets": {
"walt.id": {
"description": "walt.id web wallet",
"id": "walt.id",
"presentPath": "api/siop/initiatePresentation",
"receivePath": "api/siop/initiateIssuance",
"url": "http://localhost:3000"
}
}
}

View File

@ -1,184 +0,0 @@
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
def generate_did_controller_key():
return didkit.generate_ed25519_key()
def keydid_from_controller_key(key):
return didkit.key_to_did("key", key)
def resolve_did(keydid):
async def inner():
return await didkit.resolve_did(keydid, "{}")
return asyncio.run(inner())
def webdid_from_controller_key(key, domain):
"""
Se siguen los pasos para generar un webdid a partir de un keydid.
Documentado en la docu de spruceid.
"""
keydid = keydid_from_controller_key(key) # "did:key:<...>"
pubkeyid = keydid.rsplit(":")[-1] # <...>
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"
# Reemplazamos los campos del documento DID necesarios:
document["id"] = webdid_url
document["verificationMethod"][0]["id"] = webdid_url_owner
document["verificationMethod"][0]["controller"] = webdid_url
document["authentication"][0] = webdid_url_owner
document["assertionMethod"][0] = webdid_url_owner
document_fixed_serialized = json.dumps(document)
return webdid_url, document_fixed_serialized
def generate_generic_vc_id():
# TODO agree on a system for Verifiable Credential IDs
return "https://pangea.org/credentials/42"
def render_and_sign_credential(vc_template: jinja2.Template, jwk_issuer, vc_data: dict[str, str]):
"""
Populates a VC template with data for issuance, and signs the result with the provided key.
The `vc_data` parameter must at a minimum include:
* issuer_did
* subject_did
* vc_id
and must include whatever other fields are relevant for the vc_template to be instantiated.
The following field(s) will be auto-generated if not passed in `vc_data`:
* issuance_date (to `datetime.datetime.now()`)
"""
async def inner():
unsigned_vc = vc_template.render(vc_data)
signed_vc = await didkit.issue_credential(
unsigned_vc,
'{"proofFormat": "ldp"}',
jwk_issuer
)
return signed_vc
if vc_data.get("issuance_date") is None:
vc_data["issuance_date"] = datetime.datetime.now().replace(microsecond=0).isoformat()
return asyncio.run(inner())
def sign_credential(unsigned_vc: str, jwk_issuer):
"""
Signs the unsigned credential with the provided key.
The credential template must be rendered with all user data.
"""
async def inner():
signed_vc = await didkit.issue_credential(
unsigned_vc,
'{"proofFormat": "ldp"}',
jwk_issuer
)
return signed_vc
return asyncio.run(inner())
def verify_credential(vc):
"""
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():
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:
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:
async def inner():
unsigned_vp = vp_template.render(data)
signed_vp = await didkit.issue_presentation(
unsigned_vp,
'{"proofFormat": "ldp"}',
jwk_holder
)
return signed_vp
data = {
"holder_did": holder_did,
"verifiable_credential_list": "[" + ",".join(vc_list) + "]"
}
return asyncio.run(inner())
def create_verifiable_presentation(jwk_holder: str, unsigned_vp: str) -> str:
async def inner():
signed_vp = await didkit.issue_presentation(
unsigned_vp,
'{"proofFormat": "ldp"}',
jwk_holder
)
return signed_vp
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 await didkit.verify_presentation(vp, proof_options)
return asyncio.run(inner())