Podpis cyfrowy i uwierzytelnianie dokumentów w Scala
W 2024 roku światowy rynek podpisu elektronicznego przekroczył 5 miliardów dolarów, z oczekiwanym rocznym wzrostem na poziomie ponad 30% do 2030 r. Jednak wykraczając poza dane rynkowe, masowe przyjęcie podpisów cyfrowych stwarza konkretne wyzwania techniczne: jak zarządzać milionami podpisów dziennie, zachowując jednocześnie ważność prawną w wielu jurysdykcjach? Jak wdrożyć A skalowalny system PKI zgodny z eIDAS 2.0 w Europie i równoważnymi przepisami w reszcie świata?
W tym artykule budujemy system podpisu cyfrowego klasy korporacyjnej: z generowanie i zarządzanie certyfikatami PKI, wdrażanie obiegów podpisów wieloetapowe, zgodne ze standardem RFC 3161 znaczniki czasu i długoterminowa archiwizacja według standardów europejskich. Kod jest w Pythonie i TypeScript, z przykładami integracji do aplikacji Angularowych.
Czego się nauczysz
- Rodzaje podpisu elektronicznego według eIDAS 2.0 (SES, AES, QES)
- Architektura PKI: certyfikaty CA, RA, X.509, CRL i OCSP
- Implementacja podpisu PDF w PyHanko (Python)
- Znacznik czasu zgodny z RFC 3161 jako dowód istnienia
- Przepływ pracy z podpisami wielostronnymi ze stanem maszyny
- Integracja kątowa z API DocuSign/Adobe Sign
- Archiwizacja długoterminowa (LTV - Walidacja długoterminowa)
Poziomy podpisu elektronicznego według eIDAS 2.0
Rozporządzenie eIDAS (UE) 2024/1183 – które weszło w życie 20 maja 2024 r. – definiuje trzy poziomy podpisu elektronicznego o różnych wymaganiach bezpieczeństwa i mocy prawnej:
| Typ | Akronim | Wymagania | Wartość prawna | Użyj przypadku |
|---|---|---|---|---|
| Prosty podpis elektroniczny | SES | Wszelkie dane elektroniczne powiązane z osobą podpisującą | Bas | Zawijanie po kliknięciu, zatwierdzenie e-mailem |
| Zaawansowany podpis elektroniczny | AES | Unikalnie odnoszące się do sygnatariusza, utworzone z danych znajdujących się pod jego kontrolą | Średni | Umowy handlowe, HR |
| Kwalifikowany podpis elektroniczny | PYTANIA | Certyfikat kwalifikowany + QSCD (urządzenie do składania podpisu kwalifikowanego) | Równoważny podpis odręczny | Akty notarialne, umowy dotyczące nieruchomości |
eIDAS 2.0 i Portfel Tożsamości Cyfrowej
Od grudnia 2026 r. wszystkie 27 państw członkowskich UE muszą zapewnić obywatelom: Portfel tożsamości cyfrowej UE (portfel EUDI) który umożliwia podpisy QES na urządzeniu mobilnym. To zasadniczo zmieni proces wdrażania użytkowników dla systemów podpisu cyfrowego: pożegnajcie tokeny sprzętowe USB, powitajcie smartfony.
Architektura PKI dla podpisu cyfrowego
Infrastruktura Klucza Publicznego (PKI) i infrastruktura kryptograficzna, którą zabezpiecza autentyczność i integralność podpisów cyfrowych. Podstawowe elementy to:
- Główny urząd certyfikacji (urząd certyfikacji): zaufana kotwica hierarchii. Wydaje certyfikaty pośrednie. Aby zapewnić maksymalne bezpieczeństwo, musi być w trybie offline (szczelina powietrzna).
- Średnio zaawansowany CA: Operacyjny urząd certyfikacji wystawiający certyfikaty użytkownikom końcowym.
- RA (Organ Rejestracyjny): weryfikuje tożsamość wnioskodawcy przed zezwoleniem na wydanie certyfikatu.
- Osoby odpowiadające OCSP: usługa w czasie rzeczywistym w celu sprawdzenia, czy certyfikat i został unieważniony (alternatywa dla CRL).
- TSA (organ znacznika czasu): wyprowadza kwalifikowane znaczniki czasu 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
Podpisz PDF za pomocą PyHanko
PyHanko to referencyjna biblioteka Pythona do cyfrowego podpisywania dokumentów PDF zgodnie ze standardami PDF/A i PAdES (PDF Advanced Electronic Signature). Wsparcie niewidoczne, widoczne podpisy, interaktywne pola podpisu i zintegrowane znaczniki czasu.
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()
}
Przepływ pracy z podpisem wielostronnym z maszyną stanową
W rzeczywistych scenariuszach umowa często wymaga podpisania wielu stron w jednym zamówieniu precyzyjnie: najpierw wewnętrzny menadżer prawny, potem klient, na końcu notariusz. Implementujemy maszynę stanów, aby niezawodnie obsługiwać ten przepływ pracy.
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
)
Integracja kątowa z API Signature
Od strony frontendu Angulara należy obsłużyć integrację z usługą podpisu przepływ przekierowań (użytkownik zostaje przeniesiony na platformę podpisującą, a następnie wraca), lub bezpośrednie osadzanie poprzez 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);
});
}
}
Walidacja długoterminowa (LTV) i archiwizacja
Podpis cyfrowy musi być weryfikowalny nie tylko dziś, ale także za 10, 20 lat, kiedy oryginalne certyfikaty mogły wygasnąć lub algorytm kryptograficzny przestarzałe. Tam Walidacja długoterminowa (LTV) rozwiązuje ten problem zawarcie w podpisanym dokumencie wszystkich informacji niezbędnych do zatwierdzenia przyszłość: łańcuch certyfikatów, odpowiedzi OCSP i znaczniki czasu.
Standardy archiwizacji długoterminowej
- PODKŁADKI-LTV: Podpis PDF z osadzeniem certyfikatu i odpowiedzią OCSP
- XAdES-A: Podpis XML z okresowymi archiwalnymi znacznikami czasu
- CADES-A: Podpis CMS z archiwalnymi znacznikami czasu
- ASiC-E: Kontener ZIP z podpisem + dokumentem + metadanymi
W przypadku dokumentów prawnych o okresie ważności dłuższym niż 10 lat zaleca się ponowne oznaczenie czasu okresowo (co 5 lat) w celu aktualizacji siły kryptograficznej, zanim stanie się SHA-256 przestarzałe.
Względy bezpieczeństwa
Krytyczne punkty bezpieczeństwa
- Ochrona klucza prywatnego: nigdy nie generuj ani nie przechowuj kluczy prywatnych w kodzie aplikacji. Użyj HSM (Hardware Security Module) lub chmurowego KMS (AWS KMS, Azure Key Vault, Google Cloud KMS).
- Unieważnienie certyfikatów: należy wdrożyć zszywanie OCSP, aby tego uniknąć sprawdzaj OCSP w czasie rzeczywistym przy każdym podpisie – ma to kluczowe znaczenie dla wydajności.
- Niezmienny dziennik audytu: każde zdarzenie przepływu pracy (podpis, odrzucenie, wygaśnięcie) musi zostać zapisany w dzienniku tylko do dołączania z łańcuchem mieszającym, aby wykryć manipulację.
- Walidacja dokumentu przed podpisaniem: sprawdź, czy plik PDF nie zawiera Wbudowany JavaScript lub interaktywne formularze, które mogą zmieniać wyświetlaną treść.
Wnioski
Wdrożenie skalowalnego, prawnie obowiązującego systemu podpisu cyfrowego wymaga wiele więcej niż tylko „dodanie podpisu” do pliku PDF. Zarządzanie cyklem życia Zgodność z PKI, eIDAS 2.0, wielostronne przepływy pracy, znaczniki czasu i długoterminowe Walidacja to komponenty, które należy od początku projektować wspólnie.
Kod zawarty w tym artykule zapewnia solidną podstawę do budowania systemu produkcja. Do zastosowań krytycznych (akty notarialne, umowy dotyczące nieruchomości, dokumenty firm), rozważ integrację z wykwalifikowanymi TSP (Trust Service Providers). takie jak Namirial, InfoCert, Aruba czy DocuSign – które zarządzają złożonością regulacyjną eIDAS 2.0 dla Ciebie.
Seria LegalTech i AI
- NLP w analizie kontraktów: od OCR do zrozumienia
- Architektura platformy e-Discovery
- Automatyzacja zgodności z silnikiem dynamicznych reguł
- Inteligentna umowa dotycząca umów prawnych: Solidity i Vyper
- Podsumowanie dokumentów prawnych z generatywną sztuczną inteligencją
- Prawo dotyczące wyszukiwarek: osadzanie wektorów
- Podpis cyfrowy i uwierzytelnianie dokumentów w Scali (ten artykuł)
- Systemy ochrony danych i zgodności z RODO
- Budowanie prawnego asystenta AI (drugi pilot prawniczy)
- Wzór integracji danych LegalTech







