From c836112f362f3326eef6ea64cc3c1874971fe432 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 10 Jan 2024 13:53:43 +0100 Subject: [PATCH] sig credential in pdf --- idhub/admin/forms.py | 69 ++++++++++++++++++- idhub/admin/views.py | 19 ++++- idhub/migrations/0001_initial.py | 18 +++-- idhub/models.py | 3 +- .../templates/idhub/admin/wallet_issues.html | 23 +++++++ idhub/user/views.py | 65 +++++++++++++---- idhub_auth/migrations/0001_initial.py | 18 +++-- oidc4vp/migrations/0001_initial.py | 6 +- promotion/migrations/0001_initial.py | 2 +- utils/certs.py | 36 ++++++++++ 10 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 utils/certs.py diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 2649f4b..ab2f3db 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -1,11 +1,14 @@ import csv import json +import base64 import pandas as pd +from pyhanko.sign import signers + from django import forms from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError -from utils import credtools +from utils import credtools, certs from idhub.models import ( DID, File_datas, @@ -216,3 +219,67 @@ class UserRolForm(forms.ModelForm): raise forms.ValidationError(msg) return data['service'] + + +class ImportCertificateForm(forms.Form): + label = forms.CharField(label=_("Label")) + password = forms.CharField( + label=_("Password of certificate"), + widget=forms.PasswordInput + ) + file_import = forms.FileField(label=_("File import")) + + def __init__(self, *args, **kwargs): + self._did = None + self._s = None + self._label = None + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + def clean(self): + data = super().clean() + # import pdb; pdb.set_trace() + file_import = data.get('file_import') + self.pfx_file = file_import.read() + self.file_name = file_import.name + self._pss = data.get('password') + self._label = data.get('label') + if not self.pfx_file or not self._pss: + msg = _("Is not a valid certificate") + raise forms.ValidationError(msg) + + self.signer_init() + if not self._s: + msg = _("Is not a valid certificate") + raise forms.ValidationError(msg) + + self.new_did() + return data + + def new_did(self): + cert = self.pfx_file + keys = { + "cert": base64.b64encode(self.pfx_file).decode('utf-8'), + "passphrase": self._pss + } + key_material = json.dumps(keys) + self._did = DID( + key_material=key_material, + did=self.file_name, + label=self._label, + eidas1=True, + user=self.user + ) + + def save(self, commit=True): + + if commit: + self._did.save() + return self._did + + return + + def signer_init(self): + self._s = certs.load_cert( + self.pfx_file, self._pss.encode('utf-8') + ) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index b6dcbc8..7f385ab 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -30,6 +30,7 @@ from idhub.admin.forms import ( MembershipForm, SchemaForm, UserRolForm, + ImportCertificateForm, ) from idhub.admin.tables import ( DashboardTable @@ -695,11 +696,27 @@ class WalletCredentialsView(Credentials): wallet = True -class WalletConfigIssuesView(Credentials): +class WalletConfigIssuesView(Credentials, FormView): template_name = "idhub/admin/wallet_issues.html" subtitle = _('Configure credential issuance') icon = 'bi bi-patch-check-fill' wallet = True + form_class = ImportCertificateForm + success_url = reverse_lazy('idhub:admin_dids') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + cred = form.save() + if cred: + messages.success(self.request, _("The credential was imported successfully!")) + Event.set_EV_ORG_DID_CREATED_BY_ADMIN(cred) + else: + messages.error(self.request, _("Error importing the credential!")) + return super().form_valid(form) class SchemasView(SchemasMix): diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 6d87ae7..6982654 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-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.conf import settings from django.db import migrations, models @@ -26,9 +26,10 @@ class Migration(migrations.Migration): ), ), ('created_at', models.DateTimeField(auto_now=True)), - ('label', models.CharField(max_length=50)), + ('label', models.CharField(max_length=50, verbose_name='Label')), ('did', models.CharField(max_length=250)), - ('key_material', models.CharField(max_length=250)), + ('key_material', models.TextField()), + ('eidas1', models.BooleanField(default=False)), ( 'user', models.ForeignKey( @@ -256,8 +257,11 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), - ('created', models.DateTimeField(auto_now=True)), - ('message', models.CharField(max_length=350)), + ('created', models.DateTimeField(auto_now=True, verbose_name='Date')), + ( + 'message', + models.CharField(max_length=350, verbose_name='Description'), + ), ( 'type', models.PositiveSmallIntegerField( @@ -295,7 +299,8 @@ class Migration(migrations.Migration): (28, 'Organisational DID deleted by admin'), (29, 'User deactivated'), (30, 'User activated'), - ] + ], + verbose_name='Event', ), ), ( @@ -327,6 +332,7 @@ class Migration(migrations.Migration): on_delete=django.db.models.deletion.CASCADE, related_name='users', to='idhub.service', + verbose_name='Service', ), ), ( diff --git a/idhub/models.py b/idhub/models.py index 7d1ef6c..d76ea41 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -409,7 +409,8 @@ class DID(models.Model): # 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) + key_material = models.TextField() + eidas1 = models.BooleanField(default=False) user = models.ForeignKey( User, on_delete=models.CASCADE, diff --git a/idhub/templates/idhub/admin/wallet_issues.html b/idhub/templates/idhub/admin/wallet_issues.html index f5849fd..b14df56 100644 --- a/idhub/templates/idhub/admin/wallet_issues.html +++ b/idhub/templates/idhub/admin/wallet_issues.html @@ -6,4 +6,27 @@ {{ subtitle }} +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} + + +
{% endblock %} diff --git a/idhub/user/views.py b/idhub/user/views.py index fe6bb60..906cf43 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -1,7 +1,9 @@ import os +import json import base64 import qrcode import logging +import datetime import weasyprint import qrcode.image.svg @@ -28,6 +30,7 @@ from idhub.user.forms import ( RequestCredentialForm, DemandAuthorizationForm ) +from utils import certs from idhub.mixins import UserView from idhub.models import DID, VerificableCredential, Event @@ -130,17 +133,16 @@ class CredentialJsonView(MyWallet, TemplateView): _pss = '123456' def get(self, request, *args, **kwargs): - # pk = kwargs['pk'] - # self.object = get_object_or_404( - # VerificableCredential, - # pk=pk, - # user=self.request.user - # ) - # return self.render_to_response(context=self.get_context_data()) + pk = kwargs['pk'] + self.user = self.request.user + self.object = get_object_or_404( + VerificableCredential, + pk=pk, + user=self.request.user + ) data = self.build_certificate() doc = self.insert_signature(data) - import pdb; pdb.set_trace() response = HttpResponse(doc, content_type="application/pdf") response['Content-Disposition'] = 'attachment; filename={}'.format(self.file_name) return response @@ -159,10 +161,31 @@ class CredentialJsonView(MyWallet, TemplateView): img_head = base64.b64encode(_f.read()).decode('utf-8') qr = self.generate_qr_code("http://localhost") + if DID.objects.filter(eidas1=True).exists(): + qr = "" + + first_name = self.user.first_name and self.user.first_name.upper() or "" + last_name = self.user.first_name and self.user.last_name.upper() or "" + document_id = "0000000-L" + course = "COURSE 1" + address = "ADDRESS" + date_course = datetime.datetime.now() + n_hours = 40 + n_lections = 5 + issue_date = datetime.datetime.now() context.update({ - # 'object': self.object, + 'object': self.object, "image_signature": img_sig, "image_header": img_head, + "first_name": first_name, + "last_name": last_name, + "document_id": document_id, + "course": course, + "address": address, + "date_course": date_course, + "n_hours": n_hours, + "n_lections": n_lections, + "issue_date": issue_date, "qr": qr }) return context @@ -188,17 +211,31 @@ class CredentialJsonView(MyWallet, TemplateView): return base64.b64encode(img_buffer.getvalue()).decode('utf-8') + def get_pfx_data(self): + did = DID.objects.filter(eidas1=True).first() + if not did: + return None, None + key_material = json.loads(did.key_material) + cert = key_material.get("cert") + passphrase = key_material.get("passphrase") + if cert and passphrase: + return base64.b64decode(cert), passphrase.encode('utf-8') + return None, None + + def signer_init(self): - fname = "examples/signerDNIe004.pfx" - # pfx_buffer = BytesIO() - pfx_file= next(Path.cwd().glob(fname)) - s = signers.SimpleSigner.load_pkcs12( - pfx_file=pfx_file, passphrase=self._pss.encode('utf-8') + pfx_data, passphrase = self.get_pfx_data() + s = certs.load_cert( + pfx_data, passphrase ) return s def insert_signature(self, doc): + # import pdb; pdb.set_trace() sig = self.signer_init() + if not sig: + return + _buffer = BytesIO() _buffer.write(doc) w = IncrementalPdfFileWriter(_buffer) diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index f460a62..41a937b 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-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.db import migrations, models @@ -31,13 +31,23 @@ class Migration(migrations.Migration): ( 'email', models.EmailField( - max_length=255, unique=True, verbose_name='email address' + max_length=255, unique=True, verbose_name='Email address' ), ), ('is_active', models.BooleanField(default=True)), ('is_admin', models.BooleanField(default=False)), - ('first_name', models.CharField(blank=True, max_length=255, null=True)), - ('last_name', models.CharField(blank=True, max_length=255, null=True)), + ( + 'first_name', + models.CharField( + blank=True, max_length=255, null=True, verbose_name='First name' + ), + ), + ( + 'last_name', + models.CharField( + blank=True, max_length=255, null=True, verbose_name='Last name' + ), + ), ], options={ 'abstract': False, diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 700c4e8..55792e2 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.conf import settings from django.db import migrations, models @@ -30,7 +30,7 @@ class Migration(migrations.Migration): 'code', models.CharField(default=oidc4vp.models.set_code, max_length=24), ), - ('code_used', models.BooleanField()), + ('code_used', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now=True)), ('presentation_definition', models.CharField(max_length=250)), ], @@ -91,7 +91,7 @@ class Migration(migrations.Migration): models.ForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='oauth2vptoken', + related_name='vp_tokens', to='oidc4vp.authorization', ), ), diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py index cbc1f17..5373e75 100644 --- a/promotion/migrations/0001_initial.py +++ b/promotion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-11 08:35 +# Generated by Django 4.2.5 on 2024-01-10 11:52 from django.db import migrations, models import django.db.models.deletion diff --git a/utils/certs.py b/utils/certs.py new file mode 100644 index 0000000..ff2c241 --- /dev/null +++ b/utils/certs.py @@ -0,0 +1,36 @@ +from pyhanko.sign.signers import SimpleSigner +from cryptography.hazmat.primitives.serialization import pkcs12 +from pyhanko_certvalidator.registry import SimpleCertificateStore +from pyhanko.keys import _translate_pyca_cryptography_cert_to_asn1 +from pyhanko.keys import _translate_pyca_cryptography_key_to_asn1 + + +def load_cert(pfx_bytes, passphrase): + try: + ( + private_key, + cert, + other_certs_pkcs12, + ) = pkcs12.load_key_and_certificates(pfx_bytes, passphrase) + except (IOError, ValueError, TypeError) as e: + # logger.error( + # 'Could not load key material from PKCS#12 file', exc_info=e + # ) + return None + + kinfo = _translate_pyca_cryptography_key_to_asn1(private_key) + cert = _translate_pyca_cryptography_cert_to_asn1(cert) + other_certs_pkcs12 = set( + map(_translate_pyca_cryptography_cert_to_asn1, other_certs_pkcs12) + ) + + cs = SimpleCertificateStore() + certs_to_register = set(other_certs_pkcs12) + cs.register_multiple(certs_to_register) + return SimpleSigner( + signing_key=kinfo, + signing_cert=cert, + cert_registry=cs, + signature_mechanism=None, + prefer_pss=False, + )