Scala의 디지털 서명 및 문서 인증
2024년 전세계 전자서명 시장 규모는 50억 달러를 넘어섰으며, 2030년까지 연간 30% 이상의 성장이 예상됩니다. 그러나 시장 수치를 넘어서는 디지털 서명의 대규모 채택으로 인해 수백만 달러를 관리하는 방법과 같은 구체적인 기술적 과제가 발생했습니다. 여러 관할권에서 법적 유효성을 유지하면서 하루에 서명할 수 있습니까? 구현 방법 유럽의 eIDAS 2.0 및 기타 국가의 동등한 규정을 준수하는 확장 가능한 PKI 시스템이 있습니까?
이 기사에서는 엔터프라이즈급 디지털 서명 시스템을 구축합니다. PKI 인증서 생성 및 관리, 서명 작업 흐름 구현 다단계, 최대 RFC 3161 호환 타임스탬프 및 장기 보관 유럽 표준에 따르면. 코드는 통합 예제와 함께 Python 및 TypeScript로 되어 있습니다. 각도 응용 프로그램의 경우.
무엇을 배울 것인가
- eIDAS 2.0(SES, AES, QES)에 따른 전자 서명 유형
- PKI 아키텍처: CA, RA, X.509 인증서, CRL 및 OCSP
- PyHanko(Python)를 사용한 PDF 서명 구현
- 존재 증명을 위한 RFC 3161 호환 타임스탬프
- 시스템 상태를 사용한 다자간 서명 작업 흐름
- DocuSign/Adobe Sign API와의 Angular 통합
- 장기 보관(LTV - 장기 검증)
eIDAS 2.0에 따른 전자 서명 수준
2024년 5월 20일 발효된 eIDAS 규정(EU) 2024/1183은 다음을 정의합니다. 보안 요구 사항과 법적 가치가 서로 다른 세 가지 수준의 전자 서명:
| 유형 | 두문자어 | 요구사항 | 법적 가치 | 사용 사례 |
|---|---|---|---|---|
| 단순전자서명 | SES | 서명자와 관련된 모든 전자 데이터 | 베이스 | 클릭 래핑, 이메일 승인 |
| 고급 전자 서명 | AES | 서명자를 고유하게 참조할 수 있으며 서명자가 관리하는 데이터로 생성됨 | 중간 | 상업 계약, HR |
| 적격 전자 서명 | QES | 적격 인증서 + QSCD(적격 서명 생성 장치) | 동등한 자필 서명 | 공증 증서, 부동산 계약 |
eIDAS 2.0과 디지털 신원 지갑
2026년 12월부터 모든 27개 EU 회원국은 시민들에게 다음을 제공해야 합니다. EU 디지털 신원 지갑(EUDI 지갑) QES 서명을 허용하는 모바일 장치에서. 이는 사용자 온보딩을 근본적으로 변화시킵니다. 디지털 서명 시스템: USB 하드웨어 토큰은 안녕, 스마트폰은 안녕.
디지털 서명을 위한 PKI 아키텍처
공개 키 인프라(PKI) 및 이를 통해 보호되는 암호화 인프라 디지털 서명의 신뢰성과 무결성. 기본 구성 요소는 다음과 같습니다.
- 루트 CA(인증 기관): 계층 구조의 신뢰할 수 있는 앵커입니다. 중간 인증서를 발급합니다. 보안을 극대화하려면 오프라인(에어갭)이어야 합니다.
- 중간 CA: 최종 사용자에게 인증서를 발급하는 운영 CA입니다.
- RA(등록 기관): 신청자의 신원을 확인합니다. 인증서 발급을 승인하기 전에.
- OCSP 응답자: 인증서 유무를 실시간으로 확인하는 서비스 취소되었습니다(CRL 대신).
- TSA(타임스탬프 기관): 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
PyHanko로 PDF에 서명
PyHanko는 PDF 문서에 디지털 서명을 위한 참조 Python 라이브러리입니다. PDF/A 및 PAdES(PDF Advanced Electronic Signature) 표준을 따릅니다. 지원 눈에 보이지 않는 서명, 대화형 서명 필드 및 통합 타임스탬프.
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()
}
상태 머신을 사용한 다자간 서명 워크플로
실제 시나리오에서는 계약에 따라 여러 당사자가 하나의 주문에 서명해야 하는 경우가 많습니다. 정확합니다. 먼저 내부 법률 관리자, 그 다음 고객, 마지막으로 공증인입니다. 우리는 이 워크플로를 강력하게 처리하기 위해 상태 머신을 구현합니다.
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
)
Signature API와의 각도 통합
Angular 프런트엔드 측에서 서명 서비스와의 통합은 다음을 처리해야 합니다. 리디렉션 흐름(사용자가 서명 플랫폼으로 이동한 후 돌아옴) 또는 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);
});
}
}
LTV(장기 검증) 및 보관
디지털 서명은 현재뿐만 아니라 10년, 20년 후에도 검증 가능해야 하며, 원래 인증서가 만료되었거나 암호화 알고리즘이 만료된 경우 구식. 거기 장기 검증(LTV) 이 문제를 해결합니다 검증에 필요한 모든 정보를 서명된 문서에 통합 미래: 인증서 체인, OCSP 응답 및 타임스탬프.
장기 보관 표준
- PAdES-LTV: 인증서 내장 및 OCSP 응답이 포함된 PDF 서명
- XAdES-A: 정기적인 보관 타임스탬프가 포함된 XML 서명
- CAdES-A: 보관 타임스탬프가 포함된 CMS 서명
- ASiC-E: 서명 + 문서 + 메타데이터가 포함된 ZIP 컨테이너
유효 기간이 10년 이상인 법적 문서의 경우 타임스탬프를 다시 찍는 것이 좋습니다. SHA-256이 암호화되기 전에 정기적으로(5년마다) 암호화 강도를 업데이트합니다. 구식.
보안 고려 사항
중요한 안전 사항
- 개인 키 보호: 개인 키를 생성하거나 저장하지 마십시오 애플리케이션 코드에서. HSM(하드웨어 보안 모듈) 또는 클라우드 KMS(AWS KMS, Azure Key Vault, Google Cloud KMS).
- 인증서 취소: 방지하기 위해 OCSP 스테이플링을 구현합니다. 모든 서명에서 실시간으로 OCSP를 확인하세요. 이는 성능에 매우 중요합니다.
- 변경할 수 없는 감사 로그: 각 워크플로 이벤트(서명, 거부, 만료) 변조를 감지하려면 해시 체인이 포함된 추가 전용 로그에 기록되어야 합니다.
- 사전 서명 문서 검증: PDF에 다음이 포함되어 있지 않은지 확인하십시오. 표시되는 콘텐츠를 변경할 수 있는 내장된 JavaScript 또는 대화형 양식입니다.
결론
확장 가능하고 법적으로 유효한 디지털 서명 시스템을 구현하려면 많은 노력이 필요합니다. 단순히 PDF에 "서명을 추가"하는 것 이상입니다. 수명주기 관리 PKI, eIDAS 2.0 규정 준수, 다자간 워크플로우, 타임스탬프 및 장기 검증은 처음부터 함께 설계해야 하는 구성 요소입니다.
이 문서의 코드는 시스템 구축을 위한 견고한 기반을 제공합니다. 생산. 중요한 응용프로그램(공증인 증서, 부동산 계약, 문서) 회사), 자격을 갖춘 TSP(신뢰 서비스 제공업체)와의 통합을 고려하세요. 규제 복잡성을 관리하는 Namirial, InfoCert, Aruba 또는 DocuSign 등 eIDAS 2.0을 만나보세요.
LegalTech 및 AI 시리즈
- 계약 분석을 위한 NLP: OCR에서 이해까지
- e-Discovery 플랫폼 아키텍처
- 동적 규칙 엔진을 통한 규정 준수 자동화
- 법적 계약을 위한 스마트 계약: Solidity 및 Vyper
- Generative AI를 사용한 법률 문서 요약
- 검색 엔진 법칙: 벡터 임베딩
- Scala의 디지털 서명 및 문서 인증(이 문서)
- 데이터 개인정보 보호 및 GDPR 규정 준수 시스템
- 법률 AI 보조원 구축(Legal Copilot)
- LegalTech 데이터 통합 패턴







