diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 8ff83b5..90a9c0c 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -114,12 +114,12 @@ class ImportForm(forms.Form): return user.first() def create_credential(self, user, row): - d = self.json_schema.copy() - d['instance'] = row return VerificableCredential( verified=False, user=user, - data=json.dumps(d) + csv_data=json.dumps(row), + issuer_did=self._did, + schema=self._schema, ) def exception(self, msg): diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 9b40faa..6ee837b 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -19,7 +19,6 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages -from utils.apiregiter import iota from utils import credtools from idhub_auth.models import User from idhub_auth.forms import ProfileForm @@ -646,7 +645,7 @@ class DidRegisterView(Credentials, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.did = iota.issue_did() + form.instance.set_did() form.save() messages.success(self.request, _('DID created successfully')) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index bbd5d49..b4d6ac7 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-14 16:32 +# Generated by Django 4.2.5 on 2023-11-15 09:58 from django.conf import settings from django.db import migrations, models @@ -13,6 +13,33 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='DID', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('created_at', models.DateTimeField(auto_now=True)), + ('label', models.CharField(max_length=50)), + ('did', models.CharField(max_length=250)), + ('key_material', models.CharField(max_length=250)), + ( + 'user', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='dids', + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), migrations.CreateModel( name='File_datas', fields=[ @@ -141,9 +168,9 @@ class Migration(migrations.Migration): ('verified', models.BooleanField()), ('created_on', models.DateTimeField(auto_now=True)), ('issued_on', models.DateTimeField(null=True)), - ('did_issuer', models.CharField(max_length=250)), - ('did_subject', models.CharField(max_length=250)), + ('subject_did', models.CharField(max_length=250)), ('data', models.TextField()), + ('csv_data', models.TextField()), ( 'status', models.PositiveSmallIntegerField( @@ -156,6 +183,22 @@ class Migration(migrations.Migration): default=1, ), ), + ( + 'issuer_did', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='vcredentials', + to='idhub.did', + ), + ), + ( + 'schema', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='vcredentials', + to='idhub.schemas', + ), + ), ( 'user', models.ForeignKey( @@ -275,32 +318,6 @@ class Migration(migrations.Migration): ), ], ), - migrations.CreateModel( - name='DID', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('created_at', models.DateTimeField(auto_now=True)), - ('did', models.CharField(max_length=250, unique=True)), - ('label', models.CharField(max_length=50)), - ( - 'user', - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name='dids', - to=settings.AUTH_USER_MODEL, - ), - ), - ], - ), migrations.CreateModel( name='UserRol', fields=[ diff --git a/idhub/models.py b/idhub/models.py index 19c42c6..4e9cf6f 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -2,7 +2,13 @@ import json import requests import datetime from django.db import models +from django.template.loader import get_template from django.utils.translation import gettext_lazy as _ +from utils.idhub_ssikit import ( + generate_did_controller_key, + keydid_from_controller_key, + sign_credential, +) from idhub_auth.models import User @@ -396,15 +402,18 @@ class Event(models.Model): class DID(models.Model): created_at = models.DateTimeField(auto_now=True) - did = models.CharField(max_length=250, unique=True) label = models.CharField(max_length=50) + did = models.CharField(max_length=250) + # In JWK format. Must be stored as-is and passed whole to library functions. + # Example key material: + # '{"kty":"OKP","crv":"Ed25519","x":"oB2cPGFx5FX4dtS1Rtep8ac6B__61HAP_RtSzJdPxqs","d":"OJw80T1CtcqV0hUcZdcI-vYNBN1dlubrLaJa0_se_gU"}' + key_material = models.CharField(max_length=250) user = models.ForeignKey( User, on_delete=models.CASCADE, related_name='dids', null=True, ) - # kind = "KEY|WEB" @property def is_organization_did(self): @@ -412,6 +421,13 @@ class DID(models.Model): return True return False + def set_did(self): + self.key_material = generate_did_controller_key() + self.did = keydid_from_controller_key(self.key_material) + + def get_key(self): + return json.loads(self.key_material) + class Schemas(models.Model): file_schema = models.CharField(max_length=250) @@ -445,9 +461,9 @@ class VerificableCredential(models.Model): verified = models.BooleanField() created_on = models.DateTimeField(auto_now=True) issued_on = models.DateTimeField(null=True) - did_issuer = models.CharField(max_length=250) - did_subject = models.CharField(max_length=250) + subject_did = models.CharField(max_length=250) data = models.TextField() + csv_data = models.TextField() status = models.PositiveSmallIntegerField( choices=Status.choices, default=Status.ENABLED @@ -457,6 +473,16 @@ class VerificableCredential(models.Model): on_delete=models.CASCADE, related_name='vcredentials', ) + issuer_did = models.ForeignKey( + DID, + on_delete=models.CASCADE, + related_name='vcredentials', + ) + schema = models.ForeignKey( + Schemas, + on_delete=models.CASCADE, + related_name='vcredentials', + ) @property def get_schema(self): @@ -474,16 +500,49 @@ class VerificableCredential(models.Model): return self.Status(self.status).label def get_datas(self): - data = json.loads(self.data).get('instance').items() + data = json.loads(self.csv_data).items() return data def issue(self, did): + if self.status == self.Status.ISSUED: + return + self.status = self.Status.ISSUED - self.did_subject = did + self.subject_did = did self.issued_on = datetime.datetime.now() + self.data = sign_credential( + self.render(), + self.issuer_did.key_material + ) + + def get_context(self): + d = json.loads(self.csv_data) + format = "%Y-%m-%dT%H:%M:%SZ" + issuance_date = self.issued_on.strftime(format) + + context = { + 'vc_id': self.id, + 'issuer_did': self.issuer_did.did, + 'subject_did': self.subject_did, + 'issuance_date': issuance_date, + } + context.update(d) + return context + + def render(self): + context = self.get_context() + template_name = 'credentials/{}'.format( + self.schema.file_schema + ) + tmpl = get_template(template_name) + return tmpl.render(context) + def get_issued_on(self): - return self.issued_on.strftime("%m/%d/%Y") + if self.issued_on: + return self.issued_on.strftime("%m/%d/%Y") + + return '' class VCTemplate(models.Model): wkit_template_id = models.CharField(max_length=250) diff --git a/idhub/templates/credentials/member.json b/idhub/templates/credentials/member.json new file mode 100644 index 0000000..e98bef2 --- /dev/null +++ b/idhub/templates/credentials/member.json @@ -0,0 +1,32 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "type": ["VerifiableCredential", "VerifiableAttestation"], + "id": "{{ vc_id }}", + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "Member": { + "name": "{{ name }}", + "email": "{{ email }}", + "membershipType": "{{ membershipType }}", + "startDate": "{{ startDate }}" + }, + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/member-schema.json" + } + } +} \ No newline at end of file diff --git a/idhub/user/views.py b/idhub/user/views.py index 6c73df3..3dffbc9 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -12,7 +12,6 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages -from utils.apiregiter import iota from idhub.user.forms import ProfileForm, RequestCredentialForm, CredentialPresentationForm from idhub.mixins import UserView from idhub.models import DID, VerificableCredential, Event @@ -190,7 +189,7 @@ class DidRegisterView(MyWallet, CreateView): def form_valid(self, form): form.instance.user = self.request.user - form.instance.did = iota.issue_did() + form.instance.set_did() form.save() messages.success(self.request, _('DID created successfully')) diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 46412dd..d40f0a4 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-14 16:32 +# Generated by Django 4.2.5 on 2023-11-15 09:58 from django.db import migrations, models diff --git a/requirements.txt b/requirements.txt index 03a9c58..745c483 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,7 @@ python-decouple==3.8 jsonschema==4.19.1 pandas==2.1.1 requests==2.31.0 +didkit==0.3.2 +jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 diff --git a/schemas/member-schema.json b/schemas/member-schema.json deleted file mode 100644 index 38199be..0000000 --- a/schemas/member-schema.json +++ /dev/null @@ -1,21 +0,0 @@ - { - "$id": "https://pangea.org/schemas/member-credential-schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "name": "MemberCredential", - "description": "MemberCredential using JsonSchemaCredential", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "membershipType": { - "type": "string", - "enum": ["individual", "organization"] - } - }, - "required": ["name", "email", "membershipType"] - } diff --git a/schemas/member.json b/schemas/member.json index 845ab50..38199be 100644 --- a/schemas/member.json +++ b/schemas/member.json @@ -1,21 +1,4 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://www.w3.org/ns/credentials/examples/v2" - ], - "id": "https://example.com/credentials/3734", - "type": ["VerifiableCredential", "JsonSchemaCredential"], - "issuer": "https://pangea.org/issuers/10", - "issuanceDate": "2023-09-01T19:23:24Z", - "credentialSchema": { - "id": "https://www.w3.org/2022/credentials/v2/json-schema-credential-schema.json", - "type": "JsonSchema", - "digestSRI": "sha384-S57yQDg1MTzF56Oi9DbSQ14u7jBy0RDdx0YbeV7shwhCS88G8SCXeFq82PafhCrW" - }, - "credentialSubject": { - "id": "https://pangea.org/schemas/member-credential-schema.json", - "type": "JsonSchema", - "jsonSchema": { + { "$id": "https://pangea.org/schemas/member-credential-schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "name": "MemberCredential", @@ -36,5 +19,3 @@ }, "required": ["name", "email", "membershipType"] } - } -} diff --git a/trustchain_walletkit/__init__.py b/trustchain_walletkit/__init__.py deleted file mode 100644 index 02d0144..0000000 --- a/trustchain_walletkit/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -from pathlib import Path - -import requests -import json - -WALLETKITD = 'http://localhost:8080/' -ISSUER = f'{WALLETKITD}issuer-api/default/' -VERIFIER = f'{WALLETKITD}verifier-api/default/' - -default_ctype_header = { - 'Content-Type': 'application/json', # specify the type of data you're sending - 'Accept': 'application/json', # specify the type of data you can accept -} - - -def include_str(path): - with open(path, "r") as f: - return f.read().strip() - - -# Create DID for tenant -# Valid methods: 'key'|'web' -def user_create_did(did_method): - url = f'{ISSUER}config/did/create' - data = { - 'method': did_method - } - response = requests.post(url, json=data, headers=default_ctype_header) - response.raise_for_status() - return response.text - - -def admin_create_template(template_name, template_body): - url = f'{ISSUER}config/templates/{template_name}' - body = template_body - response = requests.post(url, data=body, headers=default_ctype_header) - response.raise_for_status() - return - - -def user_issue_vc(vc_name, vc_params): - url = f'{ISSUER}credentials/issuance/request' - # ... - # TODO examine cross-device issuance workflow - pass - - - - - -TENANT_CFG_TMEPLATE = include_str("./TENANT_CFG_TEMPLATE") - diff --git a/utils/apiregiter.py b/utils/apiregiter.py deleted file mode 100644 index e794181..0000000 --- a/utils/apiregiter.py +++ /dev/null @@ -1,17 +0,0 @@ -import uuid -import hashlib - - -class Iota: - """ - Framework for simulate the comunication with IOTA DLT - """ - - def issue_did(self): - u = str(uuid.uuid4()).encode() - d = hashlib.sha3_256(u).hexdigest() - did = "did:iota:{}".format(d) - return did - - -iota = Iota() diff --git a/utils/idhub_ssikit/README.md b/utils/idhub_ssikit/README.md new file mode 100644 index 0000000..e69de29 diff --git a/trustchain_walletkit/TENANT_CFG_TEMPLATE b/utils/idhub_ssikit/TENANT_CFG_TEMPLATE similarity index 100% rename from trustchain_walletkit/TENANT_CFG_TEMPLATE rename to utils/idhub_ssikit/TENANT_CFG_TEMPLATE diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py new file mode 100644 index 0000000..18a5ff2 --- /dev/null +++ b/utils/idhub_ssikit/__init__.py @@ -0,0 +1,74 @@ +import asyncio +import datetime +import didkit +import json +import jinja2 +from django.template.backends.django import Template + + +def generate_did_controller_key(): + return didkit.generate_ed25519_key() + + +def keydid_from_controller_key(key): + return didkit.key_to_did("key", key) + + +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 and unsigned credential with the provided key. + """ + 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, proof_options): + """ + 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(): + return didkit.verify_credential(vc, proof_options) + + return asyncio.run(inner())