Digitální podpis a ověřování dokumentů ve společnosti Scala
V roce 2024 přesáhl celosvětový trh s elektronickým podpisem 5 miliard USD, s očekávaným ročním růstem o více než 30 % do roku 2030. Ale nad rámec tržních údajů, masivní přijetí digitálních podpisů představuje konkrétní technické výzvy: jak spravovat miliony podpisů za den při zachování právní platnosti ve více jurisdikcích? Jak implementovat a škálovatelný systém PKI, který je v souladu s eIDAS 2.0 v Evropě a ekvivalentními předpisy ve zbytku světa?
V tomto článku vytváříme podnikový systém digitálního podpisu: od generování a správa PKI certifikátů, implementace podpisových workflow vícekrokové časové razítko v souladu s RFC 3161 a dlouhodobá archivace podle evropských norem. Kód je v Pythonu a TypeScriptu s příklady integrace pro úhlové aplikace.
Co se naučíte
- Typy elektronického podpisu podle eIDAS 2.0 (SES, AES, QES)
- Architektura PKI: CA, RA, certifikáty X.509, CRL a OCSP
- Implementace podpisu PDF pomocí PyHanko (Python)
- Časové razítko vyhovující RFC 3161 pro důkaz existence
- Pracovní postup podpisu více stran se stavem stroje
- Úhlová integrace s DocuSign/Adobe Sign API
- Dlouhodobá archivace (LTV - Long-Term Validation)
Úrovně elektronického podpisu podle eIDAS 2.0
Nařízení eIDAS (EU) 2024/1183 – které vstoupilo v platnost 20. května 2024 – definuje tři úrovně elektronického podpisu s různými bezpečnostními požadavky a právní hodnotou:
| Typ | Akronym | Požadavky | Právní hodnota | Případ použití |
|---|---|---|---|---|
| Jednoduchý elektronický podpis | SES | Jakákoli elektronická data spojená s podepisovatelem | Bas | Click-wrap, e-mail schválení |
| Pokročilý elektronický podpis | AES | Jedinečně odkazovatelné na signatáře, vytvořené s daty pod jeho kontrolou | Střední | Obchodní smlouvy, HR |
| Kvalifikovaný elektronický podpis | QES | Kvalifikovaný certifikát + QSCD (Zařízení pro vytváření kvalifikovaných podpisů) | Ekvivalentní vlastnoruční podpis | Notářské zápisy, smlouvy o nemovitostech |
eIDAS 2.0 a Digital Identity Wallet
Od prosince 2026 musí všech 27 členských států EU poskytovat občanům a Peněženka EU Digital Identity Wallet (EUDI Wallet) který umožňuje podpisy QES na mobilním zařízení. Tím se zásadně změní onboarding uživatelů pro systémy digitálního podpisu: sbohem hardwarovým tokenům USB, ahoj chytrým telefonům.
Architektura PKI pro digitální podpis
Infrastruktura veřejného klíče (PKI) a kryptografická infrastruktura, kterou zabezpečuje pravost a integrita digitálních podpisů. Základní komponenty jsou:
- Kořenová CA (Certifikační autorita): důvěryhodná kotva hierarchie. Vydává zprostředkující certifikáty. Pro maximální zabezpečení musí být offline (vzduchová mezera).
- Středně pokročilá CA: Provozní CA, která vydává certifikáty koncovým uživatelům.
- RA (registrační orgán): ověřuje totožnost žadatele před autorizací vydání certifikátu.
- Odpovídače OCSP: služba v reálném čase pro ověření, zda certifikát a byla odvolána (alternativa k CRL).
- TSA (Úřad pro časové razítko): vydává kvalifikovaná časová razítka 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
Podepište PDF pomocí PyHanko
PyHanko je referenční knihovna Pythonu pro digitální podepisování dokumentů PDF podle standardů PDF/A a PAdES (PDF Advanced Electronic Signature). Podpora neviditelné, viditelné podpisy, interaktivní pole podpisů a integrované časové razítko.
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 pro vícestranný podpis s State Machine
Ve scénářích reálného světa smlouva často vyžaduje, aby v jedné objednávce podepsalo více stran přesné: nejprve interní právní manažer, poté klient, nakonec notář. Implementujeme stavový automat, abychom tento pracovní postup zvládli robustně.
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
)
Úhlová integrace s Signature API
Z frontendové strany Angular je třeba zvládnout integraci s podpisovou službou tok přesměrování (uživatel je přiveden na podepisovací platformu a poté se vrací), nebo přímé vložení přes 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);
});
}
}
Dlouhodobá validace (LTV) a archivace
Digitální podpis musí být ověřitelný nejen dnes, ale i za 10 nebo 20 let, kdy může vypršet platnost původních certifikátů nebo kryptografický algoritmus zastaralé. Tam Dlouhodobé ověření (LTV) řeší tento problém začlenění všech informací nezbytných pro validaci do podepsaného dokumentu budoucnost: řetězec certifikátů, odpovědi OCSP a časová razítka.
Standardy dlouhodobé archivace
- PAdES-LTV: Podpis PDF s vložením certifikátu a odpovědí OCSP
- XAdES-A: XML podpis s pravidelnými archivními časovými razítky
- CAdES-A: CMS podpis s archivními časovými razítky
- ASiC-E: ZIP kontejner s podpisem + dokument + metadata
U právních dokumentů s platností delší než 10 let se doporučuje opětovné časové razítko pravidelně (každých 5 let) aktualizovat šifrovací sílu, než se stane SHA-256 zastaralé.
Bezpečnostní aspekty
Kritické bezpečnostní body
- Ochrana soukromého klíče: nikdy nevytvářejte ani neuchovávejte soukromé klíče v kódu aplikace. Použijte HSM (Hardware Security Module) nebo cloud KMS (AWS KMS, Azure Key Vault, Google Cloud KMS).
- Zneplatnění certifikátů: implementujte sešívání OCSP, abyste se vyhnuli kontrolovat OCSP v reálném čase při každém podpisu – rozhodující pro výkon.
- Neměnný protokol auditu: každá událost pracovního postupu (podpis, odmítnutí, vypršení platnosti) musí být zapsán do protokolu pouze pro připojení s hash řetězcem, aby bylo možné detekovat manipulaci.
- Ověření dokumentu před podpisem: zkontrolujte, zda PDF neobsahuje Vložený JavaScript nebo interaktivní formuláře, které by mohly změnit zobrazený obsah.
Závěry
Implementace škálovatelného, právně platného systému digitálního podpisu vyžaduje hodně více než pouhé „přidání podpisu“ do PDF. Řízení životního cyklu PKI, soulad s eIDAS 2.0, pracovní postupy pro více stran, časové razítko a dlouhodobé Validace jsou komponenty, které musí být od začátku navrženy společně.
Kód v tomto článku poskytuje pevný základ pro budování systému výroby. Pro kritické aplikace (notářské zápisy, smlouvy o nemovitostech, dokumenty společnosti), zvažte integraci s kvalifikovanými poskytovateli služeb TSP (Trust Service Providers). jako jsou Namirial, InfoCert, Aruba nebo DocuSign – které řídí regulační složitost eIDAS 2.0 pro vás.
Série LegalTech a AI
- NLP pro analýzu smluv: od OCR k porozumění
- Architektura platformy e-Discovery
- Automatizace shody s dynamickými pravidly
- Chytrá smlouva pro právní dohody: Solidita a Vyper
- Shrnutí právních dokumentů s generativní AI
- Zákon o vyhledávačích: vektorové vkládání
- Digitální podpis a ověřování dokumentů na Scala (tento článek)
- Ochrana osobních údajů a systémy dodržování GDPR
- Vytvoření právního asistenta AI (právní kopilot)
- Vzor integrace dat LegalTech







