diff --git a/docs/CCSP.md b/docs/CCSP.md
index 0ef3c4f..7496d5c 100644
--- a/docs/CCSP.md
+++ b/docs/CCSP.md
@@ -29,7 +29,10 @@ O protocolo CCSP/1 adota o princípio de menor privilégio e segurança em profu
| `GET` | `/ccsp/v1/status` | Verifica integridade e status de prontidão do agente local | Nenhuma |
| `POST` | `/ccsp/v1/authorize` | Handshake de autorização inicial de uma aplicação web | Consentimento visual do usuário |
| `GET` | `/ccsp/v1/certificates` | Lista certificados digitais ICP-Brasil válidos e operacionais | Aplicação Autorizada |
-| `POST` | `/ccsp/v1/sign` | Executa assinatura digital do digest informado | Aplicação Autorizada + Diálogo Nativo |
+| `GET` | `/ccsp/v1/seals` | Lista temas e estilos visuais de selos suportados pelo agente | Aplicação Autorizada |
+| `POST` | `/ccsp/v1/sign` | Executa assinatura digital do digest informado (PAdES/CAdES/XMLDSig) | Aplicação Autorizada + Diálogo Nativo |
+| `POST` | `/ccsp/v1/sign-pdf` | Assina documento PDF em Base64 com selo posicionado e devolve PDF assinado | Aplicação Autorizada + Diálogo Nativo |
+| `POST` | `/ccsp/v1/sign-xml` | Assina documento XML (XAdES-BES / XMLDSig) e devolve XML assinado | Aplicação Autorizada + Diálogo Nativo |
| `POST` | `/ccsp/v1/authenticate` | Realiza login seguro via desafio criptográfico (*Challenge-Response*) | Aplicação Autorizada + Diálogo Nativo |
---
@@ -214,6 +217,123 @@ Executa o login criptográfico de um usuário em sistemas web através do mecani
---
+### 3.6 `GET /ccsp/v1/seals`
+Retorna a lista de temas e estilos de selos visuais embutidos e instalados no CyberCore Signer.
+
+#### Cabeçalhos
+- `Origin: https://meusistema.empresa.com.br`
+- `X-CyberCore-AppId: 550e8400-e29b-41d4-a716-446655440000`
+
+#### Resposta de Sucesso (`200 OK`)
+```json
+{
+ "version": "1",
+ "status": "success",
+ "default_seal_id": "icp-brasil",
+ "seals": [
+ {
+ "id": "icp-brasil",
+ "name": "ICP-Brasil Institucional",
+ "description": "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
+ "primary_color": "#0d47a1",
+ "secondary_color": "#0284c7",
+ "border_color": "#93c5fd",
+ "background_color": "#ffffff",
+ "text_color": "#0f172a",
+ "icon_style": "shield",
+ "width": 320,
+ "height": 82,
+ "default_cpf_mode": "FULL",
+ "default_include_date_time": true
+ },
+ {
+ "id": "gov-br",
+ "name": "Governamental (Padrão Gov.br / ITI)",
+ "description": "Faixa verde e ouro com declaração expressa de validade pela MP nº 2.200-2/2001",
+ "primary_color": "#047857",
+ "secondary_color": "#eab308"
+ }
+ ]
+}
+```
+
+---
+
+### 3.7 `POST /ccsp/v1/sign-pdf`
+Recebe um documento PDF em Base64, a página e as coordenadas desejadas (`x`, `y`, `scale`), aplica o selo visual e a assinatura digital PAdES (ISO 32000-1 / RFC 5652) e retorna o PDF assinado completo em Base64.
+
+#### Payload da Requisição
+```json
+{
+ "version": "1",
+ "request_id": "b1b74288-7264-44a5-9271-bf10d32c5f11",
+ "application_id": "550e8400-e29b-41d4-a716-446655440000",
+ "operation": "sign-pdf",
+ "certificate_hint": "cert-1",
+ "pdf": "JVBERi0xLjQKJ...Base64...",
+ "page": 1,
+ "x": 60.0,
+ "y": 60.0,
+ "scale": 1.0,
+ "visible_seal": true,
+ "seal_theme_id": "icp-brasil",
+ "reason": "Aprovado e assinado digitalmente",
+ "location": "Curitiba, PR",
+ "nonce": "c301d011-8012-4261-bcfa-e7912ab5a420"
+}
+```
+
+> **Selo Customizado Inline (`seal_custom`):**
+> Em vez de `seal_theme_id`, a requisição pode fornecer um objeto `seal_custom` com cores hexadecimais, título customizado, ícone vetorial (`shield`, `certificate`, `lock`) e política de privacidade LGPD (`FULL`, `MASKED`, `HIDDEN`).
+
+#### Resposta de Sucesso (`200 OK`)
+```json
+{
+ "version": "1",
+ "request_id": "b1b74288-7264-44a5-9271-bf10d32c5f11",
+ "status": "signed",
+ "signed_pdf": "JVBERi0xLjQKJ...PDF_ASSINADO_BASE64...",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "certificate": "MIIFazCCBF...Base64...",
+ "algorithm": "SHA256withRSA"
+}
+```
+
+---
+
+### 3.8 `POST /ccsp/v1/sign-xml`
+Executa a assinatura digital de documentos XML em conformidade com o padrão XMLDSig W3C ou XAdES-BES ICP-Brasil (Padrão NF-e / NFC-e / CT-e / NFS-e). O documento assinado completo é devolvido diretamente ao sistema web.
+
+#### Payload da Requisição
+```json
+{
+ "version": "1",
+ "request_id": "e44d32e1-455b-4357-9351-41c10d3b6f22",
+ "application_id": "550e8400-e29b-41d4-a716-446655440000",
+ "operation": "sign-xml",
+ "certificate_hint": "cert-1",
+ "xml": "...",
+ "profile": "XADES_BES",
+ "target_element_tag": "infNFe",
+ "target_element_id": "NFe35261012345678000199550010000001001234567890",
+ "nonce": "d80b0322-8012-4261-bcfa-e7912ab5a420"
+}
+```
+
+#### Resposta de Sucesso (`200 OK`)
+```json
+{
+ "version": "1",
+ "request_id": "e44d32e1-455b-4357-9351-41c10d3b6f22",
+ "status": "signed",
+ "signed_xml": "...",
+ "certificate": "MIIFazCCBF...Base64...",
+ "algorithm": "SHA256withRSA"
+}
+```
+
+---
+
## 4. Estrutura Padrão de Erro e Códigos HTTP
Quando qualquer operação falha, o servidor local CCSP/1 responde com um objeto JSON padronizado e o código de status HTTP correspondente:
diff --git a/docs/pades.md b/docs/pades.md
index b299fba..e8da082 100644
--- a/docs/pades.md
+++ b/docs/pades.md
@@ -90,3 +90,55 @@ A posição e a página da estampa são parametrizáveis via `PdfSignatureOption
- **Filtro:** `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`.
- **Algoritmo:** `SHA256withRSA` / PKCS#1 v1.5 com digest SHA-256.
- **Salva Incremental:** Preserva a integridade de assinaturas anteriores e do documento original sem quebrar certificações prévias.
+
+---
+
+## 5. Ferramentas Web do SDK (@cybercore/signer-sdk)
+
+O SDK web fornece componentes visuais nativos em JavaScript vanilla para aplicações web:
+
+### 5.1 `PdfStampPicker` (Posicionador Visual de Selo)
+Permite que o usuário final suba um PDF no navegador, folheie páginas, selecione cantos predefinidos ou arraste a estampa visual interativamente. O componente mapeia as coordenadas do canvas para o sistema de coordenadas do PDFBox (ISO 32000, com origem no canto inferior esquerdo) e gera em tempo real o payload JSON para a requisição.
+
+```javascript
+import { PdfStampPicker } from '@cybercore/signer-sdk';
+
+const picker = new PdfStampPicker({
+ container: '#picker-container',
+ pdfSource: pdfArrayBuffer, // ou URL / Base64
+ initialScale: 1.0,
+ onPositionChange: (pos) => {
+ console.log('Página:', pos.page, 'X:', pos.x, 'Y:', pos.y);
+ }
+});
+
+// Obtém parâmetros prontos para a requisição CCSP
+const stampConfig = picker.getStampConfig();
+```
+
+### 5.2 `PdfSealDesigner` (Criador & Personalizador de Selos)
+Componente interativo com live preview em SVG vetorial para personalização de selos:
+- Paleta de cores primária, secundária, fundo e texto.
+- Ícones vetoriais (`shield`, `certificate`, `lock`) e upload de logotipo em Base64.
+- Regras LGPD para mascaramento ou ocultação de CPF/CNPJ.
+- Exportação da configuração JSON para envio inline via `seal_custom` na requisição `signPdfDocument()`.
+
+### 5.3 Assinatura Completa via Base64 (`signPdfDocument`)
+Permite enviar o PDF completo em Base64 e receber de volta o PDF assinado completo em Base64 para envio direto ao backend:
+
+```javascript
+const result = await signer.signPdfDocument({
+ certificateId: cert.id,
+ pdf: pdfArrayBuffer, // ou base64
+ page: stampConfig.page,
+ x: stampConfig.x,
+ y: stampConfig.y,
+ scale: stampConfig.scale,
+ seal_theme_id: 'icp-brasil', // ou seal_custom: customSealConfig
+ reason: 'Documento homologado',
+ location: 'São Paulo, SP'
+});
+
+console.log('PDF Assinado Base64:', result.signed_pdf);
+console.log('SHA-256 do Documento Final:', result.sha256);
+```
diff --git a/docs/xml-xmldsig.md b/docs/xml-xmldsig.md
index 940e38a..40507b0 100644
--- a/docs/xml-xmldsig.md
+++ b/docs/xml-xmldsig.md
@@ -116,3 +116,27 @@ O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` e o `Signat
2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
3. **Validação de integridade do XAdES:** Confere o digest das propriedades qualificadas (`SignedProperties`), rejeitando qualquer adulteração no horário de assinatura (`SigningTime`) ou no certificado declarado.
4. **Inspeção de signatário e datação:** Extração de nome, documento (CPF/CNPJ), autoridade certificadora emissora e datação auditável.
+
+---
+
+## 5. Integração Web e SDK (@cybercore/signer-sdk)
+
+As aplicações web autorizadas podem assinar arquivos XML diretamente via protocolo CCSP/1 com o método `signXmlDocument()`:
+
+```javascript
+import { CyberCoreSigner } from '@cybercore/signer-sdk';
+
+const signer = new CyberCoreSigner();
+
+// Assinatura de documento XML ou evento fiscal
+const result = await signer.signXmlDocument({
+ certificateId: cert.id,
+ xml: xmlString, // texto puro ou xml_base64
+ profile: 'XADES_BES', // ou 'XMLDSIG'
+ targetElementTag: 'infNFe', // opcional
+ targetElementId: 'NFe35261012345678000199550010000001001234567890' // opcional
+});
+
+console.log('XML Assinado Retornado:', result.signed_xml);
+console.log('Algoritmo Utilizado:', result.algorithm);
+```
diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java
index f744e37..c0ffd94 100644
--- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java
+++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspRequest.java
@@ -56,7 +56,67 @@ public record CcspRequest(
String purpose,
@JsonProperty("challenge")
- String challenge
+ String challenge,
+
+ @JsonProperty("pdf")
+ String pdf,
+
+ @JsonProperty("page")
+ Integer page,
+
+ @JsonProperty("page_choice")
+ String pageChoice,
+
+ @JsonProperty("position")
+ String position,
+
+ @JsonProperty("x")
+ Float x,
+
+ @JsonProperty("y")
+ Float y,
+
+ @JsonProperty("scale")
+ Double scale,
+
+ @JsonProperty("visible_seal")
+ Boolean visibleSeal,
+
+ @JsonProperty("seal_theme_id")
+ String sealThemeId,
+
+ @JsonProperty("seal_custom")
+ java.util.Map sealCustom,
+
+ @JsonProperty("reason")
+ String reason,
+
+ @JsonProperty("location")
+ String location,
+
+ @JsonProperty("cpf_display_mode")
+ String cpfDisplayMode,
+
+ @JsonProperty("include_date_time")
+ Boolean includeDateTime,
+
+ @JsonProperty("custom_extra_info")
+ String customExtraInfo,
+
+ @JsonProperty("xml")
+ String xml,
+
+ @JsonProperty("xml_base64")
+ String xmlBase64,
+
+ @JsonProperty("profile")
+ String profile,
+
+ @JsonProperty("target_element_tag")
+ String targetElementTag,
+
+ @JsonProperty("target_element_id")
+ String targetElementId
) implements Serializable {
public static final String PROTOCOL_VERSION = "1";
@@ -68,7 +128,9 @@ public record CcspRequest(
) {
this(version, requestId, applicationId, operation, algorithm, digest,
certificateHint, expiresAt, timestamp, nonce, signatureType,
- null, null, null, null);
+ null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null);
}
public boolean isVersionSupported() {
diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java
index 26aeb88..8263fbf 100644
--- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java
+++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspResponse.java
@@ -49,7 +49,16 @@ public record CcspResponse(
String documentName,
@JsonProperty("document_type")
- String documentType
+ String documentType,
+
+ @JsonProperty("signed_pdf")
+ String signedPdf,
+
+ @JsonProperty("signed_xml")
+ String signedXml,
+
+ @JsonProperty("sha256")
+ String sha256
) implements Serializable {
public CcspResponse {
@@ -88,7 +97,58 @@ public record CcspResponse(
subjectName,
subjectCpfCnpj,
documentName,
- documentType
+ documentType,
+ null,
+ null,
+ null
+ );
+ }
+
+ public static CcspResponse pdfSuccess(String requestId, String signedPdfBase64, String sha256,
+ String signatureBase64, String certBase64, List chain,
+ String algorithm, String subjectName, String subjectCpfCnpj,
+ String documentName) {
+ return new CcspResponse(
+ CcspRequest.PROTOCOL_VERSION,
+ requestId,
+ STATUS_SIGNED,
+ signatureBase64,
+ certBase64,
+ chain,
+ algorithm,
+ null,
+ null,
+ subjectName,
+ subjectCpfCnpj,
+ documentName,
+ "PDF",
+ signedPdfBase64,
+ null,
+ sha256
+ );
+ }
+
+ public static CcspResponse xmlSuccess(String requestId, String signedXml, String sha256,
+ String signatureBase64, String certBase64, List chain,
+ String algorithm, String subjectName, String subjectCpfCnpj,
+ String documentName) {
+ return new CcspResponse(
+ CcspRequest.PROTOCOL_VERSION,
+ requestId,
+ STATUS_SIGNED,
+ signatureBase64,
+ certBase64,
+ chain,
+ algorithm,
+ null,
+ null,
+ subjectName,
+ subjectCpfCnpj,
+ documentName,
+ "XML",
+ null,
+ signedXml,
+ sha256
);
}
@@ -108,7 +168,10 @@ public record CcspResponse(
subjectName,
subjectCpfCnpj,
null,
- "AUTH_CHALLENGE"
+ "AUTH_CHALLENGE",
+ null,
+ null,
+ null
);
}
@@ -126,6 +189,9 @@ public record CcspResponse(
null,
null,
null,
+ null,
+ null,
+ null,
null
);
}
diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java
index 6a760d2..70d1f7b 100644
--- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java
+++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/protocol/CcspServer.java
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.crypto.SigningService;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext;
+import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.domain.SigningResult;
import java.security.MessageDigest;
import br.com.cybercore.signer.core.provider.CertificateManager;
@@ -22,6 +23,20 @@ import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import br.com.cybercore.signer.core.pdf.PdfSealPageChoice;
+import br.com.cybercore.signer.core.pdf.PdfSealPosition;
+import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
+import br.com.cybercore.signer.core.pdf.PdfSigningService;
+import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
+import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
+import br.com.cybercore.signer.core.pdf.seal.PdfSealThemeRegistry;
+import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
+import br.com.cybercore.signer.core.xml.XmlSignatureProfile;
+import br.com.cybercore.signer.core.xml.XmlSigningService;
+import java.util.Arrays;
+import java.util.UUID;
+import java.time.format.DateTimeFormatter;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -56,6 +71,8 @@ public class CcspServer {
private final int port;
private final CertificateManager certificateManager;
private final SigningService signingService;
+ private final PdfSigningService pdfSigningService;
+ private final XmlSigningService xmlSigningService;
private final ApplicationRegistry applicationRegistry;
private final NonceManager nonceManager;
private final OriginValidator originValidator;
@@ -75,6 +92,8 @@ public class CcspServer {
this.port = port;
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
this.signingService = Objects.requireNonNull(signingService, "signingService cannot be null");
+ this.pdfSigningService = new PdfSigningService(this.certificateManager);
+ this.xmlSigningService = new XmlSigningService(this.certificateManager);
this.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
this.promptListener = promptListener;
this.nonceManager = new NonceManager();
@@ -114,6 +133,9 @@ public class CcspServer {
httpServer.createContext("/ccsp/v1/sign", new SignHandler());
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler());
+ httpServer.createContext("/ccsp/v1/sign-pdf", new SignPdfHandler());
+ httpServer.createContext("/ccsp/v1/sign-xml", new SignXmlHandler());
+ httpServer.createContext("/ccsp/v1/seals", new SealsHandler());
httpServer.start();
running = true;
@@ -458,6 +480,480 @@ public class CcspServer {
}
}
+ private class SignPdfHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if (handleCors(exchange)) return;
+
+ if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
+ sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas POST é suportado.");
+ return;
+ }
+
+ String origin = getOrigin(exchange);
+ CcspRequest request;
+ try (InputStream is = exchange.getRequestBody()) {
+ request = objectMapper.readValue(is, CcspRequest.class);
+ } catch (Exception e) {
+ sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
+ return;
+ }
+
+ if (!request.isVersionSupported()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
+ "Versão do protocolo não suportada: " + request.version());
+ return;
+ }
+
+ if (request.isExpired()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED,
+ "A requisição de assinatura expirou.");
+ return;
+ }
+
+ if (request.nonce() != null && !request.nonce().isBlank()) {
+ boolean accepted = nonceManager.registerNonce(request.nonce());
+ if (!accepted) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
+ "Requisição duplicada detectada (replay prevenido).");
+ return;
+ }
+ }
+
+ String appId = request.applicationId();
+ if (appId == null || appId.isBlank()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED,
+ "application_id é obrigatório.");
+ return;
+ }
+
+ if (!applicationRegistry.isAuthorized(appId, origin)) {
+ boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
+ if (!approved) {
+ sendCcspError(exchange, 403, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED,
+ "Aplicação não autorizada pelo usuário.");
+ return;
+ }
+ }
+
+ if (request.pdf() == null || request.pdf().isBlank()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
+ "O campo 'pdf' contendo o documento em Base64 é obrigatório.");
+ return;
+ }
+
+ byte[] pdfBytes;
+ try {
+ pdfBytes = Base64.getDecoder().decode(request.pdf());
+ } catch (Exception e) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
+ "O campo 'pdf' não contém dados Base64 válidos.");
+ return;
+ }
+
+ Optional certOpt = certificateManager.findByHint(request.certificateHint());
+ if (certOpt.isEmpty()) {
+ sendCcspError(exchange, 404, request.requestId(), CcspErrorCode.CERTIFICATE_NOT_FOUND,
+ "Nenhum certificado correspondente encontrado.");
+ return;
+ }
+ ManagedCertificate cert = certOpt.get();
+
+ if (promptListener != null) {
+ try {
+ CompletableFuture approvalFuture = promptListener.requestSignatureConfirmation(request, cert);
+ Boolean confirmed = approvalFuture.get(60, TimeUnit.SECONDS);
+ if (!Boolean.TRUE.equals(confirmed)) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
+ "Operação de assinatura cancelada pelo usuário.");
+ return;
+ }
+ } catch (Exception e) {
+ logger.warn("Falha ou timeout na confirmação visual de assinatura: {}", e.getMessage());
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
+ "Confirmação da assinatura expirou ou foi cancelada.");
+ return;
+ }
+ }
+
+ PdfSealTheme sealTheme;
+ if (request.sealCustom() != null && !request.sealCustom().isEmpty()) {
+ sealTheme = parseCustomSealTheme(request.sealCustom());
+ } else if (request.sealThemeId() != null && !request.sealThemeId().isBlank()) {
+ sealTheme = PdfSealThemeRegistry.findById(request.sealThemeId())
+ .orElse(PdfSealThemeRegistry.getDefaultTheme());
+ } else {
+ sealTheme = PdfSealThemeRegistry.getDefaultTheme();
+ }
+
+ PdfSealPosition position = PdfSealPosition.BOTTOM_RIGHT;
+ if (request.x() != null && request.y() != null) {
+ position = PdfSealPosition.CUSTOM;
+ } else if (request.position() != null) {
+ try {
+ position = PdfSealPosition.valueOf(request.position().toUpperCase());
+ } catch (Exception ignored) {}
+ }
+
+ PdfSealPageChoice pageChoice = PdfSealPageChoice.LAST_PAGE;
+ if (request.pageChoice() != null) {
+ try {
+ pageChoice = PdfSealPageChoice.valueOf(request.pageChoice().toUpperCase());
+ } catch (Exception ignored) {}
+ }
+
+ CpfDisplayMode cpfMode = sealTheme.defaultCpfMode();
+ if (request.cpfDisplayMode() != null) {
+ try {
+ cpfMode = CpfDisplayMode.valueOf(request.cpfDisplayMode().toUpperCase());
+ } catch (Exception ignored) {}
+ }
+
+ boolean includeDateTime = request.includeDateTime() != null
+ ? request.includeDateTime()
+ : sealTheme.defaultIncludeDateTime();
+
+ boolean visibleSeal = request.visibleSeal() == null || Boolean.TRUE.equals(request.visibleSeal());
+ double customScale = request.scale() != null && request.scale() > 0 ? request.scale() : 1.0;
+
+ PdfSignatureOptions options = new PdfSignatureOptions(
+ cert.id(),
+ null,
+ visibleSeal,
+ sealTheme,
+ position,
+ pageChoice,
+ request.reason() != null ? request.reason() : "Assinatura Digital de Documento",
+ request.location() != null ? request.location() : "Brasil",
+ request.x(),
+ request.y(),
+ request.page(),
+ cpfMode,
+ includeDateTime,
+ request.customExtraInfo(),
+ customScale
+ );
+
+ signLock.lock();
+ try {
+ byte[] signedPdfBytes = pdfSigningService.signPdf(pdfBytes, options);
+ String signedPdfBase64 = Base64.getEncoder().encodeToString(signedPdfBytes);
+
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] shaBytes = md.digest(signedPdfBytes);
+ String shaHex = bytesToHex(shaBytes);
+
+ Optional keyMat = certificateManager.getKeyMaterial(cert.id(), null);
+ String certBase64 = null;
+ List chainBase64 = Collections.emptyList();
+ if (keyMat.isPresent()) {
+ try {
+ certBase64 = Base64.getEncoder().encodeToString(keyMat.get().signingCertificate().getEncoded());
+ chainBase64 = Arrays.stream(keyMat.get().certificateChain())
+ .map(c -> {
+ try {
+ return Base64.getEncoder().encodeToString(c.getEncoded());
+ } catch (Exception e) {
+ return "";
+ }
+ })
+ .filter(s -> !s.isEmpty())
+ .toList();
+ } catch (Exception ignored) {}
+ }
+
+ String subjectName = X509CertificateUtils.extractHolderName(cert.commonName());
+ String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj();
+
+ CcspResponse response = CcspResponse.pdfSuccess(
+ request.requestId(),
+ signedPdfBase64,
+ shaHex,
+ null,
+ certBase64,
+ chainBase64,
+ "SHA256withRSA",
+ subjectName,
+ cpfCnpj,
+ request.documentName() != null ? request.documentName() : "documento.pdf"
+ );
+
+ sendJsonResponse(exchange, 200, response);
+
+ } catch (CertificateProviderException cpe) {
+ sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage());
+ } catch (Exception e) {
+ logger.error("Falha ao assinar PDF: {}", e.getMessage(), e);
+ sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
+ "Erro ao assinar PDF: " + e.getMessage());
+ } finally {
+ signLock.unlock();
+ }
+ }
+ }
+
+ private class SignXmlHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if (handleCors(exchange)) return;
+
+ if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
+ sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas POST é suportado.");
+ return;
+ }
+
+ String origin = getOrigin(exchange);
+ CcspRequest request;
+ try (InputStream is = exchange.getRequestBody()) {
+ request = objectMapper.readValue(is, CcspRequest.class);
+ } catch (Exception e) {
+ sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
+ return;
+ }
+
+ if (!request.isVersionSupported()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
+ "Versão do protocolo não suportada: " + request.version());
+ return;
+ }
+
+ if (request.isExpired()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REQUEST_EXPIRED,
+ "A requisição de assinatura expirou.");
+ return;
+ }
+
+ if (request.nonce() != null && !request.nonce().isBlank()) {
+ boolean accepted = nonceManager.registerNonce(request.nonce());
+ if (!accepted) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.REPLAY_DETECTED,
+ "Requisição duplicada detectada (replay prevenido).");
+ return;
+ }
+ }
+
+ String appId = request.applicationId();
+ if (appId == null || appId.isBlank()) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED,
+ "application_id é obrigatório.");
+ return;
+ }
+
+ if (!applicationRegistry.isAuthorized(appId, origin)) {
+ boolean approved = promptApplicationAuthorization(origin, "Aplicação Web", appId);
+ if (!approved) {
+ sendCcspError(exchange, 403, request.requestId(), CcspErrorCode.APPLICATION_NOT_AUTHORIZED,
+ "Aplicação não autorizada pelo usuário.");
+ return;
+ }
+ }
+
+ byte[] xmlBytes;
+ if (request.xmlBase64() != null && !request.xmlBase64().isBlank()) {
+ try {
+ xmlBytes = Base64.getDecoder().decode(request.xmlBase64());
+ } catch (Exception e) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
+ "O campo 'xml_base64' não contém dados válidos.");
+ return;
+ }
+ } else if (request.xml() != null && !request.xml().isBlank()) {
+ xmlBytes = request.xml().getBytes(StandardCharsets.UTF_8);
+ } else {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
+ "É obrigatório fornecer o XML em texto ('xml') ou Base64 ('xml_base64').");
+ return;
+ }
+
+ Optional certOpt = certificateManager.findByHint(request.certificateHint());
+ if (certOpt.isEmpty()) {
+ sendCcspError(exchange, 404, request.requestId(), CcspErrorCode.CERTIFICATE_NOT_FOUND,
+ "Nenhum certificado correspondente encontrado.");
+ return;
+ }
+ ManagedCertificate cert = certOpt.get();
+
+ if (promptListener != null) {
+ try {
+ CompletableFuture approvalFuture = promptListener.requestSignatureConfirmation(request, cert);
+ Boolean confirmed = approvalFuture.get(60, TimeUnit.SECONDS);
+ if (!Boolean.TRUE.equals(confirmed)) {
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
+ "Operação de assinatura cancelada pelo usuário.");
+ return;
+ }
+ } catch (Exception e) {
+ logger.warn("Falha ou timeout na confirmação visual de assinatura XML: {}", e.getMessage());
+ sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.USER_CANCELLED,
+ "Confirmação da assinatura expirou ou foi cancelada.");
+ return;
+ }
+ }
+
+ XmlSignatureProfile profile = XmlSignatureProfile.XADES_BES;
+ if (request.profile() != null) {
+ try {
+ profile = XmlSignatureProfile.valueOf(request.profile().toUpperCase());
+ } catch (Exception ignored) {}
+ }
+
+ String algo = request.algorithm() != null && !request.algorithm().isBlank()
+ ? request.algorithm()
+ : XmlSignatureOptions.DEFAULT_ALGORITHM;
+
+ XmlSignatureOptions options = new XmlSignatureOptions(
+ cert.id(),
+ null,
+ request.targetElementTag(),
+ request.targetElementId(),
+ algo,
+ profile
+ );
+
+ signLock.lock();
+ try {
+ byte[] signedXmlBytes = xmlSigningService.signXml(xmlBytes, options);
+ String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
+
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] shaBytes = md.digest(signedXmlBytes);
+ String shaHex = bytesToHex(shaBytes);
+
+ Optional keyMat = certificateManager.getKeyMaterial(cert.id(), null);
+ String certBase64 = null;
+ List chainBase64 = Collections.emptyList();
+ if (keyMat.isPresent()) {
+ try {
+ certBase64 = Base64.getEncoder().encodeToString(keyMat.get().signingCertificate().getEncoded());
+ chainBase64 = Arrays.stream(keyMat.get().certificateChain())
+ .map(c -> {
+ try {
+ return Base64.getEncoder().encodeToString(c.getEncoded());
+ } catch (Exception e) {
+ return "";
+ }
+ })
+ .filter(s -> !s.isEmpty())
+ .toList();
+ } catch (Exception ignored) {}
+ }
+
+ String subjectName = X509CertificateUtils.extractHolderName(cert.commonName());
+ String cpfCnpj = cert.cpf() != null ? cert.cpf() : cert.cnpj();
+
+ CcspResponse response = CcspResponse.xmlSuccess(
+ request.requestId(),
+ signedXmlStr,
+ shaHex,
+ null,
+ certBase64,
+ chainBase64,
+ algo,
+ subjectName,
+ cpfCnpj,
+ request.documentName() != null ? request.documentName() : "documento.xml"
+ );
+
+ sendJsonResponse(exchange, 200, response);
+
+ } catch (CertificateProviderException cpe) {
+ sendCcspError(exchange, 400, request.requestId(), cpe.getErrorCode(), cpe.getMessage());
+ } catch (Exception e) {
+ logger.error("Falha ao assinar XML: {}", e.getMessage(), e);
+ sendCcspError(exchange, 500, request.requestId(), CcspErrorCode.SIGNATURE_FAILED,
+ "Erro ao assinar XML: " + e.getMessage());
+ } finally {
+ signLock.unlock();
+ }
+ }
+ }
+
+ private class SealsHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ if (handleCors(exchange)) return;
+
+ if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
+ sendErrorResponse(exchange, 405, "METHOD_NOT_ALLOWED", "Apenas GET é suportado.");
+ return;
+ }
+
+ List