Author SHA1 Message Date
Claudecio Martins f4adc99a01 fix: update CPF and CNPJ document formatting and parsing logic in demo sdk 2026-10-11 21:10:47 -03:00
Claudecio Martins 5f4a212786 feat: implement in-memory log buffer, add ICP-Brasil seal assets, and improve SDK parameter handling 2026-10-11 20:58:49 -03:00
Claudecio Martins 2a50704a7f refactor: update PDF seal text, improve CCSP logging, and add signed PDF response aliases 2026-10-11 20:11:45 -03:00
Claudecio Martins 994a27886e feat: update PDF stamp picker and server CORS settings for enhanced UI rendering 2026-10-11 19:45:30 -03:00
Claudecio Martins b46d8b665f feat: add SDK support for PDF document signing and visual seal customization tools 2026-10-11 18:34:47 -03:00
Claudecio Martins d8d2e6ee46 docs: update documentation files and project README structure 2026-10-11 18:04:46 -03:00
Claudecio Martins 176acf6f40 feat: add SVG asset management, icons, and UI component enhancements 2026-10-11 12:27:47 -03:00
Claudecio Martins 3fab68c118 feat(xml): implementar assinatura digital no padrão XAdES-BES (ETSI TS 101 903 / ICP-Brasil) 2026-10-11 11:28:35 -03:00
Claudecio Martins 0cd4025f3c fix(pdf): assinatura visual PAdES em conformidade ITI via SignatureOptions e nome limpo do assinante sem CPF 2026-10-11 10:57:19 -03:00
Claudecio Martins 5360caaddf fix(pdf): co-assinatura incremental com preservação de assinaturas anteriores e trava anti-salto no redimensionamento 2026-10-11 10:43:59 -03:00
Claudecio Martins 4c30649598 feat(pdf): suporte a redimensionamento interativo e proporcional do selo de assinatura 2026-10-11 10:30:49 -03:00
86 changed files with 7271 additions and 1084 deletions
+111 -74
View File
@@ -1,100 +1,137 @@
# CyberCore Signer # CyberCore Signer
> **Agente Desktop de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.** > **Agente Desktop Nativo de Assinatura Digital e Autenticação Criptográfica (A1 / A3 / PKCS#11) para Windows e Linux.**
O **CyberCore Signer** é uma solução desktop em Java 21 LTS com JavaFX que permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e carimbos do tempo utilizando certificados digitais ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) instalados ou conectados na estação de trabalho do usuário. [![Java 21+](https://img.shields.io/badge/Java-21%2B%20LTS-orange.svg)](https://openjdk.org/)
[![JavaFX 25](https://img.shields.io/badge/JavaFX-25-blue.svg)](https://openjfx.io/)
[![Protocol CCSP/1](https://img.shields.io/badge/Protocol-CCSP%2F1-green.svg)](docs/CCSP.md)
[![Standard ICP-Brasil](https://img.shields.io/badge/Standard-ICP--Brasil-emerald.svg)](https://www.gov.br/iti/pt-br)
[![License](https://img.shields.io/badge/License-Proprietary-darkred.svg)](#licença)
A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP/WebSocket localhost autenticado e seguro. O **CyberCore Signer** é uma solução desktop desenvolvida em Java 21+ LTS (homologada em Java 25 LTS) com interface gráfica moderna JavaFX e segurança criptográfica estrita JCA/JCE. Permite que aplicações web autorizadas solicitem assinaturas digitais, autenticação de sessão e validação de documentos utilizando certificados digitais padrão ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) conectados ou instalados na máquina do usuário.
A comunicação local entre a página web e o agente utiliza o protocolo **CCSP (CyberCore Signing Protocol)** via HTTP localhost (`127.0.0.1:21325`), garantindo isolamento total de rede, consentimento visual explícito e preservação integral das chaves privadas na máquina local.
---
## ⚡ Quick Start
### 🌐 Para Aplicações Web (via SDK JavaScript)
Instale e utilize o SDK oficial `@cybercore/signer-sdk` para conectar sua aplicação web ao agente local em poucos passos:
```javascript
import { CyberCoreSigner } from '@cybercore/signer-sdk';
// 1. Inicializar o cliente do assinador
const signer = new CyberCoreSigner({
applicationName: 'Portal Meu Negócio'
});
// 2. Obter certificados disponíveis na máquina do usuário
const certificates = await signer.getCertificates();
const cert = certificates[0]; // Seleciona o primeiro certificado disponível
// 3. Assinar o hash SHA-256 de um documento (a chave nunca sai da máquina!)
const result = await signer.sign({
certificateId: cert.id,
digest: '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
documentName: 'contrato_prestacao_servicos.pdf'
});
console.log('Assinatura gerada:', result.signature);
console.log('Certificado titular:', result.subject_name);
```
> Para o manual completo com exemplos de login digital, assinatura de PDF e XML, consulte o [Guia do SDK](docs/sdk.md).
---
### ☕ Para Desenvolvedores Desktop
#### Requisitos
* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin ou Corretto).
* **Maven:** 3.9+ (ou utilizar o `./mvnw` embutido).
* **Sistema Operacional:** Windows 10/11 ou Linux x86_64.
#### Compilação e Execução
```bash
# Compilar e gerar o JAR consolidado (Fat JAR sombreado):
./mvnw clean package -DskipTests
# Executar o agente no Linux / macOS:
./run.sh
# Executar o agente no Windows:
run.bat
# Executar com logs detalhados de diagnóstico:
java -jar modules/cybercore-app/target/cybercore-signer.jar --dev
```
--- ---
## 🏛 Arquitetura do Projeto ## 🏛 Arquitetura do Projeto
O projeto é estruturado em módulos Maven: O projeto é estruturado em módulos Maven desacoplados, garantindo isolamento entre regras criptográficas de baixo nível e a camada de interface gráfica:
* **[modules/cybercore-core](modules/cybercore-core)**: Núcleo de regras criptográficas (JCA/JCE, SunPKCS11, X.509, PAdES, XMLDSig, carimbo de tempo TSA, LTV e validação de cadeias). Não possui dependência de interface gráfica. * **[`modules/cybercore-core`](modules/cybercore-core):** Núcleo de regras criptográficas (JCA/JCE, SunPKCS11, X.509, PAdES com Apache PDFBox e Bouncy Castle, XAdES-BES/XMLDSig, carimbo de tempo TSA, LTV, fila FIFO de concorrência e validação de cadeias). **Agnóstico de UI**, sem nenhuma dependência de JavaFX.
* **[modules/cybercore-ui](modules/cybercore-ui)**: Interface gráfica com o usuário construída em JavaFX (Dashboard, Gerenciador de Certificados, Diagnóstico Criptográfico, Diálogo de Confirmação de Assinatura, PIN dialog e System Tray). * **[`modules/cybercore-ui`](modules/cybercore-ui):** Interface gráfica com o usuário construída em JavaFX 25, apresentando design moderno com alternância dinâmica Claro/Escuro (Dual-Theme), Dashboard, Gerenciador de Certificados, Designer de Selos Visuais SVG com Live Preview, Diálogos de Confirmação de Assinatura, PIN dialog e System Tray.
* **[modules/cybercore-app](modules/cybercore-app)**: Ponto de entrada da aplicação (`CyberCoreApp`), orquestrador dos serviços de ciclo de vida e servidor CCSP local. * **[`modules/cybercore-app`](modules/cybercore-app):** Ponto de entrada da aplicação (`CyberCoreApp`), orquestrador dos serviços de ciclo de vida, diagnóstico pré-inicialização e servidor CCSP local.
* **[sdk/](sdk)**: SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração de aplicações web com o agente desktop. * **[`sdk/`](sdk):** SDK JavaScript oficial (`@cybercore/signer-sdk`) para integração simplificada de aplicações web modernas com o agente desktop.
* **[docs/](docs)**: Documentação técnica detalhada de arquitetura, protocolo, segurança e casos de uso. * **[`docs/`](docs):** Documentação técnica abrangente contendo especificações do protocolo, arquitetura, segurança e manuais de integração.
--- ---
## 🚀 Requisitos ## 📊 Matriz de Recursos e Conformidade
* **Java Development Kit (JDK):** Versão 21+ LTS (OpenJDK, Temurin, Corretto, etc.). | Recurso | Padrão / Tecnologia | Descrição |
* **Maven:** 3.9+ (ou utilizar o wrapper `./mvnw` incluído no projeto). |---|---|---|
* **Sistema Operacional Alvo:** Windows 10/11 ou Linux (x86_64). | **Certificados A1** | PKCS#12 / `.pfx` / `.p12` | Leitura em memória volátil com destruição segura de senhas |
| **Certificados A3** | Windows-MY (CryptoAPI/CNG) & PKCS#11 | Acesso nativo a tokens USB e smartcards (SafeSign, SafeNet, GD Burti, OpenSC) |
| **Assinatura PDF** | PAdES (ISO 32000-1 / ETSI TS 102 778) | Envelope CMS/PKCS#7, suporte a salvamento incremental e carimbo TSA |
| **Selos Visuais PDF** | Renderização Java2D de Alta Resolução (300 DPI) | Brasões vetoriais, upload de logos, controle de sigilo de CPF (LGPD) e pré-visualização interativa |
| **Assinatura XML** | XAdES-BES (ETSI TS 101 903 / DOC-ICP-15.03) | Suporte completo a NF-e, CT-e, MDF-e, eSocial e documentos envelopados |
| **Assinatura Genérica** | CMS / CAdES / P7S / P7M | Modalidades atachada (enveloped) e destacada (detached) |
| **Login Digital** | Challenge-Response ICP-Brasil | Autenticação web segura sem senhas tradicionais |
| **Validação** | Verificação Criptográfica Local | Auditoria de integridade de hashes, validade de certificados e signatários |
| **Interface Desktop** | JavaFX 25 com Dual-Theme | Modo Claro e Modo Escuro com comutação instantânea e atalhos na bandeja |
| **Protocolo Local** | CCSP/1 REST (`127.0.0.1:21325`) | Bind estrito em loopback, proteção anti-replay via nonce e autorização de origens |
--- ---
## 🛠 Compilação e Empacotamento ## 🔒 Princípios Fundamentais de Segurança
### 1. Compilação do JAR Executável 1. **A Chave Privada Permanece na Estação de Trabalho:** A chave privada e os segredos (PIN do token A3 ou senha do arquivo A1) nunca saem da máquina do usuário nem trafegam pela rede ou saem em logs.
2. **Assinatura por Digest Descentralizada:** Por padrão, a aplicação web calcula o hash SHA-256 do documento localmente e o CyberCore assina apenas o digest de 32 bytes, eliminando o tráfego desnecessário de arquivos inteiros.
Para compilar e gerar o JAR consolidado (com todas as dependências sombreadas e assets): 3. **Consentimento Visual Explícito:** Nenhuma operação de assinatura ocorre silenciosamente. Toda requisição exige autorização física do usuário em diálogo nativo do sistema operacional.
4. **Isolamento de Origem e Anti-Replay:** Cada requisição web deve apresentar identificador de aplicação aprovado, nonce único com validação temporal (`expires_at`) e cabeçalho de origem verificado.
```bash 5. **Fila FIFO de Operações:** O acesso a tokens físicos A3 é protegido por trava de concorrência (`signLock`) serializada, prevenindo travamento ou corrupção de sessão nos chips criptográficos.
./build-jar.sh
```
Ou diretamente via Maven:
```bash
./mvnw clean package -DskipTests
```
O artefato consolidado será gerado em:
`modules/cybercore-app/target/cybercore-signer.jar`
### 2. Execução Local
Para iniciar o agente em ambiente de desenvolvimento:
```bash
# No Linux / macOS:
./run.sh
# No Windows:
run.bat
```
### 3. Empacotamento com Runtime Nativo (`jpackage`)
Para criar a distribuição com Java Runtime embutido (sem exigir JDK pré-instalado pelo usuário final):
```bash
./scripts/package-jpackage.sh
```
--- ---
## 🔒 Princípios de Segurança e Privacidade ## 📚 Documentação Técnica Detalhada
1. **Chave Privada Preservada:** A chave privada e os segredos (PIN de token A3 e senha de certificado A1) nunca saem da máquina do usuário nem trafegam pela rede. Para detalhes completos sobre arquitetura, protocolos e manuais técnicos, acesse o [Portal de Documentação](docs/README.md) ou consulte diretamente os links abaixo:
2. **Assinatura por Digest:** Por padrão, a aplicação web calcula o hash/digest (ex: SHA-256) do documento e o CyberCore assina localmente apenas o digest, minimizando o tráfego de dados sensíveis.
3. **Confirmação Explícita:** Toda operação de assinatura exige consentimento e autorização visual do usuário antes da aplicação da chave. * 🌐 **[Protocolo CCSP/1](docs/CCSP.md):** Contrato REST, endpoints, códigos de status e anti-replay.
4. **Isolamento de Origem e CCSP:** O agente atende apenas origens web autorizadas, protegendo contra abusos via CSRF ou requisições locais não autenticadas. * 📦 **[SDK Web JavaScript](docs/sdk.md):** Guia de integração e métodos da biblioteca oficial.
* 🏛 **[Arquitetura do Sistema](docs/architecture.md):** Módulos, threading e fluxo sequencial.
* 💳 **[Certificados A1, A3 e PKCS#11](docs/a1-a3-certificates.md):** Suporte a hardware, drivers e OIDs ICP-Brasil.
* 📄 **[Assinaturas PDF e Selos SVG](docs/pades.md):** PAdES, renderização Java2D e selos visuais.
* 📑 **[Assinaturas XML (XAdES-BES)](docs/xml-xmldsig.md):** Padrão DOC-ICP-15.03 e notas fiscais.
* 🔑 **[Autenticação e Login Digital](docs/authentication-login.md):** Fluxo Challenge-Response via ICP-Brasil.
* ⚠️ **[Catálogo Estruturado de Erros](docs/errors.md):** Tabela de erros `CcspErrorCode` e códigos HTTP.
* 🛡️ **[Modelo de Segurança e Privacidade](docs/security.md):** Diretrizes de isolamento, memória e LGPD.
* 🩺 **[Diagnóstico e Troubleshooting](docs/diagnostics.md):** Auto-teste e guia de resolução de problemas.
* 💿 **[Instalação e Empacotamento Nativo](docs/installation.md):** Distribuição via `jpackage` sem JRE pré-instalado.
* ⚖️ **[Limitações Técnicas](docs/limitations.md):** Declaração de dependências conforme a Regra 23.
* 🛠️ **[Modo de Desenvolvimento](docs/development-mode.md):** Parâmetro `--dev` e certificados de teste.
* 🎨 **[Layout Dual-Theme e Assinatura em Lote](docs/serpro-layout-and-batch.md):** Design da UI e operações em lote.
--- ---
## 📚 Documentação Técnica ## 📄 Licença e Propriedade
Consulte a pasta [docs/](docs) para especificações completas: Copyright © 2026 **CyberCore Systems**. Todos os direitos reservados.
Este software é proprietário e confidencial. O uso, distribuição ou modificação não autorizados são estritamente proibidos.
* [Protocolo CCSP (CyberCore Signing Protocol)](docs/CCSP.md)
* [Arquitetura e Fluxo do Sistema](docs/architecture.md)
* [Certificados A1 e A3 (PKCS#11 / MSCAPI)](docs/a1-a3-certificates.md)
* [Assinatura de Documentos PDF (PAdES / LTV / TSA)](docs/pades.md)
* [Assinatura de Documentos XML (XMLDSig)](docs/xml-xmldsig.md)
* [Autenticação e Sessão Criptográfica](docs/authentication-login.md)
* [Diagnóstico e Resolução de Problemas](docs/diagnostics.md)
* [Segurança e Tratamento de Erros](docs/security.md)
* [SDK Web JavaScript](docs/sdk.md)
* [Guia de Instalação](docs/installation.md)
---
## 📄 Licença
Propriedade de **CyberCore Systems**. Todos os direitos reservados.
+30 -19
View File
@@ -1,6 +1,6 @@
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer # Recursos Visuais (Ícones e Imagens em SVG) — CyberCore Signer
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**. Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente todos os **ícones em SVG** e as **imagens em SVG** do **CyberCore Signer**.
--- ---
@@ -31,13 +31,23 @@ assets/
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais │ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação │ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── error.svg # Ícone de Erro / Alerta em vermelho │ ├── error.svg # Ícone de Erro / Alerta em vermelho
│ └── star.svg # Ícone de Estrela notarial / favorita │ ├── star.svg # Ícone de Estrela notarial / favorita
│ ├── close.svg # Ícone Fechar / Cancelar (cruz geométrica moderna)
│ ├── arrow-right.svg # Ícone Avançar / Seta direcional
│ ├── zoom-in.svg # Ícone Zoom In (+)
│ ├── zoom-out.svg # Ícone Zoom Out (-)
│ ├── zoom-fit.svg # Ícone Ajustar à Tela
│ └── refresh.svg # Ícone Atualizar / Recarregar listas e status
│ │
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência) └── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro) ├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER) ├── app-icon.png # Fallback em PNG (256x256 px)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue) ├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px) ├── app-logo.png # Fallback em PNG (480x120 px)
├── brand-banner.svg # Banner de cabeçalho e marca em SVG (600x160 px)
├── brand-banner.png # Fallback em PNG (600x160 px)
├── default-seal.svg # Selo de assinatura padrão ICP-Brasil em SVG (640x164 px)
└── default-seal.png # Fallback em PNG (640x164 px)
``` ```
--- ---
@@ -45,20 +55,21 @@ assets/
## 🎨 Como Customizar Qualquer Ícone ou Imagem ## 🎨 Como Customizar Qualquer Ícone ou Imagem
1. **Ícones:** 1. **Ícones:**
- Estão no formato `.svg` padrão. - Todos estão no formato `.svg` padrão da W3C.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**. - Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
- Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente. - Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
2. **Imagens do Sistema:** 2. **Imagens do Sistema:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente). - Tanto o logotipo horizontal (`app-logo.svg`), o ícone do sistema (`app-icon.svg`), o banner institucional (`brand-banner.svg`) e o selo notarial padrão (`default-seal.svg`) agora são arquivos **SVG vetoriais editáveis**.
- Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa. - Você pode alterar cores, tipografias, brasões e dimensões diretamente nos arquivos SVG.
- O `AssetManager` renderiza os arquivos SVG preservando fidelidade geométrica e suportando gradientes e alinhamentos.
3. **Ordem de Prioridade e Hot-Reloading:** 3. **Ordem de Resolução e Carregamento:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem: Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar! 1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Arquivos `.svg` editados aqui têm prioridade máxima e entram em vigor imediatamente sem necessidade de recompilar.
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional. 2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Para customizações no diretório pessoal do usuário.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`. 3. **Recursos Empacotados no JAR (`/assets/`)**: SVGs e imagens internas empacotadas no pacote de distribuição.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído. 4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique desprovida de grafismos caso algum arquivo seja removido.
--- ---
+6 -1
View File
@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><title>info-circle-fill</title><path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.93-9.412l-1 4.705c-.07.34.029.533.304.533c.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598c-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081l.082-.381l2.29-.287zM8 5.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Círculo azul e letra 'i' informativa centrada em (24, 24) -->
<circle cx="24" cy="24" r="18" fill="#0288d1"/>
<circle cx="24" cy="15" r="2.5" fill="#ffffff"/>
<line x1="24" y1="21" x2="24" y2="33" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 362 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 8 5 L 15 12 L 8 19" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 6 6 L 18 18 M 18 6 L 6 18" fill="none" stroke="#64748b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

+4 -4
View File
@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Lua crescente ciano/índigo --> <!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
<path d="M 40 26.5 A 18 18 0 1 1 21.5 8 A 14 14 0 0 0 40 26.5 Z" fill="#38bdf8"/> <path d="M 24 8 A 16 16 0 1 0 40 24 A 14 14 0 0 1 24 8 Z" fill="#38bdf8"/>
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/> <circle cx="34" cy="14" r="1.8" fill="#818cf8"/>
<circle cx="42" cy="18" r="1" fill="#818cf8"/> <circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 344 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 20 4 L 20 9 L 15 9" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 4 20 L 4 15 L 9 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

+2 -6
View File
@@ -1,8 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Engrenagem simétrica de 8 dentes centrada em (24, 24) --> <!-- Engrenagem corporativa simétrica centrada em (24, 24) -->
<path d="M 21 4 L 27 4 L 28 10 C 30 10.8 31.8 11.8 33.4 13.2 L 39 10 L 43 14 L 39.8 19.6 C 41.2 21.2 42.2 23 43 25 L 49 26 L 49 32 L 43 33 C 42.2 35 41.2 36.8 39.8 38.4 L 43 44 L 39 48 L 33.4 44.8 C 31.8 46.2 30 47.2 28 48 L 27 54 L 21 54 L 20 48 C 18 47.2 16.2 46.2 14.6 44.8 L 9 48 L 5 44 L 8.2 38.4 C 6.8 36.8 5.8 35 5 33 L -1 32 L -1 26 L 5 25 C 5.8 23 6.8 21.2 8.2 19.6 L 5 14 L 9 10 L 14.6 13.2 C 16.2 11.8 18 10.8 20 10 Z" transform="scale(0.8) translate(6, 4)" fill="#0288d1"/> <path fill="#0288d1" fill-rule="evenodd" d="M 21.5 5 L 26.5 5 L 27.2 10.2 C 29 10.9 30.7 11.9 32.2 13.1 L 36.6 10.6 L 40.1 14.1 L 37.6 18.5 C 38.8 20 39.8 21.7 40.5 23.5 L 45.7 24.2 L 45.7 29.2 L 40.5 29.9 C 39.8 31.7 38.8 33.4 37.6 34.9 L 40.1 39.3 L 36.6 42.8 L 32.2 40.3 C 30.7 41.5 29 42.5 27.2 43.2 L 26.5 48.4 L 21.5 48.4 L 20.8 43.2 C 19 42.5 17.3 41.5 15.8 40.3 L 11.4 42.8 L 7.9 39.3 L 10.4 34.9 C 9.2 33.4 8.2 31.7 7.5 29.9 L 2.3 29.2 L 2.3 24.2 L 7.5 23.5 C 8.2 21.7 9.2 20 10.4 18.5 L 7.9 14.1 L 11.4 10.6 L 15.8 13.1 C 17.3 11.9 19 10.9 20.8 10.2 Z M 24 17.5 A 6.5 6.5 0 1 0 24 30.5 A 6.5 6.5 0 1 0 24 17.5 Z"/>
<!-- Círculo externo da engrenagem -->
<circle cx="24" cy="24" r="14" fill="#0288d1"/>
<!-- Furo central vazado -->
<circle cx="24" cy="24" r="6" fill="#ffffff"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 784 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 4 9 L 4 4 L 9 4 M 15 4 L 20 4 L 20 9 M 20 15 L 20 20 L 15 20 M 9 20 L 4 20 L 4 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M 10 7 L 10 13 M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1976d2"/>
<stop offset="45%" stop-color="#0d47a1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
<linearGradient id="topFacetGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="leftFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.75"/>
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.35"/>
</linearGradient>
<linearGradient id="rightFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.85"/>
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.45"/>
</linearGradient>
<radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fundo com cantos arredondados -->
<rect x="8" y="8" width="240" height="240" rx="68" ry="68" fill="url(#bgGrad)"/>
<rect x="10" y="10" width="236" height="236" rx="66" ry="66" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-opacity="0.25"/>
<!-- Facetas 3D do Cristal / Diamante Azul -->
<polygon points="128,46 210,113 128,143 46,113" fill="url(#topFacetGrad)"/>
<polygon points="46,113 128,143 128,215 72,179" fill="url(#leftFacetGrad)"/>
<polygon points="210,113 128,143 128,215 184,179" fill="url(#rightFacetGrad)"/>
<!-- Brilho estelar central -->
<circle cx="128" cy="128" r="38" fill="url(#centerGlow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
<defs>
<linearGradient id="logoIconBg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1976d2"/>
<stop offset="50%" stop-color="#0d47a1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
<linearGradient id="logoTopFacet" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.6"/>
</linearGradient>
<linearGradient id="logoLeftFacet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="logoRightFacet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.5"/>
</linearGradient>
</defs>
<!-- Container do Ícone Cristal 3D -->
<rect x="14" y="16" width="88" height="88" rx="24" ry="24" fill="url(#logoIconBg)"/>
<rect x="16" y="18" width="84" height="84" rx="22" ry="22" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-opacity="0.25"/>
<!-- Facetas Cristalinas (cx=58, cy=60, sz=63) -->
<polygon points="58,32 86,55 58,66 30,55" fill="url(#logoTopFacet)"/>
<polygon points="30,55 58,66 58,92 39,79" fill="url(#logoLeftFacet)"/>
<polygon points="86,55 58,66 58,92 77,79" fill="url(#logoRightFacet)"/>
<circle cx="58" cy="60" r="14" fill="#ffffff" fill-opacity="0.3"/>
<!-- Tipografia Oficial "CyberCore" -->
<text x="124" y="62" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="#0a2540">Cyber<tspan fill="#0288d1">Core</tspan></text>
<!-- Badge "SIGNER" -->
<rect x="124" y="76" width="94" height="22" rx="6" ry="6" fill="#0288d1"/>
<text x="139" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#ffffff" letter-spacing="1">SIGNER</text>
<!-- Subtítulo ICP-Brasil -->
<text x="230" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" fill="#64748b">ICP-Brasil • Assinador Digital Seguro</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+32
View File
@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" width="600" height="160">
<defs>
<linearGradient id="bannerBgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0d47a1"/>
<stop offset="60%" stop-color="#0284c7"/>
<stop offset="100%" stop-color="#0369a1"/>
</linearGradient>
<linearGradient id="bannerIconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e88e5"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
</defs>
<!-- Fundo do Banner -->
<rect x="0" y="0" width="600" height="160" rx="16" ry="16" fill="url(#bannerBgGrad)"/>
<!-- Formas geométricas de segurança no fundo -->
<circle cx="480" cy="50" r="110" fill="#ffffff" fill-opacity="0.08"/>
<circle cx="540" cy="120" r="80" fill="#ffffff" fill-opacity="0.06"/>
<circle cx="380" cy="-20" r="90" fill="#ffffff" fill-opacity="0.04"/>
<!-- Ícone Cristal à esquerda -->
<rect x="30" y="35" width="90" height="90" rx="22" ry="22" fill="url(#bannerIconGrad)"/>
<rect x="33" y="38" width="84" height="84" rx="19" ry="19" fill="none" stroke="#ffffff" stroke-width="2" stroke-opacity="0.3"/>
<polygon points="75,52 101,80 75,108 49,80" fill="#ffffff" fill-opacity="0.9"/>
<polygon points="75,52 75,108 49,80" fill="#80d8ff" fill-opacity="0.5"/>
<!-- Textos do Banner -->
<text x="148" y="74" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="34" font-weight="bold" fill="#ffffff">CyberCore Signer</text>
<text x="148" y="104" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="16" font-weight="normal" fill="#e0f2fe">Agente de Assinatura Digital ICP-Brasil A1 &amp; A3</text>
<text x="148" y="128" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#38bdf8" letter-spacing="1">PADES • XADES • CADES • TIMESTAMPS • CCSP/1</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+34
View File
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 164" width="640" height="164">
<defs>
<linearGradient id="badgeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0288d1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
</defs>
<!-- Fundo do Selo -->
<rect x="2" y="2" width="636" height="160" rx="12" ry="12" fill="#f8fafc" stroke="#0288d1" stroke-width="2.5"/>
<!-- Brasão Notarial no lado esquerdo -->
<rect x="18" y="16" width="130" height="132" rx="10" ry="10" fill="url(#badgeGrad)"/>
<!-- Textos do Brasão -->
<text x="83" y="44" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">ASSINADO</text>
<text x="83" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">DIGITALMENTE</text>
<!-- Estrela dourada -->
<circle cx="83" cy="85" r="14" fill="#fbbf24"/>
<text x="83" y="91" font-family="'Segoe UI', sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">★</text>
<text x="83" y="126" font-family="'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="bold" fill="#e0f2fe" text-anchor="middle" letter-spacing="1">ICP-BRASIL</text>
<!-- Linha divisória vertical -->
<line x1="166" y1="16" x2="166" y2="148" stroke="#cfd8dc" stroke-width="1.5"/>
<!-- Informações da Assinatura -->
<text x="184" y="36" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#64748b" letter-spacing="0.5">DOCUMENTO ASSINADO DIGITALMENTE POR:</text>
<text x="184" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="17" font-weight="bold" fill="#0a2540">NOME DO TITULAR DO CERTIFICADO</text>
<text x="184" y="82" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">CPF: ***.000.000-**</text>
<text x="184" y="102" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">Data e Hora: 11/10/2026 12:00:00 -03:00</text>
<text x="184" y="128" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-style="italic" fill="#0288d1">A conformidade com a ICP-Brasil pode ser verificada no ITI.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+294 -108
View File
@@ -1,28 +1,55 @@
# CyberCore Signing Protocol (CCSP/1) # CyberCore Signing Protocol (CCSP/1)
O **CyberCore Signing Protocol (CCSP)** é o protocolo padrão para comunicação local entre aplicações web rodando no navegador e o agente desktop **CyberCore Signer**. > **Especificação Técnica do Protocolo de Comunicação Local e API REST (Versão 1)**
A versão inicial descrita neste documento é a **CCSP/1**. O **CyberCore Signing Protocol (CCSP)** é o protocolo padrão para comunicação local entre aplicações web rodando no navegador do usuário e o agente desktop **CyberCore Signer**.
A versão atual especificada neste documento é a **CCSP/1**.
--- ---
## 1. Princípios de Segurança ## 1. Princípios de Segurança e Isolamento
1. **Isolamento de Rede:** O servidor local escuta exclusivamente no endereço de loopback `127.0.0.1` (porta padrão `21325`), impedindo qualquer acesso a partir da rede local (LAN) ou externa (WAN). O protocolo CCSP/1 adota o princípio de menor privilégio e segurança em profundidade:
2. **Controle Estrito de Origem (CORS):** O agente valida cabeçalhos `Origin` e responde apenas a pré-voos `OPTIONS` com origens autorizadas.
3. **Registro e Autorização de Aplicações:** Nenhuma aplicação desconhecida tem acesso a certificados ou assinaturas. Ao tentar conectar pela primeira vez, uma caixa de diálogo nativa solicita aprovação explícita do usuário. 1. **Loopback Estrito (`127.0.0.1`):** O servidor HTTP local vincula seu socket exclusivamente ao endereço IP `127.0.0.1` na porta padrão `21325`. O agente rejeita escuta em `0.0.0.0`, prevenindo qualquer acesso proveniente da rede local (LAN) ou externa (WAN).
4. **Confirmação Visual da Assinatura:** Toda operação de assinatura exige confirmação manual na interface gráfica, exibindo a aplicação solicitante, o hash do documento e o certificado selecionado. 2. **Validação de Cabeçalhos e DNS Rebinding:** O agente valida o cabeçalho `Host` para garantir que as requisições se originem estritamente de `127.0.0.1:21325` ou `localhost:21325`.
5. **Proteção Contra Ataques de Replay:** Toda requisição deve conter um `nonce` UUID único com validação temporal (`expires_at` ou `timestamp`). Requisições com nonces repetidos são sumariamente rejeitadas (`REPLAY_DETECTED`). 3. **Controle de Origem (CORS):** Todas as respostas incluem cabeçalhos CORS restritivos. Métodos HTTP `OPTIONS` prévios (preflight) são tratados para liberar apenas métodos e headers homologados (`Content-Type`, `Origin`, `X-CyberCore-AppId`).
6. **Privacidade Criptográfica:** Apenas o **digest (hash)** do documento é enviado ao agente. O documento completo nunca sai da aplicação web e a chave privada nunca sai do dispositivo/computador do usuário. 4. **Handshake de Autorização de Aplicações:** Nenhuma página web desconhecida pode listar certificados ou solicitar assinaturas. Uma aplicação deve se identificar (`POST /ccsp/v1/authorize`) e obter aprovação explícita do usuário via diálogo nativo do sistema operacional.
5. **Proteção Anti-Replay com Nonce e TTL:** Todas as chamadas de assinatura exigem um identificador pseudoaleatório `nonce` único e um timestamp de expiração (`expires_at`). Requisições com nonces repetidos são sumariamente rejeitadas com erro estruturado `REPLAY_DETECTED`.
6. **Privacidade Criptográfica por Digest:** Por padrão, a aplicação web calcula o hash SHA-256 no próprio navegador utilizando a Web Crypto API nativa e envia apenas o digest de 32 bytes (em Base64). O documento confidencial nunca trafega para o agente e a chave privada nunca sai do computador ou do chip criptográfico do usuário.
7. **Trava FIFO Serializada (`signLock`):** O processamento de assinaturas físicas em smartcards/tokens USB A3 é serializado em fila FIFO para evitar colisões no hardware mono-thread do middleware do fabricante.
--- ---
## 2. Endpoints do Protocolo (CCSP/1) ## 2. Visão Geral dos Endpoints (CCSP/1)
### 2.1 `GET /ccsp/v1/status` | Método | Endpoint | Finalidade | Autenticação Requerida |
Verifica se o agente está instalado, em execução e pronto para operações. |---|---|---|---|
| `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 |
| `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 |
#### Resposta de Sucesso (200 OK) ---
## 3. Especificação Detalhada dos Endpoints
### 3.1 `GET /ccsp/v1/status`
Verifica se o agente desktop está instalado, em execução e pronto para operações criptográficas.
#### Requisição
```http
GET /ccsp/v1/status HTTP/1.1
Host: 127.0.0.1:21325
Origin: https://meusistema.empresa.com.br
```
#### Resposta de Sucesso (`200 OK`)
```json ```json
{ {
"version": "1", "version": "1",
@@ -30,20 +57,58 @@ Verifica se o agente está instalado, em execução e pronto para operações.
"agent": "CyberCore Signer", "agent": "CyberCore Signer",
"agent_version": "0.1.0", "agent_version": "0.1.0",
"os": "Windows 11", "os": "Windows 11",
"timestamp": "2026-10-08T19:40:00Z" "timestamp": "2026-10-11T20:45:00Z"
} }
``` ```
--- ---
### 2.2 `GET /ccsp/v1/certificates` ### 3.2 `POST /ccsp/v1/authorize`
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura. Inicia o handshake para autorizar uma nova aplicação web. Quando a aplicação ainda não foi homologada pelo usuário, o CyberCore Signer suspende a requisição e exibe um modal nativo na tela do desktop para confirmação.
#### Headers / Parâmetros #### Cabeçalhos
- `X-CyberCore-AppId`: UUID da aplicação autorizada (ou query param `?application_id=uuid`). - `Content-Type: application/json`
- `Origin: https://meusistema.empresa.com.br`
#### Payload da Requisição
```json
{
"version": "1",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Portal Jurídico e Tributário"
}
```
#### Resposta de Sucesso (`200 OK`)
```json
{
"version": "1",
"status": "authorized",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"message": "Aplicação autorizada pelo usuário com sucesso."
}
```
#### Resposta de Recusa do Usuário (`403 Forbidden`)
```json
{
"version": "1",
"status": "error",
"error_code": "APPLICATION_NOT_AUTHORIZED",
"message": "O usuário recusou a autorização para esta aplicação."
}
```
---
### 3.3 `GET /ccsp/v1/certificates`
Retorna a lista de certificados digitais atualmente disponíveis e aptos para assinatura no sistema (A1 importados e A3 conectados via Windows Store ou PKCS#11).
#### Cabeçalhos / Parâmetros
- `Origin`: Origem web autorizada. - `Origin`: Origem web autorizada.
- `X-CyberCore-AppId` (Header) ou `?application_id=uuid` (Query Param).
#### Resposta de Sucesso (200 OK) #### Resposta de Sucesso (`200 OK`)
```json ```json
{ {
"version": "1", "version": "1",
@@ -51,16 +116,18 @@ Retorna a lista de certificados digitais atualmente disponíveis e aptos para as
"certificates": [ "certificates": [
{ {
"id": "win-4340656a", "id": "win-4340656a",
"alias": "CLAUDIO SILVA:12345678900", "alias": "MARIA DA SILVA:12345678900",
"common_name": "CLAUDIO SILVA:12345678900", "common_name": "MARIA DA SILVA:12345678900",
"subject_dn": "CN=CLAUDIO SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR", "subject_dn": "CN=MARIA DA SILVA:12345678900, OU=Autenticado por AR, O=ICP-Brasil, C=BR",
"issuer_dn": "CN=AC SERPRO v5, O=ICP-Brasil, C=BR", "issuer_dn": "CN=AC SERPRO v5, OU=Servico Federal de Processamento de Dados, O=ICP-Brasil, C=BR",
"type": "A3", "type": "A3",
"provider_id": "provider-win-store", "provider_id": "provider-win-store",
"device_name": "Token / Smart Card (Windows CAPI/CNG)", "device_name": "Token USB / Smart Card (Windows CAPI/CNG)",
"valid_from": "2024-05-10T12:00:00Z", "valid_from": "2025-01-10T10:00:00Z",
"valid_to": "2027-05-10T12:00:00Z", "valid_to": "2028-01-10T10:00:00Z",
"serial_number": "72910482910482019482", "serial_number": "72910482910482019482",
"cpf": "12345678900",
"cnpj": null,
"can_sign": true, "can_sign": true,
"status": "AVAILABLE" "status": "AVAILABLE"
} }
@@ -70,33 +137,10 @@ Retorna a lista de certificados digitais atualmente disponíveis e aptos para as
--- ---
### 2.3 `POST /ccsp/v1/authorize` ### 3.4 `POST /ccsp/v1/sign`
Handshake inicial de uma nova aplicação web para solicitar autorização ao usuário. Solicita a assinatura digital local do digest informado com o certificado escolhido.
#### Requisição #### Payload da Requisição
```json
{
"version": "1",
"application_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Portal Assinaturas Jurídicas"
}
```
#### Resposta (200 OK)
```json
{
"version": "1",
"status": "authorized",
"application_id": "550e8400-e29b-41d4-a716-446655440000"
}
```
---
### 2.4 `POST /ccsp/v1/sign`
Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquivos gerais).
#### Requisição
```json ```json
{ {
"version": "1", "version": "1",
@@ -106,103 +150,245 @@ Executa a assinatura digital do digest informado (PDF, XML, documentos ou arquiv
"algorithm": "SHA256withRSA", "algorithm": "SHA256withRSA",
"digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", "digest": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
"certificate_hint": "win-4340656a", "certificate_hint": "win-4340656a",
"document_name": "contrato_locacao.pdf", "document_name": "contrato_prestacao_servicos.pdf",
"document_type": "PDF", "document_type": "PDF",
"nonce": "c4ca4238a0b923820dcc509a6f75849b", "nonce": "c4ca4238a0b923820dcc509a6f75849b",
"expires_at": "2026-10-08T19:45:00Z" "expires_at": "2026-10-11T20:50:00Z"
} }
``` ```
#### Resposta de Sucesso (200 OK) #### Resposta de Sucesso (`200 OK`)
```json ```json
{ {
"version": "1", "version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "signed", "status": "signed",
"signature": "MEUCIQDq...base64...", "signature": "MEUCIQDq95kZc...Base64...",
"certificate": "MIIFazC...base64...", "certificate": "MIIFazCCBF...Base64...",
"certificate_chain": [ "certificate_chain": [
"MIIFazC...base64...", "MIIFazCCBF...Base64...",
"MIIEazC...base64..." "MIIEazCCA1...Base64..."
], ],
"algorithm": "SHA256withRSA", "algorithm": "SHA256withRSA",
"subject_name": "CLAUDIO SILVA", "subject_name": "MARIA DA SILVA",
"subject_cpf_cnpj": "12345678900", "subject_cpf_cnpj": "12345678900",
"document_name": "contrato_locacao.pdf", "document_name": "contrato_prestacao_servicos.pdf",
"document_type": "PDF" "document_type": "PDF"
} }
``` ```
--- ---
### 2.5 `POST /ccsp/v1/authenticate` ### 3.5 `POST /ccsp/v1/authenticate`
Executa o login / autenticação digital segura de um usuário em sistemas web via desafio criptográfico (Challenge-Response ICP-Brasil). Executa o login criptográfico de um usuário em sistemas web através do mecanismo de desafio e resposta (*Challenge-Response*).
#### Requisição #### Payload da Requisição
```json ```json
{ {
"version": "1", "version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "request_id": "8f3b201a-8c9d-4e5f-b012-3456789abcde",
"application_id": "550e8400-e29b-41d4-a716-446655440000", "application_id": "550e8400-e29b-41d4-a716-446655440000",
"operation": "authenticate", "operation": "authenticate",
"challenge": "server-session-challenge-nonce-2026", "challenge": "session-nonce-random-server-token-998877",
"certificate_hint": "win-4340656a", "certificate_hint": "win-4340656a",
"purpose": "AUTHENTICATION", "purpose": "AUTHENTICATION",
"nonce": "c4ca4238a0b923820dcc509a6f75849b", "nonce": "d3b07384d113edec49eaa6238ad5ff00",
"expires_at": "2026-10-08T19:45:00Z" "expires_at": "2026-10-11T20:50:00Z"
} }
``` ```
#### Resposta de Sucesso (200 OK) #### Resposta de Sucesso (`200 OK`)
```json ```json
{ {
"version": "1", "version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "request_id": "8f3b201a-8c9d-4e5f-b012-3456789abcde",
"status": "authenticated", "status": "authenticated",
"signature": "MEUCIQDq...base64...", "signature": "MEQCIF61pP...Base64...",
"certificate": "MIIFazC...base64...", "certificate": "MIIFazCCBF...Base64...",
"certificate_chain": [ "certificate_chain": [
"MIIFazC...base64..." "MIIFazCCBF...Base64...",
"MIIEazCCA1...Base64..."
], ],
"algorithm": "SHA256withRSA", "algorithm": "SHA256withRSA",
"subject_name": "CLAUDIO SILVA", "subject_name": "MARIA DA SILVA",
"subject_cpf_cnpj": "12345678900", "subject_cpf_cnpj": "12345678900"
"document_type": "AUTH_CHALLENGE"
}
```
#### Resposta de Erro (400/403/404)
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "error",
"error_code": "USER_CANCELLED",
"message": "Operação de assinatura cancelada pelo usuário."
} }
``` ```
--- ---
## 3. Tabela de Códigos de Erro Estruturados ### 3.6 `GET /ccsp/v1/seals`
Retorna a lista de temas e estilos de selos visuais embutidos e instalados no CyberCore Signer.
| Código | Significado | #### Cabeçalhos
|---|---| - `Origin: https://meusistema.empresa.com.br`
| `CERTIFICATE_NOT_FOUND` | Certificado indicado no hint não foi encontrado. | - `X-CyberCore-AppId: 550e8400-e29b-41d4-a716-446655440000`
| `CERTIFICATE_EXPIRED` | O certificado selecionado está fora do período de vigência. |
| `CERTIFICATE_INVALID` | O certificado é inválido ou corrompido. | #### Resposta de Sucesso (`200 OK`)
| `PRIVATE_KEY_NOT_AVAILABLE` | Chave privada inacessível ou ausente. | ```json
| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. | {
| `TOKEN_REMOVED` | Dispositivo de hardware foi removido durante a assinatura. | "version": "1",
| `PIN_REQUIRED` | PIN obrigatório para liberar chave privada em PKCS#11. | "status": "success",
| `PIN_INCORRECT` | PIN digitado está incorreto. | "default_seal_id": "icp-brasil",
| `PKCS11_ERROR` | Falha no middleware PKCS#11 do fabricante. | "seals": [
| `SIGNATURE_FAILED` | Erro durante a geração da assinatura digital. | {
| `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou recusada pelo usuário. | "id": "icp-brasil",
| `REQUEST_EXPIRED` | A data/hora de validade da requisição expirou. | "name": "ICP-Brasil Institucional",
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | "description": "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
| `INVALID_ORIGIN` | O cabeçalho Origin não corresponde ao cadastro da aplicação. | "primary_color": "#0d47a1",
| `UNSUPPORTED_ALGORITHM` | Algoritmo não suportado (ex: diferente de SHA256withRSA). | "secondary_color": "#0284c7",
| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela de confirmação. | "border_color": "#93c5fd",
| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário está inativo no SO. | "background_color": "#ffffff",
| `COMMUNICATION_ERROR` | Erro interno no agente de comunicação local. | "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": "<nfeProc versao=\"4.00\" xmlns=\"http://www.portalfiscal.inf.br/nfe\">...</nfeProc>",
"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": "<nfeProc versao=\"4.00\" ...><Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">...</Signature></nfeProc>",
"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:
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "error",
"error_code": "PIN_INCORRECT",
"message": "O PIN informado para o token A3 é inválido."
}
```
### Mapeamento de Status HTTP
| Código HTTP | Erro Típico | Descrição |
|---|---|---|
| `400 Bad Request` | `USER_CANCELLED`, `PIN_INCORRECT`, `SIGNATURE_FAILED` | Requisição malformada, cancelamento pelo usuário ou falha de PIN |
| `401 Unauthorized` | `APPLICATION_NOT_AUTHORIZED` | Aplicação não autorizada ou sem cabeçalho `X-CyberCore-AppId` |
| `403 Forbidden` | `INVALID_ORIGIN` | Cabeçalho `Origin` não confere com o domínio cadastrado |
| `404 Not Found` | `CERTIFICATE_NOT_FOUND`, `TOKEN_NOT_FOUND` | Certificado ou dispositivo físico ausente |
| `409 Conflict` | `REPLAY_DETECTED` | Nonce já utilizado anteriormente na janela de validação |
| `422 Unprocessable`| `REQUEST_EXPIRED`, `CERTIFICATE_EXPIRED` | Parâmetros sintaticamente válidos mas expirados temporalmente |
| `500 Server Error` | `PKCS11_ERROR`, `PROVIDER_NOT_AVAILABLE` | Falha interna no middleware do fabricante ou driver criptográfico |
| `503 Service Unavail`| `TOKEN_REMOVED`, `COMMUNICATION_ERROR` | Hardware desconectado ou serviço em recuperação |
---
## 5. Diagrama de Sequência Completo
```mermaid
sequenceDiagram
autonumber
actor User as Usuário
participant Browser as Aplicação Web (Browser)
participant SDK as @cybercore/signer-sdk
participant CCSP as CyberCore CcspServer (127.0.0.1)
participant UI as CyberCore Desktop UI
participant HW as Hardware Token / Windows MY
Browser->>SDK: signer.sign({ digest, certId })
SDK->>CCSP: POST /ccsp/v1/sign (Headers, AppId, Nonce, Digest)
CCSP->>CCSP: Valida Origin, AppId e Nonce (anti-replay)
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
UI->>User: Exibe Diálogo Nativo (Aplicação, Documento, Certificado)
User->>UI: Clica em "Assinar"
opt Certificado A3 exige PIN
UI->>User: Solicita PIN do Hardware
User->>UI: Digita PIN
end
UI-->>CCSP: Confirmação concedida
CCSP->>HW: signLock.lock() -> JCA/JCE Signature.sign()
HW-->>CCSP: Bytes da assinatura + Cadeia X.509
CCSP->>CCSP: signLock.unlock()
CCSP-->>SDK: 200 OK (signature Base64, cert, subject)
SDK-->>Browser: Retorna resultado para a página web
```
+66
View File
@@ -0,0 +1,66 @@
# Documentação Técnica — CyberCore Signer
> **Índice Unificado e Guias Técnicos do Agente Desktop e Protocolo CCSP**
Bem-vindo à documentação oficial do **CyberCore Signer**. O agente foi desenvolvido em Java 21+ LTS (Java 25 LTS) com interface JavaFX e segurança criptográfica nativa JCA/JCE, permitindo que aplicações web autorizadas realizem operações de assinatura digital e autenticação com certificados ICP-Brasil (A1 em arquivo e A3 em tokens/smartcards físicos) mantendo as chaves privadas estritamente no dispositivo do usuário.
---
## 🗺️ Trilhas de Navegação por Perfil
Para facilitar a consulta, os documentos estão organizados de acordo com o seu perfil de atuação:
### 🌐 1. Para Desenvolvedores Web e Integradores
Se você desenvolve aplicações web (frontend/backend) que precisam interagir com o agente do usuário:
* **[Protocolo CCSP/1 (CyberCore Signing Protocol)](CCSP.md):** Especificação completa da API REST local (`127.0.0.1:21325`), endpoints, ciclo de handshake, proteção anti-replay e exemplos de requisição/resposta.
* **[SDK Web JavaScript (@cybercore/signer-sdk)](sdk.md):** Manual de instalação e uso da biblioteca oficial cliente para integração em aplicações web (Vite, Next.js, Vue, Angular, Vanilla JS).
* **[Autenticação e Login Digital ICP-Brasil](authentication-login.md):** Fluxo de autenticação segura via desafio criptográfico (*Challenge-Response*) para substituição ou complemento de senhas tradicionais.
* **[Catálogo Estruturado de Erros](errors.md):** Lista completa de códigos de erro (`CcspErrorCode`), status HTTP correspondentes e orientações de tratamento na interface web.
---
### ☕ 2. Para Desenvolvedores Desktop e Contribuidores
Se você atua no desenvolvimento, manutenção ou auditoria do código-fonte do agente desktop:
* **[Arquitetura do Sistema](architecture.md):** Organização modular Maven (`cybercore-core`, `cybercore-ui`, `cybercore-app`), fluxo de dados por digest, modelo de concorrência com fila FIFO (`signLock`) e isolamento de dependências.
* **[Certificados A1, A3 e PKCS#11](a1-a3-certificates.md):** Funcionamento dos provedores `Pkcs12CertificateProvider`, `WindowsCertificateStoreProvider` (SunMSCAPI/CNG) e `Pkcs11CertificateProvider` (SunPKCS11), monitoramento dinâmico PC/SC (`SmartCardWatcher`) e extração de OIDs ICP-Brasil.
* **[Assinaturas PDF (PAdES) e Selos Visuais SVG](pades.md):** Motor de assinatura PDFBox + Bouncy Castle, salvamento incremental, renderização de alta resolução (300 DPI Java2D) e gerenciamento de selos digitais.
* **[Assinaturas XML (XAdES-BES e XMLDSig)](xml-xmldsig.md):** Conformidade com o padrão ETSI TS 101 903 / DOC-ICP-15.03 e suporte a notas fiscais eletrônicas (NF-e, CT-e, MDF-e, eSocial).
* **[Layout Dual-Theme e Assinatura em Lote](serpro-layout-and-batch.md):** Especificação de interface gráfica moderna, alternância dinâmica de tema (Claro/Escuro) e fila de processamento em lote.
* **[Modo de Desenvolvimento](development-mode.md):** Guia de inicialização em modo `--dev`, políticas de não utilização de mocks em produção e geração de certificados A1 de teste locais.
---
### 🛡️ 3. Para DevOps, Infraestrutura e Segurança
Se você precisa implantar, homologar ou validar os aspectos de conformidade e segurança da solução:
* **[Modelo de Segurança e Privacidade](security.md):** Diretrizes de menor privilégio, proteção contra CSRF e DNS Rebinding, descarte seguro de senhas e PIN em memória e conformidade com a LGPD.
* **[Instalação e Empacotamento Nativo](installation.md):** Criação de instaladores autocontidos para Windows (MSI/EXE) via `jpackage`, requisitos do sistema e parâmetros de instalação corporativa silenciosa.
* **[Diagnóstico e Troubleshooting](diagnostics.md):** Subsistema de auto-teste, checklist de integridade de runtime e guia de resolução de problemas comuns de hardware/middleware.
* **[Limitações Técnicas Declaradas](limitations.md):** Declaração formal de requisitos de drivers, limitações de hardware mono-thread e dependências de sistema operacional (em conformidade com a Regra 23 do projeto).
---
## 📋 Sumário Geral de Arquivos
| Documento | Assunto Principal | Público-Alvo |
|---|---|---|
| [`CCSP.md`](CCSP.md) | Especificação técnica do protocolo local CCSP versão 1 | Integradores Web & Core Devs |
| [`architecture.md`](architecture.md) | Estrutura modular, threading e fluxo de operações | Desenvolvedores Desktop |
| [`a1-a3-certificates.md`](a1-a3-certificates.md) | Provedores A1/A3, PKCS#11, PC/SC e OIDs ICP-Brasil | Especialistas em Criptografia |
| [`pades.md`](pades.md) | Assinatura PAdES, envelope CMS e selos vetoriais | Desenvolvedores Desktop & Web |
| [`xml-xmldsig.md`](xml-xmldsig.md) | XAdES-BES, XMLDSig e documentos fiscais | Integradores de NF-e/XAdES |
| [`authentication-login.md`](authentication-login.md) | Login Challenge-Response via ICP-Brasil | Integradores Web |
| [`errors.md`](errors.md) | Tabela estruturada de erros e mapeamentos HTTP | Todos |
| [`security.md`](security.md) | Políticas de isolamento, memória e privacidade | Segurança & Compliance |
| [`diagnostics.md`](diagnostics.md) | Auto-testes e guia de resolução de problemas | Suporte & Operações |
| [`installation.md`](installation.md) | Empacotamento com `jpackage` e instaladores MSI | DevOps & Usuários Finais |
| [`limitations.md`](limitations.md) | Dependências de drivers e limitações declaradas | Todos |
| [`development-mode.md`](development-mode.md) | Parâmetro `--dev` e certificados de teste | Desenvolvedores Desktop |
| [`sdk.md`](sdk.md) | Guia completo da biblioteca JavaScript | Desenvolvedores Web |
| [`serpro-layout-and-batch.md`](serpro-layout-and-batch.md) | Design de interface e processamento em lote | UI/UX & Desenvolvedores |
---
> Dúvidas ou inconsistências? Verifique os testes unitários e de arquitetura em `modules/cybercore-core/src/test` ou consulte o arquivo [README.md principal](../README.md).
+77 -23
View File
@@ -1,46 +1,100 @@
# Certificados Digitais A1, A3 e PKCS#11 # Certificados Digitais A1, A3 e PKCS#11
O **CyberCore Signer** oferece suporte abrangente aos formatos e repositórios de certificados digitais padrão ICP-Brasil utilizados no Brasil. > **Guia Técnico de Suporte a Provedores Criptográficos, Hardware PC/SC e ICP-Brasil**
O **CyberCore Signer** fornece uma camada unificada de abstração para gerenciamento e consumo de certificados digitais brasileiros padrão **ICP-Brasil**, cobrindo certificados em arquivo de software (**A1**) e dispositivos físicos criptográficos em hardware (**A3** via tokens USB e cartões inteligentes smartcards).
--- ---
## 1. Certificados Digitais A1 (Software / PKCS#12) ## 1. Certificados Digitais A1 (Software / PKCS#12)
Os certificados A1 são arquivos com extensão `.pfx` ou `.p12` protegidos por senha. Os certificados padrão A1 são contêineres criptográficos protegidos por senha em formato `.pfx` ou `.p12`.
### Fluxo de Uso ### 1.1 Arquitetura e Ciclo de Vida
1. O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou importa via tela de Dashboard. 1. **Importação:** O usuário seleciona o arquivo via diálogo nativo (`FileChooser`) na interface ou pelo método de importação no Dashboard.
2. É solicitada a senha em um diálogo seguro (`A1PasswordDialog`), mantida em `char[]` temporário e sobrescrita com zeros logo após a leitura da chave. 2. **Coleta Segura de Senha:** A senha é obtida por meio do diálogo `A1PasswordDialog` e armazenada estritamente em um array de caracteres `char[]` volátil. Logo após o carregamento do `KeyStore`, o array é preenchido com zeros via `Arrays.fill(passwordChars, (char) 0)`.
3. O provedor `Pkcs12CertificateProvider` instancia um `KeyStore.getInstance("PKCS12")` e extrai os metadados do certificado X.509 (`subject`, `issuer`, `validade`, `algoritmo`, `tamanho da chave`). 3. **Instanciação JCA/JCE:** O provedor `Pkcs12CertificateProvider` inicializa um `KeyStore.getInstance("PKCS12")` e extrai o certificado X.509 e o manipulador da chave privada (`PrivateKey`).
4. Durante a assinatura, a chave privada em memória gera a assinatura digital via `Signature.getInstance("SHA256withRSA")`. 4. **Isolamento de Memória:** A chave privada nunca é serializada, exportada para disco ou enviada em logs. Durante a assinatura, a chave privada em memória gera os bytes da assinatura via `Signature.getInstance("SHA256withRSA")`.
--- ---
## 2. Certificados Digitais A3 no Windows (Windows Certificate Store) ## 2. Certificados Digitais A3 no Windows (Windows Certificate Store)
No Windows 10/11, a maioria dos drivers de tokens USB e Smart Cards (ex: SafeSign, SafeNet Authentication Client, Serpro, Certisign) registra seus certificados diretamente no repositório de certificados do usuário (`Windows-MY`). No sistema operacional Microsoft Windows (Windows 10 e Windows 11), a grande maioria dos middlewares de fabricantes de tokens e smartcards registra os certificados automaticamente no repositório de certificados pessoais do usuário (`Windows-MY`).
### Vantagens do Provider `WindowsCertificateStoreProvider` ### 2.1 Funcionamento do `WindowsCertificateStoreProvider`
- Acessa certificados físicos via CryptoAPI (CAPI) e Cryptography Next Generation (CNG) através do provedor JDK `SunMSCAPI`. * **Integração SunMSCAPI / CNG:** O agente consome o provedor JCA nativo `SunMSCAPI` (`KeyStore.getInstance("Windows-MY")`), integrando-se diretamente à CryptoAPI e à moderna Cryptography Next Generation (CNG) do Windows.
- A chave privada permanece dentro do chip criptográfico do token. * **Preservação no Chip Físico:** A chave privada nunca sai do chip criptográfico do token. As operações de assinatura delegam a cifragem ao módulo de segurança em hardware (HSM/Token).
- Quando o token exige autenticação por PIN, a janela nativa do próprio driver do fabricante é invocada com segurança pelo sistema operacional Windows. * **Diálogo Nativo de PIN do Fabricante:** Quando o token exige autenticação por PIN para liberar a chave privada, a janela nativa do próprio driver do fabricante (ex: diálogo nativo SafeSign ou SafeNet) é exibida com total segurança pelo subsistema de segurança do Windows.
* **Tratamento de Chaves Não Exportáveis:** O CyberCore Signer respeita integralmente a flag de chave privada não exportável, utilizando apenas referências de handle sem tentar copiar os bytes brutos da chave.
--- ---
## 3. Hardware PKCS#11 Direto (Tokens USB e Smartcards) ## 3. Acesso Direto a Hardware PKCS#11 (Tokens USB e Smartcards)
Para ambientes onde se deseja acesso direto ao middleware ou no Linux: Para ambientes corporativos que requerem comunicação direta com bibliotecas de middleware ou para execução em ambientes Linux, o componente `Pkcs11CertificateProvider` interage com o driver PKCS#11 do fabricante através do módulo do JDK `jdk.crypto.cryptoki` (`SunPKCS11`).
- `Pkcs11CertificateProvider` pesquisa bibliotecas DLL / SO padrão:
- `eps2003csp11.dll` (SafeNet / eToken) ### 3.1 Mapeamento de Middlewares Homologados no Brasil
- `aetpkss1.dll` (SafeSign AET)
- `acospkcs11.dll` (ACOS / G&D) O CyberCore Signer possui rotinas automáticas de varredura nos diretórios padrão do sistema (`System32`, `SysWOW64` no Windows e `/usr/lib/`, `/usr/local/lib/` no Linux):
- `opensc-pkcs11.dll` / `opensc-pkcs11.so` (OpenSC)
- Utiliza o provider SunPKCS11 do JDK (`jdk.crypto.cryptoki`). | Fabricante / Modelo | Biblioteca Windows (DLL) | Biblioteca Linux (.so) | Tipo de Dispositivo |
|---|---|---|---|
| **SafeSign Identity Client** (AET / GD) | `aetpkss1.dll` | `libaetpkss.so` | Tokens e Smartcards (Certisign, Valid, Serasa) |
| **SafeNet Authentication Client** (Gemalto / Thales) | `eps2003csp11.dll`, `eTPKCS11.dll` | `libeTPkcs11.so` | Tokens eToken Pro / 5110 (Serpro, Safeweb) |
| **ACOS / Oberthur** (Idemia) | `acospkcs11.dll`, `OcsCryptoki.dll` | `libacospkcs11.so` | Smartcards e leitores de cartão |
| **Watchdata / PROXKey** | `Watchdata ProxKey PKCS11.dll` | `libwatchdata.so` | Tokens USB |
| **OpenSC (Padrão Aberto)** | `opensc-pkcs11.dll` | `opensc-pkcs11.so` | Leitores genéricos PC/SC e smartcards |
### 3.2 Configuração Dinâmica do Provedor SunPKCS11
A configuração do provedor é criada dinamicamente em memória (sem gravar arquivos temporários em disco com dados sensíveis):
```properties
name = CyberCore-Token
library = C:\Windows\System32\aetpkss1.dll
slotListIndex = 0
```
--- ---
## 4. Monitoramento Dinâmico de Hardware (`SmartCardWatcher`) ## 4. Monitoramento Dinâmico de Hardware (`SmartCardWatcher`)
O componente `SmartCardWatcher` monitora os leitores PC/SC em segundo plano: O componente `SmartCardWatcher` executa um loop em segundo plano baseado na API padrão `javax.smartcardio` (subsistema PC/SC):
- **Ao inserir um token:** Atualiza a lista de certificados automaticamente e notifica a interface gráfica.
- **Ao remover o token:** Registra evento de auditoria (`AUDITORIA [TOKEN_REMOVED]`), limpa o cache de PIN de sessão em `PinSessionManager` e força um warm reset via `SmartCardSessionManager`. 1. **Detecção de Conexão (Card Inserted):**
- Detecta quando um novo smartcard ou token USB é conectado a qualquer leitora do sistema.
- Dispara recarregamento assíncrono do repositório de certificados.
- Notifica a interface gráfica (`UIEventListener`) e emite evento de auditoria (`AUDITORIA [TOKEN_INSERTED]`).
2. **Detecção de Desconexão (Card Removed):**
- Ao detectar a remoção do dispositivo, registra imediatamente o evento de auditoria `AUDITORIA [TOKEN_REMOVED]`.
- Limpa e zera os buffers de memória de PIN mantidos pelo `PinSessionManager`.
- Força o encerramento das conexões com o leitor via `SmartCardSessionManager` para liberar handles travados no sistema operacional.
3. **Serialização de Concorrência (`signLock`):**
- Como grande parte dos drivers de smartcard e chips criptográficos físicos não suporta acesso concorrente multi-thread, todas as requisições de assinatura são processadas de forma estritamente sequencial (fila FIFO garantida por `ReentrantLock`).
---
## 5. Extração e Parsing de Identidade ICP-Brasil (OIDs X.509)
O CyberCore Signer implementa o analisador de extensões X.509 conforme os padrões da **Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil)** estabelecidos pelo ITI (normas DOC-ICP-04).
As informações são extraídas da extensão `SubjectAlternativeName` (OID `2.5.29.17`) e analisadas a partir de seus identificadores de objeto (OIDs):
### 5.1 OID ICP-Brasil de Pessoa Física (e-CPF) — `2.16.76.1.3.1`
Contém uma estrutura ASN.1 com as informações cadastrais do titular:
* **Data de Nascimento:** 8 caracteres no formato `DDMMAAAA`;
* **CPF do Titular:** 11 dígitos numéricos;
* **Número do NIS/PIS/PASEP:** 11 dígitos numéricos;
* **Registro Geral (RG):** Número do documento de identidade;
* **Órgão Expedidor e UF:** Identificação do órgão emissor.
### 5.2 OID ICP-Brasil de Pessoa Jurídica (e-CNPJ) — `2.16.76.1.3.3`
Contém os dados cadastrais da empresa e do seu representante legal:
* **CNPJ da Entidade:** 14 dígitos numéricos;
* **Nome do Responsável:** Nome completo do signatário cadastrado na Receita Federal.
### 5.3 Demais OIDs Mapeados
* **OID `2.16.76.1.3.2`:** Título de Eleitor, zona e seção eleitoral.
* **OID `2.16.76.1.3.4`:** Cadastro Específico do INSS (CEI / CAEPF).
Essa extração permite ao agente preencher automaticamente o nome e documento do signatário para o SDK Web e selos visuais em PDF, garantindo conformidade com a legislação e auditoria fiscal.
+84 -39
View File
@@ -1,12 +1,14 @@
# Arquitetura do CyberCore Signer # Arquitetura do CyberCore Signer
O **CyberCore Signer** é um agente desktop nativo desenvolvido em Java 21+ LTS (Java 25 LTS) com interface JavaFX e segurança criptográfica JCA/JCE pura, projetado para permitir que aplicações web autorizadas solicitem assinaturas digitais ICP-Brasil padrão **A1** e **A3** diretamente da máquina do usuário. > **Especificação da Arquitetura Modular, Modelo de Threads e Fluxo Criptográfico**
O **CyberCore Signer** foi concebido a partir de princípios de alta coesão, baixo acoplamento e estrito isolamento de camadas. O software é desenvolvido em **Java 21+ LTS (Java 25 LTS)**, utilizando o runtime modular do JDK, JavaFX para a camada de apresentação e os provedores JCA/JCE da plataforma para operações criptográficas seguras.
--- ---
## 1. Visão Geral da Arquitetura em Módulos ## 1. Visão Geral da Arquitetura em Módulos
O projeto adota uma arquitetura desacoplada em múltiplos módulos Maven independentes: O projeto adota uma divisão modular rigorosa no Maven, garantindo que regras de negócio e algoritmos de criptografia nunca se acoplem a componentes visuais:
``` ```
cybercore-signer/ cybercore-signer/
@@ -19,43 +21,75 @@ cybercore-signer/
└── docs/ # Especificações técnicas e manuais de integração └── docs/ # Especificações técnicas e manuais de integração
``` ```
### 1.1 `cybercore-core` ### 1.1 Módulo `cybercore-core` (Agnóstico de UI)
- **Domínio:** Modelos imutáveis como `ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType` e `CertificateStatus`. O módulo de núcleo é totalmente independente de JavaFX ou bibliotecas visuais:
- **Criptografia:** `SigningService` e `DigestAlgorithm` utilizando exclusivamente os provedores padrão JCA/JCE (sem reimplementação manual de algoritmos). * **Domínio Imutável:** Modelos imutáveis (`ManagedCertificate`, `SigningContext`, `SigningResult`, `CertificateType`, `CertificateStatus`).
- **Provedores:** Abstração `CertificateProvider` com implementações: * **Serviços de Assinatura:** `SigningService` orquestra a aplicação de algoritmos certificados (`SHA256withRSA`) delegando a operação ao provedor de certificado resolvido.
- `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória protegida. * **Provedores de Certificados (`CertificateProvider`):** Abstração que unifica:
- `WindowsCertificateStoreProvider`: integração nativa com o repositório de chaves do Windows (`Windows-MY` via SunMSCAPI/CNG). - `Pkcs12CertificateProvider`: leitura de arquivos `.pfx`/`.p12` em memória com limpeza volátil de senha.
- `Pkcs11CertificateProvider`: integração com drivers de middleware e tokens USB/Smartcards via `SunPKCS11`. - `WindowsCertificateStoreProvider`: integração com o repositório nativo do Windows (`Windows-MY` via SunMSCAPI/CNG).
- **Protocolo & Rede Local:** `CcspServer` escuta estritamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1. - `Pkcs11CertificateProvider`: integração direta com bibliotecas dinâmicas (DLL/SO) de drivers e tokens USB através do SunPKCS11.
- **Segurança & Configurações:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` (persistência em `~/.cybercore/config.json`). * **Protocolo e Rede Local:** `CcspServer` escuta exclusivamente em `127.0.0.1:21325`, fornecendo a API REST CCSP/1 com validação de origens e controle de concorrência.
- **Validação Criptográfica:** `SignatureValidationService` com suporte a Assinaturas Incorporadas (PAdES PDF, CMS/P7M com conteúdo embutido, XMLDSig) e Assinaturas Destacadas (Detached CMS/P7S). * **Segurança e Persistência:** `ApplicationRegistry`, `NonceManager` (proteção anti-replay), `OriginValidator`, `SmartCardSessionManager` e `AgentConfigurationService` com armazenamento persistente em diretório de usuário (`~/.cybercore/`).
- **Selos Visuais Personalizados:** `CustomSealStorage` (persistência em `~/.cybercore/seals/`), `PdfSealThemeRegistry` dinâmico e `CpfDisplayMode` (privacidade LGPD). * **Assinatura de Documentos e Validação:** `PdfSigningService` (PAdES com salvamento incremental e carimbo TSA via PDFBox e Bouncy Castle), `XmlSigningService` (XAdES-BES e XMLDSig via `javax.xml.crypto`) e `SignatureValidationService` para conferência de integridade e signatários.
### 1.2 `cybercore-ui` ### 1.2 Módulo `cybercore-ui` (Apresentação JavaFX)
- Interface desenvolvida com JavaFX 25, empregando layout moderno e simétrico de grade 3x3 de operações e coluna lateral de utilitários: Responsável por toda a interação com o usuário:
- `MainAppView`: grade 3x3 otimizada: * **Interface Principal (`MainAppView`):** Painel de navegação moderno com alternador dinâmico de tema (Claro/Escuro), grade 3x3 de operações rápidas (PDF, XML, Arquivo Genérico, Assinatura em Lote, Assinatura Atachada, Co-Assinatura, Carimbo de Tempo, Designer de Selos, Validador) e coluna lateral de utilitários (Logs em tempo real, Configurações e Diagnóstico).
- Linha 0: Assinar PDF, Assinar XML, Assinatura Atachada * **Diálogos de Consentimento e PIN:** Modais nativos de aprovação visual para requisições do protocolo CCSP/1 (`SignatureConfirmationDialog`, `ApplicationAuthorizeDialog`) e coleta segura de PIN de tokens (`A1PasswordDialog`).
- Linha 1: Co-Assinar Documento, Carimbo do Tempo, Gerenciar Selos * **Designer de Selos Digitais (`SealDesignerDialog`):** Ferramenta com Live Preview para criação de selos visuais com controle de cores, estilo de brasão, exibição de nome completo sem truncamento e privacidade de CPF.
- Linha 2: Validar Assinatura, Validar Carimbo, Sair do Agente * **Bandeja do Sistema (System Tray):** Execução minimizada com menu de contexto rápido para restauração da tela ou encerramento da sessão.
- `SealDesignerDialog`: criador e instalador de selos digitais personalizados com pré-visualização ao vivo (Live Preview), seletores de cores, estilos de emblema, obrigatoriedade de nome completo, controle de mascaramento de CPF e estampa de data/hora.
- `SignatureValidationDialog`: diálogo de validação criptográfica real com alternância entre Assinatura Incorporada e Assinatura Destacada, extração de documento embutido e relatório de signatários.
- `PdfSignDialog`: modal de assinatura PAdES com posicionamento por clique na folha, atalho direto para criar/instalar selos e opções dinâmicas de CPF/data-hora.
- `SettingsView`: configurações gerais persistentes, política de PIN e gerenciamento de selos instalados.
### 1.3 `cybercore-app` ### 1.3 Módulo `cybercore-app` (Orquestração e Bootstrap)
- Main bootstrap (`CyberCoreSignerApplication`). Ponto de partida da aplicação (`CyberCoreApp`):
- Tratamento de parâmetros de inicialização (`--dev`, `--headless`). * Processa argumentos de linha de comando (`--dev`, `--headless`).
- Execução de diagnósticos preliminares de ambiente e engine criptográfica antes do início das threads de escuta. * Executa rotinas de diagnóstico pré-inicialização do ambiente Java e disponibilidade criptográfica.
- Geração de Fat JAR autocontido via `maven-shade-plugin` e preparação para distribuição `jpackage`. * Inicia o `CcspServer` em segundo plano e abre a interface gráfica na `JavaFX Application Thread`.
* Empacota o executável consolidado via `maven-shade-plugin` e orquestra o build nativo via `jpackage`.
--- ---
## 2. Fluxo de Dados e Princípio de Menor Privilégio ## 2. Modelo de Threads e Concorrência
O CyberCore Signer opera sob o **modelo de assinatura descentralizada por digest**: Para manter a interface gráfica sempre fluida e ágil, o CyberCore Signer utiliza um particionamento rígido de threads:
1. A aplicação web calcula o hash criptográfico SHA-256 no próprio navegador utilizando a Web Crypto API nativa.
2. Apenas o hash Base64 de 32 bytes trafega localmente para o CyberCore Signer. ```mermaid
3. O documento completo nunca sai do navegador e a chave privada nunca sai do computador ou do hardware do usuário. graph TD
subgraph UI Layer
JFX[JavaFX Application Thread]
end
subgraph Local Protocol Layer
HTTPPool[CcspServer Worker Pool (FixedThreadPool 8)]
end
subgraph Background Services
Watcher[SmartCardWatcher Thread (PC/SC Loop)]
AsyncTasks[JavaFX Task / CompletableFuture Workers]
end
subgraph Hardware Critical Section
Lock[Trava Serializada FIFO: signLock]
Hardware[Token Físico A3 / SmartCard Driver]
end
HTTPPool -->|Solicita Aprovação Visual| JFX
JFX -->|Consentimento Concedido| HTTPPool
HTTPPool --> Lock
AsyncTasks --> Lock
Lock --> Hardware
```
1. **JavaFX Application Thread:** Dedicada exclusivamente à renderização da interface e tratamento de eventos visuais. Nenhuma operação criptográfica, leitura de disco ou chamada de rede é executada nesta thread.
2. **CcspServer Worker Pool:** Pool de 8 threads daemon dedicadas ao atendimento de requisições HTTP da API REST CCSP/1.
3. **SmartCardWatcher Thread:** Thread daemon isolada que executa `cardTerminal.waitForCardPresent()` e `waitForCardAbsent()` para monitorar hardware PC/SC sem onerar a CPU.
4. **Trava Serializada FIFO (`signLock`):** Implementada via `ReentrantLock(true)`. Garante que operações de assinatura em cartões inteligentes físicos sejam enfileiradas ordenadamente, prevenindo falhas de hardware e estados corrompidos em drivers de fabricantes mono-thread.
---
## 3. Fluxo de Dados e Assinatura Descentralizada por Digest
A arquitetura do CyberCore Signer prioriza a privacidade e o modelo de **assinatura por digest**:
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -66,14 +100,25 @@ sequenceDiagram
participant UI as CyberCore JavaFX participant UI as CyberCore JavaFX
participant Crypto as SigningService & Providers participant Crypto as SigningService & Providers
Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce) Web->>CCSP: POST /ccsp/v1/sign (digest, appId, certHint, nonce, expires_at)
CCSP->>CCSP: Valida autorização, nonce anti-replay e digest CCSP->>CCSP: Valida autorização, nonce anti-replay e integridade do digest
CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation) CCSP->>UI: Solicita confirmação visual (requestSignatureConfirmation)
UI->>Usuario: Exibe Diálogo (Hash, Certificado, Aplicação) UI->>Usuario: Exibe Diálogo Nativo (Nome do Documento, Hash SHA-256, Certificado)
Usuario->>UI: Aprova assinatura (e insere PIN se A3) Usuario->>UI: Clica em "Autorizar Assinatura" (e digita PIN se requerido)
UI-->>CCSP: Confirmação concedida UI-->>CCSP: Confirmação concedida
CCSP->>Crypto: sign(SigningContext) [Trava FIFO] CCSP->>Crypto: sign(SigningContext) sob signLock.lock()
Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign() Crypto->>Crypto: JCA/JCE Signature.update(digest) + sign()
Crypto-->>CCSP: SigningResult (signatureBytes, certChain) Crypto-->>CCSP: SigningResult (signatureBytes, certChain)
CCSP-->>Web: JSON { status: "signed", signature: "...", certificate: "..." } CCSP-->>Web: 200 OK JSON { status: "signed", signature: "Base64", certificate: "Base64" }
``` ```
---
## 4. Persistência de Dados e Configurações
Todos os dados de configuração e personalizações do usuário são armazenados no diretório do usuário no sistema operacional:
* **Diretório Base:** `~/.cybercore/` (ou `%USERPROFILE%\.cybercore\` no Windows).
* **`config.json`:** Configurações globais do agente (política de cache de PIN, tema padrão, selo padrão ativo).
* **`authorized-apps.json`:** Registro de aplicações web autorizadas pelo usuário, contendo `application_id`, nome amigável e data da aprovação.
* **`seals/*.json`:** Selos visuais SVG personalizados criados ou importados pelo usuário no designer de selos.
+64 -23
View File
@@ -1,48 +1,89 @@
# Modo de Desenvolvimento e Diagnóstico Detalhado # Modo de Desenvolvimento e Homologação
O **CyberCore Signer** opera exclusivamente com **certificados digitais e provedores criptográficos reais** (A1 via arquivos `.pfx`/`.p12`, repositório nativo do Windows `Windows-MY` e tokens/smart cards físicos PKCS#11). Em conformidade com a exigência de segurança de produção, **não existem mocks ou certificados sintéticos no agente**. > **Diretrizes para Execução Local com Argumento `--dev`, Testes e Criação de Certificados A1 de Homologação**
O **CyberCore Signer** opera exclusivamente com certificados digitais e provedores criptográficos reais (arquivos `.pfx`/`.p12`, Windows-MY e tokens físicos PKCS#11). Em cumprimento às normas de integridade do projeto, **não existem mocks ou certificados artificiais no runtime de produção**.
--- ---
## 1. Ativação do Modo de Desenvolvimento ## 1. Ativação do Modo de Desenvolvimento
Para iniciar o agente em modo de desenvolvimento (com logs detalhados e diagnósticos ampliados), utilize o argumento `--dev`: Para iniciar o agente em ambiente de homologação e desenvolvimento com logs estendidos de auditoria e detalhamento de operações de rede, utilize o argumento de linha de comando `--dev`:
```bash ```bash
java -jar cybercore-signer.jar --dev # Executando via script Linux / macOS:
./run.sh --dev
# Executando no Windows:
run.bat --dev
# Ou diretamente pelo JAR compilado:
java -jar modules/cybercore-app/target/cybercore-signer.jar --dev
``` ```
Ao iniciar, uma mensagem informativa é registrada no log: Ao ser iniciado com esta flag, o agente imprime no terminal o banner de diagnóstico:
``` ```
========================================================= =========================================================
MODO DE DESENVOLVIMENTO ATIVADO (--dev) CYBERCORE SIGNER — MODO DE DESENVOLVIMENTO ATIVADO (--dev)
Logs detalhados ativos. Operação com certificados reais. Logs detalhados ativos. Operação com certificados reais JCA/JCE.
Servidor local CCSP/1 pronto em: http://127.0.0.1:21325
========================================================= =========================================================
``` ```
--- ---
## 2. Testando em Ambiente de Desenvolvimento ## 2. Geração de Certificados A1 de Teste com OpenSSL
Para desenvolvedores testarem fluxos completos de assinatura de PDF, XML, arquivos em geral e login digital: Para desenvolvedores e testadores que precisam simular fluxos completos de assinatura digital (PAdES, XAdES, login digital) sem utilizar um certificado ICP-Brasil real de produção, é possível gerar um certificado digital de teste no formato `.pfx` protegido por senha utilizando o utilitário **OpenSSL**:
1. **Certificados A1 de Teste/Homologação:** ### Passo 1: Gerar a Chave Privada RSA (2048 bits)
- Adicione um certificado A1 `.pfx` ou `.p12` emitido por uma autoridade de teste ou autoassinado. ```bash
- Pela interface desktop, clique no botão **"Importar A1 (.pfx/.p12)"** no Dashboard e informe o arquivo e a senha. openssl genrsa -out teste_signer.key 2048
- O certificado ficará disponível imediatamente para todas as operações CCSP/1. ```
2. **Repositório Nativo do Windows (Windows-MY):** ### Passo 2: Gerar o Certificado X.509 Autoassinado
- Instale o certificado no repositório de certificados pessoais do Windows (`certmgr.msc`). ```bash
- O provedor `WindowsCertificateStoreProvider` detectará o certificado automaticamente. openssl req -new -x509 -key teste_signer.key -out teste_signer.crt -days 365 \
-subj "/C=BR/O=ICP-Brasil Teste/OU=Desenvolvimento CyberCore/CN=DESENVOLVEDOR TESTE:12345678900"
```
3. **Tokens USB / Smart Cards (PKCS#11):** ### Passo 3: Exportar o Certificado e Chave no Contêiner PKCS#12 (`.pfx`)
- Conecte o dispositivo físico com middleware homologado (SafeSign, GD Burti, SafeNet, OpenSC, etc.). ```bash
- O provedor `Pkcs11CertificateProvider` detectará o token e seus slots. openssl pkcs12 -export -out certificado_teste.pfx \
-inkey teste_signer.key -in teste_signer.crt \
-name "DESENVOLVEDOR TESTE:12345678900" \
-passout pass:123456
```
### Passo 4: Importar no CyberCore Signer
1. Abra o CyberCore Signer.
2. Na tela principal, clique em **"Importar A1 (.pfx/.p12)"**.
3. Selecione o arquivo `certificado_teste.pfx` gerado e informe a senha `123456`.
4. O certificado aparecerá instantaneamente como disponível no Dashboard e poderá ser utilizado para todos os testes do SDK e CCSP/1.
--- ---
## 3. Garantia de Operação Real ## 3. Execução da Suíte de Testes Automatizados
1. Toda assinatura é gerada criptograficamente através dos provedores JCA/JCE reais. O repositório do CyberCore Signer conta com suíte abrangente de testes unitários, testes de concorrência e testes de arquitetura (ArchUnit):
2. A resposta do protocolo CCSP/1 contém assinaturas válidas e certificados X.509 autênticos.
3. Não há certificados simulados ou flags artificiais nas respostas do agente. ```bash
# Executar todos os testes unitários do projeto:
./mvnw test
# Executar testes específicos do protocolo CCSP/1:
./mvnw test -Dtest=CcspServerTest
# Executar testes de concorrência e trava FIFO (signLock):
./mvnw test -Dtest=CcspConcurrencyTest
# Executar testes de conformidade arquitetural (garante desacoplamento de UI no core):
./mvnw test -Dtest=ArchitectureTest
```
---
## 4. Garantia de Operação Real
1. **Assinaturas Autênticas:** Mesmo ao utilizar um certificado autoassinado de teste, os bytes da assinatura são calculados matematicamente pelos algoritmos reais do JCA/JCE (`SHA256withRSA`).
2. **Sem Falsos Positivos:** O comportamento do agente é 100% idêntico entre o ambiente de teste e o ambiente de produção com tokens físicos A3.
+89 -20
View File
@@ -1,34 +1,103 @@
# Diagnóstico de Ambiente e Auto-Teste # Diagnóstico de Ambiente e Resolução de Problemas
O **CyberCore Signer** possui um subsistema nativo de diagnóstico integrado (`DiagnosticService`), permitindo auditoria detalhada de saúde e integridade técnica da máquina do usuário. > **Guia de Auto-Teste, Checklist de Integridade de Runtime e Troubleshooting de Hardware**
O **CyberCore Signer** conta com um subsistema nativo de diagnóstico e telemetria local (`DiagnosticService`), projetado para validar a integridade da estação de trabalho e identificar rapidamente falhas de drivers, certificados ou subsistemas criptográficos do sistema operacional.
--- ---
## 1. Itens Inspecionados no Diagnóstico ## 1. Itens Inspecionados pelo Diagnóstico Automático
Ao abrir a aba **Diagnóstico** na interface ou invocar `diagnosticService.runDiagnostic()`, o agente avalia: Ao acessar a aba **Diagnóstico** na interface do agente desktop ou ao inicializar em modo `--dev`, o agente executa uma auditoria completa nos seguintes componentes:
1. **Ambiente Java / Runtime:** 1. **Ambiente Java / Runtime Nativo:**
- Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS); - Versão do Java (requer Java 21+ LTS, homologado em Java 25 LTS);
- Vendor do JDK (Temurin, Oracle, etc.); - Vendor do JDK (Eclipse Temurin, Oracle OpenJDK, Amazon Corretto);
- Arquitetura do processador e sistema operacional. - Arquitetura da CPU (x86_64 / amd64) e sistema operacional (Windows 10/11 ou Linux).
2. **Engine Criptográfica JCA/JCE:** 2. **Engine Criptográfica JCA/JCE:**
- Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.); - Disponibilidade dos provedores de segurança registrados na JVM (`SUN`, `SunRsaSign`, `SunMSCAPI`, etc.);
- Suporte ao algoritmo de hash `SHA-256`; - Suporte aos algoritmos de hash criptográfico (`SHA-256`, `SHA-512`);
- Suporte ao algoritmo de assinatura `SHA256withRSA`. - Suporte aos algoritmos de assinatura digital (`SHA256withRSA`).
3. **Repositório de Chaves do Sistema Operacional:** 3. **Repositório de Certificados do Sistema Operacional:**
- Detecção do repositório `Windows-MY` (Windows CryptoAPI / CNG). - Detecção do repositório de chaves `Windows-MY` via SunMSCAPI / CryptoAPI / CNG.
4. **Middlewares PKCS#11 e SmartCard:** 4. **Middlewares PKCS#11 e Subsistema PC/SC:**
- Verificação de bibliotecas de smartcard registradas no sistema (`eps2003csp11.dll`, `aetpkss1.dll`, `opensc-pkcs11.dll`, etc.); - Presença das bibliotecas dinâmicas do fabricante (`aetpkss1.dll`, `eps2003csp11.dll`, `acospkcs11.dll`, `opensc-pkcs11.dll`);
- Disponibilidade do subsistema PC/SC `javax.smartcardio`. - Disponibilidade do subsistema de leitoras de smartcard `javax.smartcardio` (PC/SC Daemon / Smart Card Service do Windows).
5. **Servidor Local CCSP/1:**
- Teste de bind do socket em `127.0.0.1:21325` e integridade do pool de threads.
--- ---
## 2. Auto-Teste de Assinatura Local (Self-Test) ## 2. Auto-Teste de Assinatura Local (Self-Test)
A aba de diagnóstico disponibiliza o botão **"Executar Auto-teste de Assinatura"**: A tela de diagnóstico oferece a função **"Executar Auto-teste de Assinatura"**:
1. Seleciona o primeiro certificado operacional disponível na máquina. 1. O agente identifica o primeiro certificado digital operacional conectado.
2. Gera um hash SHA-256 interno de teste: `"Documento_Teste_CyberCore_<timestamp>"`. 2. Gera um hash SHA-256 interno sintético para teste: `"Documento_Teste_CyberCore_<timestamp>"`.
3. Abre o diálogo de confirmação visual para validação do usuário. 3. Abre o diálogo nativo de confirmação visual para validação do usuário.
4. Executa a assinatura real JCA/JCE e afere os bytes gerados. 4. Executa a assinatura real através do provedor JCA/JCE correspondente (solicitando PIN se A3).
5. Emite relatório de sucesso com tempo decorrido e tamanho da assinatura. 5. Valida os bytes da assinatura gerada e emite relatório com tempo decorrido em milissegundos e tamanho da assinatura.
---
## 3. Guia de Resolução de Problemas (Troubleshooting)
### 3.1 Token USB ou Smartcard A3 não é detectado
* **Sintoma:** O token está plugado na porta USB mas não aparece no CyberCore nem na lista de certificados.
* **Causa Comum:** O serviço de cartão inteligente do Windows (`ScardSvr`) está desativado ou o middleware do fabricante (SafeSign/SafeNet) não está instalado.
* **Solução:**
1. No Windows, pressione `Win + R`, digite `services.msc` e pressione Enter.
2. Localize o serviço **Cartão Inteligente (Smart Card)** e garanta que seu status esteja **Em Execução** (Tipo de Inicialização: Automático).
3. Verifique se o aplicativo de gerenciamento do token (ex: *SafeSign Token Utility* ou *SafeNet Authentication Client Tools*) reconhece o token.
4. Desconecte o token USB, aguarde 3 segundos e reconecte em outra porta USB.
### 3.2 Erro `NTE_BAD_KEYSET` ou `Chave Privada Não Acessível`
* **Sintoma:** O certificado aparece na lista, mas a assinatura falha com erro `PRIVATE_KEY_NOT_AVAILABLE` ou código Windows `0x80090016`.
* **Causa Comum:** O certificado foi importado no Windows sem a chave privada correspondente, ou o usuário não tem permissão de leitura sobre o contêiner de chaves.
* **Solução:**
1. Abra o gerenciador de certificados do Windows: `certmgr.msc`.
2. Navegue até **Pessoal -> Certificados** e dê um duplo clique sobre o certificado.
3. Verifique se na parte inferior da aba Geral consta a mensagem: *"Você tem uma chave privada correspondente a este certificado"*. Se não constar, reinstale o arquivo A1 com a opção "Marcar esta chave como exportável" ativada.
### 3.3 Porta local `21325` já em uso (`BindException`)
* **Sintoma:** O CyberCore Signer não inicializa e os logs acusam `Address already in use: bind`.
* **Causa Comum:** Outra instância do CyberCore Signer ou aplicativo conflitante já está em execução na porta `21325`.
* **Solução:**
1. No Windows, abra o Gerenciador de Tarefas (`Ctrl + Shift + Esc`) e encerre eventuais processos `cybercore-signer` ou `javaw.exe` órfãos.
2. No terminal (PowerShell), você pode identificar o processo ocupando a porta:
```powershell
Get-NetTCPConnection -LocalPort 21325 | Select-Object OwningProcess
```
3. No Linux:
```bash
sudo lsof -i :21325
```
### 3.4 Aplicação Web acusa "Agente não instalado" ou Falha de Conexão
* **Sintoma:** A chamada `signer.isInstalled()` no navegador retorna `false` ou dá timeout.
* **Causa Comum:** O CyberCore Signer está fechado, o antivírus bloqueou a conexão loopback local, ou a aplicação web tenta conectar com protocolo incorreto.
* **Solução:**
1. Verifique se o ícone do CyberCore Signer está ativo na barra de tarefas (System Tray).
2. Abra uma nova aba no navegador e acesse diretamente: `http://127.0.0.1:21325/ccsp/v1/status`.
3. O navegador deve exibir o JSON com `"status": "ready"`. Se não exibir, abra o executável do CyberCore Signer.
---
## 4. Comandos Úteis de Diagnóstico no Terminal
### No Windows:
```cmd
:: Testar leitura de smartcards e drivers via utilitário nativo do Windows:
certutil -scinfo
:: Listar certificados instalados no repositório Pessoal do usuário:
certutil -user -store My
```
### No Linux:
```bash
:: Verificar leitoras e cartões conectados via subsistema PC/SC:
pcsc_scan
:: Verificar status do serviço pcscd:
systemctl status pcscd
```
+74 -22
View File
@@ -1,28 +1,80 @@
# Catálogo Estruturado de Erros # Catálogo Estruturado de Erros
O **CyberCore Signer** adota um sistema unificado e determinístico de códigos de erro (`CcspErrorCode`) compartilhados entre o agente Java desktop, o protocolo CCSP/1 e o SDK Web em JavaScript. > **Especificação Unificada de Códigos de Erro (`CcspErrorCode`), Status HTTP e Tratamento no SDK**
O **CyberCore Signer** adota uma taxonomia estrita e estável de erros estruturados, garantindo que o agente desktop Java, a API REST do protocolo CCSP/1 e o SDK JavaScript manipulem falhas com previsibilidade e sem expor detalhes internos sensíveis ao usuário final.
--- ---
## 1. Tabela de Erros Estruturados ## 1. Tabela Completa de Códigos de Erro
| Código de Erro | Descrição Detalhada | Ação Recomendada para a Aplicação Web | | Código de Erro (`CcspErrorCode`) | Status HTTP | Descrição do Erro | Ação Recomendada para a Aplicação Web |
|---|---|---| |---|---|---|---|
| `CERTIFICATE_NOT_FOUND` | O certificado indicado pelo `certificate_hint` não foi localizado. | Solicite ao usuário selecionar novamente o certificado via `getCertificates()`. | | `CERTIFICATE_NOT_FOUND` | `404 Not Found` | O certificado indicado pelo `certificate_hint` não foi localizado na máquina. | Solicitar ao usuário selecionar novamente o certificado via `getCertificates()`. |
| `CERTIFICATE_EXPIRED` | O certificado selecionado expirou. | Oriente o usuário a renovar o certificado digital junto à autoridade certificadora. | | `CERTIFICATE_EXPIRED` | `422 Unprocessable` | O certificado digital selecionado ultrapassou sua data limite de validade. | Orientar o usuário a renovar o certificado digital junto à autoridade certificadora (AC). |
| `CERTIFICATE_INVALID` | O certificado possui estrutura corrompida ou ainda não é válido. | Verificar a data e integridade do arquivo/dispositivo. | | `CERTIFICATE_INVALID` | `400 Bad Request` | Estrutura ASN.1/X.509 corrompida ou ainda não iniciada (`NotBefore`). | Verificar integridade do arquivo `.pfx` ou relógio do sistema operacional. |
| `PRIVATE_KEY_NOT_AVAILABLE` | O certificado está presente mas a chave privada não está acessível. | Em tokens A3, verifique se o middleware/driver está instalado corretamente. | | `PRIVATE_KEY_NOT_AVAILABLE` | `404 Not Found` | O certificado está presente mas a chave privada não está acessível no repositório. | Em tokens A3, verificar se o middleware/driver do token está instalado corretamente. |
| `TOKEN_NOT_FOUND` | Nenhum dispositivo ou leitor compatível foi localizado. | Solicite ao usuário conectar o token USB ou inserir o smartcard no leitor. | | `TOKEN_NOT_FOUND` | `404 Not Found` | Nenhum dispositivo criptográfico físico ou leitora de cartão foi detectado. | Solicitar ao usuário conectar o token USB ou inserir o smartcard no leitor. |
| `TOKEN_REMOVED` | O token USB ou cartão foi removido fisicamente durante a assinatura. | Peça ao usuário reconectar o token e reiniciar a operação de assinatura. | | `TOKEN_REMOVED` | `503 Service Unavail`| O token USB ou cartão foi desconectado fisicamente durante o processamento. | Orientar o usuário a reconectar o token e reiniciar a operação de assinatura. |
| `PIN_REQUIRED` | O token exige PIN para liberar a chave privada e nenhum PIN foi fornecido. | Exibir campo de solicitação de PIN na tela. | | `PIN_REQUIRED` | `400 Bad Request` | O token exige PIN para liberar a chave privada e nenhum PIN foi informado. | Exibir campo para inserção do PIN do dispositivo. |
| `PIN_INCORRECT` | O PIN digitado está incorreto. | Informar o usuário e alertar sobre o risco de bloqueio de PUK após 3 tentativas. | | `PIN_INCORRECT` | `400 Bad Request` | O PIN digitado para o token A3 é incorreto. | Alertar o usuário sobre o risco de bloqueio do cartão (PUK) após tentativas consecutivas. |
| `PKCS11_ERROR` | Falha no driver ou biblioteca PKCS#11 do fabricante. | Verificar a instalação do SafeSign, SafeNet ou OpenSC. | | `PKCS11_ERROR` | `500 Server Error` | Falha na biblioteca DLL/SO do middleware ou driver do fabricante. | Verificar a instalação do SafeSign, SafeNet, GD Burti ou OpenSC. |
| `SIGNATURE_FAILED` | Erro durante a geração dos bytes da assinatura digital. | Tentar novamente ou reiniciar o agente desktop. | | `SIGNATURE_FAILED` | `400 Bad Request` | Falha criptográfica na geração dos bytes da assinatura digital. | Reiniciar a operação ou verificar integridade do algoritmo especificado. |
| `APPLICATION_NOT_AUTHORIZED` | A aplicação web teve autorização recusada pelo usuário. | Informar o usuário que é necessário aceitar a notificação na tela do agente. | | `APPLICATION_NOT_AUTHORIZED` | `401 Unauthorized` | A aplicação web teve autorização recusada pelo usuário ou não foi cadastrada. | Notificar o usuário que é necessário aceitar a solicitação na janela do CyberCore. |
| `REQUEST_EXPIRED` | O timestamp ou `expires_at` da requisição expirou. | Gerar nova requisição de assinatura com timestamp atualizado. | | `REQUEST_EXPIRED` | `422 Unprocessable` | A validade temporal (`expires_at`) da requisição foi ultrapassada. | Gerar nova requisição de assinatura com timestamp atualizado no backend/frontend. |
| `REPLAY_DETECTED` | O nonce informado já foi processado anteriormente. | Sempre gerar um UUID único para cada chamada de assinatura. | | `REPLAY_DETECTED` | `409 Conflict` | O `nonce` informado já foi processado anteriormente dentro da janela ativa. | Sempre gerar um UUID criptograficamente novo para cada chamada de assinatura. |
| `INVALID_ORIGIN` | O cabeçalho `Origin` não confere com o cadastro da aplicação. | Conectar-se com a URL e protocolo autorizados. | | `INVALID_ORIGIN` | `403 Forbidden` | O cabeçalho `Origin` não confere com o domínio autorizado da aplicação. | Conectar a partir da URL e domínio homologados pelo usuário. |
| `UNSUPPORTED_ALGORITHM` | Algoritmo solicitado não é suportado pelo agente ou certificado. | Utilizar algoritmo padrão homologado (`SHA256withRSA`). | | `UNSUPPORTED_ALGORITHM` | `400 Bad Request` | O algoritmo de hash ou assinatura solicitado não é suportado pelo provedor. | Utilizar os algoritmos padrão homologados (`SHA256withRSA`). |
| `USER_CANCELLED` | O usuário clicou em Cancelar ou fechou a janela do CyberCore. | Tratar como cancelamento voluntário sem mensagem de erro alarmante. | | `USER_CANCELLED` | `400 Bad Request` | O usuário clicou em Cancelar ou fechou o diálogo nativo de confirmação. | Tratar como cancelamento voluntário do usuário sem exibir alerta de erro crítico. |
| `PROVIDER_NOT_AVAILABLE` | O provedor criptográfico necessário não está ativo no sistema operacional. | Verificar a integridade do Windows Certificate Store ou Java Runtime. | | `PROVIDER_NOT_AVAILABLE` | `500 Server Error` | O provedor criptográfico necessário não está ativo no sistema operacional. | Verificar o Windows Certificate Store ou bibliotecas de runtime do Java. |
| `COMMUNICATION_ERROR` | Erro interno de socket ou timeout de rede local. | Verificar se a porta `21325` não está bloqueada por antivírus local. | | `COMMUNICATION_ERROR` | `503 Service Unavail`| Erro de socket, timeout na confirmação visual ou porta ocupada. | Verificar se o CyberCore Signer está em execução e se a porta `21325` está livre. |
---
## 2. Estrutura do Payload JSON de Erro
Em qualquer resposta com status diferente de `200 OK`, o payload retornado pela API CCSP/1 adota o seguinte formato:
```json
{
"version": "1",
"request_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"status": "error",
"error_code": "PIN_INCORRECT",
"message": "O PIN informado para o token A3 é incorreto."
}
```
---
## 3. Tratamento Tipado no SDK JavaScript
No SDK `@cybercore/signer-sdk`, cada código estruturado possui uma classe de erro correspondente herdando de `CyberCoreError`:
```javascript
import {
UserCancelledError,
PinIncorrectError,
TokenNotFoundError,
CertificateNotFoundError
} from '@cybercore/signer-sdk/errors';
try {
const result = await signer.sign({
certificateId: cert.id,
digest: documentoDigest
});
} catch (error) {
if (error instanceof UserCancelledError) {
console.info('Assinatura cancelada voluntariamente pelo usuário.');
} else if (error instanceof PinIncorrectError) {
alert('PIN incorreto! Verifique a senha do seu token para evitar o bloqueio.');
} else if (error instanceof TokenNotFoundError) {
alert('Dispositivo não detectado. Por favor, conecte seu token USB na porta do computador.');
} else if (error instanceof CertificateNotFoundError) {
alert('O certificado selecionado não foi encontrado ou expirou.');
} else {
console.error('Erro na operação de assinatura:', error.message);
}
}
```
+78 -22
View File
@@ -1,43 +1,99 @@
# Instalação e Empacotamento Nativo # Instalação e Empacotamento Nativo
O **CyberCore Signer** foi projetado para distribuição sem exigir que o usuário final tenha o Java ou JRE instalado em seu computador. > **Guia de Distribuição com Runtime Embutido (`jpackage`), Instaladores MSI e Implantação Corporativa**
O **CyberCore Signer** foi projetado para distribuição nativa através da ferramenta `jpackage` (introduzida no JDK 14 e consolidada nas versões LTS 21/25). Isso significa que o usuário final **não precisa instalar o Java ou configurar variáveis de ambiente (JRE/JDK)** em sua máquina. O aplicativo carrega um runtime Java reduzido e isolado exclusivamente para sua execução.
--- ---
## 1. Requisitos de Sistema ## 1. Requisitos de Sistema
- **Sistema Operacional Homologado:** Windows 10 (64-bit) ou Windows 11 (64-bit). * **Sistema Operacional Alvo:** Windows 10 (64-bit) ou Windows 11 (64-bit).
- **Compatibilidade Adicional:** Linux x86_64 e macOS (arquitetura preparada). * **Compatibilidade Adicional:** Distribuições Linux modernas x86_64 (Ubuntu 20.04+, Debian 11+, Fedora 38+, RHEL 9+).
- **Hardware:** Mínimo de 4 GB RAM, 150 MB de espaço em disco. * **Memória RAM:** Mínimo de 4 GB (Recomendado: 8 GB).
- **Porta de Rede Local:** A porta TCP `21325` deve estar livre em `127.0.0.1`. * **Espaço em Disco:** 180 MB livres para os arquivos da aplicação e runtime embutido.
* **Rede Local:** A porta TCP `21325` deve estar livre no endereço de loopback `127.0.0.1`.
--- ---
## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage` ## 2. Geração do Instalador Windows (MSI / EXE) com `jpackage`
O empacotamento utiliza a ferramenta nativa `jpackage` incluída no JDK 21/25. ### 2.1 Pré-requisitos para a Máquina de Build
Para compilar e gerar o pacote de instalação no Windows:
1. **JDK 21+ LTS:** Com a variável `JAVA_HOME` devidamente configurada.
2. **WiX Toolset (v3.11+):** Necessário pelo `jpackage` para gerar instaladores `.msi`. Baixe em [wixtoolset.org](https://wixtoolset.org/) e adicione a pasta `bin` ao `PATH`.
### 2.2 Execução do Script de Empacotamento
Na raiz do projeto, execute o script em lote:
### No Windows:
Execute o script em lote localizado na raiz:
```cmd ```cmd
scripts\package-windows.bat scripts\package-windows.bat
``` ```
### O que o script realiza: Ou execute manualmente os comandos do pipeline:
1. Compila o projeto em modo release:
```cmd ```cmd
mvnw clean package -DskipTests :: 1. Compilação do projeto e geração do Fat JAR consolidado
``` mvnw clean package -DskipTests
2. Invoca o `jpackage` criando o instalador Windows:
- Extrai um runtime mínimo customizado contendo apenas os módulos necessários (`java.base`, `java.desktop`, `java.smartcardio`, `jdk.crypto.cryptoki`, etc.). :: 2. Geração do instalador nativo via jpackage
- Cria atalhos na Área de Trabalho e no Menu Iniciar. jpackage ^
- Gera um instalador padrão MSI ou EXE em `dist/`. --type msi ^
--name "CyberCore Signer" ^
--app-version 0.1.0 ^
--vendor "CyberCore Systems" ^
--input modules\cybercore-app\target ^
--main-jar cybercore-signer.jar ^
--main-class br.com.cybercore.signer.app.CyberCoreApp ^
--icon assets\cybercore.ico ^
--win-menu ^
--win-shortcut ^
--win-dir-chooser ^
--dest dist
```
O instalador `CyberCore Signer-0.1.0.msi` será gerado dentro da pasta `dist/`.
--- ---
## 3. Instalação e Inicialização com o Sistema ## 3. Empacotamento para Linux (DEB / RPM / AppImage)
1. Execute o instalador `CyberCore-Signer-0.1.0.msi`. No ambiente Linux x86_64:
2. Siga as instruções do assistente de instalação.
3. Ao concluir, o ícone do **CyberCore Signer** aparecerá na Área de Notificação (System Tray) ao lado do relógio do Windows. ```bash
4. O serviço local `http://127.0.0.1:21325` estará imediatamente disponível para atender as aplicações web autorizadas. # Executar o script nativo:
./scripts/package-jpackage.sh
```
O script utiliza o `jpackage` com o parâmetro `--type deb` ou `--type app-image`, empacotando os binários nativos e dependências do JavaFX com suporte ao subsistema gráfico X11/Wayland.
---
## 4. Implantação Corporativa e Instalação Silenciosa
Para administradores de rede (AD / GPO / SCCM / Intune) que precisam instalar o agente em centenas de estações de trabalho de forma automatizada:
### Instalação Silenciosa via Linha de Comando (MSI):
```cmd
msiexec /i "CyberCore Signer-0.1.0.msi" /qn /norestart
```
### Instalação com Registro de Log de Auditoria:
```cmd
msiexec /i "CyberCore Signer-0.1.0.msi" /qn /l*v "C:\Temp\cybercore_install.log"
```
### Desinstalação Silenciosa:
```cmd
msiexec /x "CyberCore Signer-0.1.0.msi" /qn /norestart
```
---
## 5. Inicialização com o Sistema e Área de Notificação (System Tray)
1. **Atalhos Criados:** O instalador cria automaticamente atalhos no Menu Iniciar e na Área de Trabalho.
2. **Inicialização Automática:** Durante a instalação, pode ser adicionada uma entrada no registro do Windows:
`HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run`
Chave: `CyberCoreSigner` -> Caminho: `"C:\Program Files\CyberCore Systems\CyberCore Signer\CyberCore Signer.exe" --minimized`
3. **Área de Notificação:** Quando iniciado, o agente minimiza para a bandeja do sistema (System Tray) ao lado do relógio do Windows, garantindo que o servidor local `127.0.0.1:21325` esteja sempre pronto para atender as solicitações das aplicações web homologadas.
+32 -15
View File
@@ -1,25 +1,42 @@
# Limitações Técnicas e Dependências Declaradas # Limitações Técnicas e Dependências Declaradas
Conforme estabelecido na **Regra 23** do projeto (*"Não invente suporte a certificados, tokens, providers, algoritmos ou hardware que não tenham sido realmente implementados/testados. Quando algo depender de fabricante, middleware, Windows ou hardware físico, declare a dependência"*), as seguintes limitações são formalmente declaradas: > **Declaração Formal de Dependências de Plataforma, Limitações de Hardware e Concorrência (Regra 23)**
Em estrita conformidade com a **Regra 23** do projeto CyberCore Signer (*"Não invente suporte a certificados, tokens, providers, algoritmos ou hardware que não tenham sido realmente implementados/testados. Quando algo depender de fabricante, middleware, Windows ou hardware físico, declare a dependência"*), as seguintes limitações técnicas e dependências são formalmente declaradas:
--- ---
## 1. Dependências de Sistema Operacional e Hardware ## 1. Dependências de Sistema Operacional e Repositório Nativo
1. **Windows Certificate Store (Windows-MY):** 1. **Repositório Nativo do Windows (`Windows-MY`):**
- Disponível apenas em sistemas operacionais Microsoft Windows (Windows 10/11). - O provedor `WindowsCertificateStoreProvider` depende exclusivamente das APIs Win32/CryptoAPI e CNG expostas pelo provedor `SunMSCAPI` do JDK.
- Em Linux e macOS, o provedor `WindowsCertificateStoreProvider` declara-se automaticamente indisponível (`isAvailable() == false`), operando via arquivos A1 ou PKCS#11 direto. - **Disponibilidade:** Suportado exclusivamente em Microsoft Windows (Windows 10 e Windows 11).
2. **Tokens A3 e Middlewares Proprietários:** - **Comportamento em Linux e macOS:** O provedor declara-se indisponível (`isAvailable() == false`) em sistemas não-Windows, operando através de arquivos A1 (PKCS#12) ou acesso PKCS#11 direto.
- O reconhecimento de smartcards e tokens USB depende da instalação prévia dos drivers do fabricante do dispositivo físico (ex: SafeSign Identity Client, SafeNet Authentication Client, drivers de leitoras Gemalto/Omnikey).
- O CyberCore Signer não instala drivers de hardware nem substitui o middleware do fabricante. 2. **Arquitetura de CPU dos Middlewares Proprietários:**
3. **Bloqueio de Cartão por Tentativas de PIN:** - A maioria dos drivers comerciais de tokens USB no Brasil (ex: SafeSign Identity Client da AET Europe e SafeNet da Thales) é compilada nativamente para a arquitetura **x86_64 (64-bit)**.
- A política de bloqueio por tentativas consecutivas de PIN incorreto (geralmente 3 tentativas) é governada estritamente pelo chip físico do dispositivo A3 e pelo driver do fabricante, estando fora do controle do software do agente. - Ambientes executando em processadores ARM64 (como novos computadores Windows on ARM ou Apple Silicon) requerem a camada de emulação x64 do sistema operacional para carregar essas bibliotecas dinâmicas legadas.
--- ---
## 2. Limitações de Rede e Concorrência ## 2. Dependências de Hardware e Middlewares de Terceiros
1. **Escuta Exclusiva em Loopback:** 1. **Tokens A3 e Leitores de Smartcard Físicos:**
- O agente não oferece nem oferece suporte para escuta em endereços de rede externa (IPs locais de LAN ou WAN). Essa restrição é um elemento fundamental do modelo de segurança. - O CyberCore Signer **não substitui os drivers do fabricante do token**. É indispensável que os drivers de hardware e o middleware do fabricante (SafeSign, SafeNet, GD Burti, Certisign, OpenSC, etc.) estejam instalados na estação de trabalho para que o sistema operacional e a leitora USB reconheçam o chip físico.
2. **Operações Concorrentes no Mesmo Token Físico:** - O agente detecta a presença da biblioteca e interage com o dispositivo através de interfaces padronizadas (PKCS#11 ou CAPI/CNG).
- Drivers PKCS#11 e smartcards ICP-Brasil frequentemente operam em modo mono-thread ou canal serial exclusivo. Por essa razão, o CyberCore Signer serializa as assinaturas em fila FIFO (`signLock`) para impedir corrupção de sessão no chip do cartão.
2. **Política de Bloqueio por PIN Incorreto (Controle do Chip Físico):**
- O chip criptográfico de qualquer smartcard ou token ICP-Brasil possui um contador de segurança interno em hardware (geralmente limitado a 3 tentativas incorretas consecutivas).
- O bloqueio do cartão e a exigência do código de desbloqueio (PUK) são governados com exclusividade pelo hardware do fabricante. O software do CyberCore Signer não tem como burlar, redefinir ou impedir esse bloqueio.
---
## 3. Concorrência e Acesso a Dispositivos Mono-Thread
1. **Serialização Mandatória de Operações (`signLock`):**
- A especificação PKCS#11 e a maioria das implementações de drivers de smartcard operam sob canal serial exclusivo de I/O por leitora.
- Requisições simultâneas de assinatura contra o mesmo token físico causam erro de sessão ou congelamento no driver caso executadas em paralelo.
- **Solução Arquitetural:** O CyberCore Signer enfileira todas as operações criptográficas através de uma trava FIFO (`signLock`), processando cada assinatura individualmente e garantindo a estabilidade do hardware.
2. **Isolamento de Origem e Rede Loopback:**
- O servidor local CCSP/1 vincula-se estritamente ao endereço `127.0.0.1:21325`. O agente não suporta e deliberadamente rejeita conexões de computadores remotos da rede local (LAN), priorizando a segurança e a inviolabilidade da máquina do usuário.
+52
View File
@@ -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`. - **Filtro:** `/Filter /Adobe.PPKLite` e `/SubFilter /adbe.pkcs7.detached`.
- **Algoritmo:** `SHA256withRSA` / PKCS#1 v1.5 com digest SHA-256. - **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. - **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);
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

+142 -55
View File
@@ -1,17 +1,30 @@
# SDK Web JavaScript (@cybercore/signer-sdk) # SDK Web JavaScript (@cybercore/signer-sdk)
O `@cybercore/signer-sdk` é a biblioteca oficial cliente em JavaScript para integrar aplicações web modernas com o agente desktop **CyberCore Signer** via protocolo **CCSP/1**. > **Manual de Integração de Aplicações Web Modernas com o Agente Desktop CyberCore Signer**
O **`@cybercore/signer-sdk`** é a biblioteca cliente oficial em JavaScript (TypeScript ready) desenvolvida para permitir que aplicações web modernas (React, Next.js, Vue, Angular ou JavaScript nativo) se comuniquem com o agente desktop **CyberCore Signer** através do protocolo local **CCSP/1**.
--- ---
## 1. Instalação e Importação ## 1. Instalação e Configuração
### Via Módulo ES6 (Browser ou Bundler Vite/Webpack/Next.js) ### 1.1 Instalação via Gerenciador de Pacotes
```bash
npm install @cybercore/signer-sdk
# ou
yarn add @cybercore/signer-sdk
# ou
pnpm add @cybercore/signer-sdk
```
### 1.2 Inicialização do Cliente
```javascript ```javascript
import { CyberCoreSigner } from '@cybercore/signer-sdk'; import { CyberCoreSigner } from '@cybercore/signer-sdk';
const signer = new CyberCoreSigner({ const signer = new CyberCoreSigner({
applicationName: 'Portal Meu Negócio' applicationName: 'Portal Meu Negócio', // Nome exibido no diálogo nativo do usuário
port: 21325, // Porta padrão do agente local (opcional)
timeoutMs: 30000 // Timeout padrão para operações (opcional)
}); });
``` ```
@@ -19,99 +32,173 @@ const signer = new CyberCoreSigner({
## 2. Métodos da API ## 2. Métodos da API
### `isInstalled(timeoutMs = 2000): Promise<boolean>` ### 2.1 Verificação de Disponibilidade e Conexão
Testa rapidamente se o agente desktop está ativo na porta local `21325`.
### `connect(): Promise<boolean>` #### `isInstalled(timeoutMs = 2000): Promise<boolean>`
Realiza o handshake inicial de autorização. Se for a primeira vez que a aplicação tenta se conectar, abre uma caixa de diálogo nativa no desktop do usuário para concessão de acesso. Testa de forma rápida e silenciosa se o agente desktop está ativo na máquina do usuário.
### `getCertificates(): Promise<Array<Object>>`
Retorna todos os certificados A1 e A3 disponíveis e operacionais na máquina do usuário.
```javascript ```javascript
const certs = await signer.getCertificates(); const disponivel = await signer.isInstalled();
certs.forEach(cert => { if (!disponivel) {
console.log(`[${cert.type}] ${cert.common_name} (Validade: ${cert.valid_to})`); alert('O CyberCore Signer não foi detectado em sua máquina. Por favor, instale o aplicativo para continuar.');
}
```
#### `connect(): Promise<boolean>`
Inicia o handshake de autorização com o agente. Se a aplicação ainda não foi homologada pelo usuário, abre um diálogo nativo na tela do computador para autorização expressa.
```javascript
const autorizado = await signer.connect();
if (!autorizado) {
console.warn('O usuário recusou a autorização da aplicação.');
}
```
---
### 2.2 Descoberta de Certificados Digitais
#### `getCertificates(): Promise<Array<CertificateInfo>>`
Retorna a lista de certificados digitais válidos e disponíveis na máquina (A1 importados e A3 conectados via USB).
```javascript
const certificados = await signer.getCertificates();
certificados.forEach(cert => {
console.log(`[${cert.type}] ${cert.common_name}`);
console.log(`Titular: ${cert.subject_dn}`);
console.log(`Validade até: ${cert.valid_to}`);
console.log(`CPF/CNPJ: ${cert.cpf || cert.cnpj}`);
}); });
``` ```
### `sign(options): Promise<Object>` ---
Solicita a assinatura digital local de um digest ou conteúdo bruto.
### `signPdf(pdfFile, options): Promise<Object>` ### 2.3 Operações de Assinatura Digital
Assina um documento PDF (PAdES / Assinatura desacoplada). Recebe `File`, `Blob`, `ArrayBuffer` ou `Uint8Array`.
Calcula o SHA-256 no próprio navegador e transmite metadados do documento (`PDF`) ao agente para exibição clara na janela de confirmação. #### `sign(options): Promise<SignatureResult>`
Assina o hash SHA-256 (digest) de qualquer informação. A chave privada nunca sai do computador e apenas o digest é assinado.
```javascript ```javascript
const pdfResult = await signer.signPdf(fileInput.files[0], { const resultado = await signer.sign({
certificateId: certs[0].id certificateId: 'win-4340656a',
digest: '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', // Hash SHA-256 em Base64
documentName: 'contrato_prestacao_servicos.pdf',
documentType: 'PDF'
}); });
console.log('Assinatura gerada (Base64):', resultado.signature);
console.log('Certificado do signatário:', resultado.certificate);
console.log('Cadeia de certificação:', resultado.certificate_chain);
``` ```
### `signXml(xmlContent, options): Promise<Object>` #### `signPdf(file, options): Promise<SignatureResult>`
Assina um documento XML (XMLDSig / NF-e). Aceita string com o XML formatado ou objeto de arquivo `File`/`Blob`. Calcula o hash do documento PDF no próprio navegador e solicita a assinatura ao CyberCore Signer.
```javascript ```javascript
const xmlResult = await signer.signXml(xmlString, { const inputElement = document.getElementById('arquivo-pdf');
certificateId: certs[0].id, const arquivoPdf = inputElement.files[0];
const resultadoPdf = await signer.signPdf(arquivoPdf, {
certificateId: certificados[0].id
});
console.log('PDF assinado digitalmente com sucesso!');
```
#### `signXml(xmlContent, options): Promise<SignatureResult>`
Assina o hash de documentos XML ou notas fiscais eletrônicas (NF-e, CT-e, MDF-e, XAdES). Aceita `string` com o XML ou arquivo `File`/`Blob`.
```javascript
const xmlString = '<infNFe Id="NFe3524...">...</infNFe>';
const resultadoXml = await signer.signXml(xmlString, {
certificateId: certificados[0].id,
documentName: 'nfe-3524.xml' documentName: 'nfe-3524.xml'
}); });
``` ```
### `signFile(file, options): Promise<Object>` #### `signFile(file, options): Promise<SignatureResult>`
Assina qualquer arquivo (ZIP, DOCX, imagens, binários, executáveis) gerando pacote de assinatura detached / CAdES. Assina arquivos genéricos de qualquer formato (ZIP, DOCX, XLSX, imagens, binários).
```javascript ```javascript
const fileResult = await signer.signFile(fileInput.files[0], { const fileInput = document.getElementById('input-qualquer-arquivo');
certificateId: certs[0].id
const resultadoArquivo = await signer.signFile(fileInput.files[0], {
certificateId: certificados[0].id
}); });
``` ```
### `authenticate(options): Promise<Object>` (ou `login(options)`) ---
Executa login seguro com certificado digital via desafio criptográfico (Challenge-Response).
### 2.4 Autenticação e Login Digital (Challenge-Response)
#### `authenticate(options): Promise<AuthResult>`
Realiza o login seguro de um usuário através de seu certificado digital ICP-Brasil. O backend da sua aplicação fornece um desafio único (*challenge / nonce*) e o CyberCore assina esse desafio.
```javascript ```javascript
const auth = await signer.authenticate({ // 1. Obter desafio único do backend da sua aplicação
certificateId: certs[0].id, const { challenge } = await fetch('/api/auth/challenge').then(res => res.json());
challenge: 'server-nonce-12345'
// 2. Solicitar assinatura do desafio ao CyberCore Signer
const authResult = await signer.authenticate({
certificateId: certificados[0].id,
challenge: challenge
}); });
console.log('Bem-vindo,', auth.subjectName); // 3. Enviar a assinatura e o certificado para o backend validar e autenticar a sessão
console.log('CPF/CNPJ:', auth.subjectCpfCnpj); const response = await fetch('/api/auth/validate-login', {
console.log('Assinatura do Desafio:', auth.signature); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge: authResult.challenge,
signature: authResult.signature,
certificate: authResult.certificate
})
});
if (response.ok) {
console.log(`Login aprovado para: ${authResult.subjectName} (${authResult.subjectCpfCnpj})`);
window.location.href = '/dashboard';
}
``` ```
### `extractSubjectInfo(certificateBase64): { name, cpfCnpj }`
Decodifica o certificado X.509 e extrai os dados do titular ICP-Brasil (Nome, CPF ou CNPJ).
### `verify(options): Promise<boolean>`
Validação sintática e criptográfica da integridade da assinatura gerada.
--- ---
## 3. Tratamento Estruturado de Erros ## 3. Tratamento Estruturado de Erros
O SDK expõe classes de erro dedicadas que herdam de `CyberCoreError`: O SDK expõe classes de erro dedicadas que herdam de `CyberCoreError`, permitindo tratar cenários de forma elegante na interface:
```javascript ```javascript
import { import {
UserCancelledError, UserCancelledError,
CertificateNotFoundError,
PinIncorrectError, PinIncorrectError,
TokenNotFoundError TokenNotFoundError,
CertificateNotFoundError,
CertificateExpiredError
} from '@cybercore/signer-sdk/errors'; } from '@cybercore/signer-sdk/errors';
try { try {
await signer.sign({ certificateId: 'cert-1', data: 'Contrato' }); const res = await signer.sign({
} catch (err) { certificateId: certId,
if (err instanceof UserCancelledError) { digest: documentoDigest
alert('Você cancelou a assinatura no CyberCore Signer.'); });
} else if (err instanceof PinIncorrectError) { } catch (error) {
alert('O PIN do token informado está incorreto.'); if (error instanceof UserCancelledError) {
} else if (err instanceof TokenNotFoundError) { // Usuário cancelou a assinatura voluntariamente na janela nativa
alert('Conecte o seu token USB na máquina para assinar.'); console.info('Operação cancelada pelo usuário.');
} else if (error instanceof PinIncorrectError) {
// PIN do token A3 incorreto
alert('PIN incorreto! Por favor, digite o PIN correto do seu token.');
} else if (error instanceof TokenNotFoundError) {
// Token USB desconectado
alert('Token não encontrado! Por favor, conecte seu dispositivo USB para assinar.');
} else if (error instanceof CertificateExpiredError) {
// Certificado ultrapassou a data de validade
alert('O certificado selecionado expirou. Entre em contato com sua autoridade certificadora.');
} else { } else {
console.error('Erro na assinatura:', err); // Demais falhas
console.error('Falha na operação com o CyberCore Signer:', error);
} }
} }
``` ```
+49 -24
View File
@@ -1,46 +1,71 @@
# Modelo de Segurança e Privacidade # Modelo de Segurança e Privacidade
O **CyberCore Signer** foi projetado seguindo as diretrizes rigorosas da ICP-Brasil e os princípios de *Security by Design* e *Least Privilege*. > **Diretrizes de Segurança em Profundidade, Proteção Criptográfica e Conformidade LGPD**
O **CyberCore Signer** foi concebido sob os princípios de *Security by Design* (Segurança desde a Concepção) e *Least Privilege* (Menor Privilégio), seguindo as normas da **Infraestrutura de Chaves Públicas Brasileira (ICP-Brasil)** e as exigências da **Lei Geral de Proteção de Dados (LGPD — Lei nº 13.709/2018)**.
--- ---
## 1. Isolamento de Rede ## 1. Isolamento de Rede e Proteção Local
1. **Loopback Estrito:** O socket do servidor HTTP local vincula-se exclusivamente ao endereço IP `127.0.0.1`. Nenhuma interface de rede externa (LAN ou WAN) escuta a porta de serviço `21325`. ### 1.1 Ligação Estrita ao Loopback (`127.0.0.1`)
2. **Prevenção contra Exposição Acidental:** Testes unitários e de arquitetura garantem que conexões fora de `127.0.0.1` sejam bloqueadas imediatamente no nível de bind do socket. O servidor HTTP do protocolo CCSP/1 vincula-se exclusivamente ao endereço IPv4 local `127.0.0.1`.
* **Sem Escuta Externa:** O agente nunca realiza bind em `0.0.0.0` ou em interfaces de rede de área local (LAN) ou externa (WAN).
* **Bloqueio no Socket:** Tentativas de acesso originadas fora da própria estação de trabalho são sumariamente recusadas pelo subsistema de rede do sistema operacional antes de qualquer processamento da aplicação.
### 1.2 Proteção Contra Ataques de DNS Rebinding
Para neutralizar ataques em que domínios externos tentam resolver para `127.0.0.1` e burlar a política de mesma origem do navegador:
* O `CcspServer` valida expressamente o cabeçalho HTTP `Host`.
* Somente requisições com `Host: 127.0.0.1:21325` ou `Host: localhost:21325` são aceitas; qualquer outro valor é rejeitado com status `403 Forbidden`.
### 1.3 Controle de Origem (CORS) e Preflight
* Requisições prévias HTTP `OPTIONS` são validadas contra as origens web homologadas.
* O cabeçalho `Access-Control-Allow-Origin` é devolvido estritamente com a origem solicitante autorizada (nunca com wildcard `*`).
--- ---
## 2. Autenticação e Autorização de Aplicações ## 2. Autenticação e Autorização de Aplicações
Nenhuma aplicação desconhecida tem autorização para obter certificados ou assinar documentos sem consentimento expresso: Nenhuma aplicação web externa tem acesso a certificados digitais ou serviços de assinatura sem autorização prévia e explícita do usuário:
1. **Handshake de Autorização (`POST /ccsp/v1/authorize`):** 1. **Handshake de Aprovação (`POST /ccsp/v1/authorize`):**
- A aplicação web identifica-se enviando seu `application_id` (UUID estável) e seu nome amigável. - Ao tentar comunicar-se pela primeira vez, a aplicação web deve fornecer seu `application_id` (UUID estável) e seu nome amigável.
- O CyberCore Signer suspende a requisição e abre uma janela modal nativa na tela principal do sistema operacional perguntando se o usuário autoriza aquele domínio web. - O CyberCore Signer suspende a requisição e exibe um modal nativo no desktop informando o nome da aplicação e solicitando autorização do usuário.
2. **Armazenamento Seguro das Autorizações:** 2. **Armazenamento Seguro de Permissões:**
- As aplicações autorizadas são gravadas em `~/.cybercore/authorized-apps.json`. - As aplicações autorizadas são gravadas no arquivo local `~/.cybercore/authorized-apps.json`.
- O usuário pode visualizar e **revogar a qualquer momento** qualquer permissão através da aba **Configurações** na interface gráfica. - O usuário pode visualizar e **revogar a qualquer momento** a autorização de qualquer aplicação através da aba **Configurações** na interface gráfica.
--- ---
## 3. Proteção Contra Ataques de Replay ## 3. Proteção Contra Ataques de Replay
Cada solicitação de assinatura enviada pelo SDK deve incluir: Para impedir que requisições de assinatura capturadas por softwares maliciosos na máquina sejam reenviadas:
- Um `nonce` (UUID criptograficamente aleatório); 1. **Identificador Único (`nonce`):** Toda requisição ao `/ccsp/v1/sign` deve conter um UUID aleatório criptograficamente forte gerado pelo chamador.
- Um campo `expires_at` (ou `timestamp`). 2. **Janela de Validade Temporal (`expires_at`):** A requisição inclui uma estampa de tempo limite (TTL).
3. **Gerenciador de Nonces (`NonceManager`):**
O componente `NonceManager`: - Mantém uma estrutura em memória com os identificadores recentes;
- Armazena os nonces recentes em memória protegida com expiração temporal (TTL); - Se o mesmo `nonce` for submetido mais de uma vez dentro da janela de validade, a operação é rejeitada com erro estruturado `REPLAY_DETECTED` (HTTP `409 Conflict`);
- Rejeita com código estruturado `REPLAY_DETECTED` caso o mesmo nonce seja submetido mais de uma vez; - Se a data/hora atual ultrapassar `expires_at`, a operação é rejeitada com erro `REQUEST_EXPIRED` (HTTP `422 Unprocessable Entity`).
- Rejeita com código `REQUEST_EXPIRED` requisições cuja data/hora de validade já tenha sido ultrapassada.
--- ---
## 4. Confirmação Visual Obrigatória e Proteção de PIN/Chaves ## 4. Confirmação Visual Obrigatória e Proteção de PIN/Chaves
1. **Confirmação Manual do Usuário:** Toda e qualquer assinatura exige intervenção física e visual do usuário. Não é possível assinar "silenciosamente" por chamadas de API em segundo plano. 1. **Assinatura Exclusivamente Assistida:** Toda operação de assinatura exige interação humana voluntária. O agente nunca assina documentos em segundo plano de forma silenciosa ou desassistida via API.
2. **Inviolabilidade da Chave Privada:** A chave privada nunca é exportada nem manipulada em forma de bytes puros. Todas as operações utilizam handles JCA/JCE ou comandos de hardware nativo. 2. **Inviolabilidade da Chave Privada:** As chaves privadas ICP-Brasil nunca são exportadas em formato de bytes brutos. Operações em certificados A3 utilizam handles criptográficos do hardware, garantindo que a chave nunca deixe o chip criptográfico.
3. **Proteção de PIN e Senhas:** 3. **Higienização Segura de Memória Volátil:**
- O PIN de smartcards e a senha de arquivos A1 nunca são transmitidos para a aplicação web e nunca aparecem em logs de auditoria. - O PIN de smartcards e senhas de certificados A1 nunca são convertidos para strings imutáveis do Java (`java.lang.String`).
- Em caso de remoção do hardware, o `SmartCardWatcher` limpa e zera os buffers de memória de PIN imediatamente. - São manipulados estritamente como arrays de caracteres mutáveis `char[]`.
- Imediatamente após o carregamento da chave, o array é sobrescrito com zeros através de `Arrays.fill(pinBuffer, (char) 0)`.
4. **Remoção de Hardware (`TOKEN_REMOVED`):** Se o usuário desconectar o token USB ou retirar o cartão do leitor durante ou após a assinatura, o `SmartCardWatcher` força a limpeza imediata do cache de sessão e dos buffers do sistema operacional.
---
## 5. Privacidade de Dados e Conformidade com a LGPD
1. **Assinatura Descentralizada por Digest:** O CyberCore Signer opera prioritariamente recebendo apenas o hash SHA-256 do arquivo. O conteúdo completo do documento (contrato, laudo, prontuário) não transita pela API local nem é gravado em logs.
2. **Mascaramento e Sigilo de CPF:** Nos selos visuais em PDF, o usuário pode configurar o modo de exibição de seu CPF (`CpfDisplayMode`):
- `FULL`: Exibição completa (`123.456.789-00`);
- `MASKED`: Dígitos sensíveis ocultados (`***.456.789-**`);
- `NONE`: Remoção completa do CPF do gráfico visual do selo.
3. **Auditoria e Logs Transparentes:** Os logs do agente registram apenas identificadores de correlação (`request_id`), algoritmo utilizado e status da operação, sem armazenar hashes de documentos sigilosos ou dados pessoais desnecessários.
+105 -19
View File
@@ -1,16 +1,27 @@
# Assinaturas XML (XMLDSig e ICP-Brasil) # Assinaturas XML no Padrão XAdES e ICP-Brasil
Este documento descreve a especificação e o funcionamento da assinatura digital de arquivos XML no padrão **W3C XMLDSig (XML Digital Signatures)** e **ICP-Brasil** no CyberCore Signer. Este documento descreve a especificação, conformidade e o funcionamento da assinatura digital de arquivos XML no padrão avançado **XAdES (XML Advanced Electronic Signatures - ETSI TS 101 903 e ICP-Brasil DOC-ICP-15.03)** e **W3C XMLDSig** no CyberCore Signer.
--- ---
## 1. Visão Geral ## 1. Visão Geral
O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`). O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`), garantindo conformidade com o padrão **XAdES-BES** adotado pelo ITI (Instituto Nacional de Tecnologia da Informação) na infraestrutura ICP-Brasil (perfil AD-RB).
### 1.1 Casos de Uso Suportados ### 1.1 Perfis Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato XML. * **`XAdES-BES` (Padrão Recomendado / ICP-Brasil):**
- Conforme especificação técnica **ETSI TS 101 903** e norma **DOC-ICP-15.03**.
- Encapsula o elemento `<ds:Object>` contendo `<xades:QualifyingProperties>` e `<xades:SignedProperties>`.
- Inclui propriedade qualificada de data/hora da assinatura (`<xades:SigningTime>`) em formato UTC ISO-8601.
- Inclui vínculo criptográfico irrefutável do certificado do signatário (`<xades:SigningCertificate>`) contendo o digest SHA-256 do certificado DER e referências do emissor/serial (`<xades:IssuerSerial>`).
- Protege as propriedades qualificadas através de uma `<ds:Reference>` em `<ds:SignedInfo>` com `Type="http://uri.etsi.org/01903#SignedProperties"`.
* **`XMLDSig` Clássico (W3C Enveloped):**
- Mantido para compatibilidade com layouts estritos de notas fiscais eletrônicas legadas (ex: NF-e, CT-e, MDF-e SEFAZ).
### 1.2 Casos de Uso Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações, laudos e relatórios em XML.
- **Assinatura de Elementos Específicos por ID:** - **Assinatura de Elementos Específicos por ID:**
- **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">` - **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">`
- **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">` - **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">`
@@ -19,19 +30,69 @@ O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando
--- ---
## 2. Padrões Criptográficos ## 2. Estrutura Criptográfica do XAdES-BES
- **Canonicalização (C14N):** `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (C14N inclusiva sem comentários). ```xml
- **Transformações:** <ds:Signature Id="sig-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest). <ds:SignedInfo>
- C14N padrão. <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#sha256`). <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- **Algoritmo de Assinatura:** RSA-SHA256 (`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`).
- **KeyInfo:** Informações do certificado X.509 embutidas em `<X509Data><X509Certificate>`. <!-- Referência 0: Dados de Negócio (Documento completo ou elemento específico) -->
<ds:Reference URI="#[ElementoId]">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[Digest do nó em Base64]</ds:DigestValue>
</ds:Reference>
<!-- Referência 1: Propriedades Qualificadas Assinadas XAdES -->
<ds:Reference URI="#signed-props-[UUID]" Type="http://uri.etsi.org/01903#SignedProperties">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[Digest das SignedProperties em Base64]</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>[Assinatura RSA-SHA256 em Base64]</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>[Certificado DER do signatário em Base64]</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#sig-[UUID]">
<xades:SignedProperties Id="signed-props-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">
<xades:SignedSignatureProperties>
<xades:SigningTime>2026-10-11T14:05:00Z</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[SHA-256 do Certificado]</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<ds:X509IssuerName>[Emissor X.500]</ds:X509IssuerName>
<ds:X509SerialNumber>[Número Serial]</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
```
--- ---
## 3. Segurança e Mitigação contra XXE ## 3. Segurança e Mitigação contra Ataques XXE
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades: O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
@@ -47,10 +108,35 @@ dbf.setExpandEntityReferences(false);
--- ---
## 4. Validação Pós-Assinatura ## 4. Validação Pós-Assinatura e Auditoria
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando: O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` e o `SignatureValidationService` realizam:
1. A integridade criptográfica da assinatura (`SignedInfo`). 1. **Validação criptográfica de SignedInfo:** Confere se o `SignatureValue` foi emitido pela chave pública correspondente.
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`). 2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
3. O certificado X.509 público extraído da chave. 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);
```
@@ -131,12 +131,12 @@ public class CyberCoreSignerApplication {
private static void printBanner() { private static void printBanner() {
System.out.println(""" System.out.println("""
============================================================ ============================================================
____ _ ____ ____ _ ____ \s
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___ / ___| _ _ | |__ ___ _ __ / ___|___ _ __ ___ \s
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\ | | | | | | | '_ \\ / _ \\ '__| | | / _ \\| '__/ _ \\ \s
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/ | |___ | |_| | | |_) | __/ | | |__| (_) | | | __/ \s
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___| \\____| \\__, | |_.__/ \\___|_| \\____\\___/|_| \\___| \s
|___/ |___/ \s
SIGNER AGENT v0.1.0 SIGNER AGENT v0.1.0
============================================================ ============================================================
"""); """);
@@ -0,0 +1,41 @@
package br.com.cybercore.signer.app.log;
import br.com.cybercore.signer.core.log.AppLogBuffer;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
* Appender do Logback que intercepta eventos reais de log e os encaminha para o buffer em memória.
*/
public class InMemoryLogAppender extends AppenderBase<ILoggingEvent> {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
@Override
protected void append(ILoggingEvent eventObject) {
if (eventObject == null) return;
LocalDateTime time = LocalDateTime.ofInstant(
Instant.ofEpochMilli(eventObject.getTimeStamp()),
ZoneId.systemDefault()
);
String timestamp = time.format(FORMATTER);
String level = eventObject.getLevel().toString();
String loggerName = eventObject.getLoggerName();
if (loggerName != null && loggerName.length() > 30) {
int lastDot = loggerName.lastIndexOf('.');
if (lastDot > 0) {
loggerName = loggerName.substring(lastDot + 1);
}
}
String message = eventObject.getFormattedMessage();
String line = String.format("[%s] [%s] %s - %s", timestamp, level, loggerName, message);
AppLogBuffer.append(line);
}
}
@@ -6,8 +6,11 @@
</encoder> </encoder>
</appender> </appender>
<appender name="MEMORY" class="br.com.cybercore.signer.app.log.InMemoryLogAppender" />
<root level="INFO"> <root level="INFO">
<appender-ref ref="CONSOLE" /> <appender-ref ref="CONSOLE" />
<appender-ref ref="MEMORY" />
</root> </root>
<logger name="br.com.cybercore.signer" level="DEBUG" /> <logger name="br.com.cybercore.signer" level="DEBUG" />
@@ -0,0 +1,48 @@
package br.com.cybercore.signer.core.log;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* Buffer circular thread-safe em memória para armazenar eventos reais de log da aplicação,
* permitindo auditoria visual em tempo real pela interface gráfica desktop.
*/
public final class AppLogBuffer {
private static final int MAX_ENTRIES = 1000;
private static final Deque<String> BUFFER = new ArrayDeque<>(MAX_ENTRIES);
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
static {
appendSystemMessage("CyberCore Signer iniciado. Aguardando eventos criptográficos e conexões locais...");
}
private AppLogBuffer() {
}
public static synchronized void append(String logLine) {
if (logLine == null || logLine.isBlank()) return;
if (BUFFER.size() >= MAX_ENTRIES) {
BUFFER.pollFirst();
}
BUFFER.addLast(logLine.trim());
}
public static synchronized void appendSystemMessage(String msg) {
String timestamp = LocalDateTime.now().format(FORMATTER);
append("[" + timestamp + "] [INFO] " + msg);
}
public static synchronized List<String> getLogs() {
return new ArrayList<>(BUFFER);
}
public static synchronized void clear() {
BUFFER.clear();
appendSystemMessage("Histórico de logs limpo pelo usuário.");
}
}
@@ -25,7 +25,8 @@ public record PdfSignatureOptions(
Integer targetPageNumber, Integer targetPageNumber,
CpfDisplayMode cpfDisplayMode, CpfDisplayMode cpfDisplayMode,
Boolean includeDateTime, Boolean includeDateTime,
String customExtraInfo String customExtraInfo,
Double customSealScale
) { ) {
public PdfSignatureOptions { public PdfSignatureOptions {
@@ -51,6 +52,21 @@ public record PdfSignatureOptions(
if (includeDateTime == null) { if (includeDateTime == null) {
includeDateTime = sealTheme.defaultIncludeDateTime(); includeDateTime = sealTheme.defaultIncludeDateTime();
} }
if (customSealScale == null || customSealScale <= 0) {
customSealScale = 1.0;
}
}
/**
* Construtor para compatibilidade com assinaturas anteriores a customSealScale.
*/
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime,
String customExtraInfo) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, customExtraInfo, 1.0);
} }
/** /**
@@ -61,7 +77,7 @@ public record PdfSignatureOptions(
String reason, String location, Float customX, Float customY, String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) { Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null); customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null, 1.0);
} }
/** /**
@@ -70,7 +86,7 @@ public record PdfSignatureOptions(
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice, PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location) { String reason, String location) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null); this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null, 1.0);
} }
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
@@ -143,6 +159,7 @@ public record PdfSignatureOptions(
} }
private String customExtraInfo; private String customExtraInfo;
private Double customSealScale = 1.0;
public Builder includeDateTime(boolean include) { public Builder includeDateTime(boolean include) {
this.includeDateTime = include; this.includeDateTime = include;
@@ -154,6 +171,11 @@ public record PdfSignatureOptions(
return this; return this;
} }
public Builder customSealScale(Double scale) {
this.customSealScale = scale;
return this;
}
public Builder customPosition(Float x, Float y, Integer pageNumber) { public Builder customPosition(Float x, Float y, Integer pageNumber) {
this.customX = x; this.customX = x;
this.customY = y; this.customY = y;
@@ -179,7 +201,8 @@ public record PdfSignatureOptions(
targetPageNumber, targetPageNumber,
cpfDisplayMode, cpfDisplayMode,
includeDateTime, includeDateTime,
customExtraInfo customExtraInfo,
customSealScale
); );
} }
} }
@@ -1,5 +1,6 @@
package br.com.cybercore.signer.core.pdf; package br.com.cybercore.signer.core.pdf;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial; import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.pdf.seal.PdfSealData; import br.com.cybercore.signer.core.pdf.seal.PdfSealData;
@@ -12,10 +13,19 @@ import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSProcessableByteArray;
@@ -172,6 +182,10 @@ public class PdfSigningService {
String issuerCn = extractIssuerCn(cert.issuerDn()); String issuerCn = extractIssuerCn(cert.issuerDn());
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now()); String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now());
// Extrai o nome amigável do titular (removendo qualquer sufixo como :CPF ou :CNPJ para o nome do assinante)
String rawName = cert.commonName() != null ? cert.commonName() : cert.alias();
String signerName = X509CertificateUtils.extractHolderName(rawName);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] pdfDigest = sha256.digest(pdfBytes); byte[] pdfDigest = sha256.digest(pdfBytes);
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase(); String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
@@ -183,7 +197,7 @@ public class PdfSigningService {
// 3. Monta dados do selo visual // 3. Monta dados do selo visual
PdfSealData sealData = new PdfSealData( PdfSealData sealData = new PdfSealData(
cert.commonName() != null ? cert.commonName() : cert.alias(), signerName,
signerDoc, signerDoc,
signingTimeStr, signingTimeStr,
issuerCn, issuerCn,
@@ -195,92 +209,78 @@ public class PdfSigningService {
effectiveExtraInfo effectiveExtraInfo
); );
// 4. Carrega o PDF e estampa o selo se solicitado // 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
byte[] intermediatePdf; // preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
if (options.visibleSeal()) { // o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData); // das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
} else {
intermediatePdf = pdfBytes;
}
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
ByteArrayOutputStream finalOut = new ByteArrayOutputStream(); ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) { try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
PDSignature signature = new PDSignature(); PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(cert.commonName()); signature.setName(signerName);
signature.setLocation(options.location()); signature.setLocation(options.location());
signature.setReason(options.reason()); signature.setReason(options.reason());
signature.setSignDate(Calendar.getInstance()); signature.setSignDate(Calendar.getInstance());
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial); SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
if (options.visibleSeal()) {
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
int targetPage = resolveTargetPage(doc, options);
PDRectangle rect = calculateSealRectangle(doc, targetPage, theme, options);
try (InputStream visualTemplate = createVisualSignatureTemplate(doc, targetPage, rect, sealPng);
SignatureOptions signatureOptions = new SignatureOptions()) {
signatureOptions.setVisualSignature(visualTemplate);
signatureOptions.setPage(targetPage);
doc.addSignature(signature, signer, signatureOptions);
doc.saveIncremental(finalOut);
}
} else {
doc.addSignature(signature, signer); doc.addSignature(signature, signer);
doc.saveIncremental(finalOut); doc.saveIncremental(finalOut);
} }
}
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size()); logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
return finalOut.toByteArray(); return finalOut.toByteArray();
} }
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme, private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
PdfSignatureOptions options) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
int totalPages = doc.getNumberOfPages(); int totalPages = doc.getNumberOfPages();
if (totalPages == 0) { if (totalPages <= 0) {
return pdfBytes; return 0;
} }
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
float sealWidth = theme.width() * 0.72f;
float sealHeight = theme.height() * 0.72f;
float margin = 20f;
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
if (options.customX() != null && options.customY() != null) {
int targetPageIdx = totalPages - 1;
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) { if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
targetPageIdx = options.targetPageNumber() - 1; return options.targetPageNumber() - 1;
} }
PDPage page = doc.getPage(targetPageIdx);
PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight();
float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
}
doc.save(baos);
return baos.toByteArray();
}
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
int startPage = 0;
int endPage = totalPages - 1;
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) { if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
endPage = 0; return 0;
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) { }
startPage = totalPages - 1; return totalPages - 1;
} }
for (int i = startPage; i <= endPage; i++) { private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
PDPage page = doc.getPage(i); PDPage page = doc.getPage(pageIndex);
PDRectangle mediaBox = page.getMediaBox(); PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth(); float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight(); float pageH = mediaBox.getHeight();
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
? options.customSealScale().floatValue()
: 1.0f;
float sealWidth = (theme.width() * 0.72f) * scale;
float sealHeight = (theme.height() * 0.72f) * scale;
float margin = 20f;
float x; float x;
float y; float y;
if (options.customX() != null && options.customY() != null) {
x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
} else {
switch (options.position()) { switch (options.position()) {
case BOTTOM_LEFT -> { case BOTTOM_LEFT -> {
x = margin; x = margin;
@@ -299,15 +299,58 @@ public class PdfSigningService {
y = margin; y = margin;
} }
} }
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
}
} }
return new PDRectangle(x, y, sealWidth, sealHeight);
}
/**
* Cria um documento template PAdES para o SignatureOptions do PDFBox, incorporando o selo visual
* como a Aparência Normal (/AP /N) da anotação da assinatura (PDAnnotationWidget).
* O stream /Contents da página do documento original permanece completamente inalterado.
*/
private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum,
PDRectangle rect, byte[] sealPngBytes) throws IOException {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
doc.addPage(page);
PDAcroForm acroForm = new PDAcroForm(doc);
doc.getDocumentCatalog().setAcroForm(acroForm);
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
acroForm.getFields().add(signatureField);
acroForm.setSignaturesExist(true);
acroForm.setAppendOnly(true);
acroForm.getCOSObject().setDirect(true);
widget.setRectangle(rect);
PDStream stream = new PDStream(doc);
PDFormXObject form = new PDFormXObject(stream);
PDResources res = new PDResources();
form.setResources(res);
form.setFormType(1);
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
form.setBBox(bbox);
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
appearance.getCOSObject().setDirect(true);
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
appearance.setNormalAppearance(appearanceStream);
widget.setAppearance(appearance);
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
PDImageXObject img = PDImageXObject.createFromByteArray(doc, sealPngBytes, "cybercore_seal");
cs.drawImage(img, 0, 0, rect.getWidth(), rect.getHeight());
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos); doc.save(baos);
return new ByteArrayInputStream(baos.toByteArray());
} }
return baos.toByteArray();
} }
private static String bytesToHex(byte[] bytes) { private static String bytesToHex(byte[] bytes) {
@@ -112,8 +112,10 @@ public final class PdfSealRenderer {
String headerText = resolveHeaderText(theme); String headerText = resolveHeaderText(theme);
String footerText = resolveFooterText(theme, null); String footerText = resolveFooterText(theme, null);
Color footerColor = new Color(100, 116, 139); // Cinza corporativo neutro e sóbrio (#64748b)
if (hasIdentity && hasDate) { if (hasIdentity && hasDate) {
// Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/Matrícula), Data, Rodapé // Layout com 5 linhas: Cabeçalho, Nome, Identificação (CPF/CNPJ/Matrícula), Data, Rodapé
g2d.setColor(primaryColor); g2d.setColor(primaryColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9)); g2d.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 9));
g2d.drawString(headerText, textX, 16); g2d.drawString(headerText, textX, 16);
@@ -127,9 +129,8 @@ public final class PdfSealRenderer {
g2d.drawString("Data: " + data.signingDate(), textX, 60); g2d.drawString("Data: " + data.signingDate(), textX, 60);
g2d.setColor(secondaryColor); g2d.setColor(footerColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8)); drawFittedText(g2d, footerText, textX, 73, textWidthAvailable, 7.8f);
g2d.drawString(footerText, textX, 73);
} else if (hasIdentity || hasDate) { } else if (hasIdentity || hasDate) {
// Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé // Layout com 4 linhas: Cabeçalho, Nome, (Identificação ou Data), Rodapé
@@ -145,9 +146,8 @@ public final class PdfSealRenderer {
String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate()); String singleLineInfo = hasIdentity ? identityLine : ("Data: " + data.signingDate());
drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f); drawFittedText(g2d, singleLineInfo, textX, 53, textWidthAvailable, 10.0f);
g2d.setColor(secondaryColor); g2d.setColor(footerColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 8)); drawFittedText(g2d, footerText, textX, 71, textWidthAvailable, 7.8f);
g2d.drawString(footerText, textX, 71);
} else { } else {
// Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé // Layout com 3 linhas: Cabeçalho, Nome em Destaque Máximo, Rodapé
@@ -158,9 +158,8 @@ public final class PdfSealRenderer {
g2d.setColor(textColor); g2d.setColor(textColor);
drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f); drawFullSignerName(g2d, fullName, textX, 44, textWidthAvailable, 14.5f);
g2d.setColor(secondaryColor); g2d.setColor(footerColor);
g2d.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 9)); drawFittedText(g2d, footerText, textX, 68, textWidthAvailable, 8.0f);
g2d.drawString(footerText, textX, 68);
} }
} finally { } finally {
@@ -250,19 +249,28 @@ public final class PdfSealRenderer {
* Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado. * Formata os dados de documento (CPF/CNPJ) de acordo com o modo de exibição configurado.
*/ */
private static String formatSignerDoc(PdfSealData data) { private static String formatSignerDoc(PdfSealData data) {
CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL; String rawDoc = data.identifier() != null ? data.identifier().trim() : "";
String rawDoc = data.identifier() != null ? data.identifier() : ""; if (rawDoc.isBlank()) return "";
String digits = rawDoc.replaceAll("\\D", "");
// Pessoa Jurídica (14 dígitos): Rótulo CNPJ, SEMPRE completo, sem mascaramento
if (digits.length() == 14 || rawDoc.toUpperCase().contains("CNPJ")) {
if (digits.length() == 14) {
return "CNPJ: " + String.format("%s.%s.%s/%s-%s",
digits.substring(0, 2), digits.substring(2, 5), digits.substring(5, 8),
digits.substring(8, 12), digits.substring(12, 14));
}
return rawDoc.toUpperCase().startsWith("CNPJ") ? rawDoc : "CNPJ: " + rawDoc;
}
// Pessoa Física (11 dígitos): Rótulo CPF, respeitando CpfDisplayMode
CpfDisplayMode mode = data.cpfDisplayMode() != null ? data.cpfDisplayMode() : CpfDisplayMode.FULL;
if (mode == CpfDisplayMode.NONE) { if (mode == CpfDisplayMode.NONE) {
// Se for CPF, oculta. Se for CNPJ, pode manter ou formatar.
if (rawDoc.toUpperCase().contains("CPF")) {
return ""; return "";
} }
return rawDoc;
}
if (rawDoc.toUpperCase().contains("CPF")) { if (digits.length() == 11) {
String digits = rawDoc.replaceAll("\\D", "");
String formatted = mode.formatCpf(digits); String formatted = mode.formatCpf(digits);
return formatted.isBlank() ? "" : "CPF: " + formatted; return formatted.isBlank() ? "" : "CPF: " + formatted;
} }
@@ -378,10 +386,10 @@ public final class PdfSealRenderer {
return theme.customHeader().toUpperCase(); return theme.customHeader().toUpperCase();
} }
return switch (theme.id().toLowerCase()) { return switch (theme.id().toLowerCase()) {
case "cybercore-modern" -> "CYBERCORE SIGNER • AUTENTICAÇÃO DIGITAL"; case "cybercore-modern" -> "DOCUMENTO ASSINADO DIGITALMENTE";
case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE"; case "minimal-clean" -> "DOCUMENTO ASSINADO DIGITALMENTE";
case "gov-br" -> "GOV.BR • CONFORMIDADE MP Nº 2.200-2/2001"; case "gov-br" -> "DOCUMENTO ASSINADO DIGITALMENTE";
default -> "ASSINADO DIGITALMENTE • ICP-BRASIL"; default -> "DOCUMENTO ASSINADO DIGITALMENTE";
}; };
} }
@@ -393,10 +401,10 @@ public final class PdfSealRenderer {
return "Hash Criptográfico: " + hash; return "Hash Criptográfico: " + hash;
} }
return switch (theme.id().toLowerCase()) { return switch (theme.id().toLowerCase()) {
case "cybercore-modern" -> "Autenticidade assegurada por chave criptográfica local"; case "cybercore-modern" -> "Verifique a autenticidade em https://validar.iti.gov.br";
case "minimal-clean" -> "Assinatura qualificada padrão ICP-Brasil"; case "minimal-clean" -> "Verifique a autenticidade em https://validar.iti.gov.br";
case "gov-br" -> "Validade jurídica plena conforme MP nº 2.200-2/2001"; case "gov-br" -> "Verifique a autenticidade em https://validar.iti.gov.br";
default -> "Conformidade ICP-Brasil e validade jurídica plena"; default -> "Verifique a autenticidade em https://validar.iti.gov.br";
}; };
} }
@@ -35,10 +35,10 @@ public final class PdfSealThemeRegistry {
} }
private static void registerBuiltInThemes() { private static void registerBuiltInThemes() {
// 1. Tema ICP-Brasil Institucional // Único Selo Padrão Embutido do Sistema: ICP-Brasil Institucional Oficial
registerTheme(new PdfSealTheme( registerTheme(new PdfSealTheme(
"icp-brasil", DEFAULT_THEME_ID,
"ICP-Brasil Institucional", "ICP-Brasil Institucional (Padrão)",
"Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular", "Estilo oficial corporativo com escudo de segurança vetorial e dados completos do titular",
320, 320,
82, 82,
@@ -51,7 +51,7 @@ public final class PdfSealThemeRegistry {
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
<rect x="1" y="1" width="318" height="80" rx="6" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/> <rect x="1" y="1" width="318" height="80" rx="6" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="3" fill="${THEME_PRIMARY}"/> <rect x="1" y="1" width="6" height="80" rx="3" fill="${THEME_PRIMARY}"/>
<!-- Escudo Criptográfico Vetorial --> <!-- Escudo Criptográfico Vetorial ICP-Brasil -->
<g transform="translate(14, 18)"> <g transform="translate(14, 18)">
<path d="M18 2 L32 7 V18 C32 28 25 36 18 39 C11 36 4 28 4 18 V7 Z" fill="${THEME_PRIMARY}" opacity="0.12"/> <path d="M18 2 L32 7 V18 C32 28 25 36 18 39 C11 36 4 28 4 18 V7 Z" fill="${THEME_PRIMARY}" opacity="0.12"/>
<path d="M18 2 L32 7 V18 C32 28 25 36 18 39 C11 36 4 28 4 18 V7 Z" fill="none" stroke="${THEME_PRIMARY}" stroke-width="2"/> <path d="M18 2 L32 7 V18 C32 28 25 36 18 39 C11 36 4 28 4 18 V7 Z" fill="none" stroke="${THEME_PRIMARY}" stroke-width="2"/>
@@ -65,104 +65,7 @@ public final class PdfSealThemeRegistry {
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text> <text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER}</text> <text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER}</text>
<text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Data/Hora: ${SIGNING_DATE}</text> <text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Data/Hora: ${SIGNING_DATE}</text>
<text x="56" y="72" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="${THEME_SECONDARY}">Verificação criptográfica válida e com fé pública</text> <text x="56" y="72" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">Verifique a autenticidade em https://validar.iti.gov.br</text>
</svg>
"""
));
// 2. Tema CyberCore Moderno
registerTheme(new PdfSealTheme(
"cybercore-modern",
"CyberCore Moderno",
"Identidade tecnológica com cristal vetorial CyberCore e visual ciano/esmeralda",
320,
82,
"#0284c7",
"#10b981",
"#ffffff",
"#38bdf8",
"#0f172a",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
<rect x="1" y="1" width="318" height="80" rx="8" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/>
<rect x="1" y="1" width="5" height="80" rx="2.5" fill="${THEME_SECONDARY}"/>
<!-- Cristal CyberCore Vetorial -->
<g transform="translate(14, 20)">
<polygon points="18,2 32,10 32,28 18,36 4,28 4,10" fill="${THEME_PRIMARY}" opacity="0.15"/>
<polygon points="18,2 32,10 32,28 18,36 4,28 4,10" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.8"/>
<line x1="18" y1="2" x2="18" y2="36" stroke="${THEME_SECONDARY}" stroke-width="1.2"/>
<line x1="4" y1="19" x2="32" y2="19" stroke="${THEME_PRIMARY}" stroke-width="1.2"/>
</g>
<!-- Dados da Assinatura -->
<text x="56" y="17" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${THEME_PRIMARY}">CYBERCORE SIGNER • ASSINATURA ELETRÔNICA</text>
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">${SIGNER_DOC} • ${CERT_ISSUER}</text>
<text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Autenticado em: ${SIGNING_DATE}</text>
<text x="56" y="72" font-family="monospace" font-size="7.5" fill="${THEME_SECONDARY}">Hash: ${HASH_SUMMARY}</text>
</svg>
"""
));
// 3. Tema Minimalista Clássico
registerTheme(new PdfSealTheme(
"minimal-clean",
"Minimalista Clássico",
"Design monocromático sóbrio em tons de grafite, ideal para contratos e certidões",
300,
76,
"#334155",
"#64748b",
"#fafafa",
"#cbd5e1",
"#1e293b",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 76" width="300" height="76">
<rect x="1" y="1" width="298" height="74" rx="4" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1"/>
<!-- Ícone de Carimbo Notarial -->
<g transform="translate(12, 18)">
<circle cx="18" cy="18" r="16" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.5"/>
<circle cx="18" cy="18" r="13" fill="none" stroke="${THEME_PRIMARY}" stroke-width="0.8" stroke-dasharray="2,2"/>
<path d="M12 18 L16 22 L24 14" fill="none" stroke="${THEME_PRIMARY}" stroke-width="2"/>
</g>
<!-- Dados da Assinatura -->
<text x="52" y="17" font-family="Georgia, serif" font-size="9" font-weight="bold" fill="${THEME_PRIMARY}">Assinado Digitalmente</text>
<text x="52" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="11.5" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="52" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="8" fill="#475569">${SIGNER_DOC} • ${CERT_ISSUER}</text>
<text x="52" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="8" fill="#475569">${SIGNING_DATE}</text>
<text x="52" y="69" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="7.5" fill="#64748b">Conforme MP 2.200-2/2001 e ICP-Brasil</text>
</svg>
"""
));
// 4. Tema Governamental (Gov.br / ITI)
registerTheme(new PdfSealTheme(
"gov-br",
"Governamental (Padrão Gov.br / ITI)",
"Faixa verde e ouro com declaração expressa de validade pela MP nº 2.200-2/2001",
320,
82,
"#047857",
"#eab308",
"#ffffff",
"#a7f3d0",
"#064e3b",
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82">
<rect x="1" y="1" width="318" height="80" rx="4" fill="${THEME_BG}" stroke="${THEME_BORDER}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="2" fill="${THEME_PRIMARY}"/>
<rect x="7" y="1" width="3" height="80" fill="${THEME_SECONDARY}"/>
<!-- Brasão Estilizado -->
<g transform="translate(16, 20)">
<circle cx="17" cy="18" r="15" fill="${THEME_PRIMARY}" opacity="0.1"/>
<circle cx="17" cy="18" r="15" fill="none" stroke="${THEME_PRIMARY}" stroke-width="1.5"/>
<path d="M17 6 L20 14 L28 14 L22 19 L24 27 L17 22 L10 27 L12 19 L6 14 L14 14 Z" fill="${THEME_SECONDARY}"/>
</g>
<!-- Dados da Assinatura -->
<text x="56" y="17" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${THEME_PRIMARY}">GOV.BR • DOCUMENTO ASSINADO DIGITALMENTE</text>
<text x="56" y="32" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${THEME_TEXT}">${SIGNER_NAME}</text>
<text x="56" y="46" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#065f46">Doc: ${SIGNER_DOC} • AC: ${CERT_ISSUER}</text>
<text x="56" y="59" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" fill="#475569">Data/Hora: ${SIGNING_DATE}</text>
<text x="56" y="72" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#047857">Validade jurídica assegurada pela MP nº 2.200-2/2001</text>
</svg> </svg>
""" """
)); ));
@@ -205,10 +108,9 @@ public final class PdfSealThemeRegistry {
if (themeId == null) return false; if (themeId == null) return false;
String idLower = themeId.toLowerCase(); String idLower = themeId.toLowerCase();
// Protege temas padrão // Protege o tema padrão
if (idLower.equals("icp-brasil") || idLower.equals("cybercore-modern") || if (idLower.equals(DEFAULT_THEME_ID)) {
idLower.equals("minimal-clean") || idLower.equals("gov-br")) { throw new IllegalArgumentException("Não é permitido excluir o selo padrão pré-instalado do sistema.");
throw new IllegalArgumentException("Não é permitido excluir os temas de selo pré-instalados do sistema.");
} }
boolean deleted = STORAGE.deleteSeal(idLower); boolean deleted = STORAGE.deleteSeal(idLower);
@@ -218,7 +120,11 @@ public final class PdfSealThemeRegistry {
public static Optional<PdfSealTheme> findById(String id) { public static Optional<PdfSealTheme> findById(String id) {
if (id == null) return Optional.empty(); if (id == null) return Optional.empty();
return Optional.ofNullable(THEMES.get(id.toLowerCase())); PdfSealTheme theme = THEMES.get(id.toLowerCase());
if (theme == null && ("cybercore-modern".equalsIgnoreCase(id) || "minimal-clean".equalsIgnoreCase(id) || "gov-br".equalsIgnoreCase(id))) {
return Optional.of(getDefaultTheme());
}
return Optional.ofNullable(theme);
} }
public static Optional<PdfSealTheme> getTheme(String id) { public static Optional<PdfSealTheme> getTheme(String id) {
@@ -56,7 +56,67 @@ public record CcspRequest(
String purpose, String purpose,
@JsonProperty("challenge") @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<String, Object> 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 { ) implements Serializable {
public static final String PROTOCOL_VERSION = "1"; public static final String PROTOCOL_VERSION = "1";
@@ -68,7 +128,9 @@ public record CcspRequest(
) { ) {
this(version, requestId, applicationId, operation, algorithm, digest, this(version, requestId, applicationId, operation, algorithm, digest,
certificateHint, expiresAt, timestamp, nonce, signatureType, 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() { public boolean isVersionSupported() {
@@ -49,7 +49,16 @@ public record CcspResponse(
String documentName, String documentName,
@JsonProperty("document_type") @JsonProperty("document_type")
String documentType String documentType,
@JsonProperty("signed_pdf")
String signedPdf,
@JsonProperty("signed_xml")
String signedXml,
@JsonProperty("sha256")
String sha256
) implements Serializable { ) implements Serializable {
public CcspResponse { public CcspResponse {
@@ -88,7 +97,58 @@ public record CcspResponse(
subjectName, subjectName,
subjectCpfCnpj, subjectCpfCnpj,
documentName, documentName,
documentType documentType,
null,
null,
null
);
}
public static CcspResponse pdfSuccess(String requestId, String signedPdfBase64, String sha256,
String signatureBase64, String certBase64, List<String> 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<String> 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, subjectName,
subjectCpfCnpj, subjectCpfCnpj,
null, null,
"AUTH_CHALLENGE" "AUTH_CHALLENGE",
null,
null,
null
); );
} }
@@ -126,6 +189,9 @@ public record CcspResponse(
null, null,
null, null,
null, null,
null,
null,
null,
null null
); );
} }
@@ -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.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext; 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 br.com.cybercore.signer.core.domain.SigningResult;
import java.security.MessageDigest; import java.security.MessageDigest;
import br.com.cybercore.signer.core.provider.CertificateManager; 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.Logger;
import org.slf4j.LoggerFactory; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@@ -56,6 +71,8 @@ public class CcspServer {
private final int port; private final int port;
private final CertificateManager certificateManager; private final CertificateManager certificateManager;
private final SigningService signingService; private final SigningService signingService;
private final PdfSigningService pdfSigningService;
private final XmlSigningService xmlSigningService;
private final ApplicationRegistry applicationRegistry; private final ApplicationRegistry applicationRegistry;
private final NonceManager nonceManager; private final NonceManager nonceManager;
private final OriginValidator originValidator; private final OriginValidator originValidator;
@@ -75,6 +92,8 @@ public class CcspServer {
this.port = port; this.port = port;
this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null"); this.certificateManager = Objects.requireNonNull(certificateManager, "certificateManager cannot be null");
this.signingService = Objects.requireNonNull(signingService, "signingService 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.applicationRegistry = applicationRegistry != null ? applicationRegistry : ApplicationRegistry.getInstance();
this.promptListener = promptListener; this.promptListener = promptListener;
this.nonceManager = new NonceManager(); this.nonceManager = new NonceManager();
@@ -114,6 +133,9 @@ public class CcspServer {
httpServer.createContext("/ccsp/v1/sign", new SignHandler()); httpServer.createContext("/ccsp/v1/sign", new SignHandler());
httpServer.createContext("/ccsp/v1/authenticate", new SignHandler()); httpServer.createContext("/ccsp/v1/authenticate", new SignHandler());
httpServer.createContext("/ccsp/v1/authorize", new AuthorizeHandler()); 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(); httpServer.start();
running = true; running = true;
@@ -306,10 +328,14 @@ public class CcspServer {
try (InputStream is = exchange.getRequestBody()) { try (InputStream is = exchange.getRequestBody()) {
request = objectMapper.readValue(is, CcspRequest.class); request = objectMapper.readValue(is, CcspRequest.class);
} catch (Exception e) { } catch (Exception e) {
logger.warn("[CCSP REQ INVÁLIDO] POST /ccsp/v1/sign de Origin: {} | Erro parsing: {}", origin, e.getMessage());
sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido."); sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
return; return;
} }
logger.info("[CCSP REQ] POST /ccsp/v1/sign | Origin: {} | ReqId: {} | App: {} | Op: {} | Algoritmo: {} | CertHint: {}",
origin, request.requestId(), request.applicationId(), request.operation(), request.algorithm(), request.certificateHint());
// 1. Validação de versão // 1. Validação de versão
if (!request.isVersionSupported()) { if (!request.isVersionSupported()) {
sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR, sendCcspError(exchange, 400, request.requestId(), CcspErrorCode.COMMUNICATION_ERROR,
@@ -458,6 +484,525 @@ 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) {
logger.warn("[CCSP REQ INVÁLIDO] POST /ccsp/v1/sign-pdf de Origin: {} | Erro parsing: {}", origin, e.getMessage());
sendCcspError(exchange, 400, null, CcspErrorCode.SIGNATURE_FAILED, "Payload de requisição inválido.");
return;
}
int pdfLength = request.pdf() != null ? request.pdf().length() : 0;
logger.info("[CCSP REQ] POST /ccsp/v1/sign-pdf | Origin: {} | ReqId: {} | App: {} | Doc: '{}' | Base64: {} chars | CertHint: {}",
origin, request.requestId(), request.applicationId(), request.documentName(), pdfLength, request.certificateHint());
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<ManagedCertificate> 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<Boolean> 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();
}
logger.info("[CCSP REQ] Selo visual configurado: '{}' (custom={}, hasLogo={}, header='{}')",
sealTheme.name(), sealTheme.isCustom(), sealTheme.customLogoBase64() != null, sealTheme.customHeader());
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<SigningKeyMaterial> keyMat = certificateManager.getKeyMaterial(cert.id(), null);
String certBase64 = null;
List<String> 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<ManagedCertificate> 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<Boolean> 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<SigningKeyMaterial> keyMat = certificateManager.getKeyMaterial(cert.id(), null);
String certBase64 = null;
List<String> 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<Map<String, Object>> sealList = PdfSealThemeRegistry.listThemes().stream().map(t -> {
Map<String, Object> dto = new HashMap<>();
dto.put("id", t.id());
dto.put("name", t.name());
dto.put("description", t.description());
dto.put("width", t.width());
dto.put("height", t.height());
dto.put("primary_color", t.primaryColor());
dto.put("secondary_color", t.secondaryColor());
dto.put("background_color", t.backgroundColor());
dto.put("border_color", t.borderColor());
dto.put("text_color", t.textColor());
dto.put("icon_style", t.iconStyle());
dto.put("is_custom", t.isCustom());
dto.put("default_cpf_mode", t.defaultCpfMode() != null ? t.defaultCpfMode().name() : "FULL");
dto.put("default_include_date_time", t.defaultIncludeDateTime());
return dto;
}).toList();
Map<String, Object> resp = new HashMap<>();
resp.put("version", CcspRequest.PROTOCOL_VERSION);
resp.put("status", "success");
resp.put("default_seal_id", PdfSealThemeRegistry.getDefaultTheme().id());
resp.put("seals", sealList);
sendJsonResponse(exchange, 200, resp);
}
}
private static String getMapString(Map<String, Object> map, String defaultVal, String... keys) {
if (map == null) return defaultVal;
for (String k : keys) {
Object v = map.get(k);
if (v != null && !v.toString().isBlank()) {
return v.toString().trim();
}
}
return defaultVal;
}
private static int getMapInt(Map<String, Object> map, int defaultVal, String... keys) {
if (map == null) return defaultVal;
for (String k : keys) {
Object v = map.get(k);
if (v instanceof Number n) {
return n.intValue();
} else if (v != null) {
try {
return Integer.parseInt(v.toString().trim());
} catch (Exception ignored) {}
}
}
return defaultVal;
}
private PdfSealTheme parseCustomSealTheme(Map<String, Object> map) {
if (map == null || map.isEmpty()) {
return PdfSealThemeRegistry.getDefaultTheme();
}
String id = "custom-" + UUID.randomUUID().toString().substring(0, 8);
String name = getMapString(map, "Selo Personalizado", "name", "theme_name", "title");
String description = getMapString(map, "Selo customizado via aplicação web", "description", "desc");
int width = getMapInt(map, 320, "width", "w");
int height = getMapInt(map, 82, "height", "h");
String primaryColor = getMapString(map, "#0d47a1", "primaryColor", "primary_color");
String secondaryColor = getMapString(map, "#0284c7", "secondaryColor", "secondary_color");
String backgroundColor = getMapString(map, "#ffffff", "backgroundColor", "background_color");
String borderColor = getMapString(map, "#cbd5e1", "borderColor", "border_color");
String textColor = getMapString(map, "#0f172a", "textColor", "text_color");
String customHeader = getMapString(map, null, "customHeader", "custom_header", "title");
String customFooter = getMapString(map, null, "customFooter", "custom_footer");
String iconStyle = getMapString(map, "shield", "iconStyle", "icon_style");
String customLogoBase64 = getMapString(map, null, "customLogoBase64", "custom_logo_base64", "logo", "logoBase64");
String customFieldLabel = getMapString(map, null, "customFieldLabel", "custom_field_label");
String customFieldValue = getMapString(map, null, "customFieldValue", "custom_field_value");
CpfDisplayMode cpfMode = CpfDisplayMode.FULL;
String rawCpfMode = getMapString(map, null, "defaultCpfMode", "default_cpf_mode", "cpfMode", "cpf_mode");
if (rawCpfMode != null) {
try {
cpfMode = CpfDisplayMode.valueOf(rawCpfMode.toUpperCase());
} catch (Exception ignored) {}
}
boolean includeDateTime = true;
Object dtObj = map.containsKey("defaultIncludeDateTime") ? map.get("defaultIncludeDateTime")
: (map.containsKey("default_include_date_time") ? map.get("default_include_date_time")
: (map.containsKey("includeDateTime") ? map.get("includeDateTime")
: map.get("include_date_time")));
if (dtObj instanceof Boolean b) {
includeDateTime = b;
} else if (dtObj != null) {
includeDateTime = Boolean.parseBoolean(dtObj.toString());
}
return new PdfSealTheme(
id, name, description, width, height,
primaryColor, secondaryColor, backgroundColor, borderColor, textColor,
null, customHeader, customFooter, iconStyle,
cpfMode, includeDateTime, true, customLogoBase64,
customFieldLabel, customFieldValue
);
}
private static String bytesToHex(byte[] bytes) {
if (bytes == null) return "";
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// ========================================================================= // =========================================================================
// Utilitários de Segurança e HTTP // Utilitários de Segurança e HTTP
// ========================================================================= // =========================================================================
@@ -494,7 +1039,8 @@ public class CcspServer {
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin); exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce"); exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400"); exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) { if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
@@ -531,6 +1077,22 @@ public class CcspServer {
try (OutputStream os = exchange.getResponseBody()) { try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes); os.write(bytes);
} }
String path = exchange.getRequestURI().getPath();
if (data instanceof CcspResponse resp) {
if ("signed".equalsIgnoreCase(resp.status())) {
logger.info("[CCSP RESP] {} -> HTTP {} | ReqId: {} | Status: signed | Resposta: {} bytes | SHA-256: {}",
path, statusCode, resp.requestId(), bytes.length, resp.sha256() != null ? resp.sha256() : "n/a");
} else if ("error".equalsIgnoreCase(resp.status())) {
logger.warn("[CCSP RESP ERROR] {} -> HTTP {} | ReqId: {} | Code: {} | Msg: {}",
path, statusCode, resp.requestId(), resp.errorCode(), resp.message());
} else {
logger.info("[CCSP RESP] {} -> HTTP {} | ReqId: {} | Status: {} | Resposta: {} bytes",
path, statusCode, resp.requestId(), resp.status(), bytes.length);
}
} else {
logger.info("[CCSP RESP] {} -> HTTP {} | Resposta: {} bytes", path, statusCode, bytes.length);
}
} }
private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException { private void sendErrorResponse(HttpExchange exchange, int httpStatus, String code, String message) throws IOException {
@@ -15,6 +15,7 @@ import org.bouncycastle.util.Store;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignature;
@@ -221,32 +222,59 @@ public final class SignatureValidationService {
} }
/** /**
* Validação de assinaturas embutidas em documentos XML (XMLDSig). * Validação de assinaturas embutidas em documentos XML (XAdES ou XMLDSig).
*/ */
private ValidationResult validateXml(byte[] xmlBytes, String fileName) { private ValidationResult validateXml(byte[] xmlBytes, String fileName) {
try { try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes)); Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes));
// Registro de atributos Id no DOM
registerAllIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) { if (nl.getLength() == 0) {
return new ValidationResult( return new ValidationResult(
ValidationStatus.INVALID, ValidationStatus.INVALID,
ValidationType.XML_DSIG, ValidationType.XML_DSIG,
"Nenhum elemento <Signature> XMLDSig encontrado no documento.", "Nenhum elemento <Signature> XML encontrado no documento.",
List.of(), List.of(),
null, null,
fileName fileName
); );
} }
// Identificação de padrão XAdES
NodeList qpList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "QualifyingProperties");
if (qpList.getLength() == 0) {
qpList = doc.getElementsByTagName("QualifyingProperties");
}
boolean isXades = qpList.getLength() > 0;
ValidationType valType = isXades ? ValidationType.XADES_XML : ValidationType.XML_DSIG;
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
List<ValidationSignerInfo> signers = new ArrayList<>(); List<ValidationSignerInfo> signers = new ArrayList<>();
boolean allValid = true; boolean allValid = true;
for (int i = 0; i < nl.getLength(); i++) { for (int i = 0; i < nl.getLength(); i++) {
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(i)); Element sigElement = (Element) nl.item(i);
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), sigElement);
// Registra nós com atributo Id no valContext
NodeList allNodes = doc.getElementsByTagName("*");
for (int n = 0; n < allNodes.getLength(); n++) {
if (allNodes.item(n) instanceof Element e && e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
}
XMLSignature signature = fac.unmarshalXMLSignature(valContext); XMLSignature signature = fac.unmarshalXMLSignature(valContext);
boolean coreValidity = signature.validate(valContext); boolean coreValidity = signature.validate(valContext);
@@ -254,22 +282,66 @@ public final class SignatureValidationService {
allValid = false; allValid = false;
} }
// Extração de metadados do signatário a partir do KeyInfo
String signerName = "Signatário XML #" + (i + 1);
String identifier = "";
String issuer = isXades ? "ICP-Brasil / XAdES" : "Assinatura Enveloped XML";
String certSubject = "XML Signature Element";
if (signature.getKeyInfo() != null) {
for (Object kiContent : signature.getKeyInfo().getContent()) {
if (kiContent instanceof javax.xml.crypto.dsig.keyinfo.X509Data x509Data) {
for (Object o : x509Data.getContent()) {
if (o instanceof X509Certificate cert) {
certSubject = cert.getSubjectX500Principal().getName();
signerName = X509CertificateUtils.extractCommonName(certSubject);
String cpf = X509CertificateUtils.extractCpf(cert, signerName);
if (cpf != null) {
identifier = "CPF: " + cpf;
} else {
String cnpj = X509CertificateUtils.extractCnpj(cert, signerName);
if (cnpj != null) {
identifier = "CNPJ: " + cnpj;
}
}
issuer = X509CertificateUtils.extractCommonName(cert.getIssuerX500Principal().getName());
break;
}
}
}
}
}
// Data de assinatura (SigningTime se XAdES, senão data atual)
String dateStr = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date());
if (isXades) {
NodeList stList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SigningTime");
if (stList.getLength() > 0) {
dateStr = stList.item(0).getTextContent();
}
}
signers.add(new ValidationSignerInfo( signers.add(new ValidationSignerInfo(
"Signatário XML #" + (i + 1), signerName,
"", identifier,
"Assinatura Enveloped XML", issuer,
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()), dateStr,
coreValidity, coreValidity,
coreValidity, coreValidity,
signature.getSignedInfo().getSignatureMethod().getAlgorithm(), signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
"XMLDSig Element", certSubject,
"Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA") (isXades ? "Padrão XAdES-BES (ETSI TS 101 903 / ICP-Brasil)" : "Padrão XMLDSig W3C")
+ " • " + (coreValidity ? "Assinatura íntegra" : "Integridade violada")
)); ));
} }
ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID; ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID;
String msg = allValid ? "Documento XML assinado e válido conforme padrão W3C XML-DSig." : "Assinatura XML inválida ou com integridade violada."; String msg = allValid
return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName); ? (isXades
? "Documento XML assinado e íntegro no padrão avançado XAdES-BES (ICP-Brasil)."
: "Documento XML assinado e válido conforme padrão W3C XMLDSig.")
: "Assinatura XML inválida ou com integridade violada.";
return new ValidationResult(status, valType, msg, signers, null, fileName);
} catch (Exception e) { } catch (Exception e) {
logger.error("Erro ao validar XML: {}", e.getMessage(), e); logger.error("Erro ao validar XML: {}", e.getMessage(), e);
@@ -284,6 +356,23 @@ public final class SignatureValidationService {
} }
} }
private static void registerAllIdAttributes(Element root) {
if (root == null) return;
if (root.hasAttribute("Id")) {
root.setIdAttribute("Id", true);
} else if (root.hasAttribute("id")) {
root.setIdAttribute("id", true);
} else if (root.hasAttribute("ID")) {
root.setIdAttribute("ID", true);
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element child) {
registerAllIdAttributes(child);
}
}
}
/** /**
* Processa e valida criptograficamente os signatários de um pacote CMSSignedData. * Processa e valida criptograficamente os signatários de um pacote CMSSignedData.
*/ */
@@ -7,6 +7,7 @@ public enum ValidationType {
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false), DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true), ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true),
PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true), PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true),
XADES_XML("Assinatura Incorporada em XML (XAdES / ICP-Brasil)", true),
XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true), XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true),
UNKNOWN("Formato Desconhecido", false); UNKNOWN("Formato Desconhecido", false);
@@ -3,30 +3,43 @@ package br.com.cybercore.signer.core.xml;
import java.util.Objects; import java.util.Objects;
/** /**
* Parâmetros de configuração para a operação de assinatura digital XMLDSig. * Parâmetros de configuração para a operação de assinatura digital XML (XAdES ou XMLDSig).
*/ */
public record XmlSignatureOptions( public record XmlSignatureOptions(
String certificateId, String certificateId,
char[] pinOrPassword, char[] pinOrPassword,
String targetElementTag, String targetElementTag,
String targetElementId, String targetElementId,
String signatureAlgorithm String signatureAlgorithm,
XmlSignatureProfile profile
) { ) {
public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
public static final XmlSignatureProfile DEFAULT_PROFILE = XmlSignatureProfile.XADES_BES;
public XmlSignatureOptions { public XmlSignatureOptions {
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo."); Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) { if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
signatureAlgorithm = DEFAULT_ALGORITHM; signatureAlgorithm = DEFAULT_ALGORITHM;
} }
if (profile == null) {
profile = DEFAULT_PROFILE;
}
} }
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) { public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) {
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM); return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
}
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword, XmlSignatureProfile profile) {
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, profile);
} }
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) { public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) {
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM); return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
}
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id, XmlSignatureProfile profile) {
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, profile);
} }
} }
@@ -0,0 +1,37 @@
package br.com.cybercore.signer.core.xml;
/**
* Perfis de assinatura digital para documentos XML suportados pelo CyberCore Signer.
*/
public enum XmlSignatureProfile {
/**
* Padrão XAdES-BES (XML Advanced Electronic Signatures - Basic Electronic Signature).
* Em conformidade com a especificação ETSI TS 101 903 e padrão ICP-Brasil DOC-ICP-15.03 (AD-RB).
* Inclui propriedades qualificadas assinadas: data/hora de assinatura (SigningTime)
* e referência criptográfica do certificado do signatário (SigningCertificate).
*/
XADES_BES("XAdES-BES (Padrão ICP-Brasil / ETSI)", "Assinatura avançada com data/hora e certificado qualificado vinculados"),
/**
* Padrão W3C XMLDSig convencional (Enveloped Signature sem propriedades qualificadas XAdES).
* Mantido para compatibilidade com layouts fiscais estritos (ex: NF-e, CT-e, MDF-e SEFAZ legados).
*/
XMLDSIG("XMLDSig Clássico (W3C)", "Assinatura enveloped básica sem propriedades qualificadas");
private final String displayName;
private final String description;
XmlSignatureProfile(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
}
@@ -17,12 +17,14 @@ import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException; import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult; import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext; import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference; import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.dom.DOMSignContext;
@@ -43,25 +45,41 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.Key; import java.security.MessageDigest;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
/** /**
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo o padrão W3C XMLDSig e ICP-Brasil. * Serviço de alto nível para Assinatura Digital de Documentos XML segundo os padrões:
* Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.). * - XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)
* - W3C XMLDSig convencional (Enveloped / NF-e / SPED legado)
*
* Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig). * Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig).
*/ */
public class XmlSigningService { public class XmlSigningService {
private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class); private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class);
public static final String XADES_132_NS = "http://uri.etsi.org/01903/v1.3.2#";
public static final String XADES_SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties";
public static final String XMLDSIG_NS = XMLSignature.XMLNS;
private static final DateTimeFormatter ISO_INSTANT_FORMATTER =
DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
private final CertificateManager certificateManager; private final CertificateManager certificateManager;
public XmlSigningService(CertificateManager certificateManager) { public XmlSigningService(CertificateManager certificateManager) {
@@ -85,7 +103,7 @@ public class XmlSigningService {
try (FileOutputStream fos = new FileOutputStream(targetXml)) { try (FileOutputStream fos = new FileOutputStream(targetXml)) {
fos.write(signedBytes); fos.write(signedBytes);
} }
logger.info("XML assinado com sucesso e salvo em: {}", targetXml.getAbsolutePath()); logger.info("XML assinado com sucesso ({}) e salvo em: {}", options.profile().getDisplayName(), targetXml.getAbsolutePath());
} }
/** /**
@@ -117,7 +135,8 @@ public class XmlSigningService {
} }
SigningKeyMaterial keyMaterial = keyMaterialOpt.get(); SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
logger.info("Iniciando assinatura XMLDSig com certificado '{}' [tipo={}]", cert.commonName(), cert.type()); logger.info("Iniciando assinatura XML [perfil={}] com certificado '{}' [tipo={}]",
options.profile(), cert.commonName(), cert.type());
// 2. Parse Seguro do XML (Prevenção rigorosa de XXE) // 2. Parse Seguro do XML (Prevenção rigorosa de XXE)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
@@ -134,13 +153,13 @@ public class XmlSigningService {
// 3. Configuração do XMLSignatureFactory // 3. Configuração do XMLSignatureFactory
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
// Transformações padrão: Enveloped + C14N // Transformações padrão para os dados de negócio: Enveloped + C14N
List<Transform> transforms = new ArrayList<>(); List<Transform> dataTransforms = new ArrayList<>();
transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)); dataTransforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)); dataTransforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
// Determinação da Referência e Ponto de Inserção // Determinação da Referência de Dados e Ponto de Inserção da Assinatura
Reference ref; Reference dataRef;
Node insertionParent; Node insertionParent;
if (options.targetElementId() != null && !options.targetElementId().isBlank()) { if (options.targetElementId() != null && !options.targetElementId().isBlank()) {
@@ -150,7 +169,7 @@ public class XmlSigningService {
throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento."); throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento.");
} }
targetElement.setIdAttribute("Id", true); targetElement.setIdAttribute("Id", true);
ref = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement(); insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement();
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) { } else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
NodeList list = doc.getElementsByTagName(options.targetElementTag()); NodeList list = doc.getElementsByTagName(options.targetElementTag());
@@ -160,42 +179,87 @@ public class XmlSigningService {
if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) { if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) {
String idVal = elem.getAttribute("Id"); String idVal = elem.getAttribute("Id");
elem.setIdAttribute("Id", true); elem.setIdAttribute("Id", true);
ref = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement(); insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement();
} else { } else {
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = doc.getDocumentElement(); insertionParent = doc.getDocumentElement();
} }
} else { } else {
// Assinatura Enveloped do documento completo // Assinatura Enveloped do documento completo
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = doc.getDocumentElement(); insertionParent = doc.getDocumentElement();
} }
// 4. Montagem do SignedInfo List<Reference> references = new ArrayList<>();
references.add(dataRef);
List<XMLObject> xmlObjects = new ArrayList<>();
String signatureUniqueId = "sig-" + UUID.randomUUID();
Element signedPropertiesElem = null;
// 4. Se for perfil XAdES-BES, constrói as QualifyingProperties e a Reference correspondente
if (options.profile() == XmlSignatureProfile.XADES_BES) {
String signedPropsId = "signed-props-" + UUID.randomUUID();
signedPropertiesElem = buildXadesSignedProperties(doc, signedPropsId, keyMaterial.signingCertificate(), Instant.now());
Element qualifyingProperties = doc.createElementNS(XADES_132_NS, "xades:QualifyingProperties");
qualifyingProperties.setAttribute("Target", "#" + signatureUniqueId);
qualifyingProperties.appendChild(signedPropertiesElem);
XMLStructure xadesStructure = new DOMStructure(qualifyingProperties);
XMLObject xadesObject = fac.newXMLObject(Collections.singletonList(xadesStructure), null, null, null);
xmlObjects.add(xadesObject);
// Adiciona Referência Criptográfica cobrindo as SignedProperties
List<Transform> xadesTransforms = Collections.singletonList(
fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)
);
Reference xadesRef = fac.newReference(
"#" + signedPropsId,
fac.newDigestMethod(DigestMethod.SHA256, null),
xadesTransforms,
XADES_SIGNED_PROPERTIES_TYPE,
null
);
references.add(xadesRef);
}
// 5. Montagem do SignedInfo
SignedInfo si = fac.newSignedInfo( SignedInfo si = fac.newSignedInfo(
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
fac.newSignatureMethod(options.signatureAlgorithm(), null), fac.newSignatureMethod(options.signatureAlgorithm(), null),
Collections.singletonList(ref) references
); );
// 5. Inclusão da Cadeia de Certificados no KeyInfo // 6. Inclusão da Cadeia de Certificados no KeyInfo
KeyInfoFactory kif = fac.getKeyInfoFactory(); KeyInfoFactory kif = fac.getKeyInfoFactory();
List<Object> x509Content = new ArrayList<>(); List<Object> x509Content = new ArrayList<>();
x509Content.add(keyMaterial.signingCertificate()); x509Content.add(keyMaterial.signingCertificate());
X509Data x509Data = kif.newX509Data(x509Content); X509Data x509Data = kif.newX509Data(x509Content);
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data)); KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));
// 6. Execução da Assinatura DOM // 7. Execução da Assinatura DOM
DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent); DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent);
if (keyMaterial.jcaProvider() != null) { if (keyMaterial.jcaProvider() != null) {
dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider()); dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider());
} }
XMLSignature signature = fac.newXMLSignature(si, ki); if (signedPropertiesElem != null) {
dsc.setIdAttributeNS(signedPropertiesElem, null, "Id");
}
XMLSignature signature = fac.newXMLSignature(
si,
ki,
xmlObjects,
signatureUniqueId,
null
);
signature.sign(dsc); signature.sign(dsc);
// 7. Serialização do XML Assinado // 8. Serialização do XML Assinado
TransformerFactory tf = TransformerFactory.newInstance(); TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer(); Transformer trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
@@ -205,12 +269,13 @@ public class XmlSigningService {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
trans.transform(new DOMSource(doc), new StreamResult(baos)); trans.transform(new DOMSource(doc), new StreamResult(baos));
logger.info("Assinatura XMLDSig gerada com sucesso. Tamanho: {} bytes", baos.size()); logger.info("Assinatura XML ({}) gerada com sucesso. Tamanho final: {} bytes",
options.profile(), baos.size());
return baos.toByteArray(); return baos.toByteArray();
} }
/** /**
* Valida a integridade criptográfica de um documento XML assinado com XMLDSig. * Valida a integridade criptográfica de um documento XML assinado com XAdES ou XMLDSig.
*/ */
public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception { public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos."); Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
@@ -218,24 +283,17 @@ public class XmlSigningService {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes)); Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
// Registra nós com atributo Id/id/ID para que o ResourceResolver do XMLDSig localize a referência // Registra nós com atributo Id/id/ID para que o ResourceResolver localize as referências
NodeList allElements = doc.getElementsByTagName("*"); registerIdAttributes(doc.getDocumentElement());
for (int i = 0; i < allElements.getLength(); i++) {
if (allElements.item(i) instanceof Element e) {
if (e.hasAttribute("Id")) {
e.setIdAttribute("Id", true);
} else if (e.hasAttribute("id")) {
e.setIdAttribute("id", true);
} else if (e.hasAttribute("ID")) {
e.setIdAttribute("ID", true);
}
}
}
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) { if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature"); nl = doc.getElementsByTagName("Signature");
} }
@@ -248,8 +306,168 @@ public class XmlSigningService {
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement); DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement);
// Registra explicitamente elementos com atributo Id no valContext
NodeList allNodes = doc.getElementsByTagName("*");
for (int i = 0; i < allNodes.getLength(); i++) {
if (allNodes.item(i) instanceof Element e) {
if (e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
}
}
XMLSignature signature = fac.unmarshalXMLSignature(valContext); XMLSignature signature = fac.unmarshalXMLSignature(valContext);
return signature.validate(valContext); boolean coreValidity = signature.validate(valContext);
if (!coreValidity) {
boolean sv = signature.getSignatureValue().validate(valContext);
logger.warn("Validação XML falhou. SignatureValue válido: {}", sv);
int refIdx = 0;
for (Object o : signature.getSignedInfo().getReferences()) {
if (o instanceof Reference r) {
boolean rValid = r.validate(valContext);
byte[] calcDigest = r.getCalculatedDigestValue();
byte[] storedDigest = r.getDigestValue();
String calcB64 = calcDigest != null ? Base64.getEncoder().encodeToString(calcDigest) : "null";
String storedB64 = storedDigest != null ? Base64.getEncoder().encodeToString(storedDigest) : "null";
logger.warn("Reference[{}] URI='{}' válida: {} | calcDigest={} | storedDigest={}",
refIdx++, r.getURI(), rValid, calcB64, storedB64);
}
}
}
return coreValidity;
}
/**
* Inspeciona e extrai informações detalhadas da assinatura XML (XAdES ou XMLDSig).
*/
public XmlSignatureDetails inspectXmlSignature(byte[] signedXmlBytes) throws Exception {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
registerIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) {
return null;
}
Element sigElem = (Element) nl.item(0);
String sigId = sigElem.getAttribute("Id");
// Verifica presença de XAdES
NodeList qpNodes = doc.getElementsByTagNameNS(XADES_132_NS, "QualifyingProperties");
boolean isXades = qpNodes.getLength() > 0;
String signingTime = null;
if (isXades) {
NodeList timeNodes = doc.getElementsByTagNameNS(XADES_132_NS, "SigningTime");
if (timeNodes.getLength() > 0) {
signingTime = timeNodes.item(0).getTextContent();
}
}
// Validação criptográfica
boolean isValid = false;
try {
isValid = validateXmlSignature(signedXmlBytes);
} catch (Exception ex) {
logger.warn("Validação da assinatura XML falhou durante inspeção: {}", ex.getMessage());
}
String sigMethod = "";
NodeList smNodes = sigElem.getElementsByTagNameNS(XMLDSIG_NS, "SignatureMethod");
if (smNodes.getLength() > 0 && smNodes.item(0) instanceof Element smElem) {
sigMethod = smElem.getAttribute("Algorithm");
}
return new XmlSignatureDetails(
isValid,
isXades ? XmlSignatureProfile.XADES_BES : XmlSignatureProfile.XMLDSIG,
sigMethod,
signingTime,
sigId
);
}
/**
* Constrói a estrutura DOM das propriedades assinadas do XAdES (SignedProperties).
*/
private Element buildXadesSignedProperties(Document doc, String signedPropsId, X509Certificate cert, Instant signInstant) throws Exception {
Element signedProperties = doc.createElementNS(XADES_132_NS, "xades:SignedProperties");
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xades", XADES_132_NS);
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ds", XMLDSIG_NS);
signedProperties.setAttribute("Id", signedPropsId);
signedProperties.setIdAttribute("Id", true);
Element signedSignatureProperties = doc.createElementNS(XADES_132_NS, "xades:SignedSignatureProperties");
// 1. Data e Hora UTC da Assinatura
Element signingTime = doc.createElementNS(XADES_132_NS, "xades:SigningTime");
signingTime.setTextContent(ISO_INSTANT_FORMATTER.format(signInstant));
signedSignatureProperties.appendChild(signingTime);
// 2. Vínculo Criptográfico do Certificado do Signatário (SigningCertificate)
Element signingCertificate = doc.createElementNS(XADES_132_NS, "xades:SigningCertificate");
Element certElem = doc.createElementNS(XADES_132_NS, "xades:Cert");
// Digest SHA-256 do Certificado DER
Element certDigest = doc.createElementNS(XADES_132_NS, "xades:CertDigest");
Element digestMethod = doc.createElementNS(XMLDSIG_NS, "ds:DigestMethod");
digestMethod.setAttribute("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256");
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] certHash = md.digest(cert.getEncoded());
String certHashB64 = Base64.getEncoder().encodeToString(certHash);
Element digestValue = doc.createElementNS(XMLDSIG_NS, "ds:DigestValue");
digestValue.setTextContent(certHashB64);
certDigest.appendChild(digestMethod);
certDigest.appendChild(digestValue);
certElem.appendChild(certDigest);
// Dados do Emissor e Serial do Certificado
Element issuerSerial = doc.createElementNS(XADES_132_NS, "xades:IssuerSerial");
Element x509IssuerName = doc.createElementNS(XMLDSIG_NS, "ds:X509IssuerName");
x509IssuerName.setTextContent(cert.getIssuerX500Principal().getName());
Element x509SerialNumber = doc.createElementNS(XMLDSIG_NS, "ds:X509SerialNumber");
x509SerialNumber.setTextContent(cert.getSerialNumber().toString());
issuerSerial.appendChild(x509IssuerName);
issuerSerial.appendChild(x509SerialNumber);
certElem.appendChild(issuerSerial);
signingCertificate.appendChild(certElem);
signedSignatureProperties.appendChild(signingCertificate);
signedProperties.appendChild(signedSignatureProperties);
return signedProperties;
}
private static void registerIdAttributes(Element root) {
if (root == null) return;
if (root.hasAttribute("Id")) {
root.setIdAttribute("Id", true);
} else if (root.hasAttribute("id")) {
root.setIdAttribute("id", true);
} else if (root.hasAttribute("ID")) {
root.setIdAttribute("ID", true);
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element child) {
registerIdAttributes(child);
}
}
} }
private static Element findElementById(Document doc, String id, String hintTag) { private static Element findElementById(Document doc, String id, String hintTag) {
@@ -264,6 +482,17 @@ public class XmlSigningService {
return doc.getElementById(id); return doc.getElementById(id);
} }
/**
* DTO de detalhes de uma assinatura XML.
*/
public record XmlSignatureDetails(
boolean valid,
XmlSignatureProfile profile,
String algorithm,
String signingTime,
String signatureId
) {}
/** /**
* Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo. * Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo.
*/ */
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 54" width="36" height="54">
<!-- Escudo Criptográfico Vetorial ICP-Brasil -->
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="#0d47a1" opacity="0.14"/>
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="none" stroke="#0d47a1" stroke-width="2.2" stroke-linejoin="round"/>
<!-- Corpo do Cadeado de Segurança -->
<rect x="11" y="24" width="14" height="12" rx="2.5" fill="#0284c7"/>
<!-- Arco Superior do Cadeado -->
<path d="M13.5 24 V19 C13.5 16.5 15.5 14.5 18 14.5 C20.5 14.5 22.5 16.5 22.5 19 V24" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
<!-- Orifício da Chave -->
<circle cx="18" cy="29" r="1.6" fill="#ffffff"/>
<path d="M17.3 29.5 L18.7 29.5 L19.2 33 L16.8 33 Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 858 B

@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import br.com.cybercore.signer.core.validation.SignatureValidationService;
import br.com.cybercore.signer.core.validation.ValidationResult;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
private CertificateManager certificateManager; private CertificateManager certificateManager;
private PdfSigningService pdfSigningService; private PdfSigningService pdfSigningService;
private String testCertId; private String testCertId;
private String testCert2Id;
@BeforeEach @BeforeEach
void setUp() throws Exception { void setUp() throws Exception {
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
char[] pwd = "password123".toCharArray(); char[] pwd = "password123".toCharArray();
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert}); ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
// Gera segundo par de chaves e certificado para co-assinatura
KeyPair kp2 = kpg.generateKeyPair();
X500Name subject2 = new X500Name("CN=Maria Souza:98765432100, O=CyberCore Testes, C=BR");
X509v3CertificateBuilder certBuilder2 = new JcaX509v3CertificateBuilder(
subject2,
BigInteger.valueOf(System.currentTimeMillis() + 100),
notBefore,
notAfter,
subject2,
kp2.getPublic()
);
ContentSigner signer2 = new JcaContentSignerBuilder("SHA256withRSA").build(kp2.getPrivate());
X509Certificate cert2 = new JcaX509CertificateConverter().getCertificate(certBuilder2.build(signer2));
ks.setKeyEntry("test-cert-2", kp2.getPrivate(), pwd, new Certificate[]{cert2});
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste"); Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
provider.setKeyStore(ks, pwd); provider.setKeyStore(ks, pwd);
certificateManager.registerProvider(provider); certificateManager.registerProvider(provider);
List<ManagedCertificate> list = certificateManager.discoverCertificates(); List<ManagedCertificate> list = certificateManager.discoverCertificates();
assertThat(list).isNotEmpty(); assertThat(list).hasSize(2);
this.testCertId = list.get(0).id(); this.testCertId = list.stream().filter(c -> c.alias().equals("test-cert")).findFirst().orElseThrow().id();
this.testCert2Id = list.stream().filter(c -> c.alias().equals("test-cert-2")).findFirst().orElseThrow().id();
this.pdfSigningService = new PdfSigningService(certificateManager); this.pdfSigningService = new PdfSigningService(certificateManager);
} }
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f); byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
assertThat(pngBytes).isNotEmpty(); assertThat(pngBytes).isNotEmpty();
} }
@Test
@DisplayName("Deve manter ambas as assinaturas válidas ao assinar um PDF que já possui assinatura digital (Co-assinatura PAdES)")
void shouldKeepBothSignaturesValidWhenSigningAlreadySignedPdf() throws Exception {
byte[] originalPdf = createSamplePdf();
// 1ª Assinatura com o primeiro certificado
PdfSignatureOptions firstOptions = PdfSignatureOptions.builder(testCertId)
.visibleSeal(true)
.position(PdfSealPosition.BOTTOM_RIGHT)
.reason("Primeira Aprovação")
.location("Brasília, DF")
.build();
byte[] firstSignedPdf = pdfSigningService.signPdf(originalPdf, firstOptions);
assertThat(firstSignedPdf).isNotNull();
// 2ª Assinatura sobre o documento já assinado com o segundo certificado
PdfSignatureOptions secondOptions = PdfSignatureOptions.builder(testCert2Id)
.visibleSeal(true)
.position(PdfSealPosition.TOP_LEFT)
.reason("Segunda Aprovação Conjunta")
.location("São Paulo, SP")
.build();
byte[] doubleSignedPdf = pdfSigningService.signPdf(firstSignedPdf, secondOptions);
assertThat(doubleSignedPdf).isNotNull();
assertThat(doubleSignedPdf.length).isGreaterThan(firstSignedPdf.length);
// Validação estrutural das assinaturas no PDDocument
try (PDDocument doc = Loader.loadPDF(doubleSignedPdf)) {
List<PDSignature> signatures = doc.getSignatureDictionaries();
assertThat(signatures).hasSize(2);
assertThat(signatures.get(0).getName()).contains("Joao da Silva");
assertThat(signatures.get(0).getReason()).isEqualTo("Primeira Aprovação");
assertThat(signatures.get(1).getName()).contains("Maria Souza");
assertThat(signatures.get(1).getReason()).isEqualTo("Segunda Aprovação Conjunta");
}
// Validação criptográfica rigorosa via SignatureValidationService (ambas as assinaturas devem ser íntegras)
SignatureValidationService validationService = new SignatureValidationService();
ValidationResult validationResult = validationService.validateAttached(doubleSignedPdf, "duplo_assinado.pdf");
assertThat(validationResult.isValid()).isTrue();
assertThat(validationResult.signers()).hasSize(2);
// Ambas as assinaturas e digests devem ser 100% válidos
assertThat(validationResult.signers().get(0).signatureValid()).isTrue();
assertThat(validationResult.signers().get(0).digestValid()).isTrue();
assertThat(validationResult.signers().get(0).signerName()).contains("Joao da Silva");
assertThat(validationResult.signers().get(1).signatureValid()).isTrue();
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
}
} }
@@ -11,22 +11,43 @@ import br.com.cybercore.signer.core.provider.CertificateProvider;
import br.com.cybercore.signer.core.provider.ProviderType; import br.com.cybercore.signer.core.provider.ProviderType;
import br.com.cybercore.signer.core.security.ApplicationRegistry; import br.com.cybercore.signer.core.security.ApplicationRegistry;
import br.com.cybercore.signer.core.security.AuthorizedApplication; import br.com.cybercore.signer.core.security.AuthorizedApplication;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.cert.X509Certificate;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Base64; import java.util.Base64;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -49,6 +70,28 @@ class CcspServerTest {
SigningService signingService = mock(SigningService.class); SigningService signingService = mock(SigningService.class);
ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json")); ApplicationRegistry appRegistry = new ApplicationRegistry(tempDir.resolve("apps.json"));
// Gera par de chaves e certificado real em memória para suporte a PDF e XML
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
long now = System.currentTimeMillis();
Date notBefore = new Date(now - 100000);
Date notAfter = new Date(now + 365L * 24 * 3600 * 1000);
X500Name subject = new X500Name("CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR");
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
subject,
BigInteger.valueOf(System.currentTimeMillis()),
notBefore,
notAfter,
subject,
kp.getPublic()
);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate());
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer));
// Registra uma aplicação de teste // Registra uma aplicação de teste
appRegistry.register(new AuthorizedApplication( appRegistry.register(new AuthorizedApplication(
"app-test", "app-test",
@@ -64,8 +107,8 @@ class CcspServerTest {
"AliasTeste", "AliasTeste",
CertificateType.A1, CertificateType.A1,
CertificateStatus.AVAILABLE, CertificateStatus.AVAILABLE,
"CN=Teste ICP-Brasil, C=BR", "CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR",
"CN=AC Raiz, C=BR", "CN=Teste ICP-Brasil:12345678900, O=CyberCore Testes, C=BR",
"12345678", "12345678",
"a1b2c3", "a1b2c3",
"d4e5f6", "d4e5f6",
@@ -89,6 +132,9 @@ class CcspServerTest {
@Override public SigningResult sign(SigningContext context) { @Override public SigningResult sign(SigningContext context) {
return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now()); return new SigningResult(context.requestId(), new byte[]{1, 2, 3}, "mock-cert", List.of("mock-cert"), "SHA256withRSA", Instant.now());
} }
@Override public Optional<SigningKeyMaterial> getKeyMaterial(String certificateId, char[] pinOrPassword) {
return Optional.of(new SigningKeyMaterial(kp.getPrivate(), new X509Certificate[]{cert}, null));
}
}; };
certManager.registerProvider(dummyProvider); certManager.registerProvider(dummyProvider);
@@ -149,7 +195,7 @@ class CcspServerTest {
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200); assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"success\""); assertThat(resp.body()).contains("\"status\":\"success\"");
assertThat(resp.body()).contains("CN=Teste ICP-Brasil, C=BR"); assertThat(resp.body()).contains("Teste ICP-Brasil");
} }
@Test @Test
@@ -320,4 +366,196 @@ class CcspServerTest {
assertThat(response.body()).contains("\"document_name\":\"proposta_comercial.pdf\""); assertThat(response.body()).contains("\"document_name\":\"proposta_comercial.pdf\"");
assertThat(response.body()).contains("\"document_type\":\"PDF\""); assertThat(response.body()).contains("\"document_type\":\"PDF\"");
} }
private byte[] createSamplePdf() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage();
doc.addPage(page);
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
cs.beginText();
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 12);
cs.newLineAtOffset(50, 700);
cs.showText("PDF de Teste CCSP - CyberCore Signer");
cs.endText();
}
doc.save(baos);
}
return baos.toByteArray();
}
@Test
@DisplayName("Endpoint /seals deve listar temas de selos visuais disponíveis")
void testGetSealsThemes() throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/seals?application_id=app-test"))
.header("Origin", "https://meusistema.com.br")
.GET()
.build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"success\"");
assertThat(resp.body()).contains("\"seals\":");
assertThat(resp.body()).contains("icp-brasil");
assertThat(resp.body()).contains("gov-br");
}
@Test
@DisplayName("Endpoint /sign-pdf deve assinar documento PDF em Base64 e retornar signed_pdf válido")
void testSignPdfSuccess() throws Exception {
byte[] pdfBytes = createSamplePdf();
String pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes);
String jsonPayload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-test",
"operation": "sign-pdf",
"certificate_hint": "cert-1",
"pdf": "%s",
"page": 1,
"x": 60.0,
"y": 60.0,
"scale": 1.0,
"seal_theme_id": "icp-brasil",
"reason": "Aprovado e assinado via CCSP",
"location": "Curitiba, PR",
"nonce": "%s"
}
""".formatted(UUID.randomUUID(), pdfBase64, UUID.randomUUID());
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign-pdf"))
.header("Content-Type", "application/json")
.header("Origin", "https://meusistema.com.br")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"signed\"");
assertThat(resp.body()).contains("\"signed_pdf\":");
assertThat(resp.body()).contains("\"sha256\":");
// Verifica que o PDF assinado retornado pode ser carregado pelo PDFBox
int signedPdfIdx = resp.body().indexOf("\"signed_pdf\":\"") + 14;
int endIdx = resp.body().indexOf("\"", signedPdfIdx);
String signedPdfBase64 = resp.body().substring(signedPdfIdx, endIdx);
byte[] signedBytes = Base64.getDecoder().decode(signedPdfBase64);
try (PDDocument signedDoc = Loader.loadPDF(signedBytes)) {
assertThat(signedDoc.getNumberOfPages()).isEqualTo(1);
assertThat(signedDoc.getSignatureDictionaries()).isNotEmpty();
}
}
@Test
@DisplayName("Endpoint /sign-pdf com seal_custom inline deve assinar com sucesso")
void testSignPdfCustomSealSuccess() throws Exception {
byte[] pdfBytes = createSamplePdf();
String pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes);
String jsonPayload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-test",
"operation": "sign-pdf",
"certificate_hint": "cert-1",
"pdf": "%s",
"page": 1,
"x": 40.0,
"y": 40.0,
"scale": 0.9,
"seal_custom": {
"name": "Custom Seal Test",
"title": "SELADO DIGITALMENTE",
"primary_color": "#2c3e50",
"accent_color": "#18bc9c",
"show_timestamp": true,
"show_document_hash": true
},
"nonce": "%s"
}
""".formatted(UUID.randomUUID(), pdfBase64, UUID.randomUUID());
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign-pdf"))
.header("Content-Type", "application/json")
.header("Origin", "https://meusistema.com.br")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"signed\"");
assertThat(resp.body()).contains("\"signed_pdf\":");
}
@Test
@DisplayName("Endpoint /sign-xml deve assinar XML plano com perfil XMLDSig")
void testSignXmlSuccess() throws Exception {
String xmlContent = "<documento id=\"doc-123\"><dados>Contrato de Teste</dados></documento>";
String jsonPayload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-test",
"operation": "sign-xml",
"certificate_hint": "cert-1",
"xml": "%s",
"profile": "XMLDSIG",
"nonce": "%s"
}
""".formatted(UUID.randomUUID(), xmlContent.replace("\"", "\\\""), UUID.randomUUID());
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign-xml"))
.header("Content-Type", "application/json")
.header("Origin", "https://meusistema.com.br")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"signed\"");
assertThat(resp.body()).contains("\"signed_xml\":");
assertThat(resp.body()).contains("SignatureValue");
}
@Test
@DisplayName("Endpoint /sign-xml deve assinar XML em Base64 com perfil XAdES-BES")
void testSignXmlBase64XadesBes() throws Exception {
String xmlContent = "<pedido id=\"ped-999\"><valor>150.00</valor></pedido>";
String xmlBase64 = Base64.getEncoder().encodeToString(xmlContent.getBytes(StandardCharsets.UTF_8));
String jsonPayload = """
{
"version": "1",
"request_id": "%s",
"application_id": "app-test",
"operation": "sign-xml",
"certificate_hint": "cert-1",
"xml_base64": "%s",
"profile": "XADES_BES",
"nonce": "%s"
}
""".formatted(UUID.randomUUID(), xmlBase64, UUID.randomUUID());
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ccsp/v1/sign-xml"))
.header("Content-Type", "application/json")
.header("Origin", "https://meusistema.com.br")
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
assertThat(resp.statusCode()).isEqualTo(200);
assertThat(resp.body()).contains("\"status\":\"signed\"");
assertThat(resp.body()).contains("\"signed_xml\":");
assertThat(resp.body()).contains("QualifyingProperties");
}
} }
@@ -72,8 +72,8 @@ class XmlSigningServiceTest {
} }
@Test @Test
@DisplayName("Deve assinar documento XML completo com XMLDSig Enveloped e validar com sucesso") @DisplayName("Deve assinar documento XML no padrão XAdES-BES por padrão e validar com sucesso")
void shouldSignWholeXmlDocumentAndValidate() throws Exception { void shouldSignWholeXmlDocumentWithXadesBesByDefaultAndValidate() throws Exception {
String xml = """ String xml = """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<pedido id="ped-1001"> <pedido id="ped-1001">
@@ -86,19 +86,38 @@ class XmlSigningServiceTest {
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options); byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8); String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
// Verifica tags essenciais do XMLDSig
assertThat(signedXmlStr).contains("<Signature"); assertThat(signedXmlStr).contains("<Signature");
assertThat(signedXmlStr).contains("<SignedInfo"); assertThat(signedXmlStr).contains("<SignedInfo");
assertThat(signedXmlStr).contains("<SignatureValue"); assertThat(signedXmlStr).contains("<SignatureValue");
assertThat(signedXmlStr).contains("<KeyInfo"); assertThat(signedXmlStr).contains("<KeyInfo");
// Validação criptográfica // Verifica propriedades qualificadas do XAdES-BES (ETSI TS 101 903 / ICP-Brasil)
assertThat(signedXmlStr).contains("QualifyingProperties");
assertThat(signedXmlStr).contains("SignedProperties");
assertThat(signedXmlStr).contains("SignedSignatureProperties");
assertThat(signedXmlStr).contains("SigningTime");
assertThat(signedXmlStr).contains("SigningCertificate");
assertThat(signedXmlStr).contains("CertDigest");
assertThat(signedXmlStr).contains("IssuerSerial");
assertThat(signedXmlStr).contains("Type=\"http://uri.etsi.org/01903#SignedProperties\"");
// Validação criptográfica do documento e das propriedades XAdES
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes); boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
assertThat(isValid).isTrue(); assertThat(isValid).isTrue();
// Inspeção detalhada
XmlSigningService.XmlSignatureDetails details = xmlSigningService.inspectXmlSignature(signedXmlBytes);
assertThat(details).isNotNull();
assertThat(details.valid()).isTrue();
assertThat(details.profile()).isEqualTo(XmlSignatureProfile.XADES_BES);
assertThat(details.signingTime()).isNotBlank();
} }
@Test @Test
@DisplayName("Deve assinar nó XML específico com atributo Id (estilo NF-e) e validar") @DisplayName("Deve assinar nó XML específico com atributo Id no padrão XAdES-BES e validar")
void shouldSignSpecificXmlElementById() throws Exception { void shouldSignSpecificXmlElementWithXadesBes() throws Exception {
String nfeXml = """ String nfeXml = """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"> <NFe xmlns="http://www.portalfiscal.inf.br/nfe">
@@ -117,21 +136,48 @@ class XmlSigningServiceTest {
testCertId, testCertId,
null, null,
"infNFe", "infNFe",
"NFe35261012345678000195550010000000011000000010" "NFe35261012345678000195550010000000011000000010",
XmlSignatureProfile.XADES_BES
); );
byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options); byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options);
String signedStr = new String(signedNfe, StandardCharsets.UTF_8); String signedStr = new String(signedNfe, StandardCharsets.UTF_8);
assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\""); assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\"");
assertThat(signedStr).contains("QualifyingProperties");
assertThat(signedStr).contains("SignedProperties");
boolean isValid = xmlSigningService.validateXmlSignature(signedNfe); boolean isValid = xmlSigningService.validateXmlSignature(signedNfe);
assertThat(isValid).isTrue(); assertThat(isValid).isTrue();
} }
@Test @Test
@DisplayName("Deve detectar adulteração em XML assinado e reprovar na validação") @DisplayName("Deve assinar documento XML no perfil XMLDSig clássico quando explicitamente solicitado")
void shouldDetectTamperingInSignedXml() throws Exception { void shouldSignWithClassicXmlDsigWhenConfigured() throws Exception {
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<documento>
<campo>Valor de teste</campo>
</documento>
""";
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null, XmlSignatureProfile.XMLDSIG);
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
assertThat(signedXmlStr).contains("<Signature");
assertThat(signedXmlStr).doesNotContain("QualifyingProperties");
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
assertThat(isValid).isTrue();
XmlSigningService.XmlSignatureDetails details = xmlSigningService.inspectXmlSignature(signedXmlBytes);
assertThat(details.profile()).isEqualTo(XmlSignatureProfile.XMLDSIG);
}
@Test
@DisplayName("Deve detectar adulteração no conteúdo do XML assinado com XAdES")
void shouldDetectTamperingInSignedXmlContent() throws Exception {
String xml = """ String xml = """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<contrato> <contrato>
@@ -142,11 +188,36 @@ class XmlSigningServiceTest {
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null); XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options); byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
// Adulteração intencional do conteúdo original // Adulteração no payload de negócio
String tampered = new String(signedXml, StandardCharsets.UTF_8) String tampered = new String(signedXml, StandardCharsets.UTF_8)
.replace("R$ 100,00", "R$ 999.999,00"); .replace("R$ 100,00", "R$ 999.999,00");
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8)); boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
assertThat(isValid).isFalse(); assertThat(isValid).isFalse();
} }
@Test
@DisplayName("Deve detectar adulteração nas propriedades qualificadas do XAdES (SigningTime)")
void shouldDetectTamperingInXadesSignedProperties() throws Exception {
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<contrato>
<clausula>Assinado conforme regras XAdES</clausula>
</contrato>
""";
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedStr = new String(signedXml, StandardCharsets.UTF_8);
// Adulteração fraudulenta da data da assinatura no nó XAdES
String tampered = signedStr.replaceFirst(
"<xades:SigningTime>[^<]+</xades:SigningTime>",
"<xades:SigningTime>1999-01-01T00:00:00Z</xades:SigningTime>"
);
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
assertThat(isValid).isFalse();
}
} }
@@ -96,6 +96,7 @@ public class CyberCoreDesktopApplication extends Application {
// Inicializa serviços centrais reais da plataforma // Inicializa serviços centrais reais da plataforma
this.certificateManager = CertificateManager.createDefault(); this.certificateManager = CertificateManager.createDefault();
this.signingService = new SigningService(this.certificateManager);
if (developmentMode) { if (developmentMode) {
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina)."); logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
@@ -424,6 +425,9 @@ public class CyberCoreDesktopApplication extends Application {
byte[] digestBytes; byte[] digestBytes;
if (request.digest() != null && !request.digest().isBlank()) { if (request.digest() != null && !request.digest().isBlank()) {
digestBytes = Base64.getDecoder().decode(request.digest()); digestBytes = Base64.getDecoder().decode(request.digest());
} else if (request.pdf() != null && !request.pdf().isBlank()) {
byte[] rawPdf = Base64.getDecoder().decode(request.pdf());
digestBytes = MessageDigest.getInstance("SHA-256").digest(rawPdf);
} else if (request.challenge() != null && !request.challenge().isBlank()) { } else if (request.challenge() != null && !request.challenge().isBlank()) {
digestBytes = MessageDigest.getInstance("SHA-256") digestBytes = MessageDigest.getInstance("SHA-256")
.digest(request.challenge().getBytes(StandardCharsets.UTF_8)); .digest(request.challenge().getBytes(StandardCharsets.UTF_8));
@@ -5,8 +5,14 @@ import javafx.scene.Group;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
import javafx.scene.shape.Line; import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon; import javafx.scene.shape.Polygon;
@@ -17,7 +23,6 @@ import javafx.scene.shape.Shape;
import javafx.scene.text.Font; import javafx.scene.text.Font;
import javafx.scene.text.FontWeight; import javafx.scene.text.FontWeight;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@@ -26,19 +31,20 @@ import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
* Gerenciador centralizado de recursos visuais (ícones e imagens). * Gerenciador centralizado de recursos visuais (ícones e imagens).
* Permite que o usuário customize ou substitua facilmente qualquer ícone (SVG/PNG) ou imagem (PNG/JPG) * Permite que o usuário customize ou substitua facilmente qualquer ícone ou imagem (SVG/PNG)
* do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/' * do CyberCore Signer simplesmente colocando os arquivos na pasta 'assets/'
* na raiz do projeto ou no diretório ~/.cybercore/assets/. * na raiz do projeto ou no diretório ~/.cybercore/assets/.
*/ */
@@ -121,7 +127,7 @@ public final class AssetManager {
} }
/** /**
* Retorna um Node de ícone customizado (SVG ou PNG) ou invoca o fallback caso não exista imagem customizada. * Retorna um Node de ícone customizado (prioritariamente SVG, com fallback para PNG ou supplier vetorial).
*/ */
public static Node getIconOrDefault(String iconName, double size, Supplier<Node> fallbackSupplier) { public static Node getIconOrDefault(String iconName, double size, Supplier<Node> fallbackSupplier) {
if (iconName != null && !iconName.isBlank()) { if (iconName != null && !iconName.isBlank()) {
@@ -140,7 +146,20 @@ public final class AssetManager {
return userSvg.get(); return userSvg.get();
} }
// 3. Se não houver SVG, tenta imagem bitmap (.png, .jpg) nas pastas externas // 3. Procura SVG no Classpath interno
String resSvgPath = "/assets/icons/" + cleanName + ".svg";
InputStream resStream = AssetManager.class.getResourceAsStream(resSvgPath);
if (resStream != null) {
try (resStream) {
Node node = parseSvgToNode(resStream, size, size);
if (node != null) {
return node;
}
} catch (Exception ignored) {
}
}
// 4. Se não houver SVG, tenta imagem bitmap (.png, .jpg)
Optional<Image> customImg = loadImage("icons", cleanName, size, size); Optional<Image> customImg = loadImage("icons", cleanName, size, size);
if (customImg.isPresent() && !customImg.get().isError()) { if (customImg.isPresent() && !customImg.get().isError()) {
ImageView iv = new ImageView(customImg.get()); ImageView iv = new ImageView(customImg.get());
@@ -155,28 +174,76 @@ public final class AssetManager {
} }
/** /**
* Retorna a imagem do logotipo do sistema ou o fallback vetorial. * Retorna uma imagem institucional ou elemento gráfico (prioritariamente SVG, com fallback para PNG ou supplier).
*/ */
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) { public static Node getImageOrDefault(String imageName, double width, double height, Supplier<Node> fallbackSupplier) {
// Tenta carregar app-logo bitmap (.png) primeiro if (imageName != null && !imageName.isBlank()) {
Optional<Image> customLogo = loadImage("images", "app-logo", width, height); String cleanName = cleanBaseName(imageName);
if (customLogo.isPresent() && !customLogo.get().isError()) {
ImageView iv = new ImageView(customLogo.get()); // 1. Prioridade para SVG em ./assets/images/
Path localSvg = Path.of("assets", "images", cleanName + ".svg");
if (Files.isRegularFile(localSvg)) {
try (InputStream in = new FileInputStream(localSvg.toFile())) {
Node node = parseSvgToNode(in, width, height);
if (node != null) {
logger.debug("Carregada imagem SVG local: {}", localSvg);
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG local {}: {}", localSvg, e.getMessage());
}
}
// 2. Procura SVG em ~/.cybercore/assets/images/
String userHome = System.getProperty("user.home", ".");
Path userSvg = Path.of(userHome, ".cybercore", "assets", "images", cleanName + ".svg");
if (Files.isRegularFile(userSvg)) {
try (InputStream in = new FileInputStream(userSvg.toFile())) {
Node node = parseSvgToNode(in, width, height);
if (node != null) {
logger.debug("Carregada imagem SVG do usuário: {}", userSvg);
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler imagem SVG do usuário {}: {}", userSvg, e.getMessage());
}
}
// 3. Procura SVG no Classpath interno
String resPath = "/assets/images/" + cleanName + ".svg";
InputStream resStream = AssetManager.class.getResourceAsStream(resPath);
if (resStream != null) {
try (resStream) {
Node node = parseSvgToNode(resStream, width, height);
if (node != null) {
return node;
}
} catch (Exception e) {
logger.warn("Falha ao ler recurso interno SVG {}: {}", resPath, e.getMessage());
}
}
// 4. Fallback para imagem bitmap (.png, .jpg)
Optional<Image> customImg = loadImage("images", cleanName, width, height);
if (customImg.isPresent() && !customImg.get().isError()) {
ImageView iv = new ImageView(customImg.get());
iv.setFitWidth(width); iv.setFitWidth(width);
iv.setFitHeight(height); iv.setFitHeight(height);
iv.setPreserveRatio(true); iv.setPreserveRatio(true);
return iv; return iv;
} }
// Tenta carregar SVG de app-logo se existir
Optional<Node> svgLogo = loadSvgIconFromPath(Path.of("assets", "images", "app-logo.svg"), height);
if (svgLogo.isPresent()) {
return svgLogo.get();
} }
return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView(); return fallbackSupplier != null ? fallbackSupplier.get() : new ImageView();
} }
/**
* Retorna a imagem do logotipo do sistema (SVG ou bitmap).
*/
public static Node getLogoOrDefault(double width, double height, Supplier<Node> fallbackSupplier) {
return getImageOrDefault("app-logo", width, height, fallbackSupplier);
}
private static String cleanBaseName(String name) { private static String cleanBaseName(String name) {
String clean = name; String clean = name;
int dotIdx = clean.lastIndexOf('.'); int dotIdx = clean.lastIndexOf('.');
@@ -189,7 +256,7 @@ public final class AssetManager {
private static Optional<Node> loadSvgIconFromPath(Path path, double size) { private static Optional<Node> loadSvgIconFromPath(Path path, double size) {
if (Files.isRegularFile(path)) { if (Files.isRegularFile(path)) {
try (InputStream in = new FileInputStream(path.toFile())) { try (InputStream in = new FileInputStream(path.toFile())) {
Node node = parseSvgToNode(in, size); Node node = parseSvgToNode(in, size, size);
if (node != null) { if (node != null) {
logger.debug("Carregado ícone SVG customizado de: {}", path); logger.debug("Carregado ícone SVG customizado de: {}", path);
return Optional.of(node); return Optional.of(node);
@@ -202,9 +269,16 @@ public final class AssetManager {
} }
/** /**
* Parser analítico leve de SVG padrão para nós gráficos JavaFX com preservação geométrica exata. * Parser analítico de SVG para nós gráficos JavaFX com suporte a dimensões quadradas.
*/ */
public static Node parseSvgToNode(InputStream in, double targetSize) { public static Node parseSvgToNode(InputStream in, double targetSize) {
return parseSvgToNode(in, targetSize, targetSize);
}
/**
* Parser analítico completo de SVG para nós gráficos JavaFX com dimensões proporcionais e suporte a defs/gradientes.
*/
public static Node parseSvgToNode(InputStream in, double targetWidth, double targetHeight) {
try { try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(false); dbf.setNamespaceAware(false);
@@ -249,27 +323,34 @@ public final class AssetManager {
if (vbWidth <= 0) vbWidth = 48; if (vbWidth <= 0) vbWidth = 48;
if (vbHeight <= 0) vbHeight = 48; if (vbHeight <= 0) vbHeight = 48;
Group contentGroup = new Group(); Map<String, Paint> defsMap = new HashMap<>();
renderChildren(svg, contentGroup); parseDefs(svg, defsMap);
double maxDim = Math.max(vbWidth, vbHeight); Group contentGroup = new Group();
double scale = (targetSize * 0.90) / maxDim; renderChildren(svg, contentGroup, defsMap);
double scaleX = targetWidth / vbWidth;
double scaleY = targetHeight / vbHeight;
double scale = Math.min(scaleX, scaleY);
if (scale <= 0) scale = 1.0;
double drawnW = vbWidth * scale; double drawnW = vbWidth * scale;
double drawnH = vbHeight * scale; double drawnH = vbHeight * scale;
double tx = (targetSize - drawnW) / 2.0 - (vbMinX * scale); double tx = (targetWidth - drawnW) / 2.0 - (vbMinX * scale);
double ty = (targetSize - drawnH) / 2.0 - (vbMinY * scale); double ty = (targetHeight - drawnH) / 2.0 - (vbMinY * scale);
contentGroup.setManaged(false);
contentGroup.setLayoutX(0);
contentGroup.setLayoutY(0);
contentGroup.getTransforms().setAll( contentGroup.getTransforms().setAll(
new javafx.scene.transform.Translate(tx, ty), new javafx.scene.transform.Translate(tx, ty),
new javafx.scene.transform.Scale(scale, scale, 0, 0) new javafx.scene.transform.Scale(scale, scale, 0, 0)
); );
StackPane container = new StackPane(contentGroup); Pane container = new Pane(contentGroup);
container.setPrefSize(targetSize, targetSize); container.setPrefSize(targetWidth, targetHeight);
container.setMaxSize(targetSize, targetSize); container.setMaxSize(targetWidth, targetHeight);
container.setMinSize(targetSize, targetSize); container.setMinSize(targetWidth, targetHeight);
container.setAlignment(Pos.CENTER);
return container; return container;
} catch (Exception e) { } catch (Exception e) {
@@ -278,12 +359,100 @@ public final class AssetManager {
} }
} }
private static void renderChildren(Element parent, Group targetGroup) { private static void parseDefs(Element root, Map<String, Paint> defsMap) {
NodeList defsList = root.getElementsByTagName("defs");
for (int i = 0; i < defsList.getLength(); i++) {
org.w3c.dom.Node defNode = defsList.item(i);
if (defNode instanceof Element defsEl) {
NodeList children = defsEl.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
org.w3c.dom.Node c = children.item(j);
if (c instanceof Element gradEl) {
String id = gradEl.getAttribute("id");
if (!id.isBlank()) {
Paint paint = createPaintFromGradient(gradEl);
if (paint != null) {
defsMap.put(id, paint);
}
}
}
}
}
}
}
private static Paint createPaintFromGradient(Element gradEl) {
String tag = gradEl.getTagName().toLowerCase();
List<Stop> stops = new ArrayList<>();
NodeList stopNodes = gradEl.getElementsByTagName("stop");
for (int i = 0; i < stopNodes.getLength(); i++) {
org.w3c.dom.Node s = stopNodes.item(i);
if (s instanceof Element stopEl) {
double offset = parseOffset(stopEl.getAttribute("offset"));
String colorStr = stopEl.getAttribute("stop-color");
double opacity = parseDouble(stopEl.getAttribute("stop-opacity"), 1.0);
Color baseColor = parseColor(colorStr, Color.BLACK);
Color finalColor = new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), baseColor.getOpacity() * opacity);
stops.add(new Stop(offset, finalColor));
}
}
if (stops.isEmpty()) {
stops.add(new Stop(0, Color.web("#0288d1")));
stops.add(new Stop(1, Color.web("#01579b")));
}
if ("lineargradient".equals(tag)) {
double x1 = parseCoordinate(gradEl.getAttribute("x1"), 0);
double y1 = parseCoordinate(gradEl.getAttribute("y1"), 0);
double x2 = parseCoordinate(gradEl.getAttribute("x2"), 1);
double y2 = parseCoordinate(gradEl.getAttribute("y2"), 1);
return new LinearGradient(x1, y1, x2, y2, true, CycleMethod.NO_CYCLE, stops);
} else if ("radialgradient".equals(tag)) {
double cx = parseCoordinate(gradEl.getAttribute("cx"), 0.5);
double cy = parseCoordinate(gradEl.getAttribute("cy"), 0.5);
double r = parseCoordinate(gradEl.getAttribute("r"), 0.5);
return new RadialGradient(0, 0, cx, cy, r, true, CycleMethod.NO_CYCLE, stops);
}
return null;
}
private static double parseOffset(String str) {
if (str == null || str.isBlank()) return 0;
String s = str.trim();
try {
if (s.endsWith("%")) {
return Double.parseDouble(s.substring(0, s.length() - 1)) / 100.0;
}
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return 0;
}
}
private static double parseCoordinate(String str, double def) {
if (str == null || str.isBlank()) return def;
String s = str.trim();
try {
if (s.endsWith("%")) {
return Double.parseDouble(s.substring(0, s.length() - 1)) / 100.0;
}
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return def;
}
}
private static void renderChildren(Element parent, Group targetGroup, Map<String, Paint> defsMap) {
NodeList children = parent.getChildNodes(); NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) { for (int i = 0; i < children.getLength(); i++) {
org.w3c.dom.Node n = children.item(i); org.w3c.dom.Node n = children.item(i);
if (n instanceof Element el) { if (n instanceof Element el) {
Node jfxNode = createElementNode(el); if ("defs".equalsIgnoreCase(el.getTagName())) {
continue;
}
Node jfxNode = createElementNode(el, defsMap);
if (jfxNode != null) { if (jfxNode != null) {
targetGroup.getChildren().add(jfxNode); targetGroup.getChildren().add(jfxNode);
} }
@@ -291,7 +460,7 @@ public final class AssetManager {
} }
} }
private static Node createElementNode(Element el) { private static Node createElementNode(Element el, Map<String, Paint> defsMap) {
String tagName = el.getTagName().toLowerCase(); String tagName = el.getTagName().toLowerCase();
Shape shape = null; Shape shape = null;
@@ -350,12 +519,12 @@ public final class AssetManager {
double x = parseDouble(el.getAttribute("x"), 0); double x = parseDouble(el.getAttribute("x"), 0);
double y = parseDouble(el.getAttribute("y"), 0); double y = parseDouble(el.getAttribute("y"), 0);
String content = el.getTextContent(); String content = el.getTextContent();
Text t = new Text(x, y, content); Text t = new Text(x, y, content != null ? content.trim() : "");
double fs = parseDouble(el.getAttribute("font-size"), 10); double fs = parseDouble(el.getAttribute("font-size"), 12);
String fw = el.getAttribute("font-weight"); String fw = el.getAttribute("font-weight");
t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs)); t.setFont(Font.font("Segoe UI", "bold".equalsIgnoreCase(fw) ? FontWeight.BOLD : FontWeight.NORMAL, fs));
if (el.hasAttribute("fill")) { if (el.hasAttribute("fill")) {
t.setFill(parseColor(el.getAttribute("fill"), Color.BLACK)); t.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.BLACK));
} }
if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) { if ("middle".equalsIgnoreCase(el.getAttribute("text-anchor"))) {
t.setTextOrigin(javafx.geometry.VPos.CENTER); t.setTextOrigin(javafx.geometry.VPos.CENTER);
@@ -364,38 +533,40 @@ public final class AssetManager {
} }
case "g" -> { case "g" -> {
Group g = new Group(); Group g = new Group();
renderChildren(el, g); renderChildren(el, g, defsMap);
if (el.hasAttribute("stroke")) { if (el.hasAttribute("stroke")) {
Color strokeCol = parseColor(el.getAttribute("stroke"), null); Paint strokePaint = parsePaint(el.getAttribute("stroke"), defsMap, null);
double sw = parseDouble(el.getAttribute("stroke-width"), 1.0); double sw = parseDouble(el.getAttribute("stroke-width"), 1.0);
for (Node child : g.getChildren()) { for (Node child : g.getChildren()) {
if (child instanceof Shape s) { if (child instanceof Shape s) {
if (strokeCol != null) s.setStroke(strokeCol); if (strokePaint != null) s.setStroke(strokePaint);
s.setStrokeWidth(sw); s.setStrokeWidth(sw);
} }
} }
} }
applyElementTransform(el, g);
return g; return g;
} }
} }
if (shape != null) { if (shape != null) {
applyShapeStyles(el, shape); applyShapeStyles(el, shape, defsMap);
applyElementTransform(el, shape);
return shape; return shape;
} }
return null; return null;
} }
private static void applyShapeStyles(Element el, Shape shape) { private static void applyShapeStyles(Element el, Shape shape, Map<String, Paint> defsMap) {
if (el.hasAttribute("fill")) { if (el.hasAttribute("fill")) {
shape.setFill(parseColor(el.getAttribute("fill"), Color.TRANSPARENT)); shape.setFill(parsePaint(el.getAttribute("fill"), defsMap, Color.TRANSPARENT));
} else if (shape instanceof Line) { } else if (shape instanceof Line) {
shape.setFill(Color.TRANSPARENT); shape.setFill(Color.TRANSPARENT);
} }
if (el.hasAttribute("stroke")) { if (el.hasAttribute("stroke")) {
shape.setStroke(parseColor(el.getAttribute("stroke"), Color.TRANSPARENT)); shape.setStroke(parsePaint(el.getAttribute("stroke"), defsMap, Color.TRANSPARENT));
} }
if (el.hasAttribute("stroke-width")) { if (el.hasAttribute("stroke-width")) {
@@ -409,6 +580,22 @@ public final class AssetManager {
} }
} }
private static Paint parsePaint(String str, Map<String, Paint> defsMap, Paint fallback) {
if (str == null || str.isBlank() || "none".equalsIgnoreCase(str)) {
return Color.TRANSPARENT;
}
String trimmed = str.trim();
if (trimmed.startsWith("url(#") && trimmed.endsWith(")")) {
String id = trimmed.substring(5, trimmed.length() - 1).trim();
Paint defPaint = defsMap.get(id);
if (defPaint != null) {
return defPaint;
}
return Color.web("#0288d1");
}
return parseColor(trimmed, fallback instanceof Color ? (Color) fallback : Color.BLACK);
}
private static void parsePoints(String ptsStr, java.util.List<Double> pointsList) { private static void parsePoints(String ptsStr, java.util.List<Double> pointsList) {
String[] tokens = ptsStr.trim().split("[,\\s]+"); String[] tokens = ptsStr.trim().split("[,\\s]+");
for (String t : tokens) { for (String t : tokens) {
@@ -446,4 +633,52 @@ public final class AssetManager {
return defaultValue; return defaultValue;
} }
} }
private static void applyElementTransform(Element el, Node node) {
if (!el.hasAttribute("transform")) return;
String tr = el.getAttribute("transform").trim();
if (tr.isBlank()) return;
try {
if (tr.contains("rotate(")) {
int start = tr.indexOf("rotate(") + 7;
int close = tr.indexOf(')', start);
if (close > 0) {
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
if (args.length >= 3) {
double deg = parseDouble(args[0], 0);
double px = parseDouble(args[1], 0);
double py = parseDouble(args[2], 0);
node.getTransforms().add(new javafx.scene.transform.Rotate(deg, px, py));
} else if (args.length >= 1) {
double deg = parseDouble(args[0], 0);
node.getTransforms().add(new javafx.scene.transform.Rotate(deg));
}
}
}
if (tr.contains("translate(")) {
int start = tr.indexOf("translate(") + 10;
int close = tr.indexOf(')', start);
if (close > 0) {
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
double tx = parseDouble(args[0], 0);
double ty = args.length > 1 ? parseDouble(args[1], 0) : 0;
node.getTransforms().add(new javafx.scene.transform.Translate(tx, ty));
}
}
if (tr.contains("scale(")) {
int start = tr.indexOf("scale(") + 6;
int close = tr.indexOf(')', start);
if (close > 0) {
String[] args = tr.substring(start, close).trim().split("[,\\s]+");
double sx = parseDouble(args[0], 1.0);
double sy = args.length > 1 ? parseDouble(args[1], sx) : sx;
node.getTransforms().add(new javafx.scene.transform.Scale(sx, sy));
}
}
} catch (Exception ignored) {
}
}
} }
@@ -38,17 +38,16 @@ public final class CertificateCardNode extends VBox {
HBox topRow = new HBox(10); HBox topRow = new HBox(10);
topRow.setAlignment(Pos.CENTER_LEFT); topRow.setAlignment(Pos.CENTER_LEFT);
Label titleLabel = new Label(certificate.commonName() != null ? certificate.commonName() : certificate.alias()); String rawName = certificate.commonName() != null ? certificate.commonName() : certificate.alias();
String cleanTitle = rawName != null && rawName.contains(":") ? rawName.split(":")[0].trim() : (rawName != null ? rawName.trim() : "Certificado Digital");
Label titleLabel = new Label(cleanTitle);
titleLabel.getStyleClass().add("cert-title"); titleLabel.getStyleClass().add("cert-title");
HBox.setHgrow(titleLabel, Priority.ALWAYS); HBox.setHgrow(titleLabel, Priority.ALWAYS);
Label badgeLabel = new Label(); Label badgeLabel = new Label("[" + certificate.type().getCode() + "]");
if (certificate.type() == CertificateType.A1) { if (certificate.type() == CertificateType.A1) {
badgeLabel.setText("A1 • Certificado local");
badgeLabel.getStyleClass().add("cert-badge-a1"); badgeLabel.getStyleClass().add("cert-badge-a1");
} else { } else {
String tokenDesc = certificate.hardwareDeviceName() != null ? certificate.hardwareDeviceName() : "Token USB";
badgeLabel.setText("A3 • " + tokenDesc);
badgeLabel.getStyleClass().add("cert-badge-a3"); badgeLabel.getStyleClass().add("cert-badge-a3");
} }
@@ -709,6 +709,10 @@ public final class SvgIcons {
} }
public static Node closeIcon(double size) { public static Node closeIcon(double size) {
return AssetManager.getIconOrDefault("close", size, () -> buildCloseIcon(size));
}
public static Node buildCloseIcon(double size) {
StackPane box = new StackPane(); StackPane box = new StackPane();
box.setPrefSize(size, size); box.setPrefSize(size, size);
box.setMaxSize(size, size); box.setMaxSize(size, size);
@@ -738,6 +742,10 @@ public final class SvgIcons {
} }
public static Node arrowRightIcon(double size, String colorHex) { public static Node arrowRightIcon(double size, String colorHex) {
return AssetManager.getIconOrDefault("arrow-right", size, () -> buildArrowRightIcon(size, colorHex));
}
public static Node buildArrowRightIcon(double size, String colorHex) {
StackPane box = new StackPane(); StackPane box = new StackPane();
box.setPrefSize(size, size); box.setPrefSize(size, size);
box.setMaxSize(size, size); box.setMaxSize(size, size);
@@ -764,6 +772,10 @@ public final class SvgIcons {
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela. * Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
*/ */
public static Node zoomInIcon(double size) { public static Node zoomInIcon(double size) {
return AssetManager.getIconOrDefault("zoom-in", size, () -> buildZoomInIcon(size));
}
public static Node buildZoomInIcon(double size) {
StackPane box = new StackPane(); StackPane box = new StackPane();
box.setPrefSize(size, size); box.setPrefSize(size, size);
box.setMaxSize(size, size); box.setMaxSize(size, size);
@@ -786,6 +798,10 @@ public final class SvgIcons {
} }
public static Node zoomOutIcon(double size) { public static Node zoomOutIcon(double size) {
return AssetManager.getIconOrDefault("zoom-out", size, () -> buildZoomOutIcon(size));
}
public static Node buildZoomOutIcon(double size) {
StackPane box = new StackPane(); StackPane box = new StackPane();
box.setPrefSize(size, size); box.setPrefSize(size, size);
box.setMaxSize(size, size); box.setMaxSize(size, size);
@@ -808,6 +824,10 @@ public final class SvgIcons {
} }
public static Node zoomFitIcon(double size) { public static Node zoomFitIcon(double size) {
return AssetManager.getIconOrDefault("zoom-fit", size, () -> buildZoomFitIcon(size));
}
public static Node buildZoomFitIcon(double size) {
StackPane box = new StackPane(); StackPane box = new StackPane();
box.setPrefSize(size, size); box.setPrefSize(size, size);
box.setMaxSize(size, size); box.setMaxSize(size, size);
@@ -828,4 +848,33 @@ public final class SvgIcons {
box.getChildren().add(path); box.getChildren().add(path);
return box; return box;
} }
/**
* Ícone de Atualização / Sincronização.
*/
public static Node refreshIcon(double size) {
return AssetManager.getIconOrDefault("refresh", size, () -> buildRefreshIcon(size));
}
public static Node buildRefreshIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 20 4 L 20 9 L 15 9 M 4 20 L 4 15 L 9 15 M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 24.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
} }
@@ -1,12 +1,15 @@
package br.com.cybercore.signer.ui.view; package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.ui.CyberCoreUiConstants; import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.assets.AssetManager;
import br.com.cybercore.signer.ui.components.SvgIcons;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
/** /**
* Painel informativo Sobre o CyberCore Signer. * Painel informativo Sobre o CyberCore Signer com exibição de identidade visual SVG e dados do agente.
*/ */
@SuppressWarnings("this-escape") @SuppressWarnings("this-escape")
public final class AboutView extends VBox { public final class AboutView extends VBox {
@@ -16,13 +19,12 @@ public final class AboutView extends VBox {
} }
private void initView() { private void initView() {
setSpacing(16); setSpacing(14);
setAlignment(Pos.CENTER); setAlignment(Pos.CENTER);
getStyleClass().add("content-pane"); getStyleClass().add("content-pane");
Label brandLabel = new Label(CyberCoreUiConstants.APP_NAME); // Logotipo oficial vetorial SVG (app-logo.svg) com fallback para cristal 3D
brandLabel.getStyleClass().add("brand-title"); Node logoNode = AssetManager.getLogoOrDefault(320, 80, () -> SvgIcons.createAppCrystalLogo(54));
brandLabel.setStyle("-fx-font-size: 24px;");
Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1"); Label versionLabel = new Label("Versão " + CyberCoreUiConstants.APP_VERSION + " • CCSP/1");
versionLabel.getStyleClass().add("status-badge-active"); versionLabel.getStyleClass().add("status-badge-active");
@@ -44,9 +46,9 @@ public final class AboutView extends VBox {
detailsBox.getChildren().add(lbl); detailsBox.getChildren().add(lbl);
} }
Label copyLabel = new Label("© 2026 CyberCore. Todos os direitos reservados."); Label copyLabel = new Label("© 2026 CyberCore Systems. Todos os direitos reservados.");
copyLabel.getStyleClass().add("dialog-subtitle"); copyLabel.getStyleClass().add("dialog-subtitle");
getChildren().addAll(brandLabel, versionLabel, descLabel, detailsBox, copyLabel); getChildren().addAll(logoNode, versionLabel, descLabel, detailsBox, copyLabel);
} }
} }
@@ -39,7 +39,10 @@ public class ApplicationApprovalDialog {
root.setPadding(new Insets(24)); root.setPadding(new Insets(24));
root.getStyleClass().add("dialog-container"); root.getStyleClass().add("dialog-container");
// Cabeçalho // Cabeçalho com Ícone SVG de Escudo
HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT);
VBox headerBox = new VBox(4); VBox headerBox = new VBox(4);
Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO"); Label titleLabel = new Label("NOVA APLICAÇÃO WEB SOLICITA ACESSO");
titleLabel.getStyleClass().add("dialog-header"); titleLabel.getStyleClass().add("dialog-header");
@@ -48,6 +51,8 @@ public class ApplicationApprovalDialog {
subtitleLabel.getStyleClass().add("dialog-subtitle"); subtitleLabel.getStyleClass().add("dialog-subtitle");
headerBox.getChildren().addAll(titleLabel, subtitleLabel); headerBox.getChildren().addAll(titleLabel, subtitleLabel);
header.getChildren().addAll(br.com.cybercore.signer.ui.components.SvgIcons.cyberShieldLogo(34), headerBox);
// Painel de Detalhes da Aplicação // Painel de Detalhes da Aplicação
VBox detailsBox = new VBox(10); VBox detailsBox = new VBox(10);
detailsBox.getStyleClass().add("dialog-box"); detailsBox.getStyleClass().add("dialog-box");
@@ -106,7 +111,7 @@ public class ApplicationApprovalDialog {
actionsRow.getChildren().addAll(btnReject, btnAuthorize); actionsRow.getChildren().addAll(btnReject, btnAuthorize);
root.getChildren().addAll(headerBox, detailsBox, secWarning, actionsRow); root.getChildren().addAll(header, detailsBox, secWarning, actionsRow);
Scene scene = new Scene(root); Scene scene = new Scene(root);
ThemeManager.applyTheme(scene); ThemeManager.applyTheme(scene);
@@ -386,7 +386,7 @@ public final class BatchSigningDialog {
Path dest = targetDir.resolve(outPdfName); Path dest = targetDir.resolve(outPdfName);
pdfService.signPdfFile(item.getFile(), dest.toFile(), options); pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
} else if (lowerName.endsWith(".xml")) { } else if (lowerName.endsWith(".xml")) {
// Assinatura XMLDSig de documento XML // Assinatura XAdES-BES de documento XML (ETSI TS 101 903 / ICP-Brasil)
br.com.cybercore.signer.core.xml.XmlSigningService xmlService = br.com.cybercore.signer.core.xml.XmlSigningService xmlService =
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager); new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
br.com.cybercore.signer.core.xml.XmlSignatureOptions options = br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
@@ -77,7 +77,7 @@ public final class CertificatesManagerDialog {
btnAddA1.getStyleClass().add("btn-primary"); btnAddA1.getStyleClass().add("btn-primary");
btnAddA1.setOnAction(e -> handleImportA1()); btnAddA1.setOnAction(e -> handleImportA1());
Button btnRefresh = new Button("Atualizar Lista"); Button btnRefresh = new Button(" Atualizar Lista", SvgIcons.refreshIcon(14));
btnRefresh.getStyleClass().add("btn-secondary"); btnRefresh.getStyleClass().add("btn-secondary");
btnRefresh.setOnAction(e -> refreshList()); btnRefresh.setOnAction(e -> refreshList());
@@ -93,7 +93,7 @@ public final class CertificatesManagerDialog {
HBox footer = new HBox(); HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT); footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button("Fechar"); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary"); btnClose.getStyleClass().add("btn-secondary");
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose); footer.getChildren().add(btnClose);
@@ -57,24 +57,43 @@ public final class LogsViewerDialog {
txtLogs.getStyleClass().add("log-console-area"); txtLogs.getStyleClass().add("log-console-area");
VBox.setVgrow(txtLogs, Priority.ALWAYS); VBox.setVgrow(txtLogs, Priority.ALWAYS);
loadInitialLogs(); loadLogs();
HBox footer = new HBox(10); HBox footer = new HBox(10);
footer.setAlignment(Pos.CENTER_RIGHT); footer.setAlignment(Pos.CENTER_RIGHT);
Button btnCopy = new Button("Copiar");
btnCopy.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCopy.setPrefWidth(90);
btnCopy.setOnAction(e -> {
javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent();
content.putString(txtLogs.getText());
javafx.scene.input.Clipboard.getSystemClipboard().setContent(content);
btnCopy.setText("✓ Copiado");
javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.seconds(2));
pause.setOnFinished(ev -> btnCopy.setText("Copiar"));
pause.play();
});
Button btnClear = new Button("Limpar");
btnClear.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClear.setPrefWidth(90);
btnClear.setOnAction(e -> {
br.com.cybercore.signer.core.log.AppLogBuffer.clear();
loadLogs();
});
Button btnRefresh = new Button("Atualizar"); Button btnRefresh = new Button("Atualizar");
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog"); btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnRefresh.setPrefWidth(110); btnRefresh.setPrefWidth(100);
btnRefresh.setMinWidth(110); btnRefresh.setOnAction(e -> loadLogs());
btnRefresh.setOnAction(e -> loadInitialLogs());
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(110); btnClose.setPrefWidth(100);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
footer.getChildren().addAll(btnRefresh, btnClose); footer.getChildren().addAll(btnCopy, btnClear, btnRefresh, btnClose);
root.getChildren().addAll(header, txtLogs, footer); root.getChildren().addAll(header, txtLogs, footer);
@@ -84,15 +103,15 @@ public final class LogsViewerDialog {
stage.setScene(scene); stage.setScene(scene);
} }
private void loadInitialLogs() { private void loadLogs() {
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); java.util.List<String> logs = br.com.cybercore.signer.core.log.AppLogBuffer.getLogs();
String sb = "[" + now + "] [INFO] CyberCore Signer inicializado com sucesso.\n" + if (logs.isEmpty()) {
"[" + now + "] [INFO] Servidor local CCSP/1 escutando em http://127.0.0.1:21325\n" + txtLogs.setText("[Aguardando eventos de log...]");
"[" + now + "] [INFO] Provedores criptográficos registrados: Windows Certificate Store, PKCS#12, PKCS#11.\n" + } else {
"[" + now + "] [INFO] SmartCardWatcher ativo (monitoramento de tokens USB e Smart Cards ICP-Brasil).\n" + txtLogs.setText(String.join("\n", logs) + "\n");
"[" + now + "] [INFO] Política anti-replay: nonces protegidos em memória.\n" + }
"[" + now + "] [INFO] Registro de aplicações web ativo e pronto para autorização segura.\n"; txtLogs.positionCaret(txtLogs.getText().length());
txtLogs.setText(sb); txtLogs.setScrollTop(Double.MAX_VALUE);
} }
private VBox createHeaderLabel(String title, String subtitle) { private VBox createHeaderLabel(String title, String subtitle) {
@@ -13,6 +13,7 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -204,14 +205,20 @@ public final class MainAppView {
private void updateThemeToggleButton() { private void updateThemeToggleButton() {
boolean dark = ThemeManager.isDark(); boolean dark = ThemeManager.isDark();
HBox content = new HBox(6); HBox content = new HBox(8);
content.setAlignment(Pos.CENTER); content.setAlignment(Pos.CENTER);
Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15); Node icon = dark ? SvgIcons.sunIcon(15) : SvgIcons.moonIcon(15);
StackPane iconBox = new StackPane(icon);
iconBox.setPrefSize(18, 18);
iconBox.setMinSize(18, 18);
iconBox.setMaxSize(18, 18);
iconBox.setAlignment(Pos.CENTER);
Label label = new Label(dark ? "Modo Claro" : "Modo Escuro"); Label label = new Label(dark ? "Modo Claro" : "Modo Escuro");
label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;"); label.setStyle("-fx-font-size: 11.5px; -fx-font-weight: bold;");
content.getChildren().addAll(icon, label); content.getChildren().addAll(iconBox, label);
themeToggleBtn.setGraphic(content); themeToggleBtn.setGraphic(content);
} }
@@ -223,16 +230,22 @@ public final class MainAppView {
btn.setPrefHeight(68); btn.setPrefHeight(68);
btn.setMinHeight(68); btn.setMinHeight(68);
HBox content = new HBox(10); HBox content = new HBox(12);
content.setAlignment(Pos.CENTER_LEFT); content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text); Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text"); lbl.getStyleClass().addAll("cyber-action-text", "serpro-action-text");
lbl.setWrapText(true); lbl.setWrapText(true);
lbl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(lbl, Priority.ALWAYS); HBox.setHgrow(lbl, Priority.ALWAYS);
if (icon != null) { if (icon != null) {
content.getChildren().addAll(icon, lbl); StackPane iconSlot = new StackPane(icon);
iconSlot.setPrefSize(40, 40);
iconSlot.setMinSize(40, 40);
iconSlot.setMaxSize(40, 40);
iconSlot.setAlignment(Pos.CENTER);
content.getChildren().addAll(iconSlot, lbl);
} else { } else {
content.getChildren().add(lbl); content.getChildren().add(lbl);
} }
@@ -251,16 +264,22 @@ public final class MainAppView {
btn.setMinHeight(42); btn.setMinHeight(42);
btn.setMaxWidth(Double.MAX_VALUE); btn.setMaxWidth(Double.MAX_VALUE);
HBox content = new HBox(10); HBox content = new HBox(12);
content.setAlignment(Pos.CENTER_LEFT); content.setAlignment(Pos.CENTER_LEFT);
Label lbl = new Label(text); Label lbl = new Label(text);
lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text"); lbl.getStyleClass().addAll("cyber-side-text", "serpro-side-text");
lbl.setWrapText(true); lbl.setWrapText(false);
lbl.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(lbl, Priority.ALWAYS); HBox.setHgrow(lbl, Priority.ALWAYS);
if (icon != null) { if (icon != null) {
content.getChildren().addAll(icon, lbl); StackPane iconSlot = new StackPane(icon);
iconSlot.setPrefSize(26, 26);
iconSlot.setMinSize(26, 26);
iconSlot.setMaxSize(26, 26);
iconSlot.setAlignment(Pos.CENTER);
content.getChildren().addAll(iconSlot, lbl);
} else { } else {
content.getChildren().add(lbl); content.getChildren().add(lbl);
} }
@@ -1,5 +1,6 @@
package br.com.cybercore.signer.ui.view; package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.CertificateType; import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions; import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
@@ -54,6 +55,22 @@ public final class PdfCertificateSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class); private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
public record SealConfig( public record SealConfig(
boolean visibleSeal,
PdfSealTheme sealTheme,
CpfDisplayMode cpfDisplayMode,
boolean includeDateTime,
String customExtraInfo,
PdfSignDialog.PositionOption positionOption,
PdfSignDialog.PageOption pageOption,
boolean isCustomPosition,
Float customX,
Float customY,
Integer customPageNumber,
String reason,
String location,
Double customSealScale
) {
public SealConfig(
boolean visibleSeal, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealTheme sealTheme,
CpfDisplayMode cpfDisplayMode, CpfDisplayMode cpfDisplayMode,
@@ -67,7 +84,11 @@ public final class PdfCertificateSignDialog {
Integer customPageNumber, Integer customPageNumber,
String reason, String reason,
String location String location
) {} ) {
this(visibleSeal, sealTheme, cpfDisplayMode, includeDateTime, customExtraInfo, positionOption,
pageOption, isCustomPosition, customX, customY, customPageNumber, reason, location, 1.0);
}
}
private final Stage stage; private final Stage stage;
private final CertificateManager certManager; private final CertificateManager certManager;
@@ -196,8 +217,9 @@ public final class PdfCertificateSignDialog {
if (empty || item == null) { if (empty || item == null) {
setText(null); setText(null);
} else { } else {
String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias());
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " (" + item.type() + ")"); setText(holderName + doc + " (" + item.type() + ")");
} }
} }
}); });
@@ -208,8 +230,9 @@ public final class PdfCertificateSignDialog {
if (empty || item == null) { if (empty || item == null) {
setText(null); setText(null);
} else { } else {
String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias());
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(item.commonName() + doc + " (" + item.type() + ")"); setText(holderName + doc + " (" + item.type() + ")");
} }
} }
}); });
@@ -429,7 +452,8 @@ public final class PdfCertificateSignDialog {
.includeDateTime(sealConfig.includeDateTime()) .includeDateTime(sealConfig.includeDateTime())
.customExtraInfo(sealConfig.customExtraInfo()) .customExtraInfo(sealConfig.customExtraInfo())
.reason(sealConfig.reason()) .reason(sealConfig.reason())
.location(sealConfig.location()); .location(sealConfig.location())
.customSealScale(sealConfig.customSealScale());
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) { if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber()); b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
@@ -46,6 +46,8 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -127,6 +129,24 @@ public final class PdfSignDialog {
private ComboBox<PageOption> pageCombo; private ComboBox<PageOption> pageCombo;
private ImageView sealPreviewImageView; private ImageView sealPreviewImageView;
// Controle de Escala / Redimensionamento do Selo (1.0 = 100%)
private double sealScale = 1.0;
private static final double MIN_SEAL_SCALE = 0.40;
private static final double MAX_SEAL_SCALE = 1.80;
private boolean isResizeModeActive = false;
// Componentes do grupo do selo e alça interativa
private Pane sealGroupPane;
private Rectangle sealSelectionBorder;
private Circle resizeHandle;
private Button btnToggleResize;
private Button btnResetSealScale;
private Label lblSealScaleIndicator;
private double resizeStartSceneX;
private double resizeStartBaseWidth;
private boolean isResizingSealInProgress = false;
private long lastResizeActionTimestamp = 0;
// Visualizador Interativo da Folha // Visualizador Interativo da Folha
private ScrollPane sheetScroll; private ScrollPane sheetScroll;
private StackPane pageSheetContainer; private StackPane pageSheetContainer;
@@ -463,13 +483,34 @@ public final class PdfSignDialog {
zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit); zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit);
// Grupo de Tamanho do Selo
HBox resizeToolBox = new HBox(6);
resizeToolBox.setAlignment(Pos.CENTER_LEFT);
resizeToolBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;");
btnToggleResize = new Button(" Redimensionar", SvgIcons.zoomFitIcon(13));
btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact");
btnToggleResize.setTooltip(new Tooltip("Ativar/desativar alça interativa para alterar o tamanho do selo"));
btnToggleResize.setOnAction(e -> toggleResizeMode());
lblSealScaleIndicator = new Label("100%");
lblSealScaleIndicator.setStyle("-fx-font-size: 11px; -fx-font-weight: bold; -fx-min-width: 36px; -fx-alignment: center;");
btnResetSealScale = new Button(" 100%", SvgIcons.refreshIcon(12));
btnResetSealScale.getStyleClass().addAll("btn-secondary", "btn-compact");
btnResetSealScale.setTooltip(new Tooltip("Restaurar tamanho padrão original (100%)"));
btnResetSealScale.setVisible(false);
btnResetSealScale.setOnAction(e -> applySealScale(1.0));
resizeToolBox.getChildren().addAll(btnToggleResize, lblSealScaleIndicator, btnResetSealScale);
Region tbSpacer = new Region(); Region tbSpacer = new Region();
HBox.setHgrow(tbSpacer, Priority.ALWAYS); HBox.setHgrow(tbSpacer, Priority.ALWAYS);
lblPositionFeedback = new Label("👉 Clique na folha para posicionar"); lblPositionFeedback = new Label("Clique na folha para posicionar");
lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;"); lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;");
toolbar.getChildren().addAll(pageNavBox, zoomBox, tbSpacer, lblPositionFeedback); toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback);
// ScrollPane da Folha de Papel // ScrollPane da Folha de Papel
sheetScroll = new ScrollPane(); sheetScroll = new ScrollPane();
@@ -508,23 +549,61 @@ public final class PdfSignDialog {
interactiveOverlayPane = new Pane(); interactiveOverlayPane = new Pane();
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR); interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
interactiveOverlayPane.setOnMouseClicked(event -> { interactiveOverlayPane.setOnMouseClicked(event -> {
if (sealDragOccurred) { if (sealDragOccurred || isResizingSealInProgress || (System.currentTimeMillis() - lastResizeActionTimestamp < 400)) {
// Previne que a soltura do mouse no término do arraste cause clique residual // Previne que a soltura do mouse no término do redimensionamento ou arraste cause clique residual
sealDragOccurred = false; sealDragOccurred = false;
event.consume(); event.consume();
return; return;
} }
if (isResizeModeActive) {
// No modo redimensionamento ativo, clicar fora não deve saltar o selo acidentalmente
event.consume();
return;
}
handleSheetClick(event); handleSheetClick(event);
}); });
// Grupo visual do selo composto pela imagem, borda de seleção e alça de quina
sealGroupPane = new Pane();
sealGroupPane.setVisible(false);
sealStampOverlayView = new ImageView(); sealStampOverlayView = new ImageView();
sealStampOverlayView.setPreserveRatio(true); sealStampOverlayView.setPreserveRatio(true);
sealStampOverlayView.setCursor(Cursor.MOVE); sealStampOverlayView.setCursor(Cursor.MOVE);
DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65)); DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65));
sealStampOverlayView.setEffect(sealShadow); sealStampOverlayView.setEffect(sealShadow);
sealStampOverlayView.setVisible(false);
// Arraste com ancoragem precisa no ponto exato do clique dentro do selo (sem salto e sem drift) sealSelectionBorder = new Rectangle();
sealSelectionBorder.setFill(Color.TRANSPARENT);
sealSelectionBorder.setStroke(Color.rgb(2, 132, 199));
sealSelectionBorder.setStrokeWidth(1.8);
sealSelectionBorder.getStrokeDashArray().addAll(5.0, 4.0);
sealSelectionBorder.setVisible(false);
sealSelectionBorder.setOnMousePressed(e -> e.consume());
sealSelectionBorder.setOnMouseDragged(e -> e.consume());
sealSelectionBorder.setOnMouseReleased(e -> e.consume());
sealSelectionBorder.setOnMouseClicked(e -> e.consume());
resizeHandle = new Circle(8);
resizeHandle.setFill(Color.rgb(2, 132, 199));
resizeHandle.setStroke(Color.WHITE);
resizeHandle.setStrokeWidth(2.0);
resizeHandle.setCursor(Cursor.SE_RESIZE);
DropShadow handleShadow = new DropShadow(4, 0, 1, Color.rgb(0, 0, 0, 0.35));
resizeHandle.setEffect(handleShadow);
resizeHandle.setVisible(false);
Tooltip.install(resizeHandle, new Tooltip("Puxe a quina para redimensionar o selo proporcionalmente"));
// Duplo clique no selo para ativar/desativar o modo de redimensionamento
sealStampOverlayView.setOnMouseClicked(event -> {
sealDragOccurred = false;
if (event.getClickCount() == 2) {
toggleResizeMode();
}
event.consume();
});
// Arraste com ancoragem precisa no ponto exato do clique dentro do selo
sealStampOverlayView.setOnMousePressed(event -> { sealStampOverlayView.setOnMousePressed(event -> {
dragStartOffsetInSealX = event.getX(); dragStartOffsetInSealX = event.getX();
dragStartOffsetInSealY = event.getY(); dragStartOffsetInSealY = event.getY();
@@ -536,7 +615,6 @@ public final class PdfSignDialog {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
sealDragOccurred = true; sealDragOccurred = true;
// Converte coordenada da cena para coordenadas locais do painel da folha
Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY()); Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY());
double targetX = localInSheet.getX() - dragStartOffsetInSealX; double targetX = localInSheet.getX() - dragStartOffsetInSealX;
double targetY = localInSheet.getY() - dragStartOffsetInSealY; double targetY = localInSheet.getY() - dragStartOffsetInSealY;
@@ -545,16 +623,47 @@ public final class PdfSignDialog {
}); });
sealStampOverlayView.setOnMouseReleased(event -> { sealStampOverlayView.setOnMouseReleased(event -> {
// Garante que o evento de soltura do mouse não borbulhe para a folha lastResizeActionTimestamp = System.currentTimeMillis();
event.consume(); event.consume();
}); });
sealStampOverlayView.setOnMouseClicked(event -> { // Eventos da Alça de Redimensionamento Interativa
sealDragOccurred = false; resizeHandle.setOnMousePressed(event -> {
isResizingSealInProgress = true;
sealDragOccurred = true;
resizeStartSceneX = event.getSceneX();
resizeStartBaseWidth = currentBaseSealWidth * zoomFactor * sealScale;
event.consume(); event.consume();
}); });
interactiveOverlayPane.getChildren().add(sealStampOverlayView); resizeHandle.setOnMouseDragged(event -> {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
isResizingSealInProgress = true;
sealDragOccurred = true;
double deltaX = event.getSceneX() - resizeStartSceneX;
double newWidth = resizeStartBaseWidth + deltaX;
double baseW = currentBaseSealWidth * zoomFactor;
if (baseW > 0) {
applySealScalePreservingOrigin(newWidth / baseW);
}
event.consume();
});
resizeHandle.setOnMouseReleased(event -> {
isResizingSealInProgress = false;
lastResizeActionTimestamp = System.currentTimeMillis();
sealDragOccurred = true;
event.consume();
});
resizeHandle.setOnMouseClicked(event -> {
isResizingSealInProgress = false;
lastResizeActionTimestamp = System.currentTimeMillis();
event.consume();
});
sealGroupPane.getChildren().addAll(sealStampOverlayView, sealSelectionBorder, resizeHandle);
interactiveOverlayPane.getChildren().add(sealGroupPane);
pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane); pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane);
sheetWrapper.getChildren().add(pageSheetContainer); sheetWrapper.getChildren().add(pageSheetContainer);
sheetScroll.setContent(sheetWrapper); sheetScroll.setContent(sheetWrapper);
@@ -563,6 +672,98 @@ public final class PdfSignDialog {
return column; return column;
} }
private void toggleResizeMode() {
this.isResizeModeActive = !this.isResizeModeActive;
this.lastResizeActionTimestamp = System.currentTimeMillis();
if (sealSelectionBorder != null) {
sealSelectionBorder.setVisible(isResizeModeActive);
}
if (resizeHandle != null) {
resizeHandle.setVisible(isResizeModeActive);
}
if (btnToggleResize != null) {
if (isResizeModeActive) {
btnToggleResize.setText("✓ Concluir");
btnToggleResize.setStyle("-fx-background-color: #0284c7; -fx-text-fill: white; -fx-font-weight: bold;");
lblPositionFeedback.setText("👉 Puxe a alça azul no canto do selo para ajustar o tamanho");
} else {
btnToggleResize.setText("📏 Redimensionar");
btnToggleResize.setStyle("");
updateFeedbackLabel();
}
}
}
private void applySealScalePreservingOrigin(double newScale) {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
double currentX = sealGroupPane != null ? sealGroupPane.getLayoutX() : 0;
double currentY = sealGroupPane != null ? sealGroupPane.getLayoutY() : 0;
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
if (lblSealScaleIndicator != null) {
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
}
if (btnResetSealScale != null) {
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
}
updateContainerZoomDimensions();
double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor;
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
// Mantém a quina superior esquerda ancorada no mesmo ponto exato
double clampedX = Math.max(0, Math.min(currentX, sheetW - sealW));
double clampedY = Math.max(0, Math.min(currentY, sheetH - sealH));
if (sealGroupPane != null) {
sealGroupPane.setLayoutX(clampedX);
sealGroupPane.setLayoutY(clampedY);
sealGroupPane.setVisible(true);
}
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float scaledPdfW = basePdfW * (float) sealScale;
float scaledPdfH = basePdfH * (float) sealScale;
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
float calculatedPdfX = (float) (clampedX / scale);
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
this.customPageNumber = currentPageIndex + 1;
this.isCustomPositionActive = true;
if (positionCombo.getValue() != PositionOption.CUSTOM) {
positionCombo.setValue(PositionOption.CUSTOM);
}
updateFeedbackLabel();
}
private void applySealScale(double newScale) {
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
if (lblSealScaleIndicator != null) {
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
}
if (btnResetSealScale != null) {
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
}
updateContainerZoomDimensions();
repositionSealByRelativeCoords();
updateFeedbackLabel();
}
private void adjustZoom(double delta) { private void adjustZoom(double delta) {
setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta))); setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta)));
} }
@@ -592,8 +793,19 @@ public final class PdfSignDialog {
pageImageView.setFitHeight(currentH); pageImageView.setFitHeight(currentH);
} }
if (sealStampOverlayView != null) { if (sealStampOverlayView != null) {
sealStampOverlayView.setFitWidth(currentBaseSealWidth * zoomFactor); double sealW = currentBaseSealWidth * zoomFactor * sealScale;
sealStampOverlayView.setFitHeight(currentBaseSealHeight * zoomFactor); double sealH = currentBaseSealHeight * zoomFactor * sealScale;
sealStampOverlayView.setFitWidth(sealW);
sealStampOverlayView.setFitHeight(sealH);
if (sealSelectionBorder != null) {
sealSelectionBorder.setWidth(sealW);
sealSelectionBorder.setHeight(sealH);
}
if (resizeHandle != null) {
resizeHandle.setCenterX(sealW);
resizeHandle.setCenterY(sealH);
}
} }
} }
@@ -602,15 +814,17 @@ public final class PdfSignDialog {
double sheetW = currentBaseSheetWidth * zoomFactor; double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor; double sheetH = currentBaseSheetHeight * zoomFactor;
double sealW = currentBaseSealWidth * zoomFactor; double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor; double sealH = currentBaseSealHeight * zoomFactor * sealScale;
double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW)); double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW));
double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH)); double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH));
sealStampOverlayView.setLayoutX(posX); if (sealGroupPane != null) {
sealStampOverlayView.setLayoutY(posY); sealGroupPane.setLayoutX(posX);
sealStampOverlayView.setVisible(true); sealGroupPane.setLayoutY(posY);
sealGroupPane.setVisible(true);
}
} }
// ========================================================================= // =========================================================================
@@ -653,7 +867,9 @@ public final class PdfSignDialog {
renderCurrentPage(); renderCurrentPage();
if (chkVisibleSeal.isSelected()) { if (chkVisibleSeal.isSelected()) {
sealStampOverlayView.setVisible(true); if (sealGroupPane != null) {
sealGroupPane.setVisible(true);
}
resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT); resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT);
} }
} catch (Exception ex) { } catch (Exception ex) {
@@ -751,38 +967,42 @@ public final class PdfSignDialog {
// ========================================================================= // =========================================================================
private void handleSheetClick(MouseEvent event) { private void handleSheetClick(MouseEvent event) {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
double sealW = currentBaseSealWidth * zoomFactor; double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor; double sealH = currentBaseSealHeight * zoomFactor * sealScale;
updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0); updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0);
} }
private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) { private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) {
double sheetW = currentBaseSheetWidth * zoomFactor; double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor; double sheetH = currentBaseSheetHeight * zoomFactor;
double sealW = currentBaseSealWidth * zoomFactor; double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor; double sealH = currentBaseSealHeight * zoomFactor * sealScale;
double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW)); double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW));
double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH)); double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH));
sealStampOverlayView.setLayoutX(clampedX); if (sealGroupPane != null) {
sealStampOverlayView.setLayoutY(clampedY); sealGroupPane.setLayoutX(clampedX);
sealStampOverlayView.setVisible(true); sealGroupPane.setLayoutY(clampedY);
sealGroupPane.setVisible(true);
}
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0; this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0; this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme(); PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0); float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0); float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float scaledPdfW = basePdfW * (float) sealScale;
float scaledPdfH = basePdfH * (float) sealScale;
// Conversão direta e exata da escala visual na tela para os pontos do PDF // Conversão direta e exata da escala visual na tela para os pontos do PDF
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0; double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
float calculatedPdfX = (float) (clampedX / scale); float calculatedPdfX = (float) (clampedX / scale);
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - sealPdfH); float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - sealPdfW)); this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH)); this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
this.customPageNumber = currentPageIndex + 1; this.customPageNumber = currentPageIndex + 1;
this.isCustomPositionActive = true; this.isCustomPositionActive = true;
@@ -799,8 +1019,10 @@ public final class PdfSignDialog {
} }
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme(); PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0); float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0); float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float sealPdfW = basePdfW * (float) sealScale;
float sealPdfH = basePdfH * (float) sealScale;
float margin = 20f; float margin = 20f;
float targetPdfX; float targetPdfX;
@@ -853,16 +1075,22 @@ public final class PdfSignDialog {
private void updateFeedbackLabel() { private void updateFeedbackLabel() {
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) { if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]", String scaleStr = (Math.abs(sealScale - 1.0) > 0.02) ? String.format(" | Escala: %.0f%%", sealScale * 100) : "";
customPageNumber, customPdfX, customPdfY)); lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]%s",
customPageNumber, customPdfX, customPdfY, scaleStr));
} }
} }
private void toggleVisibleSeal(boolean active) { private void toggleVisibleSeal(boolean active) {
positionCombo.setDisable(!active); positionCombo.setDisable(!active);
pageCombo.setDisable(!active); pageCombo.setDisable(!active);
if (btnToggleResize != null) {
btnToggleResize.setDisable(!active);
}
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3); sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
sealStampOverlayView.setVisible(active && selectedPdfBytes != null); if (sealGroupPane != null) {
sealGroupPane.setVisible(active && selectedPdfBytes != null);
}
} }
private void updateSealPreview() { private void updateSealPreview() {
@@ -927,19 +1155,21 @@ public final class PdfSignDialog {
customPdfY, customPdfY,
customPageNumber, customPageNumber,
DEFAULT_REASON, DEFAULT_REASON,
DEFAULT_LOCATION DEFAULT_LOCATION,
sealScale
); );
String sealSummary; String sealSummary;
if (chkVisibleSeal.isSelected()) { if (chkVisibleSeal.isSelected()) {
String themeName = theme.name(); String themeName = theme.name();
String scaleInfo = (Math.abs(sealScale - 1.0) > 0.02) ? String.format(" [Escala: %.0f%%]", sealScale * 100) : "";
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) { if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
sealSummary = String.format("Selo Estampado (%s) — Pág. %d [X: %.0f pt, Y: %.0f pt]", sealSummary = String.format("Selo Estampado (%s)%s — Pág. %d [X: %.0f pt, Y: %.0f pt]",
themeName, customPageNumber, customPdfX, customPdfY); themeName, scaleInfo, customPageNumber, customPdfX, customPdfY);
} else { } else {
sealSummary = String.format("Selo Estampado (%s) — %s (%s)", sealSummary = String.format("Selo Estampado (%s)%s — %s (%s)",
themeName, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página")); themeName, scaleInfo, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página"));
} }
} else { } else {
sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)"; sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)";
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningContext; import br.com.cybercore.signer.core.domain.SigningContext;
import br.com.cybercore.signer.core.security.PinSessionManager; import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.ui.CyberCoreUiConstants; import br.com.cybercore.signer.ui.CyberCoreUiConstants;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager; import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@@ -56,17 +57,33 @@ public class SignaturePromptDialog {
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context, private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
String documentName, String documentType, boolean isAuthentication) { String documentName, String documentType, boolean isAuthentication) {
VBox root = new VBox(18); VBox root = new VBox(16);
root.getStyleClass().add("dialog-container"); root.getStyleClass().add("dialog-container");
root.setPrefWidth(540); root.setPadding(new Insets(20, 24, 20, 24));
root.setPrefWidth(560);
Label header = new Label(isAuthentication ? "🔑 SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA"); HBox headerBox = new HBox(14);
headerBox.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(3);
HBox.setHgrow(titleBox, Priority.ALWAYS);
titleBox.setMaxWidth(Double.MAX_VALUE);
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN" : "SOLICITAÇÃO DE ASSINATURA");
header.getStyleClass().add("dialog-header"); header.getStyleClass().add("dialog-header");
Label desc = new Label(isAuthentication Label desc = new Label(isAuthentication
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil." ? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
: "Uma aplicação web está solicitando a assinatura digital de um documento."); : "Uma aplicação web está solicitando a assinatura digital de um documento.");
desc.getStyleClass().add("brand-subtitle"); desc.getStyleClass().add("brand-subtitle");
desc.setWrapText(true);
desc.setMaxWidth(480);
titleBox.getChildren().addAll(header, desc);
headerBox.getChildren().addAll(
isAuthentication ? SvgIcons.certificatesIcon() : SvgIcons.cyberShieldLogo(26),
titleBox
);
// Caixa de Detalhes // Caixa de Detalhes
VBox detailsBox = new VBox(12); VBox detailsBox = new VBox(12);
@@ -89,9 +106,24 @@ public class SignaturePromptDialog {
} }
addField(grid, row++, "Operação:", "Assinatura Digital (Digest)"); addField(grid, row++, "Operação:", "Assinatura Digital (Digest)");
} }
addField(grid, row++, "Certificado:", cert.commonName() != null ? cert.commonName() : cert.alias()); String rawCertName = cert.commonName() != null ? cert.commonName() : cert.alias();
addField(grid, row++, "Tipo / Dispositivo:", cert.type().getCode() + " • " + String holderName = rawCertName.contains(":") ? rawCertName.split(":")[0].trim() : rawCertName.trim();
(cert.hardwareDeviceName() != null ? cert.hardwareDeviceName() : cert.type().getLabel())); String docInfo = "";
if (cert.cnpj() != null && !cert.cnpj().isBlank()) {
docInfo = ", CNPJ " + cert.cnpj();
} else if (cert.cpf() != null && !cert.cpf().isBlank()) {
docInfo = ", CPF " + cert.cpf();
} else if (rawCertName.contains(":")) {
String candidate = rawCertName.split(":")[1].trim();
if (candidate.length() == 14) {
docInfo = ", CNPJ " + candidate;
} else if (candidate.length() == 11) {
docInfo = ", CPF " + candidate;
}
}
String certDesc = holderName + docInfo + " [" + cert.type().getCode() + "]";
addField(grid, row++, "Certificado:", certDesc);
addField(grid, row++, "Tipo:", cert.type().getCode() + " (" + cert.type().getLabel() + ")");
addField(grid, row++, "Algoritmo:", context.algorithm()); addField(grid, row++, "Algoritmo:", context.algorithm());
detailsBox.getChildren().add(grid); detailsBox.getChildren().add(grid);
@@ -134,7 +166,8 @@ public class SignaturePromptDialog {
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " + "O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente." "Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
); );
winDesc.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); winDesc.getStyleClass().add("dialog-subtitle");
winDesc.setStyle("-fx-font-size: 12px;");
winDesc.setWrapText(true); winDesc.setWrapText(true);
winBox.getChildren().addAll(winTitle, winDesc); winBox.getChildren().addAll(winTitle, winDesc);
@@ -166,6 +199,7 @@ public class SignaturePromptDialog {
if (nowVisible) { if (nowVisible) {
cbSavePin.setSelected(true); cbSavePin.setSelected(true);
} }
stage.sizeToScene();
}); });
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin); rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
@@ -173,25 +207,26 @@ public class SignaturePromptDialog {
} else { } else {
VBox inputWrapper = new VBox(8); VBox inputWrapper = new VBox(8);
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:"); Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
pinPromptLabel.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;"); pinPromptLabel.getStyleClass().add("dialog-subtitle");
pinPromptLabel.setStyle("-fx-font-size: 12px;");
cbSavePin.setSelected(false); cbSavePin.setSelected(false);
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin); inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
pinContainer.getChildren().add(inputWrapper); pinContainer.getChildren().add(inputWrapper);
} }
} }
detailsBox.getChildren().add(pinContainer);
} }
// Label de Alerta de Validação // Label de Alerta de Validação
Label validationLabel = new Label(); Label validationLabel = new Label();
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;"); validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
validationLabel.setVisible(false); validationLabel.setVisible(false);
validationLabel.setManaged(false);
// Botões de Ação // Botões de Ação
HBox actionsRow = new HBox(12); HBox actionsRow = new HBox(12);
actionsRow.setAlignment(Pos.CENTER_RIGHT); actionsRow.setAlignment(Pos.CENTER_RIGHT);
actionsRow.setPadding(new Insets(4, 0, 0, 0));
Button btnCancel = new Button("Cancelar"); Button btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-cancel"); btnCancel.getStyleClass().add("btn-cancel");
@@ -211,6 +246,8 @@ public class SignaturePromptDialog {
if (!hasStoredPin && !pinProvided) { if (!hasStoredPin && !pinProvided) {
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11."); validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
validationLabel.setVisible(true); validationLabel.setVisible(true);
validationLabel.setManaged(true);
stage.sizeToScene();
pinField.requestFocus(); pinField.requestFocus();
return; return;
} }
@@ -231,12 +268,18 @@ public class SignaturePromptDialog {
actionsRow.getChildren().addAll(btnCancel, btnConfirm); actionsRow.getChildren().addAll(btnCancel, btnConfirm);
root.getChildren().addAll(header, desc, detailsBox, validationLabel, actionsRow); if (isA3) {
root.getChildren().addAll(headerBox, detailsBox, pinContainer, validationLabel, actionsRow);
} else {
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow);
}
Scene scene = new Scene(root); Scene scene = new Scene(root);
ThemeManager.applyTheme(scene); ThemeManager.applyTheme(scene);
stage.setScene(scene); stage.setScene(scene);
stage.sizeToScene();
stage.centerOnScreen();
stage.setOnCloseRequest(e -> approvalFuture.complete(false)); stage.setOnCloseRequest(e -> approvalFuture.complete(false));
} }
@@ -252,6 +295,8 @@ public class SignaturePromptDialog {
} }
public CompletableFuture<Boolean> showAndAwait() { public CompletableFuture<Boolean> showAndAwait() {
stage.sizeToScene();
stage.centerOnScreen();
stage.show(); stage.show();
stage.toFront(); stage.toFront();
return approvalFuture; return approvalFuture;
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.provider.CertificateManager; import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.security.PinSessionManager; import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.xml.XmlSignatureOptions; 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 br.com.cybercore.signer.core.xml.XmlSigningService;
import br.com.cybercore.signer.ui.components.SvgIcons; import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager; import br.com.cybercore.signer.ui.theme.ThemeManager;
@@ -25,6 +26,7 @@ import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.ColumnConstraints;
@@ -68,7 +70,9 @@ public final class XmlSignDialog {
private PasswordField pinField; private PasswordField pinField;
private Label lblPinNotice; private Label lblPinNotice;
// Controles de Modo de Assinatura // Controles de Padrão e Modo de Assinatura
private RadioButton rbXadesBes;
private RadioButton rbXmlDsig;
private RadioButton rbWholeDocument; private RadioButton rbWholeDocument;
private RadioButton rbSpecificElement; private RadioButton rbSpecificElement;
private TextField txtElementTag; private TextField txtElementTag;
@@ -94,10 +98,10 @@ public final class XmlSignDialog {
if (owner != null) { if (owner != null) {
this.stage.initOwner(owner); this.stage.initOwner(owner);
} }
this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XMLDSig)"); this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XAdES / ICP-Brasil)");
this.stage.setResizable(true); this.stage.setResizable(true);
this.stage.setMinWidth(720); this.stage.setMinWidth(720);
this.stage.setMinHeight(580); this.stage.setMinHeight(600);
initLayout(); initLayout();
} }
@@ -111,9 +115,9 @@ public final class XmlSignDialog {
HBox header = new HBox(14); HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT); header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(4); VBox titleBox = new VBox(4);
Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XMLDSIG)"); Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XADES / ICP-BRASIL)");
lblTitle.getStyleClass().add("dialog-header"); lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Assine digitalmente documentos XML no padrão W3C XMLDSig e ICP-Brasil (NF-e, CT-e, MDF-e, eSocial, etc.)."); Label lblSub = new Label("Assine digitalmente documentos XML no padrão avançado XAdES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03) ou XMLDSig.");
lblSub.getStyleClass().add("dialog-subtitle"); lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub); titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox); header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
@@ -198,7 +202,21 @@ public final class XmlSignDialog {
// 3. Seção: Modo de Assinatura XMLDSig // 3. Seção: Modo de Assinatura XMLDSig
VBox xmlModeSection = new VBox(12); VBox xmlModeSection = new VBox(12);
xmlModeSection.getStyleClass().add("dialog-box"); xmlModeSection.getStyleClass().add("dialog-box");
Label lblModeTitle = new Label("MODO DE ASSINATURA XMLDSIG"); Label lblProfileTitle = new Label("PADRÃO DE ASSINATURA XML");
lblProfileTitle.getStyleClass().add("dialog-field-title");
ToggleGroup profileGroup = new ToggleGroup();
rbXadesBes = new RadioButton("XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)");
rbXadesBes.setToggleGroup(profileGroup);
rbXadesBes.setSelected(true);
rbXmlDsig = new RadioButton("XMLDSig Clássico W3C (Enveloped legado / NF-e SEFAZ)");
rbXmlDsig.setToggleGroup(profileGroup);
VBox profileBox = new VBox(8);
profileBox.getChildren().addAll(rbXadesBes, rbXmlDsig);
Label lblModeTitle = new Label("ESCOPO DE ASSINATURA");
lblModeTitle.getStyleClass().add("dialog-field-title"); lblModeTitle.getStyleClass().add("dialog-field-title");
ToggleGroup modeGroup = new ToggleGroup(); ToggleGroup modeGroup = new ToggleGroup();
@@ -247,7 +265,16 @@ public final class XmlSignDialog {
chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação"); chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação");
chkValidateAfter.setSelected(true); chkValidateAfter.setSelected(true);
xmlModeSection.getChildren().addAll(lblModeTitle, rbWholeDocument, rbSpecificElement, elementConfigBox, chkValidateAfter); xmlModeSection.getChildren().addAll(
lblProfileTitle,
profileBox,
new Separator(),
lblModeTitle,
rbWholeDocument,
rbSpecificElement,
elementConfigBox,
chkValidateAfter
);
formContent.getChildren().addAll(fileSection, certSection, xmlModeSection); formContent.getChildren().addAll(fileSection, certSection, xmlModeSection);
scrollPane.setContent(formContent); scrollPane.setContent(formContent);
@@ -389,6 +416,10 @@ public final class XmlSignDialog {
pin = pinField.getText().toCharArray(); pin = pinField.getText().toCharArray();
} }
XmlSignatureProfile profile = rbXadesBes.isSelected()
? XmlSignatureProfile.XADES_BES
: XmlSignatureProfile.XMLDSIG;
XmlSignatureOptions options; XmlSignatureOptions options;
if (rbSpecificElement.isSelected()) { if (rbSpecificElement.isSelected()) {
String tag = txtElementTag.getText().trim(); String tag = txtElementTag.getText().trim();
@@ -396,14 +427,14 @@ public final class XmlSignDialog {
if (tag.isBlank()) { if (tag.isBlank()) {
tag = "infNFe"; tag = "infNFe";
} }
options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id); options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id, profile);
} else { } else {
options = XmlSignatureOptions.wholeDocument(cert.id(), pin); options = XmlSignatureOptions.wholeDocument(cert.id(), pin, profile);
} }
setFormDisabled(true); setFormDisabled(true);
progressIndicator.setVisible(true); progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento XML (XMLDSig)..."); lblStatus.setText("Assinando documento XML (" + profile.getDisplayName() + ")...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;"); lblStatus.setStyle("-fx-text-fill: -fx-accent;");
boolean validate = chkValidateAfter.isSelected(); boolean validate = chkValidateAfter.isSelected();
@@ -1,6 +1,6 @@
# Pasta de Ícones (SVG) e Imagens (PNG) — CyberCore Signer # Recursos Visuais (Ícones e Imagens em SVG) — CyberCore Signer
Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente os **ícones em SVG** e as **imagens em PNG** do **CyberCore Signer**. Esta pasta é o repositório oficial onde você pode visualizar, customizar e substituir livremente todos os **ícones em SVG** e as **imagens em SVG** do **CyberCore Signer**.
--- ---
@@ -31,13 +31,23 @@ assets/
│ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais │ ├── seal.svg # Ícone Designer e Gerenciador de Selos Notariais
│ ├── check.svg # Ícone Checkmark de sucesso e validação │ ├── check.svg # Ícone Checkmark de sucesso e validação
│ ├── error.svg # Ícone de Erro / Alerta em vermelho │ ├── error.svg # Ícone de Erro / Alerta em vermelho
│ └── star.svg # Ícone de Estrela notarial / favorita │ ├── star.svg # Ícone de Estrela notarial / favorita
│ ├── close.svg # Ícone Fechar / Cancelar (cruz geométrica moderna)
│ ├── arrow-right.svg # Ícone Avançar / Seta direcional
│ ├── zoom-in.svg # Ícone Zoom In (+)
│ ├── zoom-out.svg # Ícone Zoom Out (-)
│ ├── zoom-fit.svg # Ícone Ajustar à Tela
│ └── refresh.svg # Ícone Atualizar / Recarregar listas e status
│ │
└── images/ # Imagens de alta fidelidade em PNG com canal alfa (transparência) └── images/ # Imagens vetoriais SVG editáveis (com cópias PNG de fallback)
├── app-icon.png # Ícone oficial do app (256x256 px - Diamante 3D azul Serpro) ├── app-icon.svg # Ícone oficial do app em SVG (Cristal/Diamante 3D azul)
├── app-logo.png # Logotipo horizontal oficial (480x120 px - CyberCore SIGNER) ├── app-icon.png # Fallback em PNG (256x256 px)
├── brand-banner.png # Banner de cabeçalho e marca (600x160 px - Serpro Blue) ├── app-logo.svg # Logotipo horizontal oficial em SVG (CyberCore SIGNER)
└── default-seal.png # Selo de assinatura padrão ICP-Brasil (640x164 px) ├── app-logo.png # Fallback em PNG (480x120 px)
├── brand-banner.svg # Banner de cabeçalho e marca em SVG (600x160 px)
├── brand-banner.png # Fallback em PNG (600x160 px)
├── default-seal.svg # Selo de assinatura padrão ICP-Brasil em SVG (640x164 px)
└── default-seal.png # Fallback em PNG (640x164 px)
``` ```
--- ---
@@ -45,20 +55,21 @@ assets/
## 🎨 Como Customizar Qualquer Ícone ou Imagem ## 🎨 Como Customizar Qualquer Ícone ou Imagem
1. **Ícones:** 1. **Ícones:**
- Estão no formato `.svg` padrão. - Todos estão no formato `.svg` padrão da W3C.
- Podem ser editados diretamente no seu editor de código (VS Code, etc.) ou em editores vetoriais como **Inkscape, Figma ou Illustrator**. - Podem ser abertos e editados diretamente em editores de texto/código (VS Code) ou ferramentas vetoriais como **Inkscape, Figma, Adobe Illustrator**.
- Se preferir colocar uma imagem bitmap para um ícone específico, você também pode colocar um `.png` (ex: `pdf.png`). O sistema prioriza `.svg` e aceita `.png` com fallback transparente. - Se preferir usar um arquivo bitmap para algum ícone específico, basta salvar um `.png` correspondente na pasta (ex: `pdf.png`). O sistema detecta e renderiza automaticamente.
2. **Imagens do Sistema:** 2. **Imagens do Sistema:**
- Estão no formato `.png` com canal alfa de 32 bits (fundo transparente). - Tanto o logotipo horizontal (`app-logo.svg`), o ícone do sistema (`app-icon.svg`), o banner institucional (`brand-banner.svg`) e o selo notarial padrão (`default-seal.svg`) agora são arquivos **SVG vetoriais editáveis**.
- Você pode substituir `app-logo.png`, `app-icon.png`, `brand-banner.png` ou `default-seal.png` pelo logotipo e identidade visual da sua instituição ou empresa. - Você pode alterar cores, tipografias, brasões e dimensões diretamente nos arquivos SVG.
- O `AssetManager` renderiza os arquivos SVG preservando fidelidade geométrica e suportando gradientes e alinhamentos.
3. **Ordem de Prioridade e Hot-Reloading:** 3. **Ordem de Resolução e Carregamento:**
Quando a aplicação busca um ícone ou imagem, a resolução ocorre na ordem: Ao requisitar qualquer elemento gráfico, a aplicação segue a ordem de prioridade:
1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Qualquer alteração feita aqui entra em vigor imediatamente ao abrir a janela ou reiniciar a aplicação, sem necessidade de recompilar! 1. **Pasta Física Local (`./assets/icons/` e `./assets/images/`)**: Arquivos `.svg` editados aqui têm prioridade máxima e entram em vigor imediatamente sem necessidade de recompilar.
2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Configurações específicas por usuário do sistema operacional. 2. **Pasta do Usuário (`~/.cybercore/assets/`)**: Para customizações no diretório pessoal do usuário.
3. **Resources Empacotados no JAR (`/assets/`)**: Cópia empacotada internamente no `.jar`. 3. **Recursos Empacotados no JAR (`/assets/`)**: SVGs e imagens internas empacotadas no pacote de distribuição.
4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique em branco caso algum arquivo seja excluído. 4. **Fallback Vetorial Nativo (`SvgIcons`)**: Garantia de que a interface nunca fique desprovida de grafismos caso algum arquivo seja removido.
--- ---
@@ -1,5 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="20" fill="#0288d1"/> <!-- Círculo azul e letra 'i' informativa centrada em (24, 24) -->
<circle cx="24" cy="14" r="3" fill="#ffffff"/> <circle cx="24" cy="24" r="18" fill="#0288d1"/>
<line x1="24" y1="21" x2="24" y2="34" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/> <circle cx="24" cy="15" r="2.5" fill="#ffffff"/>
<line x1="24" y1="21" x2="24" y2="33" stroke="#ffffff" stroke-width="4.5" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 291 B

After

Width:  |  Height:  |  Size: 362 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 8 5 L 15 12 L 8 19" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 6 6 L 18 18 M 18 6 L 6 18" fill="none" stroke="#64748b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

@@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Lua crescente ciano/índigo --> <!-- Lua crescente ciano/índigo perfeitamente centrada em (24, 24) -->
<path d="M 40 26.5 A 18 18 0 1 1 21.5 8 A 14 14 0 0 0 40 26.5 Z" fill="#38bdf8"/> <path d="M 24 8 A 16 16 0 1 0 40 24 A 14 14 0 0 1 24 8 Z" fill="#38bdf8"/>
<circle cx="36" cy="12" r="1.5" fill="#818cf8"/> <circle cx="34" cy="14" r="1.8" fill="#818cf8"/>
<circle cx="42" cy="18" r="1" fill="#818cf8"/> <circle cx="38" cy="22" r="1.2" fill="#818cf8"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 314 B

After

Width:  |  Height:  |  Size: 344 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 20 4 L 20 9 L 15 9" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 4 20 L 4 15 L 9 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 5.8 9 A 7 7 0 0 1 18.5 7.5 L 20 9 M 4 15 L 5.5 16.5 A 7 7 0 0 0 18.2 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 54" width="36" height="54">
<!-- Escudo Criptográfico Vetorial ICP-Brasil -->
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="#0d47a1" opacity="0.14"/>
<path d="M18 4 L32 9 V24 C32 37 25 46 18 50 C11 46 4 37 4 24 V9 Z" fill="none" stroke="#0d47a1" stroke-width="2.2" stroke-linejoin="round"/>
<!-- Corpo do Cadeado de Segurança -->
<rect x="11" y="24" width="14" height="12" rx="2.5" fill="#0284c7"/>
<!-- Arco Superior do Cadeado -->
<path d="M13.5 24 V19 C13.5 16.5 15.5 14.5 18 14.5 C20.5 14.5 22.5 16.5 22.5 19 V24" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round"/>
<!-- Orifício da Chave -->
<circle cx="18" cy="29" r="1.6" fill="#ffffff"/>
<path d="M17.3 29.5 L18.7 29.5 L19.2 33 L16.8 33 Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 858 B

@@ -1,8 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<!-- Engrenagem simétrica de 8 dentes centrada em (24, 24) --> <!-- Engrenagem corporativa simétrica centrada em (24, 24) -->
<path d="M 21 4 L 27 4 L 28 10 C 30 10.8 31.8 11.8 33.4 13.2 L 39 10 L 43 14 L 39.8 19.6 C 41.2 21.2 42.2 23 43 25 L 49 26 L 49 32 L 43 33 C 42.2 35 41.2 36.8 39.8 38.4 L 43 44 L 39 48 L 33.4 44.8 C 31.8 46.2 30 47.2 28 48 L 27 54 L 21 54 L 20 48 C 18 47.2 16.2 46.2 14.6 44.8 L 9 48 L 5 44 L 8.2 38.4 C 6.8 36.8 5.8 35 5 33 L -1 32 L -1 26 L 5 25 C 5.8 23 6.8 21.2 8.2 19.6 L 5 14 L 9 10 L 14.6 13.2 C 16.2 11.8 18 10.8 20 10 Z" transform="scale(0.8) translate(6, 4)" fill="#0288d1"/> <path fill="#0288d1" fill-rule="evenodd" d="M 21.5 5 L 26.5 5 L 27.2 10.2 C 29 10.9 30.7 11.9 32.2 13.1 L 36.6 10.6 L 40.1 14.1 L 37.6 18.5 C 38.8 20 39.8 21.7 40.5 23.5 L 45.7 24.2 L 45.7 29.2 L 40.5 29.9 C 39.8 31.7 38.8 33.4 37.6 34.9 L 40.1 39.3 L 36.6 42.8 L 32.2 40.3 C 30.7 41.5 29 42.5 27.2 43.2 L 26.5 48.4 L 21.5 48.4 L 20.8 43.2 C 19 42.5 17.3 41.5 15.8 40.3 L 11.4 42.8 L 7.9 39.3 L 10.4 34.9 C 9.2 33.4 8.2 31.7 7.5 29.9 L 2.3 29.2 L 2.3 24.2 L 7.5 23.5 C 8.2 21.7 9.2 20 10.4 18.5 L 7.9 14.1 L 11.4 10.6 L 15.8 13.1 C 17.3 11.9 19 10.9 20.8 10.2 Z M 24 17.5 A 6.5 6.5 0 1 0 24 30.5 A 6.5 6.5 0 1 0 24 17.5 Z"/>
<!-- Círculo externo da engrenagem -->
<circle cx="24" cy="24" r="14" fill="#0288d1"/>
<!-- Furo central vazado -->
<circle cx="24" cy="24" r="6" fill="#ffffff"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 784 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M 4 9 L 4 4 L 9 4 M 15 4 L 20 4 L 20 9 M 20 15 L 20 20 L 15 20 M 9 20 L 4 20 L 4 15" fill="none" stroke="#0284c7" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M 10 7 L 10 13 M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle cx="10" cy="10" r="7" fill="none" stroke="#0284c7" stroke-width="2.2"/>
<path d="M 15 15 L 21 21" fill="none" stroke="#0284c7" stroke-width="2.5" stroke-linecap="round"/>
<path d="M 7 10 L 13 10" fill="none" stroke="#0284c7" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1976d2"/>
<stop offset="45%" stop-color="#0d47a1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
<linearGradient id="topFacetGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="leftFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.75"/>
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.35"/>
</linearGradient>
<linearGradient id="rightFacetGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.85"/>
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.45"/>
</linearGradient>
<radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Fundo com cantos arredondados -->
<rect x="8" y="8" width="240" height="240" rx="68" ry="68" fill="url(#bgGrad)"/>
<rect x="10" y="10" width="236" height="236" rx="66" ry="66" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-opacity="0.25"/>
<!-- Facetas 3D do Cristal / Diamante Azul -->
<polygon points="128,46 210,113 128,143 46,113" fill="url(#topFacetGrad)"/>
<polygon points="46,113 128,143 128,215 72,179" fill="url(#leftFacetGrad)"/>
<polygon points="210,113 128,143 128,215 184,179" fill="url(#rightFacetGrad)"/>
<!-- Brilho estelar central -->
<circle cx="128" cy="128" r="38" fill="url(#centerGlow)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" width="480" height="120">
<defs>
<linearGradient id="logoIconBg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1976d2"/>
<stop offset="50%" stop-color="#0d47a1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
<linearGradient id="logoTopFacet" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#80d8ff" stop-opacity="0.6"/>
</linearGradient>
<linearGradient id="logoLeftFacet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#40c4ff" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#0288d1" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="logoRightFacet" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e1f5fe" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#29b6f6" stop-opacity="0.5"/>
</linearGradient>
</defs>
<!-- Container do Ícone Cristal 3D -->
<rect x="14" y="16" width="88" height="88" rx="24" ry="24" fill="url(#logoIconBg)"/>
<rect x="16" y="18" width="84" height="84" rx="22" ry="22" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-opacity="0.25"/>
<!-- Facetas Cristalinas (cx=58, cy=60, sz=63) -->
<polygon points="58,32 86,55 58,66 30,55" fill="url(#logoTopFacet)"/>
<polygon points="30,55 58,66 58,92 39,79" fill="url(#logoLeftFacet)"/>
<polygon points="86,55 58,66 58,92 77,79" fill="url(#logoRightFacet)"/>
<circle cx="58" cy="60" r="14" fill="#ffffff" fill-opacity="0.3"/>
<!-- Tipografia Oficial "CyberCore" -->
<text x="124" y="62" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="#0a2540">Cyber<tspan fill="#0288d1">Core</tspan></text>
<!-- Badge "SIGNER" -->
<rect x="124" y="76" width="94" height="22" rx="6" ry="6" fill="#0288d1"/>
<text x="139" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#ffffff" letter-spacing="1">SIGNER</text>
<!-- Subtítulo ICP-Brasil -->
<text x="230" y="92" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="13" font-weight="normal" fill="#64748b">ICP-Brasil • Assinador Digital Seguro</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" width="600" height="160">
<defs>
<linearGradient id="bannerBgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0d47a1"/>
<stop offset="60%" stop-color="#0284c7"/>
<stop offset="100%" stop-color="#0369a1"/>
</linearGradient>
<linearGradient id="bannerIconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e88e5"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
</defs>
<!-- Fundo do Banner -->
<rect x="0" y="0" width="600" height="160" rx="16" ry="16" fill="url(#bannerBgGrad)"/>
<!-- Formas geométricas de segurança no fundo -->
<circle cx="480" cy="50" r="110" fill="#ffffff" fill-opacity="0.08"/>
<circle cx="540" cy="120" r="80" fill="#ffffff" fill-opacity="0.06"/>
<circle cx="380" cy="-20" r="90" fill="#ffffff" fill-opacity="0.04"/>
<!-- Ícone Cristal à esquerda -->
<rect x="30" y="35" width="90" height="90" rx="22" ry="22" fill="url(#bannerIconGrad)"/>
<rect x="33" y="38" width="84" height="84" rx="19" ry="19" fill="none" stroke="#ffffff" stroke-width="2" stroke-opacity="0.3"/>
<polygon points="75,52 101,80 75,108 49,80" fill="#ffffff" fill-opacity="0.9"/>
<polygon points="75,52 75,108 49,80" fill="#80d8ff" fill-opacity="0.5"/>
<!-- Textos do Banner -->
<text x="148" y="74" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="34" font-weight="bold" fill="#ffffff">CyberCore Signer</text>
<text x="148" y="104" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="16" font-weight="normal" fill="#e0f2fe">Agente de Assinatura Digital ICP-Brasil A1 &amp; A3</text>
<text x="148" y="128" font-family="'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#38bdf8" letter-spacing="1">PADES • XADES • CADES • TIMESTAMPS • CCSP/1</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 164" width="640" height="164">
<defs>
<linearGradient id="badgeGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0288d1"/>
<stop offset="100%" stop-color="#01579b"/>
</linearGradient>
</defs>
<!-- Fundo do Selo -->
<rect x="2" y="2" width="636" height="160" rx="12" ry="12" fill="#f8fafc" stroke="#0288d1" stroke-width="2.5"/>
<!-- Brasão Notarial no lado esquerdo -->
<rect x="18" y="16" width="130" height="132" rx="10" ry="10" fill="url(#badgeGrad)"/>
<!-- Textos do Brasão -->
<text x="83" y="44" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">ASSINADO</text>
<text x="83" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#ffffff" text-anchor="middle" letter-spacing="0.5">DIGITALMENTE</text>
<!-- Estrela dourada -->
<circle cx="83" cy="85" r="14" fill="#fbbf24"/>
<text x="83" y="91" font-family="'Segoe UI', sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">★</text>
<text x="83" y="126" font-family="'Segoe UI', Roboto, sans-serif" font-size="10" font-weight="bold" fill="#e0f2fe" text-anchor="middle" letter-spacing="1">ICP-BRASIL</text>
<!-- Linha divisória vertical -->
<line x1="166" y1="16" x2="166" y2="148" stroke="#cfd8dc" stroke-width="1.5"/>
<!-- Informações da Assinatura -->
<text x="184" y="36" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-weight="bold" fill="#64748b" letter-spacing="0.5">DOCUMENTO ASSINADO DIGITALMENTE POR:</text>
<text x="184" y="60" font-family="'Segoe UI', Roboto, sans-serif" font-size="17" font-weight="bold" fill="#0a2540">NOME DO TITULAR DO CERTIFICADO</text>
<text x="184" y="82" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">CPF: ***.000.000-**</text>
<text x="184" y="102" font-family="'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="normal" fill="#334155">Data e Hora: 11/10/2026 12:00:00 -03:00</text>
<text x="184" y="128" font-family="'Segoe UI', Roboto, sans-serif" font-size="11" font-style="italic" fill="#0288d1">A conformidade com a ICP-Brasil pode ser verificada no ITI.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -6,6 +6,7 @@
.root { .root {
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; -fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-text-secondary: #64748b;
} }
/* ========================================================================== /* ==========================================================================
@@ -17,6 +18,7 @@
-fx-background: #f1f5f9; -fx-background: #f1f5f9;
-fx-control-inner-background: #ffffff; -fx-control-inner-background: #ffffff;
-fx-text-fill: #0f172a; -fx-text-fill: #0f172a;
-fx-text-secondary: #64748b;
} }
.theme-light.cyber-root, .theme-light.cyber-root,
@@ -438,6 +440,7 @@
-fx-background: #0b0f17; -fx-background: #0b0f17;
-fx-control-inner-background: #131c2e; -fx-control-inner-background: #131c2e;
-fx-text-fill: #f1f5f9; -fx-text-fill: #f1f5f9;
-fx-text-secondary: #94a3b8;
} }
.theme-dark.cyber-root, .theme-dark.cyber-root,
@@ -855,7 +858,7 @@
/* Botões Base e Proteção contra Truncamento com Reticências */ /* Botões Base e Proteção contra Truncamento com Reticências */
.button { .button {
-fx-min-width: -fx-pref-size; -fx-min-width: -1;
} }
.btn-primary { .btn-primary {
@@ -865,7 +868,6 @@
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-background-radius: 6px; -fx-background-radius: 6px;
-fx-cursor: hand; -fx-cursor: hand;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1); -fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
} }
@@ -880,7 +882,6 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-background-radius: 6px; -fx-background-radius: 6px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -910,7 +911,6 @@
-fx-text-fill: #475569; -fx-text-fill: #475569;
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -933,14 +933,12 @@
/* Botões Padronizados para Diálogos e Modais */ /* Botões Padronizados para Diálogos e Modais */
.btn-dialog { .btn-dialog {
-fx-min-width: 110px; -fx-min-width: 110px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 34px; -fx-pref-height: 34px;
-fx-alignment: center; -fx-alignment: center;
} }
.btn-dialog-wide { .btn-dialog-wide {
-fx-min-width: 220px; -fx-min-width: 220px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 36px; -fx-pref-height: 36px;
-fx-alignment: center; -fx-alignment: center;
} }
@@ -949,13 +947,12 @@
.btn-compact { .btn-compact {
-fx-padding: 5px 12px; -fx-padding: 5px 12px;
-fx-font-size: 11.5px; -fx-font-size: 11.5px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center; -fx-alignment: center;
} }
/* Botão de Perigo / Exclusão / Remoção */ /* Botão de Perigo / Exclusão / Remoção */
.btn-danger { .btn-danger {
-fx-background-color: #fee2e2; -fx-background-color: #fee2e8;
-fx-text-fill: #b91c1c; -fx-text-fill: #b91c1c;
-fx-border-color: #fca5a5; -fx-border-color: #fca5a5;
-fx-border-radius: 6px; -fx-border-radius: 6px;
@@ -965,7 +962,6 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 6px 14px; -fx-padding: 6px 14px;
-fx-alignment: center; -fx-alignment: center;
-fx-min-width: -fx-pref-size;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -0,0 +1,58 @@
package br.com.cybercore.signer.ui.assets;
import br.com.cybercore.signer.ui.components.SvgIcons;
import javafx.scene.Node;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import static org.assertj.core.api.Assertions.assertThat;
class AssetManagerTest {
@Test
@DisplayName("Deve carregar imagens institucionais em SVG do classpath ou pasta de assets")
void shouldLoadSvgImages() {
Node logo = AssetManager.getImageOrDefault("app-logo", 480, 120, null);
assertThat(logo).isNotNull();
Node banner = AssetManager.getImageOrDefault("brand-banner", 600, 160, null);
assertThat(banner).isNotNull();
Node icon = AssetManager.getImageOrDefault("app-icon", 256, 256, null);
assertThat(icon).isNotNull();
Node seal = AssetManager.getImageOrDefault("default-seal", 640, 164, null);
assertThat(seal).isNotNull();
}
@Test
@DisplayName("Deve carregar ícones SVG dinâmicos através do AssetManager")
void shouldLoadSvgIcons() {
String[] iconNames = {
"pdf", "xml", "file", "batch", "attached", "co-sign",
"timestamp", "ltv", "validate", "validate-timestamp", "exit",
"logs", "settings", "tutorial", "cert", "about", "sun", "moon",
"shield", "seal", "check", "error", "star", "close", "arrow-right",
"zoom-in", "zoom-out", "zoom-fit", "refresh"
};
for (String name : iconNames) {
Node iconNode = AssetManager.getIconOrDefault(name, 24, null);
assertThat(iconNode).withFailMessage("Ícone SVG não encontrado ou nulo: %s", name).isNotNull();
}
}
@Test
@DisplayName("Deve instanciar nós de ícones via fábrica SvgIcons com fallback")
void shouldCreateNodesViaSvgIcons() {
assertThat(SvgIcons.closeIcon(16)).isNotNull();
assertThat(SvgIcons.arrowRightIcon(16)).isNotNull();
assertThat(SvgIcons.zoomInIcon(16)).isNotNull();
assertThat(SvgIcons.zoomOutIcon(16)).isNotNull();
assertThat(SvgIcons.zoomFitIcon(16)).isNotNull();
assertThat(SvgIcons.refreshIcon(16)).isNotNull();
assertThat(SvgIcons.createAppCrystalLogo(48)).isNotNull();
}
}
+509 -113
View File
@@ -7,6 +7,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
if (window.pdfjsLib) {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
</script>
<style> <style>
:root { :root {
--bg-base: #090d16; --bg-base: #090d16;
@@ -101,6 +107,7 @@
letter-spacing: 0.5px; letter-spacing: 0.5px;
background: linear-gradient(to right, #ffffff, #cbd5e1); background: linear-gradient(to right, #ffffff, #cbd5e1);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
@@ -615,10 +622,13 @@
<!-- Abas de Navegação --> <!-- Abas de Navegação -->
<div class="tabs-nav"> <div class="tabs-nav">
<button class="tab-btn active" data-tab="tab-pdf"> <button class="tab-btn active" data-tab="tab-pdf">
<span>📄</span> Assinar PDF <span>📄</span> Assinar PDF (Com Stamp Picker)
</button>
<button class="tab-btn" data-tab="tab-seals">
<span>🎨</span> Designer de Selos
</button> </button>
<button class="tab-btn" data-tab="tab-xml"> <button class="tab-btn" data-tab="tab-xml">
<span>📋</span> Assinar XML (NF-e) <span>📋</span> Assinar XML (XAdES / NF-e)
</button> </button>
<button class="tab-btn" data-tab="tab-file"> <button class="tab-btn" data-tab="tab-file">
<span>📁</span> Arquivos em Geral <span>📁</span> Arquivos em Geral
@@ -628,19 +638,19 @@
</button> </button>
</div> </div>
<!-- ABA 1: ASSINAR PDF --> <!-- ABA 1: ASSINAR PDF (INTERATIVO COM RETORNO EM BASE64) -->
<div id="tab-pdf" class="tab-pane active"> <div id="tab-pdf" class="tab-pane active">
<div class="card-grid"> <div class="card-grid" style="grid-template-columns: 1fr;">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<span class="card-title"><span>📄</span> Seleção do Documento PDF</span> <span class="card-title"><span>📄</span> Seleção do Documento & Posicionador de Selo (Stamp Picker)</span>
<span class="badge-tag">PAdES / Digest</span> <span class="badge-tag">PAdES ISO 32000 / Base64</span>
</div> </div>
<div id="pdfDropZone" class="drop-zone"> <div id="pdfDropZone" class="drop-zone">
<div class="drop-icon">📑</div> <div class="drop-icon">📑</div>
<p style="font-size: 13px; font-weight: 600; color: #fff;">Arraste um PDF ou clique para selecionar</p> <p style="font-size: 13px; font-weight: 600; color: #fff;">Arraste um PDF ou clique para selecionar</p>
<p style="font-size: 11px; color: var(--text-muted);">Suporta qualquer documento .pdf</p> <p style="font-size: 11px; color: var(--text-muted);">O documento será renderizado abaixo para você escolher onde posicionar o selo</p>
<input type="file" id="pdfFileInput" accept="application/pdf" style="display: none;"> <input type="file" id="pdfFileInput" accept="application/pdf" style="display: none;">
</div> </div>
@@ -649,80 +659,144 @@
<strong id="pdfFileName" style="color: #fff;">contrato_prestacao.pdf</strong> <strong id="pdfFileName" style="color: #fff;">contrato_prestacao.pdf</strong>
<div id="pdfFileSize" style="color: var(--text-muted); font-size: 11px;">0 KB</div> <div id="pdfFileSize" style="color: var(--text-muted); font-size: 11px;">0 KB</div>
</div> </div>
<span class="badge-tag">PDF</span> <span class="badge-tag">PDF Carregado</span>
</div>
<!-- Contêiner do PdfStampPicker -->
<div id="stampPickerMount" style="margin-top: 14px; display: none;"></div>
<!-- Configurações do Selo e da Assinatura -->
<div id="pdfOptionsSection" style="margin-top: 16px; display: none; background: var(--bg-surface); padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
<h4 style="font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 12px;">🎨 Configurações do Selo Visual</h4>
<div style="display: grid; grid-template-columns: minmax(240px, 360px); gap: 14px;">
<div class="form-group">
<label class="form-label">Tema do Selo Visual:</label>
<select id="pdfSealThemeSelect" class="cert-bar-select">
<option value="icp-brasil">Selo Padrão ICP-Brasil (Oficial)</option>
<option value="custom">🎨 Selo Personalizado do Designer</option>
</select>
</div>
</div>
<div style="margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap;">
<button id="btnSignPdfFull" class="btn btn-primary" style="flex: 2; padding: 12px; font-size: 13px;" disabled>
<span>✍️</span> Assinar PDF Completo no Agente (Devolve Base64)
</button>
<button id="btnSignPdfDigest" class="btn btn-secondary" style="flex: 1; padding: 12px; font-size: 12px;" disabled>
<span>🛡️</span> Assinar Apenas Digest (PAdES Remoto)
</button>
</div>
</div>
<div id="pdfAlert" class="alert alert-info" style="display: none; margin-top: 14px;"></div>
</div>
<!-- Card de Resultado do PDF Assinado -->
<div id="pdfResultCard" class="card" style="display: none;">
<div class="card-header">
<span class="card-title"><span>🎉</span> PDF Assinado Retornado com Sucesso</span>
<div style="display: flex; gap: 8px;">
<button id="btnDownloadPdf" class="btn btn-primary" style="padding: 6px 14px; font-size: 12px;">
<span>💾</span> Baixar PDF Assinado (.pdf)
</button>
<button id="btnCopyPdfBase64" class="btn btn-secondary" style="padding: 6px 12px; font-size: 12px;">
<span>📋</span> Copiar Base64
</button>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-bottom: 14px;">
<div class="form-group">
<label class="form-label">Status da Operação:</label>
<div id="pdfResultStatus" style="font-weight: 700; color: var(--success); font-size: 13px;">Assinado com Sucesso</div>
</div>
<div class="form-group">
<label class="form-label">Tamanho do PDF Assinado:</label>
<div id="pdfResultSize" style="font-family: var(--font-mono); font-size: 12px; color: #fff;">0 KB</div>
</div>
<div class="form-group">
<label class="form-label">SHA-256 do Documento Final:</label>
<div id="pdfResultSha256" style="font-family: var(--font-mono); font-size: 11px; color: var(--primary); word-break: break-all;">-</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Digest SHA-256 Calculado no Navegador (Base64):</label> <label class="form-label">Conteúdo do PDF Assinado em Base64 (pronto para envio ao Backend):</label>
<div id="pdfDigestBox" class="code-box">Selecione um arquivo PDF para calcular o digest...</div> <div id="pdfResultBase64" class="code-box" style="max-height: 140px;"></div>
</div>
</div>
</div> </div>
<button id="btnSignPdf" class="btn btn-primary" style="padding: 12px;" disabled>
<span>✍️</span> Assinar PDF com CyberCore
</button>
<div id="pdfAlert" class="alert alert-info" style="display: none;"></div>
</div> </div>
<!-- ABA 2: DESIGNER DE SELOS VISUAIS -->
<div id="tab-seals" class="tab-pane">
<div class="card-grid" style="grid-template-columns: 1fr;">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<span class="card-title"><span>🛡️</span> Resultado da Assinatura Digital</span> <span class="card-title"><span>🎨</span> Designer Interativo de Selos Visuais</span>
<span class="badge-tag">Customização em Tempo Real</span>
</div> </div>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 14px;">
Crie, personalize cores, tipografia, logotipos e metadados de selos visuais. As configurações geradas aqui podem ser enviadas diretamente na requisição de assinatura de PDF (<code style="color: var(--primary);">seal_custom</code>) ou salvas no sistema.
</p>
<div class="form-group"> <div id="sealDesignerMount"></div>
<label class="form-label">Assinatura Digital Retornada (Base64):</label>
<div id="pdfSigOutput" class="code-box" style="min-height: 120px;">Aguardando assinatura do PDF...</div>
</div>
<div class="form-group">
<label class="form-label">Cadeia do Certificado Digital X.509:</label>
<div id="pdfCertOutput" class="code-box">Nenhum certificado emitido ainda.</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- ABA 2: ASSINAR XML --> <!-- ABA 3: ASSINAR XML -->
<div id="tab-xml" class="tab-pane"> <div id="tab-xml" class="tab-pane">
<div class="card-grid"> <div class="card-grid">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<span class="card-title"><span>📋</span> Conteúdo do XML (NF-e / XMLDSig)</span> <span class="card-title"><span>📋</span> Conteúdo do XML (NF-e / CT-e / NFS-e)</span>
<button id="btnLoadXmlSample" class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px;"> <button id="btnLoadXmlSample" class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px;">
Carregar Exemplo NF-e Carregar Exemplo NF-e
</button> </button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Estrutura XML para Assinatura:</label> <label class="form-label">Perfil de Assinatura XML:</label>
<textarea id="xmlContentInput" rows="7" placeholder="Cole o XML ou selecione um arquivo..."></textarea> <select id="xmlProfileSelect" class="cert-bar-select">
<option value="XADES_BES">XAdES-BES (Padrão Oficial ICP-Brasil / NF-e)</option>
<option value="XMLDSIG">XMLDSig W3C Padrão (Enveloped)</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Digest SHA-256 Calculado no Navegador (Base64):</label> <label class="form-label">Estrutura XML para Assinatura:</label>
<div id="xmlDigestBox" class="code-box">Aguardando XML...</div> <textarea id="xmlContentInput" rows="8" placeholder="Cole o XML ou selecione um arquivo..."></textarea>
</div> </div>
<button id="btnSignXml" class="btn btn-primary" style="padding: 12px;" disabled> <div style="display: flex; gap: 10px; margin-top: 4px;">
<span>✍️</span> Assinar XML com CyberCore <button id="btnSignXmlFull" class="btn btn-primary" style="flex: 2; padding: 12px;" disabled>
<span>✍️</span> Assinar XML Completo no Agente
</button> </button>
<button id="btnSignXmlDigest" class="btn btn-secondary" style="flex: 1; padding: 12px; font-size: 12px;" disabled>
<span>🛡️</span> Apenas Digest
</button>
</div>
<div id="xmlAlert" class="alert alert-info" style="display: none;"></div> <div id="xmlAlert" class="alert alert-info" style="display: none; margin-top: 14px;"></div>
</div> </div>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<span class="card-title"><span>🛡️</span> Pacote XML Assinado (XMLDSig)</span> <span class="card-title"><span>🛡️</span> Pacote XML Assinado</span>
<div style="display: flex; gap: 6px;">
<button id="btnDownloadXml" class="btn btn-primary" style="padding: 4px 10px; font-size: 11px; display: none;">
<span>💾</span> Baixar XML
</button>
<button id="btnCopyXml" class="btn btn-secondary" style="padding: 4px 10px; font-size: 11px; display: none;">
<span>📋</span> Copiar
</button>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Bloco SignatureValue (Base64):</label> <label class="form-label">XML Assinado Completo (com envelope de assinatura):</label>
<div id="xmlSigOutput" class="code-box" style="min-height: 120px;">Aguardando assinatura do XML...</div> <div id="xmlSigOutput" class="code-box" style="min-height: 220px; max-height: 380px;">Aguardando submissão do XML para assinatura...</div>
</div>
<div class="form-group">
<label class="form-label">Tag XMLDSig Incorporável:</label>
<div id="xmlTagOutput" class="code-box">&lt;Signature xmlns="http://www.w3.org/2000/09/xmldsig#"&gt;...&lt;/Signature&gt;</div>
</div> </div>
</div> </div>
</div> </div>
@@ -851,7 +925,7 @@
<!-- Lógica Javascript com o SDK --> <!-- Lógica Javascript com o SDK -->
<script type="module"> <script type="module">
import { CyberCoreSigner } from '../src/signer-sdk.js'; import { CyberCoreSigner, PdfStampPicker, PdfSealDesigner } from '../src/signer-sdk.js';
const signer = new CyberCoreSigner({ const signer = new CyberCoreSigner({
baseUrl: 'http://127.0.0.1:21325', baseUrl: 'http://127.0.0.1:21325',
@@ -870,6 +944,16 @@
let currentCertificates = []; let currentCertificates = [];
let selectedCertificateId = null; let selectedCertificateId = null;
// Componentes e Estado
let pdfStampPicker = null;
let pdfSealDesigner = null;
let currentPdfFile = null;
let currentPdfBytes = null;
let currentPdfDigest = null;
let currentCustomSealConfig = null;
let currentSignedPdfBase64 = null;
let currentSignedXml = null;
// Gerenciador de Abas // Gerenciador de Abas
const tabButtons = document.querySelectorAll('.tab-btn'); const tabButtons = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane'); const tabPanes = document.querySelectorAll('.tab-pane');
@@ -879,7 +963,13 @@
tabButtons.forEach(b => b.classList.remove('active')); tabButtons.forEach(b => b.classList.remove('active'));
tabPanes.forEach(p => p.classList.remove('active')); tabPanes.forEach(p => p.classList.remove('active'));
btn.classList.add('active'); btn.classList.add('active');
document.getElementById(btn.dataset.tab).classList.add('active'); const targetTab = btn.dataset.tab;
document.getElementById(targetTab).classList.add('active');
// Lazy-mount do Designer de Selos
if (targetTab === 'tab-seals' && !pdfSealDesigner) {
initSealDesigner();
}
}); });
}); });
@@ -900,6 +990,29 @@
return false; return false;
} }
// Carregamento de Temas de Selos
async function loadSealThemes() {
try {
const themeSelect = document.getElementById('pdfSealThemeSelect');
const themes = await signer.getSealThemes();
if (themes && themes.length > 0) {
themeSelect.innerHTML = '';
themes.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = `${t.name} (${t.id})`;
themeSelect.appendChild(opt);
});
const customOpt = document.createElement('option');
customOpt.value = 'custom';
customOpt.textContent = '🎨 Selo Customizado do Designer';
themeSelect.appendChild(customOpt);
}
} catch (err) {
console.warn('Não foi possível carregar temas dinâmicos de selos:', err);
}
}
// Carregamento de Certificados // Carregamento de Certificados
async function loadCertificates() { async function loadCertificates() {
try { try {
@@ -913,10 +1026,35 @@
return; return;
} }
certs.forEach((c, index) => { certs.forEach((c) => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = c.id; opt.value = c.id;
opt.textContent = `${c.common_name || c.alias} [${c.type}] (${c.device_name || c.provider_id})`;
let rawName = c.common_name || c.alias || 'Certificado';
let displayName = rawName;
let docNum = c.cpf || c.cnpj || c.cpf_cnpj || '';
if (rawName.includes(':')) {
const parts = rawName.split(':');
displayName = parts[0].trim();
if (!docNum && parts[1]) {
docNum = parts[1].trim();
}
}
const digits = docNum ? String(docNum).replace(/\D/g, '') : '';
let docFormatted = '';
if (digits.length === 14) {
docFormatted = 'CNPJ ' + digits.substring(0, 2) + '.' + digits.substring(2, 5) + '.' + digits.substring(5, 8) + '/' + digits.substring(8, 12) + '-' + digits.substring(12, 14);
} else if (digits.length === 11) {
docFormatted = 'CPF ' + digits.substring(0, 3) + '.' + digits.substring(3, 6) + '.' + digits.substring(6, 9) + '-' + digits.substring(9, 11);
} else if (docNum) {
docFormatted = docNum;
}
const typeStr = '[' + (c.type || 'A1') + ']';
const text = docFormatted ? `${displayName}, ${docFormatted} ${typeStr}` : `${displayName} ${typeStr}`;
opt.textContent = text;
certSelect.appendChild(opt); certSelect.appendChild(opt);
}); });
@@ -937,6 +1075,29 @@
certTypeBadge.textContent = cert.type || 'A1'; certTypeBadge.textContent = cert.type || 'A1';
certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`; certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`;
certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`; certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`;
if (pdfStampPicker) {
const rawDoc = cert.cpf || cert.cnpj || cert.cpf_cnpj || '';
const digits = String(rawDoc).replace(/\D/g, '');
let docStr = '';
if (digits.length === 14) {
docStr = 'CNPJ: ' + digits.substring(0, 2) + '.' + digits.substring(2, 5) + '.' + digits.substring(5, 8) + '/' + digits.substring(8, 12) + '-' + digits.substring(12, 14);
} else if (digits.length === 11) {
docStr = 'CPF: ' + digits.substring(0, 3) + '.' + digits.substring(3, 6) + '.' + digits.substring(6, 9) + '-' + digits.substring(9, 11);
} else {
docStr = cert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil';
}
let cleanSignerName = cert.common_name || cert.alias || 'Titular';
if (cleanSignerName && cleanSignerName.includes(':')) {
cleanSignerName = cleanSignerName.split(':')[0].trim();
}
pdfStampPicker.updateSealTheme(null, {
signerName: cleanSignerName,
signerDoc: docStr
});
}
} else { } else {
certTypeBadge.style.display = 'none'; certTypeBadge.style.display = 'none';
certValidityText.textContent = ''; certValidityText.textContent = '';
@@ -949,10 +1110,24 @@
function updateButtonStates() { function updateButtonStates() {
const hasCert = Boolean(selectedCertificateId); const hasCert = Boolean(selectedCertificateId);
document.getElementById('btnSignPdf').disabled = !hasCert || !currentPdfDigest; const hasPdf = Boolean(currentPdfBytes);
document.getElementById('btnSignXml').disabled = !hasCert || !currentXmlDigest; const hasXml = Boolean(document.getElementById('xmlContentInput').value.trim());
document.getElementById('btnSignGen').disabled = !hasCert || !currentGenDigest;
document.getElementById('btnAuthLogin').disabled = !hasCert; const btnFull = document.getElementById('btnSignPdfFull');
const btnDigest = document.getElementById('btnSignPdfDigest');
if (btnFull) btnFull.disabled = !hasCert || !hasPdf;
if (btnDigest) btnDigest.disabled = !hasCert || !hasPdf;
const btnXmlFull = document.getElementById('btnSignXmlFull');
const btnXmlDigest = document.getElementById('btnSignXmlDigest');
if (btnXmlFull) btnXmlFull.disabled = !hasCert || !hasXml;
if (btnXmlDigest) btnXmlDigest.disabled = !hasCert || !hasXml;
const btnGen = document.getElementById('btnSignGen');
if (btnGen) btnGen.disabled = !hasCert || !currentGenDigest;
const btnAuth = document.getElementById('btnAuthLogin');
if (btnAuth) btnAuth.disabled = !hasCert;
} }
// Conexão e Handshake // Conexão e Handshake
@@ -964,6 +1139,7 @@
statusDot.classList.add('online'); statusDot.classList.add('online');
statusText.textContent = 'Conectado & Autorizado'; statusText.textContent = 'Conectado & Autorizado';
await loadCertificates(); await loadCertificates();
await loadSealThemes();
} catch (err) { } catch (err) {
alert('Erro ao conectar: ' + err.message); alert('Erro ao conectar: ' + err.message);
} finally { } finally {
@@ -976,25 +1152,31 @@
const online = await checkAgentStatus(); const online = await checkAgentStatus();
if (online) { if (online) {
await loadCertificates(); await loadCertificates();
await loadSealThemes();
} }
}); });
// ========================================================================= // =========================================================================
// ABA 1: LÓGICA DE PDF // ABA 1: LÓGICA DE PDF (STAMP PICKER & ASSINATURA BASE64)
// ========================================================================= // =========================================================================
const pdfDropZone = document.getElementById('pdfDropZone'); const pdfDropZone = document.getElementById('pdfDropZone');
const pdfFileInput = document.getElementById('pdfFileInput'); const pdfFileInput = document.getElementById('pdfFileInput');
const pdfFileMeta = document.getElementById('pdfFileMeta'); const pdfFileMeta = document.getElementById('pdfFileMeta');
const pdfFileName = document.getElementById('pdfFileName'); const pdfFileName = document.getElementById('pdfFileName');
const pdfFileSize = document.getElementById('pdfFileSize'); const pdfFileSize = document.getElementById('pdfFileSize');
const pdfDigestBox = document.getElementById('pdfDigestBox'); const stampPickerMount = document.getElementById('stampPickerMount');
const btnSignPdf = document.getElementById('btnSignPdf'); const pdfOptionsSection = document.getElementById('pdfOptionsSection');
const pdfSealThemeSelect = document.getElementById('pdfSealThemeSelect');
const btnSignPdfFull = document.getElementById('btnSignPdfFull');
const btnSignPdfDigest = document.getElementById('btnSignPdfDigest');
const pdfAlert = document.getElementById('pdfAlert'); const pdfAlert = document.getElementById('pdfAlert');
const pdfSigOutput = document.getElementById('pdfSigOutput');
const pdfCertOutput = document.getElementById('pdfCertOutput');
let currentPdfFile = null; const pdfResultCard = document.getElementById('pdfResultCard');
let currentPdfDigest = null; const pdfResultSize = document.getElementById('pdfResultSize');
const pdfResultSha256 = document.getElementById('pdfResultSha256');
const pdfResultBase64 = document.getElementById('pdfResultBase64');
const btnDownloadPdf = document.getElementById('btnDownloadPdf');
const btnCopyPdfBase64 = document.getElementById('btnCopyPdfBase64');
pdfDropZone.addEventListener('click', () => pdfFileInput.click()); pdfDropZone.addEventListener('click', () => pdfFileInput.click());
pdfFileInput.addEventListener('change', e => { pdfFileInput.addEventListener('change', e => {
@@ -1019,17 +1201,130 @@
pdfFileMeta.style.display = 'flex'; pdfFileMeta.style.display = 'flex';
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
currentPdfBytes = buffer;
currentPdfDigest = await signer._calculateSha256(buffer); currentPdfDigest = await signer._calculateSha256(buffer);
pdfDigestBox.textContent = `SHA-256 (Base64): ${currentPdfDigest}\nArquivo: ${file.name} (${file.size} bytes)`;
stampPickerMount.style.display = 'block';
pdfOptionsSection.style.display = 'block';
const activeCert = currentCertificates.find(c => c.id === selectedCertificateId);
const currentThemeVal = pdfSealThemeSelect ? pdfSealThemeSelect.value : 'icp-brasil';
const initialTheme = (currentThemeVal === 'custom' && currentCustomSealConfig)
? currentCustomSealConfig
: currentThemeVal;
const rawCertName = activeCert ? (activeCert.common_name || activeCert.alias) : '';
const cleanCertName = rawCertName.includes(':') ? rawCertName.split(':')[0].trim() : rawCertName;
const initialSignerInfo = activeCert ? {
signerName: cleanCertName,
signerDoc: activeCert.cpf_cnpj ? `CPF: ${activeCert.cpf_cnpj}` : (activeCert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil')
} : {
signerName: 'Nome do Titular do Certificado',
signerDoc: 'CPF: ***.456.789-**'
};
if (!pdfStampPicker) {
pdfStampPicker = new PdfStampPicker({
container: stampPickerMount,
pdf: buffer,
sealTheme: initialTheme,
signerInfo: initialSignerInfo,
initialScale: 1.0,
onPositionChange: (pos) => {
console.log('Posição do selo atualizada:', pos);
},
onSignRequest: () => {
btnSignPdfFull.click();
}
});
} else {
pdfStampPicker.updateSealTheme(initialTheme, initialSignerInfo);
pdfStampPicker.loadPdf(buffer);
}
updateButtonStates(); updateButtonStates();
} }
btnSignPdf.addEventListener('click', async () => { // Sincroniza alteração de tema com o selo sobre o PDF
btnSignPdf.disabled = true; pdfSealThemeSelect.addEventListener('change', () => {
btnSignPdf.innerHTML = '<span class="spinner"></span> Solicitando no Agente...'; const themeVal = pdfSealThemeSelect.value;
if (themeVal === 'custom') {
if (!currentCustomSealConfig && typeof pdfSealDesigner !== 'undefined' && pdfSealDesigner) {
currentCustomSealConfig = pdfSealDesigner.getConfig();
}
if (pdfStampPicker && currentCustomSealConfig) {
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
}
} else {
if (pdfStampPicker) {
pdfStampPicker.updateSealTheme(themeVal);
}
}
});
// Assinatura Completa de PDF (Devolve PDF Assinado Base64)
btnSignPdfFull.addEventListener('click', async () => {
btnSignPdfFull.disabled = true;
btnSignPdfFull.innerHTML = '<span class="spinner"></span> Assinando no Agente...';
pdfAlert.style.display = 'block'; pdfAlert.style.display = 'block';
pdfAlert.className = 'alert alert-info'; pdfAlert.className = 'alert alert-info';
pdfAlert.textContent = 'Confirme a assinatura na janela do CyberCore Signer aberta no seu computador...'; pdfAlert.textContent = 'Confirme a assinatura na janela do CyberCore Signer...';
try {
const stampConfig = pdfStampPicker ? pdfStampPicker.getStampConfig() : { page: 1, x: 50, y: 50, scale: 1.0 };
const selectedTheme = pdfSealThemeSelect.value;
let effectiveCustomSeal = null;
if (selectedTheme === 'custom') {
if (currentCustomSealConfig) {
effectiveCustomSeal = currentCustomSealConfig;
} else if (typeof pdfSealDesigner !== 'undefined' && pdfSealDesigner) {
effectiveCustomSeal = pdfSealDesigner.getConfig();
currentCustomSealConfig = effectiveCustomSeal;
}
}
const signParams = {
certificateId: selectedCertificateId,
pdf: currentPdfFile || currentPdfBytes,
page: stampConfig.page,
x: stampConfig.x,
y: stampConfig.y,
scale: stampConfig.scale,
sealCustom: effectiveCustomSeal,
seal_custom: effectiveCustomSeal,
sealThemeId: (selectedTheme === 'custom' ? null : selectedTheme),
seal_theme_id: (selectedTheme === 'custom' ? null : selectedTheme)
};
const result = await signer.signPdfDocument(signParams);
pdfAlert.className = 'alert alert-success';
pdfAlert.textContent = '✓ Documento PDF assinado com sucesso via PAdES (ISO 32000)!';
const signedPdfBase64 = result.signedPdf || result.signed_pdf || '';
currentSignedPdfBase64 = signedPdfBase64;
pdfResultCard.style.display = 'block';
pdfResultSize.textContent = (signedPdfBase64.length * 0.75 / 1024).toFixed(1) + ' KB';
pdfResultSha256.textContent = result.sha256 || 'Calculado com sucesso';
pdfResultBase64.textContent = signedPdfBase64;
pdfResultCard.scrollIntoView({ behavior: 'smooth' });
} catch (err) {
pdfAlert.className = 'alert alert-error';
pdfAlert.textContent = 'Erro ao assinar PDF: ' + err.message;
} finally {
btnSignPdfFull.disabled = false;
btnSignPdfFull.innerHTML = '<span>✍️</span> Assinar PDF Completo no Agente (Devolve Base64)';
}
});
// Assinatura via Digest (Fluxo Tradicional)
btnSignPdfDigest.addEventListener('click', async () => {
btnSignPdfDigest.disabled = true;
btnSignPdfDigest.innerHTML = '<span class="spinner"></span> Solicitando...';
pdfAlert.style.display = 'block';
pdfAlert.className = 'alert alert-info';
pdfAlert.textContent = 'Assinando digest do PDF no CyberCore Signer...';
try { try {
const result = await signer.signPdf(currentPdfFile, { const result = await signer.signPdf(currentPdfFile, {
@@ -1038,30 +1333,102 @@
}); });
pdfAlert.className = 'alert alert-success'; pdfAlert.className = 'alert alert-success';
pdfAlert.textContent = `✓ Documento PDF assinado com sucesso via ${result.algorithm}!`; pdfAlert.textContent = `✓ Digest assinado com sucesso! Algoritmo: ${result.algorithm}`;
pdfSigOutput.textContent = result.signature;
pdfCertOutput.textContent = result.certificate;
} catch (err) { } catch (err) {
pdfAlert.className = 'alert alert-error'; pdfAlert.className = 'alert alert-error';
pdfAlert.textContent = 'Erro ao assinar PDF: ' + err.message; pdfAlert.textContent = 'Erro ao assinar digest: ' + err.message;
} finally { } finally {
btnSignPdf.disabled = false; btnSignPdfDigest.disabled = false;
btnSignPdf.innerHTML = '<span>✍️</span> Assinar PDF com CyberCore'; btnSignPdfDigest.innerHTML = '<span>🛡️</span> Assinar Apenas Digest (PAdES Remoto)';
} }
}); });
// Download do PDF Assinado
btnDownloadPdf.addEventListener('click', () => {
if (!currentSignedPdfBase64) return;
const blob = signer._base64ToBlob(currentSignedPdfBase64, 'application/pdf');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const origName = currentPdfFile ? currentPdfFile.name.replace(/\.pdf$/i, '') : 'documento';
a.href = url;
a.download = `${origName}_assinado.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Cópia do Base64 do PDF
btnCopyPdfBase64.addEventListener('click', async () => {
if (!currentSignedPdfBase64) return;
await navigator.clipboard.writeText(currentSignedPdfBase64);
const origText = btnCopyPdfBase64.innerHTML;
btnCopyPdfBase64.innerHTML = '<span>✓</span> Copiado!';
setTimeout(() => { btnCopyPdfBase64.innerHTML = origText; }, 2000);
});
// ========================================================================= // =========================================================================
// ABA 2: LÓGICA DE XML // ABA 2: DESIGNER DE SELOS VISUAIS
// =========================================================================
function initSealDesigner() {
const mount = document.getElementById('sealDesignerMount');
pdfSealDesigner = new PdfSealDesigner({
container: mount,
initialTheme: {
name: 'Selo Customizado',
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE',
primaryColor: '#0284c7',
secondaryColor: '#10b981',
backgroundColor: '#ffffff',
textColor: '#0f172a',
borderColor: '#38bdf8',
iconStyle: 'shield',
defaultCpfMode: 'MASKED',
defaultIncludeDateTime: true
},
onChange: (cfg) => {
currentCustomSealConfig = cfg;
}
});
// Inicializa currentCustomSealConfig com o estado atual do designer
currentCustomSealConfig = pdfSealDesigner.getConfig();
// Botão para aplicar o selo na aba de PDF
const applyBtnWrap = document.createElement('div');
applyBtnWrap.style.marginTop = '16px';
applyBtnWrap.style.display = 'flex';
applyBtnWrap.style.gap = '10px';
applyBtnWrap.innerHTML = `
<button id="btnApplySealToPdf" class="btn btn-primary" style="padding: 10px 18px; font-size: 13px;">
<span>🚀</span> Usar este Selo no Posicionador de PDF
</button>
`;
mount.appendChild(applyBtnWrap);
document.getElementById('btnApplySealToPdf').addEventListener('click', () => {
currentCustomSealConfig = pdfSealDesigner.getConfig();
pdfSealThemeSelect.value = 'custom';
if (pdfStampPicker) {
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
}
// Alterna para aba PDF
document.querySelector('[data-tab="tab-pdf"]').click();
alert('Selo customizado selecionado e aplicado ao posicionador de PDF!');
});
}
// =========================================================================
// ABA 3: LÓGICA DE XML (XAdES-BES & XMLDSig)
// ========================================================================= // =========================================================================
const xmlContentInput = document.getElementById('xmlContentInput'); const xmlContentInput = document.getElementById('xmlContentInput');
const xmlDigestBox = document.getElementById('xmlDigestBox'); const xmlProfileSelect = document.getElementById('xmlProfileSelect');
const btnSignXml = document.getElementById('btnSignXml'); const btnSignXmlFull = document.getElementById('btnSignXmlFull');
const btnSignXmlDigest = document.getElementById('btnSignXmlDigest');
const btnLoadXmlSample = document.getElementById('btnLoadXmlSample'); const btnLoadXmlSample = document.getElementById('btnLoadXmlSample');
const xmlAlert = document.getElementById('xmlAlert'); const xmlAlert = document.getElementById('xmlAlert');
const xmlSigOutput = document.getElementById('xmlSigOutput'); const xmlSigOutput = document.getElementById('xmlSigOutput');
const xmlTagOutput = document.getElementById('xmlTagOutput'); const btnDownloadXml = document.getElementById('btnDownloadXml');
const btnCopyXml = document.getElementById('btnCopyXml');
let currentXmlDigest = null;
const sampleXml = `<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe"> const sampleXml = `<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
<NFe> <NFe>
@@ -1083,30 +1450,52 @@
</NFe> </NFe>
</nfeProc>`; </nfeProc>`;
async function updateXmlDigest() { xmlContentInput.addEventListener('input', () => updateButtonStates());
const xml = xmlContentInput.value.trim();
if (!xml) {
currentXmlDigest = null;
xmlDigestBox.textContent = 'Digite ou cole o XML para calcular o digest...';
} else {
currentXmlDigest = await signer._calculateSha256(xml);
xmlDigestBox.textContent = `SHA-256 Digest (Base64): ${currentXmlDigest}\nTamanho: ${xml.length} caracteres`;
}
updateButtonStates();
}
xmlContentInput.addEventListener('input', updateXmlDigest);
btnLoadXmlSample.addEventListener('click', () => { btnLoadXmlSample.addEventListener('click', () => {
xmlContentInput.value = sampleXml; xmlContentInput.value = sampleXml;
updateXmlDigest(); updateButtonStates();
}); });
btnSignXml.addEventListener('click', async () => { // Assinatura Completa de XML
btnSignXml.disabled = true; btnSignXmlFull.addEventListener('click', async () => {
btnSignXml.innerHTML = '<span class="spinner"></span> Solicitando no Agente...'; btnSignXmlFull.disabled = true;
btnSignXmlFull.innerHTML = '<span class="spinner"></span> Assinando no Agente...';
xmlAlert.style.display = 'block'; xmlAlert.style.display = 'block';
xmlAlert.className = 'alert alert-info'; xmlAlert.className = 'alert alert-info';
xmlAlert.textContent = 'Confirme a assinatura na janela do CyberCore Signer...'; xmlAlert.textContent = 'Assinando XML no CyberCore Signer...';
try {
const xml = xmlContentInput.value.trim();
const profile = xmlProfileSelect.value;
const result = await signer.signXmlDocument({
certificateId: selectedCertificateId,
xml: xml,
profile: profile
});
xmlAlert.className = 'alert alert-success';
xmlAlert.textContent = `✓ XML assinado com sucesso via perfil ${result.profile || profile}!`;
currentSignedXml = result.signed_xml;
xmlSigOutput.textContent = result.signed_xml;
btnDownloadXml.style.display = 'inline-flex';
btnCopyXml.style.display = 'inline-flex';
} catch (err) {
xmlAlert.className = 'alert alert-error';
xmlAlert.textContent = 'Erro ao assinar XML: ' + err.message;
} finally {
btnSignXmlFull.disabled = false;
btnSignXmlFull.innerHTML = '<span>✍️</span> Assinar XML Completo no Agente';
}
});
// Assinatura via Digest de XML
btnSignXmlDigest.addEventListener('click', async () => {
btnSignXmlDigest.disabled = true;
btnSignXmlDigest.innerHTML = '<span class="spinner"></span> Solicitando...';
xmlAlert.style.display = 'block';
xmlAlert.className = 'alert alert-info';
xmlAlert.textContent = 'Assinando digest do XML no CyberCore Signer...';
try { try {
const result = await signer.signXml(xmlContentInput.value, { const result = await signer.signXml(xmlContentInput.value, {
@@ -1115,33 +1504,40 @@
}); });
xmlAlert.className = 'alert alert-success'; xmlAlert.className = 'alert alert-success';
xmlAlert.textContent = `✓ XML assinado com sucesso via ${result.algorithm}!`; xmlAlert.textContent = `✓ Digest XML assinado com sucesso via ${result.algorithm}!`;
xmlSigOutput.textContent = result.signature; xmlSigOutput.textContent = result.signature;
const xmlSigSnippet = `<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#NFe35261012345678000199550010000001001234567890">
<DigestValue>${result.digest}</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>${result.signature.slice(0, 48)}...</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>${result.certificate.slice(0, 48)}...</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>`;
xmlTagOutput.textContent = xmlSigSnippet;
} catch (err) { } catch (err) {
xmlAlert.className = 'alert alert-error'; xmlAlert.className = 'alert alert-error';
xmlAlert.textContent = 'Erro ao assinar XML: ' + err.message; xmlAlert.textContent = 'Erro ao assinar digest do XML: ' + err.message;
} finally { } finally {
btnSignXml.disabled = false; btnSignXmlDigest.disabled = false;
btnSignXml.innerHTML = '<span>✍️</span> Assinar XML com CyberCore'; btnSignXmlDigest.innerHTML = '<span>🛡️</span> Apenas Digest';
} }
}); });
// Download do XML Assinado
btnDownloadXml.addEventListener('click', () => {
if (!currentSignedXml) return;
const blob = new Blob([currentSignedXml], { type: 'application/xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'documento_assinado.xml';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Copiar XML
btnCopyXml.addEventListener('click', async () => {
if (!currentSignedXml) return;
await navigator.clipboard.writeText(currentSignedXml);
const origText = btnCopyXml.innerHTML;
btnCopyXml.innerHTML = '<span>✓</span> Copiado!';
setTimeout(() => { btnCopyXml.innerHTML = origText; }, 2000);
});
// ========================================================================= // =========================================================================
// ABA 3: LÓGICA DE ARQUIVOS GERAIS // ABA 3: LÓGICA DE ARQUIVOS GERAIS
// ========================================================================= // =========================================================================
+705
View File
@@ -0,0 +1,705 @@
/**
* CyberCore Signer - PdfSealDesigner
* Ferramenta visual interativa para criação, personalização e exportação
* de temas de selos de assinatura digital PAdES (ICP-Brasil).
*/
export class PdfSealDesigner {
/**
* @param {Object} options Configurações iniciais do designer.
* @param {HTMLElement|string} options.container Elemento onde renderizar o designer.
* @param {Object} [options.initialTheme] Configuração inicial de tema.
* @param {Function} [options.onChange] Callback chamado quando o tema for modificado: fn(theme).
*/
constructor(options = {}) {
this.container = typeof options.container === 'string'
? document.querySelector(options.container)
: options.container;
if (!this.container) {
throw new Error('Container DOM obrigatório para instanciar PdfSealDesigner.');
}
this.onChange = options.onChange || options.onConfigChange || null;
const rawInitial = options.initialTheme || options.initialConfig || {};
const normalizedInitial = {
name: rawInitial.name,
description: rawInitial.description,
width: rawInitial.width,
height: rawInitial.height,
primaryColor: rawInitial.primaryColor || rawInitial.primary_color,
secondaryColor: rawInitial.secondaryColor || rawInitial.secondary_color,
backgroundColor: rawInitial.backgroundColor || rawInitial.background_color,
borderColor: rawInitial.borderColor || rawInitial.border_color,
textColor: rawInitial.textColor || rawInitial.text_color,
iconStyle: rawInitial.iconStyle || rawInitial.icon_style,
customLogoBase64: rawInitial.customLogoBase64 || rawInitial.custom_logo_base64,
customHeader: rawInitial.customHeader || rawInitial.custom_header || rawInitial.title,
customFooter: rawInitial.customFooter || rawInitial.custom_footer,
customFieldLabel: rawInitial.customFieldLabel || rawInitial.custom_field_label,
customFieldValue: rawInitial.customFieldValue || rawInitial.custom_field_value,
defaultCpfMode: rawInitial.defaultCpfMode || rawInitial.default_cpf_mode,
defaultIncludeDateTime: rawInitial.defaultIncludeDateTime !== undefined ? rawInitial.defaultIncludeDateTime : rawInitial.default_include_date_time
};
// Remove chaves indefinidas para não sobrescrever os defaults
Object.keys(normalizedInitial).forEach(k => normalizedInitial[k] === undefined && delete normalizedInitial[k]);
// Estado do tema
this.theme = {
name: 'Meu Selo Personalizado',
description: 'Selo visual corporativo customizado via CyberCore Web SDK',
width: 320,
height: 82,
primaryColor: '#0d47a1',
secondaryColor: '#0284c7',
backgroundColor: '#ffffff',
borderColor: '#93c5fd',
textColor: '#0f172a',
iconStyle: 'shield',
customLogoBase64: null,
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE',
customFooter: 'Verifique a autenticidade da assinatura eletrônica em https://validar.iti.gov.br',
customFieldLabel: '',
customFieldValue: '',
defaultCpfMode: 'MASKED',
defaultIncludeDateTime: true,
...normalizedInitial
};
this._initDom();
this._attachEvents();
this._renderPreview();
}
_initDom() {
this.container.innerHTML = `
<div class="cyber-designer-root">
<!-- Cabeçalho -->
<div class="designer-header">
<h3>🎨 Criador de Selo Visual (PAdES / ICP-Brasil)</h3>
<p>Personalize cores, brasão, logotipo próprio e regras LGPD para a estampa no PDF.</p>
</div>
<div class="designer-body">
<!-- Coluna 1: Formulário de Configuração -->
<div class="designer-form">
<div class="form-row">
<label>Nome do Selo:</label>
<input type="text" class="input-theme-name" value="${this.theme.name}" />
</div>
<div class="form-section-title">Paleta de Cores</div>
<div class="colors-grid">
<div class="color-picker-item">
<label>Primária:</label>
<div class="color-input-wrap">
<input type="color" class="color-primary" value="${this.theme.primaryColor}" />
<span class="color-hex-text">${this.theme.primaryColor}</span>
</div>
</div>
<div class="color-picker-item">
<label>Secundária:</label>
<div class="color-input-wrap">
<input type="color" class="color-secondary" value="${this.theme.secondaryColor}" />
<span class="color-hex-text">${this.theme.secondaryColor}</span>
</div>
</div>
<div class="color-picker-item">
<label>Fundo:</label>
<div class="color-input-wrap">
<input type="color" class="color-bg" value="${this.theme.backgroundColor}" />
<span class="color-hex-text">${this.theme.backgroundColor}</span>
</div>
</div>
<div class="color-picker-item">
<label>Borda:</label>
<div class="color-input-wrap">
<input type="color" class="color-border" value="${this.theme.borderColor}" />
<span class="color-hex-text">${this.theme.borderColor}</span>
</div>
</div>
<div class="color-picker-item">
<label>Texto:</label>
<div class="color-input-wrap">
<input type="color" class="color-text" value="${this.theme.textColor}" />
<span class="color-hex-text">${this.theme.textColor}</span>
</div>
</div>
</div>
<div class="form-section-title">Ícone e Emblema</div>
<div class="icon-selection-row">
<label class="radio-icon-item">
<input type="radio" name="iconStyle" value="shield" ${this.theme.iconStyle === 'shield' ? 'checked' : ''} />
<span>🛡️ Escudo ICP</span>
</label>
<label class="radio-icon-item">
<input type="radio" name="iconStyle" value="star" ${this.theme.iconStyle === 'star' ? 'checked' : ''} />
<span>⭐ Gov.br</span>
</label>
<label class="radio-icon-item">
<input type="radio" name="iconStyle" value="crystal" ${this.theme.iconStyle === 'crystal' ? 'checked' : ''} />
<span>💎 Modern</span>
</label>
<label class="radio-icon-item">
<input type="radio" name="iconStyle" value="notary" ${this.theme.iconStyle === 'notary' ? 'checked' : ''} />
<span>📜 Notarial</span>
</label>
</div>
<div class="form-row logo-upload-row">
<label>Logotipo Próprio (PNG/JPG):</label>
<div class="logo-actions">
<label class="btn-designer-upload">
<span>📷</span> Escolher Imagem
<input type="file" accept="image/png,image/jpeg,image/svg+xml" class="designer-logo-file" style="display:none;" />
</label>
<button type="button" class="btn-remove-logo" style="${this.theme.customLogoBase64 ? 'display:inline-block;' : 'display:none;'}">Remover</button>
</div>
</div>
<div class="form-section-title">Identificação & LGPD</div>
<div class="form-row-grid">
<div class="form-row">
<label>Rótulo Extra:</label>
<input type="text" class="input-field-label" placeholder="Ex: Cargo ou Matrícula" value="${this.theme.customFieldLabel || ''}" />
</div>
<div class="form-row">
<label>Valor Extra:</label>
<input type="text" class="input-field-value" placeholder="Ex: Auditor Fiscal" value="${this.theme.customFieldValue || ''}" />
</div>
</div>
<div class="form-row-grid">
<div class="form-row">
<label>Exibição do CPF:</label>
<select class="select-cpf-mode">
<option value="FULL" ${this.theme.defaultCpfMode === 'FULL' ? 'selected' : ''}>Completo (123.456.789-00)</option>
<option value="MASKED" ${this.theme.defaultCpfMode === 'MASKED' ? 'selected' : ''}>Mascarado (***.456.789-**)</option>
<option value="NONE" ${this.theme.defaultCpfMode === 'NONE' ? 'selected' : ''}>Ocultar CPF</option>
</select>
</div>
<div class="form-row checkbox-row">
<label class="checkbox-label">
<input type="checkbox" class="chk-date-time" ${this.theme.defaultIncludeDateTime ? 'checked' : ''} />
<span>Incluir Data e Hora</span>
</label>
</div>
</div>
</div>
<!-- Coluna 2: Live Preview e Exportação -->
<div class="designer-preview-column">
<div class="preview-card">
<h4>👁️ Pré-visualização em Alta Resolução (320x82 pt)</h4>
<div class="seal-live-preview-box">
<!-- SVG Dinâmico -->
</div>
<small class="preview-note">Renderização visual idêntica à estampada no PDFBox do agente desktop.</small>
</div>
<div class="json-export-card">
<div class="json-export-header">
<h4>📋 Objeto de Configuração (JSON)</h4>
<button type="button" class="btn-copy-theme-json">Copiar JSON</button>
</div>
<pre class="theme-json-box"><code>{}</code></pre>
</div>
</div>
</div>
</div>
`;
this._injectStyles();
// Cache de elementos
this.root = this.container.querySelector('.cyber-designer-root');
this.previewBox = this.root.querySelector('.seal-live-preview-box');
this.jsonBox = this.root.querySelector('.theme-json-box code');
this.btnRemoveLogo = this.root.querySelector('.btn-remove-logo');
}
_injectStyles() {
if (document.getElementById('cyber-designer-styles')) return;
const style = document.createElement('style');
style.id = 'cyber-designer-styles';
style.textContent = `
.cyber-designer-root {
background: #0f172a;
border: 1px solid #24304f;
border-radius: 12px;
padding: 20px;
color: #f8fafc;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.designer-header {
margin-bottom: 20px;
border-bottom: 1px solid #1e293b;
padding-bottom: 12px;
}
.designer-header h3 {
font-size: 16px;
font-weight: 700;
color: #38bdf8;
}
.designer-header p {
font-size: 12px;
color: #94a3b8;
margin-top: 4px;
}
.designer-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 24px;
min-width: 0;
}
@media (max-width: 860px) {
.designer-body {
grid-template-columns: minmax(0, 1fr);
}
}
.form-section-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #38bdf8;
margin: 14px 0 8px 0;
border-top: 1px solid #1e293b;
padding-top: 10px;
}
.form-row {
margin-bottom: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.form-row label {
font-size: 11px;
color: #cbd5e1;
}
.form-row input[type="text"], .form-row select {
background: #090d16;
border: 1px solid #24304f;
border-radius: 6px;
padding: 7px 10px;
color: #f8fafc;
font-size: 12px;
}
.form-row input[type="text"]:focus, .form-row select:focus {
border-color: #38bdf8;
outline: none;
}
.form-row-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.colors-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.color-picker-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.color-picker-item label {
font-size: 10px;
color: #94a3b8;
}
.color-input-wrap {
display: flex;
align-items: center;
gap: 6px;
background: #090d16;
border: 1px solid #24304f;
border-radius: 6px;
padding: 3px 6px;
}
.color-input-wrap input[type="color"] {
border: none;
width: 22px;
height: 22px;
background: transparent;
cursor: pointer;
}
.color-hex-text {
font-size: 10px;
font-family: monospace;
color: #cbd5e1;
}
.icon-selection-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.radio-icon-item {
display: flex;
align-items: center;
gap: 6px;
background: #090d16;
border: 1px solid #24304f;
border-radius: 6px;
padding: 6px 10px;
font-size: 11px;
cursor: pointer;
}
.logo-upload-row .logo-actions {
display: flex;
align-items: center;
gap: 8px;
}
.btn-designer-upload {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
background: #1e293b;
border: 1px solid #38bdf8;
border-radius: 6px;
color: #38bdf8;
font-size: 11px;
cursor: pointer;
}
.btn-remove-logo {
background: transparent;
border: 1px solid #f87171;
color: #f87171;
padding: 4px 8px;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
}
.checkbox-row {
justify-content: flex-end;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
cursor: pointer;
margin-top: 18px;
}
.designer-preview-column {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
max-width: 100%;
}
.preview-card, .json-export-card {
background: #131c31;
border: 1px solid #24304f;
border-radius: 10px;
padding: 16px;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.preview-card h4, .json-export-card h4 {
font-size: 12px;
font-weight: 700;
color: #cbd5e1;
margin-bottom: 12px;
}
.seal-live-preview-box {
background: #ffffff;
border-radius: 6px;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
max-width: 340px;
margin: 0 auto;
box-sizing: border-box;
overflow: hidden;
}
.seal-live-preview-box svg {
max-width: 100%;
width: 100%;
height: auto;
display: block;
}
.preview-note {
display: block;
font-size: 10px;
color: #64748b;
margin-top: 8px;
text-align: center;
}
.json-export-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.btn-copy-theme-json {
background: #1e293b;
border: 1px solid #24304f;
color: #38bdf8;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
}
.btn-copy-theme-json:hover {
background: #38bdf8;
color: #0f172a;
}
.theme-json-box {
background: #090d16;
border: 1px solid #1e293b;
border-radius: 6px;
padding: 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 10.5px;
color: #38bdf8;
max-height: 180px;
overflow-y: auto;
overflow-x: hidden;
margin: 0;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
}
.theme-json-box code {
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
display: block;
max-width: 100%;
}
`;
document.head.appendChild(style);
}
_attachEvents() {
const bindInput = (selector, key, formatFn = v => v) => {
this.root.querySelector(selector).addEventListener('input', e => {
this.theme[key] = formatFn(e.target.value);
this._onStateChange();
});
};
bindInput('.input-theme-name', 'name');
bindInput('.input-field-label', 'customFieldLabel');
bindInput('.input-field-value', 'customFieldValue');
// Cores
const bindColor = (selector, key, labelSelector) => {
const el = this.root.querySelector(selector);
const label = this.root.querySelector(labelSelector);
el.addEventListener('input', e => {
this.theme[key] = e.target.value;
label.textContent = e.target.value;
this._onStateChange();
});
};
bindColor('.color-primary', 'primaryColor', '.color-primary + .color-hex-text');
bindColor('.color-secondary', 'secondaryColor', '.color-secondary + .color-hex-text');
bindColor('.color-bg', 'backgroundColor', '.color-bg + .color-hex-text');
bindColor('.color-border', 'borderColor', '.color-border + .color-hex-text');
bindColor('.color-text', 'textColor', '.color-text + .color-hex-text');
// Ícones
this.root.querySelectorAll('input[name="iconStyle"]').forEach(radio => {
radio.addEventListener('change', e => {
if (e.target.checked) {
this.theme.iconStyle = e.target.value;
this._onStateChange();
}
});
});
// Upload de logo
const logoInput = this.root.querySelector('.designer-logo-file');
logoInput.addEventListener('change', async e => {
if (e.target.files.length) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = ev => {
this.theme.customLogoBase64 = ev.target.result;
this.btnRemoveLogo.style.display = 'inline-block';
this._onStateChange();
};
reader.readAsDataURL(file);
}
});
this.btnRemoveLogo.addEventListener('click', () => {
this.theme.customLogoBase64 = null;
this.btnRemoveLogo.style.display = 'none';
logoInput.value = '';
this._onStateChange();
});
// LGPD CPF
this.root.querySelector('.select-cpf-mode').addEventListener('change', e => {
this.theme.defaultCpfMode = e.target.value;
this._onStateChange();
});
// Data e hora
this.root.querySelector('.chk-date-time').addEventListener('change', e => {
this.theme.defaultIncludeDateTime = e.target.checked;
this._onStateChange();
});
// Copiar JSON
this.root.querySelector('.btn-copy-theme-json').addEventListener('click', () => {
const config = this.getThemeConfig();
navigator.clipboard.writeText(JSON.stringify(config, null, 2));
const btn = this.root.querySelector('.btn-copy-theme-json');
btn.textContent = '✓ Copiado!';
setTimeout(() => (btn.textContent = 'Copiar JSON'), 1500);
});
}
_onStateChange() {
this._renderPreview();
if (this.onChange) {
this.onChange(this.getThemeConfig());
}
}
_renderPreview() {
const t = this.theme;
// CPF conforme a escolha
let docText = 'CPF: ***.456.789-**';
if (t.defaultCpfMode === 'FULL') docText = 'CPF: 123.456.789-00';
else if (t.defaultCpfMode === 'NONE') docText = '';
const dateText = t.defaultIncludeDateTime ? '11/10/2026 18:30:00' : '';
let extraText = '';
if (t.customFieldLabel || t.customFieldValue) {
const label = t.customFieldLabel ? `${t.customFieldLabel}: ` : '';
const val = t.customFieldValue || '';
extraText = `${label}${val}`.trim();
}
let iconGraphic = '';
if (t.customLogoBase64) {
iconGraphic = `<image xlink:href="${t.customLogoBase64}" href="${t.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
} else {
switch (t.iconStyle) {
case 'star':
iconGraphic = `
<circle cx="28" cy="30" r="15" fill="${t.primaryColor}" opacity="0.12" />
<circle cx="28" cy="30" r="15" fill="none" stroke="${t.primaryColor}" stroke-width="1.5" />
<path d="M28 18 L31 26 L39 26 L33 31 L35 39 L28 34 L21 39 L23 31 L17 26 L25 26 Z" fill="${t.secondaryColor}" />
`;
break;
case 'crystal':
iconGraphic = `
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="${t.primaryColor}" opacity="0.15" />
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="none" stroke="${t.primaryColor}" stroke-width="1.5" />
<line x1="28" y1="14" x2="28" y2="46" stroke="${t.secondaryColor}" stroke-width="1.5" />
`;
break;
case 'notary':
iconGraphic = `
<circle cx="28" cy="30" r="16" fill="none" stroke="${t.primaryColor}" stroke-width="2" stroke-dasharray="3,2" />
<circle cx="28" cy="30" r="12" fill="${t.primaryColor}" opacity="0.1" />
<path d="M22 30 L26 34 L34 26" fill="none" stroke="${t.secondaryColor}" stroke-width="2" stroke-linecap="round" />
`;
break;
case 'shield':
default:
iconGraphic = `
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="${t.primaryColor}" opacity="0.12" />
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="none" stroke="${t.primaryColor}" stroke-width="1.8" />
<rect x="23" y="27" width="10" height="8" rx="2" fill="${t.secondaryColor}" />
<path d="M25 27 V23.5 C25 21.8 26.3 20.5 28 20.5 C29.7 20.5 31 21.8 31 23.5 V27" fill="none" stroke="${t.secondaryColor}" stroke-width="1.5" />
`;
break;
}
}
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320 82" width="320" height="82" style="border-radius:6px; background:${t.backgroundColor};">
<rect x="1" y="1" width="318" height="80" rx="6" fill="${t.backgroundColor}" stroke="${t.borderColor}" stroke-width="1.5"/>
<rect x="1" y="1" width="6" height="80" rx="3" fill="${t.primaryColor}"/>
<!-- Emblema / Brasão -->
<g id="seal-icon-group">
${iconGraphic}
</g>
<!-- Tipografia do Selo -->
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${dateText ? `Data: ${dateText}` : ''}</text>
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
</svg>
`;
this.previewBox.innerHTML = svg;
// Na visualização do código, trunca se o Base64 for gigante para preservar performance da UI
const displayConfig = { ...this.getThemeConfig() };
if (displayConfig.customLogoBase64 && displayConfig.customLogoBase64.length > 100) {
const lenKb = (displayConfig.customLogoBase64.length / 1024).toFixed(1);
displayConfig.customLogoBase64 = `${displayConfig.customLogoBase64.substring(0, 32)}... [${lenKb} KB]`;
}
this.jsonBox.textContent = JSON.stringify(displayConfig, null, 2);
}
/**
* Retorna a configuração do tema pronto para uso no payload seal_custom de assinatura.
*/
getThemeConfig() {
return {
name: this.theme.name,
description: this.theme.description,
width: this.theme.width,
height: this.theme.height,
primaryColor: this.theme.primaryColor,
secondaryColor: this.theme.secondaryColor,
backgroundColor: this.theme.backgroundColor,
borderColor: this.theme.borderColor,
textColor: this.theme.textColor,
iconStyle: this.theme.iconStyle,
customLogoBase64: this.theme.customLogoBase64,
customHeader: this.theme.customHeader,
customFooter: this.theme.customFooter,
customFieldLabel: this.theme.customFieldLabel,
customFieldValue: this.theme.customFieldValue,
defaultCpfMode: this.theme.defaultCpfMode,
defaultIncludeDateTime: this.theme.defaultIncludeDateTime
};
}
/**
* Alias conveniente para getThemeConfig().
*/
getConfig() {
return this.getThemeConfig();
}
/**
* Carrega um tema para edição.
*/
loadTheme(theme) {
this.theme = { ...this.theme, ...theme };
this._initDom();
this._attachEvents();
this._renderPreview();
}
}
export default PdfSealDesigner;
File diff suppressed because it is too large Load Diff
+383 -1
View File
@@ -16,6 +16,11 @@ import {
ReplayDetectedError ReplayDetectedError
} from './errors.js'; } from './errors.js';
import { PdfStampPicker } from './pdf-stamp-picker.js';
import { PdfSealDesigner } from './pdf-seal-designer.js';
export { PdfStampPicker, PdfSealDesigner };
export class CyberCoreSigner { export class CyberCoreSigner {
/** /**
* Inicializa o cliente do SDK. * Inicializa o cliente do SDK.
@@ -284,6 +289,286 @@ export class CyberCoreSigner {
}); });
} }
/**
* Obtém a lista de temas de selos visuais disponíveis e o selo padrão ativo no agente.
*
* @returns {Promise<Object>} Objeto com a lista de selos e default_seal_id.
*/
async getSealThemes() {
const resp = await this._fetch('/ccsp/v1/seals', { method: 'GET' });
return resp;
}
/**
* Assina um documento PDF completo no padrão PAdES com carimbo/selo visual estilizado.
* O PDF original pode ser fornecido como string Base64, File, Blob, ArrayBuffer ou Uint8Array.
* O CyberCore Signer aplica a assinatura incremental com estampa gráfica e retorna o PDF assinado completo em Base64.
*
* @param {Object} params Parâmetros da assinatura do documento PDF.
* @param {string|File|Blob|ArrayBuffer|Uint8Array} params.pdf PDF a ser assinado (Base64 ou arquivo binário).
* @param {string} params.certificateId Identificador ou hint do certificado digital A1/A3.
* @param {string} [params.documentName='documento.pdf'] Nome amigável do documento.
* @param {number} [params.page] Número da página (1-indexado) onde o selo deve ser posicionado.
* @param {string} [params.pageChoice='LAST_PAGE'] 'LAST_PAGE', 'FIRST_PAGE' ou null se page numérica for informada.
* @param {string} [params.position='CUSTOM'] 'BOTTOM_RIGHT', 'BOTTOM_LEFT', 'TOP_RIGHT', 'TOP_LEFT', 'CUSTOM'.
* @param {number} [params.x] Coordenada X em pontos tipográficos na página do PDF.
* @param {number} [params.y] Coordenada Y em pontos tipográficos na página do PDF.
* @param {number} [params.scale=1.0] Escala do selo (ex: 1.0 = 100%, 0.8 = 80%, 1.2 = 120%).
* @param {boolean} [params.visibleSeal=true] Se false, a assinatura PAdES não inclui estampa gráfica visível.
* @param {string} [params.sealThemeId] ID do tema do selo registrado no agente (ex: 'icp-brasil', 'gov-br').
* @param {Object} [params.sealCustom] Objeto de tema customizado inline completo.
* @param {string} [params.reason='Assinatura Digital ICP-Brasil'] Razão da assinatura.
* @param {string} [params.location='Brasil'] Localidade da assinatura.
* @param {string} [params.cpfDisplayMode] 'FULL', 'MASKED' ou 'NONE'.
* @param {boolean} [params.includeDateTime] Se deve exibir data/hora no gráfico.
* @param {string} [params.customExtraInfo] Texto extra como Cargo ou Matrícula.
* @param {number} [params.expiresInSeconds=180] Tempo de validade da requisição em segundos.
* @returns {Promise<Object>} Retorna o PDF assinado em Base64 e utilitários para download ou Blob.
*/
async signPdfDocument(params) {
if (!params) {
throw new CyberCoreError('Parâmetros de assinatura de PDF são obrigatórios.', 'BAD_REQUEST');
}
const certHint = params.certificateId || params.certificateHint;
if (!certHint) {
throw new CertificateNotFoundError('É necessário especificar o certificado digital para assinar o PDF.');
}
let pdfBase64;
if (typeof params.pdf === 'string') {
// Remove eventual prefixo data:application/pdf;base64,
pdfBase64 = params.pdf.includes(',') ? params.pdf.split(',')[1] : params.pdf;
} else if (params.pdf) {
const buffer = await this._fileToBuffer(params.pdf);
pdfBase64 = this._bufferToBase64(buffer);
}
if (!pdfBase64) {
throw new CyberCoreError('Nenhum documento PDF fornecido para assinatura.', 'BAD_REQUEST');
}
const requestId = this._generateUuid();
const nonce = this._generateUuid();
const expiresAt = new Date(Date.now() + (params.expiresInSeconds || 180) * 1000).toISOString();
const payload = {
version: '1',
request_id: requestId,
application_id: this.applicationId,
operation: 'sign_pdf',
pdf: pdfBase64,
certificate_hint: certHint,
document_name: params.documentName || params.fileName || (params.pdf?.name) || 'documento.pdf',
page: params.page !== undefined ? Number(params.page) : null,
page_choice: params.pageChoice || null,
position: params.position || ((params.x !== undefined && params.y !== undefined) ? 'CUSTOM' : 'BOTTOM_RIGHT'),
x: params.x !== undefined ? Number(params.x) : null,
y: params.y !== undefined ? Number(params.y) : null,
scale: params.scale !== undefined ? Number(params.scale) : 1.0,
visible_seal: params.visibleSeal !== undefined ? Boolean(params.visibleSeal) : true,
seal_theme_id: params.sealThemeId || params.seal_theme_id || params.sealTheme || null,
seal_custom: params.sealCustom || params.seal_custom || null,
reason: params.reason || 'Assinatura Digital ICP-Brasil',
location: params.location || 'Brasil',
cpf_display_mode: params.cpfDisplayMode || params.cpf_display_mode || null,
include_date_time: params.includeDateTime !== undefined ? Boolean(params.includeDateTime) : (params.include_date_time !== undefined ? Boolean(params.include_date_time) : null),
custom_extra_info: params.customExtraInfo || params.custom_extra_info || null,
nonce: nonce,
expires_at: expiresAt
};
try {
const resp = await this._fetch('/ccsp/v1/sign-pdf', {
method: 'POST',
body: JSON.stringify(payload)
});
if (resp.status === 'signed') {
const signedPdfBase64 = resp.signed_pdf;
const self = this;
return {
requestId: resp.request_id,
status: resp.status,
signedPdf: signedPdfBase64,
signed_pdf: signedPdfBase64,
sha256: resp.sha256,
signature: resp.signature,
certificate: resp.certificate,
certificateChain: resp.certificate_chain || [resp.certificate],
algorithm: resp.algorithm,
subjectName: resp.subject_name || null,
subjectCpfCnpj: resp.subject_cpf_cnpj || null,
documentName: resp.document_name || payload.document_name,
/**
* Converte o PDF assinado em Blob binário para upload via FormData ou visualização.
* @returns {Blob}
*/
getSignedPdfBlob() {
return self._base64ToBlob(signedPdfBase64, 'application/pdf');
},
/**
* Retorna os bytes puros do PDF assinado em Uint8Array.
* @returns {Uint8Array}
*/
getSignedPdfBytes() {
return self._base64ToBytes(signedPdfBase64);
},
/**
* Dispara o download automático do PDF assinado no navegador web.
* @param {string} [filename] Nome do arquivo para download.
*/
download(filename) {
if (typeof document === 'undefined') return;
const blob = self._base64ToBlob(signedPdfBase64, 'application/pdf');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || resp.document_name || 'documento_assinado.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
};
}
this._throwMappedError(resp.error_code, resp.message);
} catch (err) {
if (err instanceof CyberCoreError) throw err;
throw new CyberCoreError('Erro durante a assinatura do documento PDF: ' + err.message, 'SIGN_PDF_ERROR', err);
}
}
/**
* Assina um documento XML completo no padrão XAdES-BES (ICP-Brasil) ou W3C XMLDSig Enveloped.
* O XML pode ser fornecido como string de texto puro ou arquivo/bytes.
*
* @param {Object} params Parâmetros da assinatura XML.
* @param {string|File|Blob|ArrayBuffer} params.xml Conteúdo do XML a ser assinado.
* @param {string} params.certificateId Identificador do certificado digital.
* @param {string} [params.profile='XADES_BES'] Perfil de assinatura: 'XADES_BES' ou 'XMLDSIG'.
* @param {string} [params.targetElementTag] Nome da tag alvo (ex: 'infNFe', 'infCte') ou null para o documento todo.
* @param {string} [params.targetElementId] ID do elemento alvo (ex: 'NFe3526...').
* @param {string} [params.documentName='documento.xml'] Nome amigável do arquivo XML.
* @param {string} [params.algorithm] Algoritmo XMLDSig.
* @param {number} [params.expiresInSeconds=180] Tempo de validade da requisição em segundos.
* @returns {Promise<Object>} Retorna o XML assinado como string de texto e helpers para Blob.
*/
async signXmlDocument(params) {
if (!params) {
throw new CyberCoreError('Parâmetros de assinatura XML são obrigatórios.', 'BAD_REQUEST');
}
const certHint = params.certificateId || params.certificateHint;
if (!certHint) {
throw new CertificateNotFoundError('É necessário especificar o certificado digital para assinar o XML.');
}
let xmlText = null;
let xmlBase64 = null;
if (typeof params.xml === 'string') {
xmlText = params.xml;
} else if (params.xml) {
const buffer = await this._fileToBuffer(params.xml);
xmlBase64 = this._bufferToBase64(buffer);
}
if (!xmlText && !xmlBase64) {
throw new CyberCoreError('Nenhum documento XML fornecido para assinatura.', 'BAD_REQUEST');
}
const requestId = this._generateUuid();
const nonce = this._generateUuid();
const expiresAt = new Date(Date.now() + (params.expiresInSeconds || 180) * 1000).toISOString();
const payload = {
version: '1',
request_id: requestId,
application_id: this.applicationId,
operation: 'sign_xml',
xml: xmlText,
xml_base64: xmlBase64,
certificate_hint: certHint,
document_name: params.documentName || params.fileName || (params.xml?.name) || 'documento.xml',
profile: params.profile || 'XADES_BES',
target_element_tag: params.targetElementTag || null,
target_element_id: params.targetElementId || null,
algorithm: params.algorithm || null,
nonce: nonce,
expires_at: expiresAt
};
try {
const resp = await this._fetch('/ccsp/v1/sign-xml', {
method: 'POST',
body: JSON.stringify(payload)
});
if (resp.status === 'signed') {
const signedXmlText = resp.signed_xml;
const self = this;
return {
requestId: resp.request_id,
status: resp.status,
signedXml: signedXmlText,
sha256: resp.sha256,
signature: resp.signature,
certificate: resp.certificate,
certificateChain: resp.certificate_chain || [resp.certificate],
algorithm: resp.algorithm,
subjectName: resp.subject_name || null,
subjectCpfCnpj: resp.subject_cpf_cnpj || null,
documentName: resp.document_name || payload.document_name,
/**
* Converte o XML assinado em Blob binário para upload.
* @returns {Blob}
*/
getSignedXmlBlob() {
return new Blob([signedXmlText], { type: 'application/xml;charset=utf-8' });
},
/**
* Retorna os bytes UTF-8 do XML assinado em Uint8Array.
* @returns {Uint8Array}
*/
getSignedXmlBytes() {
return new TextEncoder().encode(signedXmlText);
},
/**
* Dispara o download automático do XML assinado no navegador web.
* @param {string} [filename]
*/
download(filename) {
if (typeof document === 'undefined') return;
const blob = new Blob([signedXmlText], { type: 'application/xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || resp.document_name || 'documento_assinado.xml';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
};
}
this._throwMappedError(resp.error_code, resp.message);
} catch (err) {
if (err instanceof CyberCoreError) throw err;
throw new CyberCoreError('Erro durante a assinatura do documento XML: ' + err.message, 'SIGN_XML_ERROR', err);
}
}
/** /**
* Realiza login / autenticação segura em sistemas web utilizando Certificado Digital (Challenge-Response ICP-Brasil). * Realiza login / autenticação segura em sistemas web utilizando Certificado Digital (Challenge-Response ICP-Brasil).
* Assina o desafio (challenge) emitido pelo servidor e retorna o certificado e dados decodificados do titular. * Assina o desafio (challenge) emitido pelo servidor e retorna o certificado e dados decodificados do titular.
@@ -407,8 +692,71 @@ export class CyberCoreSigner {
// Métodos Utilitários Internos // Métodos Utilitários Internos
// ========================================================================= // =========================================================================
/**
* Executa requisições HTTP REST ao servidor local CCSP/1.
*
* @param {string} endpoint Caminho do endpoint (ex: '/ccsp/v1/status').
* @param {Object} [options={}] Opções do fetch (method, body, timeoutMs, etc).
* @returns {Promise<Object>} Resposta JSON decodificada.
*/
async _fetch(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const timeout = options.timeoutMs || 15000;
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timeoutId = controller ? setTimeout(() => controller.abort(), timeout) : null;
try {
const headers = {
'Accept': 'application/json',
'X-CyberCore-AppId': this.applicationId,
...(options.headers || {})
};
if (options.body && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
}
const fetchOptions = {
...options,
headers
};
if (controller) {
fetchOptions.signal = controller.signal;
}
const res = await fetch(url, fetchOptions);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const errorCode = data.error_code || `HTTP_${res.status}`;
const message = data.message || `Erro retornado pelo agente (${res.status})`;
this._throwMappedError(errorCode, message);
}
return data;
} catch (err) {
if (err.name === 'AbortError') {
throw new CyberCoreError(`Tempo limite esgotado (${timeout}ms) na comunicação com o agente.`, 'TIMEOUT_ERROR');
}
if (err instanceof CyberCoreError) {
throw err;
}
throw new AgentNotRunningError(`Falha ao conectar com o CyberCore Signer em ${this.baseUrl}: ${err.message}`);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
async _fileToBuffer(file) { async _fileToBuffer(file) {
if (file instanceof ArrayBuffer) return file; if (file instanceof ArrayBuffer) {
if (file.byteLength === 0) {
throw new Error('O buffer binário está vazio ou foi desacoplado (detached). Forneça o objeto File ou Blob original.');
}
return file;
}
if (ArrayBuffer.isView(file)) { if (ArrayBuffer.isView(file)) {
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
} }
@@ -421,6 +769,38 @@ export class CyberCoreSigner {
throw new Error('Tipo de arquivo não suportado para leitura binária.'); throw new Error('Tipo de arquivo não suportado para leitura binária.');
} }
_bufferToBase64(buffer) {
if (typeof Buffer !== 'undefined') {
return Buffer.from(buffer).toString('base64');
}
const bytes = new Uint8Array(buffer);
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
_base64ToBytes(base64) {
const clean = base64.includes(',') ? base64.split(',')[1] : base64;
if (typeof Buffer !== 'undefined') {
return new Uint8Array(Buffer.from(clean, 'base64'));
}
const binary = atob(clean);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
_base64ToBlob(base64, mimeType = 'application/octet-stream') {
const bytes = this._base64ToBytes(base64);
return new Blob([bytes], { type: mimeType });
}
async _calculateSha256(data) { async _calculateSha256(data) {
let buffer; let buffer;
if (typeof data === 'string') { if (typeof data === 'string') {
@@ -492,6 +872,8 @@ export class CyberCoreSigner {
// Expõe globalmente caso seja carregado diretamente via tag <script> // Expõe globalmente caso seja carregado diretamente via tag <script>
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.CyberCoreSigner = CyberCoreSigner; window.CyberCoreSigner = CyberCoreSigner;
window.PdfStampPicker = PdfStampPicker;
window.PdfSealDesigner = PdfSealDesigner;
} }
export default CyberCoreSigner; export default CyberCoreSigner;
+127
View File
@@ -250,4 +250,131 @@ describe('@cybercore/signer-sdk - Testes Unitários do SDK', () => {
assert.equal(res.subjectCpfCnpj, '98765432100'); assert.equal(res.subjectCpfCnpj, '98765432100');
assert.equal(capturedPayload.purpose, 'AUTHENTICATION'); assert.equal(capturedPayload.purpose, 'AUTHENTICATION');
}); });
it('deve consultar lista de temas de selos com getSealThemes()', async () => {
const signer = new CyberCoreSigner();
signer._fetch = async (endpoint, options) => {
assert.equal(endpoint, '/ccsp/v1/seals');
return {
version: '1',
status: 'success',
default_seal_id: 'icp-brasil',
seals: [
{ id: 'icp-brasil', name: 'ICP-Brasil Oficial' },
{ id: 'gov-br', name: 'Gov.br Oficial' }
]
};
};
const res = await signer.getSealThemes();
assert.equal(res.status, 'success');
assert.equal(res.default_seal_id, 'icp-brasil');
assert.equal(res.seals.length, 2);
});
it('deve assinar documento PDF completo em Base64 com signPdfDocument()', async () => {
const signer = new CyberCoreSigner();
let capturedPayload = null;
let calledEndpoint = null;
const fakeSignedPdfB64 = Buffer.from('%PDF-1.4 Fake Signed PDF Content').toString('base64');
signer._fetch = async (endpoint, options) => {
calledEndpoint = endpoint;
capturedPayload = JSON.parse(options.body);
return {
version: '1',
request_id: capturedPayload.request_id,
status: 'signed',
signed_pdf: fakeSignedPdfB64,
sha256: 'FAKESHA256HASH12345',
certificate: 'FAKECERTBASE64',
certificate_chain: ['FAKECERTBASE64'],
algorithm: 'SHA256withRSA',
subject_name: 'MARIA DA SILVA',
subject_cpf_cnpj: '12345678900',
document_name: capturedPayload.document_name
};
};
const rawPdfBuffer = Buffer.from('%PDF-1.4 Document Original');
const res = await signer.signPdfDocument({
pdf: rawPdfBuffer,
certificateId: 'cert-a3-win',
documentName: 'contrato.pdf',
page: 2,
x: 350.5,
y: 50.0,
scale: 1.1,
sealThemeId: 'gov-br',
customExtraInfo: 'Matrícula: 1048'
});
assert.equal(calledEndpoint, '/ccsp/v1/sign-pdf');
assert.equal(res.status, 'signed');
assert.equal(res.signedPdf, fakeSignedPdfB64);
assert.equal(res.sha256, 'FAKESHA256HASH12345');
assert.equal(res.subjectName, 'MARIA DA SILVA');
assert.equal(res.subjectCpfCnpj, '12345678900');
assert.equal(capturedPayload.page, 2);
assert.equal(capturedPayload.x, 350.5);
assert.equal(capturedPayload.y, 50.0);
assert.equal(capturedPayload.scale, 1.1);
assert.equal(capturedPayload.seal_theme_id, 'gov-br');
assert.equal(capturedPayload.custom_extra_info, 'Matrícula: 1048');
// Valida método helper de bytes
const signedBytes = res.getSignedPdfBytes();
assert.ok(signedBytes instanceof Uint8Array);
assert.ok(signedBytes.length > 0);
});
it('deve assinar documento XML completo com signXmlDocument()', async () => {
const signer = new CyberCoreSigner();
let capturedPayload = null;
let calledEndpoint = null;
const fakeSignedXml = '<nfeProc><NFe>...</NFe><ds:Signature>...</ds:Signature></nfeProc>';
signer._fetch = async (endpoint, options) => {
calledEndpoint = endpoint;
capturedPayload = JSON.parse(options.body);
return {
version: '1',
request_id: capturedPayload.request_id,
status: 'signed',
signed_xml: fakeSignedXml,
sha256: 'XMLSHA256HASH',
certificate: 'FAKECERTBASE64',
certificate_chain: ['FAKECERTBASE64'],
algorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
subject_name: 'CYBERCORE LTDA',
subject_cpf_cnpj: '12345678000199',
document_name: capturedPayload.document_name
};
};
const xmlInput = '<nfeProc><NFe Id="NFe123">Dados</NFe></nfeProc>';
const res = await signer.signXmlDocument({
xml: xmlInput,
certificateId: 'cert-a1-pj',
documentName: 'nfe-123.xml',
profile: 'XADES_BES',
targetElementTag: 'NFe',
targetElementId: 'NFe123'
});
assert.equal(calledEndpoint, '/ccsp/v1/sign-xml');
assert.equal(res.status, 'signed');
assert.equal(res.signedXml, fakeSignedXml);
assert.equal(res.subjectName, 'CYBERCORE LTDA');
assert.equal(capturedPayload.profile, 'XADES_BES');
assert.equal(capturedPayload.target_element_tag, 'NFe');
assert.equal(capturedPayload.target_element_id, 'NFe123');
// Helper de bytes
const xmlBytes = res.getSignedXmlBytes();
assert.ok(xmlBytes instanceof Uint8Array);
});
}); });