Digitale handtekening en documentauthenticatie bij Scala
In 2024 overschreed de mondiale markt voor elektronische handtekeningen de $5 miljard, met een verwachte jaarlijkse groei van ruim 30% tot 2030. Maar afgezien van de marktcijfers de massale adoptie van digitale handtekeningen brengt concrete technische uitdagingen met zich mee: hoe miljoenen te beheren aantal handtekeningen per dag met behoud van de rechtsgeldigheid in meerdere rechtsgebieden? Hoe implementeer je een schaalbaar PKI-systeem dat voldoet aan eIDAS 2.0 in Europa en gelijkwaardige regelgeving in de rest van de wereld?
In dit artikel bouwen we een digitaal handtekeningsysteem op bedrijfsniveau: from genereren en beheren van PKI-certificaten, implementatie van handtekeningworkflows meerstaps, tot RFC 3161-compatibele tijdstempels en archivering op lange termijn volgens Europese normen. De code is in Python en TypeScript, met integratievoorbeelden voor hoekige toepassingen.
Wat je gaat leren
- Soorten elektronische handtekeningen volgens eIDAS 2.0 (SES, AES, QES)
- PKI-architectuur: CA, RA, X.509-certificaten, CRL en OCSP
- Implementatie van PDF-handtekeningen met PyHanko (Python)
- RFC 3161-compatibele tijdstempel voor bewijs van bestaan
- Handtekeningworkflow voor meerdere partijen met machinestatus
- Hoekige integratie met DocuSign/Adobe Sign API
- Lange termijn archivering (LTV - Lange Termijn Validatie)
De elektronische handtekeningniveaus volgens eIDAS 2.0
De eIDAS-verordening (EU) 2024/1183 – die op 20 mei 2024 in werking is getreden – definieert drie niveaus van elektronische handtekeningen met verschillende beveiligingsvereisten en juridische waarde:
| Type | Acroniem | Vereisten | Juridische waarde | Gebruiksgeval |
|---|---|---|---|---|
| Eenvoudige elektronische handtekening | SES | Alle elektronische gegevens die verband houden met de ondertekenaar | Bas | Click-wrap, e-mailgoedkeuring |
| Geavanceerde elektronische handtekening | AES | Uniek verwijzend naar de ondertekenaar, gemaakt met gegevens onder zijn controle | Medium | Commerciële contracten, HR |
| Gekwalificeerde elektronische handtekening | QES | Gekwalificeerd certificaat + QSCD (gekwalificeerd apparaat voor het maken van handtekeningen) | Gelijkwaardige handgeschreven handtekening | Notariële akten, vastgoedcontracten |
eIDAS 2.0 en de Digitale Identiteitsportemonnee
Vanaf december 2026 moeten alle 27 EU-lidstaten burgers voorzien van een EU-portemonnee voor digitale identiteit (EUDI-portemonnee) die QES-handtekeningen mogelijk maakt op mobiel apparaat. Dit zal de onboarding van gebruikers fundamenteel veranderen voor digitale handtekeningsystemen: vaarwel tegen USB-hardwaretokens, hallo tegen smartphones.
PKI-architectuur voor digitale handtekening
Een Public Key Infrastructure (PKI) en de cryptografische infrastructuur die deze beveiligt de authenticiteit en integriteit van digitale handtekeningen. De fundamentele componenten zijn:
- Root CA (Certificaatautoriteit): het vertrouwde anker van de hiërarchie. Geeft tussentijdse certificaten uit. Moet offline zijn (air-gapped) voor maximale beveiliging.
- Tussenliggende CA: Operationele CA die certificaten uitgeeft aan eindgebruikers.
- RA (Registratieautoriteit): verifieert de identiteit van de aanvrager alvorens de afgifte van het certificaat te autoriseren.
- OCSP-responders: real-time service om te verifiëren of een certificaat aanwezig is en ingetrokken (alternatief voor CRL).
- TSA (tijdstempelautoriteit): voert RFC 3161 gekwalificeerde tijdstempels uit.
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
import datetime
import uuid
class PKIManager:
"""
Gestore PKI per la creazione di certificati X.509 self-signed e firmati da CA.
Per uso in sviluppo/test. In produzione usare una CA qualificata eIDAS.
"""
def generate_key_pair(self, key_size: int = 4096):
"""Genera coppia di chiavi RSA."""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend()
)
return private_key, private_key.public_key()
def create_root_ca(self, subject_name: str, validity_years: int = 20):
"""
Crea un certificato Root CA self-signed.
Normalmente questa operazione viene eseguita offline.
"""
private_key, public_key = self.generate_key_pair()
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "IT"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject_name),
x509.NameAttribute(NameOID.COMMON_NAME, f"{subject_name} Root CA"),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=365 * validity_years)
)
.add_extension(
x509.BasicConstraints(ca=True, path_length=1),
critical=True
)
.add_extension(
x509.KeyUsage(
digital_signature=True, key_cert_sign=True,
crl_sign=True, content_commitment=False,
key_encipherment=False, data_encipherment=False,
key_agreement=False, encipher_only=False, decipher_only=False
),
critical=True
)
.sign(private_key, hashes.SHA256(), default_backend())
)
return cert, private_key
def create_end_entity_certificate(
self,
subject_cn: str,
subject_email: str,
ca_cert,
ca_private_key,
validity_days: int = 365
):
"""
Crea un certificato end-entity firmato dalla CA.
Usato per la firma digitale dei documenti.
"""
user_private_key, user_public_key = self.generate_key_pair(key_size=2048)
subject = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "IT"),
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, subject_email),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(ca_cert.subject)
.public_key(user_public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=validity_days)
)
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True
)
.add_extension(
x509.ExtendedKeyUsage([
ExtendedKeyUsageOID.EMAIL_PROTECTION,
# OID per firma documenti: 1.2.840.113549.1.9.15 (non standard)
]),
critical=False
)
.add_extension(
x509.SubjectAlternativeName([
x509.RFC822Name(subject_email),
]),
critical=False
)
.sign(ca_private_key, hashes.SHA256(), default_backend())
)
return cert, user_private_key
Onderteken PDF met PyHanko
PyHanko is de referentie-Python-bibliotheek voor het digitaal ondertekenen van PDF-documenten volgens PDF/A- en PAdES-standaarden (PDF Advanced Electronic Signature). Ondersteuning onzichtbare, zichtbare handtekeningen, interactieve handtekeningvelden en geïntegreerde tijdstempels.
from pyhanko.sign import signers, fields
from pyhanko.sign.fields import MDPPerm
from pyhanko import stamp
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign.timestamps import HTTPTimeStamper
from pyhanko.sign.validation import validate_pdf_signature
from pyhanko.pdf_utils.reader import PdfFileReader
import hashlib
from io import BytesIO
class PDFSigningService:
"""
Servizio di firma PDF con supporto PAdES e timestamping RFC 3161.
"""
def __init__(
self,
cert_pem_path: str,
key_pem_path: str,
ca_chain_pem_path: str,
tsa_url: str = "http://timestamp.digicert.com"
):
# Carica certificato e chiave privata
self.signer = signers.SimpleSigner.load(
cert_file=cert_pem_path,
key_file=key_pem_path,
ca_chain_files=[ca_chain_pem_path]
)
self.timestamper = HTTPTimeStamper(tsa_url)
def sign_document(
self,
input_pdf_bytes: bytes,
reason: str = "Approvazione contratto",
location: str = "Milano, Italia",
visible: bool = True,
page: int = 0,
add_timestamp: bool = True
) -> bytes:
"""
Firma un documento PDF e opzionalmente aggiunge un timestamp qualificato.
Restituisce il PDF firmato come bytes.
"""
writer = IncrementalPdfFileWriter(BytesIO(input_pdf_bytes))
if visible:
# Crea campo firma visibile nell'angolo in basso a destra dell'ultima pagina
fields.append_signature_field(
writer,
sig_field_spec=fields.SigFieldSpec(
sig_field_name="Signature1",
on_page=page,
box=(400, 50, 560, 110) # (x1, y1, x2, y2) in punti
)
)
# Configurazione del digest e firma
meta = signers.PdfSignatureMetadata(
field_name="Signature1",
reason=reason,
location=location,
certify=True,
certify_perm=MDPPerm.NO_CHANGES # impedisce modifiche post-firma
)
sign_result = signers.sign_pdf(
writer,
signature_meta=meta,
signer=self.signer,
timestamper=self.timestamper if add_timestamp else None,
in_place=False
)
return sign_result.getvalue()
def validate_signature(self, signed_pdf_bytes: bytes) -> dict:
"""
Valida tutte le firme in un documento PDF.
Restituisce un report strutturato per ogni firma.
"""
reader = PdfFileReader(BytesIO(signed_pdf_bytes))
validation_results = []
for sig_obj in reader.embedded_signatures:
val_status = validate_pdf_signature(sig_obj)
validation_results.append({
'field_name': sig_obj.field_name,
'signer_name': str(val_status.signing_cert.subject),
'signing_time': str(val_status.signer_reported_dt),
'timestamp_valid': val_status.timestamp_validity.valid if val_status.timestamp_validity else None,
'cert_valid': val_status.signing_cert_validity.valid,
'modification_on_unchanged': val_status.modification_level.name,
'intact': val_status.bottom_line # True = firma integra e valida
})
return {
'total_signatures': len(validation_results),
'all_valid': all(r['intact'] for r in validation_results),
'signatures': validation_results,
'document_hash_sha256': hashlib.sha256(signed_pdf_bytes).hexdigest()
}
Workflow voor handtekeningen tussen meerdere partijen met State Machine
In praktijkscenario's vereist een contract vaak dat meerdere partijen in één order ondertekenen precies: eerst de interne juridische manager, dan de cliënt, ten slotte de notaris. We implementeren een state-machine om deze workflow robuust af te handelen.
from enum import Enum, auto
from dataclasses import dataclass, field
from typing import List, Optional, Callable
from datetime import datetime
import uuid
class SignatureStatus(Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
REJECTED = "rejected"
EXPIRED = "expired"
@dataclass
class SignatureRequest:
"""Una richiesta di firma per un singolo firmatario."""
request_id: str
signer_email: str
signer_name: str
order: int # ordine di firma (1, 2, 3...)
status: SignatureStatus = SignatureStatus.PENDING
signed_at: Optional[datetime] = None
rejection_reason: Optional[str] = None
@dataclass
class SigningWorkflow:
"""
Workflow di firma multi-party con ordinamento sequenziale.
"""
workflow_id: str
document_id: str
document_name: str
created_at: datetime
expires_at: datetime
signers: List[SignatureRequest]
current_signer_order: int = 1
status: SignatureStatus = SignatureStatus.IN_PROGRESS
audit_log: List[dict] = field(default_factory=list)
def get_current_signer(self) -> Optional[SignatureRequest]:
"""Restituisce il firmatario corrente."""
for signer in self.signers:
if signer.order == self.current_signer_order:
return signer
return None
def record_signature(
self,
signer_email: str,
signed_pdf_bytes: bytes,
validation_report: dict
) -> bool:
"""
Registra una firma e avanza il workflow al prossimo firmatario.
Returns True se il workflow e completato.
"""
current = self.get_current_signer()
if not current or current.signer_email != signer_email:
raise ValueError(f"Non e il turno di {signer_email} di firmare")
if not validation_report.get('all_valid'):
raise ValueError("Firma non valida secondo il report di validazione")
# Aggiorna stato del firmatario
current.status = SignatureStatus.COMPLETED
current.signed_at = datetime.utcnow()
# Log immutabile dell'evento
self.audit_log.append({
'event': 'signature_recorded',
'signer': signer_email,
'order': self.current_signer_order,
'timestamp': datetime.utcnow().isoformat(),
'doc_hash': validation_report.get('document_hash_sha256')
})
# Avanza al prossimo firmatario
self.current_signer_order += 1
next_signer = self.get_current_signer()
if next_signer is None:
# Tutti hanno firmato: workflow completato
self.status = SignatureStatus.COMPLETED
return True
# Notifica il prossimo firmatario
next_signer.status = SignatureStatus.IN_PROGRESS
return False
def reject(self, signer_email: str, reason: str):
"""Il firmatario corrente rifiuta di firmare."""
current = self.get_current_signer()
if current and current.signer_email == signer_email:
current.status = SignatureStatus.REJECTED
current.rejection_reason = reason
self.status = SignatureStatus.REJECTED
self.audit_log.append({
'event': 'signature_rejected',
'signer': signer_email,
'reason': reason,
'timestamp': datetime.utcnow().isoformat()
})
# Factory per creare workflow
def create_signing_workflow(
document_id: str,
document_name: str,
signers_ordered: List[dict],
validity_days: int = 30
) -> SigningWorkflow:
"""
Crea un workflow di firma dal documento e dalla lista di firmatari.
signers_ordered: [{'email': '...', 'name': '...'}, ...] in ordine di firma
"""
requests = [
SignatureRequest(
request_id=str(uuid.uuid4()),
signer_email=s['email'],
signer_name=s['name'],
order=idx + 1
)
for idx, s in enumerate(signers_ordered)
]
requests[0].status = SignatureStatus.IN_PROGRESS # Primo firmatario attivo
return SigningWorkflow(
workflow_id=str(uuid.uuid4()),
document_id=document_id,
document_name=document_name,
created_at=datetime.utcnow(),
expires_at=datetime.utcnow().replace(
day=datetime.utcnow().day + validity_days
),
signers=requests
)
Hoekige integratie met Signature API
Vanaf de Angular frontend-kant moet de integratie met een handtekeningservice afgehandeld worden de omleidingsstroom (de gebruiker wordt naar het ondertekenplatform gebracht en keert vervolgens terug), of directe insluiting via iframe/SDK.
// signature.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface SigningSession {
sessionId: string;
signingUrl: string; // URL redirect alla piattaforma di firma
expiresAt: string;
documentId: string;
}
export interface SignatureStatus {
workflowId: string;
status: 'pending' | 'in_progress' | 'completed' | 'rejected';
completedSigners: number;
totalSigners: number;
nextSigner?: string;
completedAt?: string;
}
@Injectable({ providedIn: 'root' })
export class SignatureService {
private readonly http = inject(HttpClient);
private readonly apiBase = '/api/v1/signatures';
initiateSignature(documentId: string, signerEmail: string): Observable<SigningSession> {
return this.http.post<SigningSession>(
`{this.apiBase}/sessions`,
{ documentId, signerEmail }
);
}
getWorkflowStatus(workflowId: string): Observable<SignatureStatus> {
return this.http.get<SignatureStatus>(
`{this.apiBase}/workflows/{workflowId}/status`
);
}
downloadSignedDocument(workflowId: string): Observable<Blob> {
return this.http.get(
`{this.apiBase}/workflows/{workflowId}/document`,
{ responseType: 'blob' }
);
}
}
// document-signing.component.ts
import { Component, input, inject, signal } from '@angular/core';
import { SignatureService, SignatureStatus } from './signature.service';
@Component({
selector: 'app-document-signing',
template: `
<div class="signing-container">
@if (status() === 'idle') {
<button (click)="startSigning()">Firma il Documento</button>
}
@if (status() === 'loading') {
<div class="spinner">Preparazione firma in corso...</div>
}
@if (status() === 'completed') {
<div class="success">
<p>Documento firmato con successo!</p>
<button (click)="downloadSigned()">Scarica PDF firmato</button>
</div>
}
</div>
`
})
export class DocumentSigningComponent {
documentId = input.required<string>();
workflowId = input.required<string>();
private sigService = inject(SignatureService);
status = signal<'idle' | 'loading' | 'redirect' | 'completed' | 'error'>('idle');
startSigning(): void {
this.status.set('loading');
this.sigService.initiateSignature(this.documentId(), 'user@example.com').subscribe({
next: (session) => {
// Redirect alla piattaforma di firma (DocuSign, YouSign, etc.)
window.location.href = session.signingUrl;
},
error: () => this.status.set('error')
});
}
downloadSigned(): void {
this.sigService.downloadSignedDocument(this.workflowId()).subscribe(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'documento-firmato.pdf';
a.click();
URL.revokeObjectURL(url);
});
}
}
Langetermijnvalidatie (LTV) en archivering
Een digitale handtekening moet niet alleen vandaag de dag, maar ook over tien of twintig jaar verifieerbaar zijn. wanneer de originele certificaten mogelijk zijn verlopen of het cryptografische algoritme verouderd. Daar Langetermijnvalidatie (LTV) lost dit probleem op het opnemen van alle informatie die nodig is voor validatie in het ondertekende document toekomst: certificaatketen, OCSP-reacties en tijdstempels.
Normen voor archivering op lange termijn
- PAdES-LTV: PDF-handtekening met certificaatinsluiting en OCSP-antwoord
- XAdES-A: XML-handtekening met periodieke archieftijdstempels
- CADES-A: CMS-handtekening met archieftijdstempels
- ASiC-E: ZIP-container met handtekening + document + metadata
Voor juridische documenten met een geldigheidsduur van meer dan 10 jaar wordt een nieuwe tijdstempel aanbevolen periodiek (elke 5 jaar) om de cryptografische sterkte bij te werken voordat SHA-256 wordt verouderd.
Beveiligingsoverwegingen
Kritieke veiligheidspunten
- Beveiliging van privésleutels: genereer of bewaar nooit privésleutels in de applicatiecode. Gebruik HSM (Hardware Security Module) of cloud KMS (AWS KMS, Azure Key Vault, Google Cloud KMS).
- Intrekking van certificaten: implementeer OCSP-nieten om dit te voorkomen controleer OCSP in realtime bij elke handtekening: cruciaal voor de prestaties.
- Onveranderlijk auditlogboek: elke workflowgebeurtenis (handtekening, afwijzing, vervaldatum) moet worden geschreven naar een alleen-toevoegen-logboek met hash-keten om manipulatie te detecteren.
- Validatie van document vóór ondertekening: Controleer of de PDF dit niet bevat Ingebedde JavaScript- of interactieve formulieren die de weergegeven inhoud kunnen wijzigen.
Conclusies
Het implementeren van een schaalbaar, rechtsgeldig systeem voor digitale handtekeningen vergt veel meer dan simpelweg "een handtekening toevoegen" aan een PDF. Beheer van de levenscyclus PKI, eIDAS 2.0-compliance, workflows met meerdere partijen, tijdstempels en lange termijn Validatie zijn componenten die vanaf het begin samen moeten worden ontworpen.
De code in dit artikel biedt een solide basis voor het bouwen van een systeem productie. Voor kritische toepassingen (notarisakten, vastgoedcontracten, documenten bedrijven), overweeg dan integratie met gekwalificeerde TSP’s (Trust Service Providers). zoals Namirial, InfoCert, Aruba of DocuSign – die de complexiteit van de regelgeving beheren van eIDAS 2.0 voor u.
LegalTech- en AI-serie
- NLP voor Contractanalyse: van OCR tot Begrijpen
- e-Discovery Platform-architectuur
- Compliance-automatisering met Dynamic Rules Engine
- Slim contract voor juridische overeenkomsten: Soliditeit en Vyper
- Samenvatten van juridische documenten met generatieve AI
- Zoekmachinewet: vectorinbedding
- Digitale handtekening en documentauthenticatie op Scala (dit artikel)
- Systemen voor gegevensprivacy en AVG-naleving
- Een juridische AI-assistent bouwen (juridische copiloot)
- LegalTech-gegevensintegratiepatroon







