Semnătura digitală și autentificarea documentelor la Scala
În 2024, piața globală a semnăturilor electronice a depășit 5 miliarde de dolari, cu o creștere anuală estimată de peste 30% până în 2030. Dar dincolo de cifrele pieței, adoptarea masivă a semnăturilor digitale ridică provocări tehnice concrete: cum să gestionezi milioane de semnături pe zi, menținând în același timp valabilitatea legală în mai multe jurisdicții? Cum se implementează a sistem scalabil PKI care respectă eIDAS 2.0 în Europa și reglementările echivalente în restul lumii?
În acest articol construim un sistem de semnătură digitală de nivel enterprise: de la generarea și gestionarea certificatelor PKI, implementarea fluxurilor de lucru pentru semnături marcare temporală în mai multe etape, până la RFC 3161 și arhivare pe termen lung conform standardelor europene. Codul este în Python și TypeScript, cu exemple de integrare pentru aplicații unghiulare.
Ce vei învăța
- Tipuri de semnătură electronică conform eIDAS 2.0 (SES, AES, QES)
- Arhitectură PKI: certificate CA, RA, X.509, CRL și OCSP
- Implementarea semnăturii PDF cu PyHanko (Python)
- Marcare temporală compatibilă cu RFC 3161 pentru dovada existenței
- Flux de lucru cu semnături multipartite cu starea mașinii
- Integrare unghiulară cu API-ul DocuSign/Adobe Sign
- Arhivare pe termen lung (LTV - Long-Term Validation)
Nivelurile semnăturii electronice conform eIDAS 2.0
Regulamentul eIDAS (UE) 2024/1183 - care a intrat în vigoare la 20 mai 2024 - definește trei niveluri de semnătură electronică cu cerințe de securitate și valoare juridică diferite:
| Tip | Acronim | Cerințe | Valoare juridică | Caz de utilizare |
|---|---|---|---|---|
| Semnătura electronică simplă | SES | Orice date electronice asociate semnatarului | Bas | Click-wrap, aprobare prin e-mail |
| Semnătura electronică avansată | AES | Se referă unic semnatarului, creat cu date aflate sub controlul acestuia | Mediu | Contracte comerciale, HR |
| Semnătura electronică calificată | QES | Certificat calificat + QSCD (Dispozitiv de creare a semnăturii calificat) | Semnătură de mână echivalentă | Acte notariale, contracte imobiliare |
eIDAS 2.0 și portofelul de identitate digitală
Din decembrie 2026, toate cele 27 de state membre UE trebuie să ofere cetățenilor a Portofelul de identitate digitală al UE (Portofel EUDI) care permite semnăturile QES pe dispozitivul mobil. Acest lucru va schimba fundamental integrarea utilizatorilor pentru sistemele de semnătură digitală: adio token-urilor hardware USB, salut smartphone-urilor.
Arhitectura PKI pentru semnătură digitală
O infrastructură cu chei publice (PKI) și infrastructura criptografică pe care o securizează autenticitatea și integritatea semnăturilor digitale. Componentele fundamentale sunt:
- CA rădăcină (Autoritate de certificare): ancora de încredere a ierarhiei. Emite certificate intermediare. Trebuie să fie offline (cu aer liber) pentru securitate maximă.
- CA intermediar: CA operațională care emite certificate utilizatorilor finali.
- RA (Autoritatea de înregistrare): verifică identitatea solicitantului înainte de a autoriza eliberarea certificatului.
- Răspunzători OCSP: serviciu în timp real pentru a verifica dacă un certificat și a fost revocat (alternativă la CRL).
- TSA (Autoritatea de marcare temporală): scoate marcaje temporale calificate RFC 3161.
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
Semnați PDF cu PyHanko
PyHanko este biblioteca Python de referință pentru semnarea digitală a documentelor PDF conform standardelor PDF/A și PAdES (PDF Advanced Electronic Signature). Sprijin semnături invizibile, vizibile, câmpuri interactive de semnătură și marcare temporală integrată.
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()
}
Flux de lucru cu semnături multipartite cu State Machine
În scenariile din lumea reală, un contract necesită adesea semnarea mai multor părți într-o singură comandă precis: mai întâi managerul juridic intern, apoi clientul, în final notarul. Implementăm o mașină de stare pentru a gestiona acest flux de lucru în mod robust.
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
)
Integrare unghiulară cu API-ul Signature
Din partea de front-end Angular, trebuie să se ocupe integrarea cu un serviciu de semnătură fluxul de redirecționare (utilizatorul este adus pe platforma de semnare și apoi revine), sau încorporare directă prin 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);
});
}
}
Validare pe termen lung (LTV) și arhivare
O semnătură digitală trebuie să fie verificabilă nu doar astăzi, ci și peste 10 sau 20 de ani, când certificatele originale au expirat sau algoritmul criptografic învechit. Acolo Validare pe termen lung (LTV) rezolvă această problemă încorporând toate informaţiile necesare validării în documentul semnat viitor: lanț de certificate, răspunsuri OCSP și marcaje de timp.
Standarde de arhivare pe termen lung
- PAdES-LTV: Semnătură PDF cu încorporare certificat și răspuns OCSP
- XAdES-A: Semnătură XML cu marcaje periodice de arhivare
- CAdES-A: Semnătură CMS cu marcaje temporale de arhivare
- ASiC-E: Container ZIP cu semnătură + document + metadate
Pentru documentele legale cu o valabilitate mai mare de 10 ani, se recomandă re-timbrarea temporală periodic (la fiecare 5 ani) pentru a actualiza puterea criptografică înainte ca SHA-256 să devină învechit.
Considerații de securitate
Puncte critice de siguranță
- Protecție cu cheie privată: nu generați sau stocați niciodată chei private în codul aplicației. Utilizați HSM (Modul de securitate hardware) sau cloud KMS (AWS KMS, Azure Key Vault, Google Cloud KMS).
- Revocarea certificatelor: implementați capsarea OCSP pentru a evita verificați OCSP în timp real la fiecare semnătură - critic pentru performanță.
- Jurnal de audit imuabil: fiecare eveniment de flux de lucru (semnătură, respingere, expirare) trebuie să fie scris într-un jurnal de numai atașare cu lanț hash pentru a detecta manipularea.
- Validarea documentului pre-semnare: verificați dacă PDF-ul nu conține JavaScript încorporat sau formulare interactive care ar putea modifica conținutul afișat.
Concluzii
Implementarea unui sistem de semnătură digitală scalabil, valabil din punct de vedere juridic necesită multe mai mult decât simpla „adăugarea unei semnături” la un PDF. Managementul ciclului de viață Conformitatea PKI, eIDAS 2.0, fluxuri de lucru cu mai multe părți, marcare temporală și pe termen lung Validarea sunt componente care trebuie proiectate împreună de la început.
Codul din acest articol oferă o bază solidă pentru construirea unui sistem producție. Pentru aplicații critice (acte notariale, contracte imobiliare, documente companii), luați în considerare integrarea cu furnizori de servicii de încredere calificați. precum Namirial, InfoCert, Aruba sau DocuSign — care gestionează complexitatea reglementărilor de eIDAS 2.0 pentru tine.
Seria LegalTech și AI
- NLP pentru analiza contractelor: de la OCR la înțelegere
- Arhitectura platformei e-Discovery
- Automatizarea conformității cu Dynamic Rules Engine
- Contract inteligent pentru acorduri juridice: Solidity și Vyper
- Rezumat documente legale cu IA generativă
- Legea motoarelor de căutare: înglobări vectoriale
- Semnătura digitală și autentificarea documentelor pe Scala (acest articol)
- Confidențialitatea datelor și sisteme de conformitate GDPR
- Crearea unui asistent legal AI (copilot juridic)
- Model de integrare a datelor LegalTech







